diff --git a/packages/xo-web/.editorconfig b/packages/xo-web/.editorconfig
new file mode 100644
index 000000000..da21ef4c5
--- /dev/null
+++ b/packages/xo-web/.editorconfig
@@ -0,0 +1,65 @@
+# http://EditorConfig.org
+#
+# Julien Fontanet's configuration
+# https://gist.github.com/julien-f/8096213
+
+# Top-most EditorConfig file.
+root = true
+
+# Common config.
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespaces = true
+
+# CoffeeScript
+#
+# https://github.com/polarmobile/coffeescript-style-guide/blob/master/README.md
+[*.{,lit}coffee]
+indent_size = 2
+indent_style = space
+
+# Markdown
+[*.{md,mdwn,mdown,markdown}]
+indent_size = 4
+indent_style = space
+
+# Package.json
+#
+# This indentation style is the one used by npm.
+[/package.json]
+indent_size = 2
+indent_style = space
+
+# Jade
+[*.jade]
+indent_size = 2
+indent_style = space
+
+# JavaScript
+#
+# Two spaces seems to be the standard most common style, at least in
+# Node.js (http://nodeguide.com/style.html#tabs-vs-spaces).
+[*.js]
+indent_size = 2
+indent_style = space
+
+# Less
+[*.less]
+indent_size = 2
+indent_style = space
+
+# Sass
+#
+# Style used for http://libsass.com
+[*.s[ac]ss]
+indent_size = 2
+indent_style = space
+
+# YAML
+#
+# Only spaces are allowed.
+[*.yaml]
+indent_size = 2
+indent_style = space
diff --git a/packages/xo-web/.eslintrc.js b/packages/xo-web/.eslintrc.js
new file mode 100644
index 000000000..6a08f9d0a
--- /dev/null
+++ b/packages/xo-web/.eslintrc.js
@@ -0,0 +1,12 @@
+module.exports = {
+ extends: ['standard', 'standard-jsx'],
+ globals: {
+ __DEV__: true,
+ },
+ parser: 'babel-eslint',
+ rules: {
+ 'comma-dangle': ['error', 'always-multiline'],
+ 'no-var': 'error',
+ 'prefer-const': 'error',
+ },
+}
diff --git a/packages/xo-web/.gitignore b/packages/xo-web/.gitignore
new file mode 100644
index 000000000..062102753
--- /dev/null
+++ b/packages/xo-web/.gitignore
@@ -0,0 +1,9 @@
+/dist/
+/node_modules/
+/src/common/intl/locales/index.js
+/src/common/themes/index.js
+
+npm-debug.log
+npm-debug.log.*
+pnpm-debug.log
+pnpm-debug.log.*
diff --git a/packages/xo-web/.npmignore b/packages/xo-web/.npmignore
new file mode 100644
index 000000000..c31ee82cb
--- /dev/null
+++ b/packages/xo-web/.npmignore
@@ -0,0 +1,10 @@
+/examples/
+example.js
+example.js.map
+*.example.js
+*.example.js.map
+
+/test/
+/tests/
+*.spec.js
+*.spec.js.map
diff --git a/packages/xo-web/.prettierrc.js b/packages/xo-web/.prettierrc.js
new file mode 100644
index 000000000..35fcafaf2
--- /dev/null
+++ b/packages/xo-web/.prettierrc.js
@@ -0,0 +1,4 @@
+module.exports = {
+ semi: false,
+ singleQuote: true,
+}
diff --git a/packages/xo-web/.travis.yml b/packages/xo-web/.travis.yml
new file mode 100644
index 000000000..04ee3a2c1
--- /dev/null
+++ b/packages/xo-web/.travis.yml
@@ -0,0 +1,11 @@
+language: node_js
+node_js:
+ - '6'
+ #- '4' # npm 3's flat tree is needed because some packages do not
+ # declare their deps correctly (e.g. chartist-plugin-tooltip)
+
+cache: yarn
+
+# Use containers.
+# http://docs.travis-ci.com/user/workers/container-based-infrastructure/
+sudo: false
diff --git a/packages/xo-web/CHANGELOG.md b/packages/xo-web/CHANGELOG.md
new file mode 100644
index 000000000..d701d4748
--- /dev/null
+++ b/packages/xo-web/CHANGELOG.md
@@ -0,0 +1,1420 @@
+# ChangeLog
+
+## **5.15.0** (2017-12-29)
+
+### Enhancements
+
+ * VDI resize online method removed in 7.3 [#2542](https://github.com/vatesfr/xo-web/issues/2542)
+ * Smart replace VDI.pool_migrate removed from XenServer 7.3 Free [#2541](https://github.com/vatesfr/xo-web/issues/2541)
+ * New memory constraints in XenServer 7.3 [#2540](https://github.com/vatesfr/xo-web/issues/2540)
+ * Link to Settings/Logs for admins in error notifications [#2516](https://github.com/vatesfr/xo-web/issues/2516)
+ * [Self Service] Do not use placehodlers to describe inputs [#2509](https://github.com/vatesfr/xo-web/issues/2509)
+ * Obfuscate password in log in LDAP plugin test [#2506](https://github.com/vatesfr/xo-web/issues/2506)
+ * Log rotation [#2492](https://github.com/vatesfr/xo-web/issues/2492)
+ * Continuous Replication TAG [#2473](https://github.com/vatesfr/xo-web/issues/2473)
+ * Graphs in VM list view [#2469](https://github.com/vatesfr/xo-web/issues/2469)
+ * [Delta Backups] Do not include merge duration in transfer speed stat [#2426](https://github.com/vatesfr/xo-web/issues/2426)
+ * Warning for disperse mode [#2537](https://github.com/vatesfr/xo-web/issues/2537)
+
+### Bugs
+
+ * VM console doesn't work when using IPv6 in URL [#2530](https://github.com/vatesfr/xo-web/issues/2530)
+ * Retention issue with failed basic backup [#2524](https://github.com/vatesfr/xo-web/issues/2524)
+ * [VM/Advanced] Check that the autopower on setting is working [#2489](https://github.com/vatesfr/xo-web/issues/2489)
+ * Cloud config drive create fail on XenServer < 7 [#2478](https://github.com/vatesfr/xo-web/issues/2478)
+ * VM create fails due to missing vGPU id [#2466](https://github.com/vatesfr/xo-web/issues/2466)
+
+
+## **5.14.0** (2017-10-31)
+
+### Enhancements
+
+ * VM snapshot description display [#2458](https://github.com/vatesfr/xo-web/issues/2458)
+ * [Home] Ability to sort VM by number of snapshots [#2450](https://github.com/vatesfr/xo-web/issues/2450)
+ * Display XS version in host view [#2439](https://github.com/vatesfr/xo-web/issues/2439)
+ * [File restore]: Clarify the possibility to select multiple files [#2438](https://github.com/vatesfr/xo-web/issues/2438)
+ * [Continuous Replication] Time in replicated VMs [#2431](https://github.com/vatesfr/xo-web/issues/2431)
+ * [SortedTable] Active page in URL param [#2405](https://github.com/vatesfr/xo-web/issues/2405)
+ * replace all '...' with the UTF-8 equivalent [#2391](https://github.com/vatesfr/xo-web/issues/2391)
+ * [SortedTable] Explicit when no items [#2388](https://github.com/vatesfr/xo-web/issues/2388)
+ * Handle patching licenses [#2382](https://github.com/vatesfr/xo-web/issues/2382)
+ * Credential leaking in logs for messages regarding invalid credentials and "too fast authentication" [#2363](https://github.com/vatesfr/xo-web/issues/2363)
+ * [SortedTable] Keyboard support [#2330](https://github.com/vatesfr/xo-web/issues/2330)
+ * token.create should accept an expiration [#1769](https://github.com/vatesfr/xo-web/issues/1769)
+ * On updater error, display link to documentation [#1610](https://github.com/vatesfr/xo-web/issues/1610)
+* Add basic vGPU support [#2413](https://github.com/vatesfr/xo-web/issues/2413)
+ * Storage View - Disk Tab - real disk usage [#2475](https://github.com/vatesfr/xo-web/issues/2475)
+
+### Bugs
+
+ * Config drive - Custom config not working properly [#2449](https://github.com/vatesfr/xo-web/issues/2449)
+ * Snapshot sorted table breaks copyVm [#2446](https://github.com/vatesfr/xo-web/issues/2446)
+ * [vm/snapshots] Incorrect default sort order [#2442](https://github.com/vatesfr/xo-web/issues/2442)
+ * [Backups/Jobs] Incorrect months mapping [#2427](https://github.com/vatesfr/xo-web/issues/2427)
+ * [Xapi#barrier()] Not compatible with XenServer < 6.1 [#2418](https://github.com/vatesfr/xo-web/issues/2418)
+ * [SortedTable] Change page when no more items on the page [#2401](https://github.com/vatesfr/xo-web/issues/2401)
+ * Review and fix creating a VM from a snapshot [#2343](https://github.com/vatesfr/xo-web/issues/2343)
+ * Unable to edit / save restored backup job [#1922](https://github.com/vatesfr/xo-web/issues/1922)
+
+
+## **5.13.0** (2017-09-29)
+
+### Enhancements
+
+ * replace all '...' with the UTF-8 equivalent [#2391](https://github.com/vatesfr/xo-web/issues/2391)
+ * [SortedTable] Explicit when no items [#2388](https://github.com/vatesfr/xo-web/issues/2388)
+ * Auto select iqn or lun if there is only one [#2379](https://github.com/vatesfr/xo-web/issues/2379)
+ * [Sparklines] Hide points [#2370](https://github.com/vatesfr/xo-web/issues/2370)
+ * Allow xo-server-recover-account to generate a random password [#2360](https://github.com/vatesfr/xo-web/issues/2360)
+ * Add disk in existing VM as self user [#2348](https://github.com/vatesfr/xo-web/issues/2348)
+ * Sorted table for Settings/server [#2340](https://github.com/vatesfr/xo-web/issues/2340)
+ * Sign in should be case insensitive [#2337](https://github.com/vatesfr/xo-web/issues/2337)
+ * [SortedTable] Extend checkbox click to whole column [#2329](https://github.com/vatesfr/xo-web/issues/2329)
+ * [SortedTable] Ability to select all items (across pages) [#2324](https://github.com/vatesfr/xo-web/issues/2324)
+ * [SortedTable] Range selection [#2323](https://github.com/vatesfr/xo-web/issues/2323)
+ * Warning on SMB remote creation [#2316](https://github.com/vatesfr/xo-web/issues/2316)
+ * [Home | SortedTable] Add link to syntax doc in the filter input [#2305](https://github.com/vatesfr/xo-web/issues/2305)
+ * [SortedTable] Add optional binding of filter to an URL query [#2301](https://github.com/vatesfr/xo-web/issues/2301)
+ * [Home][Keyboard navigation] Allow selecting the objects [#2214](https://github.com/vatesfr/xo-web/issues/2214)
+ * SR view / Disks: option to display non managed VDIs [#1724](https://github.com/vatesfr/xo-web/issues/1724)
+ * Continuous Replication Retention [#1692](https://github.com/vatesfr/xo-web/issues/1692)
+
+### Bugs
+
+ * iSCSI issue on LUN selector [#2374](https://github.com/vatesfr/xo-web/issues/2374)
+ * Errors in VM copy are not properly reported [#2347](https://github.com/vatesfr/xo-web/issues/2347)
+ * Removing a PIF IP fails [#2346](https://github.com/vatesfr/xo-web/issues/2346)
+ * Review and fix creating a VM from a snapshot [#2343](https://github.com/vatesfr/xo-web/issues/2343)
+ * iSCSI LUN Detection fails with authentification [#2339](https://github.com/vatesfr/xo-web/issues/2339)
+ * Fix PoolActionBar to add a new SR [#2307](https://github.com/vatesfr/xo-web/issues/2307)
+ * [VM migration] Error if default SR not accessible to target host [#2180](https://github.com/vatesfr/xo-web/issues/2180)
+ * A job shouldn't executable more than once at the same time [#2053](https://github.com/vatesfr/xo-web/issues/2053)
+
+## **5.12.0** (2017-08-31)
+
+### Enhancements
+
+ * PIF selector with physical status [#2326](https://github.com/vatesfr/xo-web/issues/2326)
+ * [SortedTable] Range selection [#2323](https://github.com/vatesfr/xo-web/issues/2323)
+ * Self service filter for home/VM view [#2303](https://github.com/vatesfr/xo-web/issues/2303)
+ * SR/Disks Display total of VDIs to coalesce [#2300](https://github.com/vatesfr/xo-web/issues/2300)
+ * Pool filter in the task view [#2293](https://github.com/vatesfr/xo-web/issues/2293)
+ * "Loading" while fetching objects [#2285](https://github.com/vatesfr/xo-web/issues/2285)
+ * [SortedTable] Add grouped actions feature [#2276](https://github.com/vatesfr/xo-web/issues/2276)
+ * Add a filter to the backups' log [#2246](https://github.com/vatesfr/xo-web/issues/2246)
+ * It should not be possible to migrate a halted VM. [#2233](https://github.com/vatesfr/xo-web/issues/2233)
+ * [Home][Keyboard navigation] Allow selecting the objects [#2214](https://github.com/vatesfr/xo-web/issues/2214)
+ * Allow to set pool master [#2213](https://github.com/vatesfr/xo-web/issues/2213)
+ * Continuous Replication Retention [#1692](https://github.com/vatesfr/xo-web/issues/1692)
+
+### Bugs
+
+ * Home pagination bug [#2310](https://github.com/vatesfr/xo-web/issues/2310)
+ * Fix PoolActionBar to add a new SR [#2307](https://github.com/vatesfr/xo-web/issues/2307)
+ * VM snapshots are not correctly deleted [#2304](https://github.com/vatesfr/xo-web/issues/2304)
+ * Parallel deletion of VMs fails [#2297](https://github.com/vatesfr/xo-web/issues/2297)
+ * Continous replication create multiple zombie disks [#2292](https://github.com/vatesfr/xo-web/issues/2292)
+ * Add user to Group issue [#2196](https://github.com/vatesfr/xo-web/issues/2196)
+ * [VM migration] Error if default SR not accessible to target host [#2180](https://github.com/vatesfr/xo-web/issues/2180)
+
+## **5.11.0** (2017-07-31)
+
+### Enhancements
+
+- Storage VHD chain health [\#2178](https://github.com/vatesfr/xo-web/issues/2178)
+
+### Bug fixes
+
+- No web VNC console [\#2258](https://github.com/vatesfr/xo-web/issues/2258)
+- Patching issues [\#2254](https://github.com/vatesfr/xo-web/issues/2254)
+- Advanced button in VM creation for self service user [\#2202](https://github.com/vatesfr/xo-web/issues/2202)
+- Hide "new VM" menu entry if not admin or not self service user [\#2191](https://github.com/vatesfr/xo-web/issues/2191)
+
+## **5.10.0** (2017-06-30)
+
+### Enhancements
+
+- Improve backup log display [\#2239](https://github.com/vatesfr/xo-web/issues/2239)
+- Patch SR detection improvement [\#2215](https://github.com/vatesfr/xo-web/issues/2215)
+- Less strict coalesce detection [\#2207](https://github.com/vatesfr/xo-web/issues/2207)
+- IP pool UI improvement [\#2203](https://github.com/vatesfr/xo-web/issues/2203)
+- Ability to clear "Auto power on" flag for DR-ed VM [\#2097](https://github.com/vatesfr/xo-web/issues/2097)
+- [Delta backup restoration] Choose SR for each VDIs [\#2070](https://github.com/vatesfr/xo-web/issues/2070)
+- Ability to forget an host (even if no longer present) [\#1934](https://github.com/vatesfr/xo-web/issues/1934)
+
+### Bug fixes
+
+- Cross pool migrate fail [\#2248](https://github.com/vatesfr/xo-web/issues/2248)
+- ActionButtons with modals stay in pending state forever [\#2222](https://github.com/vatesfr/xo-web/issues/2222)
+- Permission issue for a user on self service VMs [\#2212](https://github.com/vatesfr/xo-web/issues/2212)
+- Self-Service resource loophole [\#2198](https://github.com/vatesfr/xo-web/issues/2198)
+- Backup log no longer shows the name of destination VM [\#2195](https://github.com/vatesfr/xo-web/issues/2195)
+- State not restored when exiting modal dialog [\#2194](https://github.com/vatesfr/xo-web/issues/2194)
+- [Xapi#exportDeltaVm] Cannot read property 'managed' of undefined [\#2189](https://github.com/vatesfr/xo-web/issues/2189)
+- VNC keyboard layout change [\#404](https://github.com/vatesfr/xo-web/issues/404)
+
+## **5.9.0** (2017-05-31)
+
+### Enhancements
+
+- Allow DR to remove previous backup first [\#2157](https://github.com/vatesfr/xo-web/issues/2157)
+- Feature request - add amount of RAM to memory bars [\#2149](https://github.com/vatesfr/xo-web/issues/2149)
+- Make the acceptability of invalid certificates configurable [\#2138](https://github.com/vatesfr/xo-web/issues/2138)
+- label of VM names in tasks link [\#2135](https://github.com/vatesfr/xo-web/issues/2135)
+- Backup report timezone [\#2133](https://github.com/vatesfr/xo-web/issues/2133)
+- xo-server-recover-account [\#2129](https://github.com/vatesfr/xo-web/issues/2129)
+- Detect disks attached to control domain [\#2126](https://github.com/vatesfr/xo-web/issues/2126)
+- Add task description in Tasks view [\#2125](https://github.com/vatesfr/xo-web/issues/2125)
+- Host reboot warning after patching for 7.1 [\#2124](https://github.com/vatesfr/xo-web/issues/2124)
+- Continuous Replication - possibility run VM without a clone [\#2119](https://github.com/vatesfr/xo-web/issues/2119)
+- Unreachable host should be detected [\#2099](https://github.com/vatesfr/xo-web/issues/2099)
+- Orange icon when host is is disabled [\#2098](https://github.com/vatesfr/xo-web/issues/2098)
+- Enhanced backup report logs [\#2096](https://github.com/vatesfr/xo-web/issues/2096)
+- Only show failures when configured to report on failures [\#2095](https://github.com/vatesfr/xo-web/issues/2095)
+- "Add all" button in self service [\#2081](https://github.com/vatesfr/xo-web/issues/2081)
+- Patch and pack mechanism changed on Ely [\#2058](https://github.com/vatesfr/xo-web/issues/2058)
+- Tip or ask people to patch from pool view [\#2057](https://github.com/vatesfr/xo-web/issues/2057)
+- File restore - Remind compatible backup [\#1930](https://github.com/vatesfr/xo-web/issues/1930)
+- Reporting for halted vm time [\#1613](https://github.com/vatesfr/xo-web/issues/1613)
+- Add standalone XS server to a pool and patch it to the pool level [\#878](https://github.com/vatesfr/xo-web/issues/878)
+- Add Cores-per-sockets [\#130](https://github.com/vatesfr/xo-web/issues/130)
+
+### Bug fixes
+
+- VM creation is broken for non-admins [\#2168](https://github.com/vatesfr/xo-web/issues/2168)
+- Can't create cloud config drive [\#2162](https://github.com/vatesfr/xo-web/issues/2162)
+- Select is "moving" [\#2142](https://github.com/vatesfr/xo-web/issues/2142)
+- Select issue for affinity host [\#2141](https://github.com/vatesfr/xo-web/issues/2141)
+- Dashboard Storage Usage incorrect [\#2123](https://github.com/vatesfr/xo-web/issues/2123)
+- Detect unmerged *base copy* and prevent too long chains [\#2047](https://github.com/vatesfr/xo-web/issues/2047)
+
+
+## **5.8.0** (2017-04-28)
+
+### Enhancements
+
+- Limit About view info for non-admins [\#2109](https://github.com/vatesfr/xo-web/issues/2109)
+- Enabling/disabling boot device on HVM VM [\#2105](https://github.com/vatesfr/xo-web/issues/2105)
+- Filter: Hide snapshots in SR disk view [\#2102](https://github.com/vatesfr/xo-web/issues/2102)
+- Smarter XOSAN install [\#2084](https://github.com/vatesfr/xo-web/issues/2084)
+- PL translation [\#2079](https://github.com/vatesfr/xo-web/issues/2079)
+- Remove the "share this VM" option if not in self service [\#2061](https://github.com/vatesfr/xo-web/issues/2061)
+- "connected" status graphics are not the same on the host storage and networking tabs [\#2060](https://github.com/vatesfr/xo-web/issues/2060)
+- Ability to view and edit `vga` and `videoram` fields in VM view [\#158](https://github.com/vatesfr/xo-web/issues/158)
+- Performances [\#1](https://github.com/vatesfr/xen-api/issues/1)
+
+### Bug fixes
+
+- Dashboard display issues [\#2108](https://github.com/vatesfr/xo-web/issues/2108)
+- Dashboard CPUs Usage [\#2105](https://github.com/vatesfr/xo-web/issues/2105)
+- [Dashboard/Overview] Warning [\#2090](https://github.com/vatesfr/xo-web/issues/2090)
+- VM creation displays all networks [\#2086](https://github.com/vatesfr/xo-web/issues/2086)
+- Cannot change HA mode for a VM [\#2080](https://github.com/vatesfr/xo-web/issues/2080)
+- [Smart backup] Tags selection does not work [\#2077](https://github.com/vatesfr/xo-web/issues/2077)
+- [Backup jobs] Timeout should be in seconds, not milliseconds [\#2076](https://github.com/vatesfr/xo-web/issues/2076)
+- Missing VM templates [\#2075](https://github.com/vatesfr/xo-web/issues/2075)
+- [transport-email] From header not set [\#2074](https://github.com/vatesfr/xo-web/issues/2074)
+- Missing objects should be displayed in backup edition [\#2052](https://github.com/vatesfr/xo-web/issues/2052)
+
+## **5.7.0** (2017-03-31)
+
+### Enhancements
+
+- Improve ActionButton error reporting [\#2048](https://github.com/vatesfr/xo-web/issues/2048)
+- Home view master checkbox UI issue [\#2027](https://github.com/vatesfr/xo-web/issues/2027)
+- HU Translation [\#2019](https://github.com/vatesfr/xo-web/issues/2019)
+- [Usage report] Add name for all objects [\#2017](https://github.com/vatesfr/xo-web/issues/2017)
+- [Home] Improve inter-types linkage [\#2012](https://github.com/vatesfr/xo-web/issues/2012)
+- Remove bootable checkboxes in VM creation [\#2007](https://github.com/vatesfr/xo-web/issues/2007)
+- Do not display bootable toggles for disks of non-PV VMs [\#1996](https://github.com/vatesfr/xo-web/issues/1996)
+- Try to match network VLAN for VM migration modal [\#1990](https://github.com/vatesfr/xo-web/issues/1990)
+- [Usage reports] Add VM names in addition to UUIDs [\#1984](https://github.com/vatesfr/xo-web/issues/1984)
+- Host affinity in "advanced" VM creation [\#1983](https://github.com/vatesfr/xo-web/issues/1983)
+- Add job tag in backup logs [\#1982](https://github.com/vatesfr/xo-web/issues/1982)
+- Possibility to add a label/description to servers [\#1965](https://github.com/vatesfr/xo-web/issues/1965)
+- Possibility to create shared VM in a resource set [\#1964](https://github.com/vatesfr/xo-web/issues/1964)
+- Clearer display of disabled (backup) jobs [\#1958](https://github.com/vatesfr/xo-web/issues/1958)
+- Job should have a configurable timeout [\#1956](https://github.com/vatesfr/xo-web/issues/1956)
+- Sort failed VMs in backup report [\#1950](https://github.com/vatesfr/xo-web/issues/1950)
+- Support for UNIX socket path [\#1944](https://github.com/vatesfr/xo-web/issues/1944)
+- Interface - Host Patching - Button Verbiage [\#1911](https://github.com/vatesfr/xo-web/issues/1911)
+- Display if a VM is in Self Service (and which group) [\#1905](https://github.com/vatesfr/xo-web/issues/1905)
+- Install supplemental pack on a whole pool [\#1896](https://github.com/vatesfr/xo-web/issues/1896)
+- Allow VM snapshots with ACLs [\#1865](https://github.com/vatesfr/xo-web/issues/1886)
+- Icon to indicate if a snapshot is quiesce [\#1858](https://github.com/vatesfr/xo-web/issues/1858)
+- Pool Ips input too permissive [\#1731](https://github.com/vatesfr/xo-web/issues/1731)
+- Select is going on top after each choice [\#1359](https://github.com/vatesfr/xo-web/issues/1359)
+
+### Bug fixes
+
+- Missing objects should be displayed in backup edition [\#2052](https://github.com/vatesfr/xo-web/issues/2052)
+- Search bar content changes while typing [\#2035](https://github.com/vatesfr/xo-web/issues/2035)
+- VM.$guest_metrics.PV_drivers_up_to_date is deprecated in XS 7.1 [\#2024](https://github.com/vatesfr/xo-web/issues/2024)
+- Bootable flag selection checkbox for extra disk not fetched [\#1994](https://github.com/vatesfr/xo-web/issues/1994)
+- Home view − Changing type must reset paging [\#1993](https://github.com/vatesfr/xo-web/issues/1993)
+- XOSAN menu item should only be displayed to admins [\#1968](https://github.com/vatesfr/xo-web/issues/1968)
+- Object type change are not correctly handled in UI [\#1967](https://github.com/vatesfr/xo-web/issues/1967)
+- VM creation is stuck when using ISO/DVD as install method [\#1966](https://github.com/vatesfr/xo-web/issues/1966)
+- Install pack on whole pool fails [\#1957](https://github.com/vatesfr/xo-web/issues/1957)
+- Consoles are broken in next-release [\#1954](https://github.com/vatesfr/xo-web/issues/1954)
+- [VHD merge] Increase BAT when necessary [\#1939](https://github.com/vatesfr/xo-web/issues/1939)
+- Issue on VM restore time [\#1936](https://github.com/vatesfr/xo-web/issues/1936)
+- Two remotes should not be able to have the same name [\#1879](https://github.com/vatesfr/xo-web/issues/1879)
+- Selfservice limits not honored after VM creation [\#1695](https://github.com/vatesfr/xo-web/issues/1695)
+
+## **5.6.0** (2017-01-27)
+
+Reporting, LVM File level restore.
+
+### Enhancements
+
+- Do not stop patches install if already applied [\#1904](https://github.com/vatesfr/xo-web/issues/1904)
+- Improve scheduling UI [\#1893](https://github.com/vatesfr/xo-web/issues/1893)
+- Smart backup and tag [\#1885](https://github.com/vatesfr/xo-web/issues/1885)
+- Missing embeded API documention [\#1882](https://github.com/vatesfr/xo-web/issues/1882)
+- Add local DVD in CD selector [\#1880](https://github.com/vatesfr/xo-web/issues/1880)
+- File level restore for LVM [\#1878](https://github.com/vatesfr/xo-web/issues/1878)
+- Restore multiple files from file level restore [\#1877](https://github.com/vatesfr/xo-web/issues/1877)
+- Add a VM tab for host & pool views [\#1864](https://github.com/vatesfr/xo-web/issues/1864)
+- Icon to indicate if a snapshot is quiesce [\#1858](https://github.com/vatesfr/xo-web/issues/1858)
+- UI for disconnect hosts comp [\#1833](https://github.com/vatesfr/xo-web/issues/1833)
+- Eject all xs-guest.iso in a pool [\#1798](https://github.com/vatesfr/xo-web/issues/1798)
+- Display installed supplemental pack on host [\#1506](https://github.com/vatesfr/xo-web/issues/1506)
+- Install supplemental pack on host comp [\#1460](https://github.com/vatesfr/xo-web/issues/1460)
+- Pool-wide combined stats [\#1324](https://github.com/vatesfr/xo-web/issues/1324)
+
+### Bug fixes
+
+- IP-address not released when VM removed [\#1906](https://github.com/vatesfr/xo-web/issues/1906)
+- Interface broken due to new Bootstrap Alpha [\#1871](https://github.com/vatesfr/xo-web/issues/1871)
+- Self service recompute all limits broken [\#1866](https://github.com/vatesfr/xo-web/issues/1866)
+- Patch not found error for XS 6.5 [\#1863](https://github.com/vatesfr/xo-web/issues/1863)
+- Convert To Template issues [\#1855](https://github.com/vatesfr/xo-web/issues/1855)
+- Removing PIF seems to fail [\#1853](https://github.com/vatesfr/xo-web/issues/1853)
+- Depth should be >= 1 in backup creation [\#1851](https://github.com/vatesfr/xo-web/issues/1851)
+- Wrong link in Dashboard > Health [\#1850](https://github.com/vatesfr/xo-web/issues/1850)
+- Incorrect file dates shown in new File Restore feature [\#1840](https://github.com/vatesfr/xo-web/issues/1840)
+- IP allocation problem [\#1747](https://github.com/vatesfr/xo-web/issues/1747)
+- Selfservice limits not honored after VM creation [\#1695](https://github.com/vatesfr/xo-web/issues/1695)
+
+## **5.5.0** (2016-12-20)
+
+File level restore.
+
+### Enhancements
+
+- Better auto select network when migrate VM [\#1788](https://github.com/vatesfr/xo-web/issues/1788)
+- Plugin for passive backup job reporting in Nagios [\#1664](https://github.com/vatesfr/xo-web/issues/1664)
+- File level restore for delta backup [\#1590](https://github.com/vatesfr/xo-web/issues/1590)
+- Better select filters for ACLs [\#1515](https://github.com/vatesfr/xo-web/issues/1515)
+- All pools and "negative" filters [\#1503](https://github.com/vatesfr/xo-web/issues/1503)
+- VM copy with disk selection [\#826](https://github.com/vatesfr/xo-web/issues/826)
+- Disable metadata exports [\#1818](https://github.com/vatesfr/xo-web/issues/1818)
+
+### Bug fixes
+
+- Tool small selector [\#1832](https://github.com/vatesfr/xo-web/issues/1832)
+- Replication does not work from a VM created by a CR or delta backup [\#1811](https://github.com/vatesfr/xo-web/issues/1811)
+- Can't add a SSH key in VM creation [\#1805](https://github.com/vatesfr/xo-web/issues/1805)
+- Issue when no default SR in a pool [\#1804](https://github.com/vatesfr/xo-web/issues/1804)
+- XOA doesn't refresh after an update anymore [\#1801](https://github.com/vatesfr/xo-web/issues/1801)
+- Shortcuts not inhibited on inputs on Safari [\#1691](https://github.com/vatesfr/xo-web/issues/1691)
+
+## **5.4.0** (2016-11-23)
+
+### Enhancements
+
+- XML display in alerts [\#1776](https://github.com/vatesfr/xo-web/issues/1776)
+- Remove some view for non admin users [\#1773](https://github.com/vatesfr/xo-web/issues/1773)
+- Complex matcher should support matching boolean values [\#1768](https://github.com/vatesfr/xo-web/issues/1768)
+- Home SR view [\#1764](https://github.com/vatesfr/xo-web/issues/1764)
+- Filter on tag click [\#1763](https://github.com/vatesfr/xo-web/issues/1763)
+- Testable plugins [\#1749](https://github.com/vatesfr/xo-web/issues/1749)
+- Backup/Restore Design fix. [\#1734](https://github.com/vatesfr/xo-web/issues/1734)
+- Display the owner of a \(backup\) job [\#1733](https://github.com/vatesfr/xo-web/issues/1733)
+- Use paginated table for backup jobs [\#1726](https://github.com/vatesfr/xo-web/issues/1726)
+- SR view / Disks: should display snapshot VDIs [\#1723](https://github.com/vatesfr/xo-web/issues/1723)
+- Restored VM should have an identifiable name [\#1719](https://github.com/vatesfr/xo-web/issues/1719)
+- If host reboot action returns NO\_HOSTS\_AVAILABLE, ask to force [\#1717](https://github.com/vatesfr/xo-web/issues/1717)
+- Hide xo-server timezone in backups [\#1706](https://github.com/vatesfr/xo-web/issues/1706)
+- Enable hyperlink for Hostname for Issues [\#1700](https://github.com/vatesfr/xo-web/issues/1700)
+- Pool/network - Modify column [\#1696](https://github.com/vatesfr/xo-web/issues/1696)
+- UI - Plugins - Display a message if no plugins [\#1670](https://github.com/vatesfr/xo-web/issues/1670)
+- Display warning/error for delta backup on XS older than 6.5 [\#1647](https://github.com/vatesfr/xo-web/issues/1647)
+- XO without internet access doesn't work [\#1629](https://github.com/vatesfr/xo-web/issues/1629)
+- Improve backup restore view [\#1609](https://github.com/vatesfr/xo-web/issues/1609)
+- UI Enhancement - Acronym for dummy [\#1604](https://github.com/vatesfr/xo-web/issues/1604)
+- Slack XO plugin for backup report [\#1593](https://github.com/vatesfr/xo-web/issues/1593)
+- Expose XAPI exceptions in the UI [\#1481](https://github.com/vatesfr/xo-web/issues/1481)
+- Running VMs in the host overview, all VMs in the pool overview [\#1432](https://github.com/vatesfr/xo-web/issues/1432)
+- Move location of NFS mount point [\#1405](https://github.com/vatesfr/xo-web/issues/1405)
+- Home: Pool list - additionnal informations for pool [\#1226](https://github.com/vatesfr/xo-web/issues/1226)
+- Modify VLAN of an existing network [\#1092](https://github.com/vatesfr/xo-web/issues/1092)
+- Wrong instructions for CLI upgrade [\#787](https://github.com/vatesfr/xo-web/issues/787)
+- Ability to export/import XO config [\#786](https://github.com/vatesfr/xo-web/issues/786)
+- Test button for transport-email plugin [\#697](https://github.com/vatesfr/xo-web/issues/697)
+- Merge `scheduler` API into `schedule` [\#664](https://github.com/vatesfr/xo-web/issues/664)
+
+### Bug fixes
+
+- Should jobs be accessible to non admins? [\#1759](https://github.com/vatesfr/xo-web/issues/1759)
+- Schedules deletion is not working [\#1737](https://github.com/vatesfr/xo-web/issues/1737)
+- Editing a job from the jobs overview page does not work [\#1736](https://github.com/vatesfr/xo-web/issues/1736)
+- Editing a schedule from jobs overview does not work [\#1728](https://github.com/vatesfr/xo-web/issues/1728)
+- ACLs not correctly imported [\#1722](https://github.com/vatesfr/xo-web/issues/1722)
+- Some Bootstrap style broken [\#1721](https://github.com/vatesfr/xo-web/issues/1721)
+- Not properly sign out on auth token expiration [\#1711](https://github.com/vatesfr/xo-web/issues/1711)
+- Hosts//network status is incorrect [\#1702](https://github.com/vatesfr/xo-web/issues/1702)
+- Patches application fails "Found : Moved Temporarily" [\#1701](https://github.com/vatesfr/xo-web/issues/1701)
+- Password generation for user creation is not working [\#1678](https://github.com/vatesfr/xo-web/issues/1678)
+- \#/dashboard/health Remove All Orphaned VDIs [\#1622](https://github.com/vatesfr/xo-web/issues/1622)
+- Create a new SR - CIFS/SAMBA Broken [\#1615](https://github.com/vatesfr/xo-web/issues/1615)
+- xo-cli --list-objects: truncated output ? 64k buffer limitation ? [\#1356](https://github.com/vatesfr/xo-web/issues/1356)
+
+## **5.3.0** (2016-10-20)
+
+### Enhancements
+
+- Missing favicon [\#1660](https://github.com/vatesfr/xo-web/issues/1660)
+- ipPools quota [\#1565](https://github.com/vatesfr/xo-web/issues/1565)
+- Dashboard - orphaned VDI [\#1654](https://github.com/vatesfr/xo-web/issues/1654)
+- Stats in home/host view when expanded [\#1634](https://github.com/vatesfr/xo-web/issues/1634)
+- Bar for used and total RAM on home pool view [\#1625](https://github.com/vatesfr/xo-web/issues/1625)
+- Can't translate some text [\#1624](https://github.com/vatesfr/xo-web/issues/1624)
+- Dynamic RAM allocation at creation time [\#1603](https://github.com/vatesfr/xo-web/issues/1603)
+- Display memory bar in home/host view [\#1616](https://github.com/vatesfr/xo-web/issues/1616)
+- Improve keyboard navigation [\#1578](https://github.com/vatesfr/xo-web/issues/1578)
+- Strongly suggest to install the guest tools [\#1575](https://github.com/vatesfr/xo-web/issues/1575)
+- Missing tooltip [\#1568](https://github.com/vatesfr/xo-web/issues/1568)
+- Emphasize already used ips in ipPools [\#1566](https://github.com/vatesfr/xo-web/issues/1566)
+- Change "missing feature message" for non-admins [\#1564](https://github.com/vatesfr/xo-web/issues/1564)
+- Allow VIF edition [\#1446](https://github.com/vatesfr/xo-web/issues/1446)
+- Disable browser autocomplete on credentials on the Update page [\#1304](https://github.com/vatesfr/xo-web/issues/1304)
+- keyboard shortcuts [\#1279](https://github.com/vatesfr/xo-web/issues/1279)
+- Add network bond creation [\#876](https://github.com/vatesfr/xo-web/issues/876)
+- `pool.setDefaultSr\(\)` should not require `pool` param [\#1558](https://github.com/vatesfr/xo-web/issues/1558)
+- Select default SR [\#1554](https://github.com/vatesfr/xo-web/issues/1554)
+- No error message when I exceed my resource set quota [\#1541](https://github.com/vatesfr/xo-web/issues/1541)
+- Hide some buttons for self service VMs [\#1539](https://github.com/vatesfr/xo-web/issues/1539)
+- Add Job ID to backup schedules [\#1534](https://github.com/vatesfr/xo-web/issues/1534)
+- Correct name for VM selector with templates [\#1530](https://github.com/vatesfr/xo-web/issues/1530)
+- Help text when no matches for a filter [\#1517](https://github.com/vatesfr/xo-web/issues/1517)
+- Icon or tooltip to allow VDI migration in VM disk view [\#1512](https://github.com/vatesfr/xo-web/issues/1512)
+- Create a snapshot before restoring one [\#1445](https://github.com/vatesfr/xo-web/issues/1445)
+- Auto power on setting at creation time [\#1444](https://github.com/vatesfr/xo-web/issues/1444)
+- local remotes should be avoided if possible [\#1441](https://github.com/vatesfr/xo-web/issues/1441)
+- Self service edition unclear [\#1429](https://github.com/vatesfr/xo-web/issues/1429)
+- Avoid "\_" char in job tag name [\#1414](https://github.com/vatesfr/xo-web/issues/1414)
+- Display message if host reboot needed to apply patches [\#1352](https://github.com/vatesfr/xo-web/issues/1352)
+- Color code on host PIF stats can be misleading [\#1265](https://github.com/vatesfr/xo-web/issues/1265)
+- Sign in page is not rendered correctly [\#1161](https://github.com/vatesfr/xo-web/issues/1161)
+- Template management [\#1091](https://github.com/vatesfr/xo-web/issues/1091)
+- On pool view: collapse network list [\#1461](https://github.com/vatesfr/xo-web/issues/1461)
+- Alert when trying to reboot/halt the pool master XS [\#1458](https://github.com/vatesfr/xo-web/issues/1458)
+- Adding tooltip on Home page [\#1456](https://github.com/vatesfr/xo-web/issues/1456)
+- Docker container management functionality missing from v5 [\#1442](https://github.com/vatesfr/xo-web/issues/1442)
+- bad error message - delete snapshot [\#1433](https://github.com/vatesfr/xo-web/issues/1433)
+- Create tag during VM creation [\#1431](https://github.com/vatesfr/xo-web/issues/1431)
+
+### Bug fixes
+
+- Display issues on plugin array edition [\#1663](https://github.com/vatesfr/xo-web/issues/1663)
+- Import of delta backups fails [\#1656](https://github.com/vatesfr/xo-web/issues/1656)
+- Host - Missing IP config for PIF [\#1651](https://github.com/vatesfr/xo-web/issues/1651)
+- Remote copy is always activating compression [\#1645](https://github.com/vatesfr/xo-web/issues/1645)
+- LB plugin UI problems [\#1630](https://github.com/vatesfr/xo-web/issues/1630)
+- Keyboard shortcuts should not work when a modal is open [\#1589](https://github.com/vatesfr/xo-web/issues/1589)
+- UI small bug in drop-down lists [\#1411](https://github.com/vatesfr/xo-web/issues/1411)
+- md5 delta backup error [\#1672](https://github.com/vatesfr/xo-web/issues/1672)
+- Can't edit VIF network [\#1640](https://github.com/vatesfr/xo-web/issues/1640)
+- Do not expose shortcuts while console is focused [\#1614](https://github.com/vatesfr/xo-web/issues/1614)
+- All users can see VM templates [\#1621](https://github.com/vatesfr/xo-web/issues/1621)
+- Profile page is broken [\#1612](https://github.com/vatesfr/xo-web/issues/1612)
+- SR delete should redirect to home [\#1611](https://github.com/vatesfr/xo-web/issues/1611)
+- Delta VHD backup checksum is invalidated by chaining [\#1606](https://github.com/vatesfr/xo-web/issues/1606)
+- VM with long description break on 2 lines [\#1580](https://github.com/vatesfr/xo-web/issues/1580)
+- Network status on VM edition [\#1573](https://github.com/vatesfr/xo-web/issues/1573)
+- VM template deletion fails [\#1571](https://github.com/vatesfr/xo-web/issues/1571)
+- Template edition - "no such object" [\#1569](https://github.com/vatesfr/xo-web/issues/1569)
+- missing links / element not displayed as links [\#1567](https://github.com/vatesfr/xo-web/issues/1567)
+- Backup restore stalled on some SMB shares [\#1412](https://github.com/vatesfr/xo-web/issues/1412)
+- Wrong bond display [\#1156](https://github.com/vatesfr/xo-web/issues/1156)
+- Multiple reboot selection doesn't work [\#1562](https://github.com/vatesfr/xo-web/issues/1562)
+- Server logs should be displayed in reverse chonological order [\#1547](https://github.com/vatesfr/xo-web/issues/1547)
+- Cannot create resource sets without limits [\#1537](https://github.com/vatesfr/xo-web/issues/1537)
+- UI - Weird display when editing long VM desc [\#1528](https://github.com/vatesfr/xo-web/issues/1528)
+- Useless iso selector in host console [\#1527](https://github.com/vatesfr/xo-web/issues/1527)
+- Pool and Host dummy welcome message [\#1519](https://github.com/vatesfr/xo-web/issues/1519)
+- Bug on Network VM tab [\#1518](https://github.com/vatesfr/xo-web/issues/1518)
+- Link to home with filter in query does not work [\#1513](https://github.com/vatesfr/xo-web/issues/1513)
+- VHD merge fails with "RangeError: index out of range" on SMB remote [\#1511](https://github.com/vatesfr/xo-web/issues/1511)
+- DR: previous VDIs are not removed [\#1510](https://github.com/vatesfr/xo-web/issues/1510)
+- DR: previous copies not removed when same number as depth [\#1509](https://github.com/vatesfr/xo-web/issues/1509)
+- Empty Saved Search doesn't load when set to default filter [\#1354](https://github.com/vatesfr/xo-web/issues/1354)
+- Removing a user/group should delete its ACLs [\#899](https://github.com/vatesfr/xo-web/issues/899)
+- OVA Import - XO stuck during import [\#1551](https://github.com/vatesfr/xo-web/issues/1551)
+- SMB remote empty domain fails [\#1499](https://github.com/vatesfr/xo-web/issues/1499)
+- Can't edit a remote password [\#1498](https://github.com/vatesfr/xo-web/issues/1498)
+- Issue in VM create with CoreOS [\#1493](https://github.com/vatesfr/xo-web/issues/1493)
+- Overlapping months in backup view [\#1488](https://github.com/vatesfr/xo-web/issues/1488)
+- No line break for SSH key in user view [\#1475](https://github.com/vatesfr/xo-web/issues/1475)
+- Create VIF UI issues [\#1472](https://github.com/vatesfr/xo-web/issues/1472)
+
+## **5.2.0** (2016-09-09)
+
+### Enhancements
+
+- IP management [\#1350](https://github.com/vatesfr/xo-web/issues/1350), [\#988](https://github.com/vatesfr/xo-web/issues/988), [\#1427](https://github.com/vatesfr/xo-web/issues/1427) and [\#240](https://github.com/vatesfr/xo-web/issues/240)
+- Update reverse proxy example [\#1474](https://github.com/vatesfr/xo-web/issues/1474)
+- Improve log view [\#1467](https://github.com/vatesfr/xo-web/issues/1467)
+- Backup Reports: e-mail subject [\#1463](https://github.com/vatesfr/xo-web/issues/1463)
+- Backup Reports: report the error [\#1462](https://github.com/vatesfr/xo-web/issues/1462)
+- Vif selector: select management network by default [\#1425](https://github.com/vatesfr/xo-web/issues/1425)
+- Display when browser disconnected to server [\#1417](https://github.com/vatesfr/xo-web/issues/1417)
+- Tooltip on OS icon in VM view [\#1416](https://github.com/vatesfr/xo-web/issues/1416)
+- Display pool master [\#1407](https://github.com/vatesfr/xo-web/issues/1407)
+- Missing tooltips in VM creation view [\#1402](https://github.com/vatesfr/xo-web/issues/1402)
+- Handle VBD disconnect and connect [\#1397](https://github.com/vatesfr/xo-web/issues/1397)
+- Eject host from a pool [\#1395](https://github.com/vatesfr/xo-web/issues/1395)
+- Improve pool general view [\#1393](https://github.com/vatesfr/xo-web/issues/1393)
+- Improve patching system [\#1392](https://github.com/vatesfr/xo-web/issues/1392)
+- Pool name modification [\#1390](https://github.com/vatesfr/xo-web/issues/1390)
+- Confirmation dialog before destroying VDIs [\#1388](https://github.com/vatesfr/xo-web/issues/1388)
+- Tooltips for meter object [\#1387](https://github.com/vatesfr/xo-web/issues/1387)
+- New Host assistant [\#1374](https://github.com/vatesfr/xo-web/issues/1374)
+- New VM assistant [\#1373](https://github.com/vatesfr/xo-web/issues/1373)
+- New SR assistant [\#1372](https://github.com/vatesfr/xo-web/issues/1372)
+- Direct access to VDI listing from dashboard's SR usage breakdown [\#1371](https://github.com/vatesfr/xo-web/issues/1371)
+- Can't set a network name at pool level [\#1368](https://github.com/vatesfr/xo-web/issues/1368)
+- Change a few mouse over descriptions [\#1363](https://github.com/vatesfr/xo-web/issues/1363)
+- Hide network install in VM create if template is HVM [\#1362](https://github.com/vatesfr/xo-web/issues/1362)
+- SR space left during VM creation [\#1358](https://github.com/vatesfr/xo-web/issues/1358)
+- Add destination SR on migration modal in VM view [\#1357](https://github.com/vatesfr/xo-web/issues/1357)
+- Ability to create a new VM from a snapshot [\#1353](https://github.com/vatesfr/xo-web/issues/1353)
+- Missing explanation/confirmation on Snapshot Page [\#1349](https://github.com/vatesfr/xo-web/issues/1349)
+- Log view: expose API errors in the web UI [\#1344](https://github.com/vatesfr/xo-web/issues/1344)
+- Registration on update page [\#1341](https://github.com/vatesfr/xo-web/issues/1341)
+- Add export snapshot button [\#1336](https://github.com/vatesfr/xo-web/issues/1336)
+- Use saved SSH keys in VM create CloudConfig [\#1319](https://github.com/vatesfr/xo-web/issues/1319)
+- Collapse header in console view [\#1268](https://github.com/vatesfr/xo-web/issues/1268)
+- Two max concurrent jobs in parallel [\#915](https://github.com/vatesfr/xo-web/issues/915)
+- Handle OVA import via the web UI [\#709](https://github.com/vatesfr/xo-web/issues/709)
+
+### Bug fixes
+
+- Bug on VM console when header is hidden [\#1485](https://github.com/vatesfr/xo-web/issues/1485)
+- Disks not removed when deleting multiple VMs [\#1484](https://github.com/vatesfr/xo-web/issues/1484)
+- Do not display VDI disconnect button when a VM is not running [\#1470](https://github.com/vatesfr/xo-web/issues/1470)
+- Do not display VIF disconnect button when a VM is not running [\#1468](https://github.com/vatesfr/xo-web/issues/1468)
+- Error on migration if no default SR \(even when not used\) [\#1466](https://github.com/vatesfr/xo-web/issues/1466)
+- DR issue while rotating old backup [\#1464](https://github.com/vatesfr/xo-web/issues/1464)
+- Giving resource set to end-user ends with error [\#1448](https://github.com/vatesfr/xo-web/issues/1448)
+- Error thrown when cancelling out of Delete User confirmation dialog [\#1439](https://github.com/vatesfr/xo-web/issues/1439)
+- Wrong month label shown in Backup and Job scheduler [\#1438](https://github.com/vatesfr/xo-web/issues/1438)
+- Bug on Self service creation/edition [\#1428](https://github.com/vatesfr/xo-web/issues/1428)
+- ISO selection during VM create is not mounted after [\#1415](https://github.com/vatesfr/xo-web/issues/1415)
+- Hosts general view: bad link for storage [\#1408](https://github.com/vatesfr/xo-web/issues/1408)
+- Backup Schedule - "Month" and "Day of Week" display error [\#1404](https://github.com/vatesfr/xo-web/issues/1404)
+- Migrate dialog doesn't present all available VIF's in new UI interface [\#1403](https://github.com/vatesfr/xo-web/issues/1403)
+- NFS mount issues [\#1396](https://github.com/vatesfr/xo-web/issues/1396)
+- Select component color [\#1391](https://github.com/vatesfr/xo-web/issues/1391)
+- SR created with local path shouldn't be shared [\#1389](https://github.com/vatesfr/xo-web/issues/1389)
+- Disks (VBD) are attached to VM in RO mode instead of RW even if RO is unchecked [\#1386](https://github.com/vatesfr/xo-web/issues/1386)
+- Re-connection issues between server and XS hosts [\#1384](https://github.com/vatesfr/xo-web/issues/1384)
+- Meter object style with Chrome 52 [\#1383](https://github.com/vatesfr/xo-web/issues/1383)
+- Editing a rolling snapshot job seems to fail [\#1376](https://github.com/vatesfr/xo-web/issues/1376)
+- Dashboard SR usage and total inverted [\#1370](https://github.com/vatesfr/xo-web/issues/1370)
+- XenServer connection issue with host while using VGPUs [\#1369](https://github.com/vatesfr/xo-web/issues/1369)
+- Job created with v4 are not correctly displayed in v5 [\#1366](https://github.com/vatesfr/xo-web/issues/1366)
+- CPU accounting in resource set [\#1365](https://github.com/vatesfr/xo-web/issues/1365)
+- Tooltip stay displayed when a button change state [\#1360](https://github.com/vatesfr/xo-web/issues/1360)
+- Failure on host reboot [\#1351](https://github.com/vatesfr/xo-web/issues/1351)
+- Editing Backup Jobs Without Compression, Slider Always Set To On [\#1339](https://github.com/vatesfr/xo-web/issues/1339)
+- Month Selection on Backup Screen Wrong [\#1338](https://github.com/vatesfr/xo-web/issues/1338)
+- Delta backup fail when removed VDIs [\#1333](https://github.com/vatesfr/xo-web/issues/1333)
+
+## **5.1.0** (2016-07-26)
+
+### Enhancements
+
+- Improve backups timezone UI [\#1314](https://github.com/vatesfr/xo-web/issues/1314)
+- HOME view submenus [\#1306](https://github.com/vatesfr/xo-web/issues/1306)
+- Ability for a user to save SSH keys [\#1299](https://github.com/vatesfr/xo-web/issues/1299)
+- \[ACLs\] Ability to select all hosts/VMs [\#1296](https://github.com/vatesfr/xo-web/issues/1296)
+- Improve scheduling UI [\#1295](https://github.com/vatesfr/xo-web/issues/1295)
+- Plugins: Predefined configurations [\#1289](https://github.com/vatesfr/xo-web/issues/1289)
+- Button to recompute resource sets limits [\#1287](https://github.com/vatesfr/xo-web/issues/1287)
+- Credit scheduler CAP and weight configuration [\#1283](https://github.com/vatesfr/xo-web/issues/1283)
+- Migration form problem on the /v5/vms/\_\_UUID\_\_ page when doing xenmotion inside a pool [\#1254](https://github.com/vatesfr/xo-web/issues/1254)
+- /v5/\#/pools/\_\_UUID\_\_: patch table improvement [\#1246](https://github.com/vatesfr/xo-web/issues/1246)
+- /v5/\#/hosts/\_\_UUID\_\_: patch list improvements ? [\#1245](https://github.com/vatesfr/xo-web/issues/1245)
+- F\*cking patches, how do they work? [\#1236](https://github.com/vatesfr/xo-web/issues/1236)
+- Change Default Filter [\#1235](https://github.com/vatesfr/xo-web/issues/1235)
+- Add a property on jobs to know their state [\#1232](https://github.com/vatesfr/xo-web/issues/1232)
+- Spanish translation [\#1231](https://github.com/vatesfr/xo-web/issues/1231)
+- Home: "Filter" input and keyboard focus [\#1228](https://github.com/vatesfr/xo-web/issues/1228)
+- Display xenserver version [\#1225](https://github.com/vatesfr/xo-web/issues/1225)
+- Plugin config: presets & defaults [\#1222](https://github.com/vatesfr/xo-web/issues/1222)
+- Allow halted VM migration [\#1216](https://github.com/vatesfr/xo-web/issues/1216)
+- Missing confirm dialog on critical button [\#1211](https://github.com/vatesfr/xo-web/issues/1211)
+- Backup logs are not sortable [\#1196](https://github.com/vatesfr/xo-web/issues/1196)
+- Page title with the name of current object [\#1185](https://github.com/vatesfr/xo-web/issues/1185)
+- Existing VIF management [\#1176](https://github.com/vatesfr/xo-web/issues/1176)
+- Do not display fast clone option is there isn't template disks [\#1172](https://github.com/vatesfr/xo-web/issues/1172)
+- UI issue when adding a user [\#1159](https://github.com/vatesfr/xo-web/issues/1159)
+- Combined values on stats [\#1158](https://github.com/vatesfr/xo-web/issues/1158)
+- Parallel coordinates graph [\#1157](https://github.com/vatesfr/xo-web/issues/1157)
+- VM creation on self-service as user [\#1155](https://github.com/vatesfr/xo-web/issues/1155)
+- VM copy bulk action on home view [\#1154](https://github.com/vatesfr/xo-web/issues/1154)
+- Better VDI map [\#1151](https://github.com/vatesfr/xo-web/issues/1151)
+- Missing tooltips on buttons [\#1150](https://github.com/vatesfr/xo-web/issues/1150)
+- Patching from pool view [\#1149](https://github.com/vatesfr/xo-web/issues/1149)
+- Missing patches in dashboard [\#1148](https://github.com/vatesfr/xo-web/issues/1148)
+- Improve tasks view [\#1147](https://github.com/vatesfr/xo-web/issues/1147)
+- Home bulk VM migration [\#1146](https://github.com/vatesfr/xo-web/issues/1146)
+- LDAP plugin clear password field [\#1145](https://github.com/vatesfr/xo-web/issues/1145)
+- Cron default behavior [\#1144](https://github.com/vatesfr/xo-web/issues/1144)
+- Modal for migrate on home [\#1143](https://github.com/vatesfr/xo-web/issues/1143)
+- /v5/\#/srs/\_\_UUID\_\_: UI improvements [\#1142](https://github.com/vatesfr/xo-web/issues/1142)
+- /v5/\#/pools/: some name should be links [\#1141](https://github.com/vatesfr/xo-web/issues/1141)
+- create the page /v5/\#/pools/ [\#1140](https://github.com/vatesfr/xo-web/issues/1140)
+- Dashboard: add links to different part of XOA [\#1139](https://github.com/vatesfr/xo-web/issues/1139)
+- /v5/\#/dashboard/overview: add link on the "Top 5 SR Usage" graph [\#1135](https://github.com/vatesfr/xo-web/issues/1135)
+- /v5/\#/backup/overview: display the error when there is one returned by xenserver on failed job. [\#1134](https://github.com/vatesfr/xo-web/issues/1134)
+- /v5/: add an option to set the number of element displayed in tables [\#1133](https://github.com/vatesfr/xo-web/issues/1133)
+- Updater refresh page after update [\#1131](https://github.com/vatesfr/xo-web/issues/1131)
+- /v5/\#/settings/plugins [\#1130](https://github.com/vatesfr/xo-web/issues/1130)
+- /v5/\#/new/sr: layout issue [\#1129](https://github.com/vatesfr/xo-web/issues/1129)
+- v5 /v5/\#/vms/new: layout issue [\#1128](https://github.com/vatesfr/xo-web/issues/1128)
+- v5 user page missing style [\#1127](https://github.com/vatesfr/xo-web/issues/1127)
+- Remote helper/tester [\#1075](https://github.com/vatesfr/xo-web/issues/1075)
+- Generate uiSchema from custom schema properties [\#951](https://github.com/vatesfr/xo-web/issues/951)
+- Customizing VM names generation during batch creation [\#949](https://github.com/vatesfr/xo-web/issues/949)
+
+### Bug fixes
+
+- Plugins: Don't use `default` attributes in presets list [\#1288](https://github.com/vatesfr/xo-web/issues/1288)
+- CPU weight must be an integer [\#1286](https://github.com/vatesfr/xo-web/issues/1286)
+- Overview of self service is always empty [\#1282](https://github.com/vatesfr/xo-web/issues/1282)
+- SR attach/creation issue [\#1281](https://github.com/vatesfr/xo-web/issues/1281)
+- Self service resources not modified after a VM deletion [\#1276](https://github.com/vatesfr/xo-web/issues/1276)
+- Scheduled jobs seems use GMT since 5.0 [\#1258](https://github.com/vatesfr/xo-web/issues/1258)
+- Can't create a VM with disks on 2 different SRs [\#1257](https://github.com/vatesfr/xo-web/issues/1257)
+- Graph display bug [\#1247](https://github.com/vatesfr/xo-web/issues/1247)
+- /v5/#/hosts/__UUID__: Patch list not limited to the current pool [\#1244](https://github.com/vatesfr/xo-web/issues/1244)
+- Replication issues [\#1233](https://github.com/vatesfr/xo-web/issues/1233)
+- VM creation install method disabled fields [\#1198](https://github.com/vatesfr/xo-web/issues/1198)
+- Update icon shouldn't be displayed when menu is collapsed [\#1188](https://github.com/vatesfr/xo-web/issues/1188)
+- /v5/ : Load average graph axis issue [\#1167](https://github.com/vatesfr/xo-web/issues/1167)
+- Some remote can't be opened [\#1164](https://github.com/vatesfr/xo-web/issues/1164)
+- Bulk action for hosts in home and pool view [\#1153](https://github.com/vatesfr/xo-web/issues/1153)
+- New Vif [\#1138](https://github.com/vatesfr/xo-web/issues/1138)
+- Missing SRs [\#1123](https://github.com/vatesfr/xo-web/issues/1123)
+- Continuous replication email alert does not obey per job setting [\#1121](https://github.com/vatesfr/xo-web/issues/1121)
+- Safari XO5 issue [\#1120](https://github.com/vatesfr/xo-web/issues/1120)
+- ACLs shoud be available in Enterprise Edition [\#1118](https://github.com/vatesfr/xo-web/issues/1118)
+- SR edit name or description doesn't work [\#1116](https://github.com/vatesfr/xo-web/issues/1116)
+- Bad RRD parsing for VIFs [\#969](https://github.com/vatesfr/xo-web/issues/969)
+
+## **5.0.0** (2016-06-24)
+
+### Enhancements
+
+- Handle failed quiesce in snapshots [\#1088](https://github.com/vatesfr/xo-web/issues/1088)
+- Sparklines stats [\#1061](https://github.com/vatesfr/xo-web/issues/1061)
+- Task view [\#1060](https://github.com/vatesfr/xo-web/issues/1060)
+- Improved import system [\#1048](https://github.com/vatesfr/xo-web/issues/1048)
+- Backup restore view improvements [\#1021](https://github.com/vatesfr/xo-web/issues/1021)
+- Restore VM - Wrong VLAN on the VMs interface [\#1016](https://github.com/vatesfr/xo-web/issues/1016)
+- Fast Disk Cloning [\#960](https://github.com/vatesfr/xo-web/issues/960)
+- Disaster recovery job should target SRs, not pools [\#955](https://github.com/vatesfr/xo-web/issues/955)
+- Improve Header/Content interaction in a page [\#926](https://github.com/vatesfr/xo-web/issues/926)
+- New default view [\#912](https://github.com/vatesfr/xo-web/issues/912)
+- Xen Patching - Restart Pending [\#883](https://github.com/vatesfr/xo-web/issues/883)
+- Hide About page for user that are not admin [\#877](https://github.com/vatesfr/xo-web/issues/877)
+- ACL: Ability to view/sort/group by User/Group, Objects or Role [\#875](https://github.com/vatesfr/xo-web/issues/875)
+- ACL: Ability to select multiple users & group when creating a rule [\#874](https://github.com/vatesfr/xo-web/issues/874)
+- Translation [\#839](https://github.com/vatesfr/xo-web/issues/839)
+- XO offer useless network interfaces for XenMontion [\#833](https://github.com/vatesfr/xo-web/issues/833)
+- Show HVM, PVM, PVHVM modes in guest details [\#806](https://github.com/vatesfr/xo-web/issues/806)
+- Tree view: display cpu available/total for each host [\#696](https://github.com/vatesfr/xo-web/issues/696)
+- Greenkeeper integration [\#667](https://github.com/vatesfr/xo-web/issues/667)
+- Clarify vCPUs and RAM editor [\#658](https://github.com/vatesfr/xo-web/issues/658)
+- Backup LZ4 compression [\#647](https://github.com/vatesfr/xo-web/issues/647)
+- Support enum in plugins configuration [\#638](https://github.com/vatesfr/xo-web/issues/638)
+- Add configuration option to disable xoa-updater [\#535](https://github.com/vatesfr/xo-web/issues/535)
+- Use cursors to add more context to actions [\#523](https://github.com/vatesfr/xo-web/issues/523)
+- Review UI for flat view [\#354](https://github.com/vatesfr/xo-web/issues/354)
+- Review UI for the tree view [\#353](https://github.com/vatesfr/xo-web/issues/353)
+- Tag filtering [\#233](https://github.com/vatesfr/xo-web/issues/233)
+- GUI review [\#230](https://github.com/vatesfr/xo-web/issues/230)
+- Review UI for VM creation [\#214](https://github.com/vatesfr/xo-web/issues/214)
+- Ability to collapse pools/hosts in main view [\#173](https://github.com/vatesfr/xo-web/issues/173)
+- Issue importing .xva VM via xo-web [\#1022](https://github.com/vatesfr/xo-web/issues/1022)
+- Enhancement Proposal - Cancel In Progress Backups [\#1003](https://github.com/vatesfr/xo-web/issues/1003)
+- Can't create VM with CloudConfigDrive [\#917](https://github.com/vatesfr/xo-web/issues/917)
+- Auth: LDAP User causes problems [\#893](https://github.com/vatesfr/xo-web/issues/893)
+- No tags in Continuous Replication [\#838](https://github.com/vatesfr/xo-web/issues/838)
+- Delta backup Depth not working [\#802](https://github.com/vatesfr/xo-web/issues/802)
+- Update Section - Running version info missing - gui enhancement [\#795](https://github.com/vatesfr/xo-web/issues/795)
+- On reboot, vnc console wrongly scaled [\#722](https://github.com/vatesfr/xo-web/issues/722)
+- Make the object name \(title\) "sticky" at the top of the page [\#705](https://github.com/vatesfr/xo-web/issues/705)
+- pool view: display Local SR from hosts in the current pool [\#692](https://github.com/vatesfr/xo-web/issues/692)
+- tree view: display all IPs [\#689](https://github.com/vatesfr/xo-web/issues/689)
+- XO5 parallel distribution [\#462](https://github.com/vatesfr/xo-web/issues/462)
+- Load balancing with XO [\#423](https://github.com/vatesfr/xo-web/issues/423)
+
+### Bug fixes
+
+- vCPUs number when no tools installed [\#1089](https://github.com/vatesfr/xo-web/issues/1089)
+- Config Drive textbox disappears when content is deleted [\#1012](https://github.com/vatesfr/xo-web/issues/1012)
+- storage status not changed in host view page after disconnect/connect [\#1009](https://github.com/vatesfr/xo-web/issues/1009)
+- Cannot Delete Logs From Backup Overview [\#1004](https://github.com/vatesfr/xo-web/issues/1004)
+- \[v5.x\] Plugins configuration: optional non-used objects are sent [\#1000](https://github.com/vatesfr/xo-web/issues/1000)
+- "@" char in remote password break the remote view [\#997](https://github.com/vatesfr/xo-web/issues/997)
+- Handle MEMORY\_CONSTRAINT\_VIOLATION correctly [\#970](https://github.com/vatesfr/xo-web/issues/970)
+- VM creation error on XenServer Dundee [\#964](https://github.com/vatesfr/xo-web/issues/964)
+- Copy VMs doesn't display all SRs [\#945](https://github.com/vatesfr/xo-web/issues/945)
+- Autopower\_on wrong value [\#937](https://github.com/vatesfr/xo-web/issues/937)
+- Correctly handle unknown users in group view [\#900](https://github.com/vatesfr/xo-web/issues/900)
+- Importing into Dundee [\#887](https://github.com/vatesfr/xo-web/issues/887)
+- update status - gui resize issue [\#803](https://github.com/vatesfr/xo-web/issues/803)
+- Backup Remote Stores Problem [\#751](https://github.com/vatesfr/xo-web/issues/751)
+- VM view is broken when changing a disk SR twice [\#670](https://github.com/vatesfr/xo-web/issues/670)
+- console mouse sync [\#280](https://github.com/vatesfr/xo-web/issues/280)
+
+## **4.16.0** (2016-04-29)
+
+Maintenance release
+
+### Enhancements
+
+- TOO\_MANY\_PENDING\_TASKS [\#861](https://github.com/vatesfr/xo-web/issues/861)
+
+### Bug fixes
+
+- Incorrect VM target name with continuous replication [\#904](https://github.com/vatesfr/xo-web/issues/904)
+- Error while deleting users [\#901](https://github.com/vatesfr/xo-web/issues/901)
+- Use an available path to the SR to create a config drive [\#882](https://github.com/vatesfr/xo-web/issues/882)
+- VM autoboot don't set the right pool parameter [\#879](https://github.com/vatesfr/xo-web/issues/879)
+- BUG: ACL with NFS ISO Library not working! [\#870](https://github.com/vatesfr/xo-web/issues/870)
+- Broken paths in backups in SMB [\#865](https://github.com/vatesfr/xo-web/issues/865)
+- Plugins page loads users/groups multiple times [\#829](https://github.com/vatesfr/xo-web/issues/829)
+- "Ghost" VM remains after migration [\#769](https://github.com/vatesfr/xo-web/issues/769)
+
+## **4.15.0** (2016-03-21)
+
+Load balancing, SMB delta support, advanced network operations...
+
+### Enhancements
+
+- Add the job name inside the backup email report [\#819](https://github.com/vatesfr/xo-web/issues/819)
+- Delta backup with quiesce [\#812](https://github.com/vatesfr/xo-web/issues/812)
+- Hosts: No user feedback when error occurs with SR connect / disconnect [\#810](https://github.com/vatesfr/xo-web/issues/810)
+- Expose components versions [\#807](https://github.com/vatesfr/xo-web/issues/807)
+- Rework networks/PIFs management [\#805](https://github.com/vatesfr/xo-web/issues/805)
+- Displaying all SRs and a list of available hosts for creating VM from a pool [\#790](https://github.com/vatesfr/xo-web/issues/790)
+- Add "Source network" on "VM migration" screen [\#785](https://github.com/vatesfr/xo-web/issues/785)
+- Migration queue [\#783](https://github.com/vatesfr/xo-web/issues/783)
+- Match network names for VM migration [\#782](https://github.com/vatesfr/xo-web/issues/782)
+- Disk names [\#780](https://github.com/vatesfr/xo-web/issues/780)
+- Self service: should the user be able to set the CPU weight? [\#767](https://github.com/vatesfr/xo-web/issues/767)
+- host & pool Citrix license status [\#763](https://github.com/vatesfr/xo-web/issues/763)
+- pool view: Provide "updates" section [\#762](https://github.com/vatesfr/xo-web/issues/762)
+- XOA ISO image: ambigious root disk label [\#761](https://github.com/vatesfr/xo-web/issues/761)
+- Host info: provide system serial number [\#760](https://github.com/vatesfr/xo-web/issues/760)
+- CIFS ISO SR Creation [\#731](https://github.com/vatesfr/xo-web/issues/731)
+- MAC address not preserved on VM restore [\#707](https://github.com/vatesfr/xo-web/issues/707)
+- Failing replication job should send reports [\#659](https://github.com/vatesfr/xo-web/issues/659)
+- Display networks in the Pool view [\#226](https://github.com/vatesfr/xo-web/issues/226)
+
+### Bug fixes
+
+- Broken link to backup remote [\#821](https://github.com/vatesfr/xo-web/issues/821)
+- Issue with self-signed cert for email plugin [\#817](https://github.com/vatesfr/xo-web/issues/817)
+- Plugins view, reset form and errors [\#815](https://github.com/vatesfr/xo-web/issues/815)
+- HVM recovery mode is broken [\#794](https://github.com/vatesfr/xo-web/issues/794)
+- Disk bug when creating vm from template [\#778](https://github.com/vatesfr/xo-web/issues/778)
+- Can't mount NFS shares in remote stores [\#775](https://github.com/vatesfr/xo-web/issues/775)
+- VM disk name and description not passed during creation [\#774](https://github.com/vatesfr/xo-web/issues/774)
+- NFS mount problem for Windows share [\#771](https://github.com/vatesfr/xo-web/issues/771)
+- lodash.pluck not installed [\#757](https://github.com/vatesfr/xo-web/issues/757)
+- this.\_getAuthenticationTokensForUser is not a function [\#755](https://github.com/vatesfr/xo-web/issues/755)
+- CentOS 6.x 64bit template creates a VM that won't boot [\#733](https://github.com/vatesfr/xo-web/issues/733)
+- Lot of xo:perf leading to XO crash [\#575](https://github.com/vatesfr/xo-web/issues/575)
+- New collection checklist [\#262](https://github.com/vatesfr/xo-web/issues/262)
+
+## **4.14.0** (2016-02-23)
+
+Self service, custom CloudInit...
+
+### Enhancements
+
+- VM creation self service with quotas [\#285](https://github.com/vatesfr/xo-web/issues/285)
+- Cloud config custom user data [\#706](https://github.com/vatesfr/xo-web/issues/706)
+- Patches behind a proxy [\#737](https://github.com/vatesfr/xo-web/issues/737)
+- Remote store status indicator [\#728](https://github.com/vatesfr/xo-web/issues/728)
+- Patch list order [\#724](https://github.com/vatesfr/xo-web/issues/724)
+- Enable reporting on additional backup types [\#717](https://github.com/vatesfr/xo-web/issues/717)
+- Tooltip name for cancel [\#703](https://github.com/vatesfr/xo-web/issues/703)
+- Portable VHD merging [\#646](https://github.com/vatesfr/xo-web/issues/646)
+
+### Bug fixes
+
+- Avoid merge between two delta vdi backups [\#702](https://github.com/vatesfr/xo-web/issues/702)
+- Text in table is not cut anymore [\#713](https://github.com/vatesfr/xo-web/issues/713)
+- Disk size edition issue with float numbers [\#719](https://github.com/vatesfr/xo-web/issues/719)
+- Create vm, summary is not refreshed [\#721](https://github.com/vatesfr/xo-web/issues/721)
+- Boot order problem [\#726](https://github.com/vatesfr/xo-web/issues/726)
+
+## **4.13.0** (2016-02-05)
+
+Backup checksum, SMB remotes...
+
+### Enhancements
+
+- Add SMB mount for remote [\#338](https://github.com/vatesfr/xo-web/issues/338)
+- Centralize Perm in a lib [\#345](https://github.com/vatesfr/xo-web/issues/345)
+- Expose interpool migration details [\#567](https://github.com/vatesfr/xo-web/issues/567)
+- Add checksum for delta backup [\#617](https://github.com/vatesfr/xo-web/issues/617)
+- Redirect from HTTP to HTTPS [\#626](https://github.com/vatesfr/xo-web/issues/626)
+- Expose vCPU weight [\#633](https://github.com/vatesfr/xo-web/issues/633)
+- Avoid metadata in delta backup [\#651](https://github.com/vatesfr/xo-web/issues/651)
+- Button to clear logs [\#661](https://github.com/vatesfr/xo-web/issues/661)
+- Units for RAM and disks [\#666](https://github.com/vatesfr/xo-web/issues/666)
+- Remove multiple VDIs at once [\#676](https://github.com/vatesfr/xo-web/issues/676)
+- Find orphaned VDI snapshots [\#679](https://github.com/vatesfr/xo-web/issues/679)
+- New health view in Dashboard [\#680](https://github.com/vatesfr/xo-web/issues/680)
+- Use physical usage for VDI and SR [\#682](https://github.com/vatesfr/xo-web/issues/682)
+- TLS configuration [\#685](https://github.com/vatesfr/xo-web/issues/685)
+- Better VM info on tree view [\#688](https://github.com/vatesfr/xo-web/issues/688)
+- Absolute values in tooltips for tree view [\#690](https://github.com/vatesfr/xo-web/issues/690)
+- Absolute values for host memory [\#691](https://github.com/vatesfr/xo-web/issues/691)
+
+### Bug fixes
+
+- Issues on host console screen [\#672](https://github.com/vatesfr/xo-web/issues/672)
+- NFS remote mount fails in particular case [\#665](https://github.com/vatesfr/xo-web/issues/665)
+- Unresponsive pages [\#662](https://github.com/vatesfr/xo-web/issues/662)
+- Live migration fail in the same pool with local SR fails [\#655](https://github.com/vatesfr/xo-web/issues/655)
+
+## **4.12.0** (2016-01-18)
+
+Continuous Replication, Continuous Delta backup...
+
+### Enhancements
+
+- Continuous VM replication [\#582](https://github.com/vatesfr/xo-web/issues/582)
+- Continuous Delta Backup [\#576](https://github.com/vatesfr/xo-web/issues/576)
+- Scheduler should not run job again if previous instance is not finished [\#642](https://github.com/vatesfr/xo-web/issues/642)
+- Boot VM automatically after creation [\#635](https://github.com/vatesfr/xo-web/issues/635)
+- Manage existing VIFs in templates [\#630](https://github.com/vatesfr/xo-web/issues/630)
+- Support templates with existing install repository [\#627](https://github.com/vatesfr/xo-web/issues/627)
+- Remove running VMs [\#616](https://github.com/vatesfr/xo-web/issues/616)
+- Prevent a VM to start before delta import is finished [\#613](https://github.com/vatesfr/xo-web/issues/613)
+- Spawn multiple VMs at once [\#606](https://github.com/vatesfr/xo-web/issues/606)
+- Fixed `suspendVM` in tree view. [\#619](https://github.com/vatesfr/xo-web/pull/619) ([pdonias](https://github.com/pdonias))
+
+### Bug fixes
+
+- User defined MAC address is not fetch in VM install [\#643](https://github.com/vatesfr/xo-web/issues/643)
+- CoreOsCloudConfig is not shown with CoreOS [\#639](https://github.com/vatesfr/xo-web/issues/639)
+- Plugin activation/deactivation in web UI seems broken [\#637](https://github.com/vatesfr/xo-web/issues/637)
+- Issue when creating CloudConfig drive [\#636](https://github.com/vatesfr/xo-web/issues/636)
+- CloudConfig hostname shouldn't have space [\#634](https://github.com/vatesfr/xo-web/issues/634)
+- Cloned VIFs are not properly deleted on VM creation [\#632](https://github.com/vatesfr/xo-web/issues/632)
+- Default PV args missing during VM creation [\#628](https://github.com/vatesfr/xo-web/issues/628)
+- VM creation problems from custom templates [\#625](https://github.com/vatesfr/xo-web/issues/625)
+- Emergency shutdown race condition [\#622](https://github.com/vatesfr/xo-web/issues/622)
+- `vm.delete\(\)` should not delete VDIs attached to other VMs [\#621](https://github.com/vatesfr/xo-web/issues/621)
+- VM creation error from template with a disk [\#581](https://github.com/vatesfr/xo-web/issues/581)
+- Only delete VDI exports when VM backup is successful [\#644](https://github.com/vatesfr/xo-web/issues/644)
+- Change the name of an imported VM during the import process [\#641](https://github.com/vatesfr/xo-web/issues/641)
+- Creating a new VIF in view is partially broken [\#652](https://github.com/vatesfr/xo-web/issues/652)
+- Grey out the "create button" during VM creation [\#654](https://github.com/vatesfr/xo-web/issues/654)
+
+## **4.11.0** (2015-12-22)
+
+Delta backup, CloudInit...
+
+### Enhancements
+
+- Visible list of SR inside a VM [\#601](https://github.com/vatesfr/xo-web/issues/601)
+- VDI move [\#591](https://github.com/vatesfr/xo-web/issues/591)
+- Edit pre-existing disk configuration during VM creation [\#589](https://github.com/vatesfr/xo-web/issues/589)
+- Allow disk size edition [\#587](https://github.com/vatesfr/xo-web/issues/587)
+- Better VDI resize support [\#585](https://github.com/vatesfr/xo-web/issues/585)
+- Remove manual VM export metadata in UI [\#580](https://github.com/vatesfr/xo-web/issues/580)
+- Support import VM metadata [\#579](https://github.com/vatesfr/xo-web/issues/579)
+- Set a default pool SR [\#572](https://github.com/vatesfr/xo-web/issues/572)
+- ISOs should be sorted by name [\#565](https://github.com/vatesfr/xo-web/issues/565)
+- Button to boot a VM from a disc once [\#564](https://github.com/vatesfr/xo-web/issues/564)
+- Ability to boot a PV VM from a disc [\#563](https://github.com/vatesfr/xo-web/issues/563)
+- Add an option to manually run backup jobs [\#562](https://github.com/vatesfr/xo-web/issues/562)
+- backups to unmounted storage [\#561](https://github.com/vatesfr/xo-web/issues/561)
+- Root integer properties cannot be edited in plugins configuration form [\#550](https://github.com/vatesfr/xo-web/issues/550)
+- Generic CloudConfig drive [\#549](https://github.com/vatesfr/xo-web/issues/549)
+- Auto-discovery of installed xo-server plugins [\#546](https://github.com/vatesfr/xo-web/issues/546)
+- Hide info on flat view [\#545](https://github.com/vatesfr/xo-web/issues/545)
+- Config plugin boolean properties must have a default value \(undefined prohibited\) [\#543](https://github.com/vatesfr/xo-web/issues/543)
+- Present detailed errors on plugin configuration failures [\#530](https://github.com/vatesfr/xo-web/issues/530)
+- Do not reset form on failures in plugins configuration [\#529](https://github.com/vatesfr/xo-web/issues/529)
+- XMPP alert plugin [\#518](https://github.com/vatesfr/xo-web/issues/518)
+- Hide tag adders depending on ACLs [\#516](https://github.com/vatesfr/xo-web/issues/516)
+- Choosing a framework for xo-web 5 [\#514](https://github.com/vatesfr/xo-web/issues/514)
+- Prevent adding a host in an existing XAPI connection [\#466](https://github.com/vatesfr/xo-web/issues/466)
+- Read only connection to Xen servers/pools [\#439](https://github.com/vatesfr/xo-web/issues/439)
+- generic notification system [\#391](https://github.com/vatesfr/xo-web/issues/391)
+- Data architecture review [\#384](https://github.com/vatesfr/xo-web/issues/384)
+- Make filtering easier to understand/add some "default" filters [\#207](https://github.com/vatesfr/xo-web/issues/207)
+- Improve performance [\#148](https://github.com/vatesfr/xo-web/issues/148)
+
+### Bug fixes
+
+- VM metadata export should not require a snapshot [\#615](https://github.com/vatesfr/xo-web/issues/615)
+- Missing patch for all hosts is continuously refreshed [\#609](https://github.com/vatesfr/xo-web/issues/609)
+- Backup import memory issue [\#608](https://github.com/vatesfr/xo-web/issues/608)
+- Host list missing patch is buggy [\#604](https://github.com/vatesfr/xo-web/issues/604)
+- Servers infos should not been refreshed while a field is being edited [\#595](https://github.com/vatesfr/xo-web/issues/595)
+- Servers list should not been re-order while a field is being edited [\#594](https://github.com/vatesfr/xo-web/issues/594)
+- Correctly display size in interface \(binary scale\) [\#592](https://github.com/vatesfr/xo-web/issues/592)
+- Display failures on VM boot order modification [\#560](https://github.com/vatesfr/xo-web/issues/560)
+- `vm.setBootOrder\(\)` should throw errors on failures \(non-HVM VMs\) [\#559](https://github.com/vatesfr/xo-web/issues/559)
+- Hide boot order form for non-HVM VMs [\#558](https://github.com/vatesfr/xo-web/issues/558)
+- Allow editing PV args even when empty \(but only for PV VMs\) [\#557](https://github.com/vatesfr/xo-web/issues/557)
+- Crashes when using legacy event system [\#556](https://github.com/vatesfr/xo-web/issues/556)
+- XenServer patches check error for 6.1 [\#555](https://github.com/vatesfr/xo-web/issues/555)
+- activation plugin xo-server-transport-email [\#553](https://github.com/vatesfr/xo-web/issues/553)
+- Server error with JSON on 32 bits Dom0 [\#552](https://github.com/vatesfr/xo-web/issues/552)
+- Cloud Config drive shouldn't be created on default SR [\#548](https://github.com/vatesfr/xo-web/issues/548)
+- Deep properties cannot be edited in plugins configuration form [\#521](https://github.com/vatesfr/xo-web/issues/521)
+- Aborted VM export should cancel the operation [\#490](https://github.com/vatesfr/xo-web/issues/490)
+- VM missing with same UUID after an inter-pool migration [\#284](https://github.com/vatesfr/xo-web/issues/284)
+
+## **4.10.0** (2015-11-27)
+
+Job management, email notifications, CoreOS/Docker, Quiesce snapshots...
+
+### Enhancements
+
+- Job management ([xo-web#487](https://github.com/vatesfr/xo-web/issues/487))
+- Patch upload on all connected servers ([xo-web#168](https://github.com/vatesfr/xo-web/issues/168))
+- Emergency shutdown ([xo-web#185](https://github.com/vatesfr/xo-web/issues/185))
+- CoreOS/docker template install ([xo-web#246](https://github.com/vatesfr/xo-web/issues/246))
+- Email for backups ([xo-web#308](https://github.com/vatesfr/xo-web/issues/308))
+- Console Clipboard ([xo-web#408](https://github.com/vatesfr/xo-web/issues/408))
+- Logs from CLI ([xo-web#486](https://github.com/vatesfr/xo-web/issues/486))
+- Save disconnected servers ([xo-web#489](https://github.com/vatesfr/xo-web/issues/489))
+- Snapshot with quiesce ([xo-web#491](https://github.com/vatesfr/xo-web/issues/491))
+- Start VM in reovery mode ([xo-web#495](https://github.com/vatesfr/xo-web/issues/495))
+- Username in logs ([xo-web#498](https://github.com/vatesfr/xo-web/issues/498))
+- Delete associated tokens with user ([xo-web#500](https://github.com/vatesfr/xo-web/issues/500))
+- Validate plugin configuration ([xo-web#503](https://github.com/vatesfr/xo-web/issues/503))
+- Avoid non configured plugins to be loaded ([xo-web#504](https://github.com/vatesfr/xo-web/issues/504))
+- Verbose API logs if configured ([xo-web#505](https://github.com/vatesfr/xo-web/issues/505))
+- Better backup overview ([xo-web#512](https://github.com/vatesfr/xo-web/issues/512))
+- VM auto power on ([xo-web#519](https://github.com/vatesfr/xo-web/issues/519))
+- Title property supported in config schema ([xo-web#522](https://github.com/vatesfr/xo-web/issues/522))
+- Start VM export only when necessary ([xo-web#534](https://github.com/vatesfr/xo-web/issues/534))
+- Input type should be number ([xo-web#538](https://github.com/vatesfr/xo-web/issues/538))
+
+### Bug fixes
+
+- Numbers/int support in plugins config ([xo-web#531](https://github.com/vatesfr/xo-web/issues/531))
+- Boolean support in plugins config ([xo-web#528](https://github.com/vatesfr/xo-web/issues/528))
+- Keyboard unusable outside console ([xo-web#513](https://github.com/vatesfr/xo-web/issues/513))
+- UsernameField for SAML ([xo-web#513](https://github.com/vatesfr/xo-web/issues/513))
+- Wrong display of "no plugin found" ([xo-web#508](https://github.com/vatesfr/xo-web/issues/508))
+- Bower build error ([xo-web#488](https://github.com/vatesfr/xo-web/issues/488))
+- VM cloning should require SR permission ([xo-web#472](https://github.com/vatesfr/xo-web/issues/472))
+- Xen tools status ([xo-web#471](https://github.com/vatesfr/xo-web/issues/471))
+- Can't delete ghost user ([xo-web#464](https://github.com/vatesfr/xo-web/issues/464))
+- Stats with old versions of Node ([xo-web#463](https://github.com/vatesfr/xo-web/issues/463))
+
+## **4.9.0** (2015-11-13)
+
+Automated DR, restore backup, VM copy
+
+### Enhancements
+
+- DR: schedule VM export on other host ([xo-web#447](https://github.com/vatesfr/xo-web/issues/447))
+- Scheduler logs ([xo-web#390](https://github.com/vatesfr/xo-web/issues/390) and [xo-web#477](https://github.com/vatesfr/xo-web/issues/477))
+- Restore backups ([xo-web#450](https://github.com/vatesfr/xo-web/issues/350))
+- Disable backup compression ([xo-web#467](https://github.com/vatesfr/xo-web/issues/467))
+- Copy VM to another SR (even remote) ([xo-web#475](https://github.com/vatesfr/xo-web/issues/475))
+- VM stats without time sync ([xo-web#460](https://github.com/vatesfr/xo-web/issues/460))
+- Stats perfs for high CPU numbers ([xo-web#461](https://github.com/vatesfr/xo-web/issues/461))
+
+### Bug fixes
+
+- Rolling backup bug ([xo-web#484](https://github.com/vatesfr/xo-web/issues/484))
+- vCPUs/CPUs inversion in dashboard ([xo-web#481](https://github.com/vatesfr/xo-web/issues/481))
+- Machine to template ([xo-web#459](https://github.com/vatesfr/xo-web/issues/459))
+
+### Misc
+
+- Console fix in XenServer ([xo-web#406](https://github.com/vatesfr/xo-web/issues/406))
+
+## **4.8.0** (2015-10-29)
+
+Fully automated patch system, ACLs inheritance, stats performance improved.
+
+### Enhancements
+
+- ACLs inheritance ([xo-web#279](https://github.com/vatesfr/xo-web/issues/279))
+- Patch automatically all missing updates ([xo-web#281](https://github.com/vatesfr/xo-web/issues/281))
+- Intelligent stats polling ([xo-web#432](https://github.com/vatesfr/xo-web/issues/432))
+- Cache latest result of stats request ([xo-web#431](https://github.com/vatesfr/xo-web/issues/431))
+- Improve stats polling on multiple objects ([xo-web#433](https://github.com/vatesfr/xo-web/issues/433))
+- Patch upload task should display the patch name ([xo-web#449](https://github.com/vatesfr/xo-web/issues/449))
+- Backup filename for Windows ([xo-web#448](https://github.com/vatesfr/xo-web/issues/448))
+- Specific distro icons ([xo-web#446](https://github.com/vatesfr/xo-web/issues/446))
+- PXE boot for HVM ([xo-web#436](https://github.com/vatesfr/xo-web/issues/436))
+- Favicon display before sign in ([xo-web#428](https://github.com/vatesfr/xo-web/issues/428))
+- Registration renewal ([xo-web#424](https://github.com/vatesfr/xo-web/issues/424))
+- Reconnect to the host if pool merge fails ([xo-web#403](https://github.com/vatesfr/xo-web/issues/403))
+- Avoid brute force login ([xo-web#339](https://github.com/vatesfr/xo-web/issues/339))
+- Missing FreeBSD icon ([xo-web#136](https://github.com/vatesfr/xo-web/issues/136))
+- Hide halted objects in the Health view ([xo-web#457](https://github.com/vatesfr/xo-web/issues/457))
+- Click on "Remember me" label ([xo-web#438](https://github.com/vatesfr/xo-web/issues/438))
+
+### Bug fixes
+
+- Pool patches in multiple pools not displayed ([xo-web#442](https://github.com/vatesfr/xo-web/issues/442))
+- VM Import crashes with Chrome ([xo-web#427](https://github.com/vatesfr/xo-web/issues/427))
+- Cannot open a direct link ([xo-web#371](https://github.com/vatesfr/xo-web/issues/371))
+- Patch display edge case ([xo-web#309](https://github.com/vatesfr/xo-web/issues/309))
+- VM snapshot should require user permission on SR ([xo-web#429](https://github.com/vatesfr/xo-web/issues/429))
+
+## **4.7.0** (2015-10-12)
+
+Plugin config management and browser notifications.
+
+### Enhancements
+
+- Plugin management in the web interface ([xo-web#352](https://github.com/vatesfr/xo-web/issues/352))
+- Browser notifications ([xo-web#402](https://github.com/vatesfr/xo-web/issues/402))
+- Graph selector ([xo-web#400](https://github.com/vatesfr/xo-web/issues/400))
+- Circle packing visualization ([xo-web#374](https://github.com/vatesfr/xo-web/issues/374))
+- Password generation ([xo-web#397](https://github.com/vatesfr/xo-web/issues/397))
+- Password reveal during user creation ([xo-web#396](https://github.com/vatesfr/xo-web/issues/396))
+- Add host to a pool ([xo-web#62](https://github.com/vatesfr/xo-web/issues/62))
+- Better modal when removing a host from a pool ([xo-web#405](https://github.com/vatesfr/xo-web/issues/405))
+- Drop focus on CD/ISO selector ([xo-web#290](https://github.com/vatesfr/xo-web/issues/290))
+- Allow non persistent session ([xo-web#243](https://github.com/vatesfr/xo-web/issues/243))
+
+### Bug fixes
+
+- VM export permission corrected ([xo-web#410](https://github.com/vatesfr/xo-web/issues/410))
+- Proper host removal in a pool ([xo-web#402](https://github.com/vatesfr/xo-web/issues/402))
+- Sub-optimal tooltip placement ([xo-web#421](https://github.com/vatesfr/xo-web/issues/421))
+- VM migrate host incorrect target ([xo-web#419](https://github.com/vatesfr/xo-web/issues/419))
+- Alone host can't leave its pool ([xo-web#414](https://github.com/vatesfr/xo-web/issues/414))
+
+## **4.6.0** (2015-09-25)
+
+Tags management and new visualization.
+
+### Enhancements
+
+- Multigraph for correlation ([xo-web#358](https://github.com/vatesfr/xo-web/issues/358))
+- Tags management ([xo-web#367](https://github.com/vatesfr/xo-web/issues/367))
+- Google Provider for authentication ([xo-web#363](https://github.com/vatesfr/xo-web/issues/363))
+- Password change for users ([xo-web#362](https://github.com/vatesfr/xo-web/issues/362))
+- Better live migration process ([xo-web#237](https://github.com/vatesfr/xo-web/issues/237))
+- VDI search filter in SR view ([xo-web#222](https://github.com/vatesfr/xo-web/issues/222))
+- PV args during VM creation ([xo-web#112](https://github.com/vatesfr/xo-web/issues/330))
+- PV args management ([xo-web#394](https://github.com/vatesfr/xo-web/issues/394))
+- Confirmation dialog on important actions ([xo-web#350](https://github.com/vatesfr/xo-web/issues/350))
+- New favicon ([xo-web#369](https://github.com/vatesfr/xo-web/issues/369))
+- Filename of VM for exports ([xo-web#370](https://github.com/vatesfr/xo-web/issues/370))
+- ACLs rights edited on the fly ([xo-web#323](https://github.com/vatesfr/xo-web/issues/323))
+- Heatmap values now human readable ([xo-web#342](https://github.com/vatesfr/xo-web/issues/342))
+
+### Bug fixes
+
+- Export backup fails if no tags specified ([xo-web#383](https://github.com/vatesfr/xo-web/issues/383))
+- Wrong login give an obscure error message ([xo-web#373](https://github.com/vatesfr/xo-web/issues/373))
+- Update view is broken during updates ([xo-web#356](https://github.com/vatesfr/xo-web/issues/356))
+- Settings/dashboard menu incorrect display ([xo-web#357](https://github.com/vatesfr/xo-web/issues/357))
+- Console View Not refreshing if the VM restart ([xo-web#107](https://github.com/vatesfr/xo-web/issues/107))
+
+## **4.5.1** (2015-09-16)
+
+An issue in `xo-web` with the VM view.
+
+### Bug fix
+
+- Attach disk/new disk/create interface is broken ([xo-web#378](https://github.com/vatesfr/xo-web/issues/378))
+
+## **4.5.0** (2015-09-11)
+
+A new dataviz (parallel coord), a new provider (GitHub) and faster consoles.
+
+### Enhancements
+
+- Parallel coordinates view ([xo-web#333](https://github.com/vatesfr/xo-web/issues/333))
+- Faster consoles ([xo-web#337](https://github.com/vatesfr/xo-web/issues/337))
+- Disable/hide button ([xo-web#268](https://github.com/vatesfr/xo-web/issues/268))
+- More details on missing-guest-tools ([xo-web#304](https://github.com/vatesfr/xo-web/issues/304))
+- Scheduler meta data export ([xo-web#315](https://github.com/vatesfr/xo-web/issues/315))
+- Better heatmap ([xo-web#330](https://github.com/vatesfr/xo-web/issues/330))
+- Faster dashboard ([xo-web#331](https://github.com/vatesfr/xo-web/issues/331))
+- Faster sunburst ([xo-web#332](https://github.com/vatesfr/xo-web/issues/332))
+- GitHub provider for auth ([xo-web#334](https://github.com/vatesfr/xo-web/issues/334))
+- Filter networks for users ([xo-web#347](https://github.com/vatesfr/xo-web/issues/347))
+- Add networks in ACLs ([xo-web#348](https://github.com/vatesfr/xo-web/issues/348))
+- Better looking login page ([xo-web#341](https://github.com/vatesfr/xo-web/issues/341))
+- Real time dataviz (dashboard) ([xo-web#349](https://github.com/vatesfr/xo-web/issues/349))
+
+### Bug fixes
+
+- Typo in dashboard ([xo-web#355](https://github.com/vatesfr/xo-web/issues/355))
+- Global RAM usage fix ([xo-web#356](https://github.com/vatesfr/xo-web/issues/356))
+- Re-allowing XO behind a reverse proxy ([xo-web#361](https://github.com/vatesfr/xo-web/issues/361))
+
+## **4.4.0** (2015-08-28)
+
+SSO and Dataviz are the main features for this release.
+
+### Enhancements
+
+- Dataviz storage usage ([xo-web#311](https://github.com/vatesfr/xo-web/issues/311))
+- Heatmap in health view ([xo-web#329](https://github.com/vatesfr/xo-web/issues/329))
+- SSO for SAML and other providers ([xo-web#327](https://github.com/vatesfr/xo-web/issues/327))
+- Better UI for ACL objects attribution ([xo-web#320](https://github.com/vatesfr/xo-web/issues/320))
+- Refresh the browser after an update ([xo-web#318](https://github.com/vatesfr/xo-web/issues/318))
+- Clean CSS and Flexbox usage ([xo-web#239](https://github.com/vatesfr/xo-web/issues/239))
+
+### Bug fixes
+
+- Admin only accessible views ([xo-web#328](https://github.com/vatesfr/xo-web/issues/328))
+- Hide "base copy" VDIs ([xo-web#324](https://github.com/vatesfr/xo-web/issues/324))
+- ACLs on VIFs for non-admins ([xo-web#322](https://github.com/vatesfr/xo-web/issues/322))
+- Updater display problems ([xo-web#313](https://github.com/vatesfr/xo-web/issues/313))
+
+## **4.3.0** (2015-07-22)
+
+Scheduler for rolling backups
+
+### Enhancements
+
+- Rolling backup scheduler ([xo-web#278](https://github.com/vatesfr/xo-web/issues/278))
+- Clean snapshots of removed VMs ([xo-web#301](https://github.com/vatesfr/xo-web/issues/301))
+
+### Bug fixes
+
+- VM export ([xo-web#307](https://github.com/vatesfr/xo-web/issues/307))
+- Remove VM VDIs ([xo-web#303](https://github.com/vatesfr/xo-web/issues/303))
+- Pagination fails ([xo-web#302](https://github.com/vatesfr/xo-web/issues/302))
+
+## **4.2.0** (2015-06-29)
+
+Huge performance boost, scheduler for rolling snapshots and backward compatibility for XS 5.x series
+
+### Enhancements
+
+- Rolling snapshots scheduler ([xo-web#176](https://github.com/vatesfr/xo-web/issues/176))
+- Huge perf boost ([xen-api#1](https://github.com/julien-f/js-xen-api/issues/1))
+- Backward compatibility ([xo-web#296](https://github.com/vatesfr/xo-web/issues/296))
+
+### Bug fixes
+
+- VDI attached on a VM missing in SR view ([xo-web#294](https://github.com/vatesfr/xo-web/issues/294))
+- Better VM creation process ([xo-web#292](https://github.com/vatesfr/xo-web/issues/292))
+
+## **4.1.0** (2015-06-10)
+
+Add the drag'n drop support from VM live migration, better ACLs groups UI.
+
+### Enhancements
+
+- Drag'n drop VM in tree view for live migration ([xo-web#277](https://github.com/vatesfr/xo-web/issues/277))
+- Better group view with objects ACLs ([xo-web#276](https://github.com/vatesfr/xo-web/issues/276))
+- Hide non-visible objects ([xo-web#272](https://github.com/vatesfr/xo-web/issues/272))
+
+### Bug fixes
+
+- Convert to template displayed when the VM is not halted ([xo-web#286](https://github.com/vatesfr/xo-web/issues/286))
+- Lost some data when refresh some views ([xo-web#271](https://github.com/vatesfr/xo-web/issues/271))
+- Suspend button don't trigger any permission message ([xo-web#270](https://github.com/vatesfr/xo-web/issues/270))
+- Create network interfaces shouldn't call xoApi directly ([xo-web#269](https://github.com/vatesfr/xo-web/issues/269))
+- Don't plug automatically a disk or a VIF if the VM is not running ([xo-web#287](https://github.com/vatesfr/xo-web/issues/287))
+
+## **4.0.2** (2015-06-01)
+
+An issue in `xo-server` with the password of default admin account and also a UI fix.
+
+### Bug fixes
+
+- Cannot modify admin account ([xo-web#265](https://github.com/vatesfr/xo-web/issues/265))
+- Password field seems to keep empty/reset itself after 1-2 seconds ([xo-web#264](https://github.com/vatesfr/xo-web/issues/264))
+
+## **4.0.1** (2015-05-30)
+
+An issue with the updater in HTTPS was left in the *4.0.0*. This patch release fixed
+it.
+
+### Bug fixes
+
+- allow updater to work in HTTPS ([xo-web#266](https://github.com/vatesfr/xo-web/issues/266))
+
+## **4.0.0** (2015-05-29)
+
+[Blog post of this release](https://xen-orchestra.com/blog/xen-orchestra-4-0).
+
+### Enhancements
+
+- advanced ACLs ([xo-web#209](https://github.com/vatesfr/xo-web/issues/209))
+- xenserver update management ([xo-web#174](https://github.com/vatesfr/xo-web/issues/174) & [xo-web#259](https://github.com/vatesfr/xo-web/issues/259))
+- docker control ([xo-web#211](https://github.com/vatesfr/xo-web/issues/211))
+- better responsive design ([xo-web#252](https://github.com/vatesfr/xo-web/issues/252))
+- host stats ([xo-web#255](https://github.com/vatesfr/xo-web/issues/255))
+- pagination ([xo-web#221](https://github.com/vatesfr/xo-web/issues/221))
+- web updater
+- better VM creation process([xo-web#256](https://github.com/vatesfr/xo-web/issues/256))
+- VM boot order([xo-web#251](https://github.com/vatesfr/xo-web/issues/251))
+- new mapped collection([xo-server#47](https://github.com/vatesfr/xo-server/issues/47))
+- resource location in ACL view ([xo-web#245](https://github.com/vatesfr/xo-web/issues/245))
+
+### Bug fixes
+
+- wrong calulation of RAM amounts ([xo-web#51](https://github.com/vatesfr/xo-web/issues/51))
+- checkbox not aligned ([xo-web#253](https://github.com/vatesfr/xo-web/issues/253))
+- VM stats behavior more robust ([xo-web#250](https://github.com/vatesfr/xo-web/issues/250))
+- XO not on the root of domain ([xo-web#254](https://github.com/vatesfr/xo-web/issues/254))
+
+
+## **3.9.1** (2015-04-21)
+
+A few bugs hve made their way into *3.9.0*, this minor release fixes
+them.
+
+### Bug fixes
+
+- correctly keep the VM guest metrics up to date ([xo-web#172](https://github.com/vatesfr/xo-web/issues/172))
+- fix edition of a VM snapshot ([b04111c](https://github.com/vatesfr/xo-server/commit/b04111c79ba8937778b84cb861bb7c2431162c11))
+- do not fetch stats if the VM state is transitioning ([a5c9880](https://github.com/vatesfr/xo-web/commit/a5c98803182792d2fe5ceb840ae1e23a8b767923))
+- fix broken Angular due to new version of Babel which breaks ngAnnotate ([0a9c868](https://github.com/vatesfr/xo-web/commit/0a9c868678d239e5b3e54b4d2bc3bd14b5400120))
+- feedback when connecting/disconnecting a server ([027d1e8](https://github.com/vatesfr/xo-web/commit/027d1e8cb1f2431e67042e1eec51690b2bc54ad7))
+- clearer error message if a server is unreachable ([06ca007](https://github.com/vatesfr/xo-server/commit/06ca0079b321e757aaa4112caa6f92a43193e35d))
+
+## **3.9.0** (2015-04-20)
+
+[Blog post of this release](https://xen-orchestra.com/blog/xen-orchestra-3-9).
+
+### Enhancements
+
+- ability to manually connect/disconnect a server ([xo-web#88](https://github.com/vatesfr/xo-web/issues/88) & [xo-web#234](https://github.com/vatesfr/xo-web/issues/234))
+- display the connection status of a server ([xo-web#103](https://github.com/vatesfr/xo-web/issues/103))
+- better feedback when connecting to a server ([xo-web#210](https://github.com/vatesfr/xo-web/issues/210))
+- ability to add a local LVM SR ([xo-web#219](https://github.com/vatesfr/xo-web/issues/219))
+- display virtual GPUs in VM view ([xo-web#223](https://github.com/vatesfr/xo-web/issues/223))
+- ability to automatically start a VM with its host ([xo-web#224](https://github.com/vatesfr/xo-web/issues/224))
+- ability to create networks ([xo-web#225](https://github.com/vatesfr/xo-web/issues/225))
+- live charts for a VM CPU/disk/network & RAM ([xo-web#228](https://github.com/vatesfr/xo-web/issues/228) & [xo-server#51](https://github.com/vatesfr/xo-server/issues/51))
+- remove VM import progress notifications (redundant with the tasks list) ([xo-web#235](https://github.com/vatesfr/xo-web/issues/235))
+- XO-Server sources are compiled to JS prior distribution: less bugs & faster startups ([xo-server#50](https://github.com/vatesfr/xo-server/issues/50))
+- use XAPI `event.from()` instead of `event.next()` which leads to faster connection ([xo-server#52](https://github.com/vatesfr/xo-server/issues/52))
+
+### Bug fixes
+
+- removed servers are properly disconnected ([xo-web#61](https://github.com/vatesfr/xo-web/issues/61))
+- fix VM creation with multiple interfaces ([xo-wb#229](https://github.com/vatesfr/xo-wb/issues/229))
+- disconnected servers are properly removed from the interface ([xo-web#234](https://github.com/vatesfr/xo-web/issues/234))
+
+## **3.8.0** (2015-03-27)
+
+[Blog post of this release](https://xen-orchestra.com/blog/xen-orchestra-3-8).
+
+### Enhancements
+
+- initial plugin system ([xo-server#37](https://github.com/vatesfr/xo-server/issues/37))
+- new authentication system based on providers ([xo-server#39](https://github.com/vatesfr/xo-server/issues/39))
+- LDAP authentication plugin for XO-Server ([xo-server#40](https://github.com/vatesfr/xo-server/issues/40))
+- disk creation on the VM page ([xo-web#215](https://github.com/vatesfr/xo-web/issues/215))
+- network creation on the VM page ([xo-web#216](https://github.com/vatesfr/xo-web/issues/216))
+- charts on the host and SR pages ([xo-web#217](https://github.com/vatesfr/xo-web/issues/217))
+
+### Bug fixes
+
+- fix *Invalid parameter(s)* message on the settings page ([xo-server#49](https://github.com/vatesfr/xo-server/issues/49))
+- fix mouse clicks in console ([xo-web#205](https://github.com/vatesfr/xo-web/issues/205))
+- fix user editing on the settings page ([xo-web#206](https://github.com/vatesfr/xo-web/issues/206))
+
+## **3.7.0** (2015-03-06)
+
+*Highlights in this release are the [initial ACLs implementation](https://xen-orchestra.com/blog/xen-orchestra-3-7-is-out-acls-in-early-access), [live migration for VDIs](https://xen-orchestra.com/blog/moving-vdi-in-live) and the ability to [create a new storage repository](https://xen-orchestra.com/blog/create-a-storage-repository-with-xen-orchestra/).*
+
+### Enhancements
+
+- ability to live migrate a VM between hosts with different CPUs ([xo-web#126](https://github.com/vatesfr/xo-web/issues/126))
+- ability to live migrate a VDI ([xo-web#177](https://github.com/vatesfr/xo-web/issues/177))
+- display a notification on VM creation ([xo-web#178](https://github.com/vatesfr/xo-web/issues/178))
+- ability to create/attach a iSCSI/NFS/ISO SR ([xo-web#179](https://github.com/vatesfr/xo-web/issues/179))
+- display SR available space on VM creation ([xo-web#180](https://github.com/vatesfr/xo-web/issues/180))
+- ability to enable and disable host on the tree view ([xo-web#181](https://github.com/vatesfr/xo-web/issues/181) & [xo-web#182](https://github.com/vatesfr/xo-web/issues/182))
+- ability to suspend/resume a VM ([xo-web#186](https://github.com/vatesfr/xo-web/issues/186))
+- display Linux icon for SUSE Linux Enterprise Server distribution ([xo-web#187](https://github.com/vatesfr/xo-web/issues/187))
+- correctly handle incorrectly formated token in cookies ([xo-web#192](https://github.com/vatesfr/xo-web/issues/192))
+- display host manufacturer in host view ([xo-web#195](https://github.com/vatesfr/xo-web/issues/195))
+- only display task concerning authorized objects ([xo-web#197](https://github.com/vatesfr/xo-web/issues/197))
+- better welcome message ([xo-web#199](https://github.com/vatesfr/xo-web/issues/199))
+- initial ACLs ([xo-web#202](https://github.com/vatesfr/xo-web/issues/202))
+- display an action panel to rescan, remove, attach and forget a SR ([xo-web#203](https://github.com/vatesfr/xo-web/issues/203))
+- display current active tasks in navbar ([xo-web#204](https://github.com/vatesfr/xo-web/issues/204))
+
+### Bug fixes
+
+- implements a proxy which fixes consoles over HTTPs ([xo#14](https://github.com/vatesfr/xo/issues/14))
+- fix tasks listing in host view ([xo-server#43](https://github.com/vatesfr/xo-server/issues/43))
+- fix console view on IE ([xo-web#184](https://github.com/vatesfr/xo-web/issues/184))
+- fix out of sync objects in XO-Web ([xo-web#142](https://github.com/vatesfr/xo-web/issues/142))
+- fix incorrect connection status displayed in login view ([xo-web#193](https://github.com/vatesfr/xo-web/issues/193))
+- fix *flickering* tree view ([xo-web#194](https://github.com/vatesfr/xo-web/issues/194))
+- single host pools should not have a dropdown menu in tree view ([xo-web#198](https://github.com/vatesfr/xo-web/issues/198))
+
+## **3.6.0** (2014-11-28)
+
+### Enhancements
+
+- upload and apply patches to hosts/pools ([xo-web#150](https://github.com/vatesfr/xo-web/issues/150))
+- import VMs ([xo-web#151](https://github.com/vatesfr/xo-web/issues/151))
+- export VMs ([xo-web#152](https://github.com/vatesfr/xo-web/issues/152))
+- migrate VMs to another pool ([xo-web#153](https://github.com/vatesfr/xo-web/issues/153))
+- display pool even for single host ([xo-web#155](https://github.com/vatesfr/xo-web/issues/155))
+- start halted hosts with wake-on-LAN ([xo-web#154](https://github.com/vatesfr/xo-web/issues/154))
+- list of uploaded/applied patches ([xo-web#156](https://github.com/vatesfr/xo-web/issues/156))
+- use Angular 1.3 from npm ([xo-web#157](https://github.com/vatesfr/xo-web/issues/157) & [xo-web#160](https://github.com/vatesfr/xo-web/issues/160))
+- more feedbacks on actions ([xo-web#165](https://github.com/vatesfr/xo-web/issues/165))
+- only buttons compatible with VM states are displayed ([xo-web#166](https://github.com/vatesfr/xo-web/issues/166))
+- export VM snapshot ([xo-web#167](https://github.com/vatesfr/xo-web/issues/167))
+- plug/unplug a SR to a host ([xo-web#169](https://github.com/vatesfr/xo-web/issues/169))
+- plug a SR to all available hosts ([xo-web#170](https://github.com/vatesfr/xo-web/issues/170))
+- disks editing on SR page ([xo-web#171](https://github.com/vatesfr/xo-web/issues/171))
+- export of running VMs ([xo-server#36](https://github.com/vatesfr/xo-server/issues/36))
+
+### Bug fixes
+
+- disks editing on VM page should work ([xo-web#86](https://github.com/vatesfr/xo-web/issues/86))
+- dropdown menus should close after selecting an item ([xo-web#140](https://github.com/vatesfr/xo-web/issues/140))
+- user creation should require a password ([xo-web#143](https://github.com/vatesfr/xo-web/issues/143))
+- server connection should require a user and a password ([xo-web#144](https://github.com/vatesfr/xo-web/issues/144))
+- snapshot deletion should work ([xo-server#147](https://github.com/vatesfr/xo-server/issues/147))
+- VM console should work in Chrome ([xo-web#149](https://github.com/vatesfr/xo-web/issues/149))
+- tooltips should work ([xo-web#163](https://github.com/vatesfr/xo-web/issues/163))
+- disk plugged status should be automatically refreshed ([xo-web#164](https://github.com/vatesfr/xo-web/issues/164))
+- deleting users without tokens should not trigger an error ([xo-server#34](https://github.com/vatesfr/xo-server/issues/34))
+- live pool master change should work ([xo-server#20](https://github.com/vatesfr/xo-server/issues/20))
+
+## **3.5.1** (2014-08-19)
+
+### Bug fixes
+
+- pool view works again ([#139](https://github.com/vatesfr/xo-web/issues/139))
+
+## **3.5.0** (2014-08-14)
+
+*[XO-Web](https://www.npmjs.org/package/xo-web) and [XO-Server](https://www.npmjs.org/package/xo-server) are now available as npm packages!*
+
+### Enhancements
+
+- XO-Server published on npm ([#26](https://github.com/vatesfr/xo-server/issues/26))
+- XO-Server config is now in `/etc/xo-server/config.yaml` ([#33](https://github.com/vatesfr/xo-server/issues/33))
+- paths in XO-Server's config are now relative to the config file ([#19](https://github.com/vatesfr/xo-server/issues/19))
+- use the Linux icon for Fedora ([#131](https://github.com/vatesfr/xo-web/issues/131))
+- start/stop/reboot buttons on console page ([#121](https://github.com/vatesfr/xo-web/issues/121))
+- settings page now only accessible to admin ([#77](https://github.com/vatesfr/xo-web/issues/77))
+- redirection to the home page when a VM is deleted from its own page ([#56](https://github.com/vatesfr/xo-web/issues/56))
+- XO-Web published on npm ([#123](https://github.com/vatesfr/xo-web/issues/123))
+- buid process now use Browserify (([#125](https://github.com/vatesfr/xo-web/issues/125), [#135](https://github.com/vatesfr/xo-web/issues/135)))
+- view are now written in Jade instead of HTML ([#124](https://github.com/vatesfr/xo-web/issues/124))
+- CSS autoprefixer to improve compatibility ([#132](https://github.com/vatesfr/xo-web/issues/132), [#137](https://github.com/vatesfr/xo-web/issues/137))
+
+### Bug fixes
+
+- force shutdown does not attempt a clean shutdown first anymore ([#29](https://github.com/vatesfr/xo-server/issues/29))
+- shutdown hosts are now correctly reported as such ([#31](https://github.com/vatesfr/xo-web/issues/31))
+- incorrect VM metrics ([#54](https://github.com/vatesfr/xo-web/issues/54), [#68](https://github.com/vatesfr/xo-web/issues/68), [#108](https://github.com/vatesfr/xo-web/issues/108))
+- an user cannot delete itself ([#104](https://github.com/vatesfr/xo-web/issues/104))
+- in VM creation, required fields are now marked as such ([#113](https://github.com/vatesfr/xo-web/issues/113), [#114](https://github.com/vatesfr/xo-web/issues/114))
+
+## **3.4.0** (2014-05-22)
+
+*Highlight in this release is the new events system between XO-Web
+and XO-Server which results in less bandwidth consumption as well as
+better performance and reactivity.*
+
+### Enhancements
+
+- events system between XO-Web and XO-Server ([#52](https://github.com/vatesfr/xo-web/issues/52))
+- ability to clone/copy a VM ([#116](https://github.com/vatesfr/xo-web/issues/116))
+- mandatory log in page ([#120](https://github.com/vatesfr/xo-web/issues/120))
+
+### Bug fixes
+
+- failure in VM creation ([#111](https://github.com/vatesfr/xo-web/issues/111))
+
+## **3.3.1** (2014-03-28)
+
+### Enhancements
+
+- console view is now prettier ([#92](https://github.com/vatesfr/xo-web/issues/92))
+
+### Bug fixes
+
+- VM creation fails to incorrect dependencies ([xo-server/#24](https://github.com/vatesfr/xo-server/issues/24))
+- VDIs list in SR view is blinking ([#109](https://github.com/vatesfr/xo-web/issues/109))
+
+## **3.3.0** (2014-03-07)
+
+### Enhancements
+
+- [Grunt](http://gruntjs.com/) has been replaced by [gulp.js](http://gulpjs.com/) ([#91](https://github.com/vatesfr/xo-web/issues/91))
+- a host can be detached from a pool ([#98](https://github.com/vatesfr/xo-web/issues/98))
+- snapshots management in VM view ([#99](https://github.com/vatesfr/xo-web/issues/99))
+- log deletion in VM view ([#100](https://github.com/vatesfr/xo-web/issues/100))
+
+### Bug fixes
+
+- *Snapshot* not working in VM view ([#95](https://github.com/vatesfr/xo-web/issues/95))
+- Host *Reboot*/*Restart toolstack*/*Shutdown* not working in main view ([#97](https://github.com/vatesfr/xo-web/issues/97))
+- Bower cannot install `angular` automatically due to a version conflict ([#101](https://github.com/vatesfr/xo-web/issues/101))
+- Bower installs an incorrect version of `angular-animate` ([#102](https://github.com/vatesfr/xo-web/issues/102))
+
+## **3.2.0** (2014-02-21)
+
+### Enhancements
+
+- dependencies' versions should be fixed to ease deployment ([#93](https://github.com/vatesfr/xo-web/issues/93))
+- badges added to the README to see whether dependencies are up to date ([#90](https://github.com/vatesfr/xo-web/issues/90))
+- an error notification has been added when the connection to XO-Server failed ([#89](https://github.com/vatesfr/xo-web/issues/89))
+- in host view, there is now a link to the host console ([#87](https://github.com/vatesfr/xo-web/issues/87))
+- in VM view, deleting a disk requires a confirmation ([#85](https://github.com/vatesfr/xo-web/issues/85))
+- the VM and console icons are now different ([#80](https://github.com/vatesfr/xo-web/issues/80))
+
+### Bug fixes
+
+- consoles now work in Google Chrome \o/ ([#46](https://github.com/vatesfr/xo-web/issues/46))
+- in host view, many buttons were not working ([#79](https://github.com/vatesfr/xo-web/issues/79))
+- in main view, incorrect icons were fixes ([#81](https://github.com/vatesfr/xo-web/issues/81))
+- MAC addresses should not be ignored during VM creation ([#94](https://github.com/vatesfr/xo-web/issues/94))
+
+## **3.1.0** (2014-02-14)
+
+### Enhancements
+
+- in VM view, interfaces' network should be displayed ([#64](https://github.com/vatesfr/xo-web/issues/64))
+- middle-click or `Ctrl`+click should open new windows (even on pseudo-links) ([#66](https://github.com/vatesfr/xo-web/issues/66))
+- lists should use natural sorting (e.g. *VM 2* before *VM 10*) ([#69](https://github.com/vatesfr/xo-web/issues/69))
+
+### Bug fixes
+
+- consoles are not implemented for hosts ([#57](https://github.com/vatesfr/xo-web/issues/57))
+- it makes no sense to remove a stand-alone host from a pool (58)
+- in VM view, the migrate button is not working ([#59](https://github.com/vatesfr/xo-web/issues/59))
+- pool and host names overflow their box in the main view ([#63](https://github.com/vatesfr/xo-web/issues/63))
+- in host view, interfaces incorrectly named *networks* and VLAN not shown ([#70](https://github.com/vatesfr/xo-web/issues/70))
+- VM suspended state is not properly handled ([#71](https://github.com/vatesfr/xo-web/issues/71))
+- unauthenticated users should not be able to access to consoles ([#73](https://github.com/vatesfr/xo-web/issues/73))
+- incorrect scroll (under the navbar) when the view changes ([#74](https://github.com/vatesfr/xo-web/issues/74))
diff --git a/packages/xo-web/ISSUE_TEMPLATE.md b/packages/xo-web/ISSUE_TEMPLATE.md
new file mode 100644
index 000000000..0b71d6d03
--- /dev/null
+++ b/packages/xo-web/ISSUE_TEMPLATE.md
@@ -0,0 +1,28 @@
+
+
+### Context
+
+- **XO version**: XO appliance / `stable` branch / `next-release` branch
+
+If from the sources:
+
+- **Component**: xo-web / xo-server / *unknown*
+- **Node/npm version**: *just execute `npm version`*
+
+### Expected behavior
+
+
+
+### Current behavior
+
+
diff --git a/packages/xo-web/LICENSE b/packages/xo-web/LICENSE
new file mode 100644
index 000000000..dbbe35581
--- /dev/null
+++ b/packages/xo-web/LICENSE
@@ -0,0 +1,661 @@
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU Affero General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+ .
diff --git a/packages/xo-web/README.md b/packages/xo-web/README.md
new file mode 100644
index 000000000..c0b400921
--- /dev/null
+++ b/packages/xo-web/README.md
@@ -0,0 +1,91 @@
+# Xen Orchestra Web [](https://go.crisp.im/chat/embed/?website_id=-JzqzzwddSV7bKGtEyAQ) [](https://travis-ci.org/vatesfr/xo-web)
+
+
+
+XO-Web is part of [Xen Orchestra](https://github.com/vatesfr/xo), a web interface for XenServer or XAPI enabled hosts.
+
+It is a web client for [XO-Server](https://github.com/vatesfr/xo-server).
+
+___
+
+## Installation
+
+XOA or manual install procedure is [available here](https://xen-orchestra.com/docs/installation.html)
+
+## Compilation
+
+Production build:
+
+```
+$ npm run build
+```
+
+Development build:
+
+```
+$ npm run dev
+```
+
+### Environment
+
+#### `NODE_ENV`
+
+Set to *production* it disables many checks which result in increased
+performance.
+
+#### `XOA_PLAN`
+
+- 1: Free
+- 2: Starter
+- 3: Enterprise
+- 4: Premium
+- 5: Sources
+
+```js
+if (process.env.XOA_PLAN < 5) {
+ console.log('included only in XOA')
+}
+
+if (process.env.XOA_PLAN > 3) {
+ console.log('included only in Premium and Sources')
+}
+```
+
+## How to report a bug?
+
+If you are certain the bug is exclusively related to XO-Web, you may use the [bugtracker of this repository](https://github.com/vatesfr/xo-web/issues).
+
+Otherwise, please consider using the [bugtracker of the general repository](https://github.com/vatesfr/xo/issues).
+
+## Process for new release
+
+```bash
+# Switch to the stable branch.
+git checkout stable
+
+# Fetches latest changes.
+git pull --ff-only
+
+# Merge changes of the next-release branch.
+git merge next-release
+
+# Increment the version (patch, minor or major).
+npm version minor
+
+# Go back to the next-release branch.
+git checkout next-release
+
+# Fetches the last changes (the merge and version bump) from stable to
+# next-release.
+git merge --ff-only stable
+
+# Push the changes on git.
+git push --follow-tags origin stable next-release
+
+# Publish this release to npm.
+npm publish
+```
+
+## License
+
+AGPL3 © [Vates SAS](http://vates.fr)
diff --git a/packages/xo-web/gulpfile.js b/packages/xo-web/gulpfile.js
new file mode 100644
index 000000000..94ee77124
--- /dev/null
+++ b/packages/xo-web/gulpfile.js
@@ -0,0 +1,303 @@
+'use strict'
+
+// ===================================================================
+
+const SRC_DIR = __dirname + '/src' // eslint-disable-line no-path-concat
+const DIST_DIR = __dirname + '/dist' // eslint-disable-line no-path-concat
+
+// Port to use for the livereload server.
+//
+// It must be available and if possible unique to not conflict with other projects.
+// http://www.random.org/integers/?num=1&min=1024&max=65535&col=1&base=10&format=plain&rnd=new
+const LIVERELOAD_PORT = 26242
+
+const PRODUCTION = process.env.NODE_ENV === 'production'
+const DEVELOPMENT = !PRODUCTION
+
+if (!process.env.XOA_PLAN) {
+ process.env.XOA_PLAN = '5' // Open Source
+}
+
+// ===================================================================
+
+const gulp = require('gulp')
+
+// ===================================================================
+
+function lazyFn (factory) {
+ let fn = function () {
+ fn = factory()
+ return fn.apply(this, arguments)
+ }
+
+ return function () {
+ return fn.apply(this, arguments)
+ }
+}
+
+// -------------------------------------------------------------------
+
+const livereload = lazyFn(function () {
+ const livereload = require('gulp-refresh')
+ livereload.listen({
+ port: LIVERELOAD_PORT,
+ })
+
+ return livereload
+})
+
+const pipe = lazyFn(function () {
+ let current
+ function pipeCore (streams) {
+ let i, n, stream
+ for (i = 0, n = streams.length; i < n; ++i) {
+ stream = streams[i]
+ if (!stream) {
+ // Nothing to do
+ } else if (stream instanceof Array) {
+ pipeCore(stream)
+ } else {
+ current = current ? current.pipe(stream) : stream
+ }
+ }
+ }
+
+ const push = Array.prototype.push
+ return function (streams) {
+ try {
+ if (!(streams instanceof Array)) {
+ streams = []
+ push.apply(streams, arguments)
+ }
+
+ pipeCore(streams)
+
+ return current
+ } finally {
+ current = null
+ }
+ }
+})
+
+const resolvePath = lazyFn(function () {
+ return require('path').resolve
+})
+
+// -------------------------------------------------------------------
+
+// Similar to `gulp.src()` but the pattern is relative to `SRC_DIR`
+// and files are automatically watched when not in production mode.
+const src = lazyFn(function () {
+ function resolve (path) {
+ return path ? resolvePath(SRC_DIR, path) : SRC_DIR
+ }
+
+ return PRODUCTION
+ ? function src (pattern, opts) {
+ const base = resolve(opts && opts.base)
+
+ return gulp.src(pattern, {
+ base: base,
+ cwd: base,
+ passthrough: opts && opts.passthrough,
+ sourcemaps: opts && opts.sourcemaps,
+ })
+ }
+ : function src (pattern, opts) {
+ const base = resolve(opts && opts.base)
+
+ return pipe(
+ gulp.src(pattern, {
+ base: base,
+ cwd: base,
+ passthrough: opts && opts.passthrough,
+ sourcemaps: opts && opts.sourcemaps,
+ }),
+ require('gulp-watch')(pattern, {
+ base: base,
+ cwd: base,
+ }),
+ require('gulp-plumber')()
+ )
+ }
+})
+
+// Similar to `gulp.dest()` but the output directory is relative to
+// `DIST_DIR` and default to `./`, and files are automatically live-
+// reloaded when not in production mode.
+const dest = lazyFn(function () {
+ function resolve (path) {
+ return path ? resolvePath(DIST_DIR, path) : DIST_DIR
+ }
+
+ const opts = {
+ sourcemaps: '.',
+ }
+
+ return PRODUCTION
+ ? function dest (path) {
+ return gulp.dest(resolve(path), opts)
+ }
+ : function dest (path) {
+ const stream = gulp.dest(resolve(path), opts)
+ stream.pipe(livereload())
+ return stream
+ }
+})
+
+// ===================================================================
+
+function browserify (path, opts) {
+ if (opts == null) {
+ opts = {}
+ }
+
+ let bundler = require('browserify')(path, {
+ basedir: SRC_DIR,
+ debug: true,
+ extensions: opts.extensions,
+ fullPaths: false,
+ paths: SRC_DIR + '/common',
+ standalone: opts.standalone,
+
+ // Required by Watchify.
+ cache: {},
+ packageCache: {},
+ })
+
+ const plugins = opts.plugins
+ for (let i = 0, n = plugins && plugins.length; i < n; ++i) {
+ const plugin = plugins[i]
+ bundler.plugin(require(plugin[0]), plugin[1])
+ }
+
+ if (PRODUCTION) {
+ // FIXME: does not work with react-intl (?!)
+ // bundler.plugin('bundle-collapser/plugin')
+ } else {
+ bundler = require('watchify')(bundler, {
+ // do not watch in `node_modules`
+ // https://github.com/browserify/watchify#options
+ ignoreWatch: true,
+ })
+ }
+
+ // Append the extension if necessary.
+ if (!/\.js$/.test(path)) {
+ path += '.js'
+ }
+ path = resolvePath(SRC_DIR, path)
+
+ let stream = new (require('readable-stream'))({
+ objectMode: true,
+ })
+
+ let write
+ function bundle () {
+ bundler.bundle(function onBundle (error, buffer) {
+ if (error) {
+ stream.emit('error', error)
+ return
+ }
+
+ write(
+ new (require('vinyl'))({
+ base: SRC_DIR,
+ contents: buffer,
+ path: path,
+ })
+ )
+ })
+ }
+
+ if (PRODUCTION) {
+ write = function (data) {
+ stream.push(data)
+ stream.push(null)
+ }
+ } else {
+ stream = require('gulp-plumber')().pipe(stream)
+ write = function (data) {
+ stream.push(data)
+ }
+
+ bundler.on('update', bundle)
+ }
+
+ stream._read = function () {
+ this._read = function () {}
+ bundle()
+ }
+
+ return stream
+}
+
+// ===================================================================
+
+gulp.task(function buildPages () {
+ return pipe(
+ src('index.pug'),
+ require('gulp-pug')(),
+ DEVELOPMENT &&
+ require('gulp-embedlr')({
+ port: LIVERELOAD_PORT,
+ }),
+ dest()
+ )
+})
+
+gulp.task(function buildScripts () {
+ return pipe(
+ browserify('./index.js', {
+ plugins: [
+ // ['css-modulesify', {
+ [
+ 'modular-cssify',
+ {
+ css: DIST_DIR + '/modules.css',
+ from: undefined,
+ },
+ ],
+ ],
+ }),
+ require('gulp-sourcemaps').init({ loadMaps: true }),
+ PRODUCTION && require('gulp-uglify/composer')(require('uglify-es'))(),
+ dest()
+ )
+})
+
+gulp.task(function buildStyles () {
+ return pipe(
+ src('index.scss', { sourcemaps: true }),
+ require('gulp-sass')(),
+ require('gulp-autoprefixer')(['last 1 version', '> 1%']),
+ PRODUCTION && require('gulp-csso')(),
+ dest()
+ )
+})
+
+gulp.task(function copyAssets () {
+ return pipe(
+ src(['assets/**/*', 'favicon.*']),
+ src('fontawesome-webfont.*', {
+ base: __dirname + '/node_modules/font-awesome/fonts', // eslint-disable-line no-path-concat
+ passthrough: true,
+ }),
+ src(['!*.css', 'font-mfizz.*'], {
+ base: __dirname + '/node_modules/font-mfizz/dist', // eslint-disable-line no-path-concat
+ passthrough: true,
+ }),
+ dest()
+ )
+})
+
+gulp.task(
+ 'build',
+ gulp.parallel('buildPages', 'buildScripts', 'buildStyles', 'copyAssets')
+)
+
+// -------------------------------------------------------------------
+
+gulp.task(function clean (done) {
+ require('rimraf')(DIST_DIR, done)
+})
diff --git a/packages/xo-web/package.json b/packages/xo-web/package.json
new file mode 100644
index 000000000..bc30c8a5e
--- /dev/null
+++ b/packages/xo-web/package.json
@@ -0,0 +1,229 @@
+{
+ "private": false,
+ "name": "xo-web",
+ "version": "5.16.0",
+ "license": "AGPL-3.0",
+ "description": "Web interface client for Xen-Orchestra",
+ "keywords": [
+ "xen",
+ "orchestra",
+ "xen-orchestra",
+ "web"
+ ],
+ "homepage": "https://github.com/vatesfr/xo-web",
+ "bugs": "https://github.com/vatesfr/xo-web/issues",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/vatesfr/xo-web"
+ },
+ "author": {
+ "name": "Julien Fontanet",
+ "email": "julien.fontanet@vates.fr"
+ },
+ "preferGlobal": false,
+ "main": "dist/",
+ "bin": {},
+ "files": [
+ "dist/"
+ ],
+ "engines": {
+ "node": ">=4",
+ "npm": ">=3"
+ },
+ "devDependencies": {
+ "@nraynaud/novnc": "0.6.1",
+ "ansi_up": "^2.0.2",
+ "asap": "^2.0.6",
+ "babel-core": "^6.26.0",
+ "babel-eslint": "^8.1.2",
+ "babel-plugin-dev": "^1.0.0",
+ "babel-plugin-lodash": "^3.2.11",
+ "babel-plugin-transform-decorators-legacy": "^1.3.4",
+ "babel-plugin-transform-react-constant-elements": "^6.5.0",
+ "babel-plugin-transform-react-inline-elements": "^6.6.5",
+ "babel-plugin-transform-react-jsx-self": "^6.11.0",
+ "babel-plugin-transform-react-jsx-source": "^6.9.0",
+ "babel-plugin-transform-runtime": "^6.6.0",
+ "babel-preset-env": "^1.6.1",
+ "babel-preset-react": "^6.5.0",
+ "babel-preset-stage-0": "^6.24.1",
+ "babel-register": "^6.26.0",
+ "babel-runtime": "^6.26.0",
+ "babelify": "^8.0.0",
+ "benchmark": "^2.1.0",
+ "bootstrap": "4.0.0-alpha.5",
+ "browserify": "^15.1.0",
+ "bundle-collapser": "^1.3.0",
+ "chartist": "^0.10.1",
+ "chartist-plugin-legend": "^0.6.1",
+ "chartist-plugin-tooltip": "0.0.11",
+ "classnames": "^2.2.3",
+ "complex-matcher": "^0.2.1",
+ "cookies-js": "^1.2.2",
+ "d3": "^4.12.2",
+ "debounce-input-decorator": "^0.1.0",
+ "dependency-check": "^3.0.0",
+ "enzyme": "^3.3.0",
+ "enzyme-adapter-react-15": "^1.0.5",
+ "enzyme-to-json": "^3.3.0",
+ "eslint": "^4.14.0",
+ "eslint-config-standard": "^10.2.1",
+ "eslint-config-standard-jsx": "^4.0.2",
+ "eslint-plugin-import": "^2.8.0",
+ "eslint-plugin-node": "^5.2.1",
+ "eslint-plugin-promise": "^3.6.0",
+ "eslint-plugin-react": "^7.4.0",
+ "eslint-plugin-standard": "^3.0.1",
+ "event-to-promise": "^0.8.0",
+ "font-awesome": "^4.7.0",
+ "font-mfizz": "^2.4.1",
+ "get-stream": "^3.0.0",
+ "globby": "^7.1.1",
+ "gulp": "^4.0.0",
+ "gulp-autoprefixer": "^4.1.0",
+ "gulp-csso": "^3.0.0",
+ "gulp-embedlr": "^0.5.2",
+ "gulp-plumber": "^1.1.0",
+ "gulp-pug": "^3.1.0",
+ "gulp-refresh": "^1.1.0",
+ "gulp-sass": "^3.0.0",
+ "gulp-sourcemaps": "^2.6.2",
+ "gulp-uglify": "^3.0.0",
+ "gulp-watch": "^5.0.0",
+ "human-format": "^0.10.0",
+ "husky": "^0.14.3",
+ "immutable": "^3.8.2",
+ "index-modules": "^0.3.0",
+ "is-ip": "^2.0.0",
+ "jest": "^22.0.4",
+ "jsonrpc-websocket-client": "^0.2.0",
+ "kindof": "^2.0.0",
+ "later": "^1.2.0",
+ "lint-staged": "^6.0.0",
+ "lodash": "^4.6.1",
+ "loose-envify": "^1.1.0",
+ "make-error": "^1.3.2",
+ "marked": "^0.3.9",
+ "modular-cssify": "^7.2.0",
+ "moment": "^2.20.1",
+ "moment-timezone": "^0.5.14",
+ "notifyjs": "^3.0.0",
+ "prettier": "^1.9.2",
+ "promise-toolbox": "^0.9.5",
+ "prop-types": "^15.6.0",
+ "random-password": "^0.1.2",
+ "react": "^15.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.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": "^4.2.3",
+ "react-intl": "^2.4.0",
+ "react-key-handler": "^1.0.1",
+ "react-notify": "^3.0.0",
+ "react-overlays": "^0.8.3",
+ "react-redux": "^5.0.6",
+ "react-router": "^3.0.0",
+ "react-select": "^1.1.0",
+ "react-shortcuts": "^2.0.0",
+ "react-sparklines": "1.6.0",
+ "react-test-renderer": "^15.6.2",
+ "react-virtualized": "^9.15.0",
+ "readable-stream": "^2.3.3",
+ "redux": "^3.7.2",
+ "redux-thunk": "^2.0.1",
+ "reselect": "^2.5.4",
+ "semver": "^5.4.1",
+ "styled-components": "^3.1.5",
+ "tar-stream": "^1.5.5",
+ "uglify-es": "^3.3.4",
+ "uncontrollable-input": "^0.1.1",
+ "url-parse": "^1.2.0",
+ "value-matcher": "^0.0.0",
+ "vinyl": "^2.1.0",
+ "watchify": "^3.7.0",
+ "whatwg-fetch": "^2.0.3",
+ "xml2js": "^0.4.19",
+ "xo-acl-resolver": "^0.2.3",
+ "xo-common": "^0.1.1",
+ "xo-lib": "^0.8.0",
+ "xo-remote-parser": "^0.3"
+ },
+ "scripts": {
+ "benchmarks": "./tools/run-benchmarks.js 'src/**/*.bench.js'",
+ "build": "npm run build-indexes && NODE_ENV=production gulp build",
+ "build-indexes": "index-modules --auto src",
+ "clean": "gulp clean",
+ "dev": "npm run build-indexes && NODE_ENV=development gulp build",
+ "dev-test": "jest --watch",
+ "lint-staged-stash": "touch .lint-staged && git stash save --include-untracked --keep-index && true",
+ "lint-staged-unstash": "git stash pop && rm -f .lint-staged && true",
+ "posttest": "eslint --ignore-path .gitignore src/",
+ "prebuild": "npm run clean",
+ "precommit": "lint-staged",
+ "predev": "npm run clean",
+ "prepublishOnly": "npm run build",
+ "test": "jest"
+ },
+ "browserify": {
+ "transform": [
+ "babelify",
+ "loose-envify"
+ ]
+ },
+ "babel": {
+ "env": {
+ "development": {
+ "plugins": [
+ "transform-react-jsx-self",
+ "transform-react-jsx-source"
+ ]
+ },
+ "production": {
+ "plugins": [
+ "transform-react-constant-elements",
+ "transform-react-inline-elements"
+ ]
+ }
+ },
+ "plugins": [
+ "dev",
+ "lodash",
+ "transform-decorators-legacy",
+ "transform-runtime"
+ ],
+ "presets": [
+ [
+ "env",
+ {
+ "targets": {
+ "browsers": ">2%"
+ }
+ }
+ ],
+ "react",
+ "stage-0"
+ ]
+ },
+ "jest": {
+ "setupTestFrameworkScriptFile": "./setup-tests.js",
+ "snapshotSerializers": [
+ "enzyme-to-json/serializer"
+ ]
+ },
+ "lint-staged": {
+ "*.js": [
+ "lint-staged-stash",
+ "prettier --write",
+ "eslint --fix",
+ "jest --findRelatedTests --passWithNoTests",
+ "git add",
+ "lint-staged-unstash"
+ ]
+ }
+}
diff --git a/packages/xo-web/setup-tests.js b/packages/xo-web/setup-tests.js
new file mode 100644
index 000000000..89def87f6
--- /dev/null
+++ b/packages/xo-web/setup-tests.js
@@ -0,0 +1,4 @@
+import { configure } from 'enzyme'
+import Adapter from 'enzyme-adapter-react-15'
+
+configure({ adapter: new Adapter() })
diff --git a/packages/xo-web/src/assets/loading.svg b/packages/xo-web/src/assets/loading.svg
new file mode 100644
index 000000000..9e0af0779
--- /dev/null
+++ b/packages/xo-web/src/assets/loading.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/xo-web/src/assets/logo.png b/packages/xo-web/src/assets/logo.png
new file mode 100644
index 000000000..a3f5ce33e
Binary files /dev/null and b/packages/xo-web/src/assets/logo.png differ
diff --git a/packages/xo-web/src/chartist.scss b/packages/xo-web/src/chartist.scss
new file mode 100644
index 000000000..347eb6856
--- /dev/null
+++ b/packages/xo-web/src/chartist.scss
@@ -0,0 +1,133 @@
+
+// CHARTIST ===================================================================
+
+$ct-series-colors: (
+ $brand-success,
+ $brand-primary,
+ #f17cb0,
+ #86797d,
+ #b276b2,
+ #f15854,
+ #b2912f,
+ #decf3f,
+ #dda458,
+ #60bd68,
+ #4d4d4d,
+ #eacf7d,
+ #b2c326,
+ #6188e2,
+ #a748ca
+) !default;
+
+@import "../node_modules/chartist/dist/scss/settings/_chartist-settings";
+@import "../node_modules/chartist/dist/scss/chartist";
+
+.ct-chart {
+ display: flex;
+ flex-direction: column-reverse;
+}
+
+// safari has a bug in flex computing that prevent charts from showing see #1755
+// by fixing the height with a value found in Chrome it seems like it fixes the issue without breaking the layout
+// elsewhere
+.dashboardItem .ct-chart {
+ height: 150px;
+}
+
+// Line in charts with only 2px in width
+.ct-line {
+ stroke-width: 2px;
+}
+
+.ct-bar {
+ stroke-width: 10%;
+}
+
+.ct-point {
+ stroke-width: 30px;
+ stroke-opacity: 0!important;
+}
+
+.ct-point:hover {
+ stroke-opacity: 0.2!important;
+ stroke-width: 20px;
+}
+
+.ct-tooltip {
+ position: absolute;
+ display: inline-block;
+ min-width: 5em;
+ padding: 8px 10px;
+ background: #383838;
+ color: #fff;
+ text-align: center;
+ pointer-events: none;
+ z-index: 10;
+ font-weight: 700;
+
+ // Arrow!
+ &:before {
+ bottom: -14px;
+ top: 100%;
+ left: 50%;
+ border: solid transparent;
+ content: '';
+ height: 0;
+ width: 0;
+ pointer-events: none;
+ border-color: rgba(251, 249, 228, 0);
+ border-top-color: #383838;
+ border-width: 7px;
+ margin-left: -8px;
+ }
+
+ &.hide {
+ display: block;
+ opacity: 0;
+ visibility: hidden;
+ }
+}
+
+// CHARTIST LEGEND =============================================================
+
+.ct-legend {
+ bottom: 0;
+ margin-bottom: -1em;
+
+ li {
+ position: relative;
+ padding-left: 0.5em;
+ list-style-type: none;
+ display: inline-block;
+ margin-right: 0.5em;
+ font-size: 0.8em;
+ }
+
+ li:before {
+ display: inline-block;
+ width: 1em;
+ height: 1em;
+ left: 0;
+ content: '';
+ border: 3px solid transparent;
+ border-radius: 2px;
+ margin-right: 0.2em;
+ }
+
+ li.inactive:before {
+ background: transparent;
+ }
+
+ &.ct-legend-inside {
+ position: absolute;
+ top: 0;
+ right: 0;
+ }
+
+ @for $i from 0 to length($ct-series-colors) {
+ .ct-series-#{$i}:before {
+ background-color: nth($ct-series-colors, $i + 1);
+ border-color: nth($ct-series-colors, $i + 1);
+ }
+ }
+}
diff --git a/packages/xo-web/src/common/__snapshots__/grid.spec.js.snap b/packages/xo-web/src/common/__snapshots__/grid.spec.js.snap
new file mode 100644
index 000000000..2397db79d
--- /dev/null
+++ b/packages/xo-web/src/common/__snapshots__/grid.spec.js.snap
@@ -0,0 +1,19 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Col 1`] = `
+
+`;
+
+exports[`Container 1`] = `
+
+`;
+
+exports[`Row 1`] = `
+
+`;
diff --git a/packages/xo-web/src/common/action-bar.js b/packages/xo-web/src/common/action-bar.js
new file mode 100644
index 000000000..3c8bf43ea
--- /dev/null
+++ b/packages/xo-web/src/common/action-bar.js
@@ -0,0 +1,60 @@
+import ActionButton from 'action-button'
+import propTypes from 'prop-types-decorator'
+import React, { cloneElement } from 'react'
+import { noop } from 'lodash'
+
+import ButtonGroup from './button-group'
+
+export const Action = ({
+ display,
+ handler,
+ handlerParam,
+ icon,
+ label,
+ pending,
+ redirectOnSuccess,
+}) => (
+
+ {display === 'both' && label}
+
+)
+
+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' }) => (
+
+ {React.Children.map(children, (child, key) => {
+ if (!child) {
+ return
+ }
+
+ const { props } = child
+ return cloneElement(child, {
+ display: props.display || display,
+ handlerParam: props.handlerParam || handlerParam,
+ key,
+ })
+ })}
+
+)
+
+ActionBar.propTypes = {
+ display: propTypes.oneOf(['icon', 'both']),
+ handlerParam: propTypes.any,
+}
+export { ActionBar as default }
diff --git a/packages/xo-web/src/common/action-button.js b/packages/xo-web/src/common/action-button.js
new file mode 100644
index 000000000..71e78a1f5
--- /dev/null
+++ b/packages/xo-web/src/common/action-button.js
@@ -0,0 +1,151 @@
+import isFunction from 'lodash/isFunction'
+import React from 'react'
+
+import Button from './button'
+import Component from './base-component'
+import Icon from './icon'
+import logError from './log-error'
+import propTypes from './prop-types-decorator'
+import Tooltip from './tooltip'
+import { error as _error } from './notification'
+
+@propTypes({
+ // React element to use as button content
+ children: propTypes.node,
+
+ // whether this button is disabled (default to false)
+ disabled: propTypes.bool,
+
+ // form identifier
+ //
+ // if provided, this button and its action are associated to this
+ // form for the submit event
+ form: propTypes.string,
+
+ // function to call when the action is triggered (via a clik on the
+ // button or submit on the form)
+ handler: propTypes.func.isRequired,
+
+ // optional value which will be passed as first param to the handler
+ handlerParam: propTypes.any,
+
+ // XO icon to use for this button
+ icon: propTypes.string.isRequired,
+
+ // whether the action of this action is already underway
+ pending: propTypes.bool,
+
+ // path to redirect to when the triggered action finish successfully
+ //
+ // if a function, it will be called with the result of the action to
+ // compute the path
+ redirectOnSuccess: propTypes.oneOfType([propTypes.func, propTypes.string]),
+
+ // React element to use tooltip for the component
+ tooltip: propTypes.node,
+})
+export default class ActionButton extends Component {
+ static contextTypes = {
+ router: propTypes.object,
+ }
+
+ async _execute () {
+ if (this.props.pending || this.state.working) {
+ return
+ }
+
+ const { children, handler, handlerParam, tooltip } = this.props
+
+ try {
+ this.setState({
+ error: undefined,
+ working: true,
+ })
+
+ const result = await handler(handlerParam)
+
+ const { redirectOnSuccess } = this.props
+ if (redirectOnSuccess) {
+ return this.context.router.push(
+ isFunction(redirectOnSuccess)
+ ? redirectOnSuccess(result)
+ : redirectOnSuccess
+ )
+ }
+
+ this.setState({
+ working: false,
+ })
+ } catch (error) {
+ this.setState({
+ error,
+ 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)
+ )
+ }
+ }
+ }
+ _execute = ::this._execute
+
+ _eventListener = event => {
+ event.preventDefault()
+ this._execute()
+ }
+
+ componentDidMount () {
+ const { form } = this.props
+
+ if (form) {
+ document
+ .getElementById(form)
+ .addEventListener('submit', this._eventListener)
+ }
+ }
+
+ componentWillUnmount () {
+ const { form } = this.props
+
+ if (form) {
+ document
+ .getElementById(form)
+ .removeEventListener('submit', this._eventListener)
+ }
+ }
+
+ render () {
+ const {
+ props: { children, icon, pending, tooltip, ...props },
+ state: { error, working },
+ } = this
+
+ if (error !== undefined) {
+ props.btnStyle = 'warning'
+ }
+ if (pending || working) {
+ props.disabled = true
+ }
+ delete props.handler
+ delete props.handlerParam
+ if (props.form === undefined) {
+ props.onClick = this._execute
+ }
+ delete props.redirectOnSuccess
+
+ const button = (
+
+
+ {children && ' '}
+ {children}
+
+ )
+
+ return tooltip ? {button} : button
+ }
+}
diff --git a/packages/xo-web/src/common/action-row-button/index.css b/packages/xo-web/src/common/action-row-button/index.css
new file mode 100644
index 000000000..e23658595
--- /dev/null
+++ b/packages/xo-web/src/common/action-row-button/index.css
@@ -0,0 +1,7 @@
+.button {
+ opacity: 0.5;
+}
+
+tr:hover .button, tr:focus .button {
+ opacity: 1;
+}
diff --git a/packages/xo-web/src/common/action-row-button/index.js b/packages/xo-web/src/common/action-row-button/index.js
new file mode 100644
index 000000000..be9cc8bb8
--- /dev/null
+++ b/packages/xo-web/src/common/action-row-button/index.js
@@ -0,0 +1,10 @@
+import React from 'react'
+
+import ActionButton from '../action-button'
+
+import styles from './index.css'
+
+const ActionRowButton = props => (
+
+)
+export { ActionRowButton as default }
diff --git a/packages/xo-web/src/common/action-toggle.js b/packages/xo-web/src/common/action-toggle.js
new file mode 100644
index 000000000..7887ab9eb
--- /dev/null
+++ b/packages/xo-web/src/common/action-toggle.js
@@ -0,0 +1,16 @@
+import React from 'react'
+
+import ActionButton from './action-button'
+import propTypes from './prop-types-decorator'
+
+const ActionToggle = ({ className, value, ...props }) => (
+
+)
+
+export default propTypes({
+ value: propTypes.bool,
+})(ActionToggle)
diff --git a/packages/xo-web/src/common/add-subscriptions.js b/packages/xo-web/src/common/add-subscriptions.js
new file mode 100644
index 000000000..ed361ecbf
--- /dev/null
+++ b/packages/xo-web/src/common/add-subscriptions.js
@@ -0,0 +1,29 @@
+import map from 'lodash/map'
+import React from 'react'
+
+const call = fn => fn()
+
+// `subscriptions` can be a function if we want to ensure that the subscription
+// callbacks have been correctly initialized when there are circular dependencies
+const addSubscriptions = subscriptions => Component =>
+ class SubscriptionWrapper extends React.PureComponent {
+ _unsubscribes = null
+
+ componentWillMount () {
+ this._unsubscribes = map(
+ typeof subscriptions === 'function' ? subscriptions(this.props) : subscriptions,
+ (subscribe, prop) =>
+ subscribe(value => this.setState({ [prop]: value }))
+ )
+ }
+
+ componentWillUnmount () {
+ this._unsubscribes.forEach(call)
+ this._unsubscribes = null
+ }
+
+ render () {
+ return
+ }
+ }
+export { addSubscriptions as default }
diff --git a/packages/xo-web/src/common/base-component.js b/packages/xo-web/src/common/base-component.js
new file mode 100644
index 000000000..9ee83aea9
--- /dev/null
+++ b/packages/xo-web/src/common/base-component.js
@@ -0,0 +1,119 @@
+import { PureComponent } from 'react'
+import { cowSet } from 'utils'
+import { includes, isArray, forEach, map } from 'lodash'
+
+import getEventValue from './get-event-value'
+
+// Should components logs every renders?
+//
+// Usually set to process.env.NODE_ENV !== 'production'.
+const VERBOSE = false
+
+const get = (object, path, depth) => {
+ if (object == null || depth >= path.length) {
+ return object
+ }
+
+ const prop = path[depth++]
+ return isArray(object) && prop === '*'
+ ? map(object, value => get(value, path, depth))
+ : get(object[prop], path, depth)
+}
+
+export default class BaseComponent extends PureComponent {
+ constructor (props, context) {
+ super(props, context)
+
+ // It really should have been done in React.Component!
+ this.state = {}
+
+ this._linkedState = null
+
+ if (VERBOSE) {
+ this.render = (render => () => {
+ console.log('render', this.constructor.name)
+
+ return render.call(this)
+ })(this.render)
+ }
+ }
+
+ // See https://preactjs.com/guide/linked-state
+ linkState (name, targetPath) {
+ const key = targetPath !== undefined ? `${name}##${targetPath}` : name
+
+ let linkedState = this._linkedState
+ let cb
+ if (linkedState === null) {
+ linkedState = this._linkedState = {}
+ } else if ((cb = linkedState[key]) !== undefined) {
+ return cb
+ }
+
+ let getValue
+ if (targetPath !== undefined) {
+ const path = targetPath.split('.')
+ getValue = event => get(getEventValue(event), path, 0)
+ } else {
+ getValue = getEventValue
+ }
+
+ if (includes(name, '.')) {
+ const path = name.split('.')
+ return (linkedState[key] = event => {
+ this.setState(cowSet(this.state, path, getValue(event), 0))
+ })
+ }
+
+ return (linkedState[key] = event => {
+ this.setState({
+ [name]: getValue(event),
+ })
+ })
+ }
+
+ toggleState (name) {
+ let linkedState = this._linkedState
+ let cb
+ if (linkedState === null) {
+ linkedState = this._linkedState = {}
+ } else if ((cb = linkedState[name]) !== undefined) {
+ return cb
+ }
+
+ if (includes(name, '.')) {
+ const path = name.split('.')
+ return (linkedState[path] = event => {
+ this.setState(cowSet(this.state, path, !get(this.state, path, 0), 0))
+ })
+ }
+
+ return (linkedState[name] = () => {
+ this.setState({
+ [name]: !this.state[name],
+ })
+ })
+ }
+}
+
+if (VERBOSE) {
+ const diff = (name, old, cur) => {
+ const keys = []
+
+ forEach(old, (value, key) => {
+ if (cur[key] !== value) {
+ keys.push(key)
+ }
+ })
+
+ if (keys.length) {
+ console.log(name, keys.sort().join())
+ }
+ }
+
+ BaseComponent.prototype.componentDidUpdate = function (oldProps, oldState) {
+ const prefix = `${this.constructor.name} updated because of its`
+ diff(`${prefix} props:`, oldProps, this.props)
+ diff(`${prefix} state:`, oldState, this.state)
+ }
+}
diff --git a/packages/xo-web/src/common/browser-notification.js b/packages/xo-web/src/common/browser-notification.js
new file mode 100644
index 000000000..91d20ed39
--- /dev/null
+++ b/packages/xo-web/src/common/browser-notification.js
@@ -0,0 +1,35 @@
+import { noop } from 'utils'
+import Notify from 'notifyjs'
+
+let notify
+export { notify as default }
+
+const sendNotification = (title, body) => {
+ new Notify(title, {
+ body,
+ timeout: 5,
+ icon: 'assets/logo.png',
+ }).show()
+}
+
+const requestPermission = (...args) => {
+ if (Notify.isSupported()) {
+ Notify.requestPermission(
+ () => {
+ console.log('notifications allowed')
+
+ return (notify = sendNotification)(...args)
+ },
+ () => {
+ console.log('notifications denied')
+
+ notify = noop
+ }
+ )
+ } else {
+ notify = noop
+ console.warn('notifications are not supported')
+ }
+}
+
+notify = Notify.needsPermission ? requestPermission : sendNotification
diff --git a/packages/xo-web/src/common/button-group.js b/packages/xo-web/src/common/button-group.js
new file mode 100644
index 000000000..44a7cf0ee
--- /dev/null
+++ b/packages/xo-web/src/common/button-group.js
@@ -0,0 +1,9 @@
+import React from 'react'
+
+const ButtonGroup = ({ children }) => (
+
+ {children}
+
+)
+
+export { ButtonGroup as default }
diff --git a/packages/xo-web/src/common/button-link.js b/packages/xo-web/src/common/button-link.js
new file mode 100644
index 000000000..42ee0becf
--- /dev/null
+++ b/packages/xo-web/src/common/button-link.js
@@ -0,0 +1,28 @@
+import React from 'react'
+import { routerShape } from 'react-router/lib/PropTypes'
+
+import Button from './button'
+import propTypes from './prop-types-decorator'
+
+const ButtonLink = ({ to, ...props }, { router }) => {
+ props.onClick = () => {
+ router.push(to)
+ }
+
+ return
+}
+
+propTypes(
+ {
+ to: propTypes.oneOfType([
+ propTypes.func,
+ propTypes.object,
+ propTypes.string,
+ ]),
+ },
+ {
+ router: routerShape,
+ }
+)(ButtonLink)
+
+export { ButtonLink as default }
diff --git a/packages/xo-web/src/common/button.js b/packages/xo-web/src/common/button.js
new file mode 100644
index 000000000..39e57cb7f
--- /dev/null
+++ b/packages/xo-web/src/common/button.js
@@ -0,0 +1,53 @@
+import classNames from 'classnames'
+import React from 'react'
+
+import propTypes from './prop-types-decorator'
+
+const Button = ({
+ active,
+ block,
+ btnStyle = 'secondary',
+ children,
+ outline,
+ size,
+ ...props
+}) => {
+ props.className = classNames(
+ props.className,
+ 'btn',
+ `btn${outline ? '-outline' : ''}-${btnStyle}`,
+ active !== undefined && 'active',
+ block && 'btn-block',
+ size === 'large' ? 'btn-lg' : size === 'small' ? 'btn-sm' : null
+ )
+ if (props.type === undefined && props.form === undefined) {
+ props.type = 'button'
+ }
+
+ return {children}
+}
+
+propTypes({
+ active: propTypes.bool,
+ block: propTypes.bool,
+
+ // Bootstrap button style
+ //
+ // See https://v4-alpha.getbootstrap.com/components/buttons/#examples
+ //
+ // The default value (secondary) is not listed here because it does
+ // not make sense to explicit it.
+ btnStyle: propTypes.oneOf([
+ 'danger',
+ 'info',
+ 'link',
+ 'primary',
+ 'success',
+ 'warning',
+ ]),
+
+ outline: propTypes.bool,
+ size: propTypes.oneOf(['large', 'small']),
+})(Button)
+
+export { Button as default }
diff --git a/packages/xo-web/src/common/card.js b/packages/xo-web/src/common/card.js
new file mode 100644
index 000000000..36bb43650
--- /dev/null
+++ b/packages/xo-web/src/common/card.js
@@ -0,0 +1,40 @@
+import React from 'react'
+
+import propTypes from './prop-types-decorator'
+
+const CARD_STYLE = {
+ minHeight: '100%',
+}
+
+const CARD_STYLE_WITH_SHADOW = {
+ ...CARD_STYLE,
+ boxShadow: '0 10px 6px -6px #777', // https://css-tricks.com/almanac/properties/b/box-shadow/
+}
+
+const CARD_HEADER_STYLE = {
+ minHeight: '100%',
+ textAlign: 'center',
+}
+
+export const Card = propTypes({
+ shadow: propTypes.bool,
+})(({ shadow, ...props }) => {
+ props.className = 'card'
+ props.style = shadow ? CARD_STYLE_WITH_SHADOW : CARD_STYLE
+
+ return
+})
+
+export const CardHeader = propTypes({
+ className: propTypes.string,
+})(({ children, className }) => (
+
+ {children}
+
+))
+
+export const CardBlock = propTypes({
+ className: propTypes.string,
+})(({ children, className }) => (
+ {children}
+))
diff --git a/packages/xo-web/src/common/center-panel/index.css b/packages/xo-web/src/common/center-panel/index.css
new file mode 100644
index 000000000..f253732a1
--- /dev/null
+++ b/packages/xo-web/src/common/center-panel/index.css
@@ -0,0 +1,9 @@
+.container {
+ display: flex;
+ min-height: 100%;
+}
+
+.content {
+ margin: auto;
+ text-align: center;
+}
diff --git a/packages/xo-web/src/common/center-panel/index.js b/packages/xo-web/src/common/center-panel/index.js
new file mode 100644
index 000000000..eb5e15bd1
--- /dev/null
+++ b/packages/xo-web/src/common/center-panel/index.js
@@ -0,0 +1,11 @@
+import React from 'react'
+
+import styles from './index.css'
+
+const CenterPanel = ({ children }) => (
+
+)
+
+export { CenterPanel as default }
diff --git a/packages/xo-web/src/common/collapse.js b/packages/xo-web/src/common/collapse.js
new file mode 100644
index 000000000..d2819b542
--- /dev/null
+++ b/packages/xo-web/src/common/collapse.js
@@ -0,0 +1,39 @@
+import React from 'react'
+
+import Button from './button'
+import Component from './base-component'
+import Icon from './icon'
+import propTypes from './prop-types-decorator'
+
+@propTypes({
+ children: propTypes.any.isRequired,
+ className: propTypes.string,
+ buttonText: propTypes.any.isRequired,
+ defaultOpen: propTypes.bool,
+})
+export default class Collapse extends Component {
+ state = {
+ isOpened: this.props.defaultOpen,
+ }
+
+ _onClick = () => {
+ this.setState({
+ isOpened: !this.state.isOpened,
+ })
+ }
+
+ render () {
+ const { props } = this
+ const { isOpened } = this.state
+
+ return (
+
+
+ {props.buttonText}{' '}
+
+
+ {isOpened && props.children}
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/common/combobox.js b/packages/xo-web/src/common/combobox.js
new file mode 100644
index 000000000..46d98c061
--- /dev/null
+++ b/packages/xo-web/src/common/combobox.js
@@ -0,0 +1,61 @@
+import React from 'react'
+import uncontrollableInput from 'uncontrollable-input'
+import { isEmpty, map } from 'lodash'
+import { DropdownButton, MenuItem } from 'react-bootstrap-4/lib'
+
+import Component from './base-component'
+import propTypes from './prop-types-decorator'
+
+@uncontrollableInput({
+ defaultValue: '',
+})
+@propTypes({
+ disabled: propTypes.bool,
+ options: propTypes.oneOfType([
+ propTypes.arrayOf(propTypes.string),
+ propTypes.objectOf(propTypes.string),
+ ]),
+ onChange: propTypes.func.isRequired,
+ value: propTypes.string.isRequired,
+})
+export default class Combobox extends Component {
+ _handleChange = event => {
+ this.props.onChange(event.target.value)
+ }
+
+ _setText (value) {
+ this.props.onChange(value)
+ }
+
+ render () {
+ const { options, ...props } = this.props
+
+ props.className = 'form-control'
+ props.onChange = this._handleChange
+ const Input =
+
+ if (isEmpty(options)) {
+ return Input
+ }
+
+ return (
+
+
+
+ {map(options, option => (
+ this._setText(option)}>
+ {option}
+
+ ))}
+
+
+ {Input}
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/common/copiable/index.css b/packages/xo-web/src/common/copiable/index.css
new file mode 100644
index 000000000..5345f4fa3
--- /dev/null
+++ b/packages/xo-web/src/common/copiable/index.css
@@ -0,0 +1,9 @@
+.container .button {
+ position: absolute;
+ margin-left: 1ex;
+ visibility: hidden;
+}
+
+.container:hover .button {
+ visibility: visible;
+}
diff --git a/packages/xo-web/src/common/copiable/index.js b/packages/xo-web/src/common/copiable/index.js
new file mode 100644
index 000000000..274732f44
--- /dev/null
+++ b/packages/xo-web/src/common/copiable/index.js
@@ -0,0 +1,34 @@
+import CopyToClipboard from 'react-copy-to-clipboard'
+import classNames from 'classnames'
+import React, { createElement } from 'react'
+
+import _ from '../intl'
+import Button from '../button'
+import Icon from '../icon'
+import propTypes from '../prop-types-decorator'
+import Tooltip from '../tooltip'
+
+import styles from './index.css'
+
+const Copiable = propTypes({
+ data: propTypes.string,
+ tagName: propTypes.string,
+})(({ className, tagName = 'span', ...props }) =>
+ createElement(
+ tagName,
+ {
+ ...props,
+ className: classNames(styles.container, className),
+ },
+ props.children,
+ ' ',
+
+
+
+
+
+
+
+ )
+)
+export { Copiable as default }
diff --git a/packages/xo-web/src/common/d3-utils.js b/packages/xo-web/src/common/d3-utils.js
new file mode 100644
index 000000000..68b475611
--- /dev/null
+++ b/packages/xo-web/src/common/d3-utils.js
@@ -0,0 +1,9 @@
+import forEach from 'lodash/forEach'
+
+export function setStyles (style) {
+ forEach(style, (value, key) => {
+ this.style(key, value)
+ })
+
+ return this
+}
diff --git a/packages/xo-web/src/common/debug.js b/packages/xo-web/src/common/debug.js
new file mode 100644
index 000000000..58800131d
--- /dev/null
+++ b/packages/xo-web/src/common/debug.js
@@ -0,0 +1,60 @@
+import PropTypes from 'prop-types'
+import React, { Component } from 'react'
+import { isPromise } from 'promise-toolbox'
+
+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,
+ }
+
+ constructor (props) {
+ super()
+
+ this.state = {
+ status: 'pending',
+ }
+
+ props.promise.then(
+ value => this.setState({ status: 'resolved', value }),
+ value => this.setState({ status: 'rejected', value })
+ )
+ }
+
+ shouldComponentUpdate (_, newState) {
+ return this.state.status !== newState.status
+ }
+
+ render () {
+ const { status, value } = this.state
+
+ if (status === 'pending') {
+ return {'Promise { }'}
+ }
+
+ return (
+
+ {'Promise { '}
+ {status === 'rejected' && ' '}
+ {toString(value)}
+ {' }'}
+
+ )
+ }
+}
+
+const Debug = ({ value }) =>
+ isPromise(value) ? (
+
+ ) : (
+ {toString(value)}
+ )
+
+Debug.propTypes = {
+ value: PropTypes.any.isRequired,
+}
+
+export { Debug as default }
diff --git a/packages/xo-web/src/common/dropzone/index.css b/packages/xo-web/src/common/dropzone/index.css
new file mode 100644
index 000000000..7b195906a
--- /dev/null
+++ b/packages/xo-web/src/common/dropzone/index.css
@@ -0,0 +1,22 @@
+@value dropzoneColor: #8f8686;
+
+.dropzone {
+ border-radius: 4px;
+ border: 2px dashed dropzoneColor;
+ cursor: pointer;
+ display: flex;
+ height: 12em;
+ margin-bottom: 20px;
+ width: 100%;
+}
+
+.activeDropzone {
+ background: #f0f0f0;
+ border-style: solid;
+}
+
+.dropzoneText {
+ color: dropzoneColor;
+ font-size: 1.2em;
+ margin: auto;
+}
diff --git a/packages/xo-web/src/common/dropzone/index.js b/packages/xo-web/src/common/dropzone/index.js
new file mode 100644
index 000000000..0e18e7529
--- /dev/null
+++ b/packages/xo-web/src/common/dropzone/index.js
@@ -0,0 +1,26 @@
+import Component from 'base-component'
+import propTypes from 'prop-types-decorator'
+import React from 'react'
+import ReactDropzone from 'react-dropzone'
+
+import styles from './index.css'
+
+@propTypes({
+ onDrop: propTypes.func,
+ message: propTypes.node,
+})
+export default class Dropzone extends Component {
+ render () {
+ const { onDrop, message } = this.props
+
+ return (
+
+ {message}
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/common/editable/index.css b/packages/xo-web/src/common/editable/index.css
new file mode 100644
index 000000000..eaaa67e15
--- /dev/null
+++ b/packages/xo-web/src/common/editable/index.css
@@ -0,0 +1,13 @@
+.clickToEdit * {
+ cursor: context-menu !important;
+}
+.shortClick {
+ border-bottom: 1px dashed #ccc;
+}
+
+.select {
+ padding: 0px;
+}
+.size {
+ width: 10rem;
+}
diff --git a/packages/xo-web/src/common/editable/index.js b/packages/xo-web/src/common/editable/index.js
new file mode 100644
index 000000000..183e06128
--- /dev/null
+++ b/packages/xo-web/src/common/editable/index.js
@@ -0,0 +1,497 @@
+import classNames from 'classnames'
+import findKey from 'lodash/findKey'
+import isFunction from 'lodash/isFunction'
+import isString from 'lodash/isString'
+import map from 'lodash/map'
+import pick from 'lodash/pick'
+import React from 'react'
+
+import _ from '../intl'
+import Component from '../base-component'
+import getEventValue from '../get-event-value'
+import Icon from '../icon'
+import logError from '../log-error'
+import propTypes from '../prop-types-decorator'
+import Tooltip from '../tooltip'
+import { formatSize } from '../utils'
+import { SizeInput } from '../form'
+import {
+ SelectHost,
+ SelectIp,
+ SelectNetwork,
+ SelectPool,
+ SelectRemote,
+ SelectResourceSetIp,
+ SelectSr,
+ SelectSubject,
+ SelectTag,
+ SelectVgpuType,
+ SelectVm,
+ SelectVmTemplate,
+} from '../select-objects'
+
+import styles from './index.css'
+
+const LONG_CLICK = 400
+
+@propTypes({
+ alt: propTypes.node.isRequired,
+})
+class Hover extends Component {
+ constructor () {
+ super()
+
+ this.state = {
+ hover: false,
+ }
+
+ this._onMouseEnter = () => this.setState({ hover: true })
+ this._onMouseLeave = () => this.setState({ hover: false })
+ }
+
+ render () {
+ if (this.state.hover) {
+ return {this.props.alt}
+ }
+
+ return {this.props.children}
+ }
+}
+
+@propTypes({
+ onChange: propTypes.func.isRequired,
+ onUndo: propTypes.oneOfType([propTypes.bool, propTypes.func]),
+ useLongClick: propTypes.bool,
+ value: propTypes.any.isRequired,
+})
+class Editable extends Component {
+ get value () {
+ throw new Error('not implemented')
+ }
+
+ _onKeyDown = event => {
+ const { keyCode } = event
+ if (keyCode === 27) {
+ return this._closeEdition()
+ }
+
+ if (keyCode === 13) {
+ return this._save()
+ }
+ }
+
+ _closeEdition = () => {
+ this.setState({ editing: false })
+ }
+
+ _openEdition = () => {
+ this.setState({
+ editing: true,
+ error: null,
+ saving: false,
+ })
+ }
+
+ _undo = () => {
+ const { props } = this
+ const { onUndo } = props
+ if (onUndo === false) {
+ return
+ }
+
+ return this.__save(
+ () => this.state.previous,
+ isFunction(onUndo) ? onUndo : props.onChange
+ )
+ }
+
+ _save () {
+ return this.__save(() => this.value, this.props.onChange)
+ }
+
+ async __save (getValue, saveValue) {
+ const { props } = this
+
+ try {
+ const value = getValue()
+ const previous = props.value
+ if (value === previous) {
+ return this._closeEdition()
+ }
+
+ this.setState({ saving: true })
+
+ await saveValue(value)
+
+ this.setState({ previous })
+ this._closeEdition()
+ } catch (error) {
+ this.setState({
+ // `error` may be undefined if the action has been cancelled
+ error: error !== undefined && (isString(error) ? error : error.message),
+ saving: false,
+ })
+ logError(error)
+ }
+ }
+
+ __startTimer = event => {
+ event.persist()
+ this._timeout = setTimeout(() => {
+ event.preventDefault()
+ this._openEdition()
+ }, LONG_CLICK)
+ }
+ __stopTimer = () => clearTimeout(this._timeout)
+
+ render () {
+ const { state, props } = this
+
+ if (!state.editing) {
+ const { onUndo, previous } = state
+ const { useLongClick } = props
+
+ const success =
+ return (
+
+
+ {this._renderDisplay()}
+
+ {previous != null &&
+ (onUndo !== false ? (
+
+
+
+ }
+ >
+ {success}
+
+ ) : (
+ success
+ ))}
+
+ )
+ }
+
+ const { error, saving } = state
+
+ return (
+
+ {this._renderEdition()}
+ {saving && (
+
+ {' '}
+
+
+ )}
+ {error != null && (
+
+ {' '}
+
+
+
+
+ )}
+
+ )
+ }
+}
+
+@propTypes({
+ autoComplete: propTypes.string,
+ maxLength: propTypes.number,
+ minLength: propTypes.number,
+ pattern: propTypes.string,
+ value: propTypes.string.isRequired,
+})
+export class Text extends Editable {
+ get value () {
+ const { input } = this.refs
+
+ // FIXME: should be properly forwarded to the user.
+ const error = input.validationMessage
+ if (error) {
+ throw new Error(error)
+ }
+
+ return input.value
+ }
+
+ _onInput = ({ target }) => {
+ target.style.width = `${target.value.length + 1}ex`
+ }
+
+ _renderDisplay () {
+ const { children, value } = this.props
+
+ if (children || value) {
+ return {children || value}
+ }
+
+ const { placeholder, useLongClick } = this.props
+
+ return (
+
+ {placeholder ||
+ (useLongClick
+ ? _('editableLongClickPlaceholder')
+ : _('editableClickPlaceholder'))}
+
+ )
+ }
+
+ _renderEdition () {
+ const { value } = this.props
+ const { saving } = this.state
+
+ // Optional props that the user may set on the input.
+ const extraProps = pick(this.props, [
+ 'autoComplete',
+ 'maxLength',
+ 'minLength',
+ 'pattern',
+ ])
+
+ return (
+
+ )
+ }
+}
+
+export class Password extends Text {
+ // TODO: this is a hack, this class should probably have a better
+ // implementation.
+ _isPassword = true
+}
+
+@propTypes({
+ nullable: propTypes.bool,
+ value: propTypes.number,
+})
+export class Number extends Component {
+ get value () {
+ return +this.refs.input.value
+ }
+
+ _onChange = value => {
+ if (value === '') {
+ if (this.props.nullable) {
+ value = null
+ } else {
+ return
+ }
+ } else {
+ value = +value
+ }
+
+ this.props.onChange(value)
+ }
+
+ render () {
+ const { value } = this.props
+ return (
+
+ )
+ }
+}
+
+@propTypes({
+ options: propTypes.oneOfType([propTypes.array, propTypes.object]).isRequired,
+ renderer: propTypes.func,
+})
+export class Select extends Editable {
+ componentWillReceiveProps (props) {
+ if (
+ props.value !== this.props.value ||
+ props.options !== this.props.options
+ ) {
+ this.setState({
+ valueKey: findKey(props.options, option => option === props.value),
+ })
+ }
+ }
+
+ get value () {
+ return this.props.options[this.state.valueKey]
+ }
+
+ _onChange = event => {
+ this.setState({ valueKey: getEventValue(event) }, this._save)
+ }
+
+ _optionToJsx = (option, key) => {
+ const { renderer } = this.props
+
+ return (
+
+ {renderer ? renderer(option) : option}
+
+ )
+ }
+
+ _onEditionMount = ref => {
+ // Seems to work in Google Chrome (not in Firefox)
+ ref && ref.dispatchEvent(new window.MouseEvent('mousedown'))
+ }
+
+ _renderDisplay () {
+ const { children, renderer, value } = this.props
+
+ return children || {renderer ? renderer(value) : value}
+ }
+
+ _renderEdition () {
+ const { saving, valueKey } = this.state
+ const { options } = this.props
+
+ return (
+
+ {map(options, this._optionToJsx)}
+
+ )
+ }
+}
+
+const MAP_TYPE_SELECT = {
+ host: SelectHost,
+ ip: SelectIp,
+ network: SelectNetwork,
+ pool: SelectPool,
+ remote: SelectRemote,
+ resourceSetIp: SelectResourceSetIp,
+ SR: SelectSr,
+ subject: SelectSubject,
+ tag: SelectTag,
+ vgpuType: SelectVgpuType,
+ VM: SelectVm,
+ 'VM-template': SelectVmTemplate,
+}
+
+@propTypes({
+ value: propTypes.oneOfType([propTypes.string, propTypes.object]),
+})
+export class XoSelect extends Editable {
+ get value () {
+ return this.state.value
+ }
+
+ _renderDisplay () {
+ return (
+ this.props.children || (
+ {this.props.value[this.props.labelProp]}
+ )
+ )
+ }
+
+ _onChange = object => this.setState({ value: object }, object && this._save)
+
+ _renderEdition () {
+ const { saving, xoType, ...props } = this.props
+
+ const Select = MAP_TYPE_SELECT[xoType]
+ if (process.env.NODE_ENV !== 'production') {
+ if (!Select) {
+ throw new Error(`${xoType} is not a valid XoSelect type.`)
+ }
+ }
+
+ // Anchor is needed so that the BlockLink does not trigger a redirection
+ // when this element is clicked.
+ return (
+
+
+
+ )
+ }
+}
+
+@propTypes({
+ value: propTypes.number.isRequired,
+})
+export class Size extends Editable {
+ get value () {
+ return this.refs.input.value
+ }
+
+ _renderDisplay () {
+ return this.props.children || formatSize(this.props.value)
+ }
+
+ _closeEditionIfUnfocused = () => {
+ this._focused = false
+ setTimeout(() => {
+ !this._focused && this._closeEdition()
+ }, 10)
+ }
+
+ _focus = () => {
+ this._focused = true
+ }
+
+ _renderEdition () {
+ const { saving } = this.state
+ const { value } = this.props
+
+ return (
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/common/ellipsis.js b/packages/xo-web/src/common/ellipsis.js
new file mode 100644
index 000000000..b64221ac4
--- /dev/null
+++ b/packages/xo-web/src/common/ellipsis.js
@@ -0,0 +1,24 @@
+import React from 'react'
+
+const ellipsisStyle = {
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ whiteSpace: 'nowrap',
+}
+
+const ellipsisContainerStyle = {
+ display: 'flex',
+}
+
+const Ellipsis = ({ children }) => {children}
+export { Ellipsis as default }
+
+export const EllipsisContainer = ({ children }) => (
+
+ {React.Children.map(
+ children,
+ child =>
+ child == null || child.type === Ellipsis ? child : {child}
+ )}
+
+)
diff --git a/packages/xo-web/src/common/fetch.js b/packages/xo-web/src/common/fetch.js
new file mode 100644
index 000000000..d18f0133f
--- /dev/null
+++ b/packages/xo-web/src/common/fetch.js
@@ -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',
+ })
diff --git a/packages/xo-web/src/common/filter-reduce.js b/packages/xo-web/src/common/filter-reduce.js
new file mode 100644
index 000000000..5b0117bc0
--- /dev/null
+++ b/packages/xo-web/src/common/filter-reduce.js
@@ -0,0 +1,37 @@
+import findIndex from 'lodash/findIndex'
+import identity from 'lodash/identity'
+
+// Returns a copy of the array containing:
+// - the elements which did not matches the predicate
+// - the result of the reduction of the elements matching the
+// predicates
+//
+// As a special case, if the predicate is not provided, it is
+// considered to have not matched.
+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 (reducer == null) {
+ reducer = identity
+ }
+
+ const result = array.slice(0, i)
+ let value = initial == null ? array[i] : reducer(initial, array[i], i, array)
+
+ for (i = i + 1; i < length; ++i) {
+ const current = array[i]
+ if (predicate(current, i, array)) {
+ value = reducer(value, current, i, array)
+ } else {
+ result.push(current)
+ }
+ }
+
+ result.push(value)
+ return result
+}
+export { filterReduce as default }
diff --git a/packages/xo-web/src/common/filter-reduce.spec.js b/packages/xo-web/src/common/filter-reduce.spec.js
new file mode 100644
index 000000000..7595ed9f3
--- /dev/null
+++ b/packages/xo-web/src/common/filter-reduce.spec.js
@@ -0,0 +1,19 @@
+/* eslint-env jest */
+
+import filterReduce from './filter-reduce'
+
+const add = (a, b) => a + b
+const data = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
+const 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])
+
+ // The default reducer is the identity.
+ 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])
+})
diff --git a/packages/xo-web/src/common/form-grid.js b/packages/xo-web/src/common/form-grid.js
new file mode 100644
index 000000000..887fdcdb9
--- /dev/null
+++ b/packages/xo-web/src/common/form-grid.js
@@ -0,0 +1,18 @@
+import React from 'react'
+
+import * as Grid from './grid'
+import propTypes from './prop-types-decorator'
+
+export const LabelCol = propTypes({
+ children: propTypes.any.isRequired,
+})(({ children }) => (
+ {children}
+))
+
+export const InputCol = propTypes({
+ children: propTypes.any.isRequired,
+})(({ children }) => {children} )
+
+export const Row = propTypes({
+ children: propTypes.arrayOf(propTypes.element).isRequired,
+})(({ children }) => {children} )
diff --git a/packages/xo-web/src/common/form/index.js b/packages/xo-web/src/common/form/index.js
new file mode 100644
index 000000000..e63dcf806
--- /dev/null
+++ b/packages/xo-web/src/common/form/index.js
@@ -0,0 +1,304 @@
+import BaseComponent from 'base-component'
+import classNames from 'classnames'
+import Icon from 'icon'
+import map from 'lodash/map'
+import randomPassword from 'random-password'
+import React from 'react'
+import round from 'lodash/round'
+import SingleLineRow from 'single-line-row'
+import { Container, Col } from 'grid'
+import { DropdownButton, MenuItem } from 'react-bootstrap-4/lib'
+
+import 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 { formatSizeRaw, parseSize } from '../utils'
+
+export Select from './select'
+
+// ===================================================================
+
+@propTypes({
+ enableGenerator: propTypes.bool,
+})
+export class Password extends Component {
+ get value () {
+ return this.refs.field.value
+ }
+
+ set value (value) {
+ this.refs.field.value = value
+ }
+
+ _generate = () => {
+ const value = randomPassword(8)
+ const isControlled = this.props.value !== undefined
+ if (isControlled) {
+ this.props.onChange(value)
+ } else {
+ this.refs.field.value = value
+ }
+
+ // FIXME: in controlled mode, visibility should only be updated
+ // when the value prop is changed according to the emitted value.
+ this.setState({
+ visible: true,
+ })
+ }
+
+ _toggleVisibility = () => {
+ this.setState({
+ visible: !this.state.visible,
+ })
+ }
+
+ render () {
+ const { className, enableGenerator = false, ...props } = this.props
+ const { visible } = this.state
+
+ return (
+
+ {enableGenerator && (
+
+
+
+
+
+ )}
+
+
+
+
+
+
+
+ )
+ }
+}
+
+// ===================================================================
+
+@propTypes({
+ max: propTypes.number.isRequired,
+ min: propTypes.number.isRequired,
+ onChange: propTypes.func,
+ step: propTypes.number,
+ value: propTypes.number,
+})
+export class Range extends Component {
+ componentDidMount () {
+ const { min, onChange, value } = this.props
+
+ if (!value) {
+ onChange && onChange(min)
+ }
+ }
+
+ _onChange = value => this.props.onChange(getEventValue(value))
+
+ render () {
+ const { max, min, step, value } = this.props
+
+ return (
+
+
+
+ {value}
+
+
+
+
+
+
+ )
+ }
+}
+
+export Toggle from './toggle'
+
+const UNITS = ['kiB', 'MiB', 'GiB']
+const DEFAULT_UNIT = 'GiB'
+@propTypes({
+ autoFocus: propTypes.bool,
+ className: propTypes.string,
+ defaultUnit: propTypes.oneOf(UNITS),
+ defaultValue: propTypes.number,
+ onChange: propTypes.func,
+ placeholder: propTypes.string,
+ readOnly: propTypes.bool,
+ required: propTypes.bool,
+ style: propTypes.object,
+ value: propTypes.oneOfType([propTypes.number, propTypes.oneOf([null])]),
+})
+export class SizeInput extends BaseComponent {
+ constructor (props) {
+ super(props)
+
+ this.state = this._createStateFromBytes(
+ defined(props.value, props.defaultValue, null)
+ )
+ }
+
+ componentWillReceiveProps (props) {
+ const { value } = props
+ if (value !== undefined && value !== this.props.value) {
+ this.setState(this._createStateFromBytes(value))
+ }
+ }
+
+ _createStateFromBytes (bytes) {
+ if (bytes === this._bytes) {
+ return {
+ input: this._input,
+ unit: this._unit,
+ }
+ }
+
+ if (bytes === null) {
+ return {
+ input: '',
+ unit: this.props.defaultUnit || DEFAULT_UNIT,
+ }
+ }
+
+ const { prefix, value } = formatSizeRaw(bytes)
+ return {
+ input: String(round(value, 2)),
+ unit: `${prefix}B`,
+ }
+ }
+
+ get value () {
+ const { input, unit } = this.state
+
+ if (!input) {
+ return null
+ }
+
+ return parseSize(`${+input} ${unit}`)
+ }
+
+ set value (value) {
+ if (
+ process.env.NODE_ENV !== 'production' &&
+ this.props.value !== undefined
+ ) {
+ throw new Error('cannot set value of controlled SizeInput')
+ }
+ this.setState(this._createStateFromBytes(value))
+ }
+
+ _onChange (input, unit) {
+ const { onChange } = this.props
+
+ // Empty input equals null.
+ const bytes = input ? parseSize(`${+input} ${unit}`) : null
+
+ const isControlled = this.props.value !== undefined
+ if (isControlled) {
+ // Store input and unit for this change to update correctly on new
+ // props.
+ this._bytes = bytes
+ this._input = input
+ this._unit = unit
+ } else {
+ this.setState({ input, unit })
+
+ // onChange is optional in uncontrolled mode.
+ if (!onChange) {
+ return
+ }
+ }
+
+ onChange(bytes)
+ }
+
+ _updateNumber = event => {
+ const input = event.target.value
+
+ if (!input) {
+ return this._onChange(input, this.state.unit)
+ }
+
+ const number = +input
+
+ if (Number.isNaN(number)) {
+ return
+ }
+
+ // Same numeric value: simply update the input.
+ const prevInput = this.state.input
+ if (prevInput && +prevInput === number) {
+ return this.setState({ input })
+ }
+
+ this._onChange(input, this.state.unit)
+ }
+
+ _updateUnit = unit => {
+ const { input } = this.state
+
+ // 0 is always 0, no matter the unit.
+ if (+input) {
+ this._onChange(input, unit)
+ } else {
+ this.setState({ unit })
+ }
+ }
+
+ render () {
+ const {
+ autoFocus,
+ className,
+ readOnly,
+ placeholder,
+ required,
+ style,
+ } = this.props
+
+ return (
+
+
+
+
+ {map(UNITS, unit => (
+ this._updateUnit(unit)}>
+ {unit}
+
+ ))}
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/common/form/select.js b/packages/xo-web/src/common/form/select.js
new file mode 100644
index 000000000..6f1fefdce
--- /dev/null
+++ b/packages/xo-web/src/common/form/select.js
@@ -0,0 +1,177 @@
+import classNames from 'classnames'
+import isEmpty from 'lodash/isEmpty'
+import PropTypes from 'prop-types'
+import React from 'react'
+import ReactSelect from 'react-select'
+import uncontrollableInput from 'uncontrollable-input'
+import {
+ AutoSizer,
+ CellMeasurer,
+ CellMeasurerCache,
+ List,
+} from 'react-virtualized'
+
+const SELECT_STYLE = {
+ minWidth: '10em',
+}
+const MENU_STYLE = {
+ overflow: 'hidden',
+}
+
+@uncontrollableInput()
+export default class Select extends React.PureComponent {
+ static defaultProps = {
+ maxHeight: 200,
+
+ multi: ReactSelect.defaultProps.multi,
+ options: [],
+ required: ReactSelect.defaultProps.required,
+ simpleValue: ReactSelect.defaultProps.simpleValue,
+ valueKey: ReactSelect.defaultProps.valueKey,
+ }
+
+ static propTypes = {
+ autoSelectSingleOption: PropTypes.bool, // default to props.required
+ maxHeight: PropTypes.number,
+ options: PropTypes.array.isRequired, // cannot be an object
+ }
+
+ _cellMeasurerCache = new CellMeasurerCache({
+ fixedWidth: true,
+ })
+
+ // https://github.com/JedWatson/react-select/blob/dd32c27d7ea338a93159da5e40bc06697d0d86f9/src/utils/defaultMenuRenderer.js#L4
+ _renderMenu (opts) {
+ const { focusOption, options, selectValue } = opts
+
+ const focusFromEvent = event =>
+ focusOption(options[event.currentTarget.dataset.index])
+ const selectFromEvent = event =>
+ selectValue(options[event.currentTarget.dataset.index])
+ const renderRow = opts2 =>
+ this._renderRow(opts, opts2, focusFromEvent, selectFromEvent)
+
+ let focusedOptionIndex = options.indexOf(opts.focusedOption)
+ if (focusedOptionIndex === -1) {
+ focusedOptionIndex = undefined
+ }
+
+ const { length } = options
+ const { maxHeight } = this.props
+ const { rowHeight } = this._cellMeasurerCache
+
+ let height = 0
+ for (let i = 0; i < length; ++i) {
+ height += rowHeight({ index: i })
+ if (height > maxHeight) {
+ height = maxHeight
+ break
+ }
+ }
+
+ return (
+
+ {({ width }) => (
+
+ )}
+
+ )
+ }
+ _renderMenu = this._renderMenu.bind(this)
+
+ _renderRow (
+ {
+ focusedOption,
+ focusOption,
+ inputValue,
+ optionClassName,
+ optionRenderer,
+ options,
+ selectValue,
+ },
+ { index, key, parent, style },
+ focusFromEvent,
+ selectFromEvent
+ ) {
+ const option = options[index]
+ const { disabled } = option
+
+ return (
+
+
+ {optionRenderer(option, index, inputValue)}
+
+
+ )
+ }
+
+ componentDidMount () {
+ this.componentDidUpdate()
+ }
+
+ componentDidUpdate () {
+ const { props } = this
+ const {
+ autoSelectSingleOption = props.required,
+ multi,
+ options,
+ simpleValue,
+ value,
+ } = props
+ if (
+ autoSelectSingleOption &&
+ options != null &&
+ options.length === 1 &&
+ (value == null ||
+ (simpleValue && value === '') ||
+ (multi && value.length === 0))
+ ) {
+ const option = options[0]
+ props.onChange(
+ simpleValue ? option[props.valueKey] : multi ? [option] : option
+ )
+ }
+ }
+
+ render () {
+ const { props } = this
+ const { multi } = props
+ return (
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/common/form/toggle.js b/packages/xo-web/src/common/form/toggle.js
new file mode 100644
index 000000000..f02331482
--- /dev/null
+++ b/packages/xo-web/src/common/form/toggle.js
@@ -0,0 +1,46 @@
+import React from 'react'
+import classNames from 'classnames'
+import uncontrollableInput from 'uncontrollable-input'
+
+import Component from '../base-component'
+import Icon from '../icon'
+import propTypes from '../prop-types-decorator'
+
+@uncontrollableInput()
+@propTypes({
+ className: propTypes.string,
+ onChange: propTypes.func.isRequired,
+ icon: propTypes.string,
+ iconOn: propTypes.string,
+ iconOff: propTypes.string,
+ iconSize: propTypes.number,
+ value: propTypes.bool.isRequired,
+})
+export default class Toggle extends Component {
+ static defaultProps = {
+ iconOn: 'toggle-on',
+ iconOff: 'toggle-off',
+ iconSize: 2,
+ }
+
+ _toggle = () => {
+ const { props } = this
+ props.onChange(!props.value)
+ }
+
+ render () {
+ const { props } = this
+
+ return (
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/common/get-event-value.js b/packages/xo-web/src/common/get-event-value.js
new file mode 100644
index 000000000..e1b49355a
--- /dev/null
+++ b/packages/xo-web/src/common/get-event-value.js
@@ -0,0 +1,15 @@
+// If the param is an event, returns the value of it's target,
+// otherwise returns the param.
+const getEventValue = event => {
+ let target
+ if (!event || !(target = event.target)) {
+ return event
+ }
+
+ return target.nodeName.toLowerCase() === 'input' &&
+ target.type.toLowerCase() === 'checkbox'
+ ? target.checked
+ : target.value
+}
+
+export { getEventValue as default }
diff --git a/packages/xo-web/src/common/grid.js b/packages/xo-web/src/common/grid.js
new file mode 100644
index 000000000..f799acecf
--- /dev/null
+++ b/packages/xo-web/src/common/grid.js
@@ -0,0 +1,61 @@
+import classNames from 'classnames'
+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,
+ smallSize: propTypes.number,
+ mediumSize: propTypes.number,
+ largeSize: propTypes.number,
+ 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,
+ }) => (
+
+ {children}
+
+ )
+)
+
+// This is the root component of the grid layout, containers should not be
+// nested.
+export const Container = propTypes({
+ className: propTypes.string,
+})(({ children, className }) => (
+ {children}
+))
+
+// Only columns can be children of a row.
+export const Row = propTypes({
+ className: propTypes.string,
+})(({ children, className }) => (
+ {children}
+))
diff --git a/packages/xo-web/src/common/grid.spec.js b/packages/xo-web/src/common/grid.spec.js
new file mode 100644
index 000000000..756d55a7f
--- /dev/null
+++ b/packages/xo-web/src/common/grid.spec.js
@@ -0,0 +1,13 @@
+/* eslint-env jest */
+
+import React from 'react'
+import { forEach } from 'lodash'
+import { shallow } from 'enzyme'
+
+import * as grid from './grid'
+
+forEach(grid, (Component, name) => {
+ it(name, () => {
+ expect(shallow( )).toMatchSnapshot()
+ })
+})
diff --git a/packages/xo-web/src/common/home-filters.js b/packages/xo-web/src/common/home-filters.js
new file mode 100644
index 000000000..8b5a8fbf0
--- /dev/null
+++ b/packages/xo-web/src/common/home-filters.js
@@ -0,0 +1,33 @@
+const common = {
+ homeFilterNone: '',
+}
+
+export const VM = {
+ ...common,
+ homeFilterPendingVms: 'current_operations:"" ',
+ homeFilterNonRunningVms: '!power_state:running ',
+ homeFilterHvmGuests: 'virtualizationMode:hvm ',
+ homeFilterRunningVms: 'power_state:running ',
+ homeFilterTags: 'tags:',
+}
+
+export const host = {
+ ...common,
+ homeFilterRunningHosts: 'power_state:running ',
+ homeFilterTags: 'tags:',
+}
+
+export const pool = {
+ ...common,
+ homeFilterTags: 'tags:',
+}
+
+export const vmTemplate = {
+ ...common,
+ homeFilterTags: 'tags:',
+}
+
+export const SR = {
+ ...common,
+ homeFilterTags: 'tags:',
+}
diff --git a/packages/xo-web/src/common/home-tags.js b/packages/xo-web/src/common/home-tags.js
new file mode 100644
index 000000000..3048b7c04
--- /dev/null
+++ b/packages/xo-web/src/common/home-tags.js
@@ -0,0 +1,40 @@
+import * as CM from 'complex-matcher'
+import React from 'react'
+
+import Component from './base-component'
+import propTypes from './prop-types-decorator'
+import Tags from './tags'
+
+@propTypes({
+ labels: propTypes.arrayOf(React.PropTypes.string).isRequired,
+ onAdd: propTypes.func,
+ onChange: propTypes.func,
+ onDelete: propTypes.func,
+ type: propTypes.string,
+})
+export default class HomeTags extends Component {
+ static contextTypes = {
+ router: React.PropTypes.object,
+ }
+
+ _onClick = label => {
+ const s = encodeURIComponent(
+ new CM.Property('tags', new CM.String(label)).toString()
+ )
+ const t = encodeURIComponent(this.props.type)
+
+ this.context.router.push(`/home?t=${t}&s=${s}`)
+ }
+
+ render () {
+ return (
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/common/hosts-patches-table.js b/packages/xo-web/src/common/hosts-patches-table.js
new file mode 100644
index 000000000..5bf75b231
--- /dev/null
+++ b/packages/xo-web/src/common/hosts-patches-table.js
@@ -0,0 +1,232 @@
+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 Link from './link'
+import propTypes from './prop-types-decorator'
+import SortedTable from './sorted-table'
+import TabButton from './tab-button'
+import { connectStore } from './utils'
+import {
+ createGetObjectsOfType,
+ createFilter,
+ createSelector,
+} from './selectors'
+import {
+ installAllHostPatches,
+ installAllPatchesOnPool,
+ subscribeHostMissingPatches,
+} from './xo'
+
+// ===================================================================
+
+const MISSING_PATCHES_COLUMNS = [
+ {
+ name: _('srHost'),
+ itemRenderer: host => (
+ {host.name_label}
+ ),
+ sortCriteria: host => host.name_label,
+ },
+ {
+ name: _('hostDescription'),
+ itemRenderer: host => host.name_description,
+ sortCriteria: host => host.name_description,
+ },
+ {
+ name: _('hostMissingPatches'),
+ itemRenderer: (host, { missingPatches }) => (
+ {missingPatches[host.id]}
+ ),
+ sortCriteria: (host, { missingPatches }) => missingPatches[host.id],
+ },
+ {
+ name: _('patchUpdateButton'),
+ itemRenderer: (host, { installAllHostPatches }) => (
+
+ ),
+ },
+]
+
+const POOLS_MISSING_PATCHES_COLUMNS = [
+ {
+ name: _('srPool'),
+ itemRenderer: (host, { pools }) => {
+ const pool = pools[host.$pool]
+ return {pool.name_label}
+ },
+ sortCriteria: (host, { pools }) => pools[host.$pool].name_label,
+ },
+].concat(MISSING_PATCHES_COLUMNS)
+
+// Small component to homogenize Button usage in HostsPatchesTable
+const ActionButton_ = ({ children, labelId, ...props }) => (
+
+ {children}
+
+)
+
+// ===================================================================
+
+@connectStore({
+ hostsById: createGetObjectsOfType('host').groupBy('id'),
+})
+class HostsPatchesTable extends Component {
+ constructor (props) {
+ super(props)
+ this.state.missingPatches = {}
+ }
+
+ _getHosts = createFilter(
+ () => this.props.hosts,
+ createSelector(
+ () => this.state.missingPatches,
+ missingPatches => host => missingPatches[host.id]
+ )
+ )
+
+ _subscribeMissingPatches = (hosts = this.props.hosts) => {
+ const { hostsById } = this.props
+
+ const unsubs = map(
+ hosts,
+ host =>
+ hostsById
+ ? subscribeHostMissingPatches(hostsById[host.id][0], patches =>
+ this.setState({
+ missingPatches: {
+ ...this.state.missingPatches,
+ [host.id]: patches.length,
+ },
+ })
+ )
+ : noop
+ )
+
+ if (this.unsubscribeMissingPatches !== undefined) {
+ this.unsubscribeMissingPatches()
+ }
+
+ this.unsubscribeMissingPatches = () => forEach(unsubs, unsub => unsub())
+ }
+
+ _installAllMissingPatches = () => {
+ const pools = {}
+ forEach(this._getHosts(), host => {
+ pools[host.$pool] = true
+ })
+
+ return Promise.all(map(keys(pools), installAllPatchesOnPool))
+ }
+
+ componentDidMount () {
+ // Force one Portal refresh.
+ // Because Portal cannot see the container reference at first rendering.
+ this.forceUpdate()
+ this._subscribeMissingPatches()
+ }
+
+ componentWillReceiveProps (nextProps) {
+ if (nextProps.hosts !== this.props.hosts) {
+ this._subscribeMissingPatches(nextProps.hosts)
+ }
+ }
+
+ componentWillUnmount () {
+ this.unsubscribeMissingPatches()
+ }
+
+ render () {
+ const {
+ buttonsGroupContainer,
+ container,
+ displayPools,
+ pools,
+ useTabButton,
+ } = this.props
+
+ const hosts = this._getHosts()
+ const noPatches = isEmpty(hosts)
+
+ const Container = container || 'div'
+
+ const Button = useTabButton ? TabButton : ActionButton_
+
+ return (
+
+ {!noPatches ? (
+
+ ) : (
+
{_('patchNothing')}
+ )}
+
buttonsGroupContainer()}>
+
+
+
+
+
+ )
+ }
+}
+
+// ===================================================================
+
+@connectStore(() => {
+ const getPools = createGetObjectsOfType('pool')
+
+ return {
+ pools: getPools,
+ }
+})
+class HostsPatchesTableByPool extends Component {
+ render () {
+ const { props } = this
+ return
+ }
+}
+
+// ===================================================================
+
+export default propTypes({
+ buttonsGroupContainer: propTypes.func.isRequired,
+ container: propTypes.any,
+ displayPools: propTypes.bool,
+ hosts: propTypes.oneOfType([
+ propTypes.arrayOf(propTypes.object),
+ propTypes.objectOf(propTypes.object),
+ ]).isRequired,
+ useTabButton: propTypes.bool,
+})(
+ props =>
+ props.displayPools ? (
+
+ ) : (
+
+ )
+)
diff --git a/packages/xo-web/src/common/icon.js b/packages/xo-web/src/common/icon.js
new file mode 100644
index 000000000..45f6ce7ef
--- /dev/null
+++ b/packages/xo-web/src/common/icon.js
@@ -0,0 +1,24 @@
+import classNames from 'classnames'
+import isInteger from 'lodash/isInteger'
+import React from 'react'
+
+import propTypes from './prop-types-decorator'
+
+const Icon = ({ icon, size = 1, color, fixedWidth, ...props }) => {
+ props.className = classNames(
+ props.className,
+ icon !== undefined ? `xo-icon-${icon}` : 'fa', // Without icon prop, is a placeholder.
+ isInteger(size) ? `fa-${size}x` : `fa-${size}`,
+ color,
+ fixedWidth && 'fa-fw'
+ )
+
+ return
+}
+propTypes(Icon)({
+ color: propTypes.string,
+ fixedWidth: propTypes.bool,
+ icon: propTypes.string,
+ size: propTypes.oneOfType([propTypes.string, propTypes.number]),
+})
+export default Icon
diff --git a/packages/xo-web/src/common/intl/index.js b/packages/xo-web/src/common/intl/index.js
new file mode 100644
index 000000000..124d18101
--- /dev/null
+++ b/packages/xo-web/src/common/intl/index.js
@@ -0,0 +1,111 @@
+import isFunction from 'lodash/isFunction'
+import isString from 'lodash/isString'
+import moment from 'moment'
+import PropTypes from 'prop-types'
+import React, { Component } from 'react'
+import { connect } from 'react-redux'
+import { FormattedMessage, IntlProvider as IntlProvider_ } from 'react-intl'
+
+import locales from './locales'
+import messages from './messages'
+import Tooltip from '.././tooltip'
+import { createSelector } from '.././selectors'
+
+// ===================================================================
+
+// Params:
+//
+// - props (optional): properties to add to the FormattedMessage
+// - messageId: identifier of the message to format/translate
+// - values (optional): values to pass to the message
+// - render (optional): a function receiving the React nodes of the
+// translated message and returning the React node to render
+const getMessage = (props, messageId, values, render) => {
+ if (isString(props)) {
+ render = values
+ values = messageId
+ messageId = props
+ props = undefined
+ }
+
+ const message = messages[messageId]
+ if (process.env.NODE_ENV !== 'production' && !message) {
+ throw new Error(`no message defined for ${messageId}`)
+ }
+
+ if (isFunction(values)) {
+ render = values
+ values = undefined
+ }
+
+ return (
+
+ {render}
+
+ )
+}
+getMessage.keyValue = (key, value) =>
+ getMessage('keyValue', {
+ key: {key} ,
+ value,
+ })
+
+export { getMessage as default }
+
+export { messages }
+
+@connect(({ lang }) => ({ lang }))
+export class IntlProvider extends Component {
+ static propTypes = {
+ children: PropTypes.node.isRequired,
+ lang: PropTypes.string.isRequired,
+ }
+
+ render () {
+ const { lang, children } = this.props
+ // Adding a key prop is a work-around suggested by react-intl documentation
+ // to make sure changes to the locale trigger a re-render of the child components
+ // https://github.com/yahoo/react-intl/wiki/Components#dynamic-language-selection
+ //
+ // FIXME: remove the key prop when React context propagation is fixed (https://github.com/facebook/react/issues/2517)
+ return (
+
+ {children}
+
+ )
+ }
+}
+
+const parseDuration = milliseconds => {
+ let seconds = Math.floor(milliseconds / 1e3)
+ const days = Math.floor(seconds / 86400)
+ seconds -= days * 86400
+ const hours = Math.floor(seconds / 3600)
+ seconds -= hours * 3600
+ const minutes = Math.floor(seconds / 60)
+ seconds -= minutes * 60
+ return { days, hours, minutes, seconds }
+}
+
+@connect(({ lang }) => ({ lang }))
+export class FormattedDuration extends Component {
+ _parseDuration = createSelector(() => this.props.duration, parseDuration)
+
+ _humanizeDuration = createSelector(
+ () => this.props.duration,
+ () => this.props.lang,
+ (duration, lang) =>
+ moment
+ .duration(duration)
+ .locale(lang)
+ .humanize()
+ )
+
+ render () {
+ return (
+
+ {this._humanizeDuration()}
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/common/intl/locales/.index-modules b/packages/xo-web/src/common/intl/locales/.index-modules
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/xo-web/src/common/intl/locales/es.js b/packages/xo-web/src/common/intl/locales/es.js
new file mode 100644
index 000000000..c334f38d5
--- /dev/null
+++ b/packages/xo-web/src/common/intl/locales/es.js
@@ -0,0 +1,3838 @@
+// See http://momentjs.com/docs/#/use-it/browserify/
+import 'moment/locale/es'
+
+import reactIntlData from 'react-intl/locale-data/es'
+import { addLocaleData } from 'react-intl'
+addLocaleData(reactIntlData)
+
+// ===================================================================
+
+export default {
+ // Original text: 'Connecting'
+ statusConnecting: undefined,
+
+ // Original text: 'Disconnected'
+ statusDisconnected: undefined,
+
+ // Original text: 'Loading…'
+ statusLoading: undefined,
+
+ // Original text: 'Page not found'
+ errorPageNotFound: undefined,
+
+ // Original text: 'no such item'
+ errorNoSuchItem: undefined,
+
+ // Original text: "Long click to edit"
+ editableLongClickPlaceholder: 'Click largo para editar',
+
+ // Original text: "Click to edit"
+ editableClickPlaceholder: 'Click para editar',
+
+ // Original text: 'Browse files'
+ browseFiles: undefined,
+
+ // Original text: "OK"
+ alertOk: 'OK',
+
+ // Original text: "OK"
+ confirmOk: 'OK',
+
+ // Original text: "Cancel"
+ genericCancel: 'Cancelar',
+
+ // Original text: 'On error'
+ onError: undefined,
+
+ // Original text: 'Successful'
+ successful: undefined,
+
+ // Original text: 'Managed disks'
+ filterOnlyManaged: undefined,
+
+ // Original text: 'Orphaned disks'
+ filterOnlyOrphaned: undefined,
+
+ // Original text: 'Normal disks'
+ filterOnlyRegular: undefined,
+
+ // Original text: 'Snapshot disks'
+ filterOnlySnapshots: undefined,
+
+ // Original text: 'Unmanaged disks'
+ filterOnlyUnmanaged: undefined,
+
+ // Original text: 'Copy to clipboard'
+ copyToClipboard: undefined,
+
+ // Original text: 'Master'
+ pillMaster: undefined,
+
+ // Original text: "Home"
+ homePage: 'Inicio',
+
+ // Original text: 'VMs'
+ homeVmPage: undefined,
+
+ // Original text: 'Hosts'
+ homeHostPage: undefined,
+
+ // Original text: 'Pools'
+ homePoolPage: undefined,
+
+ // Original text: 'Templates'
+ homeTemplatePage: undefined,
+
+ // Original text: 'Storages'
+ homeSrPage: undefined,
+
+ // Original text: "Dashboard"
+ dashboardPage: 'Resumen',
+
+ // Original text: "Overview"
+ overviewDashboardPage: 'Vista General',
+
+ // Original text: "Visualizations"
+ overviewVisualizationDashboardPage: 'Visualizaciones',
+
+ // Original text: "Statistics"
+ overviewStatsDashboardPage: 'Estadísticas',
+
+ // Original text: "Health"
+ overviewHealthDashboardPage: 'Estado',
+
+ // Original text: "Self service"
+ selfServicePage: 'Auto servicio',
+
+ // Original text: "Backup"
+ backupPage: 'Backup',
+
+ // Original text: "Jobs"
+ jobsPage: 'Trabajos',
+
+ // Original text: "Updates"
+ updatePage: 'Actualizaciones',
+
+ // Original text: "Settings"
+ settingsPage: 'Configuración',
+
+ // Original text: "Servers"
+ settingsServersPage: 'Servidores',
+
+ // Original text: "Users"
+ settingsUsersPage: 'Usuarios',
+
+ // Original text: "Groups"
+ settingsGroupsPage: 'Grupos',
+
+ // Original text: "ACLs"
+ settingsAclsPage: 'Control de acceso',
+
+ // Original text: "Plugins"
+ settingsPluginsPage: 'Plugins',
+
+ // Original text: 'Logs'
+ settingsLogsPage: undefined,
+
+ // Original text: 'IPs'
+ settingsIpsPage: undefined,
+
+ // Original text: 'Config'
+ settingsConfigPage: undefined,
+
+ // Original text: "About"
+ aboutPage: 'Acerca de',
+
+ // Original text: 'About XO {xoaPlan}'
+ aboutXoaPlan: undefined,
+
+ // Original text: "New"
+ newMenu: 'Nuevo',
+
+ // Original text: "Tasks"
+ taskMenu: 'Tareas',
+
+ // Original text: 'Tasks'
+ taskPage: undefined,
+
+ // Original text: "VM"
+ newVmPage: 'VM',
+
+ // Original text: "Storage"
+ newSrPage: 'Almacenamiento',
+
+ // Original text: "Server"
+ newServerPage: 'Servidor',
+
+ // Original text: "Import"
+ newImport: 'Importar',
+
+ // Original text: 'XOSAN'
+ xosan: undefined,
+
+ // Original text: "Overview"
+ backupOverviewPage: 'Visión General',
+
+ // Original text: "New"
+ backupNewPage: 'Nuevo',
+
+ // Original text: "Remotes"
+ backupRemotesPage: 'Remotos',
+
+ // Original text: "Restore"
+ backupRestorePage: 'Restaurar',
+
+ // Original text: 'File restore'
+ backupFileRestorePage: undefined,
+
+ // Original text: "Schedule"
+ schedule: 'Programación',
+
+ // Original text: "New VM backup"
+ newVmBackup: 'Nuevo backup de VM',
+
+ // Original text: "Edit VM backup"
+ editVmBackup: 'Editar backup de VM',
+
+ // Original text: "Backup"
+ backup: 'Backup',
+
+ // Original text: "Rolling Snapshot"
+ rollingSnapshot: 'Snapshot rotatorio',
+
+ // Original text: "Delta Backup"
+ deltaBackup: 'Backup diferencial',
+
+ // Original text: "Disaster Recovery"
+ disasterRecovery: 'Recuperación de desastres',
+
+ // Original text: "Continuous Replication"
+ continuousReplication: 'Replicación continua',
+
+ // Original text: "Overview"
+ jobsOverviewPage: 'Vistazo',
+
+ // Original text: "New"
+ jobsNewPage: 'Nuevo',
+
+ // Original text: "Scheduling"
+ jobsSchedulingPage: 'Programación',
+
+ // Original text: "Custom Job"
+ customJob: 'Trabajo personalizado',
+
+ // Original text: 'User'
+ userPage: undefined,
+
+ // Original text: 'No support'
+ noSupport: undefined,
+
+ // Original text: 'Free upgrade!'
+ freeUpgrade: undefined,
+
+ // Original text: "Sign out"
+ signOut: 'Salir',
+
+ // Original text: 'Edit my settings {username}'
+ editUserProfile: undefined,
+
+ // Original text: "Fetching data…"
+ homeFetchingData: 'Recuperando datos…',
+
+ // Original text: "Welcome on Xen Orchestra!"
+ homeWelcome: '¡Bienvenido a Xen Orchestra!',
+
+ // Original text: "Add your XenServer hosts or pools"
+ homeWelcomeText: 'Añade tus hosts/pools de XenServer',
+
+ // Original text: 'Some XenServers have been registered but are not connected'
+ homeConnectServerText: undefined,
+
+ // Original text: "Want some help?"
+ homeHelp: '¿Necesitas ayuda?',
+
+ // Original text: "Add server"
+ homeAddServer: 'Añadir servidor',
+
+ // Original text: 'Connect servers'
+ homeConnectServer: undefined,
+
+ // Original text: "Online Doc"
+ homeOnlineDoc: 'Documentación en línea',
+
+ // Original text: "Pro Support"
+ homeProSupport: 'Soporte Pro',
+
+ // Original text: "There are no VMs!"
+ homeNoVms: '¡No hay VMs!',
+
+ // Original text: "Or…"
+ homeNoVmsOr: 'O…',
+
+ // Original text: "Import VM"
+ homeImportVm: 'Importar VM',
+
+ // Original text: "Import an existing VM in xva format"
+ homeImportVmMessage: 'Importar una VM existente en formato xva',
+
+ // Original text: "Restore a backup"
+ homeRestoreBackup: 'Restaurar un backup',
+
+ // Original text: "Restore a backup from a remote store"
+ homeRestoreBackupMessage: 'Restaurar un backup de un almacenamiento remoto',
+
+ // Original text: "This will create a new VM"
+ homeNewVmMessage: 'Esto creará una nueva VM',
+
+ // Original text: "Filters"
+ homeFilters: 'Filtros',
+
+ // Original text: 'No results! Click here to reset your filters'
+ homeNoMatches: undefined,
+
+ // Original text: "Pool"
+ homeTypePool: 'Pool',
+
+ // Original text: "Host"
+ homeTypeHost: 'Host',
+
+ // Original text: "VM"
+ homeTypeVm: 'VM',
+
+ // Original text: "SR"
+ homeTypeSr: 'SR',
+
+ // Original text: 'Template'
+ homeTypeVmTemplate: undefined,
+
+ // Original text: "Sort"
+ homeSort: 'Ordenar',
+
+ // Original text: "Pools"
+ homeAllPools: 'Pools',
+
+ // Original text: "Hosts"
+ homeAllHosts: 'Hosts',
+
+ // Original text: "Tags"
+ homeAllTags: 'Etiquetas',
+
+ // Original text: "New VM"
+ homeNewVm: 'Nueva VM',
+
+ // Original text: 'None'
+ homeFilterNone: undefined,
+
+ // Original text: "Running hosts"
+ homeFilterRunningHosts: 'Hosts en funcionamiento',
+
+ // Original text: "Disabled hosts"
+ homeFilterDisabledHosts: 'Hosts inhabilitados',
+
+ // Original text: "Running VMs"
+ homeFilterRunningVms: 'VMs en funcionamiento',
+
+ // Original text: "Non running VMs"
+ homeFilterNonRunningVms: 'VMs paradas',
+
+ // Original text: "Pending VMs"
+ homeFilterPendingVms: 'VMs pendientes',
+
+ // Original text: "HVM guests"
+ homeFilterHvmGuests: 'Invitados HVM',
+
+ // Original text: "Tags"
+ homeFilterTags: 'Etiquetas',
+
+ // Original text: "Sort by"
+ homeSortBy: 'Ordenar por',
+
+ // Original text: "Name"
+ homeSortByName: 'Nombre',
+
+ // Original text: "Power state"
+ homeSortByPowerstate: 'Estado alimentación',
+
+ // Original text: "RAM"
+ homeSortByRAM: 'RAM',
+
+ // Original text: "vCPUs"
+ homeSortByvCPUs: 'vCPUs',
+
+ // Original text: 'CPUs'
+ homeSortByCpus: undefined,
+
+ // Original text: 'Shared/Not shared'
+ homeSortByShared: undefined,
+
+ // Original text: 'Size'
+ homeSortBySize: undefined,
+
+ // Original text: 'Usage'
+ homeSortByUsage: undefined,
+
+ // Original text: 'Type'
+ homeSortByType: undefined,
+
+ // Original text: "{displayed, number}x {icon} (on {total, number})"
+ homeDisplayedItems: '{displayed, number}x {icon} (sobre {total, number})',
+
+ // Original text: "{selected, number}x {icon} selected (on {total, number})"
+ homeSelectedItems:
+ '{selected, number}x {icon} seleccionados (sobre {total, number})',
+
+ // Original text: "More"
+ homeMore: 'Más',
+
+ // Original text: "Migrate to…"
+ homeMigrateTo: 'Migrar a…',
+
+ // Original text: 'Missing patches'
+ homeMissingPaths: undefined,
+
+ // Original text: 'Master:'
+ homePoolMaster: undefined,
+
+ // Original text: 'Resource set: {resourceSet}'
+ homeResourceSet: undefined,
+
+ // Original text: 'High Availability'
+ highAvailability: undefined,
+
+ // Original text: 'Shared {type}'
+ srSharedType: undefined,
+
+ // Original text: 'Not shared {type}'
+ srNotSharedType: undefined,
+
+ // Original text: "Add"
+ add: 'Añadir',
+
+ // Original text: 'Select all'
+ selectAll: undefined,
+
+ // Original text: "Remove"
+ remove: 'Quitar',
+
+ // Original text: "Preview"
+ preview: 'Vista previa',
+
+ // Original text: "Item"
+ item: 'Elemento',
+
+ // Original text: "No selected value"
+ noSelectedValue: 'No se ha seleccionado un valor',
+
+ // Original text: "Choose user(s) and/or group(s)"
+ selectSubjects: 'Elegir usuario(s) y/o grupo(s)',
+
+ // Original text: "Select Object(s)…"
+ selectObjects: 'Elegir Objeto(s)…',
+
+ // Original text: "Choose a role"
+ selectRole: 'Elegir un rol',
+
+ // Original text: "Select Host(s)…"
+ selectHosts: 'Elegir Host(s)…',
+
+ // Original text: "Select object(s)…"
+ selectHostsVms: 'Elegir objeto(s)…',
+
+ // Original text: "Select Network(s)…"
+ selectNetworks: 'Elegir Red(es)…',
+
+ // Original text: "Select PIF(s)…"
+ selectPifs: 'Elegir PIF(s)…',
+
+ // Original text: "Select Pool(s)…"
+ selectPools: 'Elegir Pool(s)…',
+
+ // Original text: "Select Remote(s)…"
+ selectRemotes: 'Elegir almacenamiento(s) remoto(s)…',
+
+ // Original text: 'Select resource set(s)…'
+ selectResourceSets: undefined,
+
+ // Original text: 'Select template(s)…'
+ selectResourceSetsVmTemplate: undefined,
+
+ // Original text: 'Select SR(s)…'
+ selectResourceSetsSr: undefined,
+
+ // Original text: 'Select network(s)…'
+ selectResourceSetsNetwork: undefined,
+
+ // Original text: 'Select disk(s)…'
+ selectResourceSetsVdi: undefined,
+
+ // Original text: 'Select SSH key(s)…'
+ selectSshKey: undefined,
+
+ // Original text: "Select SR(s)…"
+ selectSrs: 'Elegir SR(s)…',
+
+ // Original text: "Select VM(s)…"
+ selectVms: 'Elegir VM(s)…',
+
+ // Original text: "Select VM template(s)…"
+ selectVmTemplates: 'Elegir plantilla(s) de VM…',
+
+ // Original text: "Select tag(s)…"
+ selectTags: 'Elegir etiqueta(s)…',
+
+ // Original text: "Select disk(s)…"
+ selectVdis: 'Elegir disco(s)…',
+
+ // Original text: 'Select timezone…'
+ selectTimezone: undefined,
+
+ // Original text: 'Select IP(s)…'
+ selectIp: undefined,
+
+ // Original text: 'Select IP pool(s)…'
+ selectIpPool: undefined,
+
+ // Original text: "Fill required informations."
+ fillRequiredInformations: 'Introducir la información requerida',
+
+ // Original text: "Fill informations (optional)"
+ fillOptionalInformations: 'Introducir datos (opcional)',
+
+ // Original text: "Reset"
+ selectTableReset: 'Reiniciar',
+
+ // Original text: "Month"
+ schedulingMonth: 'Mes',
+
+ // Original text: 'Every N month'
+ schedulingEveryNMonth: undefined,
+
+ // Original text: "Each selected month"
+ schedulingEachSelectedMonth: 'Cada mes seleccionado',
+
+ // Original text: 'Day'
+ schedulingDay: undefined,
+
+ // Original text: 'Every N day'
+ schedulingEveryNDay: undefined,
+
+ // Original text: "Each selected day"
+ schedulingEachSelectedDay: 'Cada día seleccionado',
+
+ // Original text: 'Switch to week days'
+ schedulingSetWeekDayMode: undefined,
+
+ // Original text: 'Switch to month days'
+ schedulingSetMonthDayMode: undefined,
+
+ // Original text: "Hour"
+ schedulingHour: 'Hora',
+
+ // Original text: "Each selected hour"
+ schedulingEachSelectedHour: 'Cada hora seleccionada',
+
+ // Original text: "Every N hour"
+ schedulingEveryNHour: 'Cada N horas',
+
+ // Original text: "Minute"
+ schedulingMinute: 'Minuto',
+
+ // Original text: "Each selected minute"
+ schedulingEachSelectedMinute: 'Cada minuto seleccionado',
+
+ // Original text: "Every N minute"
+ schedulingEveryNMinute: 'Cada N minutos',
+
+ // Original text: 'Every month'
+ selectTableAllMonth: undefined,
+
+ // Original text: 'Every day'
+ selectTableAllDay: undefined,
+
+ // Original text: 'Every hour'
+ selectTableAllHour: undefined,
+
+ // Original text: 'Every minute'
+ selectTableAllMinute: undefined,
+
+ // Original text: "Reset"
+ schedulingReset: 'Reiniciar',
+
+ // Original text: "Unknown"
+ unknownSchedule: 'Desconocido',
+
+ // Original text: 'Web browser timezone'
+ timezonePickerUseLocalTime: undefined,
+
+ // Original text: 'Server timezone ({value})'
+ serverTimezoneOption: undefined,
+
+ // Original text: 'Cron Pattern:'
+ cronPattern: undefined,
+
+ // Original text: "Cannot edit backup"
+ backupEditNotFoundTitle: 'Imposible editar backup',
+
+ // Original text: "Missing required info for edition"
+ backupEditNotFoundMessage: 'Falta información requerida para la edición',
+
+ // Original text: 'Successful'
+ successfulJobCall: undefined,
+
+ // Original text: 'Failed'
+ failedJobCall: undefined,
+
+ // Original text: 'In progress'
+ jobCallInProgess: undefined,
+
+ // Original text: 'size:'
+ jobTransferredDataSize: undefined,
+
+ // Original text: 'speed:'
+ jobTransferredDataSpeed: undefined,
+
+ // Original text: "Job"
+ job: 'Tarea',
+
+ // Original text: 'Job {job}'
+ jobModalTitle: undefined,
+
+ // Original text: "ID"
+ jobId: 'ID de Tarea',
+
+ // Original text: 'Type'
+ jobType: undefined,
+
+ // Original text: "Name"
+ jobName: 'Nombre',
+
+ // Original text: 'Name of your job (forbidden: "_")'
+ jobNamePlaceholder: undefined,
+
+ // Original text: "Start"
+ jobStart: 'Comenzar',
+
+ // Original text: "End"
+ jobEnd: 'Finalizar',
+
+ // Original text: "Duration"
+ jobDuration: 'Duración',
+
+ // Original text: "Status"
+ jobStatus: 'Estado',
+
+ // Original text: "Action"
+ jobAction: 'Acción',
+
+ // Original text: "Tag"
+ jobTag: 'Etiqueta',
+
+ // Original text: "Scheduling"
+ jobScheduling: 'Programación',
+
+ // Original text: "State"
+ jobState: 'Estado',
+
+ // Original text: 'Enabled'
+ jobStateEnabled: undefined,
+
+ // Original text: 'Disabled'
+ jobStateDisabled: undefined,
+
+ // Original text: 'Timezone'
+ jobTimezone: undefined,
+
+ // Original text: 'Server'
+ jobServerTimezone: undefined,
+
+ // Original text: "Run job"
+ runJob: 'Ejecutar tarea',
+
+ // Original text: "One shot running started. See overview for logs."
+ runJobVerbose: 'Comienzo del backup manual. Ir a Resumen para ver logs',
+
+ // Original text: "Started"
+ jobStarted: 'Comenzado',
+
+ // Original text: "Finished"
+ jobFinished: 'Finalizado',
+
+ // Original text: "Save"
+ saveBackupJob: 'Guardar',
+
+ // Original text: "Remove backup job"
+ deleteBackupSchedule: 'Eliminar tarea de backup',
+
+ // Original text: "Are you sure you want to delete this backup job?"
+ deleteBackupScheduleQuestion:
+ '¿Estás seguro de querer borrar esta tarea de backup?',
+
+ // Original text: "Enable immediately after creation"
+ scheduleEnableAfterCreation: 'Activar inmediatamente tras la creación',
+
+ // Original text: "You are editing Schedule {name} ({id}). Saving will override previous schedule state."
+ scheduleEditMessage:
+ 'Estás editando la Programación {name} ({id}). Se sobreescribirá el estado actual al guardar.',
+
+ // Original text: "You are editing job {name} ({id}). Saving will override previous job state."
+ jobEditMessage:
+ 'Estás editando la Tarea {name} ({id}). Se sobreescribirá el estado actual al guardar.',
+
+ // Original text: "No scheduled jobs."
+ noScheduledJobs: 'No hay tareas programadas',
+
+ // Original text: "No jobs found."
+ noJobs: 'No se han encontrado tareas',
+
+ // Original text: "No schedules found"
+ noSchedules: 'No se han encontrado programaciones',
+
+ // Original text: "Select a xo-server API command"
+ jobActionPlaceHolder: 'Elige un comando de la API de xo-server',
+
+ // Original text: ' Timeout (number of seconds after which a VM is considered failed)'
+ jobTimeoutPlaceHolder: undefined,
+
+ // Original text: 'Schedules'
+ jobSchedules: undefined,
+
+ // Original text: 'Name of your schedule'
+ jobScheduleNamePlaceHolder: undefined,
+
+ // Original text: 'Select a Job'
+ jobScheduleJobPlaceHolder: undefined,
+
+ // Original text: 'Job owner'
+ jobOwnerPlaceholder: undefined,
+
+ // Original text: "This job's creator no longer exists"
+ jobUserNotFound: undefined,
+
+ // Original text: "This backup's creator no longer exists"
+ backupUserNotFound: undefined,
+
+ // Original text: 'Backup owner'
+ backupOwner: undefined,
+
+ // Original text: "Select your backup type:"
+ newBackupSelection: 'Elige el tipo de backup',
+
+ // Original text: 'Select backup mode:'
+ smartBackupModeSelection: undefined,
+
+ // Original text: 'Normal backup'
+ normalBackup: undefined,
+
+ // Original text: 'Smart backup'
+ smartBackup: undefined,
+
+ // Original text: 'Local remote selected'
+ localRemoteWarningTitle: undefined,
+
+ // Original text: 'Warning: local remotes will use limited XOA disk space. Only for advanced users.'
+ localRemoteWarningMessage: undefined,
+
+ // Original text: 'Warning: this feature works only with XenServer 6.5 or newer.'
+ backupVersionWarning: undefined,
+
+ // Original text: 'VMs'
+ editBackupVmsTitle: undefined,
+
+ // Original text: 'VMs statuses'
+ editBackupSmartStatusTitle: undefined,
+
+ // Original text: 'Resident on'
+ editBackupSmartResidentOn: undefined,
+
+ // Original text: 'Pools'
+ editBackupSmartPools: undefined,
+
+ // Original text: 'Tags'
+ editBackupSmartTags: undefined,
+
+ // Original text: 'VMs Tags'
+ editBackupSmartTagsTitle: undefined,
+
+ // Original text: 'Reverse'
+ editBackupNot: undefined,
+
+ // Original text: 'Tag'
+ editBackupTagTitle: undefined,
+
+ // Original text: 'Report'
+ editBackupReportTitle: undefined,
+
+ // Original text: 'Automatically run as scheduled'
+ editBackupScheduleEnabled: undefined,
+
+ // Original text: 'Depth'
+ editBackupDepthTitle: undefined,
+
+ // Original text: 'Remote'
+ editBackupRemoteTitle: undefined,
+
+ // Original text: 'Delete the old backups first'
+ deleteOldBackupsFirst: undefined,
+
+ // Original text: "Remote stores for backup"
+ remoteList: 'Almacenamientos remotos para el backup',
+
+ // Original text: "New File System Remote"
+ newRemote: 'Nuevo Sistema de Archivos Remoto',
+
+ // Original text: "Local"
+ remoteTypeLocal: 'Local',
+
+ // Original text: "NFS"
+ remoteTypeNfs: 'NFS',
+
+ // Original text: "SMB"
+ remoteTypeSmb: 'Samba',
+
+ // Original text: "Type"
+ remoteType: 'Tipo',
+
+ // Original text: 'Test your remote'
+ remoteTestTip: undefined,
+
+ // Original text: 'Test Remote'
+ testRemote: undefined,
+
+ // Original text: 'Test failed for {name}'
+ remoteTestFailure: undefined,
+
+ // Original text: 'Test passed for {name}'
+ remoteTestSuccess: undefined,
+
+ // Original text: 'Error'
+ remoteTestError: undefined,
+
+ // Original text: 'Test Step'
+ remoteTestStep: undefined,
+
+ // Original text: 'Test file'
+ remoteTestFile: undefined,
+
+ // Original text: 'Test name'
+ remoteTestName: undefined,
+
+ // Original text: 'Remote name already exists!'
+ remoteTestNameFailure: undefined,
+
+ // Original text: 'The remote appears to work correctly'
+ remoteTestSuccessMessage: undefined,
+
+ // Original text: 'Connection failed'
+ remoteConnectionFailed: undefined,
+
+ // Original text: 'Name'
+ remoteName: undefined,
+
+ // Original text: 'Path'
+ remotePath: undefined,
+
+ // Original text: 'State'
+ remoteState: undefined,
+
+ // Original text: 'Device'
+ remoteDevice: undefined,
+
+ // Original text: 'Share'
+ remoteShare: undefined,
+
+ // Original text: 'Action'
+ remoteAction: undefined,
+
+ // Original text: 'Auth'
+ remoteAuth: undefined,
+
+ // Original text: 'Mounted'
+ remoteMounted: undefined,
+
+ // Original text: 'Unmounted'
+ remoteUnmounted: undefined,
+
+ // Original text: 'Connect'
+ remoteConnectTip: undefined,
+
+ // Original text: 'Disconnect'
+ remoteDisconnectTip: undefined,
+
+ // Original text: 'Connected'
+ remoteConnected: undefined,
+
+ // Original text: 'Disconnected'
+ remoteDisconnected: undefined,
+
+ // Original text: 'Delete'
+ remoteDeleteTip: undefined,
+
+ // Original text: 'remote name *'
+ remoteNamePlaceHolder: undefined,
+
+ // Original text: 'Name *'
+ remoteMyNamePlaceHolder: undefined,
+
+ // Original text: '/path/to/backup'
+ remoteLocalPlaceHolderPath: undefined,
+
+ // Original text: 'host *'
+ remoteNfsPlaceHolderHost: undefined,
+
+ // Original text: 'path/to/backup'
+ remoteNfsPlaceHolderPath: undefined,
+
+ // Original text: 'subfolder [path\\to\\backup]'
+ remoteSmbPlaceHolderRemotePath: undefined,
+
+ // Original text: 'Username'
+ remoteSmbPlaceHolderUsername: undefined,
+
+ // Original text: 'Password'
+ remoteSmbPlaceHolderPassword: undefined,
+
+ // Original text: 'Domain'
+ remoteSmbPlaceHolderDomain: undefined,
+
+ // Original text: '\\ *'
+ remoteSmbPlaceHolderAddressShare: undefined,
+
+ // Original text: 'password(fill to edit)'
+ remotePlaceHolderPassword: undefined,
+
+ // Original text: 'Create a new SR'
+ newSrTitle: undefined,
+
+ // Original text: "General"
+ newSrGeneral: 'General',
+
+ // Original text: "Select Storage Type:"
+ newSrTypeSelection: 'Elige el tipo de almacenamiento',
+
+ // Original text: "Settings"
+ newSrSettings: 'Configuración',
+
+ // Original text: "Storage Usage"
+ newSrUsage: 'Utilización del almacenamiento',
+
+ // Original text: "Summary"
+ newSrSummary: 'Sumario',
+
+ // Original text: "Host"
+ newSrHost: 'Host',
+
+ // Original text: "Type"
+ newSrType: 'Tipo',
+
+ // Original text: "Name"
+ newSrName: 'Nombre',
+
+ // Original text: "Description"
+ newSrDescription: 'Descripción',
+
+ // Original text: "Server"
+ newSrServer: 'Servidor',
+
+ // Original text: "Path"
+ newSrPath: 'Ruta',
+
+ // Original text: "IQN"
+ newSrIqn: 'IQN',
+
+ // Original text: "LUN"
+ newSrLun: 'LUN',
+
+ // Original text: "with auth."
+ newSrAuth: 'con autenticación',
+
+ // Original text: "User Name"
+ newSrUsername: 'Nombre de usuario',
+
+ // Original text: "Password"
+ newSrPassword: 'Clave',
+
+ // Original text: "Device"
+ newSrDevice: 'Dispositivo',
+
+ // Original text: "in use"
+ newSrInUse: 'en uso',
+
+ // Original text: "Size"
+ newSrSize: 'Tamaño',
+
+ // Original text: "Create"
+ newSrCreate: 'Crear',
+
+ // Original text: 'Storage name'
+ newSrNamePlaceHolder: undefined,
+
+ // Original text: 'Storage description'
+ newSrDescPlaceHolder: undefined,
+
+ // Original text: 'Address'
+ newSrAddressPlaceHolder: undefined,
+
+ // Original text: '[port]'
+ newSrPortPlaceHolder: undefined,
+
+ // Original text: 'Username'
+ newSrUsernamePlaceHolder: undefined,
+
+ // Original text: 'Password'
+ newSrPasswordPlaceHolder: undefined,
+
+ // Original text: 'Device, e.g /dev/sda…'
+ newSrLvmDevicePlaceHolder: undefined,
+
+ // Original text: '/path/to/directory'
+ newSrLocalPathPlaceHolder: undefined,
+
+ // Original text: "Users/Groups"
+ subjectName: 'Usuarios/Grupos',
+
+ // Original text: "Object"
+ objectName: 'Objeto',
+
+ // Original text: 'No acls found'
+ aclNoneFound: undefined,
+
+ // Original text: "Role"
+ roleName: 'Rol',
+
+ // Original text: 'Create'
+ aclCreate: undefined,
+
+ // Original text: "New Group Name"
+ newGroupName: 'Nuevo nombre del Grupo',
+
+ // Original text: "Create Group"
+ createGroup: 'Crear grupo',
+
+ // Original text: "Create"
+ createGroupButton: 'Crear',
+
+ // Original text: "Delete Group"
+ deleteGroup: 'Borrar grupo',
+
+ // Original text: "Are you sure you want to delete this group?"
+ deleteGroupConfirm: '¿Estás seguro de querer borrar este grupo?',
+
+ // Original text: "Remove user from Group"
+ removeUserFromGroup: '¿Quitar usuario del grupo?',
+
+ // Original text: "Are you sure you want to delete this user?"
+ deleteUserConfirm: '¿Estás seguro de querer borrar este usuario?',
+
+ // Original text: "Delete User"
+ deleteUser: 'Borrar usuario',
+
+ // Original text: 'no user'
+ noUser: undefined,
+
+ // Original text: "unknown user"
+ unknownUser: 'usuario desconocido',
+
+ // Original text: "No group found"
+ noGroupFound: 'Grupo no encontrado',
+
+ // Original text: "Name"
+ groupNameColumn: 'Nombre',
+
+ // Original text: "Users"
+ groupUsersColumn: 'Usuarios',
+
+ // Original text: "Add User"
+ addUserToGroupColumn: 'Añadir usuario',
+
+ // Original text: "Email"
+ userNameColumn: 'Email',
+
+ // Original text: "Permissions"
+ userPermissionColumn: 'Permisos',
+
+ // Original text: "Password"
+ userPasswordColumn: 'Clave',
+
+ // Original text: "Email"
+ userName: 'Email',
+
+ // Original text: "Password"
+ userPassword: 'Clave',
+
+ // Original text: "Create"
+ createUserButton: 'Crear',
+
+ // Original text: "No user found"
+ noUserFound: 'Usuario no encontrado',
+
+ // Original text: "User"
+ userLabel: 'Usuario',
+
+ // Original text: "Admin"
+ adminLabel: 'Admin',
+
+ // Original text: "No user in group"
+ noUserInGroup: 'Grupo sin usuarios',
+
+ // Original text: "{users, number} user{users, plural, one {} other {s}}"
+ countUsers: '{users, number} user{users, plural, one {} other {s}}',
+
+ // Original text: "Select Permission"
+ selectPermission: 'Seleccionar permiso',
+
+ // Original text: 'No plugins found'
+ noPlugins: undefined,
+
+ // Original text: "Auto-load at server start"
+ autoloadPlugin: 'Cargar al iniciar el servidor',
+
+ // Original text: "Save configuration"
+ savePluginConfiguration: 'Guardar configuración',
+
+ // Original text: "Delete configuration"
+ deletePluginConfiguration: 'Borrar configuración',
+
+ // Original text: "Plugin error"
+ pluginError: 'Error del Plugin',
+
+ // Original text: "Unknown error"
+ unknownPluginError: 'Error desconocido',
+
+ // Original text: "Purge plugin configuration"
+ purgePluginConfiguration: 'Purgar la configuración de plugins',
+
+ // Original text: "Are you sure you want to purge this configuration ?"
+ purgePluginConfigurationQuestion:
+ '¿Estás seguro de querer purgar esta configuración?',
+
+ // Original text: "Edit"
+ editPluginConfiguration: 'Editar',
+
+ // Original text: "Cancel"
+ cancelPluginEdition: 'Cancelar',
+
+ // Original text: "Plugin configuration"
+ pluginConfigurationSuccess: 'Configuración del Plugin',
+
+ // Original text: "Plugin configuration successfully saved!"
+ pluginConfigurationChanges:
+ '¡Configuración del Plugin guardada correctamente!',
+
+ // Original text: 'Predefined configuration'
+ pluginConfigurationPresetTitle: undefined,
+
+ // Original text: 'Choose a predefined configuration.'
+ pluginConfigurationChoosePreset: undefined,
+
+ // Original text: 'Apply'
+ applyPluginPreset: undefined,
+
+ // Original text: 'Save filter error'
+ saveNewUserFilterErrorTitle: undefined,
+
+ // Original text: 'Bad parameter: name must be given.'
+ saveNewUserFilterErrorBody: undefined,
+
+ // Original text: 'Name:'
+ filterName: undefined,
+
+ // Original text: 'Value:'
+ filterValue: undefined,
+
+ // Original text: 'Save new filter'
+ saveNewFilterTitle: undefined,
+
+ // Original text: 'Set custom filters'
+ setUserFiltersTitle: undefined,
+
+ // Original text: 'Are you sure you want to set custom filters?'
+ setUserFiltersBody: undefined,
+
+ // Original text: 'Remove custom filter'
+ removeUserFilterTitle: undefined,
+
+ // Original text: 'Are you sure you want to remove custom filter?'
+ removeUserFilterBody: undefined,
+
+ // Original text: 'Default filter'
+ defaultFilter: undefined,
+
+ // Original text: 'Default filters'
+ defaultFilters: undefined,
+
+ // Original text: 'Custom filters'
+ customFilters: undefined,
+
+ // Original text: 'Customize filters'
+ customizeFilters: undefined,
+
+ // Original text: 'Save custom filters'
+ saveCustomFilters: undefined,
+
+ // Original text: "Start"
+ startVmLabel: 'Iniciar',
+
+ // Original text: "Recovery start"
+ recoveryModeLabel: 'Inicio de rescate',
+
+ // Original text: "Suspend"
+ suspendVmLabel: 'Suspender',
+
+ // Original text: "Stop"
+ stopVmLabel: 'Parar',
+
+ // Original text: "Force shutdown"
+ forceShutdownVmLabel: 'Forzar apagado',
+
+ // Original text: "Reboot"
+ rebootVmLabel: 'Reiniciar',
+
+ // Original text: "Force reboot"
+ forceRebootVmLabel: 'Forzar reinicio',
+
+ // Original text: "Delete"
+ deleteVmLabel: 'Borrar',
+
+ // Original text: "Migrate"
+ migrateVmLabel: 'Migrar',
+
+ // Original text: "Snapshot"
+ snapshotVmLabel: 'Snapshot',
+
+ // Original text: "Export"
+ exportVmLabel: 'Exportar',
+
+ // Original text: "Resume"
+ resumeVmLabel: 'Reanudar',
+
+ // Original text: "Copy"
+ copyVmLabel: 'Copiar',
+
+ // Original text: "Clone"
+ cloneVmLabel: 'Clonar',
+
+ // Original text: "Fast clone"
+ fastCloneVmLabel: 'Clonado rápido',
+
+ // Original text: "Convert to template"
+ convertVmToTemplateLabel: 'Convertir en plantilla',
+
+ // Original text: "Console"
+ vmConsoleLabel: 'Consola',
+
+ // Original text: "Rescan all disks"
+ srRescan: 'Releer todos los discos',
+
+ // Original text: "Connect to all hosts"
+ srReconnectAll: 'Conectar a todos los hosts',
+
+ // Original text: "Disconnect from all hosts"
+ srDisconnectAll: 'Desconectar de todos los hosts',
+
+ // Original text: "Forget this SR"
+ srForget: 'Olvidar este SR',
+
+ // Original text: 'Forget SRs'
+ srsForget: undefined,
+
+ // Original text: "Remove this SR"
+ srRemoveButton: 'Borrar este SR',
+
+ // Original text: "No VDIs in this storage"
+ srNoVdis: 'No hay VDIs en éste almancenamiento',
+
+ // Original text: 'Pool RAM usage:'
+ poolTitleRamUsage: undefined,
+
+ // Original text: '{used} used on {total}'
+ poolRamUsage: undefined,
+
+ // Original text: 'Master:'
+ poolMaster: undefined,
+
+ // Original text: 'Display all hosts of this pool'
+ displayAllHosts: undefined,
+
+ // Original text: 'Display all storages of this pool'
+ displayAllStorages: undefined,
+
+ // Original text: 'Display all VMs of this pool'
+ displayAllVMs: undefined,
+
+ // Original text: "Hosts"
+ hostsTabName: 'Hosts',
+
+ // Original text: 'Vms'
+ vmsTabName: undefined,
+
+ // Original text: 'Srs'
+ srsTabName: undefined,
+
+ // Original text: "High Availability"
+ poolHaStatus: 'Alta Disponibilidad',
+
+ // Original text: "Enabled"
+ poolHaEnabled: 'Activado',
+
+ // Original text: "Disabled"
+ poolHaDisabled: 'Desactivado',
+
+ // Original text: "Name"
+ hostNameLabel: 'Nombre',
+
+ // Original text: "Description"
+ hostDescription: 'Descripción',
+
+ // Original text: "Memory"
+ hostMemory: 'Memoria',
+
+ // Original text: "No hosts"
+ noHost: 'Sin hosts',
+
+ // Original text: '{used}% used ({free} free)'
+ memoryLeftTooltip: undefined,
+
+ // Original text: 'PIF'
+ pif: undefined,
+
+ // Original text: "Name"
+ poolNetworkNameLabel: 'Nombre',
+
+ // Original text: "Description"
+ poolNetworkDescription: 'Descripción',
+
+ // Original text: "PIFs"
+ poolNetworkPif: 'PIFs',
+
+ // Original text: "No networks"
+ poolNoNetwork: 'Sin redes',
+
+ // Original text: "MTU"
+ poolNetworkMTU: 'MTU',
+
+ // Original text: "Connected"
+ poolNetworkPifAttached: 'Conectado',
+
+ // Original text: "Disconnected"
+ poolNetworkPifDetached: 'Desconectado',
+
+ // Original text: 'Show PIFs'
+ showPifs: undefined,
+
+ // Original text: 'Hide PIFs'
+ hidePifs: undefined,
+
+ // Original text: 'Show details'
+ showDetails: undefined,
+
+ // Original text: 'Hide details'
+ hideDetails: undefined,
+
+ // Original text: 'No stats'
+ poolNoStats: undefined,
+
+ // Original text: 'All hosts'
+ poolAllHosts: undefined,
+
+ // Original text: "Add SR"
+ addSrLabel: 'Añadir SR',
+
+ // Original text: "Add VM"
+ addVmLabel: 'Añadir VM',
+
+ // Original text: "Add Host"
+ addHostLabel: 'Añadir host',
+
+ // Original text: '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.'
+ hostNeedsPatchUpdate: undefined,
+
+ // Original text: "This host cannot be added to the pool because it's missing some patches."
+ hostNeedsPatchUpdateNoInstall: undefined,
+
+ // Original text: 'Adding host failed'
+ addHostErrorTitle: undefined,
+
+ // Original text: 'Host patches could not be homogenized.'
+ addHostNotHomogeneousErrorMessage: undefined,
+
+ // Original text: "Disconnect"
+ disconnectServer: 'Desconectar',
+
+ // Original text: "Start"
+ startHostLabel: 'Arrancar',
+
+ // Original text: "Stop"
+ stopHostLabel: 'Parar',
+
+ // Original text: "Enable"
+ enableHostLabel: 'Activar',
+
+ // Original text: "Disable"
+ disableHostLabel: 'Desactivar',
+
+ // Original text: "Restart toolstack"
+ restartHostAgent: 'Reiniciar toolstack',
+
+ // Original text: "Force reboot"
+ forceRebootHostLabel: 'Forzar reinicio',
+
+ // Original text: "Reboot"
+ rebootHostLabel: 'Reiniciar',
+
+ // Original text: 'Error while restarting host'
+ noHostsAvailableErrorTitle: undefined,
+
+ // Original text: 'Some VMs cannot be migrated before restarting this host. Please try force reboot.'
+ noHostsAvailableErrorMessage: undefined,
+
+ // Original text: 'Error while restarting hosts'
+ failHostBulkRestartTitle: undefined,
+
+ // Original text: '{failedHosts, number}/{totalHosts, number} host{failedHosts, plural, one {} other {s}} could not be restarted.'
+ failHostBulkRestartMessage: undefined,
+
+ // Original text: 'Reboot to apply updates'
+ rebootUpdateHostLabel: undefined,
+
+ // Original text: "Emergency mode"
+ emergencyModeLabel: 'Modo de Emergencia',
+
+ // Original text: "Storage"
+ storageTabName: 'Almacenamiento',
+
+ // Original text: "Patches"
+ patchesTabName: 'Parches',
+
+ // Original text: "Load average"
+ statLoad: 'Carga',
+
+ // Original text: 'RAM Usage: {memoryUsed}'
+ memoryHostState: undefined,
+
+ // Original text: "Hardware"
+ hardwareHostSettingsLabel: 'Hardware',
+
+ // Original text: "Address"
+ hostAddress: 'Dirección',
+
+ // Original text: "Status"
+ hostStatus: 'Estado',
+
+ // Original text: "Build number"
+ hostBuildNumber: 'Número de compilación',
+
+ // Original text: "iSCSI name"
+ hostIscsiName: 'Nombre iSCSI',
+
+ // Original text: "Version"
+ hostXenServerVersion: 'Versión',
+
+ // Original text: "Enabled"
+ hostStatusEnabled: 'Activado',
+
+ // Original text: "Disabled"
+ hostStatusDisabled: 'Desactivado',
+
+ // Original text: "Power on mode"
+ hostPowerOnMode: 'Modo de encendido',
+
+ // Original text: "Host uptime"
+ hostStartedSince: 'Tiempo encendido',
+
+ // Original text: "Toolstack uptime"
+ hostStackStartedSince: 'Tiempo de encendido del toolstack',
+
+ // Original text: "CPU model"
+ hostCpusModel: 'Modelo de CPU',
+
+ // Original text: "Core (socket)"
+ hostCpusNumber: 'Core (socket)',
+
+ // Original text: "Manufacturer info"
+ hostManufacturerinfo: 'Información del fabricante',
+
+ // Original text: "BIOS info"
+ hostBiosinfo: 'Información de BIOS',
+
+ // Original text: "Licence"
+ licenseHostSettingsLabel: 'Licencia',
+
+ // Original text: "Type"
+ hostLicenseType: 'Tipo',
+
+ // Original text: "Socket"
+ hostLicenseSocket: 'Socket',
+
+ // Original text: "Expiry"
+ hostLicenseExpiry: 'Expiración',
+
+ // Original text: 'Installed supplemental packs'
+ supplementalPacks: undefined,
+
+ // Original text: 'Install new supplemental pack'
+ supplementalPackNew: undefined,
+
+ // Original text: 'Install supplemental pack on every host'
+ supplementalPackPoolNew: undefined,
+
+ // Original text: '{name} (by {author})'
+ supplementalPackTitle: undefined,
+
+ // Original text: 'Installation started'
+ supplementalPackInstallStartedTitle: undefined,
+
+ // Original text: 'Installing new supplemental pack…'
+ supplementalPackInstallStartedMessage: undefined,
+
+ // Original text: 'Installation error'
+ supplementalPackInstallErrorTitle: undefined,
+
+ // Original text: 'The installation of the supplemental pack failed.'
+ supplementalPackInstallErrorMessage: undefined,
+
+ // Original text: 'Installation success'
+ supplementalPackInstallSuccessTitle: undefined,
+
+ // Original text: 'Supplemental pack successfully installed.'
+ supplementalPackInstallSuccessMessage: undefined,
+
+ // Original text: "Add a network"
+ networkCreateButton: 'Añadir red',
+
+ // Original text: 'Add a bonded network'
+ networkCreateBondedButton: undefined,
+
+ // Original text: "Device"
+ pifDeviceLabel: 'Dispositivo',
+
+ // Original text: "Network"
+ pifNetworkLabel: 'Red',
+
+ // Original text: "VLAN"
+ pifVlanLabel: 'VLAN',
+
+ // Original text: "Address"
+ pifAddressLabel: 'Dirección',
+
+ // Original text: 'Mode'
+ pifModeLabel: undefined,
+
+ // Original text: "MAC"
+ pifMacLabel: 'MAC',
+
+ // Original text: "MTU"
+ pifMtuLabel: 'MTU',
+
+ // Original text: "Status"
+ pifStatusLabel: 'Estado',
+
+ // Original text: "Connected"
+ pifStatusConnected: 'Conectado',
+
+ // Original text: "Disconnected"
+ pifStatusDisconnected: 'Desconectado',
+
+ // Original text: "No physical interface detected"
+ pifNoInterface: 'No se ha detectado ningún interface físico',
+
+ // Original text: 'This interface is currently in use'
+ pifInUse: undefined,
+
+ // Original text: 'Action'
+ pifAction: undefined,
+
+ // Original text: 'Default locking mode'
+ defaultLockingMode: undefined,
+
+ // Original text: 'Configure IP address'
+ pifConfigureIp: undefined,
+
+ // Original text: 'Invalid parameters'
+ configIpErrorTitle: undefined,
+
+ // Original text: 'IP address and netmask required'
+ configIpErrorMessage: undefined,
+
+ // Original text: 'Static IP address'
+ staticIp: undefined,
+
+ // Original text: 'Netmask'
+ netmask: undefined,
+
+ // Original text: 'DNS'
+ dns: undefined,
+
+ // Original text: 'Gateway'
+ gateway: undefined,
+
+ // Original text: "Add a storage"
+ addSrDeviceButton: 'Añadir un almacenamiento',
+
+ // Original text: "Name"
+ srNameLabel: 'Nombre',
+
+ // Original text: "Type"
+ srType: 'Tipo',
+
+ // Original text: 'Action'
+ pbdAction: undefined,
+
+ // Original text: "Status"
+ pbdStatus: 'Estado',
+
+ // Original text: "Connected"
+ pbdStatusConnected: 'Conectado',
+
+ // Original text: "Disconnected"
+ pbdStatusDisconnected: 'Desconectado',
+
+ // Original text: 'Connect'
+ pbdConnect: undefined,
+
+ // Original text: 'Disconnect'
+ pbdDisconnect: undefined,
+
+ // Original text: 'Forget'
+ pbdForget: undefined,
+
+ // Original text: "Shared"
+ srShared: 'Compartido',
+
+ // Original text: "Not shared"
+ srNotShared: 'No compartido',
+
+ // Original text: "No storage detected"
+ pbdNoSr: 'No se han detectado almacenamientos',
+
+ // Original text: "Name"
+ patchNameLabel: 'Nombre',
+
+ // Original text: "Install all patches"
+ patchUpdateButton: 'Instalar todos los parches',
+
+ // Original text: "Description"
+ patchDescription: 'Descripción',
+
+ // Original text: "Applied date"
+ patchApplied: 'Fecha de publicación',
+
+ // Original text: "Size"
+ patchSize: 'Tamaño',
+
+ // Original text: "Status"
+ patchStatus: 'Estado',
+
+ // Original text: "Applied"
+ patchStatusApplied: 'Aplicado',
+
+ // Original text: "Missing patches"
+ patchStatusNotApplied: 'Parches pendientes',
+
+ // Original text: "No patch detected"
+ patchNothing: 'No se ha detectado el parche',
+
+ // Original text: "Release date"
+ patchReleaseDate: 'Fecha de publicación',
+
+ // Original text: "Guidance"
+ patchGuidance: 'Guía',
+
+ // Original text: "Action"
+ patchAction: 'Acción',
+
+ // Original text: 'Applied patches'
+ hostAppliedPatches: undefined,
+
+ // Original text: "Missing patches"
+ hostMissingPatches: 'Parches pendientes',
+
+ // Original text: "Host up-to-date!"
+ hostUpToDate: '¡Host al día!',
+
+ // Original text: 'Non-recommended patch install'
+ installPatchWarningTitle: undefined,
+
+ // Original text: '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: undefined,
+
+ // Original text: 'Go to pool'
+ installPatchWarningReject: undefined,
+
+ // Original text: 'Install'
+ installPatchWarningResolve: undefined,
+
+ // Original text: 'Refresh patches'
+ refreshPatches: undefined,
+
+ // Original text: 'Install pool patches'
+ installPoolPatches: undefined,
+
+ // Original text: 'Default SR'
+ defaultSr: undefined,
+
+ // Original text: 'Set as default SR'
+ setAsDefaultSr: undefined,
+
+ // Original text: "General"
+ generalTabName: 'General',
+
+ // Original text: "Stats"
+ statsTabName: 'Estadísticas',
+
+ // Original text: "Console"
+ consoleTabName: 'Consola',
+
+ // Original text: 'Container'
+ containersTabName: undefined,
+
+ // Original text: "Snapshots"
+ snapshotsTabName: 'Snapshots',
+
+ // Original text: "Logs"
+ logsTabName: 'Logs',
+
+ // Original text: "Advanced"
+ advancedTabName: 'Avanzado',
+
+ // Original text: "Network"
+ networkTabName: 'Red',
+
+ // Original text: "Disk{disks, plural, one {} other {s}}"
+ disksTabName: 'Disco{disks, plural, one {} other {s}}',
+
+ // Original text: "halted"
+ powerStateHalted: 'parada',
+
+ // Original text: "running"
+ powerStateRunning: 'corriendo',
+
+ // Original text: "suspended"
+ powerStateSuspended: 'suspendida',
+
+ // Original text: "No Xen tools detected"
+ vmStatus: 'Xen Tools no detectado',
+
+ // Original text: "No IPv4 record"
+ vmName: 'Sin registro IPv4',
+
+ // Original text: "No IP record"
+ vmDescription: 'Sin registro IP',
+
+ // Original text: "Started {ago}"
+ vmSettings: 'Arrancada {ago}',
+
+ // Original text: "Current status:"
+ vmCurrentStatus: 'Estado actual',
+
+ // Original text: "Not running"
+ vmNotRunning: 'No está corriendo',
+
+ // Original text: 'Halted {ago}'
+ vmHaltedSince: undefined,
+
+ // Original text: "No Xen tools detected"
+ noToolsDetected: 'Xen Tools no detectado',
+
+ // Original text: "No IPv4 record"
+ noIpv4Record: 'Sin registro IPv4',
+
+ // Original text: "No IP record"
+ noIpRecord: 'Sin registro IP',
+
+ // Original text: "Started {ago}"
+ started: 'Arrancada {ago}',
+
+ // Original text: "Paravirtualization (PV)"
+ paraVirtualizedMode: 'Paravirtualización (PV)',
+
+ // Original text: "Hardware virtualization (HVM)"
+ hardwareVirtualizedMode: 'Paravirtualización por Hardware (HVM)',
+
+ // Original text: "CPU usage"
+ statsCpu: 'Uso de CPU',
+
+ // Original text: "Memory usage"
+ statsMemory: 'Uso de Memoria',
+
+ // Original text: "Network throughput"
+ statsNetwork: 'Actividad de Red',
+
+ // Original text: 'Stacked values'
+ useStackedValuesOnStats: undefined,
+
+ // Original text: "Disk throughput"
+ statDisk: 'Actividad de Disco',
+
+ // Original text: "Last 10 minutes"
+ statLastTenMinutes: 'Últimos 10 minutos',
+
+ // Original text: "Last 2 hours"
+ statLastTwoHours: 'Últimas 2 horas',
+
+ // Original text: "Last week"
+ statLastWeek: 'Última semana',
+
+ // Original text: "Last year"
+ statLastYear: 'Último año',
+
+ // Original text: "Copy"
+ copyToClipboardLabel: 'Copiar',
+
+ // Original text: "Ctrl+Alt+Del"
+ ctrlAltDelButtonLabel: 'Ctrl+Alt+Del',
+
+ // Original text: "Tip:"
+ tipLabel: 'Consejo:',
+
+ // Original text: 'Hide infos'
+ hideHeaderTooltip: undefined,
+
+ // Original text: 'Show infos'
+ showHeaderTooltip: undefined,
+
+ // Original text: 'Name'
+ containerName: undefined,
+
+ // Original text: 'Command'
+ containerCommand: undefined,
+
+ // Original text: 'Creation date'
+ containerCreated: undefined,
+
+ // Original text: 'Status'
+ containerStatus: undefined,
+
+ // Original text: 'Action'
+ containerAction: undefined,
+
+ // Original text: 'No existing containers'
+ noContainers: undefined,
+
+ // Original text: 'Stop this container'
+ containerStop: undefined,
+
+ // Original text: 'Start this container'
+ containerStart: undefined,
+
+ // Original text: 'Pause this container'
+ containerPause: undefined,
+
+ // Original text: 'Resume this container'
+ containerResume: undefined,
+
+ // Original text: 'Restart this container'
+ containerRestart: undefined,
+
+ // Original text: "Action"
+ vdiAction: 'Acción',
+
+ // Original text: "Attach disk"
+ vdiAttachDeviceButton: 'Adjuntar disco',
+
+ // Original text: "New disk"
+ vbdCreateDeviceButton: 'Nuevo disco',
+
+ // Original text: "Boot order"
+ vdiBootOrder: 'Order de arranque',
+
+ // Original text: "Name"
+ vdiNameLabel: 'Nombre',
+
+ // Original text: "Description"
+ vdiNameDescription: 'Descripción',
+
+ // Original text: 'Pool'
+ vdiPool: undefined,
+
+ // Original text: 'Disconnect'
+ vdiDisconnect: undefined,
+
+ // Original text: "Tags"
+ vdiTags: 'Tareas',
+
+ // Original text: "Size"
+ vdiSize: 'Tamaño',
+
+ // Original text: "SR"
+ vdiSr: 'SR',
+
+ // Original text: 'VM'
+ vdiVm: undefined,
+
+ // Original text: 'Migrate VDI'
+ vdiMigrate: undefined,
+
+ // Original text: 'Destination SR:'
+ vdiMigrateSelectSr: undefined,
+
+ // Original text: 'Migrate all VDIs'
+ vdiMigrateAll: undefined,
+
+ // Original text: 'No SR'
+ vdiMigrateNoSr: undefined,
+
+ // Original text: 'A target SR is required to migrate a VDI'
+ vdiMigrateNoSrMessage: undefined,
+
+ // Original text: 'Forget'
+ vdiForget: undefined,
+
+ // Original text: 'Remove VDI'
+ vdiRemove: undefined,
+
+ // Original text: 'No VDIs attached to Control Domain'
+ noControlDomainVdis: undefined,
+
+ // Original text: "Boot flag"
+ vbdBootableStatus: 'Etiqueta de Inicio',
+
+ // Original text: "Status"
+ vbdStatus: 'Estado',
+
+ // Original text: "Connected"
+ vbdStatusConnected: 'Conectado',
+
+ // Original text: "Disconnected"
+ vbdStatusDisconnected: 'Desconectado',
+
+ // Original text: "No disks"
+ vbdNoVbd: 'Sin discos',
+
+ // Original text: 'Connect VBD'
+ vbdConnect: undefined,
+
+ // Original text: 'Disconnect VBD'
+ vbdDisconnect: undefined,
+
+ // Original text: 'Bootable'
+ vbdBootable: undefined,
+
+ // Original text: 'Readonly'
+ vbdReadonly: undefined,
+
+ // Original text: 'Action'
+ vbdAction: undefined,
+
+ // Original text: 'Create'
+ vbdCreate: undefined,
+
+ // Original text: 'Disk name'
+ vbdNamePlaceHolder: undefined,
+
+ // Original text: 'Size'
+ vbdSizePlaceHolder: undefined,
+
+ // Original text: 'CD drive not completely installed'
+ cdDriveNotInstalled: undefined,
+
+ // Original text: 'Stop and start the VM to install the CD drive'
+ cdDriveInstallation: undefined,
+
+ // Original text: 'Save'
+ saveBootOption: undefined,
+
+ // Original text: 'Reset'
+ resetBootOption: undefined,
+
+ // Original text: "New device"
+ vifCreateDeviceButton: 'Nuevo dispositivo',
+
+ // Original text: "No interface"
+ vifNoInterface: 'Sin interface',
+
+ // Original text: "Device"
+ vifDeviceLabel: 'Dispositivo',
+
+ // Original text: "MAC address"
+ vifMacLabel: 'Dirección MAC',
+
+ // Original text: "MTU"
+ vifMtuLabel: 'MTU',
+
+ // Original text: "Network"
+ vifNetworkLabel: 'Red',
+
+ // Original text: "Status"
+ vifStatusLabel: 'Estado',
+
+ // Original text: "Connected"
+ vifStatusConnected: 'Conectado',
+
+ // Original text: "Disconnected"
+ vifStatusDisconnected: 'Desconectado',
+
+ // Original text: 'Connect'
+ vifConnect: undefined,
+
+ // Original text: 'Disconnect'
+ vifDisconnect: undefined,
+
+ // Original text: 'Remove'
+ vifRemove: undefined,
+
+ // Original text: "IP addresses"
+ vifIpAddresses: 'Dirección IP',
+
+ // Original text: 'Auto-generated if empty'
+ vifMacAutoGenerate: undefined,
+
+ // Original text: 'Allowed IPs'
+ vifAllowedIps: undefined,
+
+ // Original text: 'No IPs'
+ vifNoIps: undefined,
+
+ // Original text: 'Network locked'
+ vifLockedNetwork: undefined,
+
+ // Original text: 'Network locked and no IPs are allowed for this interface'
+ vifLockedNetworkNoIps: undefined,
+
+ // Original text: 'Network not locked'
+ vifUnLockedNetwork: undefined,
+
+ // Original text: 'Unknown network'
+ vifUnknownNetwork: undefined,
+
+ // Original text: 'Action'
+ vifAction: undefined,
+
+ // Original text: 'Create'
+ vifCreate: undefined,
+
+ // Original text: "No snapshots"
+ noSnapshots: 'Sin snapshots',
+
+ // Original text: "New snapshot"
+ snapshotCreateButton: 'Nuevo snapshot',
+
+ // Original text: "Just click on the snapshot button to create one!"
+ tipCreateSnapshotLabel: '¡Haz click en el botón de snapshot para crear uno!',
+
+ // Original text: 'Revert VM to this snapshot'
+ revertSnapshot: undefined,
+
+ // Original text: 'Remove this snapshot'
+ deleteSnapshot: undefined,
+
+ // Original text: 'Create a VM from this snapshot'
+ copySnapshot: undefined,
+
+ // Original text: 'Export this snapshot'
+ exportSnapshot: undefined,
+
+ // Original text: "Creation date"
+ snapshotDate: 'Fecha de creación',
+
+ // Original text: "Name"
+ snapshotName: 'Nombre',
+
+ // Original text: "Action"
+ snapshotAction: 'Acción',
+
+ // Original text: 'Quiesced snapshot'
+ snapshotQuiesce: undefined,
+
+ // Original text: "Remove all logs"
+ logRemoveAll: 'Elimintar todos los logs',
+
+ // Original text: "No logs so far"
+ noLogs: 'No hay logs',
+
+ // Original text: "Creation date"
+ logDate: 'Fecha de creación',
+
+ // Original text: "Name"
+ logName: 'Nombre',
+
+ // Original text: "Content"
+ logContent: 'Contenido',
+
+ // Original text: "Action"
+ logAction: 'Acción',
+
+ // Original text: "Remove"
+ vmRemoveButton: 'Quitar',
+
+ // Original text: "Convert"
+ vmConvertButton: 'Convertir',
+
+ // Original text: "Xen settings"
+ xenSettingsLabel: 'Configuraciones de Xen',
+
+ // Original text: "Guest OS"
+ guestOsLabel: 'SO Invitado',
+
+ // Original text: "Misc"
+ miscLabel: 'Varuis',
+
+ // Original text: "UUID"
+ uuid: 'UUID',
+
+ // Original text: "Virtualization mode"
+ virtualizationMode: 'Modo de virtualización',
+
+ // Original text: "CPU weight"
+ cpuWeightLabel: 'Peso de CPU',
+
+ // Original text: "Default ({value, number})"
+ defaultCpuWeight: 'Por defecto',
+
+ // Original text: 'CPU cap'
+ cpuCapLabel: undefined,
+
+ // Original text: 'Default ({value, number})'
+ defaultCpuCap: undefined,
+
+ // Original text: "PV args"
+ pvArgsLabel: 'Argumentos PV',
+
+ // Original text: "Xen tools status"
+ xenToolsStatus: 'Estado de Xen Tools',
+
+ // Original text: "{status}"
+ xenToolsStatusValue: '{status}',
+
+ // Original text: "OS name"
+ osName: 'Nombre del OS',
+
+ // Original text: "OS kernel"
+ osKernel: 'Kernel del OS',
+
+ // Original text: "Auto power on"
+ autoPowerOn: 'Auto encendido',
+
+ // Original text: "HA"
+ ha: 'HA',
+
+ // Original text: 'Affinity host'
+ vmAffinityHost: undefined,
+
+ // Original text: 'VGA'
+ vmVga: undefined,
+
+ // Original text: 'Video RAM'
+ vmVideoram: undefined,
+
+ // Original text: 'None'
+ noAffinityHost: undefined,
+
+ // Original text: "Original template"
+ originalTemplate: 'Plantilla original',
+
+ // Original text: "Unknown"
+ unknownOsName: 'Desconocido',
+
+ // Original text: "Unknown"
+ unknownOsKernel: 'Desconocido',
+
+ // Original text: "Unknown"
+ unknownOriginalTemplate: 'Desconocido',
+
+ // Original text: "VM limits"
+ vmLimitsLabel: 'Límites de VM',
+
+ // Original text: "CPU limits"
+ vmCpuLimitsLabel: 'Límites de CPU',
+
+ // Original text: 'Topology'
+ vmCpuTopology: undefined,
+
+ // Original text: 'Default behavior'
+ vmChooseCoresPerSocket: undefined,
+
+ // Original text: '{nSockets, number} socket{nSockets, plural, one {} other {s}} with {nCores, number} core{nCores, plural, one {} other {s}} per socket'
+ vmCoresPerSocket: undefined,
+
+ // Original text: 'Incorrect cores per socket value'
+ vmCoresPerSocketIncorrectValue: undefined,
+
+ // Original text: 'Please change the selected value to fix it.'
+ vmCoresPerSocketIncorrectValueSolution: undefined,
+
+ // Original text: "Memory limits (min/max)"
+ vmMemoryLimitsLabel: 'Límites de memoria (min/max)',
+
+ // Original text: "vCPUs max:"
+ vmMaxVcpus: 'Max vCPUs:',
+
+ // Original text: "Memory max:"
+ vmMaxRam: 'Max Memoria:',
+
+ // Original text: "Long click to add a name"
+ vmHomeNamePlaceholder: 'Click largo para poner el nombre',
+
+ // Original text: "Long click to add a description"
+ vmHomeDescriptionPlaceholder: 'Click largo para poner la descripción',
+
+ // Original text: "Click to add a name"
+ vmViewNamePlaceholder: 'Click para definir el nombre',
+
+ // Original text: "Click to add a description"
+ vmViewDescriptionPlaceholder: 'Click para definir la descripción',
+
+ // Original text: 'Click to add a name'
+ templateHomeNamePlaceholder: undefined,
+
+ // Original text: 'Click to add a description'
+ templateHomeDescriptionPlaceholder: undefined,
+
+ // Original text: 'Delete template'
+ templateDelete: undefined,
+
+ // Original text: 'Delete VM template{templates, plural, one {} other {s}}'
+ templateDeleteModalTitle: undefined,
+
+ // Original text: 'Are you sure you want to delete {templates, plural, one {this} other {these}} template{templates, plural, one {} other {s}}?'
+ templateDeleteModalBody: undefined,
+
+ // Original text: "Pool{pools, plural, one {} other {s}}"
+ poolPanel: 'Pool{pools, plural, one {} other {s}}',
+
+ // Original text: "Host{hosts, plural, one {} other {s}}"
+ hostPanel: 'Host{hosts, plural, one {} other {s}}',
+
+ // Original text: "VM{vms, plural, one {} other {s}}"
+ vmPanel: 'VM{vms, plural, one {} other {s}}',
+
+ // Original text: "RAM Usage:"
+ memoryStatePanel: 'Uso de RAM',
+
+ // Original text: "CPUs Usage"
+ cpuStatePanel: 'Uso de CPU',
+
+ // Original text: "VMs Power state"
+ vmStatePanel: 'Estado alimentación de las VMs',
+
+ // Original text: "Pending tasks"
+ taskStatePanel: 'Tareas pendientes',
+
+ // Original text: "Users"
+ usersStatePanel: 'Usuarios',
+
+ // Original text: "Storage state"
+ srStatePanel: 'Estado del almacenamiento',
+
+ // Original text: "{usage} (of {total})"
+ ofUsage: 'de',
+
+ // Original text: "No storage"
+ noSrs: 'Sin almacenamiento',
+
+ // Original text: "Name"
+ srName: 'Nombre',
+
+ // Original text: "Pool"
+ srPool: 'Pool',
+
+ // Original text: "Host"
+ srHost: 'Host',
+
+ // Original text: "Type"
+ srFormat: 'Tipo',
+
+ // Original text: "Size"
+ srSize: 'Tamaño',
+
+ // Original text: "Usage"
+ srUsage: 'Uso',
+
+ // Original text: "used"
+ srUsed: 'Usado',
+
+ // Original text: "free"
+ srFree: 'libre',
+
+ // Original text: "Storage Usage"
+ srUsageStatePanel: 'Uso del almacenamiento',
+
+ // Original text: "Top 5 SR Usage (in %)"
+ srTopUsageStatePanel: 'Top 5 de uso de SR (en %)',
+
+ // Original text: '{running, number} running ({halted, number} halted)'
+ vmsStates: undefined,
+
+ // Original text: 'Clear selection'
+ dashboardStatsButtonRemoveAll: undefined,
+
+ // Original text: 'Add all hosts'
+ dashboardStatsButtonAddAllHost: undefined,
+
+ // Original text: 'Add all VMs'
+ dashboardStatsButtonAddAllVM: undefined,
+
+ // Original text: "{value} {date, date, medium}"
+ weekHeatmapData: '{value} {date, date, medium}',
+
+ // Original text: "No data."
+ weekHeatmapNoData: 'Sin datos.',
+
+ // Original text: "Weekly Heatmap"
+ weeklyHeatmap: 'Heatmap Semanal',
+
+ // Original text: "Weekly Charts"
+ weeklyCharts: 'Gráficos Semanales',
+
+ // Original text: "Synchronize scale:"
+ weeklyChartsScaleInfo: 'Sincronizar escala',
+
+ // Original text: "Stats error"
+ statsDashboardGenericErrorTitle: 'Error de stats',
+
+ // Original text: "There is no stats available for:"
+ statsDashboardGenericErrorMessage: 'No hay stats disponibles para:',
+
+ // Original text: "No selected metric"
+ noSelectedMetric: 'Métrica no seleccionada',
+
+ // Original text: "Select"
+ statsDashboardSelectObjects: 'Seleccionar',
+
+ // Original text: "Loading…"
+ metricsLoading: 'Cargando…',
+
+ // Original text: "Coming soon!"
+ comingSoon: '¡Próximamente!',
+
+ // Original text: "Orphaned snapshot VDIs"
+ orphanedVdis: 'VIDs huérfanos',
+
+ // Original text: "Orphaned VMs snapshot"
+ orphanedVms: 'VMs huérfanas',
+
+ // Original text: "No orphans"
+ noOrphanedObject: 'Sin huérfanos',
+
+ // Original text: "Remove all orphaned snapshot VDIs"
+ removeAllOrphanedObject: 'Eliminar todos los VDIs huérfanos',
+
+ // Original text: 'VDIs attached to Control Domain'
+ vdisOnControlDomain: undefined,
+
+ // Original text: "Name"
+ vmNameLabel: 'Nombre',
+
+ // Original text: "Description"
+ vmNameDescription: 'Descripción',
+
+ // Original text: "Resident on"
+ vmContainer: 'Residente en',
+
+ // Original text: "Alarms"
+ alarmMessage: 'Alarmas',
+
+ // Original text: "No alarms"
+ noAlarms: 'Sin alarmas',
+
+ // Original text: "Date"
+ alarmDate: 'Fecha',
+
+ // Original text: "Content"
+ alarmContent: 'Contenido',
+
+ // Original text: "Issue on"
+ alarmObject: 'Producido el',
+
+ // Original text: "Pool"
+ alarmPool: 'Pool',
+
+ // Original text: "Remove all alarms"
+ alarmRemoveAll: 'Eliminar todas las alarmas',
+
+ // Original text: '{used}% used ({free} left)'
+ spaceLeftTooltip: undefined,
+
+ // Original text: "Create a new VM on {select}"
+ newVmCreateNewVmOn: 'Crear una nueva VM en {pool}',
+
+ // Original text: 'Create a new VM on {select1} or {select2}'
+ newVmCreateNewVmOn2: undefined,
+
+ // Original text: 'You have no permission to create a VM'
+ newVmCreateNewVmNoPermission: undefined,
+
+ // Original text: "Infos"
+ newVmInfoPanel: 'Información',
+
+ // Original text: "Name"
+ newVmNameLabel: 'Nombre',
+
+ // Original text: "Template"
+ newVmTemplateLabel: 'Plantilla',
+
+ // Original text: "Description"
+ newVmDescriptionLabel: 'Descripción',
+
+ // Original text: "Performances"
+ newVmPerfPanel: 'Rendimiento',
+
+ // Original text: "vCPUs"
+ newVmVcpusLabel: 'vCPUs',
+
+ // Original text: "RAM"
+ newVmRamLabel: 'RAM',
+
+ // Original text: 'Static memory max'
+ newVmStaticMaxLabel: undefined,
+
+ // Original text: 'Dynamic memory min'
+ newVmDynamicMinLabel: undefined,
+
+ // Original text: 'Dynamic memory max'
+ newVmDynamicMaxLabel: undefined,
+
+ // Original text: "Install settings"
+ newVmInstallSettingsPanel: 'Opciones de instalación',
+
+ // Original text: "ISO/DVD"
+ newVmIsoDvdLabel: 'ISO/DVD',
+
+ // Original text: "Network"
+ newVmNetworkLabel: 'Red',
+
+ // Original text: 'e.g: http://httpredir.debian.org/debian'
+ newVmInstallNetworkPlaceHolder: undefined,
+
+ // Original text: "PV Args"
+ newVmPvArgsLabel: 'Detalles del PV',
+
+ // Original text: "PXE"
+ newVmPxeLabel: 'PXE',
+
+ // Original text: "Interfaces"
+ newVmInterfacesPanel: 'Interfaces',
+
+ // Original text: "MAC"
+ newVmMacLabel: 'MAC',
+
+ // Original text: "Add interface"
+ newVmAddInterface: 'Añadir interface',
+
+ // Original text: "Disks"
+ newVmDisksPanel: 'Discos',
+
+ // Original text: "SR"
+ newVmSrLabel: 'SR',
+
+ // Original text: "Size"
+ newVmSizeLabel: 'Tamaño',
+
+ // Original text: "Add disk"
+ newVmAddDisk: 'Añadir disco',
+
+ // Original text: "Summary"
+ newVmSummaryPanel: 'Sumario',
+
+ // Original text: "Create"
+ newVmCreate: 'Crear',
+
+ // Original text: "Reset"
+ newVmReset: 'Reiniciar',
+
+ // Original text: "Select template"
+ newVmSelectTemplate: 'Elegir plantilla',
+
+ // Original text: "SSH key"
+ newVmSshKey: 'Clave SSH',
+
+ // Original text: "Config drive"
+ newVmConfigDrive: 'Config drive',
+
+ // Original text: "Custom config"
+ newVmCustomConfig: 'Configuración personalizada',
+
+ // Original text: "Boot VM after creation"
+ newVmBootAfterCreate: 'Arrancar la VM tras crearla',
+
+ // Original text: "Auto-generated if empty"
+ newVmMacPlaceholder: 'Auto generada si se deja vacío',
+
+ // Original text: "CPU weight"
+ newVmCpuWeightLabel: 'Peso de CPU',
+
+ // Original text: 'Default: {value, number}'
+ newVmDefaultCpuWeight: undefined,
+
+ // Original text: 'CPU cap'
+ newVmCpuCapLabel: undefined,
+
+ // Original text: 'Default: {value, number}'
+ newVmDefaultCpuCap: undefined,
+
+ // Original text: "Cloud config"
+ newVmCloudConfig: 'Cloud config',
+
+ // Original text: "Create VMs"
+ newVmCreateVms: 'VMs creadas',
+
+ // Original text: "Are you sure you want to create {nbVms, number} VMs?"
+ newVmCreateVmsConfirm: '¿Estás seguro de querer crear {nbVms, number} VMs?',
+
+ // Original text: "Multiple VMs:"
+ newVmMultipleVms: 'Múltiple VMs:',
+
+ // Original text: 'Select a resource set:'
+ newVmSelectResourceSet: undefined,
+
+ // Original text: 'Name pattern:'
+ newVmMultipleVmsPattern: undefined,
+
+ // Original text: 'e.g.: \\{name\\}_%'
+ newVmMultipleVmsPatternPlaceholder: undefined,
+
+ // Original text: 'First index:'
+ newVmFirstIndex: undefined,
+
+ // Original text: 'Recalculate VMs number'
+ newVmNumberRecalculate: undefined,
+
+ // Original text: 'Refresh VMs name'
+ newVmNameRefresh: undefined,
+
+ // Original text: 'Affinity host'
+ newVmAffinityHost: undefined,
+
+ // Original text: 'Advanced'
+ newVmAdvancedPanel: undefined,
+
+ // Original text: 'Show advanced settings'
+ newVmShowAdvanced: undefined,
+
+ // Original text: 'Hide advanced settings'
+ newVmHideAdvanced: undefined,
+
+ // Original text: 'Share this VM'
+ newVmShare: undefined,
+
+ // Original text: "Resource sets"
+ resourceSets: 'Conjunto de recursos',
+
+ // Original text: "No resource sets."
+ noResourceSets: 'No hay conjuntos de recursos',
+
+ // Original text: 'Loading resource sets'
+ loadingResourceSets: undefined,
+
+ // Original text: "Resource set name"
+ resourceSetName: 'Nombre del conjunto de recursos',
+
+ // Original text: 'Recompute all limits'
+ recomputeResourceSets: undefined,
+
+ // Original text: "Save"
+ saveResourceSet: 'Guardar',
+
+ // Original text: "Reset"
+ resetResourceSet: 'Reiniciar',
+
+ // Original text: "Edit"
+ editResourceSet: 'Editar',
+
+ // Original text: "Delete"
+ deleteResourceSet: 'Bprrar',
+
+ // Original text: "Delete resource set"
+ deleteResourceSetWarning: 'Borrar conjunto de recursos',
+
+ // Original text: "Are you sure you want to delete this resource set?"
+ deleteResourceSetQuestion:
+ '¿Estás seguro de querer borrar este conjunto de recursos?',
+
+ // Original text: "Missing objects:"
+ resourceSetMissingObjects: 'Objetos perdidos:',
+
+ // Original text: "vCPUs"
+ resourceSetVcpus: 'vCPUs',
+
+ // Original text: "Memory"
+ resourceSetMemory: 'Memoria',
+
+ // Original text: "Storage"
+ resourceSetStorage: 'Almacenamiento',
+
+ // Original text: "Unknown"
+ unknownResourceSetValue: 'Desconocido',
+
+ // Original text: "Available hosts"
+ availableHosts: 'Hosts disponibles',
+
+ // Original text: "Excluded hosts"
+ excludedHosts: 'Hosts excluídos',
+
+ // Original text: "No hosts available."
+ noHostsAvailable: 'No hay hosts disponibles',
+
+ // Original text: "VMs created from this resource set shall run on the following hosts."
+ availableHostsDescription:
+ 'Las VMs creadas con este conjunto de recursos correrán en los siguientes hosts.',
+
+ // Original text: "Maximum CPUs"
+ maxCpus: 'Máximas CPUs',
+
+ // Original text: "Maximum RAM (GiB)"
+ maxRam: 'Máxima RAM (GiB)',
+
+ // Original text: "Maximum disk space"
+ maxDiskSpace: 'Máximo espacio en disco',
+
+ // Original text: 'IP pool'
+ ipPool: undefined,
+
+ // Original text: 'Quantity'
+ quantity: undefined,
+
+ // Original text: "No limits."
+ noResourceSetLimits: 'Sin límites',
+
+ // Original text: "Total:"
+ totalResource: 'Total:',
+
+ // Original text: "Remaining:"
+ remainingResource: 'Restante:',
+
+ // Original text: "Used:"
+ usedResource: 'Utilizado:',
+
+ // Original text: 'New'
+ resourceSetNew: undefined,
+
+ // Original text: "Try dropping some VMs files here, or click to select VMs to upload. Accept only .xva/.ova files."
+ importVmsList:
+ 'Haz drag & drop de backups aquí, o haz click para seleccionar qué backups subir. Sólo se aceptan ficheros .xva',
+
+ // Original text: "No selected VMs."
+ noSelectedVms: 'No se han seleccionado VMs',
+
+ // Original text: "To Pool:"
+ vmImportToPool: 'Al Pool:',
+
+ // Original text: "To SR:"
+ vmImportToSr: 'al SR:',
+
+ // Original text: "VMs to import"
+ vmsToImport: 'VMs para importar',
+
+ // Original text: "Reset"
+ importVmsCleanList: 'Reiniciar',
+
+ // Original text: "VM import success"
+ vmImportSuccess: 'Importación de VM satisfactoria',
+
+ // Original text: "VM import failed"
+ vmImportFailed: 'Falló la importación de VM',
+
+ // Original text: "Import starting…"
+ startVmImport: 'Comenzando importación…',
+
+ // Original text: "Export starting…"
+ startVmExport: 'Comenzando export…',
+
+ // Original text: 'N CPUs'
+ nCpus: undefined,
+
+ // Original text: 'Memory'
+ vmMemory: undefined,
+
+ // Original text: 'Disk {position} ({capacity})'
+ diskInfo: undefined,
+
+ // Original text: 'Disk description'
+ diskDescription: undefined,
+
+ // Original text: 'No disks.'
+ noDisks: undefined,
+
+ // Original text: 'No networks.'
+ noNetworks: undefined,
+
+ // Original text: 'Network {name}'
+ networkInfo: undefined,
+
+ // Original text: 'No description available'
+ noVmImportErrorDescription: undefined,
+
+ // Original text: 'Error:'
+ vmImportError: undefined,
+
+ // Original text: '{type} file:'
+ vmImportFileType: undefined,
+
+ // Original text: 'Please to check and/or modify the VM configuration.'
+ vmImportConfigAlert: undefined,
+
+ // Original text: "No pending tasks"
+ noTasks: 'No hay tareas pendientes',
+
+ // Original text: "Currently, there are not any pending XenServer tasks"
+ xsTasks: 'Ahora mismo no hay tareas de XenServer pendientes',
+
+ // Original text: 'Schedules'
+ backupSchedules: undefined,
+
+ // Original text: 'Get remote'
+ getRemote: undefined,
+
+ // Original text: "List Remote"
+ listRemote: 'Listar backups remotos',
+
+ // Original text: "simple"
+ simpleBackup: 'simple',
+
+ // Original text: "delta"
+ delta: 'diferencial',
+
+ // Original text: "Restore Backups"
+ restoreBackups: 'Restaurar backups',
+
+ // Original text: 'Click on a VM to display restore options'
+ restoreBackupsInfo: undefined,
+
+ // Original text: 'Only the files of Delta Backup which are not on a SMB remote can be restored'
+ restoreDeltaBackupsInfo: undefined,
+
+ // Original text: "Enabled"
+ remoteEnabled: 'activado',
+
+ // Original text: "Error"
+ remoteError: 'error',
+
+ // Original text: "No backup available"
+ noBackup: 'No hay backups disponibles',
+
+ // Original text: "VM Name"
+ backupVmNameColumn: 'Nombre VM',
+
+ // Original text: 'Tags'
+ backupTags: undefined,
+
+ // Original text: "Last Backup"
+ lastBackupColumn: 'Último backup',
+
+ // Original text: "Available Backups"
+ availableBackupsColumn: 'Backups disponibles',
+
+ // Original text: 'Missing parameters'
+ backupRestoreErrorTitle: undefined,
+
+ // Original text: 'Choose a SR and a backup'
+ backupRestoreErrorMessage: undefined,
+
+ // Original text: 'Select default SR…'
+ backupRestoreSelectDefaultSr: undefined,
+
+ // Original text: 'Choose a SR for each VDI'
+ backupRestoreChooseSrForEachVdis: undefined,
+
+ // Original text: 'VDI'
+ backupRestoreVdiLabel: undefined,
+
+ // Original text: 'SR'
+ backupRestoreSrLabel: undefined,
+
+ // Original text: 'Display backups'
+ displayBackup: undefined,
+
+ // Original text: "Import VM"
+ importBackupTitle: 'Importar VM',
+
+ // Original text: "Starting your backup import"
+ importBackupMessage: 'Comenzando importación del backup',
+
+ // Original text: 'VMs to backup'
+ vmsToBackup: undefined,
+
+ // Original text: 'List remote backups'
+ listRemoteBackups: undefined,
+
+ // Original text: 'Restore backup files'
+ restoreFiles: undefined,
+
+ // Original text: 'Invalid options'
+ restoreFilesError: undefined,
+
+ // Original text: 'Restore file from {name}'
+ restoreFilesFromBackup: undefined,
+
+ // Original text: 'Select a backup…'
+ restoreFilesSelectBackup: undefined,
+
+ // Original text: 'Select a disk…'
+ restoreFilesSelectDisk: undefined,
+
+ // Original text: 'Select a partition…'
+ restoreFilesSelectPartition: undefined,
+
+ // Original text: 'Folder path'
+ restoreFilesSelectFolderPath: undefined,
+
+ // Original text: 'Select a file…'
+ restoreFilesSelectFiles: undefined,
+
+ // Original text: 'Content not found'
+ restoreFileContentNotFound: undefined,
+
+ // Original text: 'No files selected'
+ restoreFilesNoFilesSelected: undefined,
+
+ // Original text: 'Selected files ({files}):'
+ restoreFilesSelectedFiles: undefined,
+
+ // Original text: 'Error while scanning disk'
+ restoreFilesDiskError: undefined,
+
+ // Original text: "Select all this folder's files"
+ restoreFilesSelectAllFiles: undefined,
+
+ // Original text: 'Unselect all files'
+ restoreFilesUnselectAll: undefined,
+
+ // Original text: 'Emergency shutdown Host{nHosts, plural, one {} other {s}}'
+ emergencyShutdownHostsModalTitle: undefined,
+
+ // Original text: 'Are you sure you want to shutdown {nHosts, number} Host{nHosts, plural, one {} other {s}}?'
+ emergencyShutdownHostsModalMessage: undefined,
+
+ // Original text: 'Shutdown host'
+ stopHostModalTitle: undefined,
+
+ // 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: undefined,
+
+ // Original text: 'Add host'
+ addHostModalTitle: undefined,
+
+ // Original text: 'Are you sure you want to add {host} to {pool}?'
+ addHostModalMessage: undefined,
+
+ // Original text: 'Restart host'
+ restartHostModalTitle: undefined,
+
+ // Original text: 'This will restart your host. Do you want to continue?'
+ restartHostModalMessage: undefined,
+
+ // Original text: 'Restart Host{nHosts, plural, one {} other {s}} agent{nHosts, plural, one {} other {s}}'
+ restartHostsAgentsModalTitle: undefined,
+
+ // Original text: 'Are you sure you want to restart {nHosts, number} Host{nHosts, plural, one {} other {s}} agent{nHosts, plural, one {} other {s}}?'
+ restartHostsAgentsModalMessage: undefined,
+
+ // Original text: 'Restart Host{nHosts, plural, one {} other {s}}'
+ restartHostsModalTitle: undefined,
+
+ // Original text: 'Are you sure you want to restart {nHosts, number} Host{nHosts, plural, one {} other {s}}?'
+ restartHostsModalMessage: undefined,
+
+ // Original text: "Start VM{vms, plural, one {} other {s}}"
+ startVmsModalTitle: 'Arrancar VM{vms, plural, one {} other {s}}',
+
+ // Original text: 'Start a copy'
+ cloneAndStartVM: undefined,
+
+ // Original text: 'Force start'
+ forceStartVm: undefined,
+
+ // Original text: 'Forbidden operation'
+ forceStartVmModalTitle: undefined,
+
+ // Original text: 'Start operation for this vm is blocked.'
+ blockedStartVmModalMessage: undefined,
+
+ // Original text: 'Forbidden operation start for {nVms, number} vm{nVms, plural, one {} other {s}}.'
+ blockedStartVmsModalMessage: undefined,
+
+ // Original text: "Are you sure you want to start {vms, number} VM{vms, plural, one {} other {s}}?"
+ startVmsModalMessage:
+ '¿Estás seguro de querar arrancar {vms} VM{vms, plural, one {} other {s}}?',
+
+ // Original text: '{nVms, number} vm{nVms, plural, one {} other {s}} are failed. Please see your logs to get more information'
+ failedVmsErrorMessage: undefined,
+
+ // Original text: 'Start failed'
+ failedVmsErrorTitle: undefined,
+
+ // Original text: 'Stop Host{nHosts, plural, one {} other {s}}'
+ stopHostsModalTitle: undefined,
+
+ // Original text: 'Are you sure you want to stop {nHosts, number} Host{nHosts, plural, one {} other {s}}?'
+ stopHostsModalMessage: undefined,
+
+ // Original text: "Stop VM{vms, plural, one {} other {s}}"
+ stopVmsModalTitle: 'Parar VM{vms, plural, one {} other {s}}',
+
+ // Original text: "Are you sure you want to stop {vms, number} VM{vms, plural, one {} other {s}}?"
+ stopVmsModalMessage:
+ '¿Estás seguro de querer parar {vms, number} VM{vms, plural, one {} other {s}}?',
+
+ // Original text: 'Restart VM'
+ restartVmModalTitle: undefined,
+
+ // Original text: 'Are you sure you want to restart {name}?'
+ restartVmModalMessage: undefined,
+
+ // Original text: 'Stop VM'
+ stopVmModalTitle: undefined,
+
+ // Original text: 'Are you sure you want to stop {name}?'
+ stopVmModalMessage: undefined,
+
+ // Original text: "Restart VM{vms, plural, one {} other {s}}"
+ restartVmsModalTitle: 'Reiniciar VM{vms, plural, one {} other {s}}',
+
+ // Original text: "Are you sure you want to restart {vms, number} VM{vms, plural, one {} other {s}}?"
+ restartVmsModalMessage:
+ '¿Estás seguro de querer reiniciar {vms, number} 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, number} VM{vms, plural, one {} other {s}}?"
+ snapshotVmsModalMessage:
+ '¿Estás seguro de querer hacer snapshot de {vms, number} VM{vms, plural, one {} other {s}}?',
+
+ // Original text: "Delete VM{vms, plural, one {} other {s}}"
+ deleteVmsModalTitle: 'Borrar VM{vms, plural, one {} other {s}}',
+
+ // Original text: "Are you sure you want to delete {vms, number} VM{vms, plural, one {} other {s}}? ALL VM DISKS WILL BE REMOVED"
+ deleteVmsModalMessage:
+ '¿Estás seguro de querar borrar {vms, number} VM{vms, plural, one {} other {s}}? TODOS SUS DISCOS SERAN ELIMINADOS',
+
+ // Original text: "Delete VM"
+ deleteVmModalTitle: 'Borrar VM',
+
+ // Original text: "Are you sure you want to delete this VM? ALL VM DISKS WILL BE REMOVED"
+ deleteVmModalMessage:
+ '¿Estás seguro de querer borrar esta VM? TODOS SUS DISCOS SERAN ELIMINADOS',
+
+ // Original text: "Migrate VM"
+ migrateVmModalTitle: 'Migrar VM',
+
+ // Original text: 'Select a destination host:'
+ migrateVmSelectHost: undefined,
+
+ // Original text: 'Select a migration network:'
+ migrateVmSelectMigrationNetwork: undefined,
+
+ // Original text: 'For each VIF, select a network:'
+ migrateVmSelectNetworks: undefined,
+
+ // Original text: 'Select a destination SR:'
+ migrateVmsSelectSr: undefined,
+
+ // Original text: 'Select a destination SR for local disks:'
+ migrateVmsSelectSrIntraPool: undefined,
+
+ // Original text: 'Select a network on which to connect each VIF:'
+ migrateVmsSelectNetwork: undefined,
+
+ // Original text: 'Smart mapping'
+ migrateVmsSmartMapping: undefined,
+
+ // Original text: 'VIF'
+ migrateVmVif: undefined,
+
+ // Original text: 'Network'
+ migrateVmNetwork: undefined,
+
+ // Original text: 'No target host'
+ migrateVmNoTargetHost: undefined,
+
+ // Original text: 'A target host is required to migrate a VM'
+ migrateVmNoTargetHostMessage: undefined,
+
+ // Original text: 'No default SR'
+ migrateVmNoDefaultSrError: undefined,
+
+ // Original text: 'Default SR not connected to host'
+ migrateVmNotConnectedDefaultSrError: undefined,
+
+ // Original text: 'For each VDI, select an SR:'
+ chooseSrForEachVdisModalSelectSr: undefined,
+
+ // Original text: 'Select main SR…'
+ chooseSrForEachVdisModalMainSr: undefined,
+
+ // Original text: 'VDI'
+ chooseSrForEachVdisModalVdiLabel: undefined,
+
+ // Original text: 'SR*'
+ chooseSrForEachVdisModalSrLabel: undefined,
+
+ // Original text: '* optional'
+ chooseSrForEachVdisModalOptionalEntry: undefined,
+
+ // Original text: 'Delete VDI'
+ deleteVdiModalTitle: undefined,
+
+ // Original text: 'Are you sure you want to delete this disk? ALL DATA ON THIS DISK WILL BE LOST'
+ deleteVdiModalMessage: undefined,
+
+ // Original text: 'Revert your VM'
+ revertVmModalTitle: undefined,
+
+ // Original text: 'Delete snapshot'
+ deleteSnapshotModalTitle: undefined,
+
+ // Original text: 'Are you sure you want to delete this snapshot?'
+ deleteSnapshotModalMessage: undefined,
+
+ // Original text: 'Are you sure you want to revert this VM to the snapshot state? This operation is irreversible.'
+ revertVmModalMessage: undefined,
+
+ // Original text: 'Snapshot before'
+ revertVmModalSnapshotBefore: undefined,
+
+ // Original text: "Import a {name} Backup"
+ importBackupModalTitle: 'Importar un backup {name}',
+
+ // Original text: "Start VM after restore"
+ importBackupModalStart: 'Arrancar la VM tras la restauración',
+
+ // Original text: "Select your backup…"
+ importBackupModalSelectBackup: 'Elige el backup…',
+
+ // Original text: "Are you sure you want to remove all orphaned snapshot VDIs?"
+ removeAllOrphanedModalWarning:
+ '¿Estás seguro de querer borrar todos los VDIs huérfanos?',
+
+ // Original text: "Remove all logs"
+ removeAllLogsModalTitle: 'Borrar todos los logs',
+
+ // Original text: "Are you sure you want to remove all logs?"
+ removeAllLogsModalWarning: '¿Estás seguro de querar borrar todos los logs?',
+
+ // Original text: "This operation is definitive."
+ definitiveMessageModal: 'Esta operación es definitiva',
+
+ // Original text: "Previous SR Usage"
+ existingSrModalTitle: 'Uso anterior del 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:
+ 'Esta ruta ya ha sido utilizada anteriormente como Almacenamiento por un host XenServer. Todos los datos existentes se perderán si continuas con la creación del SR.',
+
+ // Original text: "Previous LUN Usage"
+ existingLunModalTitle: 'Uso anterior de la 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:
+ 'Esta LUN ya ha sido utilizada anteriormente como Almacenamiento por un host XenServer. Todos los datos existentes se perderán si continuas con la creación del SR.',
+
+ // Original text: "Replace current registration?"
+ alreadyRegisteredModal: '¿Reemplazar el registro actual?',
+
+ // Original text: "Your XO appliance is already registered to {email}, do you want to forget and replace this registration ?"
+ alreadyRegisteredModalText:
+ 'Tu XOA ya está registrado en {email}, ¿quieres olvidar y reemplazar este registro?',
+
+ // Original text: "Ready for trial?"
+ trialReadyModal: '¿Preparado para el periodo de prueba?',
+
+ // 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 el periodo de prueba, XOA necesita conexión a Internet. Esta limitación no aplica a los planes de pago',
+
+ // Original text: 'Label'
+ serverLabel: undefined,
+
+ // Original text: "Host"
+ serverHost: 'Host',
+
+ // Original text: "Username"
+ serverUsername: 'Usuario',
+
+ // Original text: "Password"
+ serverPassword: 'Clave',
+
+ // Original text: "Action"
+ serverAction: 'Acción',
+
+ // Original text: "Read Only"
+ serverReadOnly: 'Sólo lectura',
+
+ // Original text: 'Unauthorized Certificates'
+ serverUnauthorizedCertificates: undefined,
+
+ // Original text: 'Allow Unauthorized Certificates'
+ serverAllowUnauthorizedCertificates: undefined,
+
+ // Original text: "Enable it if your certificate is rejected, but it's not recommended because your connection will not be secured."
+ serverUnauthorizedCertificatesInfo: undefined,
+
+ // Original text: 'Disconnect server'
+ serverDisconnect: undefined,
+
+ // Original text: 'username'
+ serverPlaceHolderUser: undefined,
+
+ // Original text: 'password'
+ serverPlaceHolderPassword: undefined,
+
+ // Original text: 'address[:port]'
+ serverPlaceHolderAddress: undefined,
+
+ // Original text: 'label'
+ serverPlaceHolderLabel: undefined,
+
+ // Original text: 'Connect'
+ serverConnect: undefined,
+
+ // Original text: 'Error'
+ serverError: undefined,
+
+ // Original text: 'Adding server failed'
+ serverAddFailed: undefined,
+
+ // Original text: 'Status'
+ serverStatus: undefined,
+
+ // Original text: 'Connection failed. Click for more information.'
+ serverConnectionFailed: undefined,
+
+ // Original text: 'Connecting…'
+ serverConnecting: undefined,
+
+ // Original text: 'Connected'
+ serverConnected: undefined,
+
+ // Original text: 'Disconnected'
+ serverDisconnected: undefined,
+
+ // Original text: 'Authentication error'
+ serverAuthFailed: undefined,
+
+ // Original text: 'Unknown error'
+ serverUnknownError: undefined,
+
+ // Original text: 'Invalid self-signed certificate'
+ serverSelfSignedCertError: undefined,
+
+ // Original text: 'Do you want to accept self-signed certificate for this server even though it would decrease security?'
+ serverSelfSignedCertQuestion: undefined,
+
+ // Original text: "Copy VM"
+ copyVm: 'Copiar VM',
+
+ // Original text: "Are you sure you want to copy this VM to {SR}?"
+ copyVmConfirm: '¿Estás seguro de querer copiar esta VM a {SR}?',
+
+ // Original text: "Name"
+ copyVmName: 'Nombre',
+
+ // Original text: 'Name pattern'
+ copyVmNamePattern: undefined,
+
+ // Original text: "If empty: name of the copied VM"
+ copyVmNamePlaceholder: 'Vacío: nombre de la VM copiada',
+
+ // Original text: 'e.g.: "\\{name\\}_COPY"'
+ copyVmNamePatternPlaceholder: undefined,
+
+ // Original text: "Select SR"
+ copyVmSelectSr: 'Elegir SR',
+
+ // Original text: "Use compression"
+ copyVmCompress: 'Usar compresión',
+
+ // Original text: 'No target SR'
+ copyVmsNoTargetSr: undefined,
+
+ // Original text: 'A target SR is required to copy a VM'
+ copyVmsNoTargetSrMessage: undefined,
+
+ // Original text: 'Detach host'
+ detachHostModalTitle: undefined,
+
+ // 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: undefined,
+
+ // Original text: 'Detach'
+ detachHost: undefined,
+
+ // Original text: 'Forget host'
+ forgetHostModalTitle: undefined,
+
+ // Original text: "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: undefined,
+
+ // Original text: 'Forget'
+ forgetHost: undefined,
+
+ // Original text: "Create network"
+ newNetworkCreate: 'Crear red',
+
+ // Original text: 'Create bonded network'
+ newBondedNetworkCreate: undefined,
+
+ // Original text: "Interface"
+ newNetworkInterface: 'Interface',
+
+ // Original text: "Name"
+ newNetworkName: 'Nombre',
+
+ // Original text: "Description"
+ newNetworkDescription: 'Descripción',
+
+ // Original text: "VLAN"
+ newNetworkVlan: 'VLAN',
+
+ // Original text: "No VLAN if empty"
+ newNetworkDefaultVlan: 'Sin VLAN si se deja vacío',
+
+ // Original text: "MTU"
+ newNetworkMtu: 'MTU',
+
+ // Original text: "Default: 1500"
+ newNetworkDefaultMtu: 'Por defecto: 1500',
+
+ // Original text: 'Name required'
+ newNetworkNoNameErrorTitle: undefined,
+
+ // Original text: 'A name is required to create a network'
+ newNetworkNoNameErrorMessage: undefined,
+
+ // Original text: 'Bond mode'
+ newNetworkBondMode: undefined,
+
+ // Original text: "Delete network"
+ deleteNetwork: 'Borrar red',
+
+ // Original text: "Are you sure you want to delete this network?"
+ deleteNetworkConfirm: '¿Estás seguro de querer borrar esta red?',
+
+ // Original text: 'This network is currently in use'
+ networkInUse: undefined,
+
+ // Original text: 'Bonded'
+ pillBonded: undefined,
+
+ // Original text: 'Host'
+ addHostSelectHost: undefined,
+
+ // Original text: 'No host'
+ addHostNoHost: undefined,
+
+ // Original text: 'No host selected to be added'
+ addHostNoHostMessage: undefined,
+
+ // Original text: "Xen Orchestra"
+ xenOrchestra: 'Xen Orchestra',
+
+ // Original text: "Xen Orchestra server"
+ xenOrchestraServer: 'servidor',
+
+ // Original text: "Xen Orchestra web client"
+ xenOrchestraWeb: 'cliente web',
+
+ // Original text: "No pro support provided!"
+ noProSupport: '¡Sin soporte Pro!',
+
+ // Original text: "Use in production at your own risks"
+ noProductionUse: 'Usar en producción bajo tu propia cuenta y riesgo',
+
+ // Original text: "You can download our turnkey appliance at {website}""
+ downloadXoaFromWebsite: 'Puedes descargar nuestro appliance en {website}',
+
+ // Original text: "Bug Tracker"
+ bugTracker: 'Bug Tracker',
+
+ // Original text: "Issues? Report it!"
+ bugTrackerText: '¿Problemas? ¡Repórtalos!',
+
+ // Original text: "Community"
+ community: 'Comunidad',
+
+ // Original text: "Join our community forum!"
+ communityText: '¡Únete al foro de nuestra comunidad!',
+
+ // Original text: "Free Trial for Premium Edition!"
+ freeTrial: 'Prueba gratis de la Edición Premium',
+
+ // Original text: "Request your trial now!"
+ freeTrialNow: '¡Pide la prueba gratuíta ahora!',
+
+ // Original text: "Any issue?"
+ issues: '¿Algún problema?',
+
+ // Original text: "Problem? Contact us!"
+ issuesText: '¿Problemas? ¡Ponte en contacto con nosotros!',
+
+ // Original text: "Documentation"
+ documentation: 'Documentación',
+
+ // Original text: "Read our official doc"
+ documentationText: 'Lee nuestra documentación oficial',
+
+ // Original text: "Pro support included"
+ proSupportIncluded: 'Soporte Pro incluído',
+
+ // Original text: "Access your XO Account"
+ xoAccount: 'Entra en tu cuenta XO',
+
+ // Original text: "Report a problem"
+ openTicket: 'Reportar un problema',
+
+ // Original text: "Problem? Open a ticket!"
+ openTicketText: '¿Problemas? ¡Abre un ticket!',
+
+ // Original text: "Upgrade needed"
+ upgradeNeeded: 'Actualización necesaria',
+
+ // Original text: "Upgrade now!"
+ upgradeNow: '¡Actualiza ahora!',
+
+ // Original text: "Or"
+ or: 'O',
+
+ // Original text: "Try it for free!"
+ tryIt: '¡Pruébalo gratis!',
+
+ // Original text: "This feature is available starting from {plan} Edition"
+ availableIn:
+ 'Esta característica está sólo disponible a partir de la Edición {plan}',
+
+ // Original text: 'This feature is not available in your version, contact your administrator to know more.'
+ notAvailable: undefined,
+
+ // Original text: 'Updates'
+ updateTitle: undefined,
+
+ // Original text: "Registration"
+ registration: 'Registro',
+
+ // Original text: "Trial"
+ trial: 'Prueba',
+
+ // Original text: "Settings"
+ settings: 'Configuración',
+
+ // Original text: 'Proxy settings'
+ proxySettings: undefined,
+
+ // Original text: 'Host (myproxy.example.org)'
+ proxySettingsHostPlaceHolder: undefined,
+
+ // Original text: 'Port (eg: 3128)'
+ proxySettingsPortPlaceHolder: undefined,
+
+ // Original text: 'Username'
+ proxySettingsUsernamePlaceHolder: undefined,
+
+ // Original text: 'Password'
+ proxySettingsPasswordPlaceHolder: undefined,
+
+ // Original text: 'Your email account'
+ updateRegistrationEmailPlaceHolder: undefined,
+
+ // Original text: 'Your password'
+ updateRegistrationPasswordPlaceHolder: undefined,
+
+ // Original text: "Update"
+ update: 'Refrescar',
+
+ // Original text: 'Refresh'
+ refresh: undefined,
+
+ // Original text: "Upgrade"
+ upgrade: 'Actualizar',
+
+ // Original text: "No updater available for Community Edition"
+ noUpdaterCommunity: 'No hay actualizador para la Edición Community',
+
+ // Original text: "Please consider subscribe and try it with all features for free during 15 days on {link}.""
+ considerSubscribe:
+ 'Por favor plantéate la suscripción y pruébala con todas las características gratis durante 15 días {link}',
+
+ // Original text: "Manual update could break your current installation due to dependencies issues, do it with caution"
+ noUpdaterWarning:
+ 'La actualización manual podría romper tu instalación actual debido a problemas de dependencias, hazlo con precaución',
+
+ // Original text: "Current version:"
+ currentVersion: 'Versión actual',
+
+ // Original text: "Register"
+ register: 'Registrar',
+
+ // Original text: 'Edit registration'
+ editRegistration: undefined,
+
+ // Original text: "Please, take time to register in order to enjoy your trial."
+ trialRegistration:
+ 'Por favor, regístrate para poder disfrutar del periodo de prueba',
+
+ // Original text: "Start trial"
+ trialStartButton: 'Comenzar prueba',
+
+ // Original text: "You can use a trial version until {date, date, medium}. Upgrade your appliance to get it."
+ trialAvailableUntil:
+ 'Puedes usar la versión de prueba hasta {date, date, medium}. Actualiza tu instalación para obtenerla.',
+
+ // Original text: "Your trial has been ended. Contact us or downgrade to Free version"
+ trialConsumed:
+ 'Tu periodo de prueba ha terminado. Contacta con nosotros o vuelve a la Edición Libre',
+
+ // Original text: "Your xoa-updater service appears to be down. Your XOA cannot run fully without reaching this service."
+ trialLocked:
+ 'Tu servicio xoa-updater parece estar caído. XOA no puede funcionar correctamente sin contactar con este servicio',
+
+ // Original text: "No update information available"
+ noUpdateInfo: 'No hay información de actualización',
+
+ // Original text: "Update information may be available"
+ waitingUpdateInfo: 'Podría haber información de actualización disponible',
+
+ // Original text: "Your XOA is up-to-date"
+ upToDate: 'Tu XOA está al día',
+
+ // Original text: "You need to update your XOA (new version is available)"
+ mustUpgrade: 'Necesitas actualizar tu XOA (hay disponible una nueva versión)',
+
+ // Original text: "Your XOA is not registered for updates"
+ registerNeeded: 'Tu XOA no está registrado para recibir actualizaciones',
+
+ // Original text: "Can't fetch update information"
+ updaterError: 'No se puede obtener la información de actualización',
+
+ // Original text: 'Upgrade successful'
+ promptUpgradeReloadTitle: undefined,
+
+ // Original text: 'Your XOA has successfully upgraded, and your browser must reload the application. Do you want to reload now ?'
+ promptUpgradeReloadMessage: undefined,
+
+ // Original text: "Xen Orchestra from the sources"
+ disclaimerTitle: 'Xen Orchestra desde código fuente',
+
+ // Original text: "You are using XO from the sources! That's great for a personal/non-profit usage."
+ disclaimerText1:
+ '¡Estás utilizando XO a partir del código fuente! Estupendo para un uso personal/sin ánimo de lucro',
+
+ // Original text: "If you are a company, it's better to use it with our appliance + pro support included:"
+ disclaimerText2:
+ 'Si eres una empresa, es mejor utilizarlo con nuestro appliance con soporte Pro incluído',
+
+ // Original text: "This version is not bundled with any support nor updates. Use it with caution for critical tasks."
+ disclaimerText3:
+ 'Esta versión no está creada para recibir soporte ni actualizaciones. Úsala con precaución para tareas críticas.',
+
+ // Original text: "Connect PIF"
+ connectPif: 'Conectar PIF',
+
+ // Original text: "Are you sure you want to connect this PIF?"
+ connectPifConfirm: '¿Estás seguro de querer conectar este PIF?',
+
+ // Original text: "Disconnect PIF"
+ disconnectPif: 'Desconectar PIF',
+
+ // Original text: "Are you sure you want to disconnect this PIF?"
+ disconnectPifConfirm: '¿Estás seguro de querer desconectar este PIF?',
+
+ // Original text: "Delete PIF"
+ deletePif: 'Borrar PIF',
+
+ // Original text: "Are you sure you want to delete this PIF?"
+ deletePifConfirm: '¿Estás seguro de querer borrar este PIF?',
+
+ // Original text: 'Connected'
+ pifConnected: undefined,
+
+ // Original text: 'Disconnected'
+ pifDisconnected: undefined,
+
+ // Original text: 'Physically connected'
+ pifPhysicallyConnected: undefined,
+
+ // Original text: 'Physically disconnected'
+ pifPhysicallyDisconnected: undefined,
+
+ // Original text: 'Username'
+ username: undefined,
+
+ // Original text: 'Password'
+ password: undefined,
+
+ // Original text: 'Language'
+ language: undefined,
+
+ // Original text: 'Old password'
+ oldPasswordPlaceholder: undefined,
+
+ // Original text: 'New password'
+ newPasswordPlaceholder: undefined,
+
+ // Original text: 'Confirm new password'
+ confirmPasswordPlaceholder: undefined,
+
+ // Original text: 'Confirmation password incorrect'
+ confirmationPasswordError: undefined,
+
+ // Original text: 'Password does not match the confirm password.'
+ confirmationPasswordErrorBody: undefined,
+
+ // Original text: 'Password changed'
+ pwdChangeSuccess: undefined,
+
+ // Original text: 'Your password has been successfully changed.'
+ pwdChangeSuccessBody: undefined,
+
+ // Original text: 'Incorrect password'
+ pwdChangeError: undefined,
+
+ // Original text: 'The old password provided is incorrect. Your password has not been changed.'
+ pwdChangeErrorBody: undefined,
+
+ // Original text: 'OK'
+ changePasswordOk: undefined,
+
+ // Original text: 'SSH keys'
+ sshKeys: undefined,
+
+ // Original text: 'New SSH key'
+ newSshKey: undefined,
+
+ // Original text: 'Delete'
+ deleteSshKey: undefined,
+
+ // Original text: 'No SSH keys'
+ noSshKeys: undefined,
+
+ // Original text: 'New SSH key'
+ newSshKeyModalTitle: undefined,
+
+ // Original text: 'Invalid key'
+ sshKeyErrorTitle: undefined,
+
+ // Original text: 'An SSH key requires both a title and a key.'
+ sshKeyErrorMessage: undefined,
+
+ // Original text: 'Title'
+ title: undefined,
+
+ // Original text: 'Key'
+ key: undefined,
+
+ // Original text: 'Delete SSH key'
+ deleteSshKeyConfirm: undefined,
+
+ // Original text: 'Are you sure you want to delete the SSH key {title}?'
+ deleteSshKeyConfirmMessage: undefined,
+
+ // Original text: 'Others'
+ others: undefined,
+
+ // Original text: 'Loading logs…'
+ loadingLogs: undefined,
+
+ // Original text: 'User'
+ logUser: undefined,
+
+ // Original text: 'Method'
+ logMethod: undefined,
+
+ // Original text: 'Params'
+ logParams: undefined,
+
+ // Original text: 'Message'
+ logMessage: undefined,
+
+ // Original text: 'Error'
+ logError: undefined,
+
+ // Original text: 'Display details'
+ logDisplayDetails: undefined,
+
+ // Original text: 'Date'
+ logTime: undefined,
+
+ // Original text: 'No stack trace'
+ logNoStackTrace: undefined,
+
+ // Original text: 'No params'
+ logNoParams: undefined,
+
+ // Original text: 'Delete log'
+ logDelete: undefined,
+
+ // Original text: 'Delete all logs'
+ logDeleteAll: undefined,
+
+ // Original text: 'Delete all logs'
+ logDeleteAllTitle: undefined,
+
+ // Original text: 'Are you sure you want to delete all the logs?'
+ logDeleteAllMessage: undefined,
+
+ // Original text: 'Click to enable'
+ logIndicationToEnable: undefined,
+
+ // Original text: 'Click to disable'
+ logIndicationToDisable: undefined,
+
+ // Original text: 'Report a bug'
+ reportBug: undefined,
+
+ // Original text: 'Name'
+ ipPoolName: undefined,
+
+ // Original text: 'IPs'
+ ipPoolIps: undefined,
+
+ // Original text: 'IPs (e.g.: 1.0.0.12-1.0.0.17;1.0.0.23)'
+ ipPoolIpsPlaceholder: undefined,
+
+ // Original text: 'Networks'
+ ipPoolNetworks: undefined,
+
+ // Original text: 'No IP pools'
+ ipsNoIpPool: undefined,
+
+ // Original text: 'Create'
+ ipsCreate: undefined,
+
+ // Original text: 'Delete all IP pools'
+ ipsDeleteAllTitle: undefined,
+
+ // Original text: 'Are you sure you want to delete all the IP pools?'
+ ipsDeleteAllMessage: undefined,
+
+ // Original text: 'VIFs'
+ ipsVifs: undefined,
+
+ // Original text: 'Not used'
+ ipsNotUsed: undefined,
+
+ // Original text: 'unknown VIF'
+ ipPoolUnknownVif: undefined,
+
+ // Original text: 'Name already exists'
+ ipPoolNameAlreadyExists: undefined,
+
+ // Original text: 'Keyboard shortcuts'
+ shortcutModalTitle: undefined,
+
+ // Original text: 'Global'
+ shortcut_XoApp: undefined,
+
+ // Original text: 'Go to hosts list'
+ shortcut_GO_TO_HOSTS: undefined,
+
+ // Original text: 'Go to pools list'
+ shortcut_GO_TO_POOLS: undefined,
+
+ // Original text: 'Go to VMs list'
+ shortcut_GO_TO_VMS: undefined,
+
+ // Original text: 'Go to SRs list'
+ shortcut_GO_TO_SRS: undefined,
+
+ // Original text: 'Create a new VM'
+ shortcut_CREATE_VM: undefined,
+
+ // Original text: 'Unfocus field'
+ shortcut_UNFOCUS: undefined,
+
+ // Original text: 'Show shortcuts key bindings'
+ shortcut_HELP: undefined,
+
+ // Original text: 'Home'
+ shortcut_Home: undefined,
+
+ // Original text: 'Focus search bar'
+ shortcut_SEARCH: undefined,
+
+ // Original text: 'Next item'
+ shortcut_NAV_DOWN: undefined,
+
+ // Original text: 'Previous item'
+ shortcut_NAV_UP: undefined,
+
+ // Original text: 'Select item'
+ shortcut_SELECT: undefined,
+
+ // Original text: 'Open'
+ shortcut_JUMP_INTO: undefined,
+
+ // Original text: 'VM'
+ settingsAclsButtonTooltipVM: undefined,
+
+ // Original text: 'Hosts'
+ settingsAclsButtonTooltiphost: undefined,
+
+ // Original text: 'Pool'
+ settingsAclsButtonTooltippool: undefined,
+
+ // Original text: 'SR'
+ settingsAclsButtonTooltipSR: undefined,
+
+ // Original text: 'Network'
+ settingsAclsButtonTooltipnetwork: undefined,
+
+ // Original text: 'No config file selected'
+ noConfigFile: undefined,
+
+ // Original text: 'Try dropping a config file here, or click to select a config file to upload.'
+ importTip: undefined,
+
+ // Original text: 'Config'
+ config: undefined,
+
+ // Original text: 'Import'
+ importConfig: undefined,
+
+ // Original text: 'Config file successfully imported'
+ importConfigSuccess: undefined,
+
+ // Original text: 'Error while importing config file'
+ importConfigError: undefined,
+
+ // Original text: 'Export'
+ exportConfig: undefined,
+
+ // Original text: 'Download current config'
+ downloadConfig: undefined,
+
+ // Original text: 'No config import available for Community Edition'
+ noConfigImportCommunity: undefined,
+
+ // Original text: 'Reconnect all hosts'
+ srReconnectAllModalTitle: undefined,
+
+ // Original text: 'This will reconnect this SR to all its hosts.'
+ srReconnectAllModalMessage: undefined,
+
+ // Original text: 'This will reconnect each selected SR to its host (local SR) or to every hosts of its pool (shared SR).'
+ srsReconnectAllModalMessage: undefined,
+
+ // Original text: 'Disconnect all hosts'
+ srDisconnectAllModalTitle: undefined,
+
+ // Original text: 'This will disconnect this SR from all its hosts.'
+ srDisconnectAllModalMessage: undefined,
+
+ // Original text: 'This will disconnect each selected SR from its host (local SR) or from every hosts of its pool (shared SR).'
+ srsDisconnectAllModalMessage: undefined,
+
+ // Original text: 'Forget SR'
+ srForgetModalTitle: undefined,
+
+ // Original text: 'Forget selected SRs'
+ srsForgetModalTitle: undefined,
+
+ // Original text: "Are you sure you want to forget this SR? VDIs on this storage won't be removed."
+ srForgetModalMessage: undefined,
+
+ // Original text: "Are you sure you want to forget all the selected SRs? VDIs on these storages won't be removed."
+ srsForgetModalMessage: undefined,
+
+ // Original text: 'Disconnected'
+ srAllDisconnected: undefined,
+
+ // Original text: 'Partially connected'
+ srSomeConnected: undefined,
+
+ // Original text: 'Connected'
+ srAllConnected: undefined,
+
+ // Original text: 'XOSAN'
+ xosanTitle: undefined,
+
+ // Original text: 'Xen Orchestra SAN SR'
+ xosanSrTitle: undefined,
+
+ // Original text: 'Select local SRs (lvm)'
+ xosanAvailableSrsTitle: undefined,
+
+ // Original text: 'Suggestions'
+ xosanSuggestions: undefined,
+
+ // Original text: 'Name'
+ xosanName: undefined,
+
+ // Original text: 'Host'
+ xosanHost: undefined,
+
+ // Original text: 'Hosts'
+ xosanHosts: undefined,
+
+ // Original text: 'Volume ID'
+ xosanVolumeId: undefined,
+
+ // Original text: 'Size'
+ xosanSize: undefined,
+
+ // Original text: 'Used space'
+ xosanUsedSpace: undefined,
+
+ // Original text: 'XOSAN pack needs to be installed on each host of the pool.'
+ xosanNeedPack: undefined,
+
+ // Original text: 'Install it now!'
+ xosanInstallIt: undefined,
+
+ // Original text: 'Some hosts need their toolstack to be restarted before you can create an XOSAN'
+ xosanNeedRestart: undefined,
+
+ // Original text: 'Restart toolstacks'
+ xosanRestartAgents: undefined,
+
+ // Original text: 'Pool master is not running'
+ xosanMasterOffline: undefined,
+
+ // Original text: 'Install XOSAN pack on {pool}'
+ xosanInstallPackTitle: undefined,
+
+ // Original text: 'Select at least 2 SRs'
+ xosanSelect2Srs: undefined,
+
+ // Original text: 'Layout'
+ xosanLayout: undefined,
+
+ // Original text: 'Redundancy'
+ xosanRedundancy: undefined,
+
+ // Original text: 'Capacity'
+ xosanCapacity: undefined,
+
+ // Original text: 'Available space'
+ xosanAvailableSpace: undefined,
+
+ // Original text: '* Can fail without data loss'
+ xosanDiskLossLegend: undefined,
+
+ // Original text: 'Create'
+ xosanCreate: undefined,
+
+ // Original text: 'Installing XOSAN. Please wait…'
+ xosanInstalling: undefined,
+
+ // Original text: 'No XOSAN available for Community Edition'
+ xosanCommunity: undefined,
+
+ // Original text: 'Install cloud plugin first'
+ xosanInstallCloudPlugin: undefined,
+
+ // Original text: 'Load cloud plugin first'
+ xosanLoadCloudPlugin: undefined,
+
+ // Original text: 'Loading…'
+ xosanLoading: undefined,
+
+ // Original text: 'XOSAN is not available at the moment'
+ xosanNotAvailable: undefined,
+
+ // Original text: 'Register for the XOSAN beta'
+ xosanRegisterBeta: undefined,
+
+ // Original text: 'You have successfully registered for the XOSAN beta. Please wait until your request has been approved.'
+ xosanSuccessfullyRegistered: undefined,
+
+ // Original text: 'Install XOSAN pack on these hosts:'
+ xosanInstallPackOnHosts: undefined,
+
+ // Original text: 'Install {pack} v{version}?'
+ xosanInstallPack: undefined,
+
+ // Original text: 'No compatible XOSAN pack found for your XenServer versions.'
+ xosanNoPackFound: undefined,
+
+ // Original text: 'At least one of these version requirements must be satisfied by all the hosts in this pool:'
+ xosanPackRequirements: undefined,
+}
diff --git a/packages/xo-web/src/common/intl/locales/fr.js b/packages/xo-web/src/common/intl/locales/fr.js
new file mode 100644
index 000000000..1dc61fcfd
--- /dev/null
+++ b/packages/xo-web/src/common/intl/locales/fr.js
@@ -0,0 +1,3937 @@
+// See http://momentjs.com/docs/#/use-it/browserify/
+import 'moment/locale/fr'
+
+import reactIntlData from 'react-intl/locale-data/fr'
+import { addLocaleData } from 'react-intl'
+addLocaleData(reactIntlData)
+
+// ===================================================================
+
+export default {
+ // Original text: "{key}: {value}"
+ keyValue: '{key} : {value}',
+
+ // Original text: "Connecting"
+ statusConnecting: 'Connexion…',
+
+ // Original text: "Disconnected"
+ statusDisconnected: 'Déconnecté',
+
+ // Original text: "Loading…"
+ statusLoading: 'Chargement…',
+
+ // Original text: "Page not found"
+ errorPageNotFound: 'Page introuvable',
+
+ // Original text: "no such item"
+ errorNoSuchItem: 'aucune correspondance',
+
+ // Original text: "Long click to edit"
+ editableLongClickPlaceholder: 'Clic long pour éditer',
+
+ // Original text: "Click to edit"
+ editableClickPlaceholder: 'Cliquer pour éditer',
+
+ // Original text: "Browse files"
+ browseFiles: 'Parcourir',
+
+ // Original text: "OK"
+ alertOk: 'OK',
+
+ // Original text: "OK"
+ confirmOk: 'OK',
+
+ // Original text: "Cancel"
+ genericCancel: 'Annuler',
+
+ // Original text: "On error"
+ onError: 'En erreur',
+
+ // Original text: "Successful"
+ successful: 'Réussi',
+
+ // Original text: "Managed disks"
+ filterOnlyManaged: 'Supervisés',
+
+ // Original text: "Orphaned disks"
+ filterOnlyOrphaned: 'Orphelins',
+
+ // Original text: "Normal disks"
+ filterOnlyRegular: 'Normaux',
+
+ // Original text: "Snapshot disks"
+ filterOnlySnapshots: 'Instantanés',
+
+ // Original text: "Unmanaged disks"
+ filterOnlyUnmanaged: 'Bases delta',
+
+ // Original text: "Copy to clipboard"
+ copyToClipboard: 'Copier dans le presse-papier',
+
+ // Original text: "Master"
+ pillMaster: 'Maître',
+
+ // Original text: "Home"
+ homePage: 'Accueil',
+
+ // Original text: "VMs"
+ homeVmPage: 'VMs',
+
+ // Original text: "Hosts"
+ homeHostPage: 'Hôtes',
+
+ // Original text: "Pools"
+ homePoolPage: 'Pools',
+
+ // Original text: "Templates"
+ homeTemplatePage: 'Templates',
+
+ // Original text: "Storages"
+ homeSrPage: 'Stockages',
+
+ // Original text: "Dashboard"
+ dashboardPage: 'Tableau de bord',
+
+ // Original text: "Overview"
+ overviewDashboardPage: "Vue d'ensemble",
+
+ // Original text: "Visualizations"
+ overviewVisualizationDashboardPage: 'Visualisations',
+
+ // Original text: "Statistics"
+ overviewStatsDashboardPage: 'Statistiques',
+
+ // Original text: "Health"
+ overviewHealthDashboardPage: 'État de santé',
+
+ // Original text: "Self service"
+ selfServicePage: 'Libre service',
+
+ // Original text: "Backup"
+ backupPage: 'Sauvegardes',
+
+ // Original text: "Jobs"
+ jobsPage: 'Jobs',
+
+ // Original text: "Updates"
+ updatePage: 'Mises à jour',
+
+ // Original text: "Settings"
+ settingsPage: 'Paramètres',
+
+ // Original text: "Servers"
+ settingsServersPage: 'Serveurs',
+
+ // Original text: "Users"
+ settingsUsersPage: 'Utilisateurs',
+
+ // Original text: "Groups"
+ settingsGroupsPage: 'Groupes',
+
+ // Original text: "ACLs"
+ settingsAclsPage: 'Droits',
+
+ // Original text: "Plugins"
+ settingsPluginsPage: 'Greffons',
+
+ // Original text: "Logs"
+ settingsLogsPage: 'Journaux',
+
+ // Original text: "IPs"
+ settingsIpsPage: 'IPs',
+
+ // Original text: "Config"
+ settingsConfigPage: 'Configuration',
+
+ // Original text: "About"
+ aboutPage: 'À propos',
+
+ // Original text: "About XO {xoaPlan}"
+ aboutXoaPlan: 'À propos de XO {xoaPlan}',
+
+ // Original text: "New"
+ newMenu: 'Nouveau',
+
+ // Original text: "Tasks"
+ taskMenu: 'Tâches',
+
+ // Original text: "Tasks"
+ taskPage: 'Tâches',
+
+ // Original text: "VM"
+ newVmPage: 'VM',
+
+ // Original text: "Storage"
+ newSrPage: 'Stockage',
+
+ // Original text: "Server"
+ newServerPage: 'Serveur',
+
+ // Original text: "Import"
+ newImport: 'Importer',
+
+ // Original text: "XOSAN"
+ xosan: 'XOSAN',
+
+ // Original text: "Overview"
+ backupOverviewPage: "Vue d'ensemble",
+
+ // Original text: "New"
+ backupNewPage: 'Nouveau',
+
+ // Original text: "Remotes"
+ backupRemotesPage: 'Emplacement',
+
+ // Original text: "Restore"
+ backupRestorePage: 'Restaurer',
+
+ // Original text: "File restore"
+ backupFileRestorePage: 'Restauration de fichiers',
+
+ // Original text: "Schedule"
+ schedule: 'Planifier',
+
+ // Original text: "New VM backup"
+ newVmBackup: 'Nouvelle sauvegarde de VM',
+
+ // Original text: "Edit VM backup"
+ editVmBackup: 'Éditer une sauvegarde de VM',
+
+ // Original text: "Backup"
+ backup: 'Sauvegarder',
+
+ // Original text: "Rolling Snapshot"
+ rollingSnapshot: 'Rolling Snapshot',
+
+ // Original text: "Delta Backup"
+ deltaBackup: 'Delta Backup',
+
+ // Original text: "Disaster Recovery"
+ disasterRecovery: 'Reprise après panne',
+
+ // Original text: "Continuous Replication"
+ continuousReplication: 'Réplication continue',
+
+ // Original text: "Overview"
+ jobsOverviewPage: "Vue d'ensemble",
+
+ // Original text: "New"
+ jobsNewPage: 'Nouveau',
+
+ // Original text: "Scheduling"
+ jobsSchedulingPage: 'Planifier',
+
+ // Original text: "Custom Job"
+ customJob: 'Job personnalisé',
+
+ // Original text: "User"
+ userPage: 'Utilisateur',
+
+ // Original text: "No support"
+ noSupport: 'Pas de support',
+
+ // Original text: "Free upgrade!"
+ freeUpgrade: 'Mise à jour gratuite !',
+
+ // Original text: "Sign out"
+ signOut: 'Se déconnecter',
+
+ // Original text: "Edit my settings {username}"
+ editUserProfile: 'Éditer mes options {username}',
+
+ // Original text: "Fetching data…"
+ homeFetchingData: 'Récupération des données…',
+
+ // Original text: "Welcome on Xen Orchestra!"
+ homeWelcome: 'Bienvenue sur Xen Orchestra !',
+
+ // Original text: "Add your XenServer hosts or pools"
+ homeWelcomeText: 'Ajouter vos serveurs ou pools XenServer',
+
+ // Original text: "Some XenServers have been registered but are not connected"
+ homeConnectServerText:
+ "Des XenServers sont enregistrés mais aucun n'est connecté",
+
+ // Original text: "Want some help?"
+ homeHelp: "Besoin d'aide ?",
+
+ // Original text: "Add server"
+ homeAddServer: 'Ajouter un serveur',
+
+ // Original text: "Connect servers"
+ homeConnectServer: 'Connecter des serveurs',
+
+ // Original text: "Online Doc"
+ homeOnlineDoc: 'Documentation en ligne',
+
+ // Original text: "Pro Support"
+ homeProSupport: 'Support professionel',
+
+ // Original text: "There are no VMs!"
+ homeNoVms: "Il n'y a pas de VMs !",
+
+ // Original text: "Or…"
+ homeNoVmsOr: 'Ou…',
+
+ // Original text: "Import VM"
+ homeImportVm: 'Importer une VM',
+
+ // Original text: "Import an existing VM in xva format"
+ homeImportVmMessage: 'Importer une VM existante au format xva',
+
+ // Original text: "Restore a backup"
+ homeRestoreBackup: 'Restaurer une sauvegarde',
+
+ // Original text: "Restore a backup from a remote store"
+ homeRestoreBackupMessage:
+ 'Restaurer une sauvegarde depuis un stockage distant',
+
+ // Original text: "This will create a new VM"
+ homeNewVmMessage: 'Cela va créer une nouvelle VM',
+
+ // Original text: "Filters"
+ homeFilters: 'Filtres',
+
+ // Original text: "No results! Click here to reset your filters"
+ homeNoMatches: 'Aucun résultat ! Cliquez ici pour réinitialiser vos filtres',
+
+ // Original text: "Pool"
+ homeTypePool: 'Pool',
+
+ // Original text: "Host"
+ homeTypeHost: 'Hôte',
+
+ // Original text: "VM"
+ homeTypeVm: 'VM',
+
+ // Original text: "SR"
+ homeTypeSr: 'SR',
+
+ // Original text: "Template"
+ homeTypeVmTemplate: 'Template',
+
+ // Original text: "Sort"
+ homeSort: 'Trier',
+
+ // Original text: "Pools"
+ homeAllPools: 'Pools',
+
+ // Original text: "Hosts"
+ homeAllHosts: 'Hôtes',
+
+ // Original text: "Tags"
+ homeAllTags: 'Tags',
+
+ // Original text: "New VM"
+ homeNewVm: 'Nouvelle VM',
+
+ // Original text: "None"
+ homeFilterNone: 'Aucun',
+
+ // Original text: "Running hosts"
+ homeFilterRunningHosts: 'Hôtes démarrés',
+
+ // Original text: "Disabled hosts"
+ homeFilterDisabledHosts: 'Hôtes désactivés',
+
+ // Original text: "Running VMs"
+ homeFilterRunningVms: 'VMs démarrées',
+
+ // Original text: "Non running VMs"
+ homeFilterNonRunningVms: 'VMs éteintes',
+
+ // Original text: "Pending VMs"
+ homeFilterPendingVms: 'VMs en attente',
+
+ // Original text: "HVM guests"
+ homeFilterHvmGuests: 'Invités HVM',
+
+ // Original text: "Tags"
+ homeFilterTags: 'Tags',
+
+ // Original text: "Sort by"
+ homeSortBy: 'Trier par',
+
+ // Original text: "Name"
+ homeSortByName: 'Nom',
+
+ // Original text: "Power state"
+ homeSortByPowerstate: 'Alimentation',
+
+ // Original text: "RAM"
+ homeSortByRAM: 'RAM',
+
+ // Original text: "vCPUs"
+ homeSortByvCPUs: 'vCPUs',
+
+ // Original text: "CPUs"
+ homeSortByCpus: 'CPUs',
+
+ // Original text: "Shared/Not shared"
+ homeSortByShared: 'Partagé/Non partagé',
+
+ // Original text: "Size"
+ homeSortBySize: 'Taille',
+
+ // Original text: "Usage"
+ homeSortByUsage: 'Utilisation',
+
+ // Original text: "Type"
+ homeSortByType: 'Type',
+
+ // Original text: "{displayed, number}x {icon} (on {total, number})"
+ homeDisplayedItems: '{displayed, number}x {icon} (sur {total, number})',
+
+ // Original text: "{selected, number}x {icon} selected (on {total, number})"
+ homeSelectedItems:
+ '{selected, number}x {icon} sélectionné{selected, plural, one {} other {s}} (sur {total, number})',
+
+ // Original text: "More"
+ homeMore: 'Plus',
+
+ // Original text: "Migrate to…"
+ homeMigrateTo: 'Migrer vers…',
+
+ // Original text: "Missing patches"
+ homeMissingPaths: 'Patches manquant',
+
+ // Original text: "Master:"
+ homePoolMaster: 'Maître :',
+
+ // Original text: "Resource set: {resourceSet}"
+ homeResourceSet: 'Jeu de ressources : {resourceSet}',
+
+ // Original text: "High Availability"
+ highAvailability: 'Haute disponibilité',
+
+ // Original text: "Shared {type}"
+ srSharedType: '{type} partagé',
+
+ // Original text: "Not shared {type}"
+ srNotSharedType: '{type} non partagé',
+
+ // Original text: 'All of them are selected'
+ sortedTableAllItemsSelected: 'Toutes sont sélectionnées',
+
+ // Original text: '{nFiltered, number} of {nTotal, number} items'
+ sortedTableNumberOfFilteredItems:
+ '{nFiltered, number} entrées sur {nTotal, number}',
+
+ // Original text: '{nTotal, number} items'
+ sortedTableNumberOfItems: '{nTotal, number} entrées',
+
+ // Original text: '{nSelected, number} selected'
+ sortedTableNumberOfSelectedItems: '{nSelected, number} sélectionnées',
+
+ // Original text: 'Click here to select all items'
+ sortedTableSelectAllItems: 'Cliquez ici pour sélectionner toutes les entrées',
+
+ // Original text: "Add"
+ add: 'Ajouter',
+
+ // Original text: "Select all"
+ selectAll: 'Tout sélectionner',
+
+ // Original text: "Remove"
+ remove: 'Supprimer',
+
+ // Original text: "Preview"
+ preview: 'Aperçu',
+
+ // Original text: "Item"
+ item: 'Objet',
+
+ // Original text: "No selected value"
+ noSelectedValue: 'Pas de valeur sélectionnée',
+
+ // Original text: "Choose user(s) and/or group(s)"
+ selectSubjects: 'Sélectionner un ou des utilisateur(s) et/ou groupe(s)',
+
+ // Original text: "Select Object(s)…"
+ selectObjects: 'Sélectionner de(s) objet(s)…',
+
+ // Original text: "Choose a role"
+ selectRole: 'Choisir un rôle',
+
+ // Original text: "Select Host(s)…"
+ selectHosts: 'Choisir un/des hôte(s)…',
+
+ // Original text: "Select object(s)…"
+ selectHostsVms: 'Choisir un/des object(s)…',
+
+ // Original text: "Select Network(s)…"
+ selectNetworks: 'Choisir un/des réseau(x)…',
+
+ // Original text: "Select PIF(s)…"
+ selectPifs: 'Sélectionner une/des PIF(s)…',
+
+ // Original text: "Select Pool(s)…"
+ selectPools: 'Sélectionner un/des Pool(s)…',
+
+ // Original text: "Select Remote(s)…"
+ selectRemotes: 'Choisir un/des emplacement(s)…',
+
+ // Original text: "Select resource set(s)…"
+ selectResourceSets: 'Choisir un jeu de ressource(s)…',
+
+ // Original text: "Select template(s)…"
+ selectResourceSetsVmTemplate: 'Sélectionner un/des template(s)…',
+
+ // Original text: "Select SR(s)…"
+ selectResourceSetsSr: 'Sélectionner un/des SR(s)…',
+
+ // Original text: "Select network(s)…"
+ selectResourceSetsNetwork: 'Sélectionner un/des réseau(x)…',
+
+ // Original text: "Select disk(s)…"
+ selectResourceSetsVdi: 'Sélectionner un/des disque(s)…',
+
+ // Original text: "Select SSH key(s)…"
+ selectSshKey: 'Sélectionner une/des clef(s) SSH…',
+
+ // Original text: "Select SR(s)…"
+ selectSrs: 'Sélectionner un/des SR(s)…',
+
+ // Original text: "Select VM(s)…"
+ selectVms: 'Sélectionner une/des VM(s)…',
+
+ // Original text: "Select VM template(s)…"
+ selectVmTemplates: 'Sélectionner un/des template(s) de VM…',
+
+ // Original text: "Select tag(s)…"
+ selectTags: 'Sélectionner un/des tag(s)…',
+
+ // Original text: "Select disk(s)…"
+ selectVdis: 'Sélectionner un/des disque(s)…',
+
+ // Original text: "Select timezone…"
+ selectTimezone: 'Sélectionner le fuseau horaire…',
+
+ // Original text: "Select IP(s)…"
+ selectIp: 'Sélectionner une/des IP(s)…',
+
+ // Original text: "Select IP pool(s)…"
+ selectIpPool: "Sélectionner une/des plage(s) d'IP(s)…",
+
+ // Original text: "Fill required informations."
+ fillRequiredInformations: 'Saisir les informations obligatoires.',
+
+ // Original text: "Fill informations (optional)"
+ fillOptionalInformations: 'Saisir les informations (optionnel)',
+
+ // Original text: "Reset"
+ selectTableReset: 'Réinitialiser',
+
+ // Original text: "Month"
+ schedulingMonth: 'Mois',
+
+ // Original text: "Every N month"
+ schedulingEveryNMonth: 'Tous les N mois',
+
+ // Original text: "Each selected month"
+ schedulingEachSelectedMonth: 'Chaque mois sélectionné',
+
+ // Original text: "Day"
+ schedulingDay: 'Jour',
+
+ // Original text: "Every N day"
+ schedulingEveryNDay: 'Tous les N jours',
+
+ // Original text: "Each selected day"
+ schedulingEachSelectedDay: 'Chaque jour sélectionné',
+
+ // Original text: "Switch to week days"
+ schedulingSetWeekDayMode: 'Utiliser les jours de la semaine',
+
+ // Original text: "Switch to month days"
+ schedulingSetMonthDayMode: 'Utiliser les jours du mois',
+
+ // Original text: "Hour"
+ schedulingHour: 'Heure',
+
+ // Original text: "Each selected hour"
+ schedulingEachSelectedHour: 'Chaque heure sélectionnée',
+
+ // Original text: "Every N hour"
+ schedulingEveryNHour: 'Toutes les N heures',
+
+ // Original text: "Minute"
+ schedulingMinute: 'Minute',
+
+ // Original text: "Each selected minute"
+ schedulingEachSelectedMinute: 'Chaque minute sélectionnée',
+
+ // Original text: "Every N minute"
+ schedulingEveryNMinute: 'Toutes les N minutes',
+
+ // Original text: "Every month"
+ selectTableAllMonth: 'Tous les mois',
+
+ // Original text: "Every day"
+ selectTableAllDay: 'Tous les jours',
+
+ // Original text: "Every hour"
+ selectTableAllHour: 'Toutes les heures',
+
+ // Original text: "Every minute"
+ selectTableAllMinute: 'Toutes les minutes',
+
+ // Original text: "Reset"
+ schedulingReset: 'Réinitialiser',
+
+ // Original text: "Unknown"
+ unknownSchedule: 'Inconnu',
+
+ // Original text: "Web browser timezone"
+ timezonePickerUseLocalTime: 'Fuseau horaire de votre navigateur',
+
+ // Original text: "Server timezone ({value})"
+ serverTimezoneOption: 'Fuseau horaire du serveur ({value})',
+
+ // Original text: "Cron Pattern:"
+ cronPattern: 'Motif Cron :',
+
+ // Original text: "Cannot edit backup"
+ backupEditNotFoundTitle: "Impossible d'éditer la sauvegarde",
+
+ // Original text: "Missing required info for edition"
+ backupEditNotFoundMessage:
+ "Il manque les informations nécessaires à l'édition",
+
+ // Original text: "Successful"
+ successfulJobCall: 'Réussi',
+
+ // Original text: "Failed"
+ failedJobCall: 'Échoué',
+
+ // Original text: "In progress"
+ jobCallInProgess: 'En cours',
+
+ // Original text: "size:"
+ jobTransferredDataSize: 'Taille :',
+
+ // Original text: "speed:"
+ jobTransferredDataSpeed: 'Vitesse :',
+
+ // Original text: "Job"
+ job: 'Job',
+
+ // Original text: "Job {job}"
+ jobModalTitle: 'Job {job}',
+
+ // Original text: "ID"
+ jobId: 'ID du job',
+
+ // Original text: "Type"
+ jobType: 'Type',
+
+ // Original text: "Name"
+ jobName: 'Nom',
+
+ // Original text: "Name of your job (forbidden: \"_\")"
+ jobNamePlaceholder: 'Nom de votre job (caractère interdit : "_")',
+
+ // Original text: "Start"
+ jobStart: 'Début',
+
+ // Original text: "End"
+ jobEnd: 'Fin',
+
+ // Original text: "Duration"
+ jobDuration: 'Durée',
+
+ // Original text: "Status"
+ jobStatus: 'État',
+
+ // Original text: "Action"
+ jobAction: 'Action',
+
+ // Original text: "Tag"
+ jobTag: 'Tag',
+
+ // Original text: "Scheduling"
+ jobScheduling: 'Planifié',
+
+ // Original text: "State"
+ jobState: 'État',
+
+ // Original text: "Enabled"
+ jobStateEnabled: 'Activé',
+
+ // Original text: "Disabled"
+ jobStateDisabled: 'Désactivé',
+
+ // Original text: "Timezone"
+ jobTimezone: 'Fuseau horaire',
+
+ // Original text: "Server"
+ jobServerTimezone: 'xo-server',
+
+ // Original text: "Run job"
+ runJob: 'Lancer le job',
+
+ // Original text: "One shot running started. See overview for logs."
+ runJobVerbose:
+ "Une exécution a été lancée. Voir l'overview pour plus de détails.",
+
+ // Original text: "Started"
+ jobStarted: 'Démarré',
+
+ // Original text: "Finished"
+ jobFinished: 'Terminé',
+
+ // Original text: "Save"
+ saveBackupJob: 'Enregistrer',
+
+ // Original text: "Remove backup job"
+ deleteBackupSchedule: 'Supprimer ce job de sauvegarde',
+
+ // Original text: "Are you sure you want to delete this backup job?"
+ deleteBackupScheduleQuestion:
+ 'Êtes-vous sûr de vouloir supprimer ce job de sauvegarde ?',
+
+ // Original text: "Enable immediately after creation"
+ scheduleEnableAfterCreation: 'Activer aussitôt après la création',
+
+ // Original text: "You are editing Schedule {name} ({id}). Saving will override previous schedule state."
+ scheduleEditMessage:
+ "Vous êtes en train d'éditer {name} ({id}). Enregistrer écrasera l'état planifié précédent.",
+
+ // Original text: "You are editing job {name} ({id}). Saving will override previous job state."
+ jobEditMessage:
+ "Vous êtes en train d'éditer le job {name} ({id}). Enregistrer écrasera l'état du job précédent.",
+
+ // Original text: "No scheduled jobs."
+ noScheduledJobs: 'Pas de job planifié.',
+
+ // Original text: "No jobs found."
+ noJobs: 'Pas de job trouvé.',
+
+ // Original text: "No schedules found"
+ noSchedules: 'Pas de planification trouvée',
+
+ // Original text: "Select a xo-server API command"
+ jobActionPlaceHolder: "Sélectionnez une commande de l'API xo-server",
+
+ // Original text: "Timeout (number of seconds after which a VM is considered failed)"
+ jobTimeoutPlaceHolder:
+ 'Temporisation (nombre de secondes autorisé pour chaque VM)',
+
+ // Original text: "Schedules"
+ jobSchedules: 'Planning',
+
+ // Original text: "Name of your schedule"
+ jobScheduleNamePlaceHolder: 'Nom de votre planification',
+
+ // Original text: "Select a Job"
+ jobScheduleJobPlaceHolder: 'Choisir un Job',
+
+ // Original text: "Job owner"
+ jobOwnerPlaceholder: 'Propriétaire',
+
+ // Original text: "This job's creator no longer exists"
+ jobUserNotFound: "Le propriétaire de ce job n'existe plus",
+
+ // Original text: "This backup's creator no longer exists"
+ backupUserNotFound: "Le propriétaire de cette sauvegarde n'existe plus",
+
+ // Original text: "Backup owner"
+ backupOwner: 'Propriétaire',
+
+ // Original text: "Select your backup type:"
+ newBackupSelection: 'Choisissez votre type de sauvegarde :',
+
+ // Original text: "Select backup mode:"
+ smartBackupModeSelection: 'Sélectionnez votre mode de sauvegarde :',
+
+ // Original text: "Normal backup"
+ normalBackup: 'Backup normal',
+
+ // Original text: "Smart backup"
+ smartBackup: 'Backup intelligent',
+
+ // Original text: "Local remote selected"
+ localRemoteWarningTitle: 'Emplacement local sélectionné',
+
+ // Original text: "Warning: local remotes will use limited XOA disk space. Only for advanced users."
+ localRemoteWarningMessage:
+ "Attention : utiliser un emplacement local limite l'espace pour XO. Restreignez ceci aux utilisateurs avancés.",
+
+ // Original text: "Warning: this feature works only with XenServer 6.5 or newer."
+ backupVersionWarning:
+ "Attention : cette fonctionnalité ne fonctionne qu'avec XenServer 6.5 et plus récent.",
+
+ // Original text: "VMs"
+ editBackupVmsTitle: 'VMs',
+
+ // Original text: "VMs statuses"
+ editBackupSmartStatusTitle: 'État des VMs',
+
+ // Original text: "Resident on"
+ editBackupSmartResidentOn: 'Situé sur',
+
+ // Original text: "Pools"
+ editBackupSmartPools: 'Pools',
+
+ // Original text: "Tags"
+ editBackupSmartTags: 'Tags',
+
+ // Original text: "VMs Tags"
+ editBackupSmartTagsTitle: 'Tags des VMs',
+
+ // Original text: "Reverse"
+ editBackupNot: 'Inverser',
+
+ // Original text: "Tag"
+ editBackupTagTitle: 'Tag',
+
+ // Original text: "Report"
+ editBackupReportTitle: 'Rapport',
+
+ // Original text: "Automatically run as scheduled"
+ editBackupScheduleEnabled: 'Executer en fonction de la planification',
+
+ // Original text: "Depth"
+ editBackupDepthTitle: 'Profondeur',
+
+ // Original text: "Remote"
+ editBackupRemoteTitle: 'Emplacement',
+
+ // Original text: "Delete the old backups first"
+ deleteOldBackupsFirst: 'Supprimer les anciennes sauvegardes',
+
+ // Original text: "Remote stores for backup"
+ remoteList: 'Emplacement de stockage de backup',
+
+ // Original text: "New File System Remote"
+ newRemote: 'Nouvel emplacement de stockage',
+
+ // Original text: "Local"
+ remoteTypeLocal: 'Local',
+
+ // Original text: "NFS"
+ remoteTypeNfs: 'NFS',
+
+ // Original text: "SMB"
+ remoteTypeSmb: 'SMB',
+
+ // Original text: "Type"
+ remoteType: 'Type',
+
+ // Original text: "Test your remote"
+ remoteTestTip: 'Testez votre emplacement',
+
+ // Original text: "Test Remote"
+ testRemote: "Tester l'emplacement",
+
+ // Original text: "Test failed for {name}"
+ remoteTestFailure: 'Test réussi pour {name}',
+
+ // Original text: "Test passed for {name}"
+ remoteTestSuccess: 'Test échoué pour {name}',
+
+ // Original text: "Error"
+ remoteTestError: 'Erreur',
+
+ // Original text: "Test Step"
+ remoteTestStep: 'Étape de test',
+
+ // Original text: "Test file"
+ remoteTestFile: 'Fichier de test',
+
+ // Original text: 'Test name'
+ remoteTestName: undefined,
+
+ // Original text: "Remote name already exists!"
+ remoteTestNameFailure: "Le nom de l'emplacement existe déjà",
+
+ // Original text: "The remote appears to work correctly"
+ remoteTestSuccessMessage: "L'emplacement distant semble marcher correctement",
+
+ // Original text: "Connection failed"
+ remoteConnectionFailed: 'La connexion a échoué',
+
+ // Original text: "Name"
+ remoteName: 'Nom',
+
+ // Original text: "Path"
+ remotePath: 'Chemin',
+
+ // Original text: "State"
+ remoteState: 'État',
+
+ // Original text: "Device"
+ remoteDevice: 'Équipement',
+
+ // Original text: "Share"
+ remoteShare: 'Partage',
+
+ // Original text: "Action"
+ remoteAction: 'Action',
+
+ // Original text: "Auth"
+ remoteAuth: 'Accès',
+
+ // Original text: "Mounted"
+ remoteMounted: 'Monté',
+
+ // Original text: "Unmounted"
+ remoteUnmounted: 'Démonté',
+
+ // Original text: "Connect"
+ remoteConnectTip: 'Connecter',
+
+ // Original text: "Disconnect"
+ remoteDisconnectTip: 'Déconnecter',
+
+ // Original text: "Connected"
+ remoteConnected: 'Connecté',
+
+ // Original text: "Disconnected"
+ remoteDisconnected: 'Déconnecté',
+
+ // Original text: "Delete"
+ remoteDeleteTip: 'Supprimer',
+
+ // Original text: "remote name *"
+ remoteNamePlaceHolder: 'nom distant*',
+
+ // Original text: "Name *"
+ remoteMyNamePlaceHolder: 'Nom *',
+
+ // Original text: "/path/to/backup"
+ remoteLocalPlaceHolderPath: '/chemin/de/la/sauvegarde',
+
+ // Original text: "host *"
+ remoteNfsPlaceHolderHost: 'hôte *',
+
+ // Original text: "path/to/backup"
+ remoteNfsPlaceHolderPath: 'chemin/de/la/sauvegarde',
+
+ // Original text: "subfolder [path\\to\\backup]"
+ remoteSmbPlaceHolderRemotePath:
+ 'sous-répertoire [chemin\\vers\\la\\sauvegarde]',
+
+ // Original text: "Username"
+ remoteSmbPlaceHolderUsername: "Nom d'utilisateur",
+
+ // Original text: "Password"
+ remoteSmbPlaceHolderPassword: 'Mot de passe',
+
+ // Original text: "Domain"
+ remoteSmbPlaceHolderDomain: 'Domaine',
+
+ // Original text: "\\ *"
+ remoteSmbPlaceHolderAddressShare: '\\ *',
+
+ // Original text: "password(fill to edit)"
+ remotePlaceHolderPassword: 'mot de passe (saisir ici pour éditer)',
+
+ // Original text: "Create a new SR"
+ newSrTitle: 'Créer un nouvel SR',
+
+ // Original text: "General"
+ newSrGeneral: 'Général',
+
+ // Original text: "Select Storage Type:"
+ newSrTypeSelection: 'Sélectionner un type de stockage :',
+
+ // Original text: "Settings"
+ newSrSettings: 'Paramètres',
+
+ // Original text: "Storage Usage"
+ newSrUsage: 'Utilisation du stockage',
+
+ // Original text: "Summary"
+ newSrSummary: 'Récapitulatif',
+
+ // Original text: "Host"
+ newSrHost: 'Hôtes',
+
+ // Original text: "Type"
+ newSrType: 'Type',
+
+ // Original text: "Name"
+ newSrName: 'Nom',
+
+ // Original text: "Description"
+ newSrDescription: 'Description',
+
+ // Original text: "Server"
+ newSrServer: 'Serveur',
+
+ // Original text: "Path"
+ newSrPath: 'Chemin',
+
+ // Original text: "IQN"
+ newSrIqn: 'IQN',
+
+ // Original text: "LUN"
+ newSrLun: 'LUN',
+
+ // Original text: "with auth."
+ newSrAuth: 'avec identification',
+
+ // Original text: "User Name"
+ newSrUsername: "Nom d'utilisateur",
+
+ // Original text: "Password"
+ newSrPassword: 'Mot de passe',
+
+ // Original text: "Device"
+ newSrDevice: 'Équipement',
+
+ // Original text: "in use"
+ newSrInUse: 'utilisé',
+
+ // Original text: "Size"
+ newSrSize: 'Taille',
+
+ // Original text: "Create"
+ newSrCreate: 'Créer',
+
+ // Original text: "Storage name"
+ newSrNamePlaceHolder: "Nom de l'emplacement",
+
+ // Original text: "Storage description"
+ newSrDescPlaceHolder: "Description de l'emplacement",
+
+ // Original text: "Address"
+ newSrAddressPlaceHolder: 'Adresse',
+
+ // Original text: "[port]"
+ newSrPortPlaceHolder: '[port]',
+
+ // Original text: "Username"
+ newSrUsernamePlaceHolder: "Nom d'utilisateur",
+
+ // Original text: "Password"
+ newSrPasswordPlaceHolder: 'Mot de passe',
+
+ // Original text: "Device, e.g /dev/sda…"
+ newSrLvmDevicePlaceHolder: 'Matériel, par exemple /dev/sda…',
+
+ // Original text: "/path/to/directory"
+ newSrLocalPathPlaceHolder: '/chemin/du/répertoire',
+
+ // Original text: "Users/Groups"
+ subjectName: 'Utilisateurs/Groupes',
+
+ // Original text: "Object"
+ objectName: 'Objet',
+
+ // Original text: "No acls found"
+ aclNoneFound: 'Aucun droit existant',
+
+ // Original text: "Role"
+ roleName: 'Rôle',
+
+ // Original text: "Create"
+ aclCreate: 'Créer',
+
+ // Original text: "New Group Name"
+ newGroupName: 'Nouveau nom de groupe',
+
+ // Original text: "Create Group"
+ createGroup: 'Créer un groupe',
+
+ // Original text: "Create"
+ createGroupButton: 'Créer',
+
+ // Original text: "Delete Group"
+ deleteGroup: 'Supprimer le groupe',
+
+ // Original text: "Are you sure you want to delete this group?"
+ deleteGroupConfirm: 'Êtes-vous sûr de vouloir supprimer ce groupe ?',
+
+ // Original text: "Remove user from Group"
+ removeUserFromGroup: "Supprimer l'utilisateur du groupe",
+
+ // Original text: "Are you sure you want to delete this user?"
+ deleteUserConfirm: 'Êtes-vous sûr de vouloir supprimer cet utilisateur ?',
+
+ // Original text: "Delete User"
+ deleteUser: "Supprimer l'utilisateur",
+
+ // Original text: "no user"
+ noUser: "pas d'utilisateur",
+
+ // Original text: "unknown user"
+ unknownUser: 'utilisateur inconnu',
+
+ // Original text: "No group found"
+ noGroupFound: 'Pas de groupe trouvé',
+
+ // Original text: "Name"
+ groupNameColumn: 'Nom',
+
+ // Original text: "Users"
+ groupUsersColumn: 'Utilisateur',
+
+ // Original text: "Add User"
+ addUserToGroupColumn: 'Ajouter un utilisateur',
+
+ // Original text: "Email"
+ userNameColumn: 'Email',
+
+ // Original text: "Permissions"
+ userPermissionColumn: 'Droits',
+
+ // Original text: "Password"
+ userPasswordColumn: 'Mot de passe',
+
+ // Original text: "Email"
+ userName: 'Email',
+
+ // Original text: "Password"
+ userPassword: 'Mot de passe',
+
+ // Original text: "Create"
+ createUserButton: 'Créer',
+
+ // Original text: "No user found"
+ noUserFound: "Pas d'utilisateur trouvé",
+
+ // Original text: "User"
+ userLabel: 'Utilisateur',
+
+ // Original text: "Admin"
+ adminLabel: 'Administrateur',
+
+ // Original text: "No user in group"
+ noUserInGroup: "Pas d'utilisateur dans ce groupe",
+
+ // Original text: "{users, number} user{users, plural, one {} other {s}}"
+ countUsers: '{users} utilisateur{users, plural, one {} other {s}}',
+
+ // Original text: "Select Permission"
+ selectPermission: 'Choisir les droits',
+
+ // Original text: "No plugins found"
+ noPlugins: 'Aucun plugin trouvé',
+
+ // Original text: "Auto-load at server start"
+ autoloadPlugin: 'Charger automatiquement au démarrage du serveur',
+
+ // Original text: "Save configuration"
+ savePluginConfiguration: 'Sauvegarder la configuration',
+
+ // Original text: "Delete configuration"
+ deletePluginConfiguration: 'Supprimer la configuration',
+
+ // Original text: "Plugin error"
+ pluginError: 'Erreur du greffon',
+
+ // Original text: "Unknown error"
+ unknownPluginError: 'Erreur inconnue',
+
+ // Original text: "Purge plugin configuration"
+ purgePluginConfiguration: 'Purger la configuration du greffon',
+
+ // Original text: "Are you sure you want to purge this configuration ?"
+ purgePluginConfigurationQuestion:
+ 'Êtes-vous sûr de vouloir purger la configuration de ce greffon ?',
+
+ // Original text: "Edit"
+ editPluginConfiguration: 'Éditer',
+
+ // Original text: "Cancel"
+ cancelPluginEdition: 'Annuler',
+
+ // Original text: "Plugin configuration"
+ pluginConfigurationSuccess: 'Configuration du greffon',
+
+ // Original text: "Plugin configuration successfully saved!"
+ pluginConfigurationChanges:
+ 'La configuration du greffon a été sauvegardée avec succés !',
+
+ // Original text: "Predefined configuration"
+ pluginConfigurationPresetTitle: 'Configuration pré-définie',
+
+ // Original text: "Choose a predefined configuration."
+ pluginConfigurationChoosePreset: 'Choisir une configuration pré-définie.',
+
+ // Original text: "Apply"
+ applyPluginPreset: 'Appliquer',
+
+ // Original text: "Save filter error"
+ saveNewUserFilterErrorTitle: "Erreur lors de l'enregistrement du filtre",
+
+ // Original text: "Bad parameter: name must be given."
+ saveNewUserFilterErrorBody: 'Erreur: un nom doit être spécifié.',
+
+ // Original text: "Name:"
+ filterName: 'Nom :',
+
+ // Original text: "Value:"
+ filterValue: 'Valeur :',
+
+ // Original text: "Save new filter"
+ saveNewFilterTitle: 'Enregistrer un nouveau filtre',
+
+ // Original text: "Set custom filters"
+ setUserFiltersTitle: 'Personnaliser un filtre',
+
+ // Original text: "Are you sure you want to set custom filters?"
+ setUserFiltersBody: 'Êtes-vous sûr de vouloir personnaliser un filtre ?',
+
+ // Original text: "Remove custom filter"
+ removeUserFilterTitle: 'Supprimer un filtre personnalisé',
+
+ // Original text: "Are you sure you want to remove custom filter?"
+ removeUserFilterBody:
+ 'Êtes-vous sûr de vouloir supprimer ce filtre personnalisé ?',
+
+ // Original text: "Default filter"
+ defaultFilter: 'Filtre par défaut',
+
+ // Original text: "Default filters"
+ defaultFilters: 'Filtres par défaut',
+
+ // Original text: "Custom filters"
+ customFilters: 'Filtre personnalisé',
+
+ // Original text: "Customize filters"
+ customizeFilters: 'Personnaliser les filtres',
+
+ // Original text: "Save custom filters"
+ saveCustomFilters: 'Sauvegarder les filtres personnalisés',
+
+ // Original text: "Start"
+ startVmLabel: 'Créer',
+
+ // Original text: "Recovery start"
+ recoveryModeLabel: 'Démarrage de dépannage',
+
+ // Original text: "Suspend"
+ suspendVmLabel: 'Suspendre',
+
+ // Original text: "Stop"
+ stopVmLabel: 'Arrêter',
+
+ // Original text: "Force shutdown"
+ forceShutdownVmLabel: "Forcer l'arrêt",
+
+ // Original text: "Reboot"
+ rebootVmLabel: 'Redémarrer',
+
+ // Original text: "Force reboot"
+ forceRebootVmLabel: 'Forcer le redémarrage',
+
+ // Original text: "Delete"
+ deleteVmLabel: 'Supprimer',
+
+ // Original text: "Migrate"
+ migrateVmLabel: 'Migrer',
+
+ // Original text: "Snapshot"
+ snapshotVmLabel: 'Instantané',
+
+ // Original text: "Export"
+ exportVmLabel: 'Exporter',
+
+ // Original text: "Resume"
+ resumeVmLabel: 'Reprendre',
+
+ // Original text: "Copy"
+ copyVmLabel: 'Copier',
+
+ // Original text: "Clone"
+ cloneVmLabel: 'Cloner',
+
+ // Original text: "Fast clone"
+ fastCloneVmLabel: 'Clonage rapide',
+
+ // Original text: "Convert to template"
+ convertVmToTemplateLabel: 'Convertir en template',
+
+ // Original text: "Console"
+ vmConsoleLabel: 'Console',
+
+ // Original text: "Rescan all disks"
+ srRescan: 'Rescanner tous les disques',
+
+ // Original text: "Connect to all hosts"
+ srReconnectAll: 'Connecter sur tous les hôtes',
+
+ // Original text: "Disconnect from all hosts"
+ srDisconnectAll: 'Déconnecter de tous les hôtes',
+
+ // Original text: "Forget this SR"
+ srForget: 'Oublier ce SR',
+
+ // Original text: "Forget SRs"
+ srsForget: 'Oublier les stockages',
+
+ // Original text: "Remove this SR"
+ srRemoveButton: 'Supprimer ce SR',
+
+ // Original text: "No VDIs in this storage"
+ srNoVdis: 'Pas de VDI sur ce stockage',
+
+ // Original text: "Pool RAM usage:"
+ poolTitleRamUsage: 'Utilisation RAM du pool :',
+
+ // Original text: "{used} used on {total}"
+ poolRamUsage: '{used} utilisé sur {total}',
+
+ // Original text: "Master:"
+ poolMaster: 'Maître :',
+
+ // Original text: "Display all hosts of this pool"
+ displayAllHosts: 'Afficher les hôtes du pool',
+
+ // Original text: "Display all storages of this pool"
+ displayAllStorages: 'Afficher les stockages du pool',
+
+ // Original text: "Display all VMs of this pool"
+ displayAllVMs: 'Afficher les VMs du pool',
+
+ // Original text: "Hosts"
+ hostsTabName: 'Hôtes',
+
+ // Original text: "Vms"
+ vmsTabName: 'VMs',
+
+ // Original text: "Srs"
+ srsTabName: 'stockages',
+
+ // Original text: "High Availability"
+ poolHaStatus: 'Haute Disponibilité',
+
+ // Original text: "Enabled"
+ poolHaEnabled: 'Activé',
+
+ // Original text: "Disabled"
+ poolHaDisabled: 'Désactivé',
+
+ // Original text: "Name"
+ hostNameLabel: 'Nom',
+
+ // Original text: "Description"
+ hostDescription: 'Description',
+
+ // Original text: "Memory"
+ hostMemory: 'Mémoire',
+
+ // Original text: "No hosts"
+ noHost: "Pas d'hôte",
+
+ // Original text: "{used}% used ({free} free)"
+ memoryLeftTooltip: '{used}% utilisé ({free} libre)',
+
+ // Original text: "PIF"
+ pif: 'PIF',
+
+ // Original text: "Name"
+ poolNetworkNameLabel: 'Nom',
+
+ // Original text: "Description"
+ poolNetworkDescription: 'Description',
+
+ // Original text: "PIFs"
+ poolNetworkPif: 'PIFs',
+
+ // Original text: "No networks"
+ poolNoNetwork: 'Pas de réseaux',
+
+ // Original text: "MTU"
+ poolNetworkMTU: 'MTU',
+
+ // Original text: "Connected"
+ poolNetworkPifAttached: 'Connecté',
+
+ // Original text: "Disconnected"
+ poolNetworkPifDetached: 'Déconnecté',
+
+ // Original text: "Show PIFs"
+ showPifs: 'Afficher les PIFs',
+
+ // Original text: "Hide PIFs"
+ hidePifs: 'Cacher les PIFs',
+
+ // Original text: "Show details"
+ showDetails: 'Afficher les détails',
+
+ // Original text: "Hide details"
+ hideDetails: 'Cacher les détails',
+
+ // Original text: "No stats"
+ poolNoStats: 'Pas de statistiques',
+
+ // Original text: "All hosts"
+ poolAllHosts: 'Tous les hôtes',
+
+ // Original text: "Add SR"
+ addSrLabel: 'Ajouter un SR',
+
+ // Original text: "Add VM"
+ addVmLabel: 'Ajouter une VM',
+
+ // Original text: "Add Host"
+ addHostLabel: 'Ajouter un hôte',
+
+ // Original text: "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."
+ hostNeedsPatchUpdate:
+ "Cet hôte a besoin d'installer {patches, number} patch{patches, plural, one {} other {es}} avant de pouvoir être ajouté au pool. Cette opération peut être longue.",
+
+ // Original text: "This host cannot be added to the pool because it's missing some patches."
+ hostNeedsPatchUpdateNoInstall:
+ 'Cette hôte ne peut pas être ajouté au pool car il lui manque des patches.',
+
+ // Original text: "Adding host failed"
+ addHostErrorTitle: "L'ajout de l'hôte a échoué.",
+
+ // Original text: "Host patches could not be homogenized."
+ addHostNotHomogeneousErrorMessage:
+ "Les patches de l'hôte n'ont pas pu être homogénéisés.",
+
+ // Original text: "Disconnect"
+ disconnectServer: 'Déconnecter',
+
+ // Original text: "Start"
+ startHostLabel: 'Démarrer',
+
+ // Original text: "Stop"
+ stopHostLabel: 'Arrêter',
+
+ // Original text: "Enable"
+ enableHostLabel: 'Activer',
+
+ // Original text: "Disable"
+ disableHostLabel: 'Désactiver',
+
+ // Original text: "Restart toolstack"
+ restartHostAgent: 'Redémarrer la toolstack',
+
+ // Original text: "Force reboot"
+ forceRebootHostLabel: 'Forcer un redémarrage',
+
+ // Original text: "Reboot"
+ rebootHostLabel: 'Redémarrer',
+
+ // Original text: "Error while restarting host"
+ noHostsAvailableErrorTitle: "Erreur lors du redémarrage de l'hôte",
+
+ // Original text: "Some VMs cannot be migrated before restarting this host. Please try force reboot."
+ noHostsAvailableErrorMessage:
+ "Certaines VMs ne peuvent pas être migrées avant le redémarrage de l'hôte. Essayez de forcer le redémarrage.",
+
+ // Original text: "Error while restarting hosts"
+ failHostBulkRestartTitle: 'Erreur lors du redémarrage des hôtes',
+
+ // Original text: "{failedHosts, number}/{totalHosts, number} host{failedHosts, plural, one {} other {s}} could not be restarted."
+ failHostBulkRestartMessage:
+ "{failedHosts, number}/{totalHosts, number} {failedHosts, plural, one {hôte n'a pas pu être redémarré} other {n'ont pas pu être redémarrés}} ",
+
+ // Original text: "Reboot to apply updates"
+ rebootUpdateHostLabel: 'Redémarrer pour appliquer les mises à jour',
+
+ // Original text: "Emergency mode"
+ emergencyModeLabel: 'Emergency mode',
+
+ // Original text: "Storage"
+ storageTabName: 'Stockage',
+
+ // Original text: "Patches"
+ patchesTabName: 'Patches',
+
+ // Original text: "Load average"
+ statLoad: 'Charge (load) moyenne :',
+
+ // Original text: "RAM Usage: {memoryUsed}"
+ memoryHostState: 'Mémoire utilisée : {memoryUsed}',
+
+ // Original text: "Hardware"
+ hardwareHostSettingsLabel: 'Matériel',
+
+ // Original text: "Address"
+ hostAddress: 'Adresse',
+
+ // Original text: "Status"
+ hostStatus: 'État',
+
+ // Original text: "Build number"
+ hostBuildNumber: 'Numéro de build',
+
+ // Original text: "iSCSI name"
+ hostIscsiName: 'Nom iSCSI',
+
+ // Original text: "Version"
+ hostXenServerVersion: 'Version',
+
+ // Original text: "Enabled"
+ hostStatusEnabled: 'Activé',
+
+ // Original text: "Disabled"
+ hostStatusDisabled: 'Désactivé',
+
+ // Original text: "Power on mode"
+ hostPowerOnMode: "Mode d'allumage",
+
+ // Original text: "Host uptime"
+ hostStartedSince: "Temps d'activité",
+
+ // Original text: "Toolstack uptime"
+ hostStackStartedSince: 'Toolstack uptime',
+
+ // Original text: "CPU model"
+ hostCpusModel: 'Modèle de CPU',
+
+ // Original text: "Core (socket)"
+ hostCpusNumber: 'Cœur (socket)',
+
+ // Original text: "Manufacturer info"
+ hostManufacturerinfo: 'Informations constructeur',
+
+ // Original text: "BIOS info"
+ hostBiosinfo: 'Informations BIOS',
+
+ // Original text: "Licence"
+ licenseHostSettingsLabel: 'Licence',
+
+ // Original text: "Type"
+ hostLicenseType: 'Type',
+
+ // Original text: "Socket"
+ hostLicenseSocket: 'Socket',
+
+ // Original text: "Expiry"
+ hostLicenseExpiry: 'Expiration',
+
+ // Original text: "Installed supplemental packs"
+ supplementalPacks: 'Packs supplémentaires installés',
+
+ // Original text: "Install new supplemental pack"
+ supplementalPackNew: 'Installer un nouveau pack supplémentaire',
+
+ // Original text: "Install supplemental pack on every host"
+ supplementalPackPoolNew:
+ 'Installer un pack supplémentaire sur tous les hôtes',
+
+ // Original text: "{name} (by {author})"
+ supplementalPackTitle: '{name} (par {author})',
+
+ // Original text: "Installation started"
+ supplementalPackInstallStartedTitle: 'Installation démarrée',
+
+ // Original text: "Installing new supplemental pack…"
+ supplementalPackInstallStartedMessage:
+ "Installation d'un nouveau pack supplémentaire",
+
+ // Original text: "Installation error"
+ supplementalPackInstallErrorTitle: "Erreur d'installation",
+
+ // Original text: "The installation of the supplemental pack failed."
+ supplementalPackInstallErrorMessage:
+ "L'installation du pack supplémentaire a échoué.",
+
+ // Original text: "Installation success"
+ supplementalPackInstallSuccessTitle: "Succès de l'installation",
+
+ // Original text: "Supplemental pack successfully installed."
+ supplementalPackInstallSuccessMessage:
+ 'Le pack supplémentaire a été installé avec succès.',
+
+ // Original text: "Add a network"
+ networkCreateButton: 'Ajouter un réseau',
+
+ // Original text: "Add a bonded network"
+ networkCreateBondedButton: 'Ajouter un réseau agrégé',
+
+ // Original text: "Device"
+ pifDeviceLabel: 'Device',
+
+ // Original text: "Network"
+ pifNetworkLabel: 'Réseau',
+
+ // Original text: "VLAN"
+ pifVlanLabel: 'VLAN',
+
+ // Original text: "Address"
+ pifAddressLabel: 'Adresse',
+
+ // Original text: "Mode"
+ pifModeLabel: 'Mode',
+
+ // Original text: "MAC"
+ pifMacLabel: 'MAC',
+
+ // Original text: "MTU"
+ pifMtuLabel: 'MTU',
+
+ // Original text: "Status"
+ pifStatusLabel: 'État',
+
+ // Original text: "Connected"
+ pifStatusConnected: 'Connecté',
+
+ // Original text: "Disconnected"
+ pifStatusDisconnected: 'Déconnecté',
+
+ // Original text: "No physical interface detected"
+ pifNoInterface: "Pas d'interface physique détectée",
+
+ // Original text: "This interface is currently in use"
+ pifInUse: "Cette interface est en cours d'utilisation",
+
+ // Original text: "Action"
+ pifAction: 'Action',
+
+ // Original text: "Default locking mode"
+ defaultLockingMode: 'Verrouillage par défaut',
+
+ // Original text: "Configure IP address"
+ pifConfigureIp: "Configurer l'adresse IP",
+
+ // Original text: "Invalid parameters"
+ configIpErrorTitle: 'Paramètres invalides',
+
+ // Original text: "IP address and netmask required"
+ configIpErrorMessage: 'Adresse IP et masque de réseau requis',
+
+ // Original text: "Static IP address"
+ staticIp: 'Adresse IP statique',
+
+ // Original text: "Netmask"
+ netmask: 'Masque de réseau',
+
+ // Original text: "DNS"
+ dns: 'DNS',
+
+ // Original text: "Gateway"
+ gateway: 'Passerelle',
+
+ // Original text: "Add a storage"
+ addSrDeviceButton: 'Ajouter un stockage',
+
+ // Original text: "Name"
+ srNameLabel: 'Nom',
+
+ // Original text: "Type"
+ srType: 'Type',
+
+ // Original text: "Action"
+ pbdAction: 'Action',
+
+ // Original text: "Status"
+ pbdStatus: 'État',
+
+ // Original text: "Connected"
+ pbdStatusConnected: 'Connecté',
+
+ // Original text: "Disconnected"
+ pbdStatusDisconnected: 'Déconnecté',
+
+ // Original text: "Connect"
+ pbdConnect: 'Connecter',
+
+ // Original text: "Disconnect"
+ pbdDisconnect: 'Déconnecter',
+
+ // Original text: "Forget"
+ pbdForget: 'Oublier',
+
+ // Original text: "Shared"
+ srShared: 'Partager',
+
+ // Original text: "Not shared"
+ srNotShared: 'Non partagé',
+
+ // Original text: "No storage detected"
+ pbdNoSr: 'Pas de stockage détecté',
+
+ // Original text: "Name"
+ patchNameLabel: 'Nom',
+
+ // Original text: "Install all patches"
+ patchUpdateButton: 'installer tous les patchs',
+
+ // Original text: "Description"
+ patchDescription: 'Description',
+
+ // Original text: "Applied date"
+ patchApplied: "Date d'installation",
+
+ // Original text: "Size"
+ patchSize: 'Taille',
+
+ // Original text: "Status"
+ patchStatus: 'État',
+
+ // Original text: "Applied"
+ patchStatusApplied: 'Appliqué',
+
+ // Original text: "Missing patches"
+ patchStatusNotApplied: 'Patches manquants',
+
+ // Original text: "No patch detected"
+ patchNothing: 'Pas de patch détecté',
+
+ // Original text: "Release date"
+ patchReleaseDate: 'Date de diffusion',
+
+ // Original text: "Guidance"
+ patchGuidance: 'Guidance',
+
+ // Original text: "Action"
+ patchAction: 'Action',
+
+ // Original text: "Applied patches"
+ hostAppliedPatches: 'Patches appliqués',
+
+ // Original text: "Missing patches"
+ hostMissingPatches: 'Patches manquants',
+
+ // Original text: "Host up-to-date!"
+ hostUpToDate: 'Hôte à jour !',
+
+ // Original text: "Non-recommended patch install"
+ installPatchWarningTitle: 'Installation de patch non recommandée',
+
+ // Original text: "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:
+ "Installer un patch sur un hôte seul est déconseillé. Il est recommandé d'aller sur la page du pool et de faire l'installation sur tous les hôtes.",
+
+ // Original text: "Go to pool"
+ installPatchWarningReject: 'Aller au pool',
+
+ // Original text: "Install"
+ installPatchWarningResolve: 'Installer',
+
+ // Original text: "Refresh patches"
+ refreshPatches: 'Rafraichir patchs',
+
+ // Original text: "Install pool patches"
+ installPoolPatches: 'Installer les patchs sur le pool',
+
+ // Original text: "Default SR"
+ defaultSr: 'SR par défaut',
+
+ // Original text: "Set as default SR"
+ setAsDefaultSr: 'Définir comme SR par défaut',
+
+ // Original text: "General"
+ generalTabName: 'Général',
+
+ // Original text: "Stats"
+ statsTabName: 'Stats',
+
+ // Original text: "Console"
+ consoleTabName: 'Console',
+
+ // Original text: "Container"
+ containersTabName: 'Conteneur',
+
+ // Original text: "Snapshots"
+ snapshotsTabName: 'Instantanés',
+
+ // Original text: "Logs"
+ logsTabName: 'Journaux',
+
+ // Original text: "Advanced"
+ advancedTabName: 'Avancé',
+
+ // Original text: "Network"
+ networkTabName: 'Réseaux',
+
+ // Original text: "Disk{disks, plural, one {} other {s}}"
+ disksTabName: 'Disque{disks, plural, one {} other {s}}',
+
+ // Original text: "halted"
+ powerStateHalted: 'stoppé',
+
+ // Original text: "running"
+ powerStateRunning: 'en cours',
+
+ // Original text: "suspended"
+ powerStateSuspended: 'suspendu',
+
+ // Original text: "No Xen tools detected"
+ vmStatus: 'Pas de Xen tools détectés',
+
+ // Original text: "No IPv4 record"
+ vmName: "Pas d'enregistrement IPv4",
+
+ // Original text: "No IP record"
+ vmDescription: "Pas d'enregistrement IP",
+
+ // Original text: "Started {ago}"
+ vmSettings: 'Démarré il y a {ago}',
+
+ // Original text: "Current status:"
+ vmCurrentStatus: 'État actuel :',
+
+ // Original text: "Not running"
+ vmNotRunning: 'Éteinte',
+
+ // Original text: "Halted {ago}"
+ vmHaltedSince: 'Stoppée {ago}',
+
+ // Original text: "No Xen tools detected"
+ noToolsDetected: 'Pas de Xen tools détectés',
+
+ // Original text: "No IPv4 record"
+ noIpv4Record: "Pas d'enregistrement IPv4",
+
+ // Original text: "No IP record"
+ noIpRecord: "Pas d'enregistrement IP",
+
+ // Original text: "Started {ago}"
+ started: 'Démarré {ago}',
+
+ // Original text: "Paravirtualization (PV)"
+ paraVirtualizedMode: 'Paravirtualisation (PV)',
+
+ // Original text: "Hardware virtualization (HVM)"
+ hardwareVirtualizedMode: 'Virtualisation matérielle (HVM)',
+
+ // Original text: "CPU usage"
+ statsCpu: 'Utilisation CPU',
+
+ // Original text: "Memory usage"
+ statsMemory: 'Utilisation mémoire',
+
+ // Original text: "Network throughput"
+ statsNetwork: 'Échanges réseaux',
+
+ // Original text: "Stacked values"
+ useStackedValuesOnStats: 'Valeurs cumulées',
+
+ // Original text: "Disk throughput"
+ statDisk: 'Échanges disques',
+
+ // Original text: "Last 10 minutes"
+ statLastTenMinutes: 'Les 10 dernières minutes',
+
+ // Original text: "Last 2 hours"
+ statLastTwoHours: 'Les 2 dernières heures',
+
+ // Original text: "Last week"
+ statLastWeek: 'La dernière semaine',
+
+ // Original text: "Last year"
+ statLastYear: 'La dernière année',
+
+ // Original text: "Copy"
+ copyToClipboardLabel: 'Copier',
+
+ // Original text: "Ctrl+Alt+Del"
+ ctrlAltDelButtonLabel: 'Ctrl+Alt+Supp',
+
+ // Original text: "Tip:"
+ tipLabel: 'Astuces :',
+
+ // Original text: "Hide infos"
+ hideHeaderTooltip: 'Cacher les infos',
+
+ // Original text: "Show infos"
+ showHeaderTooltip: 'Afficher les infos',
+
+ // Original text: "Name"
+ containerName: 'Nom',
+
+ // Original text: "Command"
+ containerCommand: 'Commande',
+
+ // Original text: "Creation date"
+ containerCreated: 'Date de création',
+
+ // Original text: "Status"
+ containerStatus: 'État',
+
+ // Original text: "Action"
+ containerAction: 'Action',
+
+ // Original text: "No existing containers"
+ noContainers: 'Aucun conteneur',
+
+ // Original text: "Stop this container"
+ containerStop: 'Arrêter ce conteneur',
+
+ // Original text: "Start this container"
+ containerStart: 'Démarrer ce conteneur',
+
+ // Original text: "Pause this container"
+ containerPause: 'Mettre ce conteneur en pause',
+
+ // Original text: "Resume this container"
+ containerResume: 'Relancer ce conteneur',
+
+ // Original text: "Restart this container"
+ containerRestart: 'Redémarrer ce conteneur',
+
+ // Original text: "Action"
+ vdiAction: 'Action',
+
+ // Original text: "Attach disk"
+ vdiAttachDeviceButton: 'Attacher un disque',
+
+ // Original text: "New disk"
+ vbdCreateDeviceButton: 'Nouveau disque',
+
+ // Original text: "Boot order"
+ vdiBootOrder: 'Séquence de démarrage',
+
+ // Original text: "Name"
+ vdiNameLabel: 'Nom',
+
+ // Original text: "Description"
+ vdiNameDescription: 'Description',
+
+ // Original text: "Pool"
+ vdiPool: 'Pool',
+
+ // Original text: "Disconnect"
+ vdiDisconnect: 'Déconnecté',
+
+ // Original text: "Tags"
+ vdiTags: 'Tags',
+
+ // Original text: "Size"
+ vdiSize: 'Taille',
+
+ // Original text: "SR"
+ vdiSr: 'SR',
+
+ // Original text: "VM"
+ vdiVm: 'VM',
+
+ // Original text: "Migrate VDI"
+ vdiMigrate: 'Migrer le VDI',
+
+ // Original text: "Destination SR:"
+ vdiMigrateSelectSr: 'SR de destination :',
+
+ // Original text: "Migrate all VDIs"
+ vdiMigrateAll: 'Migrer tous les VDIs',
+
+ // Original text: "No SR"
+ vdiMigrateNoSr: 'Pas de SR',
+
+ // Original text: "A target SR is required to migrate a VDI"
+ vdiMigrateNoSrMessage: 'Un SR cible est nécessaire pour migrer un VDI',
+
+ // Original text: "Forget"
+ vdiForget: 'Oublier',
+
+ // Original text: "Remove VDI"
+ vdiRemove: 'Supprimer le VDI',
+
+ // Original text: "No VDIs attached to Control Domain"
+ noControlDomainVdis: 'Aucun VDI attaché au Control Domain',
+
+ // Original text: "Boot flag"
+ vbdBootableStatus: 'Boot flag',
+
+ // Original text: "Status"
+ vbdStatus: 'État',
+
+ // Original text: "Connected"
+ vbdStatusConnected: 'Connecté',
+
+ // Original text: "Disconnected"
+ vbdStatusDisconnected: 'Déconnecté',
+
+ // Original text: "No disks"
+ vbdNoVbd: 'Pas de disque',
+
+ // Original text: "Connect VBD"
+ vbdConnect: 'Connecter un VBD',
+
+ // Original text: "Disconnect VBD"
+ vbdDisconnect: 'Déconnecter un VBD',
+
+ // Original text: "Bootable"
+ vbdBootable: 'Bootable',
+
+ // Original text: "Readonly"
+ vbdReadonly: 'Lecture seule',
+
+ // Original text: "Action"
+ vbdAction: 'Action',
+
+ // Original text: "Create"
+ vbdCreate: 'Créer',
+
+ // Original text: "Disk name"
+ vbdNamePlaceHolder: 'Nom du disque',
+
+ // Original text: "Size"
+ vbdSizePlaceHolder: 'Taille',
+
+ // Original text: "CD drive not completely installed"
+ cdDriveNotInstalled: "Le lecteur CD n'est pas complètement installé",
+
+ // Original text: "Stop and start the VM to install the CD drive"
+ cdDriveInstallation: 'Arrêtez et démarrez la VM pour installer le lecteur CD',
+
+ // Original text: "Save"
+ saveBootOption: 'Enregistrer',
+
+ // Original text: "Reset"
+ resetBootOption: 'Réinitialiser',
+
+ // Original text: "New device"
+ vifCreateDeviceButton: 'Nouvelle interface',
+
+ // Original text: "No interface"
+ vifNoInterface: "Pas d'interface",
+
+ // Original text: "Device"
+ vifDeviceLabel: 'Device',
+
+ // Original text: "MAC address"
+ vifMacLabel: 'Adresse MAC',
+
+ // Original text: "MTU"
+ vifMtuLabel: 'MTU',
+
+ // Original text: "Network"
+ vifNetworkLabel: 'Réseaux',
+
+ // Original text: "Status"
+ vifStatusLabel: 'État',
+
+ // Original text: "Connected"
+ vifStatusConnected: 'Connecté',
+
+ // Original text: "Disconnected"
+ vifStatusDisconnected: 'Déconnecté',
+
+ // Original text: "Connect"
+ vifConnect: 'Connecter',
+
+ // Original text: "Disconnect"
+ vifDisconnect: 'Déconnecter',
+
+ // Original text: "Remove"
+ vifRemove: 'Supprimer',
+
+ // Original text: "IP addresses"
+ vifIpAddresses: 'Adresses IP',
+
+ // Original text: "Auto-generated if empty"
+ vifMacAutoGenerate: 'Si vide, généré automatiquement',
+
+ // Original text: "Allowed IPs"
+ vifAllowedIps: 'IPs autorisées',
+
+ // Original text: "No IPs"
+ vifNoIps: "Pas d'IP",
+
+ // Original text: "Network locked"
+ vifLockedNetwork: 'Réseau verrouillé',
+
+ // Original text: "Network locked and no IPs are allowed for this interface"
+ vifLockedNetworkNoIps:
+ "Le réseau est verrouillé et aucune IP n'est autorisée sur cette interface",
+
+ // Original text: "Network not locked"
+ vifUnLockedNetwork: 'Réseau non verrouillé',
+
+ // Original text: "Unknown network"
+ vifUnknownNetwork: 'Réseau inconnu',
+
+ // Original text: "Action"
+ vifAction: 'Action',
+
+ // Original text: "Create"
+ vifCreate: 'Créer',
+
+ // Original text: "No snapshots"
+ noSnapshots: "Pas d'instantané",
+
+ // Original text: "New snapshot"
+ snapshotCreateButton: 'Nouvel instantané',
+
+ // Original text: "Just click on the snapshot button to create one!"
+ tipCreateSnapshotLabel:
+ "Cliquer simplement sur le bouton d'instantané pour en créer un !",
+
+ // Original text: "Revert VM to this snapshot"
+ revertSnapshot: 'Restaurer la MV à cet instantané',
+
+ // Original text: "Remove this snapshot"
+ deleteSnapshot: 'Supprimer cet instantané',
+
+ // Original text: "Create a VM from this snapshot"
+ copySnapshot: 'Créer une VM depuis cet instantané',
+
+ // Original text: "Export this snapshot"
+ exportSnapshot: 'Exporter cet instantané',
+
+ // Original text: "Creation date"
+ snapshotDate: 'Date de création',
+
+ // Original text: "Name"
+ snapshotName: 'Nom',
+
+ // Original text: "Name"
+ snapshotDescription: 'Description',
+
+ // Original text: "Action"
+ snapshotAction: 'Action',
+
+ // Original text: "Quiesced snapshot"
+ snapshotQuiesce: 'Instantané quiesce',
+
+ // Original text: "Remove all logs"
+ logRemoveAll: 'Supprimer tous les journaux',
+
+ // Original text: "No logs so far"
+ noLogs: 'Pas de journaux jusque là',
+
+ // Original text: "Creation date"
+ logDate: 'Date de création',
+
+ // Original text: "Name"
+ logName: 'Nom',
+
+ // Original text: "Content"
+ logContent: 'Contenu',
+
+ // Original text: "Action"
+ logAction: 'Action',
+
+ // Original text: "Remove"
+ vmRemoveButton: 'Supprimer',
+
+ // Original text: "Convert"
+ vmConvertButton: 'Convertir',
+
+ // Original text: "Xen settings"
+ xenSettingsLabel: 'Configuration Xen',
+
+ // Original text: "Guest OS"
+ guestOsLabel: 'OS invité',
+
+ // Original text: "Misc"
+ miscLabel: 'Divers',
+
+ // Original text: "UUID"
+ uuid: 'UUID',
+
+ // Original text: "Virtualization mode"
+ virtualizationMode: 'Mode de virtualisation',
+
+ // Original text: "CPU weight"
+ cpuWeightLabel: 'Pondération CPU',
+
+ // Original text: "Default ({value, number})"
+ defaultCpuWeight: 'Défaut ({value, number})',
+
+ // Original text: "CPU cap"
+ cpuCapLabel: 'Fonctionnalités CPU',
+
+ // Original text: "Default ({value, number})"
+ defaultCpuCap: 'Défaut ({value, number})',
+
+ // Original text: "PV args"
+ pvArgsLabel: 'PV params',
+
+ // Original text: "Xen tools status"
+ xenToolsStatus: 'État des Xen tools',
+
+ // Original text: "{status}"
+ xenToolsStatusValue: '{status}',
+
+ // Original text: "OS name"
+ osName: "Nom de l'OS",
+
+ // Original text: "OS kernel"
+ osKernel: "Kernel de l'OS",
+
+ // Original text: "Auto power on"
+ autoPowerOn: 'Allumage automatique',
+
+ // Original text: "HA"
+ ha: 'Haute Dispo',
+
+ // Original text: "Affinity host"
+ vmAffinityHost: 'Hôte préféré',
+
+ // Original text: "VGA"
+ vmVga: 'VGA',
+
+ // Original text: "Video RAM"
+ vmVideoram: 'Mémoire vidéo',
+
+ // Original text: "None"
+ noAffinityHost: 'Aucun',
+
+ // Original text: "Original template"
+ originalTemplate: "Template d'origine",
+
+ // Original text: "Unknown"
+ unknownOsName: 'Inconnu',
+
+ // Original text: "Unknown"
+ unknownOsKernel: 'Inconnu',
+
+ // Original text: "Unknown"
+ unknownOriginalTemplate: 'Inconnu',
+
+ // Original text: "VM limits"
+ vmLimitsLabel: 'Limites de la VM',
+
+ // Original text: "CPU limits"
+ vmCpuLimitsLabel: 'Limites de CPU',
+
+ // Original text: "Topology"
+ vmCpuTopology: 'Topologie',
+
+ // Original text: "Default behavior"
+ vmChooseCoresPerSocket: 'Comportement par défaut',
+
+ // Original text: "{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}} avec {nCores, number} cœur{nCores, plural, one {} other {s}} par socket',
+
+ // Original text: "Incorrect cores per socket value"
+ vmCoresPerSocketIncorrectValue: 'Valeur incorrecte de cœurs par socket',
+
+ // Original text: "Please change the selected value to fix it."
+ vmCoresPerSocketIncorrectValueSolution: 'Veuillez modifier la valeur.',
+
+ // Original text: "Memory limits (min/max)"
+ vmMemoryLimitsLabel: 'Limites de mémoire (min/max)',
+
+ // Original text: "vCPUs max:"
+ vmMaxVcpus: 'vCPUs max :',
+
+ // Original text: "Memory max:"
+ vmMaxRam: 'Mémoire max :',
+
+ // Original text: "Long click to add a name"
+ vmHomeNamePlaceholder: 'Clic long pour définir un nom',
+
+ // Original text: "Long click to add a description"
+ vmHomeDescriptionPlaceholder: 'Clic long pour définir une description',
+
+ // Original text: "Click to add a name"
+ vmViewNamePlaceholder: 'Cliquer pour définir un nom',
+
+ // Original text: "Click to add a description"
+ vmViewDescriptionPlaceholder: 'Cliquer pour définir une description',
+
+ // Original text: "Click to add a name"
+ templateHomeNamePlaceholder: 'Cliquer pour ajouter un nom',
+
+ // Original text: "Click to add a description"
+ templateHomeDescriptionPlaceholder: 'Cliquer pour ajouter une description',
+
+ // Original text: "Delete template"
+ templateDelete: 'Supprimer le template',
+
+ // Original text: "Delete VM template{templates, plural, one {} other {s}}"
+ templateDeleteModalTitle:
+ 'Supprimer le(s) template{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:
+ 'Êtes-vous sûr de vouloir supprimer ce(s) template(s) ?',
+
+ // Original text: "Pool{pools, plural, one {} other {s}}"
+ poolPanel: 'Pool{pools, plural, one {} other {s}}',
+
+ // Original text: "Host{hosts, plural, one {} other {s}}"
+ hostPanel: 'Hôte{hosts, plural, one {} other {s}}',
+
+ // Original text: "VM{vms, plural, one {} other {s}}"
+ vmPanel: 'VM{vms, plural, one {} other {s}}',
+
+ // Original text: "RAM Usage:"
+ memoryStatePanel: 'Utilisation RAM',
+
+ // Original text: "CPUs Usage"
+ cpuStatePanel: 'Utilisation CPUs',
+
+ // Original text: "VMs Power state"
+ vmStatePanel: 'Etats des VMs',
+
+ // Original text: "Pending tasks"
+ taskStatePanel: 'Tâches en attente',
+
+ // Original text: "Users"
+ usersStatePanel: 'Utilisateurs',
+
+ // Original text: "Storage state"
+ srStatePanel: 'État du stockage',
+
+ // Original text: "{usage} (of {total})"
+ ofUsage: '{usage} (sur {total})',
+
+ // Original text: "No storage"
+ noSrs: 'Pas de stockage',
+
+ // Original text: "Name"
+ srName: 'Nom',
+
+ // Original text: "Pool"
+ srPool: 'Pool',
+
+ // Original text: "Host"
+ srHost: 'Hôte',
+
+ // Original text: "Type"
+ srFormat: 'Type',
+
+ // Original text: "Size"
+ srSize: 'Taille',
+
+ // Original text: "Usage"
+ srUsage: 'Usage',
+
+ // Original text: "used"
+ srUsed: 'utilisé',
+
+ // Original text: "free"
+ srFree: 'libre',
+
+ // Original text: "Storage Usage"
+ srUsageStatePanel: 'Utilisation du stockage',
+
+ // Original text: "Top 5 SR Usage (in %)"
+ srTopUsageStatePanel: "Top 5 d'utilisation des SRs (en %)",
+
+ // Original text: "{running, number} running ({halted, number} halted)"
+ vmsStates:
+ '{running} allumée{halted, plural, one {} other {s}} ({halted} éteinte{halted, plural, one {} other {s}})',
+
+ // Original text: "Clear selection"
+ dashboardStatsButtonRemoveAll: 'Vider la sélection',
+
+ // Original text: "Add all hosts"
+ dashboardStatsButtonAddAllHost: 'Ajouter tous les hôtes',
+
+ // Original text: "Add all VMs"
+ dashboardStatsButtonAddAllVM: 'Ajouter toutes les VMs',
+
+ // Original text: "{value} {date, date, medium}"
+ weekHeatmapData: '{value} {date, date, medium}',
+
+ // Original text: "No data."
+ weekHeatmapNoData: 'Pas de données.',
+
+ // Original text: "Weekly Heatmap"
+ weeklyHeatmap: 'Heatmap hebdomadaire',
+
+ // Original text: "Weekly Charts"
+ weeklyCharts: 'Graphes hebdomadaires',
+
+ // Original text: "Synchronize scale:"
+ weeklyChartsScaleInfo: 'Synchroniser les échelles :',
+
+ // Original text: "Stats error"
+ statsDashboardGenericErrorTitle: 'Erreurs de stats',
+
+ // Original text: "There is no stats available for:"
+ statsDashboardGenericErrorMessage: 'Pas de statistiques disponibles pour :',
+
+ // Original text: "No selected metric"
+ noSelectedMetric: 'Pas de métrique sélectionnée.',
+
+ // Original text: "Select"
+ statsDashboardSelectObjects: 'Sélectionner',
+
+ // Original text: "Loading…"
+ metricsLoading: 'Chargement en cours…',
+
+ // Original text: "Coming soon!"
+ comingSoon: "C'est pour bientôt !",
+
+ // Original text: "Orphaned snapshot VDIs"
+ orphanedVdis: 'Instantanés de VDIs orphelins',
+
+ // Original text: "Orphaned VMs snapshot"
+ orphanedVms: 'Instantanés VMs orphelins',
+
+ // Original text: "No orphans"
+ noOrphanedObject: "Pas d'orphelin",
+
+ // Original text: "Remove all orphaned snapshot VDIs"
+ removeAllOrphanedObject: 'Supprimer tous les snapshots de VDIs orphelins',
+
+ // Original text: "VDIs attached to Control Domain"
+ vdisOnControlDomain: 'VDIs attachés au Control Domain',
+
+ // Original text: "Name"
+ vmNameLabel: 'Nom',
+
+ // Original text: "Description"
+ vmNameDescription: 'Description',
+
+ // Original text: "Resident on"
+ vmContainer: 'Situé sur',
+
+ // Original text: "Alarms"
+ alarmMessage: 'Alarmes',
+
+ // Original text: "No alarms"
+ noAlarms: "Pas d'alarmes",
+
+ // Original text: "Date"
+ alarmDate: 'Date',
+
+ // Original text: "Content"
+ alarmContent: 'Contenu',
+
+ // Original text: "Issue on"
+ alarmObject: 'Concerné',
+
+ // Original text: "Pool"
+ alarmPool: 'Pool',
+
+ // Original text: "Remove all alarms"
+ alarmRemoveAll: 'Supprimer toutes les alarmes',
+
+ // Original text: "{used}% used ({free} left)"
+ spaceLeftTooltip: '{used}% utilisés ({free} restants)',
+
+ // Original text: "Create a new VM on {select}"
+ newVmCreateNewVmOn: 'Créer une nouvelle VM sur {select}',
+
+ // Original text: "Create a new VM on {select1} or {select2}"
+ newVmCreateNewVmOn2: 'Créer une nouvelle VM sur {select1} ou {select2}',
+
+ // Original text: "You have no permission to create a VM"
+ newVmCreateNewVmNoPermission: "Vous n'avez pas les droits pour créer une VM",
+
+ // Original text: "Infos"
+ newVmInfoPanel: 'Infos',
+
+ // Original text: "Name"
+ newVmNameLabel: 'Nom',
+
+ // Original text: "Template"
+ newVmTemplateLabel: 'Template',
+
+ // Original text: "Description"
+ newVmDescriptionLabel: 'Description',
+
+ // Original text: "Performances"
+ newVmPerfPanel: 'Performances',
+
+ // Original text: "vCPUs"
+ newVmVcpusLabel: 'vCPUs',
+
+ // Original text: "RAM"
+ newVmRamLabel: 'RAM',
+
+ // Original text: "Static memory max"
+ newVmStaticMaxLabel: 'Mémoire fixe max',
+
+ // Original text: "Dynamic memory min"
+ newVmDynamicMinLabel: 'Mémoire dynamique min',
+
+ // Original text: "Dynamic memory max"
+ newVmDynamicMaxLabel: 'Mémoire dynamique max',
+
+ // Original text: "Install settings"
+ newVmInstallSettingsPanel: "Paramètres d'installation",
+
+ // Original text: "ISO/DVD"
+ newVmIsoDvdLabel: 'ISO/DVD',
+
+ // Original text: "Network"
+ newVmNetworkLabel: 'Réseau',
+
+ // Original text: "e.g: http://httpredir.debian.org/debian"
+ newVmInstallNetworkPlaceHolder: 'ex : http://httpredir.debian.org/debian',
+
+ // Original text: "PV Args"
+ newVmPvArgsLabel: 'PV Args',
+
+ // Original text: "PXE"
+ newVmPxeLabel: 'PXE',
+
+ // Original text: "Interfaces"
+ newVmInterfacesPanel: 'Interfaces',
+
+ // Original text: "MAC"
+ newVmMacLabel: 'MAC',
+
+ // Original text: "Add interface"
+ newVmAddInterface: 'Ajouter interface',
+
+ // Original text: "Disks"
+ newVmDisksPanel: 'Disques',
+
+ // Original text: "SR"
+ newVmSrLabel: 'SR',
+
+ // Original text: "Size"
+ newVmSizeLabel: 'Taille',
+
+ // Original text: "Add disk"
+ newVmAddDisk: 'Ajouter un disque',
+
+ // Original text: "Summary"
+ newVmSummaryPanel: 'Récapitulatif',
+
+ // Original text: "Create"
+ newVmCreate: 'Créer',
+
+ // Original text: "Reset"
+ newVmReset: 'Réinitialiser',
+
+ // Original text: "Select template"
+ newVmSelectTemplate: 'Sélectionner un template',
+
+ // Original text: "SSH key"
+ newVmSshKey: 'Clef SSH',
+
+ // Original text: "Config drive"
+ newVmConfigDrive: 'Config drive',
+
+ // Original text: "Custom config"
+ newVmCustomConfig: 'Configuration personnalisée',
+
+ // Original text: "Boot VM after creation"
+ newVmBootAfterCreate: 'Démarrer la VM après sa création',
+
+ // Original text: "Auto-generated if empty"
+ newVmMacPlaceholder: 'Si vide, généré automatiquement',
+
+ // Original text: "CPU weight"
+ newVmCpuWeightLabel: 'Pondération CPU',
+
+ // Original text: "Default: {value, number}"
+ newVmDefaultCpuWeight: 'Par défaut: {value, number}',
+
+ // Original text: "CPU cap"
+ newVmCpuCapLabel: 'Fonctionnalités CPU',
+
+ // Original text: "Default: {value, number}"
+ newVmDefaultCpuCap: 'Par défaut : {value, number}',
+
+ // Original text: "Cloud config"
+ newVmCloudConfig: 'Configuration Cloud',
+
+ // Original text: "Create VMs"
+ newVmCreateVms: 'Créer les VMs',
+
+ // Original text: "Are you sure you want to create {nbVms, number} VMs?"
+ newVmCreateVmsConfirm: 'Êtes-vous sûr de vouloir créer {nbVms} VMs ?',
+
+ // Original text: "Multiple VMs:"
+ newVmMultipleVms: 'Multiples VMs :',
+
+ // Original text: "Select a resource set:"
+ newVmSelectResourceSet: 'Choisir un jeu de ressources :',
+
+ // Original text: "Name pattern:"
+ newVmMultipleVmsPattern: 'Motif de nom :',
+
+ // Original text: "e.g.: \\{name\\}_%"
+ newVmMultipleVmsPatternPlaceholder: 'ex. : \\{name\\}_%',
+
+ // Original text: "First index:"
+ newVmFirstIndex: 'Première itération :',
+
+ // Original text: "Recalculate VMs number"
+ newVmNumberRecalculate: 'Recalculer le nombre des VMs',
+
+ // Original text: "Refresh VMs name"
+ newVmNameRefresh: 'Rafraîchir le nom des VMs',
+
+ // Original text: "Affinity host"
+ newVmAffinityHost: 'Hôte préféré',
+
+ // Original text: "Advanced"
+ newVmAdvancedPanel: 'Avancé',
+
+ // Original text: "Show advanced settings"
+ newVmShowAdvanced: 'Afficher les paramètres avancés',
+
+ // Original text: "Hide advanced settings"
+ newVmHideAdvanced: 'Cacher les paramètres avancés',
+
+ // Original text: "Share this VM"
+ newVmShare: 'Partager cette VM',
+
+ // Original text: "Resource sets"
+ resourceSets: 'Jeu de ressources',
+
+ // Original text: "No resource sets."
+ noResourceSets: 'Pas de jeu de ressources.',
+
+ // Original text: "Loading resource sets"
+ loadingResourceSets: 'Chargement des jeux de ressources…',
+
+ // Original text: "Resource set name"
+ resourceSetName: 'Nom du jeu de ressources',
+
+ // Original text: "Recompute all limits"
+ recomputeResourceSets: 'Recalculer les limites',
+
+ // Original text: "Save"
+ saveResourceSet: 'Enregistrer',
+
+ // Original text: "Reset"
+ resetResourceSet: 'Réinitialiser',
+
+ // Original text: "Edit"
+ editResourceSet: 'Éditer',
+
+ // Original text: "Delete"
+ deleteResourceSet: 'Supprimer',
+
+ // Original text: "Delete resource set"
+ deleteResourceSetWarning: 'Supprimer le jeu de ressources',
+
+ // Original text: "Are you sure you want to delete this resource set?"
+ deleteResourceSetQuestion:
+ 'Êtes-vous sûr de vouloir supprimer ce jeu de ressources ?',
+
+ // Original text: "Missing objects:"
+ resourceSetMissingObjects: 'Objets manquants :',
+
+ // Original text: "vCPUs"
+ resourceSetVcpus: 'vCPUs',
+
+ // Original text: "Memory"
+ resourceSetMemory: 'Mémoire',
+
+ // Original text: "Storage"
+ resourceSetStorage: 'Stockage',
+
+ // Original text: "Unknown"
+ unknownResourceSetValue: 'Inconnu',
+
+ // Original text: "Available hosts"
+ availableHosts: 'Hôtes disponibles',
+
+ // Original text: "Excluded hosts"
+ excludedHosts: 'Hôtes exclus',
+
+ // Original text: "No hosts available."
+ noHostsAvailable: "Pas d'hôte disponible.",
+
+ // Original text: "VMs created from this resource set shall run on the following hosts."
+ availableHostsDescription:
+ 'Les VMs créées sur ce jeu de ressources doivent être démarrées sur les hôtes suivants.',
+
+ // Original text: "Maximum CPUs"
+ maxCpus: 'CPUs maximum',
+
+ // Original text: "Maximum RAM (GiB)"
+ maxRam: 'RAM maximum (GiB)',
+
+ // Original text: "Maximum disk space"
+ maxDiskSpace: 'Espace disque maximum',
+
+ // Original text: "IP pool"
+ ipPool: "Plages d'IPs",
+
+ // Original text: "Quantity"
+ quantity: 'Quantité',
+
+ // Original text: "No limits."
+ noResourceSetLimits: 'Pas de limites.',
+
+ // Original text: "Total:"
+ totalResource: 'Total :',
+
+ // Original text: "Remaining:"
+ remainingResource: 'Restant :',
+
+ // Original text: "Used:"
+ usedResource: 'Utilisé :',
+
+ // Original text: "New"
+ resourceSetNew: 'Nouvelle',
+
+ // Original text: "Try dropping some VMs files here, or click to select VMs to upload. Accept only .xva/.ova files."
+ importVmsList:
+ 'Essayez de déposer des fichiers de VMs ici, ou bien cliquez pour sélectionner des VMs à téléverser. Seuls les fichiers .xva/.ova sont acceptés.',
+
+ // Original text: "No selected VMs."
+ noSelectedVms: 'Pas de VM sélectionnée.',
+
+ // Original text: "To Pool:"
+ vmImportToPool: 'Sur le Pool:',
+
+ // Original text: "To SR:"
+ vmImportToSr: 'Sur le SR:',
+
+ // Original text: "VMs to import"
+ vmsToImport: 'VMs à importer',
+
+ // Original text: "Reset"
+ importVmsCleanList: 'Réinitialiser',
+
+ // Original text: "VM import success"
+ vmImportSuccess: 'Import de VM réussi',
+
+ // Original text: "VM import failed"
+ vmImportFailed: 'Import de VM échoué',
+
+ // Original text: "Import starting…"
+ startVmImport: "L'import commence…",
+
+ // Original text: "Export starting…"
+ startVmExport: "L'export commence…",
+
+ // Original text: "N CPUs"
+ nCpus: 'N CPUs',
+
+ // Original text: "Memory"
+ vmMemory: 'Mémoire',
+
+ // Original text: "Disk {position} ({capacity})"
+ diskInfo: 'Disque {position} ({capacity})',
+
+ // Original text: "Disk description"
+ diskDescription: 'Description du disque',
+
+ // Original text: "No disks."
+ noDisks: 'Pas de disque.',
+
+ // Original text: "No networks."
+ noNetworks: 'Pas de réseau.',
+
+ // Original text: "Network {name}"
+ networkInfo: 'Réseau {name}',
+
+ // Original text: "No description available"
+ noVmImportErrorDescription: 'Pas de description disponible',
+
+ // Original text: "Error:"
+ vmImportError: 'Erreur :',
+
+ // Original text: "{type} file:"
+ vmImportFileType: '{type} fichier:',
+
+ // Original text: "Please to check and/or modify the VM configuration."
+ vmImportConfigAlert:
+ 'Merci de vérifier et/ou modifier la configuration de la VM.',
+
+ // Original text: "No pending tasks"
+ noTasks: 'Pas de tâche en attente',
+
+ // Original text: "Currently, there are not any pending XenServer tasks"
+ xsTasks: "Actuellement, il n'y a aucune tâche en attente",
+
+ // Original text: "Schedules"
+ backupSchedules: 'Planifier',
+
+ // Original text: "Get remote"
+ getRemote: 'Récupérer les emplacements',
+
+ // Original text: "List Remote"
+ listRemote: 'Lister les emplacements',
+
+ // Original text: "simple"
+ simpleBackup: 'simple',
+
+ // Original text: "delta"
+ delta: 'delta',
+
+ // Original text: "Restore Backups"
+ restoreBackups: 'Restauration de sauvegardes',
+
+ // Original text: "Click on a VM to display restore options"
+ restoreBackupsInfo:
+ 'Cliquez sur une VM pour afficher les options de récupération',
+
+ // Original text: "Only the files of Delta Backup which are not on a SMB remote can be restored"
+ restoreDeltaBackupsInfo:
+ 'Seuls les fichiers de Delta Backup qui ne sont pas sur un emplacement SMB peuvent être restaurés',
+
+ // Original text: "Enabled"
+ remoteEnabled: 'activé',
+
+ // Original text: "Error"
+ remoteError: 'Erreur',
+
+ // Original text: "No backup available"
+ noBackup: 'Pas de sauvegarde disponible',
+
+ // Original text: "VM Name"
+ backupVmNameColumn: 'Nom de la VM',
+
+ // Original text: "Tags"
+ backupTags: 'Tags',
+
+ // Original text: "Last Backup"
+ lastBackupColumn: 'Dernière sauvegarde',
+
+ // Original text: "Available Backups"
+ availableBackupsColumn: 'Sauvegardes disponibles',
+
+ // Original text: "Missing parameters"
+ backupRestoreErrorTitle: 'Paramètres manquants',
+
+ // Original text: "Choose a SR and a backup"
+ backupRestoreErrorMessage: 'Choisir un SR et une sauvegarde',
+
+ // Original text: "Select default SR…"
+ backupRestoreSelectDefaultSr: 'Sélectionner le SR par défaut…',
+
+ // Original text: "Choose a SR for each VDI"
+ backupRestoreChooseSrForEachVdis: 'Choisir un SR pour chaque VDI',
+
+ // Original text: "VDI"
+ backupRestoreVdiLabel: 'VDI',
+
+ // Original text: "SR"
+ backupRestoreSrLabel: 'SR',
+
+ // Original text: "Display backups"
+ displayBackup: 'Afficher les sauvegardes',
+
+ // Original text: "Import VM"
+ importBackupTitle: 'Importer une VM',
+
+ // Original text: "Starting your backup import"
+ importBackupMessage: "Démarrer l'import d'une sauvegarde",
+
+ // Original text: "VMs to backup"
+ vmsToBackup: 'VMs à sauvegarder',
+
+ // Original text: "List remote backups"
+ listRemoteBackups: 'Lister les emplacements de sauvegardes',
+
+ // Original text: "Restore backup files"
+ restoreFiles: 'Restaurer les fichiers de sauvegarde',
+
+ // Original text: "Invalid options"
+ restoreFilesError: 'Options invalides',
+
+ // Original text: "Restore file from {name}"
+ restoreFilesFromBackup: 'Restaurer les fichiers depuis {name}',
+
+ // Original text: "Select a backup…"
+ restoreFilesSelectBackup: 'Sélectionner une sauvegarde…',
+
+ // Original text: "Select a disk…"
+ restoreFilesSelectDisk: 'Sélectionner un disque…',
+
+ // Original text: "Select a partition…"
+ restoreFilesSelectPartition: 'Sélectionner un partition…',
+
+ // Original text: "Folder path"
+ restoreFilesSelectFolderPath: 'Chemin du dossier',
+
+ // Original text: "Select a file…"
+ restoreFilesSelectFiles: 'Sélectionner un fichier…',
+
+ // Original text: "Content not found"
+ restoreFileContentNotFound: 'Contenu non trouvé',
+
+ // Original text: "No files selected"
+ restoreFilesNoFilesSelected: 'Pas de fichier sélectionné',
+
+ // Original text: "Selected files ({files}):"
+ restoreFilesSelectedFiles: 'Fichiers sélectionnés ({files}) :',
+
+ // Original text: "Error while scanning disk"
+ restoreFilesDiskError: 'Erreur lors du scan du disque',
+
+ // Original text: "Select all this folder's files"
+ restoreFilesSelectAllFiles: 'Sélectionner tous les fichiers de ce dossier',
+
+ // Original text: "Unselect all files"
+ restoreFilesUnselectAll: 'Déselectionner tous les fichiers',
+
+ // Original text: "Emergency shutdown Host{nHosts, plural, one {} other {s}}"
+ emergencyShutdownHostsModalTitle:
+ "Extinction d'urgence {nHosts, plural, one {de l'hôte} other {des hôtes}}",
+
+ // Original text: "Are you sure you want to shutdown {nHosts, number} Host{nHosts, plural, one {} other {s}}?"
+ emergencyShutdownHostsModalMessage:
+ 'Êtes-vous sûr de vouloir arrêter {nHosts} hôte{nHosts, plural, one {} other {s}}?',
+
+ // Original text: "Shutdown host"
+ stopHostModalTitle: "Arrêter l'hôte",
+
+ // 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:
+ "Vous allez éteindre cet hôte. Voulez-vous continuer ? Si c'est le Maître du Pool, la connexion à tout le Pool sera perdue.",
+
+ // Original text: "Add host"
+ addHostModalTitle: 'Ajouter un hôte',
+
+ // Original text: "Are you sure you want to add {host} to {pool}?"
+ addHostModalMessage: 'Êtes-vous sûr de vouloir ajouter {host} à {pool}?',
+
+ // Original text: "Restart host"
+ restartHostModalTitle: "Redémarrer l'hôte",
+
+ // Original text: "This will restart your host. Do you want to continue?"
+ restartHostModalMessage:
+ 'Votre hôte va devoir redémarrer. Voulez-vous continuer ?',
+
+ // Original text: "Restart Host{nHosts, plural, one {} other {s}} agent{nHosts, plural, one {} other {s}}"
+ restartHostsAgentsModalTitle:
+ "Redémarrer les agents {nHosts, plural, one {de l'hôte} other {des hôtes}}",
+
+ // Original text: "Are you sure you want to restart {nHosts, number} 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}} ?",
+
+ // Original text: "Restart Host{nHosts, plural, one {} other {s}}"
+ restartHostsModalTitle:
+ "Redémarrer {nHosts, plural, one {l'} other {les}} hôte{nHosts, plural, one {} other {s}}",
+
+ // Original text: "Are you sure you want to restart {nHosts, number} Host{nHosts, plural, one {} other {s}}?"
+ restartHostsModalMessage:
+ "Êtes-vous sûr de vouloir redémarrer {nHosts, plural, one {l'} other {les}} hôte{nHosts, plural, one {} other {s}} ?",
+
+ // Original text: "Start VM{vms, plural, one {} other {s}}"
+ startVmsModalTitle:
+ 'Démarrer {vms, plural, one {la} other {les}} VM{vms, plural, one {} other {s}}',
+
+ // Original text: "Start a copy"
+ cloneAndStartVM: 'Démarrer une copie',
+
+ // Original text: "Force start"
+ forceStartVm: 'Forcer le démarrage',
+
+ // Original text: "Forbidden operation"
+ forceStartVmModalTitle: 'Opération non autorisée',
+
+ // Original text: "Start operation for this vm is blocked."
+ blockedStartVmModalMessage: 'Le démarrage est bloqué pour cette VM.',
+
+ // Original text: "Forbidden operation start for {nVms, number} vm{nVms, plural, one {} other {s}}."
+ blockedStartVmsModalMessage:
+ 'Démarrage non autorisé pour {nVms, number} VM{nVms, plural, one {} other {s}}',
+
+ // Original text: "Are you sure you want to start {vms, number} VM{vms, plural, one {} other {s}}?"
+ startVmsModalMessage:
+ 'Êtes-vous sûr de vouloir démarrer {vms, plural, one {la} other {les}} {vms} VM{vms, plural, one {} other {s}} ?',
+
+ // Original text: "{nVms, number} vm{nVms, plural, one {} other {s}} are failed. Please see your logs to get more information"
+ failedVmsErrorMessage:
+ "{nVms, number} VM{nVms, plural, one {} other {s}} ont échoué. Veuillez consulter les journaux pour plus d'informations",
+
+ // Original text: "Start failed"
+ failedVmsErrorTitle: 'Echec du démarrage',
+
+ // Original text: "Stop Host{nHosts, plural, one {} other {s}}"
+ stopHostsModalTitle:
+ "Arrêter {nHosts, plural, one {l'} other {les}} hôte{nHosts, plural, one {} other {s}}",
+
+ // Original text: "Are you sure you want to stop {nHosts, number} Host{nHosts, plural, one {} other {s}}?"
+ stopHostsModalMessage:
+ "Êtes-vous sûr de vouloir arrêter {nHosts, plural, one {l'} other {les}} hôte{nHosts, plural, one {} other {s}} ?",
+
+ // Original text: "Stop VM{vms, plural, one {} other {s}}"
+ stopVmsModalTitle:
+ 'Éteindre {vms, plural, one {cette} other {ces}} VM{vms, plural, one {} other {s}}',
+
+ // Original text: "Are you sure you want to stop {vms, number} VM{vms, plural, one {} other {s}}?"
+ stopVmsModalMessage:
+ 'Êtes-vous sûr de vouloir éteindre {vms, plural, one {cette} other {ces}} {vms} VM{vms, plural, one {} other {s}} ?',
+
+ // Original text: "Restart VM"
+ restartVmModalTitle: 'Redémarrer la VM',
+
+ // Original text: "Are you sure you want to restart {name}?"
+ restartVmModalMessage: 'Êtes-vous sûr de vouloir redémarrer {name}?',
+
+ // Original text: "Stop VM"
+ stopVmModalTitle: 'Arrêter la VM',
+
+ // Original text: "Are you sure you want to stop {name}?"
+ stopVmModalMessage: 'Êtes-vous sûr de vouloir arrêter {name}?',
+
+ // Original text: "Restart VM{vms, plural, one {} other {s}}"
+ restartVmsModalTitle:
+ 'Redémarrer {vms, plural, one {la} other {les}} VM{vms, plural, one {} other {s}}',
+
+ // Original text: "Are you sure you want to restart {vms, number} VM{vms, plural, one {} other {s}}?"
+ restartVmsModalMessage:
+ 'Êtes-vous sûr de vouloir redémarrer {vms, plural, one {la} other {les}} VM{vms, plural, one {} other {s}} {vms} ?',
+
+ // Original text: "Snapshot VM{vms, plural, one {} other {s}}"
+ snapshotVmsModalTitle:
+ 'Faire un instantané {vms, plural, one {de la} other {des}} VM{vms, plural, one {} other {s}}',
+
+ // Original text: "Are you sure you want to snapshot {vms, number} VM{vms, plural, one {} other {s}}?"
+ snapshotVmsModalMessage:
+ 'Êtes-vous sûr de vouloir faire un instantané {vms, plural, one {de la VM} other {des {vms} VMs}} ?',
+
+ // Original text: "Delete VM{vms, plural, one {} other {s}}"
+ deleteVmsModalTitle:
+ 'Supprimer {vms, plural, one {la} other {les}} VM{vms, plural, one {} other {s}}',
+
+ // Original text: "Are you sure you want to delete {vms, number} VM{vms, plural, one {} other {s}}? ALL VM DISKS WILL BE REMOVED"
+ deleteVmsModalMessage:
+ 'Êtes-vous sûr de vouloir supprimer {vms, plural, one {la VM} other {les {vms} VMs}} ? TOUS LES DISQUES ASSOCIÉS SERONT SUPPRIMÉS',
+
+ // Original text: "Delete VM"
+ deleteVmModalTitle: 'Supprimer la VM',
+
+ // Original text: "Are you sure you want to delete this VM? ALL VM DISKS WILL BE REMOVED"
+ deleteVmModalMessage:
+ 'Êtes-vous sûr de vouloir supprimer cette VM ? TOUS LES DISQUES DE LA VM SERONT SUPPRIMÉS DEFINITIVEMENT',
+
+ // Original text: "Migrate VM"
+ migrateVmModalTitle: 'Migrer la VM',
+
+ // Original text: "Select a destination host:"
+ migrateVmSelectHost: 'Sélectionner un hôte de destination :',
+
+ // Original text: "Select a migration network:"
+ migrateVmSelectMigrationNetwork: 'Choisir un réseau de migration :',
+
+ // Original text: "For each VIF, select a network:"
+ migrateVmSelectNetworks: 'Pour chaque VIF, choisir un réseau :',
+
+ // Original text: "Select a destination SR:"
+ migrateVmsSelectSr: 'Sélectionner un SR de destination :',
+
+ // Original text: "Select a destination SR for local disks:"
+ migrateVmsSelectSrIntraPool:
+ 'Choisir un SR de destination pour les disques locaux :',
+
+ // Original text: "Select a network on which to connect each VIF:"
+ migrateVmsSelectNetwork: 'Choisir un réseau pour chaque VIF :',
+
+ // Original text: "Smart mapping"
+ migrateVmsSmartMapping: 'Réaffectation intelligente',
+
+ // Original text: "VIF"
+ migrateVmVif: 'VIF',
+
+ // Original text: "Network"
+ migrateVmNetwork: 'Réseaux',
+
+ // Original text: "No target host"
+ migrateVmNoTargetHost: "Pas d'hôte cible",
+
+ // Original text: "A target host is required to migrate a VM"
+ migrateVmNoTargetHostMessage:
+ 'Un hôte cible est nécessaire pour migrer une VM',
+
+ // Original text: "No default SR"
+ migrateVmNoDefaultSrError: 'Pas de SR par défaut',
+
+ // Original text: "Default SR not connected to host"
+ migrateVmNotConnectedDefaultSrError:
+ "Le SR par défaut n'est pas connecté à l'hôte",
+
+ // Original text: "For each VDI, select an SR:"
+ chooseSrForEachVdisModalSelectSr: 'Pour chaque VDI, sélectionner un SR :',
+
+ // Original text: "Select main SR…"
+ chooseSrForEachVdisModalMainSr: 'Sélectionner le SR principal…',
+
+ // Original text: "VDI"
+ chooseSrForEachVdisModalVdiLabel: 'VDI',
+
+ // Original text: "SR*"
+ chooseSrForEachVdisModalSrLabel: 'SR*',
+
+ // Original text: "* optional"
+ chooseSrForEachVdisModalOptionalEntry: '* optionnel',
+
+ // Original text: "Delete VDI"
+ deleteVdiModalTitle: 'Supprimer le VDI',
+
+ // Original text: "Are you sure you want to delete this disk? ALL DATA ON THIS DISK WILL BE LOST"
+ deleteVdiModalMessage:
+ 'Êtes-vous sûr de vouloir supprimer ce disque ? TOUTES LES DONNÉES CONTENUES SERONT PERDUES IRRÉMÉDIABLEMENT',
+
+ // Original text: "Revert your VM"
+ revertVmModalTitle: 'Restaurer la VM',
+
+ // Original text: "Delete snapshot"
+ deleteSnapshotModalTitle: "Supprimer l'instantané",
+
+ // Original text: "Are you sure you want to delete this snapshot?"
+ deleteSnapshotModalMessage:
+ 'Êtes-vous sûr de vouloir supprimer cet instantané ?',
+
+ // Original text: "Are you sure you want to revert this VM to the snapshot state? This operation is irreversible."
+ revertVmModalMessage:
+ "Êtes-vous sûr de vouloir restaurer cette VM à l'état de cet instantané ? Cette opération est irrévocable.",
+
+ // Original text: "Snapshot before"
+ revertVmModalSnapshotBefore: 'Faire un instantané avant',
+
+ // Original text: "Import a {name} Backup"
+ importBackupModalTitle: 'Importer une sauvegarde de {name}',
+
+ // Original text: "Start VM after restore"
+ importBackupModalStart: 'Démarrer la VM après la restauration',
+
+ // Original text: "Select your backup…"
+ importBackupModalSelectBackup: 'Sélectionnez votre sauvegarde…',
+
+ // Original text: "Are you sure you want to remove all orphaned snapshot VDIs?"
+ removeAllOrphanedModalWarning:
+ 'Êtes-vous sûr de vouloir supprimer tous les instantanés de VDIs orphelins ?',
+
+ // Original text: "Remove all logs"
+ removeAllLogsModalTitle: 'Supprimer tous les journaux',
+
+ // Original text: "Are you sure you want to remove all logs?"
+ removeAllLogsModalWarning:
+ 'Êtes-vous sûr de vouloir supprimer tous les journaux ?',
+
+ // Original text: "This operation is definitive."
+ definitiveMessageModal: 'Cette action est irréversible.',
+
+ // Original text: "Previous SR Usage"
+ existingSrModalTitle: 'Emplacement utilisé',
+
+ // 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:
+ 'Cet emplacement avait été utilisé auparavant comme un Stockage par un hôte XenServer. Toutes les données présentes seront perdues si vous décidez de continuer la création du SR.',
+
+ // Original text: "Previous LUN Usage"
+ existingLunModalTitle: 'LUN utilisé',
+
+ // 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:
+ 'Ce LUN avait été utilisé auparavant comme un Stockage par un hôte XenServer. Toutes les données présentes seront perdues si vous décidez de continuer la création du SR.',
+
+ // Original text: "Replace current registration?"
+ alreadyRegisteredModal: "Remplacer l'enregistrement actuel ?",
+
+ // Original text: "Your XO appliance is already registered to {email}, do you want to forget and replace this registration ?"
+ alreadyRegisteredModalText:
+ 'Votre instance XOA est déjà enregistrée pour {email}, voulez-vous remplacer cet enregistrement ?',
+
+ // Original text: "Ready for trial?"
+ trialReadyModal: "Prêt pour l'essai ?",
+
+ // Original text: "During the trial period, XOA need to have a working internet connection. This limitation does not apply for our paid plans!"
+ trialReadyModalText:
+ "Durant la période de démonstration, XOA nécessite une connexion internet fonctionnelle. Cette limitation disparaît avec la souscription d'une de nos formules.",
+
+ // Original text: "Label"
+ serverLabel: 'Nom',
+
+ // Original text: "Host"
+ serverHost: 'Hôte',
+
+ // Original text: "Username"
+ serverUsername: "Nom d'utilisateur",
+
+ // Original text: "Password"
+ serverPassword: 'Mot de passe',
+
+ // Original text: "Action"
+ serverAction: 'Action',
+
+ // Original text: "Read Only"
+ serverReadOnly: 'Lecture seule',
+
+ // Original text: "Unauthorized Certificates"
+ serverUnauthorizedCertificates: 'Certificats non approuvés',
+
+ // Original text: "Allow Unauthorized Certificates"
+ serverAllowUnauthorizedCertificates:
+ 'Autoriser les certificats non approuvés',
+
+ // Original text: "Enable it if your certificate is rejected, but it's not recommended because your connection will not be secured."
+ serverUnauthorizedCertificatesInfo:
+ "Activez ceci si votre certificat est rejeté, mais ce n'est pas recommandé car votre connexion ne sera pas sécurisée.",
+
+ // Original text: "Disconnect server"
+ serverDisconnect: 'Déconnecter le serveur',
+
+ // Original text: "username"
+ serverPlaceHolderUser: "nom d'utilisateur",
+
+ // Original text: "password"
+ serverPlaceHolderPassword: 'mot de passe',
+
+ // Original text: "address[:port]"
+ serverPlaceHolderAddress: 'adresse[:port]',
+
+ // Original text: "label"
+ serverPlaceHolderLabel: 'nom',
+
+ // Original text: "Connect"
+ serverConnect: 'Connecter',
+
+ // Original text: "Error"
+ serverError: 'Erreur',
+
+ // Original text: "Adding server failed"
+ serverAddFailed: "Echec de l'ajout du serveur",
+
+ // Original text: "Status"
+ serverStatus: 'Statut',
+
+ // Original text: "Connection failed. Click for more information."
+ serverConnectionFailed:
+ "Echec de connexion. Cliquer pour plus d'informations.",
+
+ // Original text: "Connecting…"
+ serverConnecting: 'Connexion…',
+
+ // Original text: "Connected"
+ serverConnected: 'Connecté',
+
+ // Original text: "Disconnected"
+ serverDisconnected: 'Déconnecté',
+
+ // Original text: "Authentication error"
+ serverAuthFailed: "Erreur d'authentification",
+
+ // Original text: "Unknown error"
+ serverUnknownError: 'Erreur inconnue',
+
+ // Original text: "Invalid self-signed certificate"
+ serverSelfSignedCertError: 'Certificat auto-signé rejeté',
+
+ // Original text: "Do you want to accept self-signed certificate for this server even though it would decrease security?"
+ serverSelfSignedCertQuestion:
+ 'Voulez-vous accepter un certificat auto-signé pour ce serveur même si cela réduit la sécurité ?',
+
+ // Original text: "Copy VM"
+ copyVm: 'Copier la VM',
+
+ // Original text: "Are you sure you want to copy this VM to {SR}?"
+ copyVmConfirm: 'Êtes-vous sûr de vouloir copier cette VM vers {SR}?',
+
+ // Original text: "Name"
+ copyVmName: 'Nom',
+
+ // Original text: "Name pattern"
+ copyVmNamePattern: 'Motif de nom',
+
+ // Original text: "If empty: name of the copied VM"
+ copyVmNamePlaceholder: 'Si vide : nom de la VM',
+
+ // Original text: "e.g.: \"\\{name\\}_COPY\""
+ copyVmNamePatternPlaceholder: 'ex. : "\\{name\\}_COPY"',
+
+ // Original text: "Select SR"
+ copyVmSelectSr: 'Sélectionner le SR',
+
+ // Original text: "Use compression"
+ copyVmCompress: 'Utiliser la compression',
+
+ // Original text: "No target SR"
+ copyVmsNoTargetSr: 'Pas de SR cible',
+
+ // Original text: "A target SR is required to copy a VM"
+ copyVmsNoTargetSrMessage: 'Un SR cible est nécessaire pour copier une VM',
+
+ // Original text: "Detach host"
+ detachHostModalTitle: "Détacher l'hôte",
+
+ // 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:
+ "Êtes-vous sûr de vouloir détacher {host} de son pool? CELA SUPPRIMERA TOUTES LES VMs DE SON STOCKAGE LOCAL ET REDÉMARRERA L'HÔTE.",
+
+ // Original text: "Detach"
+ detachHost: 'Détacher',
+
+ // Original text: "Forget host"
+ forgetHostModalTitle: "Oublier l'hôte",
+
+ // Original text: "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:
+ 'Êtes-vous sûr de vouloir oublier {host} de son pool ? Soyez certain que cet hôte ne peut pas être de retour en ligne ou utilisez "Détacher" à la place.',
+
+ // Original text: "Forget"
+ forgetHost: 'Oublier',
+
+ // Original text: "Create network"
+ newNetworkCreate: 'Créer un réseau',
+
+ // Original text: "Create bonded network"
+ newBondedNetworkCreate: 'Créer un réseau agrégé',
+
+ // Original text: "Interface"
+ newNetworkInterface: 'Interface',
+
+ // Original text: "Name"
+ newNetworkName: 'Nom',
+
+ // Original text: "Description"
+ newNetworkDescription: 'Description',
+
+ // Original text: "VLAN"
+ newNetworkVlan: 'VLAN',
+
+ // Original text: "No VLAN if empty"
+ newNetworkDefaultVlan: 'Si vide, pas de VLAN',
+
+ // Original text: "MTU"
+ newNetworkMtu: 'MTU',
+
+ // Original text: "Default: 1500"
+ newNetworkDefaultMtu: 'Défaut : 1500',
+
+ // Original text: "Name required"
+ newNetworkNoNameErrorTitle: 'Un nom est nécessaire',
+
+ // Original text: "A name is required to create a network"
+ newNetworkNoNameErrorMessage: 'Un nom est nécessaire pour créer un réseau',
+
+ // Original text: "Bond mode"
+ newNetworkBondMode: 'Mode agrégé',
+
+ // Original text: "Delete network"
+ deleteNetwork: 'Supprimer le réseau',
+
+ // Original text: "Are you sure you want to delete this network?"
+ deleteNetworkConfirm: 'Êtes-vous sûr de vouloir supprimer ce réseau ?',
+
+ // Original text: "This network is currently in use"
+ networkInUse: "Ce réseau est en cours d'utilisation",
+
+ // Original text: "Bonded"
+ pillBonded: 'Agrégé',
+
+ // Original text: "Host"
+ addHostSelectHost: 'Hôte',
+
+ // Original text: "No host"
+ addHostNoHost: "Pas d'hôte",
+
+ // Original text: "No host selected to be added"
+ addHostNoHostMessage: 'Aucun hôte sélectionné pour ajout',
+
+ // Original text: "Xen Orchestra"
+ xenOrchestra: 'Xen Orchestra',
+
+ // Original text: "Xen Orchestra server"
+ xenOrchestraServer: 'Serveur Xen Orchestra',
+
+ // Original text: "Xen Orchestra web client"
+ xenOrchestraWeb: 'Client web Xen Orchestra',
+
+ // Original text: "No pro support provided!"
+ noProSupport: 'Pas de support professionel fourni !',
+
+ // Original text: "Use in production at your own risks"
+ noProductionUse: 'Utilisez en production à vos risques et périls',
+
+ // Original text: "You can download our turnkey appliance at {website}"
+ downloadXoaFromWebsite:
+ 'Téléchargez notre édition clef en main sur {website}',
+
+ // Original text: "Bug Tracker"
+ bugTracker: 'Gestionnaire de tickets',
+
+ // Original text: "Issues? Report it!"
+ bugTrackerText: 'Un souci ? Dites-le nous !',
+
+ // Original text: "Community"
+ community: 'Communauté',
+
+ // Original text: "Join our community forum!"
+ communityText: 'Rejoignez notre communauté !',
+
+ // Original text: "Free Trial for Premium Edition!"
+ freeTrial: "Démonstration gratuite de l'Édition Premium !",
+
+ // Original text: "Request your trial now!"
+ freeTrialNow: 'Demandez une démonstration dès maintenant !',
+
+ // Original text: "Any issue?"
+ issues: 'Des soucis ?',
+
+ // Original text: "Problem? Contact us!"
+ issuesText: 'Un problème ? Contactez-nous !',
+
+ // Original text: "Documentation"
+ documentation: 'Documentation',
+
+ // Original text: "Read our official doc"
+ documentationText: 'Consultez notre documentation officielle',
+
+ // Original text: "Pro support included"
+ proSupportIncluded: 'Support professionel inclus',
+
+ // Original text: "Access your XO Account"
+ xoAccount: 'Accéder à votre compte XO',
+
+ // Original text: "Report a problem"
+ openTicket: 'Signaler un problème',
+
+ // Original text: "Problem? Open a ticket!"
+ openTicketText: 'Un problème ? Ouvrez un ticket !',
+
+ // Original text: "Upgrade needed"
+ upgradeNeeded: 'Mise à jour nécessaire',
+
+ // Original text: "Upgrade now!"
+ upgradeNow: 'Mettre à jour maintenant !',
+
+ // Original text: "Or"
+ or: 'Ou',
+
+ // Original text: "Try it for free!"
+ tryIt: 'Essayez gratuitement !',
+
+ // Original text: "This feature is available starting from {plan} Edition"
+ availableIn:
+ "Cette fonctionnalité est disponible à partir de l'édition {plan}",
+
+ // Original text: "This feature is not available in your version, contact your administrator to know more."
+ notAvailable:
+ "Cette fonctionnalité n'est pas disponible dans cette édtition. Pour plus d'informations, contactez votre administrateur.",
+
+ // Original text: "Updates"
+ updateTitle: 'Mise à jour',
+
+ // Original text: "Registration"
+ registration: 'Enregistrement',
+
+ // Original text: "Trial"
+ trial: 'Démonstration',
+
+ // Original text: "Settings"
+ settings: 'Paramètres',
+
+ // Original text: "Proxy settings"
+ proxySettings: 'Configuration du proxy',
+
+ // Original text: "Host (myproxy.example.org)"
+ proxySettingsHostPlaceHolder: 'Hôte (monproxy.exemple.tld)',
+
+ // Original text: "Port (eg: 3128)"
+ proxySettingsPortPlaceHolder: 'Port (ex : 3128)',
+
+ // Original text: "Username"
+ proxySettingsUsernamePlaceHolder: "Nom d'utilisateur",
+
+ // Original text: "Password"
+ proxySettingsPasswordPlaceHolder: 'Mot de passe',
+
+ // Original text: "Your email account"
+ updateRegistrationEmailPlaceHolder: 'Email du compte',
+
+ // Original text: "Your password"
+ updateRegistrationPasswordPlaceHolder: 'Mot de passe',
+
+ // Original text: "Update"
+ update: 'Actualiser',
+
+ // Original text: "Refresh"
+ refresh: 'Rafraîchir',
+
+ // Original text: "Upgrade"
+ upgrade: 'Mettre à jour',
+
+ // Original text: "No updater available for Community Edition"
+ noUpdaterCommunity: 'Pas de mise à jour sur la version Communautaire',
+
+ // Original text: "Please consider subscribe and try it with all features for free during 15 days on {link}."
+ considerSubscribe:
+ 'Envisagez de souscrire, et essayez toutes les fonctionnalités gratuitement pendant 15 jours.',
+
+ // Original text: "Manual update could break your current installation due to dependencies issues, do it with caution"
+ noUpdaterWarning:
+ 'Une mise à jour manuelle pourrait corrompre votre installation actuelle à cause des dépendances, soyez prudent.',
+
+ // Original text: "Current version:"
+ currentVersion: 'Version actuelle :',
+
+ // Original text: "Register"
+ register: "S'enregistrer",
+
+ // Original text: "Edit registration"
+ editRegistration: "Éditer l'enregistrement",
+
+ // Original text: "Please, take time to register in order to enjoy your trial."
+ trialRegistration:
+ 'Merci de prendre le temps de vous enregistrer afin de profiter de votre essai.',
+
+ // Original text: "Start trial"
+ trialStartButton: "Commencer la période d'essai",
+
+ // Original text: "You can use a trial version until {date, date, medium}. Upgrade your appliance to get it."
+ trialAvailableUntil:
+ "Vous pouvez utiliser une version d'essai jusqu'au {date, date, medium}. Mettez à jour votre XOA pour en profiter.",
+
+ // Original text: "Your trial has been ended. Contact us or downgrade to Free version"
+ trialConsumed:
+ "Votre période d'essai est terminé. Contactez-nous, ou régressez sur l'édition gratuite.",
+
+ // Original text: "Your xoa-updater service appears to be down. Your XOA cannot run fully without reaching this service."
+ trialLocked:
+ 'Votre service xoa-update semble inaccessible. Votre XOA ne peut pas fonctionner pleinement si elle ne peut pas joindre ce service.',
+
+ // Original text: "No update information available"
+ noUpdateInfo: "Pas d'informations de mises à jour disponible",
+
+ // Original text: "Update information may be available"
+ waitingUpdateInfo:
+ 'Des informations de mises à jour sont peut-être disponibles',
+
+ // Original text: "Your XOA is up-to-date"
+ upToDate: 'Votre XOA est à jour',
+
+ // Original text: "You need to update your XOA (new version is available)"
+ mustUpgrade:
+ 'Vous devez mettre à jour votre XOA (une nouvelle version est disponible)',
+
+ // Original text: "Your XOA is not registered for updates"
+ registerNeeded: "Votre XOA n'est pas enregistrée pour les mises à jour",
+
+ // Original text: "Can't fetch update information"
+ updaterError: 'Impossible de récupérer les informations de mise à jour.',
+
+ // Original text: "Upgrade successful"
+ promptUpgradeReloadTitle: 'Mise à jour réussie',
+
+ // Original text: "Your XOA has successfully upgraded, and your browser must reload the application. Do you want to reload now ?"
+ promptUpgradeReloadMessage:
+ "Votre XOA à été mise à jour avec brio, et votre navigateur doit rafraîchir l'application pour en profiter. Voulez-vous rafraîchir dès maintenant ?",
+
+ // Original text: "Xen Orchestra from the sources"
+ disclaimerTitle: 'Xen Orchestra depuis les sources',
+
+ // Original text: "You are using XO from the sources! That's great for a personal/non-profit usage."
+ disclaimerText1:
+ "Vous utilisez XO depuis les sources. C'est parfait pour un usage personnel ou non lucratif.",
+
+ // Original text: "If you are a company, it's better to use it with our appliance + pro support included:"
+ disclaimerText2:
+ "Si vous êtes une entrerprise, il est préférable d'utiliser notre applicance qui inclut du support professionel.",
+
+ // Original text: "This version is not bundled with any support nor updates. Use it with caution for critical tasks."
+ disclaimerText3:
+ "Cette version n'est fournie avec aucun support ni aucune mise à jour. Soyez prudent en cas d'utilisation pour des tâches importantes.",
+
+ // Original text: "Connect PIF"
+ connectPif: 'Connecter la PIF',
+
+ // Original text: "Are you sure you want to connect this PIF?"
+ connectPifConfirm: 'Êtes-vous sûr de vouloir connecter cette PIF ?',
+
+ // Original text: "Disconnect PIF"
+ disconnectPif: 'Déconnecter la PIF',
+
+ // Original text: "Are you sure you want to disconnect this PIF?"
+ disconnectPifConfirm: 'Êtes-vous sûr de vouloir déconnecter cette PIF ?',
+
+ // Original text: "Delete PIF"
+ deletePif: 'Supprimer la PIF',
+
+ // Original text: "Are you sure you want to delete this PIF?"
+ deletePifConfirm: 'Êtes-vous sûr de vouloir supprimer cette PIF ?',
+
+ // Original text: "Connected"
+ pifConnected: 'Connecté',
+
+ // Original text: "Disconnected"
+ pifDisconnected: 'Déconnecté',
+
+ // Original text: "Physically connected"
+ pifPhysicallyConnected: 'Connecté physiquement',
+
+ // Original text: "Physically disconnected"
+ pifPhysicallyDisconnected: 'Déconnecté physiquement',
+
+ // Original text: "Username"
+ username: "Nom d'utilisateur",
+
+ // Original text: "Password"
+ password: 'Mot de passe',
+
+ // Original text: "Language"
+ language: 'Langue',
+
+ // Original text: "Old password"
+ oldPasswordPlaceholder: 'Ancien mot de passe',
+
+ // Original text: "New password"
+ newPasswordPlaceholder: 'Nouveau mot de passe',
+
+ // Original text: "Confirm new password"
+ confirmPasswordPlaceholder: 'Confirmer le nouveau mot de passe',
+
+ // Original text: "Confirmation password incorrect"
+ confirmationPasswordError: 'Confirmation du nouveau mot de passe invalide',
+
+ // Original text: "Password does not match the confirm password."
+ confirmationPasswordErrorBody:
+ 'Le mot de passe ne correspond pas à la confirmation du mot de passe.',
+
+ // Original text: "Password changed"
+ pwdChangeSuccess: 'Mot de passe modifié',
+
+ // Original text: "Your password has been successfully changed."
+ pwdChangeSuccessBody: 'Votre mot de passe a été modifié avec succés.',
+
+ // Original text: "Incorrect password"
+ pwdChangeError: 'Mot de passe invalide',
+
+ // Original text: "The old password provided is incorrect. Your password has not been changed."
+ pwdChangeErrorBody:
+ "L'ancien mot de passe n'est pas valide. Votre mot de passe n'a pas été changé.",
+
+ // Original text: "OK"
+ changePasswordOk: 'OK',
+
+ // Original text: "SSH keys"
+ sshKeys: 'Clefs SSH',
+
+ // Original text: "New SSH key"
+ newSshKey: 'Nouvelle clef SSH',
+
+ // Original text: "Delete"
+ deleteSshKey: 'Supprimer',
+
+ // Original text: "No SSH keys"
+ noSshKeys: 'Pas de clef SSH',
+
+ // Original text: "New SSH key"
+ newSshKeyModalTitle: 'Nouvelle clef SSH',
+
+ // Original text: "Invalid key"
+ sshKeyErrorTitle: 'Clef invalide',
+
+ // Original text: "An SSH key requires both a title and a key."
+ sshKeyErrorMessage: 'Une clef SSH nécessite un titre et une clef',
+
+ // Original text: "Title"
+ title: 'Titre',
+
+ // Original text: "Key"
+ key: 'Clef',
+
+ // Original text: "Delete SSH key"
+ deleteSshKeyConfirm: 'Supprimer la clef SSH',
+
+ // Original text: "Are you sure you want to delete the SSH key {title}?"
+ deleteSshKeyConfirmMessage:
+ 'Êtes-vous sûr de vouloir supprimer la clef SSH {title}?',
+
+ // Original text: "Others"
+ others: 'Autres',
+
+ // Original text: "Loading logs…"
+ loadingLogs: 'Chargement des journaux…',
+
+ // Original text: "User"
+ logUser: 'Utilisateur',
+
+ // Original text: "Method"
+ logMethod: 'Méthode',
+
+ // Original text: "Params"
+ logParams: 'Paramètres',
+
+ // Original text: "Message"
+ logMessage: 'Message',
+
+ // Original text: "Error"
+ logError: 'Erreur',
+
+ // Original text: "Display details"
+ logDisplayDetails: 'Afficher les détails',
+
+ // Original text: "Date"
+ logTime: 'Date',
+
+ // Original text: "No stack trace"
+ logNoStackTrace: 'No stack trace',
+
+ // Original text: "No params"
+ logNoParams: 'No params',
+
+ // Original text: "Delete log"
+ logDelete: 'Supprimer le log',
+
+ // Original text: "Delete all logs"
+ logDeleteAll: 'Supprimer tous les journaux',
+
+ // Original text: "Delete all logs"
+ logDeleteAllTitle: 'Supprimer tous les journaux',
+
+ // Original text: "Are you sure you want to delete all the logs?"
+ logDeleteAllMessage: 'Êtes-vous sûr de vouloir supprimer tous les journaux ?',
+
+ // Original text: "Click to enable"
+ logIndicationToEnable: 'Cliquer pour activer',
+
+ // Original text: "Click to disable"
+ logIndicationToDisable: 'Cliquer pour désactiver',
+
+ // Original text: "Report a bug"
+ reportBug: 'Rapporter un bug',
+
+ // Original text: "Name"
+ ipPoolName: 'Nom',
+
+ // Original text: "IPs"
+ ipPoolIps: 'IPs',
+
+ // Original text: "IPs (e.g.: 1.0.0.12-1.0.0.17;1.0.0.23)"
+ ipPoolIpsPlaceholder: 'IPs (e.g.: 1.0.0.12-1.0.0.17;1.0.0.23)',
+
+ // Original text: "Networks"
+ ipPoolNetworks: 'Réseaux',
+
+ // Original text: "No IP pools"
+ ipsNoIpPool: "Pas de plages d'IPs",
+
+ // Original text: "Create"
+ ipsCreate: 'Créer',
+
+ // Original text: "Delete all IP pools"
+ ipsDeleteAllTitle: "Supprimer toutes les plages d'IPs",
+
+ // Original text: "Are you sure you want to delete all the IP pools?"
+ ipsDeleteAllMessage:
+ "Êtes-vous sûr de vouloir supprimer toutes les plages d'IPs ?",
+
+ // Original text: "VIFs"
+ ipsVifs: 'VIFs',
+
+ // Original text: "Not used"
+ ipsNotUsed: 'Non utilisé',
+
+ // Original text: "unknown VIF"
+ ipPoolUnknownVif: 'VIF inconnue',
+
+ // Original text: "Name already exists"
+ ipPoolNameAlreadyExists: 'Ce nom existe déjà',
+
+ // Original text: "Keyboard shortcuts"
+ shortcutModalTitle: 'Raccourcis clavier',
+
+ // Original text: "Global"
+ shortcut_XoApp: 'Global',
+
+ // Original text: "Go to hosts list"
+ shortcut_GO_TO_HOSTS: 'Aller sur la liste des hôtes',
+
+ // Original text: "Go to pools list"
+ shortcut_GO_TO_POOLS: 'Aller sur la liste des pools',
+
+ // Original text: "Go to VMs list"
+ shortcut_GO_TO_VMS: 'Aller sur la liste des VMs',
+
+ // Original text: "Go to SRs list"
+ shortcut_GO_TO_SRS: 'Aller à la liste des SRs',
+
+ // Original text: "Create a new VM"
+ shortcut_CREATE_VM: 'Créer une nouvelle VM',
+
+ // Original text: "Unfocus field"
+ shortcut_UNFOCUS: 'Quitter le champ',
+
+ // Original text: "Show shortcuts key bindings"
+ shortcut_HELP: 'Afficher les raccourcis clavier',
+
+ // Original text: "Home"
+ shortcut_Home: 'Accueil',
+
+ // Original text: "Focus search bar"
+ shortcut_SEARCH: 'Curseur dans la barre de recherche',
+
+ // Original text: "Next item"
+ shortcut_NAV_DOWN: 'Élément suivant',
+
+ // Original text: "Previous item"
+ shortcut_NAV_UP: 'Élément précédent',
+
+ // Original text: "Select item"
+ shortcut_SELECT: "Sélectionner l'élément",
+
+ // Original text: "Open"
+ shortcut_JUMP_INTO: 'Ouvrir',
+
+ // Original text: "VM"
+ settingsAclsButtonTooltipVM: 'VM',
+
+ // Original text: "Hosts"
+ settingsAclsButtonTooltiphost: 'Hôtes',
+
+ // Original text: "Pool"
+ settingsAclsButtonTooltippool: 'Pool',
+
+ // Original text: "SR"
+ settingsAclsButtonTooltipSR: 'SR',
+
+ // Original text: "Network"
+ settingsAclsButtonTooltipnetwork: 'Réseaux',
+
+ // Original text: "No config file selected"
+ noConfigFile: 'Pas de fichier de configuration sélectionné',
+
+ // Original text: "Try dropping a config file here, or click to select a config file to upload."
+ importTip:
+ 'Essayez de déposer un fichier de configuration ou cliquez pour sélectionner un fichier de configuration à importer.',
+
+ // Original text: "Config"
+ config: 'Configuration',
+
+ // Original text: "Import"
+ importConfig: 'Importer',
+
+ // Original text: "Config file successfully imported"
+ importConfigSuccess: 'Fichier de configuration importé avec succès',
+
+ // Original text: "Error while importing config file"
+ importConfigError: "Erreur lors de l'import du fichier de configuration",
+
+ // Original text: "Export"
+ exportConfig: 'Exporter',
+
+ // Original text: "Download current config"
+ downloadConfig: 'Télécharger la configuration actuelle',
+
+ // Original text: "No config import available for Community Edition"
+ noConfigImportCommunity:
+ 'Import de configuration non disponible pour la Community Edition',
+
+ // Original text: "Reconnect all hosts"
+ srReconnectAllModalTitle: 'Reconnecter tous les hôtes',
+
+ // Original text: "This will reconnect this SR to all its hosts."
+ srReconnectAllModalMessage: 'Ceci reconnectera ce SR à tous ses hôtes',
+
+ // Original text: "This will reconnect each selected SR to its host (local SR) or to every hosts of its pool (shared SR)."
+ srsReconnectAllModalMessage:
+ 'Ceci reconnectera tous les SRs sélectionnés à son hôte (SR local) ou à tous les hôtes de son pool (SR partagé).',
+
+ // Original text: "Disconnect all hosts"
+ srDisconnectAllModalTitle: 'Déconnecter tous les hôtes',
+
+ // Original text: "This will disconnect this SR from all its hosts."
+ srDisconnectAllModalMessage: 'Ceci déconnectera ce SR de tous ses hôtes.',
+
+ // Original text: "This will disconnect each selected SR from its host (local SR) or from every hosts of its pool (shared SR)."
+ srsDisconnectAllModalMessage:
+ 'Ceci déconnectera tous les SRs sélectionnés de leur hôte (SR local) ou de tous les hôtes de leur pool (SR partagé).',
+
+ // Original text: "Forget SR"
+ srForgetModalTitle: 'Oublier le SR',
+
+ // Original text: "Forget selected SRs"
+ srsForgetModalTitle: 'Oublier les SRs sélectionnés',
+
+ // Original text: "Are you sure you want to forget this SR? VDIs on this storage won't be removed."
+ srForgetModalMessage:
+ 'Êtes-vous sûr de vouloir oublier ce SR ? Les VDIs de ce stockage ne seront pas supprimés.',
+
+ // Original text: "Are you sure you want to forget all the selected SRs? VDIs on these storages won't be removed."
+ srsForgetModalMessage:
+ 'Êtes-vous sûr de vouloir oublier tous les SRs sélectionnés ? Les VDIs sur ces stockages ne seront pas supprimés.',
+
+ // Original text: "Disconnected"
+ srAllDisconnected: 'Déconnectés',
+
+ // Original text: "Partially connected"
+ srSomeConnected: 'Partiellement connectés',
+
+ // Original text: "Connected"
+ srAllConnected: 'Connectés',
+
+ // Original text: "XOSAN"
+ xosanTitle: 'XOSAN',
+
+ // Original text: "Xen Orchestra SAN SR"
+ xosanSrTitle: 'SR NAS Xen Orchestra',
+
+ // Original text: "Select local SRs (lvm)"
+ xosanAvailableSrsTitle: 'Sélectionner des SRs locaux (lvm)',
+
+ // Original text: "Suggestions"
+ xosanSuggestions: 'Suggestions',
+
+ // Original text: "Name"
+ xosanName: 'Nom',
+
+ // Original text: "Host"
+ xosanHost: 'Hôte',
+
+ // Original text: "Hosts"
+ xosanHosts: 'Hôtes',
+
+ // Original text: "Volume ID"
+ xosanVolumeId: 'ID du volume',
+
+ // Original text: "Size"
+ xosanSize: 'Taille',
+
+ // Original text: "Used space"
+ xosanUsedSpace: 'Espace utilisé',
+
+ // Original text: "XOSAN pack needs to be installed on each host of the pool."
+ xosanNeedPack: 'La pack XOSAN doit être installé sur tous les hôtes du pool.',
+
+ // Original text: "Install it now!"
+ xosanInstallIt: 'Installer maintenant !',
+
+ // Original text: "Some hosts need their toolstack to be restarted before you can create an XOSAN"
+ xosanNeedRestart:
+ 'Certains hôtes ont besoin que leur toolstack soit redémarrée avant de pouvoir créer un XOSAN',
+
+ // Original text: "Restart toolstacks"
+ xosanRestartAgents: 'Redémarrer les toolstacks',
+
+ // Original text: "Pool master is not running"
+ xosanMasterOffline: "Le master du pool n'est pas démarré",
+
+ // Original text: "Install XOSAN pack on {pool}"
+ xosanInstallPackTitle: 'Installer le pack XOSAN sur {pool}',
+
+ // Original text: "Select at least 2 SRs"
+ xosanSelect2Srs: 'Sélectionner au moins 2 SRs',
+
+ // Original text: "Layout"
+ xosanLayout: 'Disposition',
+
+ // Original text: "Redundancy"
+ xosanRedundancy: 'Redondance',
+
+ // Original text: "Capacity"
+ xosanCapacity: 'Capacité',
+
+ // Original text: "Available space"
+ xosanAvailableSpace: 'Espace disponible',
+
+ // Original text: "* Can fail without data loss"
+ xosanDiskLossLegend: '* Peut tomber en panne sans perte de données',
+
+ // Original text: "Create"
+ xosanCreate: 'Créer',
+
+ // Original text: "Installing XOSAN. Please wait…"
+ xosanInstalling: 'Installation de XOSAN. Veuillez patienter…',
+
+ // Original text: "No XOSAN available for Community Edition"
+ xosanCommunity: 'XOSAN non disponible pour la Community Edition',
+
+ // Original text: "Install cloud plugin first"
+ xosanInstallCloudPlugin: 'Installer le plugin cloud avant',
+
+ // Original text: "Load cloud plugin first"
+ xosanLoadCloudPlugin: 'Charger le plugin cloud avant',
+
+ // Original text: "Loading…"
+ xosanLoading: 'Chargement…',
+
+ // Original text: "XOSAN is not available at the moment"
+ xosanNotAvailable: "XOSAN n'est pas disponible pour le moment",
+
+ // Original text: "Register for the XOSAN beta"
+ xosanRegisterBeta: 'Inscrivez-vous pour la beta de XOSAN',
+
+ // Original text: "You have successfully registered for the XOSAN beta. Please wait until your request has been approved."
+ xosanSuccessfullyRegistered:
+ 'Vous êtes inscrit pour la beta de XOSAN. Veuillez attendre que votre demande soit approuvée.',
+
+ // Original text: "Install XOSAN pack on these hosts:"
+ xosanInstallPackOnHosts: 'Installer le pack XOSAN sur ces hôtes :',
+
+ // Original text: "Install {pack} v{version}?"
+ xosanInstallPack: 'Installer {pack} v{version} ?',
+
+ // Original text: "No compatible XOSAN pack found for your XenServer versions."
+ xosanNoPackFound:
+ 'Pas de pack XOSAN compatible pour vos versions de XenServers.',
+
+ // Original text: "At least one of these version requirements must be satisfied by all the hosts in this pool:"
+ xosanPackRequirements:
+ 'Au moins une de ces condtions de version doit être satisfaite par tous les hôtes de ce pool :',
+}
diff --git a/packages/xo-web/src/common/intl/locales/he.js b/packages/xo-web/src/common/intl/locales/he.js
new file mode 100644
index 000000000..cb02cfaed
--- /dev/null
+++ b/packages/xo-web/src/common/intl/locales/he.js
@@ -0,0 +1,3136 @@
+// See http://momentjs.com/docs/#/use-it/browserify/
+import 'moment/locale/he'
+
+import reactIntlData from 'react-intl/locale-data/he'
+import { addLocaleData } from 'react-intl'
+addLocaleData(reactIntlData)
+
+// ===================================================================
+
+export default {
+ // Original text: 'Connecting'
+ statusConnecting: undefined,
+
+ // Original text: 'Disconnected'
+ statusDisconnected: undefined,
+
+ // Original text: 'Loading…'
+ statusLoading: undefined,
+
+ // Original text: 'Page not found'
+ errorPageNotFound: undefined,
+
+ // Original text: 'no such item'
+ errorNoSuchItem: undefined,
+
+ // Original text: "Long click to edit"
+ editableLongClickPlaceholder: 'לחץ כאן לחיצה ערוכה לעריכה',
+
+ // Original text: "Click to edit"
+ editableClickPlaceholder: 'לחץ לעריכה',
+
+ // Original text: 'OK'
+ alertOk: undefined,
+
+ // Original text: 'OK'
+ confirmOk: undefined,
+
+ // Original text: 'Cancel'
+ confirmCancel: undefined,
+
+ // Original text: 'On error'
+ onError: undefined,
+
+ // Original text: 'Successful'
+ successful: undefined,
+
+ // Original text: 'Copy to clipboard'
+ copyToClipboard: undefined,
+
+ // Original text: 'Master'
+ pillMaster: undefined,
+
+ // Original text: "Home"
+ homePage: 'בית',
+
+ // Original text: 'VMs'
+ homeVmPage: undefined,
+
+ // Original text: 'Hosts'
+ homeHostPage: undefined,
+
+ // Original text: 'Pools'
+ homePoolPage: undefined,
+
+ // Original text: 'Templates'
+ homeTemplatePage: undefined,
+
+ // Original text: "Dashboard"
+ dashboardPage: 'לוח מכוונים',
+
+ // Original text: "Overview"
+ overviewDashboardPage: 'איזור אישי',
+
+ // Original text: "Visualizations"
+ overviewVisualizationDashboardPage: 'וירטואליזציה',
+
+ // Original text: "Statistics"
+ overviewStatsDashboardPage: 'סטטיסטיקה',
+
+ // Original text: "Health"
+ overviewHealthDashboardPage: 'בדיקת ביצועים',
+
+ // Original text: "Self service"
+ selfServicePage: 'שירות עצמי',
+
+ // Original text: "Backup"
+ backupPage: 'גיבוי',
+
+ // Original text: "Jobs"
+ jobsPage: 'משימות',
+
+ // Original text: "Updates"
+ updatePage: 'עדכונים',
+
+ // Original text: "Settings"
+ settingsPage: 'הגדרות',
+
+ // Original text: "Servers"
+ settingsServersPage: 'שרתים',
+
+ // Original text: "Users"
+ settingsUsersPage: 'משתמשים',
+
+ // Original text: "Groups"
+ settingsGroupsPage: 'קבוצות',
+
+ // Original text: "ACLs"
+ settingsAclsPage: 'רמות גישה',
+
+ // Original text: "Plugins"
+ settingsPluginsPage: 'פלאגינים',
+
+ // Original text: 'Logs'
+ settingsLogsPage: undefined,
+
+ // Original text: 'IPs'
+ settingsIpsPage: undefined,
+
+ // Original text: "About"
+ aboutPage: 'אודות',
+
+ // Original text: "New"
+ newMenu: 'חדש',
+
+ // Original text: "Tasks"
+ taskMenu: 'משימות',
+
+ // Original text: 'Tasks'
+ taskPage: undefined,
+
+ // Original text: "VM"
+ newVmPage: 'מכונה',
+
+ // Original text: "Storage"
+ newSrPage: 'אחסון',
+
+ // Original text: "Server"
+ newServerPage: 'שרת',
+
+ // Original text: "Import"
+ newImport: 'ההלעה',
+
+ // Original text: "Overview"
+ backupOverviewPage: 'הרחבה',
+
+ // Original text: "New"
+ backupNewPage: 'חדש',
+
+ // Original text: "Remotes"
+ backupRemotesPage: 'גישה מרחוק',
+
+ // Original text: "Restore"
+ backupRestorePage: 'שחזור',
+
+ // Original text: "Schedule"
+ schedule: 'תזמון',
+
+ // Original text: "New VM backup"
+ newVmBackup: 'גיבוי חדש למכונה',
+
+ // Original text: "Edit VM backup"
+ editVmBackup: 'ערוך הגדרות גיבוי למכונה',
+
+ // Original text: "Backup"
+ backup: 'גיבוי',
+
+ // Original text: "Rolling Snapshot"
+ rollingSnapshot: 'גיבוי סנאפשוט',
+
+ // Original text: "Delta Backup"
+ deltaBackup: 'גיבוי חלקי',
+
+ // Original text: "Disaster Recovery"
+ disasterRecovery: 'חזרה מDR',
+
+ // Original text: "Continuous Replication"
+ continuousReplication: 'רפליקציה מתממשכת',
+
+ // Original text: "Overview"
+ jobsOverviewPage: 'רשימת משימות',
+
+ // Original text: "New"
+ jobsNewPage: 'משימה חדשה',
+
+ // Original text: "Scheduling"
+ jobsSchedulingPage: 'תזמון משימות',
+
+ // Original text: "Custom Job"
+ customJob: 'משימה קסטומית',
+
+ // Original text: 'User'
+ userPage: undefined,
+
+ // Original text: 'No support'
+ noSupport: undefined,
+
+ // Original text: 'Free upgrade!'
+ freeUpgrade: undefined,
+
+ // Original text: "Sign out"
+ signOut: 'יציאה',
+
+ // Original text: 'Edit my settings {username}'
+ editUserProfile: undefined,
+
+ // Original text: "Fetching data…"
+ homeFetchingData: 'מקבל נתונים, נא להמתין…',
+
+ // Original text: "Welcome on Xen Orchestra!"
+ homeWelcome: 'ברוכים הבאים',
+
+ // Original text: "Add your XenServer hosts or pools"
+ homeWelcomeText: 'נא להוסיף שרתים או משאבים',
+
+ // Original text: "Want some help?"
+ homeHelp: 'צריך עזרה?',
+
+ // Original text: "Add server"
+ homeAddServer: 'הוספת שרת',
+
+ // Original text: "Online Doc"
+ homeOnlineDoc: 'דוקומנטציה',
+
+ // Original text: "Pro Support"
+ homeProSupport: 'תמיכה מקצועית',
+
+ // Original text: "There are no VMs!"
+ homeNoVms: 'אין מכונות',
+
+ // Original text: "Or…"
+ homeNoVmsOr: 'או…',
+
+ // Original text: "Import VM"
+ homeImportVm: 'ההלעה של מכונה',
+
+ // Original text: "Import an existing VM in xva format"
+ homeImportVmMessage: 'להלעות מכונה חדשה בפורמת XVA',
+
+ // Original text: "Restore a backup"
+ homeRestoreBackup: 'שחזור מגיבוי',
+
+ // Original text: "Restore a backup from a remote store"
+ homeRestoreBackupMessage: 'שחזור מגיבוי ממכונה אחרת',
+
+ // Original text: "This will create a new VM"
+ homeNewVmMessage: 'זה יצור מכונה חדשה',
+
+ // Original text: "Filters"
+ homeFilters: 'מסננים',
+
+ // Original text: 'No results! Click here to reset your filters'
+ homeNoMatches: undefined,
+
+ // Original text: "Pool"
+ homeTypePool: 'משאבים',
+
+ // Original text: "Host"
+ homeTypeHost: 'מכונה',
+
+ // Original text: "VM"
+ homeTypeVm: 'שרת',
+
+ // Original text: "SR"
+ homeTypeSr: 'SR',
+
+ // Original text: 'Template'
+ homeTypeVmTemplate: undefined,
+
+ // Original text: "Sort"
+ homeSort: 'סינון',
+
+ // Original text: "Pools"
+ homeAllPools: 'POOLS',
+
+ // Original text: "Hosts"
+ homeAllHosts: 'מכונות',
+
+ // Original text: "Tags"
+ homeAllTags: 'מילות מפתח',
+
+ // Original text: "New VM"
+ homeNewVm: 'מכונה חדשה',
+
+ // Original text: "Running hosts"
+ homeFilterRunningHosts: 'מערכות פעילות',
+
+ // Original text: "Disabled hosts"
+ homeFilterDisabledHosts: 'מערכות לא פעילות',
+
+ // Original text: "Running VMs"
+ homeFilterRunningVms: 'מכונות פעילות',
+
+ // Original text: 'Non running VMs'
+ homeFilterNonRunningVms: undefined,
+
+ // Original text: 'Pending VMs'
+ homeFilterPendingVms: undefined,
+
+ // Original text: 'HVM guests'
+ homeFilterHvmGuests: undefined,
+
+ // Original text: "Tags"
+ homeFilterTags: 'מילות מפתח',
+
+ // Original text: "Sort by"
+ homeSortBy: 'סנן לפי',
+
+ // Original text: "Name"
+ homeSortByName: 'שם',
+
+ // Original text: "Power state"
+ homeSortByPowerstate: 'מצב',
+
+ // Original text: "RAM"
+ homeSortByRAM: 'זכרון RAM',
+
+ // Original text: "vCPUs"
+ homeSortByvCPUs: 'כמות המאבדים',
+
+ // Original text: 'CPUs'
+ homeSortByCpus: undefined,
+
+ // Original text: '{displayed, number}x {icon} (on {total, number})'
+ homeDisplayedItems: undefined,
+
+ // Original text: '{selected, number}x {icon} selected (on {total, number})'
+ homeSelectedItems: undefined,
+
+ // Original text: "More"
+ homeMore: 'עוד',
+
+ // Original text: "Migrate to…"
+ homeMigrateTo: 'העבר ל…',
+
+ // Original text: 'Missing patches'
+ homeMissingPaths: undefined,
+
+ // Original text: 'Master:'
+ homePoolMaster: undefined,
+
+ // Original text: 'High Availability'
+ highAvailability: undefined,
+
+ // Original text: "Add"
+ add: 'הוסף',
+
+ // Original text: "Remove"
+ remove: 'הסר',
+
+ // Original text: 'Preview'
+ preview: undefined,
+
+ // Original text: "Item"
+ item: 'פריט',
+
+ // Original text: "No selected value"
+ noSelectedValue: 'לא נבחר כלום',
+
+ // Original text: "Choose user(s) and/or group(s)"
+ selectSubjects: 'בחר משתמש או קבוצה להוספה',
+
+ // Original text: 'Select Object(s)…'
+ selectObjects: undefined,
+
+ // Original text: 'Choose a role'
+ selectRole: undefined,
+
+ // Original text: 'Select Host(s)…'
+ selectHosts: undefined,
+
+ // Original text: 'Select object(s)…'
+ selectHostsVms: undefined,
+
+ // Original text: 'Select Network(s)…'
+ selectNetworks: undefined,
+
+ // Original text: 'Select PIF(s)…'
+ selectPifs: undefined,
+
+ // Original text: 'Select Pool(s)…'
+ selectPools: undefined,
+
+ // Original text: 'Select Remote(s)…'
+ selectRemotes: undefined,
+
+ // Original text: 'Select resource set(s)…'
+ selectResourceSets: undefined,
+
+ // Original text: 'Select template(s)…'
+ selectResourceSetsVmTemplate: undefined,
+
+ // Original text: 'Select SR(s)…'
+ selectResourceSetsSr: undefined,
+
+ // Original text: 'Select network(s)…'
+ selectResourceSetsNetwork: undefined,
+
+ // Original text: 'Select disk(s)…'
+ selectResourceSetsVdi: undefined,
+
+ // Original text: 'Select SSH key(s)…'
+ selectSshKey: undefined,
+
+ // Original text: 'Select SR(s)…'
+ selectSrs: undefined,
+
+ // Original text: 'Select VM(s)…'
+ selectVms: undefined,
+
+ // Original text: 'Select VM template(s)…'
+ selectVmTemplates: undefined,
+
+ // Original text: 'Select tag(s)…'
+ selectTags: undefined,
+
+ // Original text: 'Select disk(s)…'
+ selectVdis: undefined,
+
+ // Original text: 'Select timezone…'
+ selectTimezone: undefined,
+
+ // Original text: 'Select IP(s)…'
+ selectIp: undefined,
+
+ // Original text: 'Select IP pool(s)…'
+ selectIpPool: undefined,
+
+ // Original text: 'Fill required informations.'
+ fillRequiredInformations: undefined,
+
+ // Original text: 'Fill informations (optional)'
+ fillOptionalInformations: undefined,
+
+ // Original text: 'Reset'
+ selectTableReset: undefined,
+
+ // Original text: 'Month'
+ schedulingMonth: undefined,
+
+ // Original text: 'Each selected month'
+ schedulingEachSelectedMonth: undefined,
+
+ // Original text: 'Day of the month'
+ schedulingMonthDay: undefined,
+
+ // Original text: 'Each selected day'
+ schedulingEachSelectedMonthDay: undefined,
+
+ // Original text: 'Day of the week'
+ schedulingWeekDay: undefined,
+
+ // Original text: 'Each selected day'
+ schedulingEachSelectedWeekDay: undefined,
+
+ // Original text: 'Hour'
+ schedulingHour: undefined,
+
+ // Original text: 'Every N hour'
+ schedulingEveryNHour: undefined,
+
+ // Original text: 'Each selected hour'
+ schedulingEachSelectedHour: undefined,
+
+ // Original text: 'Minute'
+ schedulingMinute: undefined,
+
+ // Original text: 'Every N minute'
+ schedulingEveryNMinute: undefined,
+
+ // Original text: 'Each selected minute'
+ schedulingEachSelectedMinute: undefined,
+
+ // Original text: 'Reset'
+ schedulingReset: undefined,
+
+ // Original text: 'Unknown'
+ unknownSchedule: undefined,
+
+ // Original text: 'Xo-server timezone:'
+ timezonePickerServerValue: undefined,
+
+ // Original text: 'Web browser timezone'
+ timezonePickerUseLocalTime: undefined,
+
+ // Original text: 'Xo-server timezone'
+ timezonePickerUseServerTime: undefined,
+
+ // Original text: 'Server timezone ({value})'
+ serverTimezoneOption: undefined,
+
+ // Original text: 'Cron Pattern:'
+ cronPattern: undefined,
+
+ // Original text: 'Cannot edit backup'
+ backupEditNotFoundTitle: undefined,
+
+ // Original text: 'Missing required info for edition'
+ backupEditNotFoundMessage: undefined,
+
+ // Original text: 'Job'
+ job: undefined,
+
+ // Original text: 'Job ID'
+ jobId: undefined,
+
+ // Original text: 'Name'
+ jobName: undefined,
+
+ // Original text: 'Name of your job (forbidden: "_")'
+ jobNamePlaceholder: undefined,
+
+ // Original text: 'Start'
+ jobStart: undefined,
+
+ // Original text: 'End'
+ jobEnd: undefined,
+
+ // Original text: 'Duration'
+ jobDuration: undefined,
+
+ // Original text: 'Status'
+ jobStatus: undefined,
+
+ // Original text: 'Action'
+ jobAction: undefined,
+
+ // Original text: 'Tag'
+ jobTag: undefined,
+
+ // Original text: 'Scheduling'
+ jobScheduling: undefined,
+
+ // Original text: 'State'
+ jobState: undefined,
+
+ // Original text: 'Timezone'
+ jobTimezone: undefined,
+
+ // Original text: 'xo-server'
+ jobServerTimezone: undefined,
+
+ // Original text: 'Run job'
+ runJob: undefined,
+
+ // Original text: 'One shot running started. See overview for logs.'
+ runJobVerbose: undefined,
+
+ // Original text: 'Started'
+ jobStarted: undefined,
+
+ // Original text: 'Finished'
+ jobFinished: undefined,
+
+ // Original text: 'Save'
+ saveBackupJob: undefined,
+
+ // Original text: 'Remove backup job'
+ deleteBackupSchedule: undefined,
+
+ // Original text: 'Are you sure you want to delete this backup job?'
+ deleteBackupScheduleQuestion: undefined,
+
+ // Original text: 'Enable immediately after creation'
+ scheduleEnableAfterCreation: undefined,
+
+ // Original text: 'You are editing Schedule {name} ({id}). Saving will override previous schedule state.'
+ scheduleEditMessage: undefined,
+
+ // Original text: 'You are editing job {name} ({id}). Saving will override previous job state.'
+ jobEditMessage: undefined,
+
+ // Original text: 'No scheduled jobs.'
+ noScheduledJobs: undefined,
+
+ // Original text: 'No jobs found.'
+ noJobs: undefined,
+
+ // Original text: 'No schedules found'
+ noSchedules: undefined,
+
+ // Original text: 'Select a xo-server API command'
+ jobActionPlaceHolder: undefined,
+
+ // Original text: 'Schedules'
+ jobSchedules: undefined,
+
+ // Original text: 'Name of your schedule'
+ jobScheduleNamePlaceHolder: undefined,
+
+ // Original text: 'Select a Job'
+ jobScheduleJobPlaceHolder: undefined,
+
+ // Original text: 'Select your backup type:'
+ newBackupSelection: undefined,
+
+ // Original text: 'Select backup mode:'
+ smartBackupModeSelection: undefined,
+
+ // Original text: 'Normal backup'
+ normalBackup: undefined,
+
+ // Original text: 'Smart backup'
+ smartBackup: undefined,
+
+ // Original text: 'Local remote selected'
+ localRemoteWarningTitle: undefined,
+
+ // Original text: 'Warning: local remotes will use limited XOA disk space. Only for advanced users.'
+ localRemoteWarningMessage: undefined,
+
+ // Original text: 'VMs'
+ editBackupVmsTitle: undefined,
+
+ // Original text: 'VMs statuses'
+ editBackupSmartStatusTitle: undefined,
+
+ // Original text: 'Resident on'
+ editBackupSmartResidentOn: undefined,
+
+ // Original text: 'VMs Tags'
+ editBackupSmartTagsTitle: undefined,
+
+ // Original text: 'Tag'
+ editBackupTagTitle: undefined,
+
+ // Original text: 'Report'
+ editBackupReportTitle: undefined,
+
+ // Original text: 'Enable immediately after creation'
+ editBackupScheduleEnabled: undefined,
+
+ // Original text: 'Depth'
+ editBackupDepthTitle: undefined,
+
+ // Original text: 'Remote'
+ editBackupRemoteTitle: undefined,
+
+ // Original text: 'Remote stores for backup'
+ remoteList: undefined,
+
+ // Original text: 'New File System Remote'
+ newRemote: undefined,
+
+ // Original text: 'Local'
+ remoteTypeLocal: undefined,
+
+ // Original text: 'NFS'
+ remoteTypeNfs: undefined,
+
+ // Original text: 'SMB'
+ remoteTypeSmb: undefined,
+
+ // Original text: 'Type'
+ remoteType: undefined,
+
+ // Original text: 'Test your remote'
+ remoteTestTip: undefined,
+
+ // Original text: 'Test Remote'
+ testRemote: undefined,
+
+ // Original text: 'Test failed for {name}'
+ remoteTestFailure: undefined,
+
+ // Original text: 'Test passed for {name}'
+ remoteTestSuccess: undefined,
+
+ // Original text: 'Error'
+ remoteTestError: undefined,
+
+ // Original text: 'Test Step'
+ remoteTestStep: undefined,
+
+ // Original text: 'Test file'
+ remoteTestFile: undefined,
+
+ // Original text: 'The remote appears to work correctly'
+ remoteTestSuccessMessage: undefined,
+
+ // Original text: 'Name'
+ remoteName: undefined,
+
+ // Original text: 'Path'
+ remotePath: undefined,
+
+ // Original text: 'State'
+ remoteState: undefined,
+
+ // Original text: 'Device'
+ remoteDevice: undefined,
+
+ // Original text: 'Share'
+ remoteShare: undefined,
+
+ // Original text: 'Auth'
+ remoteAuth: undefined,
+
+ // Original text: 'Mounted'
+ remoteMounted: undefined,
+
+ // Original text: 'Unmounted'
+ remoteUnmounted: undefined,
+
+ // Original text: 'Connect'
+ remoteConnectTip: undefined,
+
+ // Original text: 'Disconnect'
+ remoteDisconnectTip: undefined,
+
+ // Original text: 'Delete'
+ remoteDeleteTip: undefined,
+
+ // Original text: 'remote name *'
+ remoteNamePlaceHolder: undefined,
+
+ // Original text: 'Name *'
+ remoteMyNamePlaceHolder: undefined,
+
+ // Original text: '/path/to/backup'
+ remoteLocalPlaceHolderPath: undefined,
+
+ // Original text: 'host *'
+ remoteNfsPlaceHolderHost: undefined,
+
+ // Original text: '/path/to/backup'
+ remoteNfsPlaceHolderPath: undefined,
+
+ // Original text: 'subfolder [path\\to\\backup]'
+ remoteSmbPlaceHolderRemotePath: undefined,
+
+ // Original text: 'Username'
+ remoteSmbPlaceHolderUsername: undefined,
+
+ // Original text: 'Password'
+ remoteSmbPlaceHolderPassword: undefined,
+
+ // Original text: 'Domain'
+ remoteSmbPlaceHolderDomain: undefined,
+
+ // Original text: '\\ *'
+ remoteSmbPlaceHolderAddressShare: undefined,
+
+ // Original text: 'password(fill to edit)'
+ remotePlaceHolderPassword: undefined,
+
+ // Original text: 'Create a new SR'
+ newSrTitle: undefined,
+
+ // Original text: 'General'
+ newSrGeneral: undefined,
+
+ // Original text: 'Select Storage Type:'
+ newSrTypeSelection: undefined,
+
+ // Original text: 'Settings'
+ newSrSettings: undefined,
+
+ // Original text: 'Storage Usage'
+ newSrUsage: undefined,
+
+ // Original text: 'Summary'
+ newSrSummary: undefined,
+
+ // Original text: 'Host'
+ newSrHost: undefined,
+
+ // Original text: 'Type'
+ newSrType: undefined,
+
+ // Original text: 'Name'
+ newSrName: undefined,
+
+ // Original text: 'Description'
+ newSrDescription: undefined,
+
+ // Original text: 'Server'
+ newSrServer: undefined,
+
+ // Original text: 'Path'
+ newSrPath: undefined,
+
+ // Original text: 'IQN'
+ newSrIqn: undefined,
+
+ // Original text: 'LUN'
+ newSrLun: undefined,
+
+ // Original text: 'with auth.'
+ newSrAuth: undefined,
+
+ // Original text: 'User Name'
+ newSrUsername: undefined,
+
+ // Original text: 'Password'
+ newSrPassword: undefined,
+
+ // Original text: 'Device'
+ newSrDevice: undefined,
+
+ // Original text: 'in use'
+ newSrInUse: undefined,
+
+ // Original text: 'Size'
+ newSrSize: undefined,
+
+ // Original text: 'Create'
+ newSrCreate: undefined,
+
+ // Original text: 'Storage name'
+ newSrNamePlaceHolder: undefined,
+
+ // Original text: 'Storage description'
+ newSrDescPlaceHolder: undefined,
+
+ // Original text: 'Address'
+ newSrAddressPlaceHolder: undefined,
+
+ // Original text: '[port]'
+ newSrPortPlaceHolder: undefined,
+
+ // Original text: 'Username'
+ newSrUsernamePlaceHolder: undefined,
+
+ // Original text: 'Password'
+ newSrPasswordPlaceHolder: undefined,
+
+ // Original text: 'Device, e.g /dev/sda…'
+ newSrLvmDevicePlaceHolder: undefined,
+
+ // Original text: '/path/to/directory'
+ newSrLocalPathPlaceHolder: undefined,
+
+ // Original text: 'Users/Groups'
+ subjectName: undefined,
+
+ // Original text: 'Object'
+ objectName: undefined,
+
+ // Original text: 'No acls found'
+ aclNoneFound: undefined,
+
+ // Original text: 'Role'
+ roleName: undefined,
+
+ // Original text: 'Create'
+ aclCreate: undefined,
+
+ // Original text: 'New Group Name'
+ newGroupName: undefined,
+
+ // Original text: 'Create Group'
+ createGroup: undefined,
+
+ // Original text: 'Create'
+ createGroupButton: undefined,
+
+ // Original text: 'Delete Group'
+ deleteGroup: undefined,
+
+ // Original text: 'Are you sure you want to delete this group?'
+ deleteGroupConfirm: undefined,
+
+ // Original text: 'Remove user from Group'
+ removeUserFromGroup: undefined,
+
+ // Original text: 'Are you sure you want to delete this user?'
+ deleteUserConfirm: undefined,
+
+ // Original text: 'Delete User'
+ deleteUser: undefined,
+
+ // Original text: 'no user'
+ noUser: undefined,
+
+ // Original text: 'unknown user'
+ unknownUser: undefined,
+
+ // Original text: 'No group found'
+ noGroupFound: undefined,
+
+ // Original text: "Name"
+ groupNameColumn: 'שם',
+
+ // Original text: "Users"
+ groupUsersColumn: 'משתמשים',
+
+ // Original text: "Add User"
+ addUserToGroupColumn: 'הוסף משתמש',
+
+ // Original text: "Email"
+ userNameColumn: 'מייל',
+
+ // Original text: "Permissions"
+ userPermissionColumn: 'הרשאות',
+
+ // Original text: "Password"
+ userPasswordColumn: 'סיסמא',
+
+ // Original text: "Email"
+ userName: 'שם משתמש',
+
+ // Original text: 'Password'
+ userPassword: undefined,
+
+ // Original text: 'Create'
+ createUserButton: undefined,
+
+ // Original text: 'No user found'
+ noUserFound: undefined,
+
+ // Original text: 'User'
+ userLabel: undefined,
+
+ // Original text: 'Admin'
+ adminLabel: undefined,
+
+ // Original text: 'No user in group'
+ noUserInGroup: undefined,
+
+ // Original text: '{users} user{users, plural, one {} other {s}}'
+ countUsers: undefined,
+
+ // Original text: 'Select Permission'
+ selectPermission: undefined,
+
+ // Original text: 'Auto-load at server start'
+ autoloadPlugin: undefined,
+
+ // Original text: 'Save configuration'
+ savePluginConfiguration: undefined,
+
+ // Original text: 'Delete configuration'
+ deletePluginConfiguration: undefined,
+
+ // Original text: 'Plugin error'
+ pluginError: undefined,
+
+ // Original text: 'Unknown error'
+ unknownPluginError: undefined,
+
+ // Original text: 'Purge plugin configuration'
+ purgePluginConfiguration: undefined,
+
+ // Original text: 'Are you sure you want to purge this configuration ?'
+ purgePluginConfigurationQuestion: undefined,
+
+ // Original text: 'Edit'
+ editPluginConfiguration: undefined,
+
+ // Original text: 'Cancel'
+ cancelPluginEdition: undefined,
+
+ // Original text: 'Plugin configuration'
+ pluginConfigurationSuccess: undefined,
+
+ // Original text: 'Plugin configuration successfully saved!'
+ pluginConfigurationChanges: undefined,
+
+ // Original text: 'Predefined configuration'
+ pluginConfigurationPresetTitle: undefined,
+
+ // Original text: 'Choose a predefined configuration.'
+ pluginConfigurationChoosePreset: undefined,
+
+ // Original text: 'Apply'
+ applyPluginPreset: undefined,
+
+ // Original text: 'Save filter error'
+ saveNewUserFilterErrorTitle: undefined,
+
+ // Original text: 'Bad parameter: name must be given.'
+ saveNewUserFilterErrorBody: undefined,
+
+ // Original text: 'Name:'
+ filterName: undefined,
+
+ // Original text: 'Value:'
+ filterValue: undefined,
+
+ // Original text: 'Save new filter'
+ saveNewFilterTitle: undefined,
+
+ // Original text: 'Set custom filters'
+ setUserFiltersTitle: undefined,
+
+ // Original text: 'Are you sure you want to set custom filters?'
+ setUserFiltersBody: undefined,
+
+ // Original text: 'Remove custom filter'
+ removeUserFilterTitle: undefined,
+
+ // Original text: 'Are you sure you want to remove custom filter?'
+ removeUserFilterBody: undefined,
+
+ // Original text: 'Default filter'
+ defaultFilter: undefined,
+
+ // Original text: 'Default filters'
+ defaultFilters: undefined,
+
+ // Original text: 'Custom filters'
+ customFilters: undefined,
+
+ // Original text: 'Customize filters'
+ customizeFilters: undefined,
+
+ // Original text: 'Save custom filters'
+ saveCustomFilters: undefined,
+
+ // Original text: "Start"
+ startVmLabel: 'הפעל מכונה',
+
+ // Original text: 'Recovery start'
+ recoveryModeLabel: undefined,
+
+ // Original text: "Suspend"
+ suspendVmLabel: 'הקפא מכונה',
+
+ // Original text: "Stop"
+ stopVmLabel: 'עצור מכונה',
+
+ // Original text: "Force shutdown"
+ forceShutdownVmLabel: 'עצירה בכוח',
+
+ // Original text: "Reboot"
+ rebootVmLabel: 'הפעלה מחדש',
+
+ // Original text: "Force reboot"
+ forceRebootVmLabel: 'הפעלה מחדש בכוח',
+
+ // Original text: "Delete"
+ deleteVmLabel: 'מחיקה',
+
+ // Original text: 'Migrate'
+ migrateVmLabel: undefined,
+
+ // Original text: 'Snapshot'
+ snapshotVmLabel: undefined,
+
+ // Original text: 'Export'
+ exportVmLabel: undefined,
+
+ // Original text: 'Resume'
+ resumeVmLabel: undefined,
+
+ // Original text: 'Copy'
+ copyVmLabel: undefined,
+
+ // Original text: 'Clone'
+ cloneVmLabel: undefined,
+
+ // Original text: 'Fast clone'
+ fastCloneVmLabel: undefined,
+
+ // Original text: 'Convert to template'
+ convertVmToTemplateLabel: undefined,
+
+ // Original text: "Console"
+ vmConsoleLabel: 'קונסול',
+
+ // Original text: 'Rescan all disks'
+ srRescan: undefined,
+
+ // Original text: 'Connect to all hosts'
+ srReconnectAll: undefined,
+
+ // Original text: 'Disconnect to all hosts'
+ srDisconnectAll: undefined,
+
+ // Original text: 'Forget this SR'
+ srForget: undefined,
+
+ // Original text: 'Remove this SR'
+ srRemoveButton: undefined,
+
+ // Original text: 'No VDIs in this storage'
+ srNoVdis: undefined,
+
+ // Original text: 'Pool RAM usage:'
+ poolTitleRamUsage: undefined,
+
+ // Original text: '{used} used on {total}'
+ poolRamUsage: undefined,
+
+ // Original text: 'Master:'
+ poolMaster: undefined,
+
+ // Original text: 'Hosts'
+ hostsTabName: undefined,
+
+ // Original text: 'High Availability'
+ poolHaStatus: undefined,
+
+ // Original text: 'Enabled'
+ poolHaEnabled: undefined,
+
+ // Original text: 'Disabled'
+ poolHaDisabled: undefined,
+
+ // Original text: 'Name'
+ hostNameLabel: undefined,
+
+ // Original text: 'Description'
+ hostDescription: undefined,
+
+ // Original text: 'Memory'
+ hostMemory: undefined,
+
+ // Original text: 'No hosts'
+ noHost: undefined,
+
+ // Original text: '{used}% used ({free} free)'
+ memoryLeftTooltip: undefined,
+
+ // Original text: 'Name'
+ poolNetworkNameLabel: undefined,
+
+ // Original text: 'Description'
+ poolNetworkDescription: undefined,
+
+ // Original text: 'PIFs'
+ poolNetworkPif: undefined,
+
+ // Original text: 'No networks'
+ poolNoNetwork: undefined,
+
+ // Original text: 'MTU'
+ poolNetworkMTU: undefined,
+
+ // Original text: 'Connected'
+ poolNetworkPifAttached: undefined,
+
+ // Original text: 'Disconnected'
+ poolNetworkPifDetached: undefined,
+
+ // Original text: 'Show PIFs'
+ showPifs: undefined,
+
+ // Original text: 'Hide PIFs'
+ hidePifs: undefined,
+
+ // Original text: 'Add SR'
+ addSrLabel: undefined,
+
+ // Original text: 'Add VM'
+ addVmLabel: undefined,
+
+ // Original text: 'Add Host'
+ addHostLabel: undefined,
+
+ // Original text: 'Disconnect'
+ disconnectServer: undefined,
+
+ // Original text: 'Start'
+ startHostLabel: undefined,
+
+ // Original text: 'Stop'
+ stopHostLabel: undefined,
+
+ // Original text: 'Enable'
+ enableHostLabel: undefined,
+
+ // Original text: 'Disable'
+ disableHostLabel: undefined,
+
+ // Original text: 'Restart toolstack'
+ restartHostAgent: undefined,
+
+ // Original text: 'Force reboot'
+ forceRebootHostLabel: undefined,
+
+ // Original text: 'Reboot'
+ rebootHostLabel: undefined,
+
+ // Original text: 'Reboot to apply updates'
+ rebootUpdateHostLabel: undefined,
+
+ // Original text: 'Emergency mode'
+ emergencyModeLabel: undefined,
+
+ // Original text: 'Storage'
+ storageTabName: undefined,
+
+ // Original text: 'Patches'
+ patchesTabName: undefined,
+
+ // Original text: 'Load average'
+ statLoad: undefined,
+
+ // Original text: 'Hardware'
+ hardwareHostSettingsLabel: undefined,
+
+ // Original text: 'Address'
+ hostAddress: undefined,
+
+ // Original text: 'Status'
+ hostStatus: undefined,
+
+ // Original text: 'Build number'
+ hostBuildNumber: undefined,
+
+ // Original text: 'iSCSI name'
+ hostIscsiName: undefined,
+
+ // Original text: 'Version'
+ hostXenServerVersion: undefined,
+
+ // Original text: 'Enabled'
+ hostStatusEnabled: undefined,
+
+ // Original text: 'Disabled'
+ hostStatusDisabled: undefined,
+
+ // Original text: 'Power on mode'
+ hostPowerOnMode: undefined,
+
+ // Original text: 'Host uptime'
+ hostStartedSince: undefined,
+
+ // Original text: 'Toolstack uptime'
+ hostStackStartedSince: undefined,
+
+ // Original text: 'CPU model'
+ hostCpusModel: undefined,
+
+ // Original text: 'Core (socket)'
+ hostCpusNumber: undefined,
+
+ // Original text: 'Manufacturer info'
+ hostManufacturerinfo: undefined,
+
+ // Original text: 'BIOS info'
+ hostBiosinfo: undefined,
+
+ // Original text: 'Licence'
+ licenseHostSettingsLabel: undefined,
+
+ // Original text: 'Type'
+ hostLicenseType: undefined,
+
+ // Original text: 'Socket'
+ hostLicenseSocket: undefined,
+
+ // Original text: 'Expiry'
+ hostLicenseExpiry: undefined,
+
+ // Original text: 'Add a network'
+ networkCreateButton: undefined,
+
+ // Original text: 'Add a bonded network'
+ networkCreateBondedButton: undefined,
+
+ // Original text: 'Device'
+ pifDeviceLabel: undefined,
+
+ // Original text: 'Network'
+ pifNetworkLabel: undefined,
+
+ // Original text: 'VLAN'
+ pifVlanLabel: undefined,
+
+ // Original text: 'Address'
+ pifAddressLabel: undefined,
+
+ // Original text: 'Mode'
+ pifModeLabel: undefined,
+
+ // Original text: 'MAC'
+ pifMacLabel: undefined,
+
+ // Original text: 'MTU'
+ pifMtuLabel: undefined,
+
+ // Original text: 'Status'
+ pifStatusLabel: undefined,
+
+ // Original text: 'Connected'
+ pifStatusConnected: undefined,
+
+ // Original text: 'Disconnected'
+ pifStatusDisconnected: undefined,
+
+ // Original text: 'No physical interface detected'
+ pifNoInterface: undefined,
+
+ // Original text: 'This interface is currently in use'
+ pifInUse: undefined,
+
+ // Original text: 'Default locking mode'
+ defaultLockingMode: undefined,
+
+ // Original text: 'Configure IP address'
+ pifConfigureIp: undefined,
+
+ // Original text: 'Invalid parameters'
+ configIpErrorTitle: undefined,
+
+ // Original text: 'IP address and netmask required'
+ configIpErrorMessage: undefined,
+
+ // Original text: 'Static IP address'
+ staticIp: undefined,
+
+ // Original text: 'Netmask'
+ netmask: undefined,
+
+ // Original text: 'DNS'
+ dns: undefined,
+
+ // Original text: 'Gateway'
+ gateway: undefined,
+
+ // Original text: 'Add a storage'
+ addSrDeviceButton: undefined,
+
+ // Original text: 'Name'
+ srNameLabel: undefined,
+
+ // Original text: 'Type'
+ srType: undefined,
+
+ // Original text: 'Status'
+ pbdStatus: undefined,
+
+ // Original text: 'Connected'
+ pbdStatusConnected: undefined,
+
+ // Original text: 'Disconnected'
+ pbdStatusDisconnected: undefined,
+
+ // Original text: 'Connect'
+ pbdConnect: undefined,
+
+ // Original text: 'Disconnect'
+ pbdDisconnect: undefined,
+
+ // Original text: 'Forget'
+ pbdForget: undefined,
+
+ // Original text: 'Shared'
+ srShared: undefined,
+
+ // Original text: 'Not shared'
+ srNotShared: undefined,
+
+ // Original text: 'No storage detected'
+ pbdNoSr: undefined,
+
+ // Original text: 'Name'
+ patchNameLabel: undefined,
+
+ // Original text: 'Install all patches'
+ patchUpdateButton: undefined,
+
+ // Original text: 'Description'
+ patchDescription: undefined,
+
+ // Original text: 'Applied date'
+ patchApplied: undefined,
+
+ // Original text: 'Size'
+ patchSize: undefined,
+
+ // Original text: 'Status'
+ patchStatus: undefined,
+
+ // Original text: 'Applied'
+ patchStatusApplied: undefined,
+
+ // Original text: 'Missing patches'
+ patchStatusNotApplied: undefined,
+
+ // Original text: 'No patch detected'
+ patchNothing: undefined,
+
+ // Original text: 'Release date'
+ patchReleaseDate: undefined,
+
+ // Original text: 'Guidance'
+ patchGuidance: undefined,
+
+ // Original text: 'Action'
+ patchAction: undefined,
+
+ // Original text: 'Applied patches'
+ hostAppliedPatches: undefined,
+
+ // Original text: 'Missing patches'
+ hostMissingPatches: undefined,
+
+ // Original text: 'Host up-to-date!'
+ hostUpToDate: undefined,
+
+ // Original text: 'Refresh patches'
+ refreshPatches: undefined,
+
+ // Original text: 'Install pool patches'
+ installPoolPatches: undefined,
+
+ // Original text: 'Default SR'
+ defaultSr: undefined,
+
+ // Original text: 'Set as default SR'
+ setAsDefaultSr: undefined,
+
+ // Original text: 'General'
+ generalTabName: undefined,
+
+ // Original text: 'Stats'
+ statsTabName: undefined,
+
+ // Original text: 'Console'
+ consoleTabName: undefined,
+
+ // Original text: 'Container'
+ containersTabName: undefined,
+
+ // Original text: 'Snapshots'
+ snapshotsTabName: undefined,
+
+ // Original text: 'Logs'
+ logsTabName: undefined,
+
+ // Original text: 'Advanced'
+ advancedTabName: undefined,
+
+ // Original text: 'Network'
+ networkTabName: undefined,
+
+ // Original text: 'Disk{disks, plural, one {} other {s}}'
+ disksTabName: undefined,
+
+ // Original text: 'halted'
+ powerStateHalted: undefined,
+
+ // Original text: 'running'
+ powerStateRunning: undefined,
+
+ // Original text: 'suspended'
+ powerStateSuspended: undefined,
+
+ // Original text: 'No Xen tools detected'
+ vmStatus: undefined,
+
+ // Original text: 'No IPv4 record'
+ vmName: undefined,
+
+ // Original text: 'No IP record'
+ vmDescription: undefined,
+
+ // Original text: 'Started {ago}'
+ vmSettings: undefined,
+
+ // Original text: 'Current status:'
+ vmCurrentStatus: undefined,
+
+ // Original text: 'Not running'
+ vmNotRunning: undefined,
+
+ // Original text: 'No Xen tools detected'
+ noToolsDetected: undefined,
+
+ // Original text: 'No IPv4 record'
+ noIpv4Record: undefined,
+
+ // Original text: 'No IP record'
+ noIpRecord: undefined,
+
+ // Original text: 'Started {ago}'
+ started: undefined,
+
+ // Original text: 'Paravirtualization (PV)'
+ paraVirtualizedMode: undefined,
+
+ // Original text: 'Hardware virtualization (HVM)'
+ hardwareVirtualizedMode: undefined,
+
+ // Original text: 'CPU usage'
+ statsCpu: undefined,
+
+ // Original text: 'Memory usage'
+ statsMemory: undefined,
+
+ // Original text: 'Network throughput'
+ statsNetwork: undefined,
+
+ // Original text: 'Stacked values'
+ useStackedValuesOnStats: undefined,
+
+ // Original text: 'Disk throughput'
+ statDisk: undefined,
+
+ // Original text: 'Last 10 minutes'
+ statLastTenMinutes: undefined,
+
+ // Original text: 'Last 2 hours'
+ statLastTwoHours: undefined,
+
+ // Original text: 'Last week'
+ statLastWeek: undefined,
+
+ // Original text: 'Last year'
+ statLastYear: undefined,
+
+ // Original text: 'Copy'
+ copyToClipboardLabel: undefined,
+
+ // Original text: 'Ctrl+Alt+Del'
+ ctrlAltDelButtonLabel: undefined,
+
+ // Original text: 'Tip:'
+ tipLabel: undefined,
+
+ // Original text: 'non-US keyboard could have issues with console: switch your own layout to US.'
+ tipConsoleLabel: undefined,
+
+ // Original text: 'Hide infos'
+ hideHeaderTooltip: undefined,
+
+ // Original text: 'Show infos'
+ showHeaderTooltip: undefined,
+
+ // Original text: 'Name'
+ containerName: undefined,
+
+ // Original text: 'Command'
+ containerCommand: undefined,
+
+ // Original text: 'Creation date'
+ containerCreated: undefined,
+
+ // Original text: 'Status'
+ containerStatus: undefined,
+
+ // Original text: 'Action'
+ containerAction: undefined,
+
+ // Original text: 'No existing containers'
+ noContainers: undefined,
+
+ // Original text: 'Stop this container'
+ containerStop: undefined,
+
+ // Original text: 'Start this container'
+ containerStart: undefined,
+
+ // Original text: 'Pause this container'
+ containerPause: undefined,
+
+ // Original text: 'Resume this container'
+ containerResume: undefined,
+
+ // Original text: 'Restart this container'
+ containerRestart: undefined,
+
+ // Original text: 'Action'
+ vdiAction: undefined,
+
+ // Original text: 'Attach disk'
+ vdiAttachDeviceButton: undefined,
+
+ // Original text: 'New disk'
+ vbdCreateDeviceButton: undefined,
+
+ // Original text: 'Boot order'
+ vdiBootOrder: undefined,
+
+ // Original text: 'Name'
+ vdiNameLabel: undefined,
+
+ // Original text: 'Description'
+ vdiNameDescription: undefined,
+
+ // Original text: 'Tags'
+ vdiTags: undefined,
+
+ // Original text: 'Size'
+ vdiSize: undefined,
+
+ // Original text: 'SR'
+ vdiSr: undefined,
+
+ // Original text: 'VM'
+ vdiVm: undefined,
+
+ // Original text: 'Migrate VDI'
+ vdiMigrate: undefined,
+
+ // Original text: 'Destination SR:'
+ vdiMigrateSelectSr: undefined,
+
+ // Original text: 'Migrate all VDIs'
+ vdiMigrateAll: undefined,
+
+ // Original text: 'No SR'
+ vdiMigrateNoSr: undefined,
+
+ // Original text: 'A target SR is required to migrate a VDI'
+ vdiMigrateNoSrMessage: undefined,
+
+ // Original text: 'Forget'
+ vdiForget: undefined,
+
+ // Original text: 'Remove VDI'
+ vdiRemove: undefined,
+
+ // Original text: 'Boot flag'
+ vbdBootableStatus: undefined,
+
+ // Original text: 'Status'
+ vbdStatus: undefined,
+
+ // Original text: 'Connected'
+ vbdStatusConnected: undefined,
+
+ // Original text: 'Disconnected'
+ vbdStatusDisconnected: undefined,
+
+ // Original text: 'No disks'
+ vbdNoVbd: undefined,
+
+ // Original text: 'Connect VBD'
+ vbdConnect: undefined,
+
+ // Original text: 'Disconnect VBD'
+ vbdDisconnect: undefined,
+
+ // Original text: 'Bootable'
+ vbdBootable: undefined,
+
+ // Original text: 'Readonly'
+ vbdReadonly: undefined,
+
+ // Original text: 'Create'
+ vbdCreate: undefined,
+
+ // Original text: 'Disk name'
+ vbdNamePlaceHolder: undefined,
+
+ // Original text: 'Size'
+ vbdSizePlaceHolder: undefined,
+
+ // Original text: 'Save'
+ saveBootOption: undefined,
+
+ // Original text: 'Reset'
+ resetBootOption: undefined,
+
+ // Original text: 'New device'
+ vifCreateDeviceButton: undefined,
+
+ // Original text: 'No interface'
+ vifNoInterface: undefined,
+
+ // Original text: 'Device'
+ vifDeviceLabel: undefined,
+
+ // Original text: 'MAC address'
+ vifMacLabel: undefined,
+
+ // Original text: 'MTU'
+ vifMtuLabel: undefined,
+
+ // Original text: 'Network'
+ vifNetworkLabel: undefined,
+
+ // Original text: 'Status'
+ vifStatusLabel: undefined,
+
+ // Original text: 'Connected'
+ vifStatusConnected: undefined,
+
+ // Original text: 'Disconnected'
+ vifStatusDisconnected: undefined,
+
+ // Original text: 'Connect'
+ vifConnect: undefined,
+
+ // Original text: 'Disconnect'
+ vifDisconnect: undefined,
+
+ // Original text: 'Remove'
+ vifRemove: undefined,
+
+ // Original text: 'IP addresses'
+ vifIpAddresses: undefined,
+
+ // Original text: 'Auto-generated if empty'
+ vifMacAutoGenerate: undefined,
+
+ // Original text: 'Allowed IPs'
+ vifAllowedIps: undefined,
+
+ // Original text: 'No IPs'
+ vifNoIps: undefined,
+
+ // Original text: 'Network locked'
+ vifLockedNetwork: undefined,
+
+ // Original text: 'Network locked and no IPs are allowed for this interface'
+ vifLockedNetworkNoIps: undefined,
+
+ // Original text: 'Network not locked'
+ vifUnLockedNetwork: undefined,
+
+ // Original text: 'Unknown network'
+ vifUnknownNetwork: undefined,
+
+ // Original text: 'Create'
+ vifCreate: undefined,
+
+ // Original text: 'No snapshots'
+ noSnapshots: undefined,
+
+ // Original text: 'New snapshot'
+ snapshotCreateButton: undefined,
+
+ // Original text: 'Just click on the snapshot button to create one!'
+ tipCreateSnapshotLabel: undefined,
+
+ // Original text: 'Revert VM to this snapshot'
+ revertSnapshot: undefined,
+
+ // Original text: 'Remove this snapshot'
+ deleteSnapshot: undefined,
+
+ // Original text: 'Create a VM from this snapshot'
+ copySnapshot: undefined,
+
+ // Original text: 'Export this snapshot'
+ exportSnapshot: undefined,
+
+ // Original text: 'Creation date'
+ snapshotDate: undefined,
+
+ // Original text: 'Name'
+ snapshotName: undefined,
+
+ // Original text: 'Action'
+ snapshotAction: undefined,
+
+ // Original text: 'Remove all logs'
+ logRemoveAll: undefined,
+
+ // Original text: 'No logs so far'
+ noLogs: undefined,
+
+ // Original text: 'Creation date'
+ logDate: undefined,
+
+ // Original text: 'Name'
+ logName: undefined,
+
+ // Original text: 'Content'
+ logContent: undefined,
+
+ // Original text: 'Action'
+ logAction: undefined,
+
+ // Original text: 'Remove'
+ vmRemoveButton: undefined,
+
+ // Original text: 'Convert'
+ vmConvertButton: undefined,
+
+ // Original text: 'Xen settings'
+ xenSettingsLabel: undefined,
+
+ // Original text: 'Guest OS'
+ guestOsLabel: undefined,
+
+ // Original text: 'Misc'
+ miscLabel: undefined,
+
+ // Original text: 'UUID'
+ uuid: undefined,
+
+ // Original text: 'Virtualization mode'
+ virtualizationMode: undefined,
+
+ // Original text: 'CPU weight'
+ cpuWeightLabel: undefined,
+
+ // Original text: 'Default ({value, number})'
+ defaultCpuWeight: undefined,
+
+ // Original text: 'CPU cap'
+ cpuCapLabel: undefined,
+
+ // Original text: 'Default ({value, number})'
+ defaultCpuCap: undefined,
+
+ // Original text: 'PV args'
+ pvArgsLabel: undefined,
+
+ // Original text: 'Xen tools status'
+ xenToolsStatus: undefined,
+
+ // Original text: '{status}'
+ xenToolsStatusValue: undefined,
+
+ // Original text: 'OS name'
+ osName: undefined,
+
+ // Original text: 'OS kernel'
+ osKernel: undefined,
+
+ // Original text: 'Auto power on'
+ autoPowerOn: undefined,
+
+ // Original text: 'HA'
+ ha: undefined,
+
+ // Original text: 'Original template'
+ originalTemplate: undefined,
+
+ // Original text: 'Unknown'
+ unknownOsName: undefined,
+
+ // Original text: 'Unknown'
+ unknownOsKernel: undefined,
+
+ // Original text: 'Unknown'
+ unknownOriginalTemplate: undefined,
+
+ // Original text: 'VM limits'
+ vmLimitsLabel: undefined,
+
+ // Original text: 'CPU limits'
+ vmCpuLimitsLabel: undefined,
+
+ // Original text: 'Memory limits (min/max)'
+ vmMemoryLimitsLabel: undefined,
+
+ // Original text: 'vCPUs max:'
+ vmMaxVcpus: undefined,
+
+ // Original text: 'Memory max:'
+ vmMaxRam: undefined,
+
+ // Original text: 'Long click to add a name'
+ vmHomeNamePlaceholder: undefined,
+
+ // Original text: 'Long click to add a description'
+ vmHomeDescriptionPlaceholder: undefined,
+
+ // Original text: 'Click to add a name'
+ vmViewNamePlaceholder: undefined,
+
+ // Original text: 'Click to add a description'
+ vmViewDescriptionPlaceholder: undefined,
+
+ // Original text: 'Click to add a name'
+ templateHomeNamePlaceholder: undefined,
+
+ // Original text: 'Click to add a description'
+ templateHomeDescriptionPlaceholder: undefined,
+
+ // Original text: 'Delete template'
+ templateDelete: undefined,
+
+ // Original text: 'Delete VM template{templates, plural, one {} other {s}}'
+ templateDeleteModalTitle: undefined,
+
+ // Original text: 'Are you sure you want to delete {templates, plural, one {this} other {these}} template{templates, plural, one {} other {s}}?'
+ templateDeleteModalBody: undefined,
+
+ // Original text: 'Pool{pools, plural, one {} other {s}}'
+ poolPanel: undefined,
+
+ // Original text: 'Host{hosts, plural, one {} other {s}}'
+ hostPanel: undefined,
+
+ // Original text: 'VM{vms, plural, one {} other {s}}'
+ vmPanel: undefined,
+
+ // Original text: 'RAM Usage'
+ memoryStatePanel: undefined,
+
+ // Original text: 'CPUs Usage'
+ cpuStatePanel: undefined,
+
+ // Original text: 'VMs Power state'
+ vmStatePanel: undefined,
+
+ // Original text: 'Pending tasks'
+ taskStatePanel: undefined,
+
+ // Original text: 'Users'
+ usersStatePanel: undefined,
+
+ // Original text: 'Storage state'
+ srStatePanel: undefined,
+
+ // Original text: '{usage} (of {total})'
+ ofUsage: undefined,
+
+ // Original text: 'No storage'
+ noSrs: undefined,
+
+ // Original text: 'Name'
+ srName: undefined,
+
+ // Original text: 'Pool'
+ srPool: undefined,
+
+ // Original text: 'Host'
+ srHost: undefined,
+
+ // Original text: 'Type'
+ srFormat: undefined,
+
+ // Original text: 'Size'
+ srSize: undefined,
+
+ // Original text: 'Usage'
+ srUsage: undefined,
+
+ // Original text: 'used'
+ srUsed: undefined,
+
+ // Original text: 'free'
+ srFree: undefined,
+
+ // Original text: 'Storage Usage'
+ srUsageStatePanel: undefined,
+
+ // Original text: 'Top 5 SR Usage (in %)'
+ srTopUsageStatePanel: undefined,
+
+ // Original text: '{running} running ({halted} halted)'
+ vmsStates: undefined,
+
+ // Original text: 'Clear selection'
+ dashboardStatsButtonRemoveAll: undefined,
+
+ // Original text: 'Add all hosts'
+ dashboardStatsButtonAddAllHost: undefined,
+
+ // Original text: 'Add all VMs'
+ dashboardStatsButtonAddAllVM: undefined,
+
+ // Original text: '{value} {date, date, medium}'
+ weekHeatmapData: undefined,
+
+ // Original text: 'No data.'
+ weekHeatmapNoData: undefined,
+
+ // Original text: 'Weekly Heatmap'
+ weeklyHeatmap: undefined,
+
+ // Original text: 'Weekly Charts'
+ weeklyCharts: undefined,
+
+ // Original text: 'Synchronize scale:'
+ weeklyChartsScaleInfo: undefined,
+
+ // Original text: 'Stats error'
+ statsDashboardGenericErrorTitle: undefined,
+
+ // Original text: 'There is no stats available for:'
+ statsDashboardGenericErrorMessage: undefined,
+
+ // Original text: 'No selected metric'
+ noSelectedMetric: undefined,
+
+ // Original text: 'Select'
+ statsDashboardSelectObjects: undefined,
+
+ // Original text: 'Loading…'
+ metricsLoading: undefined,
+
+ // Original text: 'Coming soon!'
+ comingSoon: undefined,
+
+ // Original text: 'Orphaned snapshot VDIs'
+ orphanedVdis: undefined,
+
+ // Original text: 'Orphaned VMs snapshot'
+ orphanedVms: undefined,
+
+ // Original text: 'No orphans'
+ noOrphanedObject: undefined,
+
+ // Original text: 'Remove all orphaned snapshot VDIs'
+ removeAllOrphanedObject: undefined,
+
+ // Original text: 'Name'
+ vmNameLabel: undefined,
+
+ // Original text: 'Description'
+ vmNameDescription: undefined,
+
+ // Original text: 'Resident on'
+ vmContainer: undefined,
+
+ // Original text: 'Alarms'
+ alarmMessage: undefined,
+
+ // Original text: 'No alarms'
+ noAlarms: undefined,
+
+ // Original text: 'Date'
+ alarmDate: undefined,
+
+ // Original text: 'Content'
+ alarmContent: undefined,
+
+ // Original text: 'Issue on'
+ alarmObject: undefined,
+
+ // Original text: 'Pool'
+ alarmPool: undefined,
+
+ // Original text: 'Remove all alarms'
+ alarmRemoveAll: undefined,
+
+ // Original text: '{used}% used ({free} left)'
+ spaceLeftTooltip: undefined,
+
+ // Original text: 'Create a new VM on {select}'
+ newVmCreateNewVmOn: undefined,
+
+ // Original text: 'Create a new VM on {select1} or {select2}'
+ newVmCreateNewVmOn2: undefined,
+
+ // Original text: 'You have no permission to create a VM'
+ newVmCreateNewVmNoPermission: undefined,
+
+ // Original text: 'Infos'
+ newVmInfoPanel: undefined,
+
+ // Original text: 'Name'
+ newVmNameLabel: undefined,
+
+ // Original text: 'Template'
+ newVmTemplateLabel: undefined,
+
+ // Original text: 'Description'
+ newVmDescriptionLabel: undefined,
+
+ // Original text: 'Performances'
+ newVmPerfPanel: undefined,
+
+ // Original text: 'vCPUs'
+ newVmVcpusLabel: undefined,
+
+ // Original text: 'RAM'
+ newVmRamLabel: undefined,
+
+ // Original text: 'Static memory max'
+ newVmStaticMaxLabel: undefined,
+
+ // Original text: 'Dynamic memory min'
+ newVmDynamicMinLabel: undefined,
+
+ // Original text: 'Dynamic memory max'
+ newVmDynamicMaxLabel: undefined,
+
+ // Original text: 'Install settings'
+ newVmInstallSettingsPanel: undefined,
+
+ // Original text: 'ISO/DVD'
+ newVmIsoDvdLabel: undefined,
+
+ // Original text: 'Network'
+ newVmNetworkLabel: undefined,
+
+ // Original text: 'e.g: http://httpredir.debian.org/debian'
+ newVmInstallNetworkPlaceHolder: undefined,
+
+ // Original text: 'PV Args'
+ newVmPvArgsLabel: undefined,
+
+ // Original text: 'PXE'
+ newVmPxeLabel: undefined,
+
+ // Original text: 'Interfaces'
+ newVmInterfacesPanel: undefined,
+
+ // Original text: 'MAC'
+ newVmMacLabel: undefined,
+
+ // Original text: 'Add interface'
+ newVmAddInterface: undefined,
+
+ // Original text: 'Disks'
+ newVmDisksPanel: undefined,
+
+ // Original text: 'SR'
+ newVmSrLabel: undefined,
+
+ // Original text: 'Bootable'
+ newVmBootableLabel: undefined,
+
+ // Original text: 'Size'
+ newVmSizeLabel: undefined,
+
+ // Original text: 'Add disk'
+ newVmAddDisk: undefined,
+
+ // Original text: 'Summary'
+ newVmSummaryPanel: undefined,
+
+ // Original text: 'Create'
+ newVmCreate: undefined,
+
+ // Original text: 'Reset'
+ newVmReset: undefined,
+
+ // Original text: 'Select template'
+ newVmSelectTemplate: undefined,
+
+ // Original text: 'SSH key'
+ newVmSshKey: undefined,
+
+ // Original text: 'Config drive'
+ newVmConfigDrive: undefined,
+
+ // Original text: 'Custom config'
+ newVmCustomConfig: undefined,
+
+ // Original text: 'Boot VM after creation'
+ newVmBootAfterCreate: undefined,
+
+ // Original text: 'Auto-generated if empty'
+ newVmMacPlaceholder: undefined,
+
+ // Original text: 'CPU weight'
+ newVmCpuWeightLabel: undefined,
+
+ // Original text: 'Default: {value, number}'
+ newVmDefaultCpuWeight: undefined,
+
+ // Original text: 'CPU cap'
+ newVmCpuCapLabel: undefined,
+
+ // Original text: 'Default: {value, number}'
+ newVmDefaultCpuCap: undefined,
+
+ // Original text: 'Cloud config'
+ newVmCloudConfig: undefined,
+
+ // Original text: 'Create VMs'
+ newVmCreateVms: undefined,
+
+ // Original text: 'Are you sure you want to create {nbVms} VMs?'
+ newVmCreateVmsConfirm: undefined,
+
+ // Original text: 'Multiple VMs:'
+ newVmMultipleVms: undefined,
+
+ // Original text: 'Select a resource set:'
+ newVmSelectResourceSet: undefined,
+
+ // Original text: 'Name pattern:'
+ newVmMultipleVmsPattern: undefined,
+
+ // Original text: 'e.g.: \\{name\\}_%'
+ newVmMultipleVmsPatternPlaceholder: undefined,
+
+ // Original text: 'First index:'
+ newVmFirstIndex: undefined,
+
+ // Original text: 'Recalculate VMs number'
+ newVmNumberRecalculate: undefined,
+
+ // Original text: 'Refresh VMs name'
+ newVmNameRefresh: undefined,
+
+ // Original text: 'Advanced'
+ newVmAdvancedPanel: undefined,
+
+ // Original text: 'Show advanced settings'
+ newVmShowAdvanced: undefined,
+
+ // Original text: 'Hide advanced settings'
+ newVmHideAdvanced: undefined,
+
+ // Original text: 'Resource sets'
+ resourceSets: undefined,
+
+ // Original text: 'No resource sets.'
+ noResourceSets: undefined,
+
+ // Original text: 'Loading resource sets'
+ loadingResourceSets: undefined,
+
+ // Original text: 'Resource set name'
+ resourceSetName: undefined,
+
+ // Original text: 'Recompute all limits'
+ recomputeResourceSets: undefined,
+
+ // Original text: 'Save'
+ saveResourceSet: undefined,
+
+ // Original text: 'Reset'
+ resetResourceSet: undefined,
+
+ // Original text: 'Edit'
+ editResourceSet: undefined,
+
+ // Original text: 'Delete'
+ deleteResourceSet: undefined,
+
+ // Original text: 'Delete resource set'
+ deleteResourceSetWarning: undefined,
+
+ // Original text: 'Are you sure you want to delete this resource set?'
+ deleteResourceSetQuestion: undefined,
+
+ // Original text: 'Missing objects:'
+ resourceSetMissingObjects: undefined,
+
+ // Original text: 'vCPUs'
+ resourceSetVcpus: undefined,
+
+ // Original text: 'Memory'
+ resourceSetMemory: undefined,
+
+ // Original text: 'Storage'
+ resourceSetStorage: undefined,
+
+ // Original text: 'Unknown'
+ unknownResourceSetValue: undefined,
+
+ // Original text: 'Available hosts'
+ availableHosts: undefined,
+
+ // Original text: 'Excluded hosts'
+ excludedHosts: undefined,
+
+ // Original text: 'No hosts available.'
+ noHostsAvailable: undefined,
+
+ // Original text: 'VMs created from this resource set shall run on the following hosts.'
+ availableHostsDescription: undefined,
+
+ // Original text: 'Maximum CPUs'
+ maxCpus: undefined,
+
+ // Original text: 'Maximum RAM (GiB)'
+ maxRam: undefined,
+
+ // Original text: 'Maximum disk space'
+ maxDiskSpace: undefined,
+
+ // Original text: 'IP pool'
+ ipPool: undefined,
+
+ // Original text: 'Quantity'
+ quantity: undefined,
+
+ // Original text: 'No limits.'
+ noResourceSetLimits: undefined,
+
+ // Original text: 'Total:'
+ totalResource: undefined,
+
+ // Original text: 'Remaining:'
+ remainingResource: undefined,
+
+ // Original text: 'Used:'
+ usedResource: undefined,
+
+ // Original text: 'New'
+ resourceSetNew: undefined,
+
+ // Original text: 'Try dropping some VMs files here, or click to select VMs to upload. Accept only .xva/.ova files.'
+ importVmsList: undefined,
+
+ // Original text: 'No selected VMs.'
+ noSelectedVms: undefined,
+
+ // Original text: 'To Pool:'
+ vmImportToPool: undefined,
+
+ // Original text: 'To SR:'
+ vmImportToSr: undefined,
+
+ // Original text: 'VMs to import'
+ vmsToImport: undefined,
+
+ // Original text: 'Reset'
+ importVmsCleanList: undefined,
+
+ // Original text: 'VM import success'
+ vmImportSuccess: undefined,
+
+ // Original text: 'VM import failed'
+ vmImportFailed: undefined,
+
+ // Original text: 'Import starting…'
+ startVmImport: undefined,
+
+ // Original text: 'Export starting…'
+ startVmExport: undefined,
+
+ // Original text: 'N CPUs'
+ nCpus: undefined,
+
+ // Original text: 'Memory'
+ vmMemory: undefined,
+
+ // Original text: 'Disk {position} ({capacity})'
+ diskInfo: undefined,
+
+ // Original text: 'Disk description'
+ diskDescription: undefined,
+
+ // Original text: 'No disks.'
+ noDisks: undefined,
+
+ // Original text: 'No networks.'
+ noNetworks: undefined,
+
+ // Original text: 'Network {name}'
+ networkInfo: undefined,
+
+ // Original text: 'No description available'
+ noVmImportErrorDescription: undefined,
+
+ // Original text: 'Error:'
+ vmImportError: undefined,
+
+ // Original text: '{type} file:'
+ vmImportFileType: undefined,
+
+ // Original text: 'Please to check and/or modify the VM configuration.'
+ vmImportConfigAlert: undefined,
+
+ // Original text: 'No pending tasks'
+ noTasks: undefined,
+
+ // Original text: 'Currently, there are not any pending XenServer tasks'
+ xsTasks: undefined,
+
+ // Original text: 'Schedules'
+ backupSchedules: undefined,
+
+ // Original text: 'Get remote'
+ getRemote: undefined,
+
+ // Original text: 'List Remote'
+ listRemote: undefined,
+
+ // Original text: 'simple'
+ simpleBackup: undefined,
+
+ // Original text: 'delta'
+ delta: undefined,
+
+ // Original text: 'Restore Backups'
+ restoreBackups: undefined,
+
+ // Original text: 'Click on a VM to display restore options'
+ restoreBackupsInfo: undefined,
+
+ // Original text: 'Enabled'
+ remoteEnabled: undefined,
+
+ // Original text: 'Error'
+ remoteError: undefined,
+
+ // Original text: 'No backup available'
+ noBackup: undefined,
+
+ // Original text: 'VM Name'
+ backupVmNameColumn: undefined,
+
+ // Original text: 'Tags'
+ backupTags: undefined,
+
+ // Original text: 'Last Backup'
+ lastBackupColumn: undefined,
+
+ // Original text: 'Available Backups'
+ availableBackupsColumn: undefined,
+
+ // Original text: 'Missing parameters'
+ backupRestoreErrorTitle: undefined,
+
+ // Original text: 'Choose a SR and a backup'
+ backupRestoreErrorMessage: undefined,
+
+ // Original text: 'Display backups'
+ displayBackup: undefined,
+
+ // Original text: 'Import VM'
+ importBackupTitle: undefined,
+
+ // Original text: 'Starting your backup import'
+ importBackupMessage: undefined,
+
+ // Original text: 'VMs to backup'
+ vmsToBackup: undefined,
+
+ // Original text: 'Emergency shutdown Host{nHosts, plural, one {} other {s}}'
+ emergencyShutdownHostsModalTitle: undefined,
+
+ // Original text: 'Are you sure you want to shutdown {nHosts} Host{nHosts, plural, one {} other {s}}?'
+ emergencyShutdownHostsModalMessage: undefined,
+
+ // Original text: 'Shutdown host'
+ stopHostModalTitle: undefined,
+
+ // 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: undefined,
+
+ // Original text: 'Add host'
+ addHostModalTitle: undefined,
+
+ // Original text: 'Are you sure you want to add {host} to {pool}?'
+ addHostModalMessage: undefined,
+
+ // Original text: 'Restart host'
+ restartHostModalTitle: undefined,
+
+ // Original text: 'This will restart your host. Do you want to continue?'
+ restartHostModalMessage: undefined,
+
+ // Original text: 'Restart Host{nHosts, plural, one {} other {s}} agent{nHosts, plural, one {} other {s}}'
+ restartHostsAgentsModalTitle: undefined,
+
+ // Original text: 'Are you sure you want to restart {nHosts} Host{nHosts, plural, one {} other {s}} agent{nHosts, plural, one {} other {s}}?'
+ restartHostsAgentsModalMessage: undefined,
+
+ // Original text: 'Restart Host{nHosts, plural, one {} other {s}}'
+ restartHostsModalTitle: undefined,
+
+ // Original text: 'Are you sure you want to restart {nHosts} Host{nHosts, plural, one {} other {s}}?'
+ restartHostsModalMessage: undefined,
+
+ // Original text: 'Start VM{vms, plural, one {} other {s}}'
+ startVmsModalTitle: undefined,
+
+ // Original text: 'Are you sure you want to start {vms} VM{vms, plural, one {} other {s}}?'
+ startVmsModalMessage: undefined,
+
+ // Original text: 'Stop Host{nHosts, plural, one {} other {s}}'
+ stopHostsModalTitle: undefined,
+
+ // Original text: 'Are you sure you want to stop {nHosts} Host{nHosts, plural, one {} other {s}}?'
+ stopHostsModalMessage: undefined,
+
+ // Original text: 'Stop VM{vms, plural, one {} other {s}}'
+ stopVmsModalTitle: undefined,
+
+ // Original text: 'Are you sure you want to stop {vms} VM{vms, plural, one {} other {s}}?'
+ stopVmsModalMessage: undefined,
+
+ // Original text: 'Restart VM'
+ restartVmModalTitle: undefined,
+
+ // Original text: 'Are you sure you want to restart {name}?'
+ restartVmModalMessage: undefined,
+
+ // Original text: 'Stop VM'
+ stopVmModalTitle: undefined,
+
+ // Original text: 'Are you sure you want to stop {name}?'
+ stopVmModalMessage: undefined,
+
+ // Original text: 'Restart VM{vms, plural, one {} other {s}}'
+ restartVmsModalTitle: undefined,
+
+ // Original text: 'Are you sure you want to restart {vms} VM{vms, plural, one {} other {s}}?'
+ restartVmsModalMessage: undefined,
+
+ // Original text: 'Snapshot VM{vms, plural, one {} other {s}}'
+ snapshotVmsModalTitle: undefined,
+
+ // Original text: 'Are you sure you want to snapshot {vms} VM{vms, plural, one {} other {s}}?'
+ snapshotVmsModalMessage: undefined,
+
+ // Original text: 'Delete VM{vms, plural, one {} other {s}}'
+ deleteVmsModalTitle: undefined,
+
+ // Original text: 'Are you sure you want to delete {vms} VM{vms, plural, one {} other {s}}? ALL VM DISKS WILL BE REMOVED'
+ deleteVmsModalMessage: undefined,
+
+ // Original text: 'Delete VM'
+ deleteVmModalTitle: undefined,
+
+ // Original text: 'Are you sure you want to delete this VM? ALL VM DISKS WILL BE REMOVED'
+ deleteVmModalMessage: undefined,
+
+ // Original text: 'Migrate VM'
+ migrateVmModalTitle: undefined,
+
+ // Original text: 'Select a destination host:'
+ migrateVmSelectHost: undefined,
+
+ // Original text: 'Select a migration network:'
+ migrateVmSelectMigrationNetwork: undefined,
+
+ // Original text: 'For each VDI, select an SR:'
+ migrateVmSelectSrs: undefined,
+
+ // Original text: 'For each VIF, select a network:'
+ migrateVmSelectNetworks: undefined,
+
+ // Original text: 'Select a destination SR:'
+ migrateVmsSelectSr: undefined,
+
+ // Original text: 'Select a destination SR for local disks:'
+ migrateVmsSelectSrIntraPool: undefined,
+
+ // Original text: 'Select a network on which to connect each VIF:'
+ migrateVmsSelectNetwork: undefined,
+
+ // Original text: 'Smart mapping'
+ migrateVmsSmartMapping: undefined,
+
+ // Original text: 'Name'
+ migrateVmName: undefined,
+
+ // Original text: 'SR'
+ migrateVmSr: undefined,
+
+ // Original text: 'VIF'
+ migrateVmVif: undefined,
+
+ // Original text: 'Network'
+ migrateVmNetwork: undefined,
+
+ // Original text: 'No target host'
+ migrateVmNoTargetHost: undefined,
+
+ // Original text: 'A target host is required to migrate a VM'
+ migrateVmNoTargetHostMessage: undefined,
+
+ // Original text: 'Delete VDI'
+ deleteVdiModalTitle: undefined,
+
+ // Original text: 'Are you sure you want to delete this disk? ALL DATA ON THIS DISK WILL BE LOST'
+ deleteVdiModalMessage: undefined,
+
+ // Original text: 'Revert your VM'
+ revertVmModalTitle: undefined,
+
+ // Original text: 'Delete snapshot'
+ deleteSnapshotModalTitle: undefined,
+
+ // Original text: 'Are you sure you want to delete this snapshot?'
+ deleteSnapshotModalMessage: undefined,
+
+ // Original text: 'Are you sure you want to revert this VM to the snapshot state? This operation is irreversible.'
+ revertVmModalMessage: undefined,
+
+ // Original text: 'Snapshot before'
+ revertVmModalSnapshotBefore: undefined,
+
+ // Original text: 'Import a {name} Backup'
+ importBackupModalTitle: undefined,
+
+ // Original text: 'Start VM after restore'
+ importBackupModalStart: undefined,
+
+ // Original text: 'Select your backup…'
+ importBackupModalSelectBackup: undefined,
+
+ // Original text: 'Are you sure you want to remove all orphaned snapshot VDIs?'
+ removeAllOrphanedModalWarning: undefined,
+
+ // Original text: 'Remove all logs'
+ removeAllLogsModalTitle: undefined,
+
+ // Original text: 'Are you sure you want to remove all logs?'
+ removeAllLogsModalWarning: undefined,
+
+ // Original text: 'This operation is definitive.'
+ definitiveMessageModal: undefined,
+
+ // Original text: 'Previous SR Usage'
+ existingSrModalTitle: undefined,
+
+ // 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: undefined,
+
+ // Original text: 'Previous LUN Usage'
+ existingLunModalTitle: undefined,
+
+ // 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: undefined,
+
+ // Original text: 'Replace current registration?'
+ alreadyRegisteredModal: undefined,
+
+ // Original text: 'Your XO appliance is already registered to {email}, do you want to forget and replace this registration ?'
+ alreadyRegisteredModalText: undefined,
+
+ // Original text: 'Ready for trial?'
+ trialReadyModal: undefined,
+
+ // Original text: 'During the trial period, XOA need to have a working internet connection. This limitation does not apply for our paid plans!'
+ trialReadyModalText: undefined,
+
+ // Original text: 'Host'
+ serverHost: undefined,
+
+ // Original text: 'Username'
+ serverUsername: undefined,
+
+ // Original text: 'Password'
+ serverPassword: undefined,
+
+ // Original text: 'Action'
+ serverAction: undefined,
+
+ // Original text: 'Read Only'
+ serverReadOnly: undefined,
+
+ // Original text: 'Disconnect server'
+ serverDisconnect: undefined,
+
+ // Original text: 'username'
+ serverPlaceHolderUser: undefined,
+
+ // Original text: 'password'
+ serverPlaceHolderPassword: undefined,
+
+ // Original text: 'address[:port]'
+ serverPlaceHolderAddress: undefined,
+
+ // Original text: 'Connect'
+ serverConnect: undefined,
+
+ // Original text: 'Copy VM'
+ copyVm: undefined,
+
+ // Original text: 'Are you sure you want to copy this VM to {SR}?'
+ copyVmConfirm: undefined,
+
+ // Original text: 'Name'
+ copyVmName: undefined,
+
+ // Original text: 'Name pattern'
+ copyVmNamePattern: undefined,
+
+ // Original text: 'If empty: name of the copied VM'
+ copyVmNamePlaceholder: undefined,
+
+ // Original text: 'e.g.: "\\{name\\}_COPY"'
+ copyVmNamePatternPlaceholder: undefined,
+
+ // Original text: 'Select SR'
+ copyVmSelectSr: undefined,
+
+ // Original text: 'Use compression'
+ copyVmCompress: undefined,
+
+ // Original text: 'No target SR'
+ copyVmsNoTargetSr: undefined,
+
+ // Original text: 'A target SR is required to copy a VM'
+ copyVmsNoTargetSrMessage: undefined,
+
+ // Original text: 'Detach host'
+ detachHostModalTitle: undefined,
+
+ // 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: undefined,
+
+ // Original text: 'Detach'
+ detachHost: undefined,
+
+ // Original text: 'Create network'
+ newNetworkCreate: undefined,
+
+ // Original text: 'Create bonded network'
+ newBondedNetworkCreate: undefined,
+
+ // Original text: 'Interface'
+ newNetworkInterface: undefined,
+
+ // Original text: 'Name'
+ newNetworkName: undefined,
+
+ // Original text: 'Description'
+ newNetworkDescription: undefined,
+
+ // Original text: 'VLAN'
+ newNetworkVlan: undefined,
+
+ // Original text: 'No VLAN if empty'
+ newNetworkDefaultVlan: undefined,
+
+ // Original text: 'MTU'
+ newNetworkMtu: undefined,
+
+ // Original text: 'Default: 1500'
+ newNetworkDefaultMtu: undefined,
+
+ // Original text: 'Name required'
+ newNetworkNoNameErrorTitle: undefined,
+
+ // Original text: 'A name is required to create a network'
+ newNetworkNoNameErrorMessage: undefined,
+
+ // Original text: 'Bond mode'
+ newNetworkBondMode: undefined,
+
+ // Original text: 'Delete network'
+ deleteNetwork: undefined,
+
+ // Original text: 'Are you sure you want to delete this network?'
+ deleteNetworkConfirm: undefined,
+
+ // Original text: 'This network is currently in use'
+ networkInUse: undefined,
+
+ // Original text: 'Bonded'
+ pillBonded: undefined,
+
+ // Original text: 'Host'
+ addHostSelectHost: undefined,
+
+ // Original text: 'No host'
+ addHostNoHost: undefined,
+
+ // Original text: 'No host selected to be added'
+ addHostNoHostMessage: undefined,
+
+ // Original text: 'Xen Orchestra'
+ xenOrchestra: undefined,
+
+ // Original text: 'server'
+ xenOrchestraServer: undefined,
+
+ // Original text: 'web client'
+ xenOrchestraWeb: undefined,
+
+ // Original text: 'No pro support provided!'
+ noProSupport: undefined,
+
+ // Original text: 'Use in production at your own risks'
+ noProductionUse: undefined,
+
+ // Original text: 'You can download our turnkey appliance at'
+ downloadXoa: undefined,
+
+ // Original text: 'Bug Tracker'
+ bugTracker: undefined,
+
+ // Original text: 'Issues? Report it!'
+ bugTrackerText: undefined,
+
+ // Original text: 'Community'
+ community: undefined,
+
+ // Original text: 'Join our community forum!'
+ communityText: undefined,
+
+ // Original text: 'Free Trial for Premium Edition!'
+ freeTrial: undefined,
+
+ // Original text: 'Request your trial now!'
+ freeTrialNow: undefined,
+
+ // Original text: 'Any issue?'
+ issues: undefined,
+
+ // Original text: 'Problem? Contact us!'
+ issuesText: undefined,
+
+ // Original text: 'Documentation'
+ documentation: undefined,
+
+ // Original text: 'Read our official doc'
+ documentationText: undefined,
+
+ // Original text: 'Pro support included'
+ proSupportIncluded: undefined,
+
+ // Original text: 'Acces your XO Account'
+ xoAccount: undefined,
+
+ // Original text: 'Report a problem'
+ openTicket: undefined,
+
+ // Original text: 'Problem? Open a ticket!'
+ openTicketText: undefined,
+
+ // Original text: 'Upgrade needed'
+ upgradeNeeded: undefined,
+
+ // Original text: 'Upgrade now!'
+ upgradeNow: undefined,
+
+ // Original text: 'Or'
+ or: undefined,
+
+ // Original text: 'Try it for free!'
+ tryIt: undefined,
+
+ // Original text: 'This feature is available starting from {plan} Edition'
+ availableIn: undefined,
+
+ // Original text: 'This feature is not available in your version, contact your administrator to know more.'
+ notAvailable: undefined,
+
+ // Original text: 'Updates'
+ updateTitle: undefined,
+
+ // Original text: 'Registration'
+ registration: undefined,
+
+ // Original text: 'Trial'
+ trial: undefined,
+
+ // Original text: 'Settings'
+ settings: undefined,
+
+ // Original text: 'Proxy settings'
+ proxySettings: undefined,
+
+ // Original text: 'Host (myproxy.example.org)'
+ proxySettingsHostPlaceHolder: undefined,
+
+ // Original text: 'Port (eg: 3128)'
+ proxySettingsPortPlaceHolder: undefined,
+
+ // Original text: 'Username'
+ proxySettingsUsernamePlaceHolder: undefined,
+
+ // Original text: 'Password'
+ proxySettingsPasswordPlaceHolder: undefined,
+
+ // Original text: 'Your email account'
+ updateRegistrationEmailPlaceHolder: undefined,
+
+ // Original text: 'Your password'
+ updateRegistrationPasswordPlaceHolder: undefined,
+
+ // Original text: 'Update'
+ update: undefined,
+
+ // Original text: 'Refresh'
+ refresh: undefined,
+
+ // Original text: 'Upgrade'
+ upgrade: undefined,
+
+ // Original text: 'No updater available for Community Edition'
+ noUpdaterCommunity: undefined,
+
+ // Original text: 'Please consider subscribe and try it with all features for free during 15 days on'
+ noUpdaterSubscribe: undefined,
+
+ // Original text: 'Manual update could break your current installation due to dependencies issues, do it with caution'
+ noUpdaterWarning: undefined,
+
+ // Original text: 'Current version:'
+ currentVersion: undefined,
+
+ // Original text: 'Register'
+ register: undefined,
+
+ // Original text: 'Edit registration'
+ editRegistration: undefined,
+
+ // Original text: 'Please, take time to register in order to enjoy your trial.'
+ trialRegistration: undefined,
+
+ // Original text: 'Start trial'
+ trialStartButton: undefined,
+
+ // Original text: 'You can use a trial version until {date, date, medium}. Upgrade your appliance to get it.'
+ trialAvailableUntil: undefined,
+
+ // Original text: 'Your trial has been ended. Contact us or downgrade to Free version'
+ trialConsumed: undefined,
+
+ // Original text: 'Your xoa-updater service appears to be down. Your XOA cannot run fully without reaching this service.'
+ trialLocked: undefined,
+
+ // Original text: 'No update information available'
+ noUpdateInfo: undefined,
+
+ // Original text: 'Update information may be available'
+ waitingUpdateInfo: undefined,
+
+ // Original text: 'Your XOA is up-to-date'
+ upToDate: undefined,
+
+ // Original text: 'You need to update your XOA (new version is available)'
+ mustUpgrade: undefined,
+
+ // Original text: 'Your XOA is not registered for updates'
+ registerNeeded: undefined,
+
+ // Original text: "Can't fetch update information"
+ updaterError: undefined,
+
+ // Original text: 'Upgrade successful'
+ promptUpgradeReloadTitle: undefined,
+
+ // Original text: 'Your XOA has successfully upgraded, and your browser must reload the application. Do you want to reload now ?'
+ promptUpgradeReloadMessage: undefined,
+
+ // Original text: 'Xen Orchestra from the sources'
+ disclaimerTitle: undefined,
+
+ // Original text: "You are using XO from the sources! That's great for a personal/non-profit usage."
+ disclaimerText1: undefined,
+
+ // Original text: "If you are a company, it's better to use it with our appliance + pro support included:"
+ disclaimerText2: undefined,
+
+ // Original text: 'This version is not bundled with any support nor updates. Use it with caution for critical tasks.'
+ disclaimerText3: undefined,
+
+ // Original text: 'Connect PIF'
+ connectPif: undefined,
+
+ // Original text: 'Are you sure you want to connect this PIF?'
+ connectPifConfirm: undefined,
+
+ // Original text: 'Disconnect PIF'
+ disconnectPif: undefined,
+
+ // Original text: 'Are you sure you want to disconnect this PIF?'
+ disconnectPifConfirm: undefined,
+
+ // Original text: 'Delete PIF'
+ deletePif: undefined,
+
+ // Original text: 'Are you sure you want to delete this PIF?'
+ deletePifConfirm: undefined,
+
+ // Original text: 'Username'
+ username: undefined,
+
+ // Original text: 'Password'
+ password: undefined,
+
+ // Original text: 'Language'
+ language: undefined,
+
+ // Original text: 'Old password'
+ oldPasswordPlaceholder: undefined,
+
+ // Original text: 'New password'
+ newPasswordPlaceholder: undefined,
+
+ // Original text: 'Confirm new password'
+ confirmPasswordPlaceholder: undefined,
+
+ // Original text: 'Confirmation password incorrect'
+ confirmationPasswordError: undefined,
+
+ // Original text: 'Password does not match the confirm password.'
+ confirmationPasswordErrorBody: undefined,
+
+ // Original text: 'Password changed'
+ pwdChangeSuccess: undefined,
+
+ // Original text: 'Your password has been successfully changed.'
+ pwdChangeSuccessBody: undefined,
+
+ // Original text: 'Incorrect password'
+ pwdChangeError: undefined,
+
+ // Original text: 'The old password provided is incorrect. Your password has not been changed.'
+ pwdChangeErrorBody: undefined,
+
+ // Original text: 'OK'
+ changePasswordOk: undefined,
+
+ // Original text: 'SSH keys'
+ sshKeys: undefined,
+
+ // Original text: 'New SSH key'
+ newSshKey: undefined,
+
+ // Original text: 'Delete'
+ deleteSshKey: undefined,
+
+ // Original text: 'No SSH keys'
+ noSshKeys: undefined,
+
+ // Original text: 'New SSH key'
+ newSshKeyModalTitle: undefined,
+
+ // Original text: 'Invalid key'
+ sshKeyErrorTitle: undefined,
+
+ // Original text: 'An SSH key requires both a title and a key.'
+ sshKeyErrorMessage: undefined,
+
+ // Original text: 'Title'
+ title: undefined,
+
+ // Original text: 'Key'
+ key: undefined,
+
+ // Original text: 'Delete SSH key'
+ deleteSshKeyConfirm: undefined,
+
+ // Original text: 'Are you sure you want to delete the SSH key {title}?'
+ deleteSshKeyConfirmMessage: undefined,
+
+ // Original text: 'Others'
+ others: undefined,
+
+ // Original text: 'Loading logs…'
+ loadingLogs: undefined,
+
+ // Original text: 'User'
+ logUser: undefined,
+
+ // Original text: 'Method'
+ logMethod: undefined,
+
+ // Original text: 'Params'
+ logParams: undefined,
+
+ // Original text: 'Message'
+ logMessage: undefined,
+
+ // Original text: 'Error'
+ logError: undefined,
+
+ // Original text: 'Display details'
+ logDisplayDetails: undefined,
+
+ // Original text: 'Date'
+ logTime: undefined,
+
+ // Original text: 'No stack trace'
+ logNoStackTrace: undefined,
+
+ // Original text: 'No params'
+ logNoParams: undefined,
+
+ // Original text: 'Delete log'
+ logDelete: undefined,
+
+ // Original text: 'Delete all logs'
+ logDeleteAll: undefined,
+
+ // Original text: 'Delete all logs'
+ logDeleteAllTitle: undefined,
+
+ // Original text: 'Are you sure you want to delete all the logs?'
+ logDeleteAllMessage: undefined,
+
+ // Original text: 'Name'
+ ipPoolName: undefined,
+
+ // Original text: 'IPs'
+ ipPoolIps: undefined,
+
+ // Original text: 'IPs (e.g.: 1.0.0.12-1.0.0.17;1.0.0.23)'
+ ipPoolIpsPlaceholder: undefined,
+
+ // Original text: 'Networks'
+ ipPoolNetworks: undefined,
+
+ // Original text: 'No IP pools'
+ ipsNoIpPool: undefined,
+
+ // Original text: 'Create'
+ ipsCreate: undefined,
+
+ // Original text: 'Delete all IP pools'
+ ipsDeleteAllTitle: undefined,
+
+ // Original text: 'Are you sure you want to delete all the IP pools?'
+ ipsDeleteAllMessage: undefined,
+
+ // Original text: 'VIFs'
+ ipsVifs: undefined,
+
+ // Original text: 'Not used'
+ ipsNotUsed: undefined,
+
+ // Original text: 'Keyboard shortcuts'
+ shortcutModalTitle: undefined,
+
+ // Original text: 'Global'
+ shortcut_XoApp: undefined,
+
+ // Original text: 'Go to hosts list'
+ shortcut_GO_TO_HOSTS: undefined,
+
+ // Original text: 'Go to pools list'
+ shortcut_GO_TO_POOLS: undefined,
+
+ // Original text: 'Go to VMs list'
+ shortcut_GO_TO_VMS: undefined,
+
+ // Original text: 'Create a new VM'
+ shortcut_CREATE_VM: undefined,
+
+ // Original text: 'Unfocus field'
+ shortcut_UNFOCUS: undefined,
+
+ // Original text: 'Show shortcuts key bindings'
+ shortcut_HELP: undefined,
+
+ // Original text: 'Home'
+ shortcut_Home: undefined,
+
+ // Original text: 'Focus search bar'
+ shortcut_SEARCH: undefined,
+
+ // Original text: 'Next item'
+ shortcut_NAV_DOWN: undefined,
+
+ // Original text: 'Previous item'
+ shortcut_NAV_UP: undefined,
+
+ // Original text: 'Select item'
+ shortcut_SELECT: undefined,
+
+ // Original text: 'Open'
+ shortcut_JUMP_INTO: undefined,
+
+ // Original text: 'VM'
+ settingsAclsButtonTooltipVM: undefined,
+
+ // Original text: 'Hosts'
+ settingsAclsButtonTooltiphost: undefined,
+
+ // Original text: 'Pool'
+ settingsAclsButtonTooltippool: undefined,
+
+ // Original text: 'SR'
+ settingsAclsButtonTooltipSR: undefined,
+
+ // Original text: 'Network'
+ settingsAclsButtonTooltipnetwork: undefined,
+}
diff --git a/packages/xo-web/src/common/intl/locales/hu.js b/packages/xo-web/src/common/intl/locales/hu.js
new file mode 100644
index 000000000..711b33aa5
--- /dev/null
+++ b/packages/xo-web/src/common/intl/locales/hu.js
@@ -0,0 +1,3658 @@
+// See http://momentjs.com/docs/#/use-it/browserify/
+import 'moment/locale/hu'
+
+import reactIntlData from 'react-intl/locale-data/hu'
+import { addLocaleData } from 'react-intl'
+addLocaleData(reactIntlData)
+
+// ===================================================================
+
+export default {
+ // Original text: "Connecting"
+ statusConnecting: 'Kapcsolódás',
+
+ // Original text: "Disconnected"
+ statusDisconnected: 'Lekapcsolódva',
+
+ // Original text: "Loading…"
+ statusLoading: 'Töltés…',
+
+ // Original text: "Page not found"
+ errorPageNotFound: 'Oldal nem található',
+
+ // Original text: "no such item"
+ errorNoSuchItem: 'nincs ilyen eszköz',
+
+ // Original text: "Long click to edit"
+ editableLongClickPlaceholder: 'Kiszolgálószan kattints a szerkesztéshez',
+
+ // Original text: "Click to edit"
+ editableClickPlaceholder: 'Kattints a szerkesztéshez',
+
+ // Original text: "Browse files"
+ browseFiles: 'Fájlok böngészése',
+
+ // Original text: "OK"
+ alertOk: 'OK',
+
+ // Original text: "OK"
+ confirmOk: 'OK',
+
+ // Original text: "Cancel"
+ confirmCancel: 'Mégsem',
+
+ // Original text: "On error"
+ onError: 'Hiba',
+
+ // Original text: "Successful"
+ successful: 'Sikeres',
+
+ // Original text: "Copy to clipboard"
+ copyToClipboard: 'Másolás a vágólapra',
+
+ // Original text: "Master"
+ pillMaster: 'Master',
+
+ // Original text: "Home"
+ homePage: 'Kezdőlap',
+
+ // Original text: "VMs"
+ homeVmPage: 'VPS',
+
+ // Original text: "Hosts"
+ homeHostPage: 'Kiszolgáló',
+
+ // Original text: "Pools"
+ homePoolPage: 'Pool',
+
+ // Original text: "Templates"
+ homeTemplatePage: 'Sablon',
+
+ // Original text: "Storages"
+ homeSrPage: 'Adattároló',
+
+ // Original text: "Dashboard"
+ dashboardPage: 'Dashboard',
+
+ // Original text: "Overview"
+ overviewDashboardPage: 'Áttekintés',
+
+ // Original text: "Visualizations"
+ overviewVisualizationDashboardPage: 'Vizualizáció',
+
+ // Original text: "Statistics"
+ overviewStatsDashboardPage: 'Statisztikák',
+
+ // Original text: "Health"
+ overviewHealthDashboardPage: 'Állapot',
+
+ // Original text: "Self service"
+ selfServicePage: 'Pirvát Datacenter',
+
+ // Original text: "Backup"
+ backupPage: 'Mentés',
+
+ // Original text: "Jobs"
+ jobsPage: 'Jobok',
+
+ // Original text: "Updates"
+ updatePage: 'Frissítések',
+
+ // Original text: "Settings"
+ settingsPage: 'Beállítások',
+
+ // Original text: "Servers"
+ settingsServersPage: 'Szerverek',
+
+ // Original text: "Users"
+ settingsUsersPage: 'Felhasználók',
+
+ // Original text: "Groups"
+ settingsGroupsPage: 'Csoportok',
+
+ // Original text: "ACLs"
+ settingsAclsPage: 'Jogok',
+
+ // Original text: "Plugins"
+ settingsPluginsPage: 'Bővítményok',
+
+ // Original text: "Logs"
+ settingsLogsPage: 'Logok',
+
+ // Original text: "IPs"
+ settingsIpsPage: 'IP Címek',
+
+ // Original text: "Config"
+ settingsConfigPage: 'Beállítás',
+
+ // Original text: "About"
+ aboutPage: 'Információ',
+
+ // Original text: "About XO {xoaPlan}"
+ aboutXoaPlan: 'Liszensz: {xoaPlan}',
+
+ // Original text: "New"
+ newMenu: 'Új',
+
+ // Original text: "Tasks"
+ taskMenu: 'Feladatok',
+
+ // Original text: "Tasks"
+ taskPage: 'Feladatok',
+
+ // Original text: "VM"
+ newVmPage: 'VPS',
+
+ // Original text: "Storage"
+ newSrPage: 'Adattároló',
+
+ // Original text: "Server"
+ newServerPage: 'Szerver',
+
+ // Original text: "Import"
+ newImport: 'Importálás',
+
+ // Original text: 'XOSAN'
+ xosan: undefined,
+
+ // Original text: "Overview"
+ backupOverviewPage: 'Áttekintés',
+
+ // Original text: "New"
+ backupNewPage: 'Új',
+
+ // Original text: "Remotes"
+ backupRemotesPage: 'Távoli Mentés',
+
+ // Original text: "Restore"
+ backupRestorePage: 'Visszaállítás',
+
+ // Original text: "File restore"
+ backupFileRestorePage: 'Fájl alapú visszaállítás',
+
+ // Original text: "Schedule"
+ schedule: 'Időzítés',
+
+ // Original text: "New VM backup"
+ newVmBackup: 'Új VPS Mentés',
+
+ // Original text: "Edit VM backup"
+ editVmBackup: 'VPS Mentés Szerkesztése',
+
+ // Original text: "Backup"
+ backup: 'Adatmentés',
+
+ // Original text: "Rolling Snapshot"
+ rollingSnapshot: 'Felülíródó Pillanatkép Mentés',
+
+ // Original text: "Delta Backup"
+ deltaBackup: 'Delta Mentés',
+
+ // Original text: "Disaster Recovery"
+ disasterRecovery: 'Disaster Recovery',
+
+ // Original text: "Continuous Replication"
+ continuousReplication: 'Folyamatos Replikáció',
+
+ // Original text: "Overview"
+ jobsOverviewPage: 'Áttekintés',
+
+ // Original text: "New"
+ jobsNewPage: 'Új',
+
+ // Original text: "Scheduling"
+ jobsSchedulingPage: 'Időzítés',
+
+ // Original text: "Custom Job"
+ customJob: 'Egyedi Job',
+
+ // Original text: "User"
+ userPage: 'Felhasználó',
+
+ // Original text: "No support"
+ noSupport: 'Nincs szupport',
+
+ // Original text: "Free upgrade!"
+ freeUpgrade: 'Ingyenes bővítés!',
+
+ // Original text: "Sign out"
+ signOut: 'Kijelentkezés',
+
+ // Original text: "Edit my settings {username}"
+ editUserProfile: 'Felhasználóm szerkesztése {felhasználónév}',
+
+ // Original text: "Fetching data…"
+ homeFetchingData: 'Adatok betöltése…',
+
+ // Original text: "Welcome on Xen Orchestra!"
+ homeWelcome: 'Üdvözöljük a Felhőben!',
+
+ // Original text: "Add your XenServer hosts or pools"
+ 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',
+
+ // Original text: "Want some help?"
+ homeHelp: 'Segítségre van szüksége?',
+
+ // Original text: "Add server"
+ homeAddServer: 'Hozzáadása server',
+
+ // Original text: "Connect servers"
+ homeConnectServer: 'Csatlakozás servers',
+
+ // Original text: "Online Doc"
+ homeOnlineDoc: 'Online Doc',
+
+ // Original text: "Pro Support"
+ homeProSupport: 'Pro Support',
+
+ // Original text: "There are no VMs!"
+ homeNoVms: 'Nincsenek VPS-ek!',
+
+ // Original text: "Or…"
+ homeNoVmsOr: 'Vagy…',
+
+ // Original text: "Import VM"
+ homeImportVm: 'VPS Importálása',
+
+ // Original text: "Import an existing VM in xva format"
+ homeImportVmMessage: 'Meglévő VPS importálása xva formátumban',
+
+ // Original text: "Restore a backup"
+ homeRestoreBackup: 'Adatmentés visszaállítása',
+
+ // Original text: "Restore a backup from a remote store"
+ homeRestoreBackupMessage: 'Adatmentés visszaállítása távoli helyről',
+
+ // Original text: "This will create a new VM"
+ homeNewVmMessage: 'Új VPS készítése',
+
+ // Original text: "Filters"
+ homeFilters: 'Szűrők',
+
+ // Original text: "No results! Click here to reset your filters"
+ homeNoMatches: 'Nincs eredmény! Szűrők visszaállításához kattintson ide',
+
+ // Original text: "Pool"
+ homeTypePool: 'Pool',
+
+ // Original text: "Host"
+ homeTypeHost: 'Kiszolgáló',
+
+ // Original text: "VM"
+ homeTypeVm: 'VPS',
+
+ // Original text: "SR"
+ homeTypeSr: 'Adattároló',
+
+ // Original text: "Template"
+ homeTypeVmTemplate: 'Sablon',
+
+ // Original text: "Sort"
+ homeSort: 'Rendezés',
+
+ // Original text: "Pools"
+ homeAllPools: 'Poolok',
+
+ // Original text: "Hosts"
+ homeAllHosts: 'Kiszolgálók',
+
+ // Original text: "Tags"
+ homeAllTags: 'Címke',
+
+ // Original text: "New VM"
+ homeNewVm: 'Új VPS',
+
+ // Original text: 'None'
+ homeFilterNone: undefined,
+
+ // Original text: "Running hosts"
+ homeFilterRunningHosts: 'Futó kiszolgálók',
+
+ // Original text: "Disabled hosts"
+ homeFilterDisabledHosts: 'Leállított kiszolgálók',
+
+ // Original text: "Running VMs"
+ homeFilterRunningVms: 'Futó VPS szerverek',
+
+ // Original text: "Non running VMs"
+ homeFilterNonRunningVms: 'Nem futó VPS szerverek',
+
+ // Original text: "Pending VMs"
+ homeFilterPendingVms: 'Függőben lévő VPS szerverek',
+
+ // Original text: "HVM guests"
+ homeFilterHvmGuests: 'HVM guests',
+
+ // Original text: "Tags"
+ homeFilterTags: 'Címkék',
+
+ // Original text: "Sort by"
+ homeSortBy: 'Rendezés',
+
+ // Original text: "Name"
+ homeSortByName: 'Név',
+
+ // Original text: "Power state"
+ homeSortByPowerstate: 'Állapot',
+
+ // Original text: "RAM"
+ homeSortByRAM: 'RAM',
+
+ // Original text: "vCPUs"
+ homeSortByvCPUs: 'vCPUs',
+
+ // Original text: "CPUs"
+ homeSortByCpus: 'CPUs',
+
+ // Original text: "Shared/Not shared"
+ homeSortByShared: 'Osztott/Nem osztott',
+
+ // Original text: "Size"
+ homeSortBySize: 'Méret',
+
+ // Original text: "Usage"
+ homeSortByUsage: 'Használat',
+
+ // Original text: "Type"
+ homeSortByType: 'Típus',
+
+ // Original text: "{displayed, number}x {icon} (on {total, number})"
+ 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})',
+
+ // Original text: "More"
+ homeMore: 'Több',
+
+ // Original text: "Migrate to…"
+ homeMigrateTo: 'Migálás ide…',
+
+ // Original text: "Missing patches"
+ homeMissingPaths: 'Missing patches',
+
+ // Original text: "Master:"
+ homePoolMaster: 'Master:',
+
+ // Original text: "Resource set: {resourceSet}"
+ homeResourceSet: 'Resource set: {resourceSet}',
+
+ // Original text: "High Availability"
+ highAvailability: 'High Availability',
+
+ // Original text: "Shared {type}"
+ srSharedType: 'Osztott {type}',
+
+ // Original text: "Not shared {type}"
+ srNotSharedType: 'Nem osztott {type}',
+
+ // Original text: "Add"
+ add: 'Hozzáadás',
+
+ // Original text: "Select all"
+ selectAll: 'Mindet kijelöl',
+
+ // Original text: "Remove"
+ remove: 'Eltávolítás',
+
+ // Original text: "Preview"
+ preview: 'Előnézet',
+
+ // Original text: "Item"
+ item: 'Eszköz',
+
+ // Original text: "No selected value"
+ noSelectedValue: 'Nincs kiválasztott érték',
+
+ // Original text: "Choose user(s) and/or group(s)"
+ selectSubjects: 'Válasszon felhasználó(ka)t és/vagy csoporto(ka)t',
+
+ // Original text: "Select Object(s)…"
+ selectObjects: 'Objektum(ok) választása…',
+
+ // Original text: "Choose a role"
+ selectRole: 'Szerep választása',
+
+ // Original text: "Select Host(s)…"
+ selectHosts: 'Kiszolgáló(k) választása)…',
+
+ // Original text: "Select object(s)…"
+ selectHostsVms: 'Objektum(ok) választása…',
+
+ // Original text: "Select Network(s)…"
+ selectNetworks: 'Hálózat(ok) választása…',
+
+ // Original text: "Select PIF(s)…"
+ selectPifs: 'PIF választása…',
+
+ // Original text: "Select Pool(s)…"
+ selectPools: 'Pool(ok) választása…',
+
+ // Original text: "Select Remote(s)…"
+ selectRemotes: 'Válasszon Remote(s)…',
+
+ // Original text: "Select resource set(s)…"
+ selectResourceSets: 'Erőforrás készlet választása…',
+
+ // Original text: "Select template(s)…"
+ selectResourceSetsVmTemplate: 'Sablon(ok) választása…',
+
+ // Original text: "Select SR(s)…"
+ selectResourceSetsSr: 'Adattároló választása…',
+
+ // Original text: "Select network(s)…"
+ selectResourceSetsNetwork: 'Hálózat(ok) választása…',
+
+ // Original text: "Select disk(s)…"
+ selectResourceSetsVdi: 'Diszk(ek) választása…',
+
+ // Original text: "Select SSH key(s)…"
+ selectSshKey: 'SSH kulcs(ok) választása…',
+
+ // Original text: "Select SR(s)…"
+ selectSrs: 'Adattároló választása…',
+
+ // Original text: "Select VM(s)…"
+ selectVms: 'VPS(ek) választása…',
+
+ // Original text: "Select VM template(s)…"
+ selectVmTemplates: 'VPS sablon(ok)választása…',
+
+ // Original text: "Select tag(s)…"
+ selectTags: 'Címkék választása…',
+
+ // Original text: "Select disk(s)…"
+ selectVdis: 'Diszk(ek) választása…',
+
+ // Original text: "Select timezone…"
+ selectTimezone: 'Időzóna választása…',
+
+ // Original text: "Select IP(s)…"
+ selectIp: 'IP(k) választása…',
+
+ // Original text: "Select IP pool(s)…"
+ selectIpPool: 'IP tartomány(ok) választása…',
+
+ // Original text: "Fill required informations."
+ fillRequiredInformations: 'Szükséges információk kitöltése.',
+
+ // Original text: "Fill informations (optional)"
+ fillOptionalInformations: 'Információk kitöltése (nem kötelező)',
+
+ // Original text: "Reset"
+ selectTableReset: 'Visszaállítás',
+
+ // Original text: "Month"
+ schedulingMonth: 'Hónap',
+
+ // Original text: "Every N month"
+ schedulingEveryNMonth: 'Minden adott hónapban',
+
+ // Original text: "Each selected month"
+ schedulingEachSelectedMonth: 'Minden kiválasztott hónapban',
+
+ // Original text: "Day"
+ schedulingDay: 'Nap',
+
+ // Original text: "Every N day"
+ schedulingEveryNDay: 'Megadott naponként',
+
+ // Original text: "Each selected day"
+ schedulingEachSelectedDay: 'Minden kiválasztott napon',
+
+ // Original text: "Switch to week days"
+ schedulingSetWeekDayMode: 'Váltás a hét napjaira',
+
+ // Original text: "Switch to month days"
+ schedulingSetMonthDayMode: 'Váltás a hónap napjaira',
+
+ // Original text: "Hour"
+ schedulingHour: 'Óra',
+
+ // Original text: "Each selected hour"
+ schedulingEachSelectedHour: 'Minden kiválasztott órában',
+
+ // Original text: "Every N hour"
+ schedulingEveryNHour: 'Minden X órában',
+
+ // Original text: "Minute"
+ schedulingMinute: 'Perc',
+
+ // Original text: "Each selected minute"
+ schedulingEachSelectedMinute: 'Minden kiválasztott percben',
+
+ // Original text: "Every N minute"
+ schedulingEveryNMinute: 'Minden X-edik percben',
+
+ // Original text: "Every month"
+ selectTableAllMonth: 'Minden hónapban',
+
+ // Original text: "Every day"
+ selectTableAllDay: 'Minden napon',
+
+ // Original text: "Every hour"
+ selectTableAllHour: 'Minden órában',
+
+ // Original text: "Every minute"
+ selectTableAllMinute: 'Minden percben',
+
+ // Original text: "Reset"
+ schedulingReset: 'Visszaállítás',
+
+ // Original text: "Unknown"
+ unknownSchedule: 'Ismeretlen',
+
+ // Original text: "Web browser timezone"
+ timezonePickerUseLocalTime: 'Böngésző időzóna',
+
+ // Original text: "Server timezone ({value})"
+ serverTimezoneOption: 'Szerver időzóna ({Value})',
+
+ // Original text: "Cron Pattern:"
+ cronPattern: 'Cron példa:',
+
+ // Original text: "Cannot edit backup"
+ backupEditNotFoundTitle: 'Az adatmentés nem szerkeszthető',
+
+ // Original text: "Missing required info for edition"
+ backupEditNotFoundMessage: 'Hiányzó szükséges információ',
+
+ // Original text: "Job"
+ job: 'Feladat',
+
+ // Original text: 'Job {job}'
+ jobModalTitle: undefined,
+
+ // Original text: "ID"
+ jobId: 'Feladat Azonosító',
+
+ // Original text: 'Type'
+ jobType: undefined,
+
+ // Original text: "Name"
+ jobName: 'Név',
+
+ // Original text: "Name of your job (forbidden: \"_\")"
+ jobNamePlaceholder: 'Feladat neve (forbidden: "_")',
+
+ // Original text: "Start"
+ jobStart: 'Elindítás',
+
+ // Original text: "End"
+ jobEnd: 'Befejezés',
+
+ // Original text: "Duration"
+ jobDuration: 'Időtartam',
+
+ // Original text: "Status"
+ jobStatus: 'Státusz',
+
+ // Original text: "Action"
+ jobAction: 'Akció',
+
+ // Original text: "Tag"
+ jobTag: 'Címke',
+
+ // Original text: "Scheduling"
+ jobScheduling: 'Időzítés',
+
+ // Original text: "State"
+ jobState: 'Állapot',
+
+ // Original text: 'Enabled'
+ jobStateEnabled: undefined,
+
+ // Original text: 'Disabled'
+ jobStateDisabled: undefined,
+
+ // Original text: "Timezone"
+ jobTimezone: 'Időzóna',
+
+ // Original text: "Server"
+ jobServerTimezone: 'Szerver',
+
+ // Original text: "Run job"
+ 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.',
+
+ // Original text: "Started"
+ jobStarted: 'Elindítva',
+
+ // Original text: "Finished"
+ jobFinished: 'Befejezve',
+
+ // Original text: "Save"
+ saveBackupJob: 'Mentés',
+
+ // Original text: "Remove backup job"
+ 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?',
+
+ // 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.',
+
+ // 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.',
+
+ // Original text: "No scheduled jobs."
+ noScheduledJobs: 'Nincsenek időzített feladatok.',
+
+ // Original text: "No jobs found."
+ noJobs: 'Nincsenek feladatok.',
+
+ // Original text: "No schedules found"
+ noSchedules: 'Nincsenek időzítések',
+
+ // Original text: "Select a xo-server API command"
+ jobActionPlaceHolder: 'API parancs kiválasztása',
+
+ // Original text: "Schedules"
+ jobSchedules: 'Időzítések',
+
+ // Original text: "Name of your schedule"
+ jobScheduleNamePlaceHolder: 'Időzítés neve',
+
+ // Original text: "Select a Job"
+ jobScheduleJobPlaceHolder: 'Feladat kiválasztása',
+
+ // Original text: "Job owner"
+ jobOwnerPlaceholder: 'Feladat tulajdonosa',
+
+ // Original text: "This job's creator no longer exists"
+ 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',
+
+ // Original text: "Backup owner"
+ backupOwner: 'Mentés tulajdonosa',
+
+ // Original text: "Select your backup type:"
+ newBackupSelection: 'Válassza ki a mentési típust:',
+
+ // Original text: "Select backup mode:"
+ smartBackupModeSelection: 'Válassza ki a mentési módot:',
+
+ // Original text: "Normal backup"
+ normalBackup: 'Normál mentés',
+
+ // Original text: "Smart backup"
+ smartBackup: 'Okos mentés',
+
+ // Original text: "Local remote selected"
+ 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!.',
+
+ // 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!',
+
+ // Original text: "VMs"
+ editBackupVmsTitle: 'VPS-ek',
+
+ // Original text: "VMs statuses"
+ editBackupSmartStatusTitle: 'VPS sátuszok',
+
+ // Original text: "Resident on"
+ editBackupSmartResidentOn: 'Helye',
+
+ // Original text: "Pools"
+ editBackupSmartPools: 'Poolok',
+
+ // Original text: "Tags"
+ editBackupSmartTags: 'Cimkék',
+
+ // Original text: "VMs Tags"
+ editBackupSmartTagsTitle: 'VPS Cimkék',
+
+ // Original text: "Reverse"
+ editBackupNot: 'Reverse',
+
+ // Original text: "Tag"
+ editBackupTagTitle: 'Cimke',
+
+ // Original text: "Report"
+ editBackupReportTitle: 'Riport',
+
+ // Original text: "Enable immediately after creation"
+ editBackupScheduleEnabled: 'Azonnal a létrehozás után',
+
+ // Original text: "Depth"
+ editBackupDepthTitle: 'Mélység',
+
+ // Original text: "Remote"
+ editBackupRemoteTitle: 'Távoli',
+
+ // Original text: "Remote stores for backup"
+ remoteList: 'Távoli Mentési Helyek',
+
+ // Original text: "New File System Remote"
+ newRemote: 'Új Távoli Fájl Rendszer',
+
+ // Original text: "Local"
+ remoteTypeLocal: 'Helyi',
+
+ // Original text: "NFS"
+ remoteTypeNfs: 'NFS',
+
+ // Original text: "SMB"
+ remoteTypeSmb: 'SMB',
+
+ // Original text: "Type"
+ remoteType: 'Típus',
+
+ // Original text: "Test your remote"
+ remoteTestTip: 'Tesztelés',
+
+ // Original text: "Test Remote"
+ testRemote: 'Távoli Tesztelése',
+
+ // Original text: "Test failed for {name}"
+ remoteTestFailure: 'Sikertelen tesztelés!!! {név}',
+
+ // Original text: "Test passed for {name}"
+ remoteTestSuccess: 'Sikeres! {név}',
+
+ // Original text: "Error"
+ remoteTestError: 'Hiba',
+
+ // Original text: "Test Step"
+ remoteTestStep: 'Teszt Lépés',
+
+ // Original text: "Test file"
+ remoteTestFile: 'Teszt Fájl',
+
+ // Original text: 'Test name'
+ remoteTestName: undefined,
+
+ // Original text: 'Remote name already exists!'
+ remoteTestNameFailure: undefined,
+
+ // Original text: "The remote appears to work correctly"
+ remoteTestSuccessMessage: 'A távoli úgy tűnik megfelelően működik',
+
+ // Original text: 'Connection failed'
+ remoteConnectionFailed: undefined,
+
+ // Original text: "Name"
+ remoteName: 'Név',
+
+ // Original text: "Path"
+ remotePath: 'Útvonal',
+
+ // Original text: "State"
+ remoteState: 'Tátusz',
+
+ // Original text: "Device"
+ remoteDevice: 'Eszköz',
+
+ // Original text: "Share"
+ remoteShare: 'Megosztás',
+
+ // Original text: 'Action'
+ remoteAction: undefined,
+
+ // Original text: "Auth"
+ remoteAuth: 'Bejelentkezés',
+
+ // Original text: "Mounted"
+ remoteMounted: 'Felcsatolva',
+
+ // Original text: "Unmounted"
+ remoteUnmounted: 'Lecsatolva',
+
+ // Original text: "Connect"
+ remoteConnectTip: 'Csatlakozás',
+
+ // Original text: "Disconnect"
+ remoteDisconnectTip: 'Lecsatlakozás',
+
+ // Original text: 'Connected'
+ remoteConnected: undefined,
+
+ // Original text: 'Disconnected'
+ remoteDisconnected: undefined,
+
+ // Original text: "Delete"
+ remoteDeleteTip: 'Törlés',
+
+ // Original text: "remote name *"
+ remoteNamePlaceHolder: 'távoli név *',
+
+ // Original text: "Name *"
+ remoteMyNamePlaceHolder: 'Név *',
+
+ // Original text: "/path/to/backup"
+ remoteLocalPlaceHolderPath: '/path/to/backup',
+
+ // Original text: "host *"
+ remoteNfsPlaceHolderHost: 'kiszolgáló *',
+
+ // Original text: "path/to/backup"
+ remoteNfsPlaceHolderPath: 'path/to/backup',
+
+ // Original text: "subfolder [path\\to\\backup]"
+ remoteSmbPlaceHolderRemotePath: 'almappa [path\\to\\backup]',
+
+ // Original text: "Username"
+ remoteSmbPlaceHolderUsername: 'Felhasználónév',
+
+ // Original text: "Password"
+ remoteSmbPlaceHolderPassword: 'Jelszó',
+
+ // Original text: "Domain"
+ remoteSmbPlaceHolderDomain: 'Domain',
+
+ // Original text: "\\ *"
+ remoteSmbPlaceHolderAddressShare: '\\ *',
+
+ // Original text: "password(fill to edit)"
+ remotePlaceHolderPassword: 'jelszó(kattintson a módosításhoz)',
+
+ // Original text: "Create a new SR"
+ newSrTitle: 'Új Adattároló készítése',
+
+ // Original text: "General"
+ newSrGeneral: 'Általános',
+
+ // Original text: "Select Storage Type:"
+ newSrTypeSelection: 'Adattároló Típus Kiválasztása:',
+
+ // Original text: "Settings"
+ newSrSettings: 'Beállítások',
+
+ // Original text: "Storage Usage"
+ newSrUsage: 'Adattároló Használat',
+
+ // Original text: "Summary"
+ newSrSummary: 'Összesítés',
+
+ // Original text: "Host"
+ newSrHost: 'Kiszolgáló',
+
+ // Original text: "Type"
+ newSrType: 'Típus',
+
+ // Original text: "Name"
+ newSrName: 'Név',
+
+ // Original text: "Description"
+ newSrDescription: 'Leírás',
+
+ // Original text: "Server"
+ newSrServer: 'Szerver',
+
+ // Original text: "Path"
+ newSrPath: 'Útvonal',
+
+ // Original text: "IQN"
+ newSrIqn: 'IQN',
+
+ // Original text: "LUN"
+ newSrLun: 'LUN',
+
+ // Original text: "with auth."
+ newSrAuth: 'with auth.',
+
+ // Original text: "User Name"
+ newSrUsername: 'Felhasználónév',
+
+ // Original text: "Password"
+ newSrPassword: 'Jelszó',
+
+ // Original text: "Device"
+ newSrDevice: 'Eszköz',
+
+ // Original text: "in use"
+ newSrInUse: 'használatban',
+
+ // Original text: "Size"
+ newSrSize: 'Méret',
+
+ // Original text: "Create"
+ newSrCreate: 'Létrehozás',
+
+ // Original text: "Storage name"
+ newSrNamePlaceHolder: 'Adattároló név',
+
+ // Original text: "Storage description"
+ newSrDescPlaceHolder: 'Adattároló leírása',
+
+ // Original text: "Address"
+ newSrAddressPlaceHolder: 'Cím',
+
+ // Original text: "[port]"
+ newSrPortPlaceHolder: '[port]',
+
+ // Original text: "Username"
+ newSrUsernamePlaceHolder: 'Felhasználónév',
+
+ // Original text: "Password"
+ newSrPasswordPlaceHolder: 'Jelszó',
+
+ // Original text: "Device, e.g /dev/sda…"
+ newSrLvmDevicePlaceHolder: 'Eszköz, pl.: /dev/sda…',
+
+ // Original text: "/path/to/directory"
+ newSrLocalPathPlaceHolder: '/path/to/directory',
+
+ // Original text: "Users/Groups"
+ subjectName: 'Felhasználók/Csoportok',
+
+ // Original text: "Object"
+ objectName: 'Objektum',
+
+ // Original text: "No acls found"
+ aclNoneFound: 'Jogosultság nem található',
+
+ // Original text: "Role"
+ roleName: 'Szerepkör',
+
+ // Original text: "Create"
+ aclCreate: 'Létrehozás',
+
+ // Original text: "New Group Name"
+ newGroupName: 'Új Csoport Név',
+
+ // Original text: "Create Group"
+ createGroup: 'Csoport Létrehozás',
+
+ // Original text: "Create"
+ createGroupButton: 'Létrehozás',
+
+ // Original text: "Delete Group"
+ deleteGroup: 'Csoport Törlés',
+
+ // Original text: "Are you sure you want to delete this group?"
+ deleteGroupConfirm: 'Biztos benne, hogy törli a csoportot?',
+
+ // Original text: "Remove user from Group"
+ removeUserFromGroup: 'Felhasználó törlése a csoportból',
+
+ // Original text: "Are you sure you want to delete this user?"
+ deleteUserConfirm: 'Biztos benne, hogy törli a felhasználót?',
+
+ // Original text: "Delete User"
+ deleteUser: 'Felhasználó Törlése',
+
+ // Original text: "no user"
+ noUser: 'no felhasználó',
+
+ // Original text: "unknown user"
+ unknownUser: 'ismeretlen felhasználó',
+
+ // Original text: "No group found"
+ noGroupFound: 'Csoport nem található',
+
+ // Original text: "Name"
+ groupNameColumn: 'Név',
+
+ // Original text: "Users"
+ groupUsersColumn: 'Felhasználók',
+
+ // Original text: "Add User"
+ addUserToGroupColumn: 'Felhasználó Hozzáadása',
+
+ // Original text: "Email"
+ userNameColumn: 'E-mail',
+
+ // Original text: "Permissions"
+ userPermissionColumn: 'Jogosultságok',
+
+ // Original text: "Password"
+ userPasswordColumn: 'Jelszó',
+
+ // Original text: "Email"
+ userName: 'E-mail',
+
+ // Original text: "Password"
+ userPassword: 'Jelszó',
+
+ // Original text: "Create"
+ createUserButton: 'Létrehozás',
+
+ // Original text: "No user found"
+ noUserFound: 'Felhasználó nem található',
+
+ // Original text: "User"
+ userLabel: 'Felhasználó',
+
+ // Original text: "Admin"
+ adminLabel: 'Admin',
+
+ // Original text: "No user in group"
+ noUserInGroup: 'Nincs felhasználó a csoportban',
+
+ // Original text: "{users} user{users, plural, one {} other {s}}"
+ countUsers: '{user} felhasználó{users, plural, one {} other {s}}',
+
+ // Original text: "Select Permission"
+ selectPermission: 'Válasszon Jogosultságot',
+
+ // Original text: "No plugins found"
+ noPlugins: 'Bővítményok nem találhatóak',
+
+ // Original text: "Auto-load at server start"
+ autoloadPlugin: 'Automatikus betöltés szerver indításakor',
+
+ // Original text: "Save configuration"
+ savePluginConfiguration: 'Beállítás Mentése',
+
+ // Original text: "Delete configuration"
+ deletePluginConfiguration: 'Beállítás Törlése',
+
+ // Original text: "Plugin error"
+ pluginError: 'Bővítmény hiba',
+
+ // Original text: "Unknown error"
+ unknownPluginError: 'Ismeretlen hiba',
+
+ // Original text: "Purge plugin configuration"
+ purgePluginConfiguration: 'Bővítmény beállítás törlése',
+
+ // Original text: "Are you sure you want to purge this configuration ?"
+ purgePluginConfigurationQuestion: 'Biztos benne hogy törli ezt a beállítást?',
+
+ // Original text: "Edit"
+ editPluginConfiguration: 'Szerkesztés',
+
+ // Original text: "Cancel"
+ cancelPluginEdition: 'Mégsem',
+
+ // Original text: "Plugin configuration"
+ pluginConfigurationSuccess: 'Bővítmény beállítás',
+
+ // Original text: "Plugin configuration successfully saved!"
+ pluginConfigurationChanges: 'Bővítmény beállítás sikeresen mentve!',
+
+ // Original text: "Predefined configuration"
+ pluginConfigurationPresetTitle: 'Előre definiált konfiguráció',
+
+ // Original text: "Choose a predefined configuration."
+ pluginConfigurationChoosePreset: 'Előre definiált konfiguráció választása.',
+
+ // Original text: "Apply"
+ applyPluginPreset: 'Mehet',
+
+ // Original text: "Save filter error"
+ saveNewUserFilterErrorTitle: 'Szűrő mentés hiba',
+
+ // Original text: "Bad parameter: name must be given."
+ saveNewUserFilterErrorBody: 'Rossz paraméter: név megadása kötelező.',
+
+ // Original text: "Name:"
+ filterName: 'Név:',
+
+ // Original text: "Value:"
+ filterValue: 'Érték:',
+
+ // Original text: "Save new filter"
+ saveNewFilterTitle: 'Új szűrő mentése',
+
+ // Original text: "Set custom filters"
+ setUserFiltersTitle: 'Egyedi szűrők beállítása',
+
+ // Original text: "Are you sure you want to set custom filters?"
+ setUserFiltersBody: 'Biztos benne, hogy beállítja az egyedi szűrőket?',
+
+ // Original text: "Remove custom filter"
+ removeUserFilterTitle: 'Egyedi szűrő eltávolítása',
+
+ // Original text: "Are you sure you want to remove custom filter?"
+ removeUserFilterBody: 'Biztos benne, hogy eltávolítja az egyedi szűrőt?',
+
+ // Original text: "Default filter"
+ defaultFilter: 'Alapértelmezett szűrő',
+
+ // Original text: "Default filters"
+ defaultFilters: 'Alapértelmezett szűrők',
+
+ // Original text: "Custom filters"
+ customFilters: 'Egyedi szűrők',
+
+ // Original text: "Customize filters"
+ customizeFilters: 'Szűrők testre szabása',
+
+ // Original text: "Save custom filters"
+ saveCustomFilters: 'Egyedi szűrők mentése',
+
+ // Original text: "Start"
+ startVmLabel: 'Elindít',
+
+ // Original text: "Recovery start"
+ recoveryModeLabel: 'Helyreállítás Elindítása',
+
+ // Original text: "Suspend"
+ suspendVmLabel: 'Felfüggesztés',
+
+ // Original text: "Stop"
+ stopVmLabel: 'Leállítás',
+
+ // Original text: "Force shutdown"
+ forceShutdownVmLabel: 'Leállítás Kényszerítése',
+
+ // Original text: "Reboot"
+ rebootVmLabel: 'Újraindítás',
+
+ // Original text: "Force reboot"
+ forceRebootVmLabel: 'Újraindítás Kényszerítése',
+
+ // Original text: "Delete"
+ deleteVmLabel: 'Törlés',
+
+ // Original text: "Migrate"
+ migrateVmLabel: 'Migrálás',
+
+ // Original text: "Snapshot"
+ snapshotVmLabel: 'Pillanatkép',
+
+ // Original text: "Export"
+ exportVmLabel: 'Exportálás',
+
+ // Original text: "Resume"
+ resumeVmLabel: 'Folytatás',
+
+ // Original text: "Copy"
+ copyVmLabel: 'Másolás',
+
+ // Original text: "Clone"
+ cloneVmLabel: 'Klónozás',
+
+ // Original text: "Fast clone"
+ fastCloneVmLabel: 'Gyors Klónozás',
+
+ // Original text: "Convert to template"
+ convertVmToTemplateLabel: 'Sablonná konvertálás',
+
+ // Original text: "Console"
+ vmConsoleLabel: 'Konzol',
+
+ // Original text: "Rescan all disks"
+ srRescan: 'Összes diszk újraszkennelése',
+
+ // Original text: "Connect to all hosts"
+ srReconnectAll: 'Csatlakoztatás az összes kiszolgálóhoz',
+
+ // Original text: "Disconnect from all hosts"
+ srDisconnectAll: 'Lecsatlakoztatás az összes kiszolgálóról',
+
+ // Original text: "Forget this SR"
+ srForget: 'Adattároló Elfelejtése',
+
+ // Original text: "Forget SRs"
+ srsForget: 'Adattároló Elfelejtése',
+
+ // Original text: "Remove this SR"
+ srRemoveButton: 'Adattároló Eltávolítás',
+
+ // Original text: "No VDIs in this storage"
+ srNoVdis: 'No VDIs in this storage',
+
+ // Original text: "Pool RAM usage:"
+ poolTitleRamUsage: 'Pool RAM használat:',
+
+ // Original text: "{used} used on {total}"
+ poolRamUsage: '{used} used on {total}',
+
+ // Original text: "Master:"
+ poolMaster: 'Master:',
+
+ // Original text: "Display all hosts of this pool"
+ displayAllHosts: 'Display all kiszolgálók of this pool',
+
+ // Original text: "Display all storages of this pool"
+ displayAllStorages: 'Display all storages of this pool',
+
+ // Original text: "Display all VMs of this pool"
+ displayAllVMs: 'Display all VMs of this pool',
+
+ // Original text: "Hosts"
+ hostsTabName: 'Kiszolgálók',
+
+ // Original text: 'Vms'
+ vmsTabName: undefined,
+
+ // Original text: 'Srs'
+ srsTabName: undefined,
+
+ // Original text: "High Availability"
+ poolHaStatus: 'High Availability',
+
+ // Original text: "Enabled"
+ poolHaEnabled: 'Bekapcsolva',
+
+ // Original text: "Disabled"
+ poolHaDisabled: 'Kikapcsolva',
+
+ // Original text: "Name"
+ hostNameLabel: 'Név',
+
+ // Original text: "Description"
+ hostDescription: 'Leírás',
+
+ // Original text: "Memory"
+ hostMemory: 'Memória',
+
+ // Original text: "No hosts"
+ noHost: 'Nincsenek kiszolgálók',
+
+ // Original text: "{used}% used ({free} free)"
+ memoryLeftTooltip: '{used}% used ({free} free)',
+
+ // Original text: "PIF"
+ pif: 'PIF',
+
+ // Original text: "Name"
+ poolNetworkNameLabel: 'Név',
+
+ // Original text: "Description"
+ poolNetworkDescription: 'Leírás',
+
+ // Original text: "PIFs"
+ poolNetworkPif: 'PIFs',
+
+ // Original text: "No networks"
+ poolNoNetwork: 'Nincsenek hálózatok',
+
+ // Original text: "MTU"
+ poolNetworkMTU: 'MTU',
+
+ // Original text: "Connected"
+ poolNetworkPifAttached: 'Kapcsolódva',
+
+ // Original text: "Disconnected"
+ poolNetworkPifDetached: 'Lekapcsolódva',
+
+ // Original text: "Show PIFs"
+ showPifs: 'Show PIFs',
+
+ // Original text: "Hide PIFs"
+ hidePifs: 'Hide PIFs',
+
+ // Original text: "Show details"
+ showDetails: 'Részletek mutatása',
+
+ // Original text: "Hide details"
+ hideDetails: 'Részletek elrejtése',
+
+ // Original text: "No stats"
+ poolNoStats: 'Nincsenek statisztikák',
+
+ // Original text: "All hosts"
+ poolAllHosts: 'Minden kiszolgáló',
+
+ // Original text: "Add SR"
+ addSrLabel: 'Adattároló Hozzáadása',
+
+ // Original text: "Add VM"
+ addVmLabel: 'VPS Hozzáadása',
+
+ // Original text: "Add Host"
+ addHostLabel: 'Kiszolgáló Hozzáadása',
+
+ // Original text: "Disconnect"
+ disconnectServer: 'Lecsatol',
+
+ // Original text: "Start"
+ startHostLabel: 'Elindítás',
+
+ // Original text: "Stop"
+ stopHostLabel: 'Leállítás',
+
+ // Original text: "Enable"
+ enableHostLabel: 'Bekapcsol',
+
+ // Original text: "Disable"
+ disableHostLabel: 'Kikapcsol',
+
+ // Original text: "Restart toolstack"
+ restartHostAgent: 'Toolstack újraindítása',
+
+ // Original text: "Force reboot"
+ forceRebootHostLabel: 'Újraindítás Kényszerítése',
+
+ // Original text: "Reboot"
+ rebootHostLabel: 'Újraindítás',
+
+ // Original text: "Error while restarting host"
+ 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.',
+
+ // 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.',
+
+ // Original text: "Reboot to apply updates"
+ rebootUpdateHostLabel:
+ 'A változtatások életbe lépéséhez újraindítás szükséges',
+
+ // Original text: "Emergency mode"
+ emergencyModeLabel: 'Vészhelyzet üzem',
+
+ // Original text: "Storage"
+ storageTabName: 'Adattároló',
+
+ // Original text: "Patches"
+ patchesTabName: 'Javítások',
+
+ // Original text: "Load average"
+ statLoad: 'Átlagos load',
+
+ // Original text: "RAM Usage: {memoryUsed}"
+ memoryHostState: 'Memória használat: {MemoryUsed}',
+
+ // Original text: "Hardware"
+ hardwareHostSettingsLabel: 'Hardver',
+
+ // Original text: "Address"
+ hostAddress: 'Cím',
+
+ // Original text: "Status"
+ hostStatus: 'Állapot',
+
+ // Original text: "Build number"
+ hostBuildNumber: 'Build number',
+
+ // Original text: "iSCSI name"
+ hostIscsiName: 'iSCSI név',
+
+ // Original text: "Version"
+ hostXenServerVersion: 'Verzió',
+
+ // Original text: "Enabled"
+ hostStatusEnabled: 'Bekapcsolva',
+
+ // Original text: "Disabled"
+ hostStatusDisabled: 'Kikapcsolva',
+
+ // Original text: "Power on mode"
+ hostPowerOnMode: 'Power on mode',
+
+ // Original text: "Host uptime"
+ hostStartedSince: 'Kiszolgáló uptime',
+
+ // Original text: "Toolstack uptime"
+ hostStackStartedSince: 'Toolstack uptime',
+
+ // Original text: "CPU model"
+ hostCpusModel: 'CPU model',
+
+ // Original text: "Core (socket)"
+ hostCpusNumber: 'Core (socket)',
+
+ // Original text: "Manufacturer info"
+ hostManufacturerinfo: 'Gyártó infó',
+
+ // Original text: "BIOS info"
+ hostBiosinfo: 'BIOS infó',
+
+ // Original text: "Licence"
+ licenseHostSettingsLabel: 'Liszensz',
+
+ // Original text: "Type"
+ hostLicenseType: 'Típus',
+
+ // Original text: "Socket"
+ hostLicenseSocket: 'Foglalat',
+
+ // Original text: "Expiry"
+ hostLicenseExpiry: 'Lejárat',
+
+ // Original text: "Installed supplemental packs"
+ supplementalPacks: 'Installed supplemental packs',
+
+ // Original text: "Install new supplemental pack"
+ supplementalPackNew: 'Install new supplemental pack',
+
+ // Original text: "Install supplemental pack on every host"
+ supplementalPackPoolNew: 'Install supplemental pack on every host',
+
+ // Original text: "{name} (by {author})"
+ supplementalPackTitle: '{név} (by {author})',
+
+ // Original text: "Installation started"
+ supplementalPackInstallStartedTitle: 'Installation Started',
+
+ // 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.',
+
+ // Original text: "Installation success"
+ supplementalPackInstallSuccessTitle: 'Installation success',
+
+ // Original text: "Supplemental pack successfully installed."
+ supplementalPackInstallSuccessMessage:
+ 'Supplemental pack successfully installed.',
+
+ // Original text: "Add a network"
+ networkCreateButton: 'Add a Hálózat',
+
+ // Original text: "Add a bonded network"
+ networkCreateBondedButton: 'Add a bonded Hálózat',
+
+ // Original text: "Device"
+ pifDeviceLabel: 'Eszköz',
+
+ // Original text: "Network"
+ pifNetworkLabel: 'Hálózat',
+
+ // Original text: "VLAN"
+ pifVlanLabel: 'VLAN',
+
+ // Original text: "Address"
+ pifAddressLabel: 'Cím',
+
+ // Original text: "Mode"
+ pifModeLabel: 'Mode',
+
+ // Original text: "MAC"
+ pifMacLabel: 'MAC',
+
+ // Original text: "MTU"
+ pifMtuLabel: 'MTU',
+
+ // Original text: "Status"
+ pifStatusLabel: 'Állapot',
+
+ // Original text: "Connected"
+ pifStatusConnected: 'Kapcsolódva',
+
+ // Original text: "Disconnected"
+ pifStatusDisconnected: 'Lekapcsolódva',
+
+ // Original text: "No physical interface detected"
+ pifNoInterface: 'No physical interface detected',
+
+ // Original text: "This interface is currently in use"
+ pifInUse: 'This interface is currently in use',
+
+ // Original text: "Default locking mode"
+ defaultLockingMode: 'Alapértelmezett locking üzem',
+
+ // Original text: "Configure IP address"
+ pifConfigureIp: 'Configure IP cím',
+
+ // Original text: "Invalid parameters"
+ configIpErrorTitle: 'Invalid parameters',
+
+ // Original text: "IP address and netmask required"
+ configIpErrorMessage: 'IP cím and netmask required',
+
+ // Original text: "Static IP address"
+ staticIp: 'Static IP cím',
+
+ // Original text: "Netmask"
+ netmask: 'Netmask',
+
+ // Original text: "DNS"
+ dns: 'DNS',
+
+ // Original text: "Gateway"
+ gateway: 'Gateway',
+
+ // Original text: "Add a storage"
+ addSrDeviceButton: 'Add a storage',
+
+ // Original text: "Name"
+ srNameLabel: 'Név',
+
+ // Original text: "Type"
+ srType: 'Típus',
+
+ // Original text: 'Action'
+ pbdAction: undefined,
+
+ // Original text: "Status"
+ pbdStatus: 'Állapot',
+
+ // Original text: "Connected"
+ pbdStatusConnected: 'Kapcsolódva',
+
+ // Original text: "Disconnected"
+ pbdStatusDisconnected: 'Lekapcsolódva',
+
+ // Original text: "Connect"
+ pbdConnect: 'Csatlakozás',
+
+ // Original text: "Disconnect"
+ pbdDisconnect: 'Lecsatlakozás',
+
+ // Original text: "Forget"
+ pbdForget: 'Elfelejt',
+
+ // Original text: "Shared"
+ srShared: 'Megosztva',
+
+ // Original text: "Not shared"
+ srNotShared: 'Nincs megosztva',
+
+ // Original text: "No storage detected"
+ pbdNoSr: 'No storage detected',
+
+ // Original text: "Name"
+ patchNameLabel: 'Név',
+
+ // Original text: "Install all patches"
+ patchUpdateButton: 'Install all patches',
+
+ // Original text: "Description"
+ patchDescription: 'Leírás',
+
+ // Original text: "Applied date"
+ patchApplied: 'Applied Dátum',
+
+ // Original text: "Size"
+ patchSize: 'Méret',
+
+ // Original text: "Status"
+ patchStatus: 'Állapot',
+
+ // Original text: "Applied"
+ patchStatusApplied: 'Applied',
+
+ // Original text: "Missing patches"
+ patchStatusNotApplied: 'Missing patches',
+
+ // Original text: "No patch detected"
+ patchNothing: 'No patch detected',
+
+ // Original text: "Release date"
+ patchReleaseDate: 'Release Dátum',
+
+ // Original text: "Guidance"
+ patchGuidance: 'Guidance',
+
+ // Original text: "Action"
+ patchAction: 'Művelet',
+
+ // Original text: "Applied patches"
+ hostAppliedPatches: 'Applied patches',
+
+ // Original text: "Missing patches"
+ hostMissingPatches: 'Missing patches',
+
+ // Original text: "Host up-to-date!"
+ hostUpToDate: 'Host up-to-Dátum!',
+
+ // Original text: "Refresh patches"
+ refreshPatches: 'Refresh patches',
+
+ // Original text: "Install pool patches"
+ installPoolPatches: 'Install pool patches',
+
+ // Original text: "Default SR"
+ defaultSr: 'Alapértelmezett Adattároló',
+
+ // Original text: "Set as default SR"
+ setAsDefaultSr: 'Beállítás Alapértelmezett Adattárolóként',
+
+ // Original text: "General"
+ generalTabName: 'Általános',
+
+ // Original text: "Stats"
+ statsTabName: 'Statisztikák',
+
+ // Original text: "Console"
+ consoleTabName: 'Konzol',
+
+ // Original text: "Container"
+ containersTabName: 'Container',
+
+ // Original text: "Snapshots"
+ snapshotsTabName: 'Pillanatképek',
+
+ // Original text: "Logs"
+ logsTabName: 'Logok',
+
+ // Original text: "Advanced"
+ advancedTabName: 'Haladó',
+
+ // Original text: "Network"
+ networkTabName: 'Hálózat',
+
+ // Original text: "Disk{disks, plural, one {} other {s}}"
+ disksTabName: 'Lemez{disks, plural, one {} other {s}}',
+
+ // Original text: "halted"
+ powerStateHalted: 'leállítva',
+
+ // Original text: "running"
+ powerStateRunning: 'fut',
+
+ // Original text: "suspended"
+ powerStateSuspended: 'Felfüggesztett',
+
+ // Original text: "No Xen tools detected"
+ vmStatus: 'No Xen tools detected',
+
+ // Original text: "No IPv4 record"
+ vmName: 'No IPv4 record',
+
+ // Original text: "No IP record"
+ vmDescription: 'No IP record',
+
+ // Original text: "Started {ago}"
+ vmSettings: 'Elindítva {ago}',
+
+ // Original text: "Current status:"
+ vmCurrentStatus: 'Jelenlegi állapot:',
+
+ // Original text: "Not running"
+ vmNotRunning: 'Nem fut',
+
+ // Original text: "No Xen tools detected"
+ noToolsDetected: 'No Xen tools detected',
+
+ // Original text: "No IPv4 record"
+ noIpv4Record: 'No IPv4 record',
+
+ // Original text: "No IP record"
+ noIpRecord: 'No IP record',
+
+ // Original text: "Started {ago}"
+ started: 'Elindítva {ago}',
+
+ // Original text: "Paravirtualization (PV)"
+ paraVirtualizedMode: 'Paravirtualization (PV)',
+
+ // Original text: "Hardware virtualization (HVM)"
+ hardwareVirtualizedMode: 'Hardware virtualization (HVM)',
+
+ // Original text: "CPU usage"
+ statsCpu: 'CPU használat',
+
+ // Original text: "Memory usage"
+ statsMemory: 'Memória használat',
+
+ // Original text: "Network throughput"
+ statsNetwork: 'Átmenő forgalom',
+
+ // Original text: "Stacked values"
+ useStackedValuesOnStats: 'Halmozott Értékek',
+
+ // Original text: "Disk throughput"
+ statDisk: 'Átmenő Diszk Érték',
+
+ // Original text: "Last 10 minutes"
+ statLastTenMinutes: 'Utóbbi 10 perc',
+
+ // Original text: "Last 2 hours"
+ statLastTwoHours: 'Utóbbi 2 óra',
+
+ // Original text: "Last week"
+ statLastWeek: 'Utóbbi 1 hét',
+
+ // Original text: "Last year"
+ statLastYear: 'Utóbbi 1 év',
+
+ // Original text: "Copy"
+ copyToClipboardLabel: 'Másolás',
+
+ // Original text: "Ctrl+Alt+Del"
+ ctrlAltDelButtonLabel: 'Ctrl+Alt+Del',
+
+ // Original text: "Tip:"
+ 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.',
+
+ // Original text: "Hide infos"
+ hideHeaderTooltip: 'Információk elrejtése',
+
+ // Original text: "Show infos"
+ showHeaderTooltip: 'Információk mutatása',
+
+ // Original text: "Name"
+ containerName: 'Név',
+
+ // Original text: "Command"
+ containerCommand: 'Parancs',
+
+ // Original text: "Creation date"
+ containerCreated: 'Létrehozás dátuma',
+
+ // Original text: "Status"
+ containerStatus: 'Állapot',
+
+ // Original text: "Action"
+ containerAction: 'Akció',
+
+ // Original text: "No existing containers"
+ noContainers: 'Jelenleg nincsenek Konténerek',
+
+ // Original text: "Stop this container"
+ containerStop: 'Konténer Leállítása',
+
+ // Original text: "Start this container"
+ containerStart: 'Konténer Elindítása',
+
+ // Original text: "Pause this container"
+ containerPause: 'Konténer Szüneteltetése',
+
+ // Original text: "Resume this container"
+ containerResume: 'Konténer Folytatása',
+
+ // Original text: "Restart this container"
+ containerRestart: 'Konténer Újraindítása',
+
+ // Original text: "Action"
+ vdiAction: 'Művelet',
+
+ // Original text: "Attach disk"
+ vdiAttachDeviceButton: 'Diszk Hozzácsatolás',
+
+ // Original text: "New disk"
+ vbdCreateDeviceButton: 'Új diszk',
+
+ // Original text: "Boot order"
+ vdiBootOrder: 'Boot sorrend',
+
+ // Original text: "Name"
+ vdiNameLabel: 'Név',
+
+ // Original text: "Description"
+ vdiNameDescription: 'Leírás',
+
+ // Original text: "Tags"
+ vdiTags: 'Cimkék',
+
+ // Original text: "Size"
+ vdiSize: 'Méret',
+
+ // Original text: "SR"
+ vdiSr: 'Adattároló',
+
+ // Original text: "VM"
+ vdiVm: 'VPS',
+
+ // Original text: "Migrate VDI"
+ vdiMigrate: 'Migrate VDI',
+
+ // Original text: "Destination SR:"
+ vdiMigrateSelectSr: 'Cél Adattároló:',
+
+ // Original text: "Migrate all VDIs"
+ vdiMigrateAll: 'Migrate all VDIs',
+
+ // Original text: "No SR"
+ vdiMigrateNoSr: 'Nincs Adattároló',
+
+ // Original text: "A target SR is required to migrate a VDI"
+ vdiMigrateNoSrMessage: 'Cél Adattároló szükséges a VDI migáláshoz',
+
+ // Original text: "Forget"
+ vdiForget: 'Elfelejt',
+
+ // Original text: "Remove VDI"
+ vdiRemove: 'VDI Eltávolítás',
+
+ // Original text: "Boot flag"
+ vbdBootableStatus: 'Boot flag',
+
+ // Original text: "Status"
+ vbdStatus: 'Állapot',
+
+ // Original text: "Connected"
+ vbdStatusConnected: 'Kapcsolódva',
+
+ // Original text: "Disconnected"
+ vbdStatusDisconnected: 'Lekapcsolódva',
+
+ // Original text: "No disks"
+ vbdNoVbd: 'Nincsenek Diszkek',
+
+ // Original text: "Connect VBD"
+ vbdConnect: 'VBD Csatlakozás',
+
+ // Original text: "Disconnect VBD"
+ vbdDisconnect: 'VBD Lecsatlakozás',
+
+ // Original text: "Bootable"
+ vbdBootable: 'Bootolható',
+
+ // Original text: "Readonly"
+ vbdReadonly: 'Csak olvasható',
+
+ // Original text: 'Action'
+ vbdAction: undefined,
+
+ // Original text: "Create"
+ vbdCreate: 'Létrehozás',
+
+ // Original text: "Disk name"
+ vbdNamePlaceHolder: 'Diszk név',
+
+ // Original text: "Size"
+ vbdSizePlaceHolder: 'Méret',
+
+ // Original text: "Save"
+ saveBootOption: 'Mentés',
+
+ // Original text: "Reset"
+ resetBootOption: 'Visszaállítás',
+
+ // Original text: "New device"
+ vifCreateDeviceButton: 'Új eszköz',
+
+ // Original text: "No interface"
+ vifNoInterface: 'Nincs interface',
+
+ // Original text: "Device"
+ vifDeviceLabel: 'Eszköz',
+
+ // Original text: "MAC address"
+ vifMacLabel: 'MAC cím',
+
+ // Original text: "MTU"
+ vifMtuLabel: 'MTU',
+
+ // Original text: "Network"
+ vifNetworkLabel: 'Hálózat',
+
+ // Original text: "Status"
+ vifStatusLabel: 'Állapot',
+
+ // Original text: "Connected"
+ vifStatusConnected: 'Kapcsolódva',
+
+ // Original text: "Disconnected"
+ vifStatusDisconnected: 'Lekapcsolódva',
+
+ // Original text: "Connect"
+ vifConnect: 'Csatlakozás',
+
+ // Original text: "Disconnect"
+ vifDisconnect: 'Lecsatlakozás',
+
+ // Original text: "Remove"
+ vifRemove: 'Eltávolítás',
+
+ // Original text: "IP addresses"
+ vifIpAddresses: 'IP címek',
+
+ // Original text: "Auto-generated if empty"
+ vifMacAutoGenerate: 'Auto-generálás ha üres',
+
+ // Original text: "Allowed IPs"
+ vifAllowedIps: 'Engedélyezett IP címek',
+
+ // Original text: "No IPs"
+ vifNoIps: 'Nincsenek IP címek',
+
+ // Original text: "Network locked"
+ 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',
+
+ // Original text: "Network not locked"
+ vifUnLockedNetwork: 'Hálózat nincs zárolva',
+
+ // Original text: "Unknown network"
+ vifUnknownNetwork: 'Ismeretlen Hálózat',
+
+ // Original text: 'Action'
+ vifAction: undefined,
+
+ // Original text: "Create"
+ vifCreate: 'Létrehozás',
+
+ // Original text: "No snapshots"
+ noSnapshots: 'Nincsenek Pillanatképek',
+
+ // Original text: "New snapshot"
+ 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!',
+
+ // Original text: "Revert VM to this snapshot"
+ revertSnapshot: 'VPS visszaállítása erre a pillanatképre',
+
+ // Original text: "Remove this snapshot"
+ deleteSnapshot: 'Pillanatkép eltávolítása',
+
+ // Original text: "Create a VM from this snapshot"
+ copySnapshot: 'VPS létrehozása ebből a pillanatképből',
+
+ // Original text: "Export this snapshot"
+ exportSnapshot: 'Pillanatkép exportálása',
+
+ // Original text: "Creation date"
+ snapshotDate: 'Létrehozás dátuma',
+
+ // Original text: "Name"
+ snapshotName: 'Név',
+
+ // Original text: "Action"
+ snapshotAction: 'Művelet',
+
+ // Original text: "Quiesced snapshot"
+ snapshotQuiesce: 'Nyugalomban lévő Pillanatkép',
+
+ // Original text: "Remove all logs"
+ logRemoveAll: 'Logok Eltávolítása',
+
+ // Original text: "No logs so far"
+ noLogs: 'Nincsenek logok ez idáig',
+
+ // Original text: "Creation date"
+ logDate: 'Létrehozás dátuma',
+
+ // Original text: "Name"
+ logName: 'Név',
+
+ // Original text: "Content"
+ logContent: 'Tartalom',
+
+ // Original text: "Action"
+ logAction: 'Művelet',
+
+ // Original text: "Remove"
+ vmRemoveButton: 'Eltávolítás',
+
+ // Original text: "Convert"
+ vmConvertButton: 'Konvertálás',
+
+ // Original text: "Xen settings"
+ xenSettingsLabel: 'Xen beállítások',
+
+ // Original text: "Guest OS"
+ guestOsLabel: 'Vendég OS',
+
+ // Original text: "Misc"
+ miscLabel: 'Egyéb',
+
+ // Original text: "UUID"
+ uuid: 'UUID',
+
+ // Original text: "Virtualization mode"
+ virtualizationMode: 'Virtualizációs üzem',
+
+ // Original text: "CPU weight"
+ cpuWeightLabel: 'CPU súly',
+
+ // Original text: "Default ({value, number})"
+ defaultCpuWeight: 'Alapértelmezett ({Value, number})',
+
+ // Original text: "CPU cap"
+ cpuCapLabel: 'CPU cap',
+
+ // Original text: "Default ({value, number})"
+ defaultCpuCap: 'Alapértelmezett ({Value, number})',
+
+ // Original text: "PV args"
+ pvArgsLabel: 'PV args',
+
+ // Original text: "Xen tools status"
+ xenToolsStatus: 'Xen tools Állapot',
+
+ // Original text: "{status}"
+ xenToolsStatusValue: '{Állapot}',
+
+ // Original text: "OS name"
+ osName: 'OS név',
+
+ // Original text: "OS kernel"
+ osKernel: 'OS kernel',
+
+ // Original text: "Auto power on"
+ autoPowerOn: 'Auto bekapcsolás',
+
+ // Original text: "HA"
+ ha: 'HA',
+
+ // Original text: "Original template"
+ originalTemplate: 'Eredeti sablon',
+
+ // Original text: "Unknown"
+ unknownOsName: 'Ismeretlen',
+
+ // Original text: "Unknown"
+ unknownOsKernel: 'Ismeretlen',
+
+ // Original text: "Unknown"
+ unknownOriginalTemplate: 'Ismeretlen',
+
+ // Original text: "VM limits"
+ vmLimitsLabel: 'VPS limitek',
+
+ // Original text: "CPU limits"
+ vmCpuLimitsLabel: 'CPU limitek',
+
+ // Original text: "Memory limits (min/max)"
+ vmMemoryLimitsLabel: 'Memória limitek (min/max)',
+
+ // Original text: "vCPUs max:"
+ vmMaxVcpus: 'vCPUs max:',
+
+ // Original text: "Memory max:"
+ vmMaxRam: 'Memória max:',
+
+ // Original text: "Long click to add a name"
+ vmHomeNamePlaceholder: 'Kattintson hosszan név hozzáadásához',
+
+ // Original text: "Long click to add a description"
+ vmHomeDescriptionPlaceholder: 'Kattintson hosszan leírás hozzáadásához',
+
+ // Original text: "Click to add a name"
+ vmViewNamePlaceholder: 'Kattintson név hozzáadásához',
+
+ // Original text: "Click to add a description"
+ vmViewDescriptionPlaceholder: 'Kattintson leírás hozzáadásához',
+
+ // Original text: "Click to add a name"
+ templateHomeNamePlaceholder: 'Kattintson név hozzáadásához',
+
+ // Original text: "Click to add a description"
+ templateHomeDescriptionPlaceholder: 'Kattintson leírás hozzáadásához',
+
+ // Original text: "Delete template"
+ 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',
+
+ // 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?',
+
+ // Original text: "Pool{pools, plural, one {} other {s}}"
+ poolPanel: 'Pool{pools, plural, one {} other {ok}}',
+
+ // Original text: "Host{hosts, plural, one {} other {s}}"
+ hostPanel: 'Kiszolgáló{kiszolgálók, plural, one {} other {k}}',
+
+ // Original text: "VM{vms, plural, one {} other {s}}"
+ vmPanel: 'VPS{vms, plural, one {} other {ek}}',
+
+ // Original text: "RAM Usage:"
+ memoryStatePanel: 'Memória használat:',
+
+ // Original text: "CPUs Usage"
+ cpuStatePanel: 'CPUs használat',
+
+ // Original text: "VMs Power state"
+ vmStatePanel: 'VPS áram állapot',
+
+ // Original text: "Pending tasks"
+ taskStatePanel: 'Függőben lévő feladatok',
+
+ // Original text: "Users"
+ usersStatePanel: 'Felhasználók',
+
+ // Original text: "Storage state"
+ srStatePanel: 'Adattároló állapot',
+
+ // Original text: "{usage} (of {total})"
+ ofUsage: '{usage} (of {total})',
+
+ // Original text: "No storage"
+ noSrs: 'Nincs adattároló',
+
+ // Original text: "Name"
+ srName: 'Név',
+
+ // Original text: "Pool"
+ srPool: 'Pool',
+
+ // Original text: "Host"
+ srHost: 'Kiszolgáló',
+
+ // Original text: "Type"
+ srFormat: 'Típus',
+
+ // Original text: "Size"
+ srSize: 'Méret',
+
+ // Original text: "Usage"
+ srUsage: 'használat',
+
+ // Original text: "used"
+ srUsed: 'használva',
+
+ // Original text: "free"
+ srFree: 'szabad',
+
+ // Original text: "Storage Usage"
+ srUsageStatePanel: 'Adattároló használat',
+
+ // Original text: "Top 5 SR Usage (in %)"
+ srTopUsageStatePanel: 'Top 5 Adattároló használat (in %)',
+
+ // Original text: "{running} running ({halted} halted)"
+ vmsStates: '{running} fut ({halted} halted)',
+
+ // Original text: "Clear selection"
+ dashboardStatsButtonRemoveAll: 'Kiválasztás törlése',
+
+ // Original text: "Add all hosts"
+ dashboardStatsButtonAddAllHost: 'Összes kiszolgáló hozzáadása',
+
+ // Original text: "Add all VMs"
+ dashboardStatsButtonAddAllVM: 'Összes VPS hozzáadása',
+
+ // Original text: "{value} {date, date, medium}"
+ weekHeatmapData: '{Value} {date, Dátum, medium}',
+
+ // Original text: "No data."
+ weekHeatmapNoData: 'Nincs adata.',
+
+ // Original text: "Weekly Heatmap"
+ weeklyHeatmap: 'Heti Hőtérkép',
+
+ // Original text: "Weekly Charts"
+ weeklyCharts: 'Heti Diagram',
+
+ // Original text: "Synchronize scale:"
+ weeklyChartsScaleInfo: 'Skála szinkronizálása:',
+
+ // Original text: "Stats error"
+ statsDashboardGenericErrorTitle: 'Statisztikák hiba',
+
+ // Original text: "There is no stats available for:"
+ statsDashboardGenericErrorMessage:
+ 'Jelenleg nincs elérhető statisztika a következőhöz:',
+
+ // Original text: "No selected metric"
+ noSelectedMetric: 'Nincs kiválasztott mérőszám',
+
+ // Original text: "Select"
+ statsDashboardSelectObjects: 'Válasszon',
+
+ // Original text: "Loading…"
+ metricsLoading: 'Töltés…',
+
+ // Original text: "Coming soon!"
+ comingSoon: 'Hamarosan!',
+
+ // Original text: "Orphaned snapshot VDIs"
+ orphanedVdis: 'Árván maradt Pillanatképek VDI-k',
+
+ // Original text: "Orphaned VMs snapshot"
+ orphanedVms: 'Árván maradt VPS Pillanatkép',
+
+ // Original text: "No orphans"
+ noOrphanedObject: 'Nincsenek árván hagyott pillanatképek',
+
+ // Original text: "Remove all orphaned snapshot VDIs"
+ removeAllOrphanedObject: 'Árván maradt VPS Pillanatkép VDI-k eltávolítása',
+
+ // Original text: "Name"
+ vmNameLabel: 'Név',
+
+ // Original text: "Description"
+ vmNameDescription: 'Leírás',
+
+ // Original text: "Resident on"
+ vmContainer: 'Itt fut:',
+
+ // Original text: "Alarms"
+ alarmMessage: 'Riasztások',
+
+ // Original text: "No alarms"
+ noAlarms: 'Nincsenek riasztások',
+
+ // Original text: "Date"
+ alarmDate: 'Dátum',
+
+ // Original text: "Content"
+ alarmContent: 'Tartalom',
+
+ // Original text: "Issue on"
+ alarmObject: 'Probléma itt',
+
+ // Original text: "Pool"
+ alarmPool: 'Pool',
+
+ // Original text: "Remove all alarms"
+ alarmRemoveAll: 'Riasztások eltávolítása',
+
+ // Original text: "{used}% used ({free} left)"
+ spaceLeftTooltip: '{used}% felhasználva ({free} maradt)',
+
+ // Original text: "Create a new VM on {select}"
+ newVmCreateNewVmOn: 'VPS létrehozása a következőn: {select}',
+
+ // Original text: "Create a new VM on {select1} or {select2}"
+ 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',
+
+ // Original text: "Infos"
+ newVmInfoPanel: 'Információk',
+
+ // Original text: "Name"
+ newVmNameLabel: 'Név',
+
+ // Original text: "Template"
+ newVmTemplateLabel: 'Sablon',
+
+ // Original text: "Description"
+ newVmDescriptionLabel: 'Leírás',
+
+ // Original text: "Performances"
+ newVmPerfPanel: 'Teljesítmények',
+
+ // Original text: "vCPUs"
+ newVmVcpusLabel: 'vCPUs',
+
+ // Original text: "RAM"
+ newVmRamLabel: 'RAM',
+
+ // Original text: "Static memory max"
+ newVmStaticMaxLabel: 'Max Statikus Memória',
+
+ // Original text: "Dynamic memory min"
+ newVmDynamicMinLabel: 'Min Dinamikus Memória',
+
+ // Original text: "Dynamic memory max"
+ newVmDynamicMaxLabel: 'Max Dinamikus Memória',
+
+ // Original text: "Install settings"
+ newVmInstallSettingsPanel: 'Telepítési beállítások',
+
+ // Original text: "ISO/DVD"
+ newVmIsoDvdLabel: 'ISO/DVD',
+
+ // Original text: "Network"
+ newVmNetworkLabel: 'Hálózat',
+
+ // Original text: "e.g: http://httpredir.debian.org/debian"
+ newVmInstallNetworkPlaceHolder: 'pl.: http://httpredir.debian.org/debian',
+
+ // Original text: "PV Args"
+ newVmPvArgsLabel: 'PV Args',
+
+ // Original text: "PXE"
+ newVmPxeLabel: 'PXE',
+
+ // Original text: "Interfaces"
+ newVmInterfacesPanel: 'Interfészek',
+
+ // Original text: "MAC"
+ newVmMacLabel: 'MAC',
+
+ // Original text: "Add interface"
+ newVmAddInterface: 'Interfész Hozzáadása',
+
+ // Original text: "Disks"
+ newVmDisksPanel: 'Diszkek',
+
+ // Original text: "SR"
+ newVmSrLabel: 'Adattároló',
+
+ // Original text: "Size"
+ newVmSizeLabel: 'Méret',
+
+ // Original text: "Add disk"
+ newVmAddDisk: 'Diszk Hozzáadása',
+
+ // Original text: "Summary"
+ newVmSummaryPanel: 'Összesítés',
+
+ // Original text: "Create"
+ newVmCreate: 'Létrehozás',
+
+ // Original text: "Reset"
+ newVmReset: 'Visszaállít',
+
+ // Original text: "Select template"
+ newVmSelectTemplate: 'Válasszon sablont',
+
+ // Original text: "SSH key"
+ newVmSshKey: 'SSH kulcs',
+
+ // Original text: "Config drive"
+ newVmConfigDrive: 'Meghajtó beállítása',
+
+ // Original text: "Custom config"
+ newVmCustomConfig: 'egyedi beállítás',
+
+ // Original text: "Boot VM after creation"
+ newVmBootAfterCreate: 'VPS bootolása létrehozás után',
+
+ // Original text: "Auto-generated if empty"
+ newVmMacPlaceholder: 'Auto-generálás ha üres',
+
+ // Original text: "CPU weight"
+ newVmCpuWeightLabel: 'CPU súly',
+
+ // Original text: "Default: {value, number}"
+ newVmDefaultCpuWeight: 'Alapértelmezett: {Value, number}',
+
+ // Original text: "CPU cap"
+ newVmCpuCapLabel: 'CPU cap',
+
+ // Original text: "Default: {value, number}"
+ newVmDefaultCpuCap: 'Alapértelmezett: {Value, number}',
+
+ // Original text: "Cloud config"
+ newVmCloudConfig: 'Cloud beállítás',
+
+ // Original text: "Create VMs"
+ newVmCreateVms: 'VPSek Létrehozása',
+
+ // Original text: "Are you sure you want to create {nbVms} VMs?"
+ newVmCreateVmsConfirm: 'Biztos benne, hogy létrehoz {nbVms} VPS-t?',
+
+ // Original text: "Multiple VMs:"
+ newVmMultipleVms: 'Több VPS:',
+
+ // Original text: "Select a resource set:"
+ newVmSelectResourceSet: 'Válasszon egy erőforrás készletet:',
+
+ // Original text: "Name pattern:"
+ newVmMultipleVmsPattern: 'Minta Név:',
+
+ // Original text: "e.g.: \\{name\\}_%"
+ newVmMultipleVmsPatternPlaceholder: 'e.g.: \\{név\\}_%',
+
+ // Original text: "First index:"
+ newVmFirstIndex: 'Első index:',
+
+ // Original text: "Recalculate VMs number"
+ newVmNumberRecalculate: 'VPS számok újraszámolása',
+
+ // Original text: "Refresh VMs name"
+ newVmNameRefresh: 'VPS nevek frissítése',
+
+ // Original text: "Advanced"
+ newVmAdvancedPanel: 'Haladó',
+
+ // Original text: "Show advanced settings"
+ newVmShowAdvanced: 'Mutassa a Haladó beállításokat',
+
+ // Original text: "Hide advanced settings"
+ newVmHideAdvanced: 'Haladó beállítások elrejtése',
+
+ // Original text: 'Share this VM'
+ newVmShare: undefined,
+
+ // Original text: "Resource sets"
+ resourceSets: 'Erőforrás készletek',
+
+ // Original text: "No resource sets."
+ noResourceSets: 'Nincsenek erőforrás készletek.',
+
+ // Original text: "Loading resource sets"
+ loadingResourceSets: 'Erőforrás készletek betöltése',
+
+ // Original text: "Resource set name"
+ resourceSetName: 'Erőforrás készlet neve',
+
+ // Original text: "Recompute all limits"
+ recomputeResourceSets: 'Összes limit újraszámolása',
+
+ // Original text: "Save"
+ saveResourceSet: 'Mentés',
+
+ // Original text: "Reset"
+ resetResourceSet: 'Visszaállítás',
+
+ // Original text: "Edit"
+ editResourceSet: 'Szerkesztés',
+
+ // Original text: "Delete"
+ deleteResourceSet: 'Törlés',
+
+ // Original text: "Delete resource set"
+ deleteResourceSetWarning: 'Erőforrás készlet törlése',
+
+ // Original text: "Are you sure you want to delete this resource set?"
+ deleteResourceSetQuestion: 'Biztos benne, hogy törli az Erőforrás készletet?',
+
+ // Original text: "Missing objects:"
+ resourceSetMissingObjects: 'Hiányzó objektum:',
+
+ // Original text: "vCPUs"
+ resourceSetVcpus: 'vCPUs',
+
+ // Original text: "Memory"
+ resourceSetMemory: 'Memória',
+
+ // Original text: "Storage"
+ resourceSetStorage: 'Adattároló',
+
+ // Original text: "Unknown"
+ unknownResourceSetValue: 'Ismeretlen',
+
+ // Original text: "Available hosts"
+ availableHosts: 'Elérhető kiszolgálók',
+
+ // Original text: "Excluded hosts"
+ excludedHosts: 'Kizárt kiszolgálók',
+
+ // Original text: "No hosts available."
+ 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.',
+
+ // Original text: "Maximum CPUs"
+ maxCpus: 'Maximum CPU',
+
+ // Original text: "Maximum RAM (GiB)"
+ maxRam: 'Maximum RAM (GiB)',
+
+ // Original text: "Maximum disk space"
+ maxDiskSpace: 'Maximum tárhely méret',
+
+ // Original text: "IP pool"
+ ipPool: 'IP pool',
+
+ // Original text: "Quantity"
+ quantity: 'Mennyiség',
+
+ // Original text: "No limits."
+ noResourceSetLimits: 'Nincs limit.',
+
+ // Original text: "Total:"
+ totalResource: 'Összesen:',
+
+ // Original text: "Remaining:"
+ remainingResource: 'Marad:',
+
+ // Original text: "Used:"
+ usedResource: 'Felhasznált:',
+
+ // Original text: "New"
+ 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.',
+
+ // Original text: "No selected VMs."
+ noSelectedVms: 'Nincs kiválasztott VPS.',
+
+ // Original text: "To Pool:"
+ vmImportToPool: 'Pool-ra:',
+
+ // Original text: "To SR:"
+ vmImportToSr: 'Adattárolóra:',
+
+ // Original text: "VMs to import"
+ vmsToImport: 'Importálandó VPS-el',
+
+ // Original text: "Reset"
+ importVmsCleanList: 'Visszaállít',
+
+ // Original text: "VM import success"
+ vmImportSuccess: 'VPS importálása sikeres',
+
+ // Original text: "VM import failed"
+ vmImportFailed: 'VPS importálása nem sikerült',
+
+ // Original text: "Import starting…"
+ startVmImport: 'Importálás indul…',
+
+ // Original text: "Export starting…"
+ startVmExport: 'Exportálás indul…',
+
+ // Original text: "N CPUs"
+ nCpus: 'N CPUs',
+
+ // Original text: "Memory"
+ vmMemory: 'Memória',
+
+ // Original text: "Disk {position} ({capacity})"
+ diskInfo: 'Diszk {position} ({capacity})',
+
+ // Original text: "Disk description"
+ diskDescription: 'Diszk leírása',
+
+ // Original text: "No disks."
+ noDisks: 'Nincsenek Diszkek.',
+
+ // Original text: "No networks."
+ noNetworks: 'Nincsenek Hálózatok.',
+
+ // Original text: "Network {name}"
+ networkInfo: 'Hálózat {name}',
+
+ // Original text: "No description available"
+ noVmImportErrorDescription: 'Nincs elérhető leírás',
+
+ // Original text: "Error:"
+ vmImportError: 'Hiba:',
+
+ // Original text: "{type} file:"
+ vmImportFileType: '{type} fájl:',
+
+ // Original text: "Please to check and/or modify the VM configuration."
+ vmImportConfigAlert: 'Kérjük ellenőrizze és vagy módosítsa a VPS beállítást.',
+
+ // Original text: "No pending tasks"
+ noTasks: 'Nincsenek függő feladatok',
+
+ // Original text: "Currently, there are not any pending XenServer tasks"
+ xsTasks: 'Jelenleg nincsenek függő XenServer feladatok',
+
+ // Original text: "Schedules"
+ backupSchedules: 'Időzítések',
+
+ // Original text: "Get remote"
+ getRemote: 'Távoli Mentés Beállítása',
+
+ // Original text: "List Remote"
+ listRemote: 'Távoli Mentés Listázása',
+
+ // Original text: "simple"
+ simpleBackup: 'simple',
+
+ // Original text: "delta"
+ delta: 'delta',
+
+ // Original text: "Restore Backups"
+ 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',
+
+ // Original text: "Enabled"
+ remoteEnabled: 'Bekapcsolva',
+
+ // Original text: "Error"
+ remoteError: 'Hiba',
+
+ // Original text: "No backup available"
+ noBackup: 'Nincs elérhető adatmentés',
+
+ // Original text: "VM Name"
+ backupVmNameColumn: 'VPS Név',
+
+ // Original text: "Tags"
+ backupTags: 'Cimkék',
+
+ // Original text: "Last Backup"
+ lastBackupColumn: 'Legutolsó Mentés',
+
+ // Original text: "Available Backups"
+ availableBackupsColumn: 'Elérhető Mentések',
+
+ // Original text: "Missing parameters"
+ backupRestoreErrorTitle: 'Hiányzó paraméterek',
+
+ // Original text: "Choose a SR and a backup"
+ backupRestoreErrorMessage: 'Válasszon egy Adattárolót és egy Mentést',
+
+ // Original text: "Display backups"
+ displayBackup: 'Mentések megjelenítése',
+
+ // Original text: "Import VM"
+ importBackupTitle: 'VPS Importálás',
+
+ // Original text: "Starting your backup import"
+ importBackupMessage: 'Mentés importálásának indítása',
+
+ // Original text: "VMs to backup"
+ vmsToBackup: 'Mentendő VPS-ek',
+
+ // Original text: "List remote backups"
+ listRemoteBackups: 'Távoli adatmentések listázása',
+
+ // Original text: "Restore backup files"
+ restoreFiles: 'Adatmentési fájlok visszaállítása',
+
+ // Original text: "Invalid options"
+ restoreFilesError: 'Érévnytelen beállítás',
+
+ // Original text: "Restore file from {name}"
+ restoreFilesFromBackup: 'Fájl visszaállítás innen: {name}',
+
+ // Original text: "Select a backup…"
+ restoreFilesSelectBackup: 'Válasszon adatmentést…',
+
+ // Original text: "Select a disk…"
+ restoreFilesSelectDisk: 'Válasszon diszket…',
+
+ // Original text: "Select a partition…"
+ restoreFilesSelectPartition: 'Válasszon egy partíciót…',
+
+ // Original text: "Folder path"
+ restoreFilesSelectFolderPath: 'Mappa útvonal',
+
+ // Original text: "Select a file…"
+ restoreFilesSelectFiles: 'Válasszon egy fájlt…',
+
+ // Original text: "Content not found"
+ restoreFileContentNotFound: 'Tartalom nem található',
+
+ // Original text: "No files selected"
+ restoreFilesNoFilesSelected: 'Nincsenek kiválasztott fájlok',
+
+ // Original text: "Selected files ({files}):"
+ restoreFilesSelectedFiles: 'kiválasztott fájlok ({files}):',
+
+ // Original text: "Error while scanning disk"
+ restoreFilesDiskError: 'Hiba a diszk szkennelése közben',
+
+ // Original text: "Select all this folder's files"
+ restoreFilesSelectAllFiles: 'A mappa összes fájljainak kiválasztása',
+
+ // Original text: "Unselect all files"
+ restoreFilesUnselectAll: 'Fájlok kijelölésének törlése',
+
+ // Original text: "Emergency shutdown Host{nHosts, plural, one {} other {s}}"
+ 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?',
+
+ // 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!',
+
+ // 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}?',
+
+ // 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?',
+
+ // Original text: "Restart Host{nHosts, plural, one {} other {s}} agent{nHosts, plural, one {} other {s}}"
+ restartHostsAgentsModalTitle: 'Kiszolgáló(k) Újraindítása',
+
+ // Original text: "Are you sure you want to restart {nHosts} Host{nHosts, plural, one {} other {s}} agent{nHosts, plural, one {} other {s}}?"
+ restartHostsAgentsModalMessage: 'Biztos benne, hogy újraindítja?',
+
+ // Original text: "Restart Host{nHosts, plural, one {} other {s}}"
+ restartHostsModalTitle: 'Kiszolgáló(k) Újraindítása',
+
+ // Original text: "Are you sure you want to restart {nHosts} Host{nHosts, plural, one {} other {s}}?"
+ restartHostsModalMessage: 'Biztos benne, hogy újraindítja?',
+
+ // Original text: "Start VM{vms, plural, one {} other {s}}"
+ startVmsModalTitle: 'VPS Elindítása',
+
+ // Original text: "Are you sure you want to start {vms} VM{vms, plural, one {} other {s}}?"
+ startVmsModalMessage: 'Biztos benne, hogy elindítja?',
+
+ // Original text: "Stop Host{nHosts, plural, one {} other {s}}"
+ 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!',
+
+ // Original text: "Stop VM{vms, plural, one {} other {s}}"
+ stopVmsModalTitle: 'VPS Leállítás',
+
+ // Original text: "Are you sure you want to stop {vms} VM{vms, plural, one {} other {s}}?"
+ stopVmsModalMessage: 'Biztos benne, hogy leállítja?',
+
+ // Original text: "Restart VM"
+ restartVmModalTitle: 'VPS Újraindítása',
+
+ // Original text: "Are you sure you want to restart {name}?"
+ restartVmModalMessage: 'Biztos benne, hogy újraindítja: {name}-t?',
+
+ // Original text: "Stop VM"
+ stopVmModalTitle: 'VPS Leállítás',
+
+ // Original text: "Are you sure you want to stop {name}?"
+ stopVmModalMessage: 'Biztos benne, hogy to leállítja: {name}-t?',
+
+ // Original text: "Restart VM{vms, plural, one {} other {s}}"
+ restartVmsModalTitle: 'VPS Újraindítás',
+
+ // Original text: "Are you sure you want to restart {vms} VM{vms, plural, one {} other {s}}?"
+ restartVmsModalMessage: 'Biztos benne, hogy újraindítja?',
+
+ // Original text: "Snapshot VM{vms, plural, one {} other {s}}"
+ snapshotVmsModalTitle: 'VPS Pillanatképek',
+
+ // Original text: "Are you sure you want to snapshot {vms} VM{vms, plural, one {} other {s}}?"
+ snapshotVmsModalMessage: 'Biztos benne, hogy készít Pillanatképet a VPS-ről?',
+
+ // Original text: "Delete VM{vms, plural, one {} other {s}}"
+ 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!',
+
+ // 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!',
+
+ // Original text: "Migrate VM"
+ migrateVmModalTitle: 'VPS Migrálása',
+
+ // Original text: "Select a destination host:"
+ migrateVmSelectHost: 'Válasszon cél Kiszolgálót:',
+
+ // Original text: "Select a migration network:"
+ migrateVmSelectMigrationNetwork: 'Válasszon egy migrációs hálózatot:',
+
+ // Original text: "For each VDI, select an SR:"
+ migrateVmSelectSrs: 'Minden VDI számára: válasszon egy Adattárolót:',
+
+ // Original text: "For each VIF, select a network:"
+ migrateVmSelectNetworks: 'Minden VIF számára, válasszon egy Hálózatot:',
+
+ // Original text: "Select a destination SR:"
+ 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:',
+
+ // Original text: "Select a network on which to connect each VIF:"
+ migrateVmsSelectNetwork:
+ 'Válasszon egy Hálózatot amelyekhez csatlakoztasson minden VIF-et:',
+
+ // Original text: "Smart mapping"
+ migrateVmsSmartMapping: 'Okos feltérképezés',
+
+ // Original text: "Name"
+ migrateVmName: 'Név',
+
+ // Original text: "SR"
+ migrateVmSr: 'Adattároló',
+
+ // Original text: "VIF"
+ migrateVmVif: 'VIF',
+
+ // Original text: "Network"
+ migrateVmNetwork: 'Hálózat',
+
+ // Original text: "No target host"
+ 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!',
+
+ // 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!',
+
+ // Original text: "Revert your VM"
+ revertVmModalTitle: 'VPS Visszaállítása',
+
+ // Original text: "Delete snapshot"
+ 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?',
+
+ // 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!',
+
+ // Original text: "Snapshot before"
+ revertVmModalSnapshotBefore: 'Pillanatkép ezelőtt',
+
+ // Original text: "Import a {name} Backup"
+ importBackupModalTitle: '{name} Mentés Importálása',
+
+ // Original text: "Start VM after restore"
+ importBackupModalStart: 'Visszaállítás után a VPS elindítása',
+
+ // Original text: "Select your backup…"
+ 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?',
+
+ // Original text: "Remove all logs"
+ removeAllLogsModalTitle: 'Összes Log Eltávolítása',
+
+ // Original text: "Are you sure you want to remove all logs?"
+ removeAllLogsModalWarning: 'Biztos benne, hogy Eltávolítja az összes Logot?',
+
+ // Original text: "This operation is definitive."
+ definitiveMessageModal: 'Ez a művelet végleges.',
+
+ // Original text: "Previous SR Usage"
+ 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.',
+
+ // 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.',
+
+ // 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 ?',
+
+ // 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!',
+
+ // Original text: "Host"
+ serverHost: 'Kiszolgáló',
+
+ // Original text: "Username"
+ serverUsername: 'Felhasználónév',
+
+ // Original text: "Password"
+ serverPassword: 'Jelszó',
+
+ // Original text: "Action"
+ serverAction: 'Művelet',
+
+ // Original text: "Read Only"
+ serverReadOnly: 'Csak Olvasható',
+
+ // Original text: "Disconnect server"
+ serverDisconnect: 'Szerver Lecsatlakozás',
+
+ // Original text: "username"
+ serverPlaceHolderUser: 'felhasználónév',
+
+ // Original text: "password"
+ serverPlaceHolderPassword: 'jelszó',
+
+ // Original text: "address[:port]"
+ serverPlaceHolderAddress: 'address[:port]',
+
+ // Original text: "Connect"
+ serverConnect: 'Csatlakozás',
+
+ // Original text: "Error"
+ serverError: 'Hiba',
+
+ // Original text: "Adding server failed"
+ serverAddFailed: 'Szerver Hozzáadása Sikertelen',
+
+ // Original text: "Status"
+ serverStatus: 'Állapot',
+
+ // Original text: "Connection failed"
+ serverConnectionFailed: 'Csatlakozás Sikertelen',
+
+ // Original text: "Connecting…"
+ serverConnecting: 'Csatlakozás…',
+
+ // Original text: "Connected"
+ serverConnected: 'Kapcsolódva',
+
+ // Original text: "Disconnected"
+ serverDisconnected: 'Lekapcsolódva',
+
+ // Original text: "Authentication error"
+ serverAuthFailed: 'Bejelentkezési hiba',
+
+ // Original text: "Unknown error"
+ serverUnknownError: 'Ismeretlen hiba',
+
+ // Original text: "Copy VM"
+ 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}?',
+
+ // Original text: "Name"
+ copyVmName: 'Név',
+
+ // Original text: "Name pattern"
+ copyVmNamePattern: 'Név minta',
+
+ // Original text: "If empty: name of the copied VM"
+ copyVmNamePlaceholder: 'Ha üres: a másolt VPS neve',
+
+ // Original text: "e.g.: \"\\{name\\}_COPY\""
+ copyVmNamePatternPlaceholder: 'pl.: "\\{name\\}_Masolat"',
+
+ // Original text: "Select SR"
+ copyVmSelectSr: 'Válasszon Adattárolót',
+
+ // Original text: "Use compression"
+ copyVmCompress: 'Tömörítés használata',
+
+ // Original text: "No target SR"
+ copyVmsNoTargetSr: 'Nincs cél Adattároló',
+
+ // Original text: "A target SR is required to copy a VM"
+ copyVmsNoTargetSrMessage: 'Egy cél Adattároló szükséges a VPS másolásához',
+
+ // Original text: "Detach host"
+ 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.',
+
+ // Original text: "Detach"
+ detachHost: 'Detach',
+
+ // Original text: "Create network"
+ newNetworkCreate: 'Hálózat Létrehozása',
+
+ // Original text: "Create bonded network"
+ newBondedNetworkCreate: 'Bond Hálózat Létrehozás',
+
+ // Original text: "Interface"
+ newNetworkInterface: 'Interfész',
+
+ // Original text: "Name"
+ newNetworkName: 'Név',
+
+ // Original text: "Description"
+ newNetworkDescription: 'Leírás',
+
+ // Original text: "VLAN"
+ newNetworkVlan: 'VLAN',
+
+ // Original text: "No VLAN if empty"
+ newNetworkDefaultVlan: 'Nincs VLAN ha üres',
+
+ // Original text: "MTU"
+ newNetworkMtu: 'MTU',
+
+ // Original text: "Default: 1500"
+ newNetworkDefaultMtu: 'Alapértelmezett: 1500',
+
+ // Original text: "Name required"
+ newNetworkNoNameErrorTitle: 'Név szükséges',
+
+ // Original text: "A name is required to create a network"
+ newNetworkNoNameErrorMessage: 'Egy név szükséges a Hálózat létrehozásához',
+
+ // Original text: "Bond mode"
+ newNetworkBondMode: 'Bond üzem',
+
+ // Original text: "Delete network"
+ deleteNetwork: 'Hálózat Törlése',
+
+ // Original text: "Are you sure you want to delete this network?"
+ deleteNetworkConfirm: 'Biztos benne, hogy törli a Hálózatot?',
+
+ // Original text: "This network is currently in use"
+ networkInUse: 'Ez a Hálózat jelenleg használatban van',
+
+ // Original text: "Bonded"
+ pillBonded: 'Bonded',
+
+ // Original text: "Host"
+ addHostSelectHost: 'Kiszolgáló',
+
+ // Original text: "No host"
+ addHostNoHost: 'Nincs Kiszolgáló',
+
+ // Original text: "No host selected to be added"
+ addHostNoHostMessage:
+ 'Nincs Kiszolgáló kiválasztva amihez hozzá lehetne adni',
+
+ // Original text: "Xen Orchestra"
+ xenOrchestra: 'CLOUDXO',
+
+ // Original text: "Xen Orchestra server"
+ xenOrchestraServer: 'Cloudxo szerver',
+
+ // Original text: "Xen Orchestra web client"
+ xenOrchestraWeb: 'Cloudxo web kliens',
+
+ // Original text: "No pro support provided!"
+ noProSupport: 'Nincsen pro-szupport!',
+
+ // Original text: "Use in production at your own risks"
+ noProductionUse: 'Use in production at your own risks',
+
+ // Original text: "You can download our turnkey appliance at {website}"
+ downloadXoaFromWebsite: 'You can download our turnkey appliance at {website}',
+
+ // Original text: "Bug Tracker"
+ bugTracker: 'Bug Tracker',
+
+ // Original text: "Issues? Report it!"
+ bugTrackerText: 'Issues? Report it!',
+
+ // Original text: "Community"
+ community: 'Community',
+
+ // Original text: "Join our community forum!"
+ communityText: 'Join our community forum!',
+
+ // Original text: "Free Trial for Premium Edition!"
+ freeTrial: 'Free Trial for Premium Edition!',
+
+ // Original text: "Request your trial now!"
+ freeTrialNow: 'Request your trial now!',
+
+ // Original text: "Any issue?"
+ issues: 'Any issue?',
+
+ // Original text: "Problem? Contact us!"
+ issuesText: 'Problem? Contact us!',
+
+ // Original text: "Documentation"
+ documentation: 'Documentation',
+
+ // Original text: "Read our official doc"
+ documentationText: 'Read our official doc',
+
+ // Original text: "Pro support included"
+ proSupportIncluded: 'Pro support included',
+
+ // Original text: "Access your XO Account"
+ xoAccount: 'Access your XO Account',
+
+ // Original text: "Report a problem"
+ openTicket: 'Report a problem',
+
+ // Original text: "Problem? Open a ticket!"
+ openTicketText: 'Problem? Open a ticket!',
+
+ // Original text: "Upgrade needed"
+ upgradeNeeded: 'Upgrade needed',
+
+ // Original text: "Upgrade now!"
+ upgradeNow: 'Upgrade now!',
+
+ // Original text: "Or"
+ or: 'Or',
+
+ // Original text: "Try it for free!"
+ tryIt: 'Try it for free!',
+
+ // Original text: "This feature is available starting from {plan} Edition"
+ 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.',
+
+ // Original text: "Updates"
+ updateTitle: 'UpDates',
+
+ // Original text: "Registration"
+ registration: 'Registration',
+
+ // Original text: "Trial"
+ trial: 'Trial',
+
+ // Original text: "Settings"
+ settings: 'Beállítások',
+
+ // Original text: "Proxy settings"
+ proxySettings: 'Proxy settings',
+
+ // Original text: "Host (myproxy.example.org)"
+ proxySettingsHostPlaceHolder: 'Kiszolgáló (myproxy.example.org)',
+
+ // Original text: "Port (eg: 3128)"
+ proxySettingsPortPlaceHolder: 'Port (eg: 3128)',
+
+ // Original text: "Username"
+ proxySettingsUsernamePlaceHolder: 'Felhasználónév',
+
+ // Original text: "Password"
+ proxySettingsPasswordPlaceHolder: 'Jelszó',
+
+ // Original text: "Your email account"
+ updateRegistrationEmailPlaceHolder: 'Your email account',
+
+ // Original text: "Your password"
+ updateRegistrationPasswordPlaceHolder: 'Your jelszó',
+
+ // Original text: "Update"
+ update: 'UpDates',
+
+ // Original text: "Refresh"
+ refresh: 'Refresh',
+
+ // Original text: "Upgrade"
+ upgrade: 'Upgrade',
+
+ // Original text: "No updater available for Community Edition"
+ 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}.',
+
+ // 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',
+
+ // Original text: "Current version:"
+ currentVersion: 'Jelenlegi Verzió:',
+
+ // Original text: "Register"
+ register: 'Register',
+
+ // Original text: "Edit registration"
+ 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.',
+
+ // 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.',
+
+ // 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ó',
+
+ // 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.',
+
+ // Original text: "No update information available"
+ noUpdateInfo: 'No upDátum information available',
+
+ // Original text: "Update information may be available"
+ waitingUpdateInfo: 'UpDátum information may be available',
+
+ // Original text: "Your XOA is up-to-date"
+ upToDate: 'Your XOA is up-to-Dátum',
+
+ // Original text: "You need to update your XOA (new version is available)"
+ mustUpgrade: 'You need to upDate your XOA (new Verzió is available)',
+
+ // Original text: "Your XOA is not registered for updates"
+ registerNeeded: 'Your XOA is not registered for upDates',
+
+ // Original text: "Can't fetch update information"
+ updaterError: "Can't fetch upDátum information",
+
+ // Original text: "Upgrade successful"
+ 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 ?',
+
+ // 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.",
+
+ // 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:",
+
+ // 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.',
+
+ // Original text: "Connect PIF"
+ connectPif: 'Csatlakozás PIF',
+
+ // Original text: "Are you sure you want to connect this PIF?"
+ connectPifConfirm: 'Biztos benne, hogyi to Csatlakozás this PIF?',
+
+ // Original text: "Disconnect PIF"
+ disconnectPif: 'Lecsatlakozás PIF',
+
+ // Original text: "Are you sure you want to disconnect this PIF?"
+ disconnectPifConfirm: 'Biztos benne, hogyi to Lecsatlakozás this PIF?',
+
+ // Original text: "Delete PIF"
+ deletePif: 'Törlés PIF',
+
+ // Original text: "Are you sure you want to delete this PIF?"
+ deletePifConfirm: 'Biztos benne, hogyi to delete this PIF?',
+
+ // Original text: "Username"
+ username: 'Felhasználónév',
+
+ // Original text: "Password"
+ password: 'Jelszó',
+
+ // Original text: "Language"
+ language: 'Nyelv',
+
+ // Original text: "Old password"
+ oldPasswordPlaceholder: 'Régi jelszó',
+
+ // Original text: "New password"
+ newPasswordPlaceholder: 'Új jelszó',
+
+ // Original text: "Confirm new password"
+ confirmPasswordPlaceholder: 'Új Jelszó Megerősítése',
+
+ // Original text: "Confirmation password incorrect"
+ confirmationPasswordError: 'Megerősítő jelszó helytelen',
+
+ // Original text: "Password does not match the confirm password."
+ confirmationPasswordErrorBody: 'A megadott jelszavak nem egyeznek.',
+
+ // Original text: "Password changed"
+ pwdChangeSuccess: 'Jelszó megváltoztatva',
+
+ // Original text: "Your password has been successfully changed."
+ pwdChangeSuccessBody: 'A jelszó sikeresen megváltoztatva.',
+
+ // Original text: "Incorrect password"
+ 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!',
+
+ // Original text: "OK"
+ changePasswordOk: 'OK',
+
+ // Original text: "SSH keys"
+ sshKeys: 'SSH kulcsok',
+
+ // Original text: "New SSH key"
+ newSshKey: 'Új SSH kulcs',
+
+ // Original text: "Delete"
+ deleteSshKey: 'Törlés',
+
+ // Original text: "No SSH keys"
+ noSshKeys: 'Nincsenek SSH kulcsok',
+
+ // Original text: "New SSH key"
+ newSshKeyModalTitle: 'Új SSH kulcs',
+
+ // Original text: "Invalid key"
+ sshKeyErrorTitle: 'Helytelen kulcs',
+
+ // Original text: "An SSH key requires both a title and a key."
+ sshKeyErrorMessage: 'Az SSH kulcshoz szükség van egy címre és egy kulcsra.',
+
+ // Original text: "Title"
+ title: 'Cím',
+
+ // Original text: "Key"
+ key: 'kulcs',
+
+ // Original text: "Delete SSH key"
+ 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?',
+
+ // Original text: "Others"
+ others: 'Egyebek',
+
+ // Original text: "Loading logs…"
+ loadingLogs: 'Logok betöltése…',
+
+ // Original text: "User"
+ logUser: 'Felhasználó',
+
+ // Original text: "Method"
+ logMethod: 'Módszer',
+
+ // Original text: "Params"
+ logParams: 'Paraméterek',
+
+ // Original text: "Message"
+ logMessage: 'Üzenet',
+
+ // Original text: "Error"
+ logError: 'Hiba',
+
+ // Original text: "Display details"
+ logDisplayDetails: 'Részletek megjelenítése',
+
+ // Original text: "Date"
+ logTime: 'Dátum',
+
+ // Original text: "No stack trace"
+ logNoStackTrace: 'No stack trace',
+
+ // Original text: "No params"
+ logNoParams: 'No params',
+
+ // Original text: "Delete log"
+ logDelete: 'Log Törlése',
+
+ // Original text: "Delete all logs"
+ logDeleteAll: 'Összes Log Törlése',
+
+ // Original text: "Delete all logs"
+ logDeleteAllTitle: 'Összes Log Törlése',
+
+ // Original text: "Are you sure you want to delete all the logs?"
+ logDeleteAllMessage: 'Biztos benne, hogy törli az összes Logot?',
+
+ // Original text: 'Click to enable'
+ logIndicationToEnable: undefined,
+
+ // Original text: 'Click to disable'
+ logIndicationToDisable: undefined,
+
+ // Original text: "Report a bug"
+ reportBug: 'Hibabejelentés',
+
+ // Original text: "Name"
+ ipPoolName: 'Név',
+
+ // Original text: "IPs"
+ ipPoolIps: 'IP címek',
+
+ // Original text: "IPs (e.g.: 1.0.0.12-1.0.0.17;1.0.0.23)"
+ ipPoolIpsPlaceholder: 'IP címek (e.g.: 1.0.0.12-1.0.0.17;1.0.0.23)',
+
+ // Original text: "Networks"
+ ipPoolNetworks: 'Hálózatok',
+
+ // Original text: "No IP pools"
+ ipsNoIpPool: 'No IP pools',
+
+ // Original text: "Create"
+ ipsCreate: 'Létrehozás',
+
+ // Original text: "Delete all IP pools"
+ ipsDeleteAllTitle: 'Delet all IP pools',
+
+ // Original text: "Are you sure you want to delete all the IP pools?"
+ ipsDeleteAllMessage: 'Are you sure you want to delete all the IP pools?',
+
+ // Original text: "VIFs"
+ ipsVifs: 'VIFs',
+
+ // Original text: "Not used"
+ ipsNotUsed: 'Not used',
+
+ // Original text: "unknown VIF"
+ ipPoolUnknownVif: 'ismeretlen VIF',
+
+ // Original text: "Keyboard shortcuts"
+ shortcutModalTitle: 'Billentyűzet kiosztások',
+
+ // Original text: "Global"
+ shortcut_XoApp: 'Globális',
+
+ // Original text: "Go to hosts list"
+ shortcut_GO_TO_HOSTS: 'Menjen a Kiszolgálók listájához',
+
+ // Original text: "Go to pools list"
+ shortcut_GO_TO_POOLS: 'Go to pools list',
+
+ // Original text: "Go to VMs list"
+ shortcut_GO_TO_VMS: 'Go to VMs list',
+
+ // Original text: "Go to SRs list"
+ shortcut_GO_TO_SRS: 'Go to Storage list',
+
+ // Original text: "Create a new VM"
+ shortcut_CREATE_VM: 'Új VPS Létrehozása',
+
+ // Original text: "Unfocus field"
+ shortcut_UNFOCUS: 'Unfocus field',
+
+ // Original text: "Show shortcuts key bindings"
+ shortcut_HELP: 'Show shortcuts key bindings',
+
+ // Original text: "Home"
+ shortcut_Home: 'Kezdőlap',
+
+ // Original text: "Focus search bar"
+ shortcut_SEARCH: 'Focus keresősáv',
+
+ // Original text: "Next item"
+ shortcut_NAV_DOWN: 'Következő',
+
+ // Original text: "Previous item"
+ shortcut_NAV_UP: 'Előző',
+
+ // Original text: "Select item"
+ shortcut_SELECT: 'Válasszon',
+
+ // Original text: "Open"
+ shortcut_JUMP_INTO: 'Megnyitás',
+
+ // Original text: "VM"
+ settingsAclsButtonTooltipVM: 'VMPS',
+
+ // Original text: "Hosts"
+ settingsAclsButtonTooltiphost: 'Kiszolgáló',
+
+ // Original text: "Pool"
+ settingsAclsButtonTooltippool: 'Pool',
+
+ // Original text: "SR"
+ settingsAclsButtonTooltipSR: 'Adattároló',
+
+ // Original text: "Network"
+ settingsAclsButtonTooltipnetwork: 'Hálózat',
+
+ // Original text: "No config file selected"
+ 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.',
+
+ // Original text: "Config"
+ config: 'Konfiguráció',
+
+ // Original text: "Import"
+ importConfig: 'Importálás',
+
+ // Original text: "Config file successfully imported"
+ importConfigSuccess: 'Config file successfully imported',
+
+ // Original text: "Error while importing config file"
+ importConfigError: 'Hiba while importing config file',
+
+ // Original text: "Export"
+ exportConfig: 'Export',
+
+ // Original text: "Download current config"
+ downloadConfig: 'Download current config',
+
+ // Original text: "No config import available for Community Edition"
+ 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.',
+
+ // 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ó).',
+
+ // 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.',
+
+ // 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ó).',
+
+ // Original text: "Forget SR"
+ srForgetModalTitle: 'Elfelejt Adattároló',
+
+ // Original text: "Forget selected SRs"
+ 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.",
+
+ // 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.",
+
+ // Original text: "Disconnected"
+ srAllDisconnected: 'Lekapcsolódva',
+
+ // Original text: "Partially connected"
+ srSomeConnected: 'Partially Kapcsolódva',
+
+ // Original text: "Connected"
+ srAllConnected: 'Kapcsolódva',
+
+ // Original text: 'XOSAN'
+ xosanTitle: undefined,
+
+ // Original text: 'Xen Orchestra SAN SR'
+ xosanSrTitle: undefined,
+
+ // Original text: 'Select local SRs (lvm)'
+ xosanAvailableSrsTitle: undefined,
+
+ // Original text: 'Suggestions'
+ xosanSuggestions: undefined,
+
+ // Original text: 'Name'
+ xosanName: undefined,
+
+ // Original text: 'Host'
+ xosanHost: undefined,
+
+ // Original text: 'Hosts'
+ xosanHosts: undefined,
+
+ // Original text: 'Volume ID'
+ xosanVolumeId: undefined,
+
+ // Original text: 'Size'
+ xosanSize: undefined,
+
+ // Original text: 'Used space'
+ xosanUsedSpace: undefined,
+
+ // Original text: 'XOSAN pack needs to be installed on each host of the pool.'
+ xosanNeedPack: undefined,
+
+ // Original text: 'Install it now!'
+ xosanInstallIt: undefined,
+
+ // Original text: 'Install XOSAN pack on {pool}'
+ xosanInstallPackTitle: undefined,
+
+ // Original text: 'Select at least 2 SRs'
+ xosanSelect2Srs: undefined,
+
+ // Original text: 'Layout'
+ xosanLayout: undefined,
+
+ // Original text: 'Redundancy'
+ xosanRedundancy: undefined,
+
+ // Original text: 'Capacity'
+ xosanCapacity: undefined,
+
+ // Original text: 'Available space'
+ xosanAvailableSpace: undefined,
+
+ // Original text: '* Can fail without data loss'
+ xosanDiskLossLegend: undefined,
+
+ // Original text: 'Create'
+ xosanCreate: undefined,
+
+ // Original text: 'Installing XOSAN. Please wait…'
+ xosanInstalling: undefined,
+
+ // Original text: 'You need XenServer 7.0 to install XOSAN'
+ xosanBadVersion: undefined,
+
+ // Original text: 'No XOSAN available for Community Edition'
+ xosanCommunity: undefined,
+
+ // Original text: 'Install cloud plugin first'
+ xosanInstallCloudPlugin: undefined,
+
+ // Original text: 'Load cloud plugin first'
+ xosanLoadCloudPlugin: undefined,
+
+ // Original text: 'Loading…'
+ xosanLoading: undefined,
+
+ // Original text: 'XOSAN is not available at the moment'
+ xosanNotAvailable: undefined,
+
+ // Original text: 'Register for the XOSAN beta'
+ xosanRegisterBeta: undefined,
+
+ // Original text: 'You have successfully registered for the XOSAN beta. Please wait until your request has been approved.'
+ xosanSuccessfullyRegistered: undefined,
+
+ // Original text: 'Install XOSAN pack on these hosts:'
+ xosanInstallPackOnHosts: undefined,
+
+ // Original text: 'Install {pack} v{version}?'
+ xosanInstallPack: undefined,
+}
diff --git a/packages/xo-web/src/common/intl/locales/pl.js b/packages/xo-web/src/common/intl/locales/pl.js
new file mode 100644
index 000000000..6b436038a
--- /dev/null
+++ b/packages/xo-web/src/common/intl/locales/pl.js
@@ -0,0 +1,3186 @@
+// See http://momentjs.com/docs/#/use-it/browserify/
+import 'moment/locale/pl'
+
+import reactIntlData from 'react-intl/locale-data/pl'
+import { addLocaleData } from 'react-intl'
+addLocaleData(reactIntlData)
+
+// ===================================================================
+
+export default {
+ // Original text: "Connecting"
+ statusConnecting: 'Trwa łączenie…',
+
+ // Original text: "Disconnected"
+ statusDisconnected: 'Rozłączono',
+
+ // Original text: "Loading…"
+ statusLoading: 'Ładowanie…',
+
+ // Original text: "Page not found"
+ errorPageNotFound: 'Nie znaleziono strony',
+
+ // Original text: "no such item"
+ errorNoSuchItem: 'nie ma takiego elementu',
+
+ // Original text: "Long click to edit"
+ editableLongClickPlaceholder: 'Przytrzymaj żeby edytować',
+
+ // Original text: "Click to edit"
+ editableClickPlaceholder: 'Kliknij żeby edytować',
+
+ // Original text: "OK"
+ alertOk: 'OK',
+
+ // Original text: "OK"
+ confirmOk: 'OK',
+
+ // Original text: "Cancel"
+ confirmCancel: 'Anuluj',
+
+ // Original text: "On error"
+ onError: 'Błąd',
+
+ // Original text: "Successful"
+ successful: 'Ukończone',
+
+ // Original text: "Copy to clipboard"
+ copyToClipboard: 'Kopiuj do schowka',
+
+ // Original text: "Master"
+ pillMaster: 'Master',
+
+ // Original text: "Home"
+ homePage: 'Start',
+
+ // Original text: "VMs"
+ homeVmPage: 'VMs',
+
+ // Original text: "Hosts"
+ homeHostPage: 'Hosty',
+
+ // Original text: "Pools"
+ homePoolPage: 'Pule',
+
+ // Original text: "Templates"
+ homeTemplatePage: 'Szablony',
+
+ // Original text: "Dashboard"
+ dashboardPage: 'Dashboard',
+
+ // Original text: "Overview"
+ overviewDashboardPage: 'Podgląd',
+
+ // Original text: "Visualizations"
+ overviewVisualizationDashboardPage: 'Wizualizacje',
+
+ // Original text: "Statistics"
+ overviewStatsDashboardPage: 'Statystyki',
+
+ // Original text: "Health"
+ overviewHealthDashboardPage: 'Stan serwera',
+
+ // Original text: "Self service"
+ selfServicePage: 'Samoobsługa',
+
+ // Original text: "Backup"
+ backupPage: 'Backup',
+
+ // Original text: "Jobs"
+ jobsPage: 'Zadania',
+
+ // Original text: "Updates"
+ updatePage: 'Aktualizacje',
+
+ // Original text: "Settings"
+ settingsPage: 'Settings',
+
+ // Original text: "Servers"
+ settingsServersPage: 'Serwery',
+
+ // Original text: "Users"
+ settingsUsersPage: 'Użytkownicy',
+
+ // Original text: "Groups"
+ settingsGroupsPage: 'Grupy',
+
+ // Original text: "ACLs"
+ settingsAclsPage: 'ACLs',
+
+ // Original text: "Plugins"
+ settingsPluginsPage: 'Dodatki',
+
+ // Original text: "Logs"
+ settingsLogsPage: 'Logi',
+
+ // Original text: "IPs"
+ settingsIpsPage: 'IPs',
+
+ // Original text: "About"
+ aboutPage: 'O oprogramowaniu',
+
+ // Original text: "About {xoaPlan}"
+ aboutXoaPlan: 'O {xoaPlan}',
+
+ // Original text: "New"
+ newMenu: 'Nowy',
+
+ // Original text: "Tasks"
+ taskMenu: 'Zadania',
+
+ // Original text: "Tasks"
+ taskPage: 'Zadania',
+
+ // Original text: "VM"
+ newVmPage: 'VM',
+
+ // Original text: "Storage"
+ newSrPage: 'Przestrzeń dyskowa',
+
+ // Original text: "Server"
+ newServerPage: 'Serwer',
+
+ // Original text: "Import"
+ newImport: 'Importuj',
+
+ // Original text: "Overview"
+ backupOverviewPage: 'Podgląd',
+
+ // Original text: "New"
+ backupNewPage: 'Nowy',
+
+ // Original text: "Remotes"
+ backupRemotesPage: 'Zdalne',
+
+ // Original text: "Restore"
+ backupRestorePage: 'Odtwórz',
+
+ // Original text: "Schedule"
+ schedule: 'Harmonogram',
+
+ // Original text: "New VM backup"
+ newVmBackup: 'Nowy backup VM',
+
+ // Original text: "Edit VM backup"
+ editVmBackup: 'Edytuj backup VM',
+
+ // Original text: "Backup"
+ backup: 'Backup',
+
+ // Original text: "Rolling Snapshot"
+ rollingSnapshot: 'Rolling Snapshot',
+
+ // Original text: "Delta Backup"
+ deltaBackup: 'Delta Backup',
+
+ // Original text: "Disaster Recovery"
+ disasterRecovery: 'Odzyskiwanie po awarii',
+
+ // Original text: "Continuous Replication"
+ continuousReplication: 'Continous Replication',
+
+ // Original text: "Overview"
+ jobsOverviewPage: 'Podgląd',
+
+ // Original text: "New"
+ jobsNewPage: 'Nowe',
+
+ // Original text: "Scheduling"
+ jobsSchedulingPage: 'Planowanie',
+
+ // Original text: "Custom Job"
+ customJob: 'Zadania niestandardowe',
+
+ // Original text: "User"
+ userPage: 'Użytkownik',
+
+ // Original text: "No support"
+ noSupport: 'Brak wsparcia',
+
+ // Original text: "Free upgrade!"
+ freeUpgrade: 'Darmowa aktualizacja!',
+
+ // Original text: "Sign out"
+ signOut: 'Wyloguj',
+
+ // Original text: "Edit my settings {username}"
+ editUserProfile: 'Edytuj moje ustawienia {username}',
+
+ // Original text: "Fetching data…"
+ homeFetchingData: 'Fetching data…',
+
+ // Original text: "Welcome on Xen Orchestra!"
+ homeWelcome: 'Witaj w Xen Orchestra!',
+
+ // Original text: "Add your XenServer hosts or pools"
+ homeWelcomeText: 'Dodaj serwery XenServer lub pule',
+
+ // Original text: "Want some help?"
+ homeHelp: 'Potrzebujesz pomocy?',
+
+ // Original text: "Add server"
+ homeAddServer: 'Dodaj serwer',
+
+ // Original text: "Online Doc"
+ homeOnlineDoc: 'Online Doc',
+
+ // Original text: "Pro Support"
+ homeProSupport: 'Profesjonalne wsparcie',
+
+ // Original text: "There are no VMs!"
+ homeNoVms: 'Nie masz żadnych VMs!',
+
+ // Original text: "Or…"
+ homeNoVmsOr: 'Lub…',
+
+ // Original text: "Import VM"
+ homeImportVm: 'Importuj VM',
+
+ // Original text: "Import an existing VM in xva format"
+ homeImportVmMessage: 'Importuj istniejącą VM w formacie xva',
+
+ // Original text: "Restore a backup"
+ homeRestoreBackup: 'Przywróć kopię zapasową',
+
+ // Original text: "Restore a backup from a remote store"
+ homeRestoreBackupMessage: 'Przywróć kopię zapasową z innego miejsca',
+
+ // Original text: "This will create a new VM"
+ homeNewVmMessage: 'Kliknij w ikonę żeby utworzyć VM',
+
+ // Original text: "Filters"
+ homeFilters: 'Filtry',
+
+ // Original text: "No results! Click here to reset your filters"
+ homeNoMatches: 'Brak wyników! Kliknij tutaj żeby usunąć filtry',
+
+ // Original text: "Pool"
+ homeTypePool: 'Pula',
+
+ // Original text: "Host"
+ homeTypeHost: 'Host',
+
+ // Original text: "VM"
+ homeTypeVm: 'VM',
+
+ // Original text: "SR"
+ homeTypeSr: 'SR',
+
+ // Original text: "Template"
+ homeTypeVmTemplate: 'Szablon',
+
+ // Original text: "Sort"
+ homeSort: 'Sortuj',
+
+ // Original text: "Pools"
+ homeAllPools: 'Pule',
+
+ // Original text: "Hosts"
+ homeAllHosts: 'Hosty',
+
+ // Original text: "Tags"
+ homeAllTags: 'Tagi',
+
+ // Original text: "New VM"
+ homeNewVm: 'Nowa VM',
+
+ // Original text: "Running hosts"
+ homeFilterRunningHosts: 'Działające hosty',
+
+ // Original text: "Disabled hosts"
+ homeFilterDisabledHosts: 'Wyłączone hosty',
+
+ // Original text: "Running VMs"
+ homeFilterRunningVms: 'Działające VMs',
+
+ // Original text: "Non running VMs"
+ homeFilterNonRunningVms: 'Niedziałające VMs',
+
+ // Original text: "Pending VMs"
+ homeFilterPendingVms: 'Oczekujące VMs',
+
+ // Original text: "HVM guests"
+ homeFilterHvmGuests: 'Gość HVM',
+
+ // Original text: "Tags"
+ homeFilterTags: 'Tagi',
+
+ // Original text: "Sort by"
+ homeSortBy: 'Sortuj po',
+
+ // Original text: "Name"
+ homeSortByName: 'Nazwa',
+
+ // Original text: "Power state"
+ homeSortByPowerstate: 'Stan zasilania',
+
+ // Original text: "RAM"
+ homeSortByRAM: 'RAM',
+
+ // Original text: "vCPUs"
+ homeSortByvCPUs: 'vCPUs',
+
+ // Original text: "CPUs"
+ homeSortByCpus: 'CPUs',
+
+ // Original text: "{displayed, number}x {icon} (on {total, number})"
+ 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})',
+
+ // Original text: "More"
+ homeMore: 'Więcej',
+
+ // Original text: "Migrate to…"
+ homeMigrateTo: 'Migruj do…',
+
+ // Original text: "Missing patches"
+ homeMissingPaths: 'Brakujące łatki',
+
+ // Original text: "Master:"
+ homePoolMaster: 'Master:',
+
+ // Original text: "High Availability"
+ highAvailability: 'Wysoka dostępność',
+
+ // Original text: "Add"
+ add: 'Dodaj',
+
+ // Original text: "Remove"
+ remove: 'Usuń',
+
+ // Original text: "Preview"
+ preview: 'Podgląd',
+
+ // Original text: "Item"
+ item: 'Obiekt',
+
+ // Original text: "No selected value"
+ noSelectedValue: 'Nie wybrano wartości ',
+
+ // Original text: "Choose user(s) and/or group(s)"
+ selectSubjects: 'Wybierz użytkownika/użytkowników i/lub grupę/grupy',
+
+ // Original text: "Select Object(s)…"
+ selectObjects: 'Wybierz obiekt/obiekty…',
+
+ // Original text: "Choose a role"
+ selectRole: 'Wybierz rolę',
+
+ // Original text: "Select Host(s)…"
+ selectHosts: 'Wybierz Host/Hosty…',
+
+ // Original text: "Select object(s)…"
+ selectHostsVms: 'Wybierz obiekt/obiekty',
+
+ // Original text: "Select Network(s)…"
+ selectNetworks: 'Wybierz sieć/sieci…',
+
+ // Original text: "Select PIF(s)…"
+ selectPifs: 'Wybierz PIF(s)…',
+
+ // Original text: "Select Pool(s)…"
+ selectPools: 'Wybierz pulę/pule…',
+
+ // Original text: "Select Remote(s)…"
+ selectRemotes: 'Select Remote(s)…',
+
+ // Original text: "Select resource set(s)…"
+ selectResourceSets: 'Wybierz pulę zasobów…',
+
+ // Original text: "Select template(s)…"
+ selectResourceSetsVmTemplate: 'Wybierz szablon/szablony…',
+
+ // Original text: "Select SR(s)…"
+ selectResourceSetsSr: 'Wybierz pulę dyskową',
+
+ // Original text: "Select network(s)…"
+ selectResourceSetsNetwork: 'Wybierz sieć/sieci',
+
+ // Original text: "Select disk(s)…"
+ selectResourceSetsVdi: 'Wybierz dysk/dyski…',
+
+ // Original text: "Select SSH key(s)…"
+ selectSshKey: 'Wybierz klucz/klucze SSH…',
+
+ // Original text: "Select SR(s)…"
+ selectSrs: 'Wybierz SR(s)…',
+
+ // Original text: "Select VM(s)…"
+ selectVms: 'Wybierz VM(s)…',
+
+ // Original text: "Select VM template(s)…"
+ selectVmTemplates: 'Wybierz szablon/szablony VM…',
+
+ // Original text: "Select tag(s)…"
+ selectTags: 'Wybierz tag(i)…',
+
+ // Original text: "Select disk(s)…"
+ selectVdis: 'Wybierz dysk/dyski…',
+
+ // Original text: "Select timezone…"
+ selectTimezone: 'Wybierz strefę czasową…',
+
+ // Original text: "Select IP(s)…"
+ selectIp: 'Wybierz IP(s)…',
+
+ // Original text: "Select IP pool(s)…"
+ selectIpPool: 'Wybierz pulę/pule IP',
+
+ // Original text: "Fill required informations."
+ fillRequiredInformations: 'Wypełnij brakujące informacje.',
+
+ // Original text: "Fill informations (optional)"
+ fillOptionalInformations: 'Wypełnij informacje (opcjonalnie)',
+
+ // Original text: "Reset"
+ selectTableReset: 'Reset',
+
+ // Original text: "Month"
+ schedulingMonth: 'Miesiąc',
+
+ // Original text: "Each selected month"
+ schedulingEachSelectedMonth: 'Każdy wybrany miesiąc',
+
+ // Original text: "Day of the month"
+ schedulingMonthDay: 'Dzień z miesiąca',
+
+ // Original text: "Each selected day"
+ schedulingEachSelectedMonthDay: 'Każdy wybrany dzień',
+
+ // Original text: "Day of the week"
+ schedulingWeekDay: 'Dzień z tygodnia',
+
+ // Original text: "Each selected day"
+ schedulingEachSelectedWeekDay: 'Każdy wybrany dzień',
+
+ // Original text: "Hour"
+ schedulingHour: 'Godzina',
+
+ // Original text: "Every N hour"
+ schedulingEveryNHour: 'Każda N godzina',
+
+ // Original text: "Each selected hour"
+ schedulingEachSelectedHour: 'Każda wybrana godzina',
+
+ // Original text: "Minute"
+ schedulingMinute: 'Minuta',
+
+ // Original text: "Every N minute"
+ schedulingEveryNMinute: 'Każda N minuta',
+
+ // Original text: "Each selected minute"
+ schedulingEachSelectedMinute: 'Każda wybrana minuta',
+
+ // Original text: "Reset"
+ schedulingReset: 'Reset',
+
+ // Original text: "Unknown"
+ unknownSchedule: 'Nieznany',
+
+ // Original text: "Xo-server timezone:"
+ timezonePickerServerValue: 'Strefa czasowa Xo-server:',
+
+ // Original text: "Web browser timezone"
+ timezonePickerUseLocalTime: 'Strefa czasowa przeglądarki internetowej',
+
+ // Original text: "Xo-server timezone"
+ timezonePickerUseServerTime: 'Strefa czasowa Xo-server',
+
+ // Original text: "Server timezone ({value})"
+ serverTimezoneOption: 'Strefa czasowa serwera({value})',
+
+ // Original text: "Cron Pattern:"
+ cronPattern: 'Cron Pattern :',
+
+ // Original text: "Cannot edit backup"
+ backupEditNotFoundTitle: 'Nie można edytować kopii zapasowej',
+
+ // Original text: "Missing required info for edition"
+ backupEditNotFoundMessage: 'Brakuje wymaganych informacji do edycji',
+
+ // Original text: "Job"
+ job: 'Job',
+
+ // Original text: "Job ID"
+ jobId: 'Job ID',
+
+ // Original text: "Name"
+ jobName: 'Nazwa',
+
+ // Original text: "Name of your job (forbidden: \"_\")"
+ jobNamePlaceholder: 'Name of your job (forbidden: "_")',
+
+ // Original text: "Start"
+ jobStart: 'Start',
+
+ // Original text: "End"
+ jobEnd: 'Koniec',
+
+ // Original text: "Duration"
+ jobDuration: 'Duration',
+
+ // Original text: "Status"
+ jobStatus: 'Status',
+
+ // Original text: "Action"
+ jobAction: 'Akcja',
+
+ // Original text: "Tag"
+ jobTag: 'Tag',
+
+ // Original text: "Scheduling"
+ jobScheduling: 'Planowanie',
+
+ // Original text: "State"
+ jobState: 'Stan',
+
+ // Original text: "Timezone"
+ jobTimezone: 'Strefa czasowa',
+
+ // Original text: "xo-server"
+ jobServerTimezone: 'xo-server',
+
+ // Original text: "Run job"
+ runJob: 'Uruchomione zadanie',
+
+ // Original text: "One shot running started. See overview for logs."
+ runJobVerbose: 'One shot running started. See overview for logs.',
+
+ // Original text: "Started"
+ jobStarted: 'Uruchomiono',
+
+ // Original text: "Finished"
+ jobFinished: 'Zakończono',
+
+ // Original text: "Save"
+ saveBackupJob: 'Zapisz',
+
+ // Original text: "Remove backup job"
+ 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?',
+
+ // 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',
+
+ // Original text: "You are editing job {name} ({id}). Saving will override previous job state."
+ jobEditMessage:
+ 'Edytujesz zadanie {name} ({id}). Zapisanie zastąpi poprzednie zadanie',
+
+ // Original text: "No scheduled jobs."
+ noScheduledJobs: 'Brak zaplanowanych zadań',
+
+ // Original text: "No jobs found."
+ noJobs: 'Brak zadań',
+
+ // Original text: "No schedules found"
+ noSchedules: 'Brak harmonogramów',
+
+ // Original text: "Select a xo-server API command"
+ jobActionPlaceHolder: 'Wybierz polecenie API dla xo-server',
+
+ // Original text: "Schedules"
+ jobSchedules: 'Harmonogramy',
+
+ // Original text: "Name of your schedule"
+ jobScheduleNamePlaceHolder: 'Nazwa twojego harmonogramu',
+
+ // Original text: "Select a Job"
+ jobScheduleJobPlaceHolder: 'Wybierz zadanie',
+
+ // Original text: "Select your backup type:"
+ newBackupSelection: 'Wybierz typ kopii zapasowej :',
+
+ // Original text: "Select backup mode:"
+ smartBackupModeSelection: 'Wybierz tryb kopii zapasowej :',
+
+ // Original text: "Normal backup"
+ normalBackup: 'Normalna kopia zapasowa',
+
+ // Original text: "Smart backup"
+ smartBackup: 'Inteligentna kopia zapsowa',
+
+ // Original text: "Local remote selected"
+ 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.',
+
+ // Original text: "VMs"
+ editBackupVmsTitle: 'VMs',
+
+ // Original text: "VMs statuses"
+ editBackupSmartStatusTitle: 'Statusy VMs',
+
+ // Original text: "Resident on"
+ editBackupSmartResidentOn: 'Resident on',
+
+ // Original text: "VMs Tags"
+ editBackupSmartTagsTitle: 'Tagi VMs',
+
+ // Original text: "Tag"
+ editBackupTagTitle: 'Tag',
+
+ // Original text: "Report"
+ editBackupReportTitle: 'Raport',
+
+ // Original text: "Enable immediately after creation"
+ editBackupScheduleEnabled: 'Uruchom natychamiast po utworzeniu',
+
+ // Original text: "Depth"
+ editBackupDepthTitle: 'Depth',
+
+ // Original text: "Remote"
+ editBackupRemoteTitle: 'Zdalny',
+
+ // Original text: "Remote stores for backup"
+ remoteList: 'Zdalne przechowywanie kopii zapasowej',
+
+ // Original text: "New File System Remote"
+ newRemote: 'Nowy zdalny system plików',
+
+ // Original text: "Local"
+ remoteTypeLocal: 'Lokalny',
+
+ // Original text: "NFS"
+ remoteTypeNfs: 'NFS',
+
+ // Original text: "SMB"
+ remoteTypeSmb: 'SMB',
+
+ // Original text: "Type"
+ remoteType: 'Typ',
+
+ // Original text: "Test your remote"
+ remoteTestTip: 'Przetestuj swoje zdalne połączenie',
+
+ // Original text: "Test Remote"
+ testRemote: 'Test Remote',
+
+ // Original text: "Test failed for {name}"
+ remoteTestFailure: 'Test zakończony niepowodzeniem dla {name}',
+
+ // Original text: "Test passed for {name}"
+ remoteTestSuccess: 'Test zakończony sukcesem {name}',
+
+ // Original text: "Error"
+ remoteTestError: 'Błąd',
+
+ // Original text: "Test Step"
+ remoteTestStep: 'Test Step',
+
+ // Original text: "Test file"
+ remoteTestFile: 'Testowy plik',
+
+ // Original text: "The remote appears to work correctly"
+ remoteTestSuccessMessage: 'The remote appears to work correctly',
+
+ // Original text: "Name"
+ remoteName: 'Name',
+
+ // Original text: "Path"
+ remotePath: 'Scieżka',
+
+ // Original text: "State"
+ remoteState: 'Stan',
+
+ // Original text: "Device"
+ remoteDevice: 'Urządzenie',
+
+ // Original text: "Share"
+ remoteShare: 'Udostępnij',
+
+ // Original text: "Auth"
+ remoteAuth: 'Auth',
+
+ // Original text: "Mounted"
+ remoteMounted: 'Zamontowane',
+
+ // Original text: "Unmounted"
+ remoteUnmounted: 'Odmontowane',
+
+ // Original text: "Connect"
+ remoteConnectTip: 'Połącz',
+
+ // Original text: "Disconnect"
+ remoteDisconnectTip: 'Rozłącz',
+
+ // Original text: "Delete"
+ remoteDeleteTip: 'Usuń',
+
+ // Original text: "remote name *"
+ remoteNamePlaceHolder: 'Nazwa zdalna*',
+
+ // Original text: "Name *"
+ remoteMyNamePlaceHolder: 'Nazwa *',
+
+ // Original text: "/path/to/backup"
+ remoteLocalPlaceHolderPath: '/ścieżka/do/kopii/zapasowej',
+
+ // Original text: "host *"
+ remoteNfsPlaceHolderHost: 'Host *',
+
+ // Original text: "/path/to/backup"
+ remoteNfsPlaceHolderPath: '/ścieżka/do/kopii/zapasowej',
+
+ // Original text: "subfolder [path\\to\\backup]"
+ remoteSmbPlaceHolderRemotePath: 'podfolder [ścieżka\\do\\kopii\\zapasowej]',
+
+ // Original text: "Username"
+ remoteSmbPlaceHolderUsername: 'Nazwa użytkownika',
+
+ // Original text: "Password"
+ remoteSmbPlaceHolderPassword: 'Hasło',
+
+ // Original text: "Domain"
+ remoteSmbPlaceHolderDomain: 'Domena',
+
+ // Original text: "\\ *"
+ remoteSmbPlaceHolderAddressShare: '\\ *',
+
+ // Original text: "password(fill to edit)"
+ remotePlaceHolderPassword: 'Hasło (wypełnij)',
+
+ // Original text: "Create a new SR"
+ newSrTitle: 'Stwórz nowy SR',
+
+ // Original text: "General"
+ newSrGeneral: 'General',
+
+ // Original text: "Select Storage Type:"
+ newSrTypeSelection: 'Wybierz typ puli dyskowej:',
+
+ // Original text: "Settings"
+ newSrSettings: 'Ustawienia',
+
+ // Original text: "Storage Usage"
+ newSrUsage: 'Użycie dysków',
+
+ // Original text: "Summary"
+ newSrSummary: 'Podsumowanie',
+
+ // Original text: "Host"
+ newSrHost: 'Host',
+
+ // Original text: "Type"
+ newSrType: 'Typ',
+
+ // Original text: "Name"
+ newSrName: 'Nazwa',
+
+ // Original text: "Description"
+ newSrDescription: 'Opis',
+
+ // Original text: "Server"
+ newSrServer: 'Serwer',
+
+ // Original text: "Path"
+ newSrPath: 'Ścieżka',
+
+ // Original text: "IQN"
+ newSrIqn: 'IQN',
+
+ // Original text: "LUN"
+ newSrLun: 'LUN',
+
+ // Original text: "with auth."
+ newSrAuth: 'z autoryzacją',
+
+ // Original text: "User Name"
+ newSrUsername: 'Nazwa użytkownika',
+
+ // Original text: "Password"
+ newSrPassword: 'Hasło',
+
+ // Original text: "Device"
+ newSrDevice: 'Urządzenie',
+
+ // Original text: "in use"
+ newSrInUse: 'Używane',
+
+ // Original text: "Size"
+ newSrSize: 'Rozmiar',
+
+ // Original text: "Create"
+ newSrCreate: 'Utwórz',
+
+ // Original text: "Storage name"
+ newSrNamePlaceHolder: 'Nazwa puli dyskowej',
+
+ // Original text: "Storage description"
+ newSrDescPlaceHolder: 'Opis puli dyskowej',
+
+ // Original text: "Address"
+ newSrAddressPlaceHolder: 'Adres',
+
+ // Original text: "[port]"
+ newSrPortPlaceHolder: '[port]',
+
+ // Original text: "Username"
+ newSrUsernamePlaceHolder: 'Nazwa użytkownika',
+
+ // Original text: "Password"
+ newSrPasswordPlaceHolder: 'Hasło',
+
+ // Original text: "Device, e.g /dev/sda…"
+ newSrLvmDevicePlaceHolder: 'Urządzenie, np. /dev/sda…',
+
+ // Original text: "/path/to/directory"
+ newSrLocalPathPlaceHolder: '/ścieżka/do/katalogu',
+
+ // Original text: "Users/Groups"
+ subjectName: 'Użytkownicy/Grupy',
+
+ // Original text: "Object"
+ objectName: 'Obiekt',
+
+ // Original text: "No acls found"
+ aclNoneFound: 'Nie znaleziono ACLs',
+
+ // Original text: "Role"
+ roleName: 'Rola',
+
+ // Original text: "Create"
+ aclCreate: 'Utwórz',
+
+ // Original text: "New Group Name"
+ newGroupName: 'Nazwa nowej grupy',
+
+ // Original text: "Create Group"
+ createGroup: 'Utwórz grupę',
+
+ // Original text: "Create"
+ createGroupButton: 'Utwórz',
+
+ // Original text: "Delete Group"
+ deleteGroup: 'Usuń grupę',
+
+ // Original text: "Are you sure you want to delete this group?"
+ deleteGroupConfirm: 'Jesteś pewny że chcesz usunąć te grupę?',
+
+ // Original text: "Remove user from Group"
+ removeUserFromGroup: 'Usuń użytkownika z grupy',
+
+ // Original text: "Are you sure you want to delete this user?"
+ deleteUserConfirm: 'Jesteś pewny że chcesz usunąć tego użytkownika?',
+
+ // Original text: "Delete User"
+ deleteUser: 'Usuń użytkownika',
+
+ // Original text: "no user"
+ noUser: 'Brak użytkownika',
+
+ // Original text: "unknown user"
+ unknownUser: 'Nieznany użytkownik',
+
+ // Original text: "No group found"
+ noGroupFound: 'Nie znaleziono grupy',
+
+ // Original text: "Name"
+ groupNameColumn: 'Nazwa',
+
+ // Original text: "Users"
+ groupUsersColumn: 'Użytkownicy',
+
+ // Original text: "Add User"
+ addUserToGroupColumn: 'Dodaj użytkownika',
+
+ // Original text: "Email"
+ userNameColumn: 'Email',
+
+ // Original text: "Permissions"
+ userPermissionColumn: 'Uprawnienia',
+
+ // Original text: "Password"
+ userPasswordColumn: 'Hasło',
+
+ // Original text: "Email"
+ userName: 'Email',
+
+ // Original text: "Password"
+ userPassword: 'Hasło',
+
+ // Original text: "Create"
+ createUserButton: 'Utwórz',
+
+ // Original text: "No user found"
+ noUserFound: 'Nie znaleziono użytkownika',
+
+ // Original text: "User"
+ userLabel: 'Użytkownik',
+
+ // Original text: "Admin"
+ adminLabel: 'Administrator',
+
+ // Original text: "No user in group"
+ noUserInGroup: 'Brak użytkownika w grupie',
+
+ // Original text: "{users} user{users, plural, one {} other {s}}"
+ countUsers: '{users} użytkownik{users, plural, one {} other {s}}',
+
+ // Original text: "Select Permission"
+ selectPermission: 'Wybierz uprawnienia',
+
+ // Original text: "Auto-load at server start"
+ autoloadPlugin: 'Ładuj automatycznie gdy serwer startuje',
+
+ // Original text: "Save configuration"
+ savePluginConfiguration: 'Zapisz konfigurację',
+
+ // Original text: "Delete configuration"
+ deletePluginConfiguration: 'Usuń konfigurację',
+
+ // Original text: "Plugin error"
+ pluginError: 'Błąd dodatku',
+
+ // Original text: "Unknown error"
+ unknownPluginError: 'Nieznany błąd',
+
+ // Original text: "Purge plugin configuration"
+ purgePluginConfiguration: 'Purge plugin configuration',
+
+ // Original text: "Are you sure you want to purge this configuration ?"
+ purgePluginConfigurationQuestion: 'Czy napewno chcesz usunąć te konfigurację',
+
+ // Original text: "Edit"
+ editPluginConfiguration: 'Edytuj',
+
+ // Original text: "Cancel"
+ cancelPluginEdition: 'Anuluj',
+
+ // Original text: "Plugin configuration"
+ pluginConfigurationSuccess: 'Konfiguracja dodatków',
+
+ // Original text: "Plugin configuration successfully saved!"
+ pluginConfigurationChanges: 'Konfiguracja dodatków została zapisana !',
+
+ // Original text: "Predefined configuration"
+ pluginConfigurationPresetTitle: 'Wstępnie zdefiniowana konfiguracja',
+
+ // Original text: "Choose a predefined configuration."
+ pluginConfigurationChoosePreset:
+ 'Wybierz wstępnie zdefiniowaną konfigurację.',
+
+ // Original text: "Apply"
+ applyPluginPreset: 'Akceptuj',
+
+ // Original text: "Save filter error"
+ saveNewUserFilterErrorTitle: 'Zapisz błąd filtra',
+
+ // Original text: "Bad parameter: name must be given."
+ saveNewUserFilterErrorBody: 'Zły parametr: nazwa musi byc nadana',
+
+ // Original text: "Name:"
+ filterName: 'Nazwa:',
+
+ // Original text: "Value:"
+ filterValue: 'Wartość :',
+
+ // Original text: "Save new filter"
+ saveNewFilterTitle: 'Zapisz nowy filtr',
+
+ // Original text: "Set custom filters"
+ setUserFiltersTitle: 'Ustaw niestandardowe filtry',
+
+ // Original text: "Are you sure you want to set custom filters?"
+ setUserFiltersBody: 'Jesteś pewny że chcesz ustawić niestandardowe filtry?',
+
+ // Original text: "Remove custom filter"
+ removeUserFilterTitle: 'Usuń niestandardowe filtry',
+
+ // Original text: "Are you sure you want to remove custom filter?"
+ removeUserFilterBody: 'Jesteś pewny że chcesz usunąć niestandardowe filtry?',
+
+ // Original text: "Default filter"
+ defaultFilter: 'Filtr domyślny',
+
+ // Original text: "Default filters"
+ defaultFilters: 'Domyślne filtry',
+
+ // Original text: "Custom filters"
+ customFilters: 'Filtry niestandardowe',
+
+ // Original text: "Customize filters"
+ customizeFilters: 'Dostosuj filtry',
+
+ // Original text: "Save custom filters"
+ saveCustomFilters: 'Zapisz filtry niestandardowe',
+
+ // Original text: "Start"
+ startVmLabel: 'Start',
+
+ // Original text: "Recovery start"
+ recoveryModeLabel: 'Rozpocznij odzyskiwanie',
+
+ // Original text: "Suspend"
+ suspendVmLabel: 'Uśpij',
+
+ // Original text: "Stop"
+ stopVmLabel: 'Stop',
+
+ // Original text: "Force shutdown"
+ forceShutdownVmLabel: 'Brtualne wyłączenie',
+
+ // Original text: "Reboot"
+ rebootVmLabel: 'Reboot',
+
+ // Original text: "Force reboot"
+ forceRebootVmLabel: 'Brutalny reboot',
+
+ // Original text: "Delete"
+ deleteVmLabel: 'Usuń',
+
+ // Original text: "Migrate"
+ migrateVmLabel: 'Migruj',
+
+ // Original text: "Snapshot"
+ snapshotVmLabel: 'Snapshot',
+
+ // Original text: "Export"
+ exportVmLabel: 'Eksportuj',
+
+ // Original text: "Resume"
+ resumeVmLabel: 'Wzów',
+
+ // Original text: "Copy"
+ copyVmLabel: 'Kopia',
+
+ // Original text: "Clone"
+ cloneVmLabel: 'Klonuj',
+
+ // Original text: "Fast clone"
+ fastCloneVmLabel: 'Szybki klon',
+
+ // Original text: "Convert to template"
+ convertVmToTemplateLabel: 'Konwertuj do szablonu',
+
+ // Original text: "Console"
+ vmConsoleLabel: 'Konsola',
+
+ // Original text: "Rescan all disks"
+ srRescan: 'Skanuj wszystkie dyski',
+
+ // Original text: "Connect to all hosts"
+ srReconnectAll: 'Połącz z wszystkimi hostami',
+
+ // Original text: "Disconnect to all hosts"
+ srDisconnectAll: 'Rozłącz z wszystkimi hostami',
+
+ // Original text: "Forget this SR"
+ srForget: 'Zapomnij te pulę dyskową',
+
+ // Original text: "Remove this SR"
+ srRemoveButton: 'Usuń te pulę dyskową',
+
+ // Original text: "No VDIs in this storage"
+ srNoVdis: 'Brak VDIs na tej przestrzeni dyskowej',
+
+ // Original text: "Pool RAM usage:"
+ poolTitleRamUsage: 'Użycie puli RAM:',
+
+ // Original text: "{used} used on {total}"
+ poolRamUsage: '{used} używane w {total}',
+
+ // Original text: "Master:"
+ poolMaster: 'Master :',
+
+ // Original text: "Hosts"
+ hostsTabName: 'Hosty',
+
+ // Original text: "High Availability"
+ poolHaStatus: 'Wysoka dostępność',
+
+ // Original text: "Enabled"
+ poolHaEnabled: 'Włączone',
+
+ // Original text: "Disabled"
+ poolHaDisabled: 'Wyłączone',
+
+ // Original text: "Name"
+ hostNameLabel: 'Nazwa',
+
+ // Original text: "Description"
+ hostDescription: 'Opis',
+
+ // Original text: "Memory"
+ hostMemory: 'Pamieć',
+
+ // Original text: "No hosts"
+ noHost: 'Brak hostów',
+
+ // Original text: "{used}% used ({free} free)"
+ memoryLeftTooltip: '{used}% używane ({free} libre)',
+
+ // Original text: "Name"
+ poolNetworkNameLabel: 'Nazwa',
+
+ // Original text: "Description"
+ poolNetworkDescription: 'Opis',
+
+ // Original text: "PIFs"
+ poolNetworkPif: 'PIFs',
+
+ // Original text: "No networks"
+ poolNoNetwork: 'Brak sieci',
+
+ // Original text: "MTU"
+ poolNetworkMTU: 'MTU',
+
+ // Original text: "Connected"
+ poolNetworkPifAttached: 'Połączone',
+
+ // Original text: "Disconnected"
+ poolNetworkPifDetached: 'Rozłączone',
+
+ // Original text: "Show PIFs"
+ showPifs: 'Pokaż PIFs',
+
+ // Original text: "Hide PIFs"
+ hidePifs: 'Ukryj PIFs',
+
+ // Original text: "Add SR"
+ addSrLabel: 'Dodaj przestrzeń dyskową',
+
+ // Original text: "Add VM"
+ addVmLabel: 'Dodaj VM',
+
+ // Original text: "Add Host"
+ addHostLabel: 'Dodaj hosta',
+
+ // Original text: "Disconnect"
+ disconnectServer: 'Rozłącz',
+
+ // Original text: "Start"
+ startHostLabel: 'Start',
+
+ // Original text: "Stop"
+ stopHostLabel: 'Stop',
+
+ // Original text: "Enable"
+ enableHostLabel: 'Włącz',
+
+ // Original text: "Disable"
+ disableHostLabel: 'Wyłącz',
+
+ // Original text: "Restart toolstack"
+ restartHostAgent: 'Restart toolstack',
+
+ // Original text: "Force reboot"
+ forceRebootHostLabel: 'Brutalny reboot',
+
+ // Original text: "Reboot"
+ rebootHostLabel: 'Reboot',
+
+ // Original text: "Reboot to apply updates"
+ rebootUpdateHostLabel: 'Reboot żeby zastosować aktualizacje',
+
+ // Original text: "Emergency mode"
+ emergencyModeLabel: 'Tryb ratunku',
+
+ // Original text: "Storage"
+ storageTabName: 'Przestrzeń dyskowa',
+
+ // Original text: "Patches"
+ patchesTabName: 'Patches',
+
+ // Original text: "Load average"
+ statLoad: 'Średni load :',
+
+ // Original text: "Hardware"
+ hardwareHostSettingsLabel: 'Sprzęt',
+
+ // Original text: "Address"
+ hostAddress: 'Adres',
+
+ // Original text: "Status"
+ hostStatus: 'Status',
+
+ // Original text: "Build number"
+ hostBuildNumber: 'Build number',
+
+ // Original text: "iSCSI name"
+ hostIscsiName: 'Nazwa iSCSI',
+
+ // Original text: "Version"
+ hostXenServerVersion: 'Wersja',
+
+ // Original text: "Enabled"
+ hostStatusEnabled: 'Włącz',
+
+ // Original text: "Disabled"
+ hostStatusDisabled: 'Wyłącz',
+
+ // Original text: "Power on mode"
+ hostPowerOnMode: 'Tryb włączenia',
+
+ // Original text: "Host uptime"
+ hostStartedSince: 'Nieprzerwany czas działania hosta',
+
+ // Original text: "Toolstack uptime"
+ hostStackStartedSince: 'Toolstack uptime',
+
+ // Original text: "CPU model"
+ hostCpusModel: 'Model CPU',
+
+ // Original text: "Core (socket)"
+ hostCpusNumber: 'Core (socket)',
+
+ // Original text: "Manufacturer info"
+ hostManufacturerinfo: 'Informacje o producencie',
+
+ // Original text: "BIOS info"
+ hostBiosinfo: 'BIOS informacje',
+
+ // Original text: "Licence"
+ licenseHostSettingsLabel: 'Licencja',
+
+ // Original text: "Type"
+ hostLicenseType: 'Typ',
+
+ // Original text: "Socket"
+ hostLicenseSocket: 'Gniazdo',
+
+ // Original text: "Expiry"
+ hostLicenseExpiry: 'Wygasa',
+
+ // Original text: "Add a network"
+ networkCreateButton: 'Dodaj sieć',
+
+ // Original text: "Add a bonded network"
+ networkCreateBondedButton: 'Dodaj bonding dla sieci',
+
+ // Original text: "Device"
+ pifDeviceLabel: 'Urządzenie',
+
+ // Original text: "Network"
+ pifNetworkLabel: 'Sieć',
+
+ // Original text: "VLAN"
+ pifVlanLabel: 'VLAN',
+
+ // Original text: "Address"
+ pifAddressLabel: 'Adres',
+
+ // Original text: 'Mode'
+ pifModeLabel: 'Tryb',
+
+ // Original text: "MAC"
+ pifMacLabel: 'MAC',
+
+ // Original text: "MTU"
+ pifMtuLabel: 'MTU',
+
+ // Original text: "Status"
+ pifStatusLabel: 'Status',
+
+ // Original text: "Connected"
+ pifStatusConnected: 'Połączono',
+
+ // Original text: "Disconnected"
+ pifStatusDisconnected: 'Rozłączono',
+
+ // Original text: "No physical interface detected"
+ pifNoInterface: 'Brak fizycznych interfejsów',
+
+ // Original text: "This interface is currently in use"
+ pifInUse: 'Ten interfejs jest obecnie używany',
+
+ // Original text: "Default locking mode"
+ defaultLockingMode: 'Domyślny tryb blokowania',
+
+ // Original text: 'Configure IP address'
+ pifConfigureIp: undefined,
+
+ // Original text: 'Invalid parameters'
+ configIpErrorTitle: undefined,
+
+ // Original text: 'IP address and netmask required'
+ configIpErrorMessage: undefined,
+
+ // Original text: 'Static IP address'
+ staticIp: undefined,
+
+ // Original text: 'Netmask'
+ netmask: undefined,
+
+ // Original text: 'DNS'
+ dns: undefined,
+
+ // Original text: 'Gateway'
+ gateway: undefined,
+
+ // Original text: "Add a storage"
+ addSrDeviceButton: 'Dodaj przestrzeń dyskową',
+
+ // Original text: "Name"
+ srNameLabel: 'Nazwa',
+
+ // Original text: "Type"
+ srType: 'Typ',
+
+ // Original text: "Status"
+ pbdStatus: 'Status',
+
+ // Original text: "Connected"
+ pbdStatusConnected: 'Połączone',
+
+ // Original text: "Disconnected"
+ pbdStatusDisconnected: 'Rozłączone',
+
+ // Original text: "Connect"
+ pbdConnect: 'Połącz',
+
+ // Original text: "Disconnect"
+ pbdDisconnect: 'Rozłącz',
+
+ // Original text: "Forget"
+ pbdForget: 'Zapomnij',
+
+ // Original text: "Shared"
+ srShared: 'Udostępnione',
+
+ // Original text: "Not shared"
+ srNotShared: 'Nieudostępnione',
+
+ // Original text: "No storage detected"
+ pbdNoSr: 'Nie wykryto macierzy dyskowej',
+
+ // Original text: "Name"
+ patchNameLabel: 'Nazwa',
+
+ // Original text: "Install all patches"
+ patchUpdateButton: 'Instaluj wszystkie łatki',
+
+ // Original text: "Description"
+ patchDescription: 'Opis',
+
+ // Original text: "Applied date"
+ patchApplied: 'Applied date',
+
+ // Original text: "Size"
+ patchSize: 'Rozmiar',
+
+ // Original text: "Status"
+ patchStatus: 'Status',
+
+ // Original text: "Applied"
+ patchStatusApplied: 'Applied',
+
+ // Original text: "Missing patches"
+ patchStatusNotApplied: 'Brakujące łatki',
+
+ // Original text: "No patch detected"
+ patchNothing: 'Nie wykryto łatek',
+
+ // Original text: "Release date"
+ patchReleaseDate: 'Data wydania',
+
+ // Original text: "Guidance"
+ patchGuidance: 'Guidance',
+
+ // Original text: "Action"
+ patchAction: 'Akcja',
+
+ // Original text: "Applied patches"
+ hostAppliedPatches: 'Łatki zostały zastosowane',
+
+ // Original text: "Missing patches"
+ hostMissingPatches: 'Brakujące łatki',
+
+ // Original text: "Host up-to-date!"
+ hostUpToDate: 'Host posiada najnowsze łatki!',
+
+ // Original text: "Refresh patches"
+ refreshPatches: 'Odśwież łatki',
+
+ // Original text: "Install pool patches"
+ installPoolPatches: 'Instaluj pulę łatek',
+
+ // Original text: "Default SR"
+ defaultSr: 'Domyślny SR',
+
+ // Original text: "Set as default SR"
+ setAsDefaultSr: 'Ustaw domyślny SR',
+
+ // Original text: "General"
+ generalTabName: 'General',
+
+ // Original text: "Stats"
+ statsTabName: 'Statystyki',
+
+ // Original text: "Console"
+ consoleTabName: 'Konsola',
+
+ // Original text: "Container"
+ containersTabName: 'Kontener',
+
+ // Original text: "Snapshots"
+ snapshotsTabName: 'Snapshoty',
+
+ // Original text: "Logs"
+ logsTabName: 'Logi',
+
+ // Original text: "Advanced"
+ advancedTabName: 'Zaawansowane',
+
+ // Original text: "Network"
+ networkTabName: 'Sieć',
+
+ // Original text: "Disk{disks, plural, one {} other {s}}"
+ disksTabName: 'Dysk{disks, plural, one {} other {s}}',
+
+ // Original text: "halted"
+ powerStateHalted: 'Zatrzymany',
+
+ // Original text: "running"
+ powerStateRunning: 'Działający',
+
+ // Original text: "suspended"
+ powerStateSuspended: 'Uśpiony',
+
+ // Original text: "No Xen tools detected"
+ vmStatus: 'Nie wykryto Xen tools',
+
+ // Original text: "No IPv4 record"
+ vmName: 'Nie zapisano IPv4',
+
+ // Original text: "No IP record"
+ vmDescription: 'Nie zapisano IP',
+
+ // Original text: "Started {ago}"
+ vmSettings: 'Uruchomiono {ago}',
+
+ // Original text: "Current status:"
+ vmCurrentStatus: 'Obecny status:',
+
+ // Original text: "Not running"
+ vmNotRunning: 'Nie uruchomione',
+
+ // Original text: "No Xen tools detected"
+ noToolsDetected: 'Nie wykryto Xen tools',
+
+ // Original text: "No IPv4 record"
+ noIpv4Record: 'Nie zapisano IPv4',
+
+ // Original text: "No IP record"
+ noIpRecord: 'Nie zapisano IP',
+
+ // Original text: "Started {ago}"
+ started: 'Uruchomiono {ago}',
+
+ // Original text: "Paravirtualization (PV)"
+ paraVirtualizedMode: 'Parawirtualizacja (PV)',
+
+ // Original text: "Hardware virtualization (HVM)"
+ hardwareVirtualizedMode: 'Wirtualizacja sprzętowa (HVM)',
+
+ // Original text: "CPU usage"
+ statsCpu: 'Użycie CPU',
+
+ // Original text: "Memory usage"
+ statsMemory: 'Użycie pamięci',
+
+ // Original text: "Network throughput"
+ statsNetwork: 'Wydajność sieci',
+
+ // Original text: "Stacked values"
+ useStackedValuesOnStats: 'Skumulowane wartości',
+
+ // Original text: "Disk throughput"
+ statDisk: 'Wydajność dysku',
+
+ // Original text: "Last 10 minutes"
+ statLastTenMinutes: 'Ostatnie 10 minut',
+
+ // Original text: "Last 2 hours"
+ statLastTwoHours: 'Ostatnie 2 godziny',
+
+ // Original text: "Last week"
+ statLastWeek: 'Ostatni tydzień',
+
+ // Original text: "Last year"
+ statLastYear: 'Ostatni rok',
+
+ // Original text: "Copy"
+ copyToClipboardLabel: 'Kopia',
+
+ // Original text: "Ctrl+Alt+Del"
+ ctrlAltDelButtonLabel: 'Ctrl+Alt+Del',
+
+ // Original text: "Tip:"
+ 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.',
+
+ // Original text: "Hide infos"
+ hideHeaderTooltip: 'Ukryj informacje',
+
+ // Original text: "Show infos"
+ showHeaderTooltip: 'Pokaż informacje',
+
+ // Original text: "Name"
+ containerName: 'Nazwa',
+
+ // Original text: "Command"
+ containerCommand: 'Komenda',
+
+ // Original text: "Creation date"
+ containerCreated: 'Data utworzenia',
+
+ // Original text: "Status"
+ containerStatus: 'Status',
+
+ // Original text: "Action"
+ containerAction: 'Akcja',
+
+ // Original text: "No existing containers"
+ noContainers: 'Brak istniejących kontenerów',
+
+ // Original text: "Stop this container"
+ containerStop: 'Wyłącz ten kontener',
+
+ // Original text: "Start this container"
+ containerStart: 'Uruchom ten kontener',
+
+ // Original text: "Pause this container"
+ containerPause: 'Zatrzymaj ten kontener',
+
+ // Original text: "Resume this container"
+ containerResume: 'Wznów ten kontener',
+
+ // Original text: "Restart this container"
+ containerRestart: 'Uruchom ponownie ten kontener',
+
+ // Original text: "Action"
+ vdiAction: 'Akcja',
+
+ // Original text: "Attach disk"
+ vdiAttachDeviceButton: 'Dołącz dysk',
+
+ // Original text: "New disk"
+ vbdCreateDeviceButton: 'Nowy dysk',
+
+ // Original text: "Boot order"
+ vdiBootOrder: 'Kolejność bootowania',
+
+ // Original text: "Name"
+ vdiNameLabel: 'Nazwa',
+
+ // Original text: "Description"
+ vdiNameDescription: 'Opis',
+
+ // Original text: "Tags"
+ vdiTags: 'Tagi',
+
+ // Original text: "Size"
+ vdiSize: 'Rozmiar',
+
+ // Original text: "SR"
+ vdiSr: 'SR',
+
+ // Original text: "VM"
+ vdiVm: 'VM',
+
+ // Original text: "Migrate VDI"
+ vdiMigrate: 'Migruj VDI',
+
+ // Original text: "Destination SR:"
+ vdiMigrateSelectSr: 'Destination SR:',
+
+ // Original text: "Migrate all VDIs"
+ vdiMigrateAll: 'Migruj wszystkie VDIs',
+
+ // Original text: "No SR"
+ vdiMigrateNoSr: 'No SR',
+
+ // Original text: "A target SR is required to migrate a VDI"
+ vdiMigrateNoSrMessage: 'Docelowy SR jest wymagany żeby zmigrować VDI',
+
+ // Original text: "Forget"
+ vdiForget: 'Zapomnij',
+
+ // Original text: "Remove VDI"
+ vdiRemove: 'Usuń VDI',
+
+ // Original text: "Boot flag"
+ vbdBootableStatus: 'Boot flag',
+
+ // Original text: "Status"
+ vbdStatus: 'Status',
+
+ // Original text: "Connected"
+ vbdStatusConnected: 'Połączono',
+
+ // Original text: "Disconnected"
+ vbdStatusDisconnected: 'Rozłączono',
+
+ // Original text: "No disks"
+ vbdNoVbd: 'Brak dysków',
+
+ // Original text: "Connect VBD"
+ vbdConnect: 'Połącz z VBD',
+
+ // Original text: "Disconnect VBD"
+ vbdDisconnect: 'Rozłącz z VBD',
+
+ // Original text: "Bootable"
+ vbdBootable: 'Bootable',
+
+ // Original text: "Readonly"
+ vbdReadonly: 'Tylko do odcztu',
+
+ // Original text: "Create"
+ vbdCreate: 'Utwórz',
+
+ // Original text: "Disk name"
+ vbdNamePlaceHolder: 'Nazwa dysku',
+
+ // Original text: "Size"
+ vbdSizePlaceHolder: 'Rozmiar',
+
+ // Original text: "Save"
+ saveBootOption: 'Zapisz',
+
+ // Original text: "Reset"
+ resetBootOption: 'Reset',
+
+ // Original text: "New device"
+ vifCreateDeviceButton: 'Nowe urządzenie',
+
+ // Original text: "No interface"
+ vifNoInterface: 'Brak interfejsu',
+
+ // Original text: "Device"
+ vifDeviceLabel: 'Urządzenie',
+
+ // Original text: "MAC address"
+ vifMacLabel: 'Adres MAC',
+
+ // Original text: "MTU"
+ vifMtuLabel: 'MTU',
+
+ // Original text: "Network"
+ vifNetworkLabel: 'Sieć',
+
+ // Original text: "Status"
+ vifStatusLabel: 'Status',
+
+ // Original text: "Connected"
+ vifStatusConnected: 'Połączono',
+
+ // Original text: "Disconnected"
+ vifStatusDisconnected: 'Rozłączono',
+
+ // Original text: "Connect"
+ vifConnect: 'Połącz',
+
+ // Original text: "Disconnect"
+ vifDisconnect: 'Rozłącz',
+
+ // Original text: "Remove"
+ vifRemove: 'Usuń',
+
+ // Original text: "IP addresses"
+ vifIpAddresses: 'Adres IP',
+
+ // Original text: "Auto-generated if empty"
+ vifMacAutoGenerate: 'Auto-generated if empty',
+
+ // Original text: "Allowed IPs"
+ vifAllowedIps: 'Dopuszczone IPs',
+
+ // Original text: "No IPs"
+ vifNoIps: 'Brak IPs',
+
+ // Original text: "Network locked"
+ 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',
+
+ // Original text: "Network not locked"
+ vifUnLockedNetwork: 'Sieć niezablokowana',
+
+ // Original text: "Unknown network"
+ vifUnknownNetwork: 'Sieć nieznana',
+
+ // Original text: "Create"
+ vifCreate: 'Utwórz',
+
+ // Original text: "No snapshots"
+ noSnapshots: 'Brak snapshotów',
+
+ // Original text: "New snapshot"
+ snapshotCreateButton: 'Nowy snapshot',
+
+ // Original text: "Just click on the snapshot button to create one!"
+ tipCreateSnapshotLabel: 'Kliknij na guzik żeby stworzyć snapshota !',
+
+ // Original text: "Revert VM to this snapshot"
+ revertSnapshot: 'Przywróć VM do tego snapshota',
+
+ // Original text: "Remove this snapshot"
+ deleteSnapshot: 'Usuń snapshot',
+
+ // Original text: "Create a VM from this snapshot"
+ copySnapshot: 'Utwórz VM z tego snapshota',
+
+ // Original text: "Export this snapshot"
+ exportSnapshot: 'Exportuj tego snapshota',
+
+ // Original text: "Creation date"
+ snapshotDate: 'Data utworzenia',
+
+ // Original text: "Name"
+ snapshotName: 'Nazwa',
+
+ // Original text: "Action"
+ snapshotAction: 'Akcja',
+
+ // Original text: "Remove all logs"
+ logRemoveAll: 'Usuń wszystkie logi',
+
+ // Original text: "No logs so far"
+ noLogs: 'No logs so far',
+
+ // Original text: "Creation date"
+ logDate: 'Data utworzenia',
+
+ // Original text: "Name"
+ logName: 'Nazwa',
+
+ // Original text: "Content"
+ logContent: 'Zawartość',
+
+ // Original text: "Action"
+ logAction: 'Akcja',
+
+ // Original text: "Remove"
+ vmRemoveButton: 'Usuń',
+
+ // Original text: "Convert"
+ vmConvertButton: 'Konwertuj',
+
+ // Original text: "Xen settings"
+ xenSettingsLabel: 'Ustawienia Xena',
+
+ // Original text: "Guest OS"
+ guestOsLabel: 'Geust OS',
+
+ // Original text: "Misc"
+ miscLabel: 'Misc',
+
+ // Original text: "UUID"
+ uuid: 'UUID',
+
+ // Original text: "Virtualization mode"
+ virtualizationMode: 'Tryb wirtualizacji',
+
+ // Original text: "CPU weight"
+ cpuWeightLabel: 'CPU weight',
+
+ // Original text: "Default ({value, number})"
+ defaultCpuWeight: 'Domyślnie ({value, number})',
+
+ // Original text: "CPU cap"
+ cpuCapLabel: 'CPU cap',
+
+ // Original text: "Default ({value, number})"
+ defaultCpuCap: 'Domyślny ({value, number})',
+
+ // Original text: "PV args"
+ pvArgsLabel: 'PV args',
+
+ // Original text: "Xen tools status"
+ xenToolsStatus: 'Status Xen tools',
+
+ // Original text: '{status}'
+ xenToolsStatusValue: undefined,
+
+ // Original text: "OS name"
+ osName: 'Nazwa systemu',
+
+ // Original text: "OS kernel"
+ osKernel: 'Kernel systemu',
+
+ // Original text: "Auto power on"
+ autoPowerOn: 'Autoamtyczne uruchamianie',
+
+ // Original text: "HA"
+ ha: 'HA',
+
+ // Original text: "Original template"
+ originalTemplate: 'Oryginalny szablon',
+
+ // Original text: "Unknown"
+ unknownOsName: 'Nieznany',
+
+ // Original text: "Unknown"
+ unknownOsKernel: 'Nieznany',
+
+ // Original text: "Unknown"
+ unknownOriginalTemplate: 'Nieznany',
+
+ // Original text: "VM limits"
+ vmLimitsLabel: 'Limity VM',
+
+ // Original text: "CPU limits"
+ vmCpuLimitsLabel: 'Limity CPU',
+
+ // Original text: "Memory limits (min/max)"
+ vmMemoryLimitsLabel: 'Limity pamięci (min/max)',
+
+ // Original text: "vCPUs max:"
+ vmMaxVcpus: 'vCPUs max :',
+
+ // Original text: "Memory max:"
+ vmMaxRam: 'Pamięć max :',
+
+ // Original text: "Long click to add a name"
+ vmHomeNamePlaceholder: 'Long click to add a name',
+
+ // Original text: "Long click to add a description"
+ vmHomeDescriptionPlaceholder: 'Long click to add a description',
+
+ // Original text: "Click to add a name"
+ vmViewNamePlaceholder: 'Click to add a name',
+
+ // Original text: "Click to add a description"
+ vmViewDescriptionPlaceholder: 'Kliknij żeby dodać opis',
+
+ // Original text: "Click to add a name"
+ templateHomeNamePlaceholder: 'Kliknij żeby dodać nazwę',
+
+ // Original text: "Click to add a description"
+ templateHomeDescriptionPlaceholder: 'Kliknij żeby dodać opis',
+
+ // Original text: "Delete template"
+ templateDelete: 'Usuń szablon',
+
+ // Original text: "Delete VM template{templates, plural, one {} other {s}}"
+ 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ąć?',
+
+ // Original text: "Pool{pools, plural, one {} other {s}}"
+ poolPanel: 'Pula{pools, plural, one {} other {s}}',
+
+ // Original text: "Host{hosts, plural, one {} other {s}}"
+ hostPanel: 'Host{hosts, plural, one {} other {s}}',
+
+ // Original text: "VM{vms, plural, one {} other {s}}"
+ vmPanel: 'VM{vms, plural, one {} other {s}}',
+
+ // Original text: "RAM Usage"
+ memoryStatePanel: 'Użycie RAM',
+
+ // Original text: "CPUs Usage"
+ cpuStatePanel: 'Użycie CPUs',
+
+ // Original text: "VMs Power state"
+ vmStatePanel: 'Stan zasilania VMs',
+
+ // Original text: "Pending tasks"
+ taskStatePanel: 'Oczekujące zadania',
+
+ // Original text: "Users"
+ usersStatePanel: 'Użytkownicy',
+
+ // Original text: "Storage state"
+ srStatePanel: 'Status przestrzeni dyskowej',
+
+ // Original text: "{usage} (of {total})"
+ ofUsage: '{usage} (sur {total})',
+
+ // Original text: "No storage"
+ noSrs: 'No storage',
+
+ // Original text: "Name"
+ srName: 'Nazwa',
+
+ // Original text: "Pool"
+ srPool: 'Pula',
+
+ // Original text: "Host"
+ srHost: 'Host',
+
+ // Original text: "Type"
+ srFormat: 'Typ',
+
+ // Original text: "Size"
+ srSize: 'Size',
+
+ // Original text: "Usage"
+ srUsage: 'Usage',
+
+ // Original text: "used"
+ srUsed: 'Used',
+
+ // Original text: "free"
+ srFree: 'free',
+
+ // Original text: "Storage Usage"
+ srUsageStatePanel: 'Storage Usage',
+
+ // Original text: "Top 5 SR Usage (in %)"
+ 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}})',
+
+ // Original text: "Clear selection"
+ dashboardStatsButtonRemoveAll: 'Clear selection',
+
+ // Original text: "Add all hosts"
+ dashboardStatsButtonAddAllHost: 'Dodaj wszystkie hosty',
+
+ // Original text: "Add all VMs"
+ dashboardStatsButtonAddAllVM: 'Dodaj wszystkie VMs',
+
+ // Original text: "{value} {date, date, medium}"
+ weekHeatmapData: '{value} {date, date, medium}',
+
+ // Original text: "No data."
+ weekHeatmapNoData: 'No data.',
+
+ // Original text: "Weekly Heatmap"
+ weeklyHeatmap: 'Tygodniowa mapa cieplna',
+
+ // Original text: "Weekly Charts"
+ weeklyCharts: 'Wykresy tygodniowe',
+
+ // Original text: "Synchronize scale:"
+ weeklyChartsScaleInfo: 'Synchronize scale:',
+
+ // Original text: "Stats error"
+ statsDashboardGenericErrorTitle: 'Błąd statystyk',
+
+ // Original text: "There is no stats available for:"
+ statsDashboardGenericErrorMessage: 'Nie ma dostępnych statystyk dla:',
+
+ // Original text: "No selected metric"
+ noSelectedMetric: 'No selected metric',
+
+ // Original text: "Select"
+ statsDashboardSelectObjects: 'Wybierz',
+
+ // Original text: "Loading…"
+ metricsLoading: 'Ładowanie…',
+
+ // Original text: "Coming soon!"
+ comingSoon: 'Coming soon!',
+
+ // Original text: "Orphaned snapshot VDIs"
+ orphanedVdis: 'Orphaned snapshot VDIs',
+
+ // Original text: "Orphaned VMs snapshot"
+ orphanedVms: 'Orphaned VMs snapshot',
+
+ // Original text: "No orphans"
+ noOrphanedObject: 'No orphans',
+
+ // Original text: "Remove all orphaned snapshot VDIs"
+ removeAllOrphanedObject: 'Remove all orphaned snapshot VDIs',
+
+ // Original text: "Name"
+ vmNameLabel: 'Nazwa',
+
+ // Original text: "Description"
+ vmNameDescription: 'Opis',
+
+ // Original text: "Resident on"
+ vmContainer: 'Resident on',
+
+ // Original text: "Alarms"
+ alarmMessage: 'Alarmy',
+
+ // Original text: "No alarms"
+ noAlarms: 'Brak alarmów',
+
+ // Original text: "Date"
+ alarmDate: 'Data',
+
+ // Original text: "Content"
+ alarmContent: 'Zawartość',
+
+ // Original text: "Issue on"
+ alarmObject: 'Issue on',
+
+ // Original text: "Pool"
+ alarmPool: 'Pula',
+
+ // Original text: "Remove all alarms"
+ alarmRemoveAll: 'Usuń wszystkie alarmy',
+
+ // Original text: "{used}% used ({free} left)"
+ spaceLeftTooltip: '{used}% used ({free} left)',
+
+ // Original text: "Create a new VM on {select}"
+ newVmCreateNewVmOn: 'Stwórz nową VM w {select}',
+
+ // Original text: "Create a new VM on {select1} or {select2}"
+ newVmCreateNewVmOn2: 'Stwórz nową VM w {select1} lub {select2}',
+
+ // Original text: "You have no permission to create a VM"
+ newVmCreateNewVmNoPermission: 'Nie masz uprawnień do tworzenia VM',
+
+ // Original text: "Infos"
+ newVmInfoPanel: 'Informacje',
+
+ // Original text: "Name"
+ newVmNameLabel: 'Nazwa',
+
+ // Original text: "Template"
+ newVmTemplateLabel: 'Szablon',
+
+ // Original text: "Description"
+ newVmDescriptionLabel: 'Opis',
+
+ // Original text: "Performances"
+ newVmPerfPanel: 'CPU i RAM',
+
+ // Original text: "vCPUs"
+ newVmVcpusLabel: 'vCPUs',
+
+ // Original text: "RAM"
+ newVmRamLabel: 'RAM',
+
+ // Original text: "Static memory max"
+ newVmStaticMaxLabel: 'Pamieć statyczna max',
+
+ // Original text: "Dynamic memory min"
+ newVmDynamicMinLabel: 'Pamieć dynamiczna min',
+
+ // Original text: "Dynamic memory max"
+ newVmDynamicMaxLabel: 'Pamieć dynamiczna max',
+
+ // Original text: "Install settings"
+ newVmInstallSettingsPanel: 'Install settings',
+
+ // Original text: "ISO/DVD"
+ newVmIsoDvdLabel: 'ISO/DVD',
+
+ // Original text: "Network"
+ newVmNetworkLabel: 'Sieć',
+
+ // Original text: "e.g: http://httpredir.debian.org/debian"
+ newVmInstallNetworkPlaceHolder: 'ex : http://httpredir.debian.org/debian',
+
+ // Original text: "PV Args"
+ newVmPvArgsLabel: 'PV Args',
+
+ // Original text: "PXE"
+ newVmPxeLabel: 'PXE',
+
+ // Original text: "Interfaces"
+ newVmInterfacesPanel: 'Interfejsy',
+
+ // Original text: "MAC"
+ newVmMacLabel: 'MAC',
+
+ // Original text: "Add interface"
+ newVmAddInterface: 'Dodaj dodatkowy interfejs',
+
+ // Original text: "Disks"
+ newVmDisksPanel: 'Dyski',
+
+ // Original text: "SR"
+ newVmSrLabel: 'SR',
+
+ // Original text: "Bootable"
+ newVmBootableLabel: 'Bootable',
+
+ // Original text: "Size"
+ newVmSizeLabel: 'Rozmiar',
+
+ // Original text: "Add disk"
+ newVmAddDisk: 'Dodaj dodatkowy dysk',
+
+ // Original text: "Summary"
+ newVmSummaryPanel: 'Podsumowanie',
+
+ // Original text: "Create"
+ newVmCreate: 'Utwórz',
+
+ // Original text: "Reset"
+ newVmReset: 'Reset',
+
+ // Original text: "Select template"
+ newVmSelectTemplate: 'Wybierz szablon',
+
+ // Original text: "SSH key"
+ newVmSshKey: 'Klucz SSH',
+
+ // Original text: "Config drive"
+ newVmConfigDrive: 'Config drive',
+
+ // Original text: "Custom config"
+ newVmCustomConfig: 'Niestandardowa konfiguracja',
+
+ // Original text: "Boot VM after creation"
+ newVmBootAfterCreate: 'Boot VM po utworzeniu',
+
+ // Original text: "Auto-generated if empty"
+ newVmMacPlaceholder: 'Auto-generated if empty',
+
+ // Original text: "CPU weight"
+ newVmCpuWeightLabel: 'CPU weight',
+
+ // Original text: "Default: {value, number}"
+ newVmDefaultCpuWeight: 'Domyślnie: {value, number}',
+
+ // Original text: "CPU cap"
+ newVmCpuCapLabel: 'CPU cap',
+
+ // Original text: "Default: {value, number}"
+ newVmDefaultCpuCap: 'Domyślnie : {value, number}',
+
+ // Original text: "Cloud config"
+ newVmCloudConfig: 'Konfiguracja chmury',
+
+ // Original text: "Create VMs"
+ newVmCreateVms: 'Utwórz VMs',
+
+ // Original text: "Are you sure you want to create {nbVms} VMs?"
+ newVmCreateVmsConfirm: 'Jesteś pewny że chcesz utworzyć {nbVms} VMs ?',
+
+ // Original text: "Multiple VMs:"
+ newVmMultipleVms: 'Multiple VMs :',
+
+ // Original text: "Select a resource set:"
+ newVmSelectResourceSet: 'Select a resource set:',
+
+ // Original text: "Name pattern:"
+ newVmMultipleVmsPattern: 'Name pattern:',
+
+ // Original text: "e.g.: \\{name\\}_%"
+ newVmMultipleVmsPatternPlaceholder: 'np. : \\{name\\}_%',
+
+ // Original text: "First index:"
+ newVmFirstIndex: 'First index:',
+
+ // Original text: "Recalculate VMs number"
+ newVmNumberRecalculate: 'Przelicz ilość VMs',
+
+ // Original text: "Refresh VMs name"
+ newVmNameRefresh: 'Odśwież nazwę VMs',
+
+ // Original text: "Advanced"
+ newVmAdvancedPanel: 'Zaawansowane',
+
+ // Original text: "Show advanced settings"
+ newVmShowAdvanced: 'Pokaż ustawienia zaawansowane',
+
+ // Original text: "Hide advanced settings"
+ newVmHideAdvanced: 'Ukryj ustawienia zaawansowane',
+
+ // Original text: "Resource sets"
+ resourceSets: 'Resource sets',
+
+ // Original text: "No resource sets."
+ noResourceSets: 'No resource sets.',
+
+ // Original text: "Loading resource sets"
+ loadingResourceSets: 'Loading resource sets',
+
+ // Original text: "Resource set name"
+ resourceSetName: 'Resource set name',
+
+ // Original text: "Recompute all limits"
+ recomputeResourceSets: 'Przelicz wszystkie limity',
+
+ // Original text: "Save"
+ saveResourceSet: 'Zapisz',
+
+ // Original text: "Reset"
+ resetResourceSet: 'Reset',
+
+ // Original text: "Edit"
+ editResourceSet: 'Edytuj',
+
+ // Original text: "Delete"
+ deleteResourceSet: 'Usuń',
+
+ // Original text: "Delete resource set"
+ 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?',
+
+ // Original text: "Missing objects:"
+ resourceSetMissingObjects: 'Brakujące obiekty:',
+
+ // Original text: "vCPUs"
+ resourceSetVcpus: 'vCPUs',
+
+ // Original text: "Memory"
+ resourceSetMemory: 'Pamieć',
+
+ // Original text: "Storage"
+ resourceSetStorage: 'Przestrzeń dyskowa',
+
+ // Original text: "Unknown"
+ unknownResourceSetValue: 'Nieznany',
+
+ // Original text: "Available hosts"
+ availableHosts: 'Dostępne hosty',
+
+ // Original text: "Excluded hosts"
+ excludedHosts: 'Wykluczone hosty',
+
+ // Original text: "No hosts available."
+ 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.',
+
+ // Original text: "Maximum CPUs"
+ maxCpus: 'Maximum CPUs',
+
+ // Original text: "Maximum RAM (GiB)"
+ maxRam: 'Maximum RAM (GiB)',
+
+ // Original text: "Maximum disk space"
+ maxDiskSpace: 'Maximum disk space',
+
+ // Original text: "IP pool"
+ ipPool: 'Pula IP',
+
+ // Original text: "Quantity"
+ quantity: 'Quantity',
+
+ // Original text: "No limits."
+ noResourceSetLimits: 'Brak limitów.',
+
+ // Original text: "Total:"
+ totalResource: 'Łącznie :',
+
+ // Original text: "Remaining:"
+ remainingResource: 'Pozostało :',
+
+ // Original text: "Used:"
+ usedResource: 'Używane:',
+
+ // Original text: "New"
+ 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.',
+
+ // Original text: "No selected VMs."
+ noSelectedVms: 'No selected VMs.',
+
+ // Original text: "To Pool:"
+ vmImportToPool: 'To Pool:',
+
+ // Original text: "To SR:"
+ vmImportToSr: 'To SR:',
+
+ // Original text: "VMs to import"
+ vmsToImport: 'VMs to import',
+
+ // Original text: "Reset"
+ importVmsCleanList: 'Reset',
+
+ // Original text: "VM import success"
+ vmImportSuccess: 'import VM udany!',
+
+ // Original text: "VM import failed"
+ vmImportFailed: 'Import VM nieudany',
+
+ // Original text: "Import starting…"
+ startVmImport: 'Rozpoczęcie importowania…',
+
+ // Original text: "Export starting…"
+ startVmExport: 'Eksport rozpoczęty…',
+
+ // Original text: "N CPUs"
+ nCpus: 'N CPUs',
+
+ // Original text: "Memory"
+ vmMemory: 'Pamieć',
+
+ // Original text: "Disk {position} ({capacity})"
+ diskInfo: 'Dysk {position} ({capacity})',
+
+ // Original text: "Disk description"
+ diskDescription: 'Opis dysku',
+
+ // Original text: "No disks."
+ noDisks: 'Brak dysków.',
+
+ // Original text: "No networks."
+ noNetworks: 'Brak sieci.',
+
+ // Original text: "Network {name}"
+ networkInfo: 'Sieć {name}',
+
+ // Original text: "No description available"
+ noVmImportErrorDescription: 'Opis jest niedostępny',
+
+ // Original text: "Error:"
+ vmImportError: 'Błąd:',
+
+ // Original text: "{type} file:"
+ vmImportFileType: '{type} plik:',
+
+ // Original text: "Please to check and/or modify the VM configuration."
+ vmImportConfigAlert: 'Proszę sprawdzić lub zmodyfikować konfigurację VM.',
+
+ // Original text: "No pending tasks"
+ noTasks: 'Brak oczekujących zadań',
+
+ // Original text: "Currently, there are not any pending XenServer tasks"
+ xsTasks: 'Aktualnie, nie ma żadnych oczekujących zadań na HyperVisorze',
+
+ // Original text: "Schedules"
+ backupSchedules: 'Harmonogramy',
+
+ // Original text: "Get remote"
+ getRemote: 'Get remote',
+
+ // Original text: "List Remote"
+ listRemote: 'List Remote',
+
+ // Original text: "simple"
+ simpleBackup: 'simple',
+
+ // Original text: "delta"
+ delta: 'delta',
+
+ // Original text: "Restore Backups"
+ restoreBackups: 'Odtwórz kopie zapasowe',
+
+ // Original text: 'Click on a VM to display restore options'
+ restoreBackupsInfo: 'Kliknij w VM żeby wyświetlić możliwości odtworzenia',
+
+ // Original text: "Enabled"
+ remoteEnabled: 'Włączone',
+
+ // Original text: "Error"
+ remoteError: 'Błąd',
+
+ // Original text: "No backup available"
+ noBackup: 'Brak dostępnej kopi zapasowej',
+
+ // Original text: "VM Name"
+ backupVmNameColumn: 'Nazwa VM',
+
+ // Original text: 'Tags'
+ backupTags: 'Tagi',
+
+ // Original text: "Last Backup"
+ lastBackupColumn: 'Ostatnia kopia zapasowa',
+
+ // Original text: "Available Backups"
+ availableBackupsColumn: 'Dostępne kopie zapasowe',
+
+ // Original text: 'Missing parameters'
+ backupRestoreErrorTitle: 'Brakujące parametry',
+
+ // Original text: 'Choose a SR and a backup'
+ backupRestoreErrorMessage: 'Wybierz SR i kopię zapasową',
+
+ // Original text: "Display backups"
+ displayBackup: 'Wyświetl kopie zapasowe',
+
+ // Original text: "Import VM"
+ importBackupTitle: 'Importuj VM',
+
+ // Original text: "Starting your backup import"
+ importBackupMessage: 'Rozpoczynanie imortu kopii zapasowej',
+
+ // Original text: "VMs to backup"
+ 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}}',
+
+ // 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}}?',
+
+ // 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',
+
+ // Original text: "Add host"
+ addHostModalTitle: 'Dodaj hosta',
+
+ // Original text: "Are you sure you want to add {host} to {pool}?"
+ addHostModalMessage: 'Jesteś pewny że chcesz dodać hosta{host} do {pool}?',
+
+ // Original text: "Restart host"
+ restartHostModalTitle: 'Restart hosta',
+
+ // Original text: "This will restart your host. Do you want to continue?"
+ 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}}',
+
+ // 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}} ?",
+
+ // 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}}?',
+
+ // 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}}?',
+
+ // 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}}?',
+
+ // 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}}?',
+
+ // Original text: "Restart VM"
+ restartVmModalTitle: 'Restart VM',
+
+ // Original text: "Are you sure you want to restart {name}?"
+ restartVmModalMessage: 'Na pewno chcesz zrestartować {name}?',
+
+ // Original text: "Stop VM"
+ stopVmModalTitle: 'Zatrzymaj VM',
+
+ // Original text: "Are you sure you want to stop {name}?"
+ stopVmModalMessage: 'Na pewno chcesz wyłączyć {name}?',
+
+ // Original text: "Restart VM{vms, plural, one {} other {s}}"
+ 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}}?',
+
+ // 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}}?',
+
+ // 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',
+
+ // 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',
+
+ // Original text: "Migrate VM"
+ migrateVmModalTitle: 'Migruj VM',
+
+ // Original text: "Select a destination host:"
+ migrateVmSelectHost: 'Select a destination host:',
+
+ // Original text: "Select a migration network:"
+ migrateVmSelectMigrationNetwork: 'Select a migration network:',
+
+ // Original text: "For each VDI, select an SR:"
+ migrateVmSelectSrs: 'For each VDI, select an SR:',
+
+ // Original text: "For each VIF, select a network:"
+ migrateVmSelectNetworks: 'For each VIF, select a network:',
+
+ // Original text: "Select a destination SR:"
+ migrateVmsSelectSr: 'Select a destination SR:',
+
+ // Original text: "Select a destination SR for local disks:"
+ migrateVmsSelectSrIntraPool: 'Select a destination SR for local disks:',
+
+ // Original text: "Select a network on which to connect each VIF:"
+ migrateVmsSelectNetwork: 'Select a network on which to connect each VIF:',
+
+ // Original text: "Smart mapping"
+ migrateVmsSmartMapping: 'Smart mapping',
+
+ // Original text: "Name"
+ migrateVmName: 'Nazwa',
+
+ // Original text: "SR"
+ migrateVmSr: 'SR',
+
+ // Original text: "VIF"
+ migrateVmVif: 'VIF',
+
+ // Original text: "Network"
+ migrateVmNetwork: 'Sieć',
+
+ // Original text: "No target host"
+ migrateVmNoTargetHost: 'No target host',
+
+ // Original text: "A target host is required to migrate a VM"
+ migrateVmNoTargetHostMessage: 'A target host is required to migrate a VM',
+
+ // Original text: "Delete VDI"
+ 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',
+
+ // Original text: "Revert your VM"
+ revertVmModalTitle: 'Revert your VM',
+
+ // Original text: "Delete snapshot"
+ deleteSnapshotModalTitle: 'Usuń snapshot',
+
+ // Original text: "Are you sure you want to delete this snapshot?"
+ 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.',
+
+ // Original text: "Snapshot before"
+ revertVmModalSnapshotBefore: 'Snapshot before',
+
+ // Original text: "Import a {name} Backup"
+ importBackupModalTitle: 'Import a {name} Backup',
+
+ // Original text: "Start VM after restore"
+ importBackupModalStart: 'Start VM after restore',
+
+ // Original text: "Select your backup…"
+ 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?',
+
+ // Original text: "Remove all logs"
+ removeAllLogsModalTitle: 'Usuń wszystkie logi',
+
+ // Original text: "Are you sure you want to remove all logs?"
+ removeAllLogsModalWarning: 'Jesteś pewien że chcesz usunąć wszystkie logi?',
+
+ // Original text: "This operation is definitive."
+ definitiveMessageModal: 'This operation is definitive.',
+
+ // Original text: "Previous SR Usage"
+ 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.',
+
+ // 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.',
+
+ // 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 ?',
+
+ // 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!',
+
+ // Original text: "Host"
+ serverHost: 'Host',
+
+ // Original text: "Username"
+ serverUsername: 'Nazwa użytkownika',
+
+ // Original text: "Password"
+ serverPassword: 'Hasło',
+
+ // Original text: "Action"
+ serverAction: 'Akcja',
+
+ // Original text: "Read Only"
+ serverReadOnly: 'Tylko do odczytu',
+
+ // Original text: "Disconnect server"
+ serverDisconnect: 'Rozłącz serwer',
+
+ // Original text: "username"
+ serverPlaceHolderUser: 'Użytkownik',
+
+ // Original text: "password"
+ serverPlaceHolderPassword: 'hasło',
+
+ // Original text: "address[:port]"
+ serverPlaceHolderAddress: 'adres[:port]',
+
+ // Original text: "Connect"
+ serverConnect: 'Połącz',
+
+ // Original text: "Copy VM"
+ copyVm: 'Copy VM',
+
+ // Original text: "Are you sure you want to copy this VM to {SR}?"
+ copyVmConfirm: 'Are you sure you want to copy this VM to {SR}?',
+
+ // Original text: "Name"
+ copyVmName: 'Nazwa',
+
+ // Original text: "Name pattern"
+ copyVmNamePattern: 'Name pattern',
+
+ // Original text: "If empty: name of the copied VM"
+ copyVmNamePlaceholder: 'If empty: name of the copied VM',
+
+ // Original text: "e.g.: \"\\{name\\}_COPY\""
+ copyVmNamePatternPlaceholder: 'np. : "\\{name\\}_COPY"',
+
+ // Original text: "Select SR"
+ copyVmSelectSr: 'Wybierz pulę dyskową',
+
+ // Original text: "Use compression"
+ copyVmCompress: 'Użyj kompresji',
+
+ // Original text: "No target SR"
+ copyVmsNoTargetSr: 'No target SR',
+
+ // Original text: "A target SR is required to copy a VM"
+ copyVmsNoTargetSrMessage: 'A target SR is required to copy a VM',
+
+ // Original text: "Detach host"
+ 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.',
+
+ // Original text: "Detach"
+ detachHost: 'Detach',
+
+ // Original text: "Create network"
+ newNetworkCreate: 'Utwórz sieć',
+
+ // Original text: "Create bonded network"
+ newBondedNetworkCreate: 'Create bonded network',
+
+ // Original text: "Interface"
+ newNetworkInterface: 'Interface',
+
+ // Original text: "Name"
+ newNetworkName: 'Nazwa',
+
+ // Original text: "Description"
+ newNetworkDescription: 'Opis',
+
+ // Original text: "VLAN"
+ newNetworkVlan: 'VLAN',
+
+ // Original text: "No VLAN if empty"
+ newNetworkDefaultVlan: 'No VLAN if empty',
+
+ // Original text: "MTU"
+ newNetworkMtu: 'MTU',
+
+ // Original text: "Default: 1500"
+ newNetworkDefaultMtu: 'Domyślnie : 1500',
+
+ // Original text: "Name required"
+ newNetworkNoNameErrorTitle: 'Nazwa wymagana',
+
+ // Original text: "A name is required to create a network"
+ newNetworkNoNameErrorMessage: 'Nazwa jest wymagana do utworzenia sieci',
+
+ // Original text: "Bond mode"
+ newNetworkBondMode: 'Tryb bondingu',
+
+ // Original text: "Delete network"
+ deleteNetwork: 'Usuń sieć',
+
+ // Original text: "Are you sure you want to delete this network?"
+ deleteNetworkConfirm: 'Jesteś pewien że chcesz usunąć te sieć ?',
+
+ // Original text: "This network is currently in use"
+ networkInUse: 'Ta sieć jest obecnie używana ',
+
+ // Original text: "Bonded"
+ pillBonded: 'Bonded',
+
+ // Original text: "Host"
+ addHostSelectHost: 'Host',
+
+ // Original text: "No host"
+ addHostNoHost: 'No host',
+
+ // Original text: "No host selected to be added"
+ addHostNoHostMessage: 'No host selected to be added',
+
+ // Original text: "Xen Orchestra"
+ xenOrchestra: 'Xen Orchestra',
+
+ // Original text: "server"
+ xenOrchestraServer: 'serwer',
+
+ // Original text: "web client"
+ xenOrchestraWeb: 'web klient',
+
+ // Original text: "No pro support provided!"
+ noProSupport: 'No pro support provided!',
+
+ // Original text: "Use in production at your own risks"
+ noProductionUse: 'Używanie produkcyjne na własne ryzyko',
+
+ // Original text: "You can download our turnkey appliance at {website}"
+ downloadXoaFromWebsite: 'You can download our turnkey appliance at {website}',
+
+ // Original text: "Bug Tracker"
+ bugTracker: 'Bug Tracker',
+
+ // Original text: "Issues? Report it!"
+ bugTrackerText: 'Issues? Report it!',
+
+ // Original text: "Community"
+ community: 'Społeczność',
+
+ // Original text: "Join our community forum!"
+ communityText: 'Dołącz do forum społeczności !',
+
+ // Original text: "Free Trial for Premium Edition!"
+ freeTrial: 'Free Trial for Premium Edition!',
+
+ // Original text: "Request your trial now!"
+ freeTrialNow: 'Request your trial now!',
+
+ // Original text: "Any issue?"
+ issues: 'Any issue?',
+
+ // Original text: "Problem? Contact us!"
+ issuesText: 'Masz problem? Zkontaktuj się z nami!',
+
+ // Original text: "Documentation"
+ documentation: 'Dokumentacja',
+
+ // Original text: "Read our official doc"
+ documentationText: 'Read our official doc',
+
+ // Original text: "Pro support included"
+ proSupportIncluded: 'Pro support included',
+
+ // Original text: "Acces your XO Account"
+ xoAccount: 'Acces your XO Account',
+
+ // Original text: "Report a problem"
+ openTicket: 'Raportuj problem',
+
+ // Original text: "Problem? Open a ticket!"
+ openTicketText: 'Masz problem? Utwórz zgłoszenie!',
+
+ // Original text: "Upgrade needed"
+ upgradeNeeded: 'Upgrade needed',
+
+ // Original text: "Upgrade now!"
+ upgradeNow: 'Aktualizuj teraz!',
+
+ // Original text: "Or"
+ or: 'Lub',
+
+ // Original text: "Try it for free!"
+ tryIt: 'Spróbuj za darmo!',
+
+ // Original text: "This feature is available starting from {plan} Edition"
+ 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.',
+
+ // Original text: "Updates"
+ updateTitle: 'Aktualizuj',
+
+ // Original text: "Registration"
+ registration: 'Rejestracja',
+
+ // Original text: "Trial"
+ trial: 'Trial',
+
+ // Original text: "Settings"
+ settings: 'Ustawienia',
+
+ // Original text: "Proxy settings"
+ proxySettings: 'Ustawienia proxy',
+
+ // Original text: "Host (myproxy.example.org)"
+ proxySettingsHostPlaceHolder: 'Host (mojeproxy.przyklad.pl)',
+
+ // Original text: "Port (eg: 3128)"
+ proxySettingsPortPlaceHolder: 'Port (np : 3128)',
+
+ // Original text: "Username"
+ proxySettingsUsernamePlaceHolder: 'Użytkownik',
+
+ // Original text: "Password"
+ proxySettingsPasswordPlaceHolder: 'Hasło',
+
+ // Original text: "Your email account"
+ updateRegistrationEmailPlaceHolder: 'Twoje konto email',
+
+ // Original text: "Your password"
+ updateRegistrationPasswordPlaceHolder: 'Twoje hasło',
+
+ // Original text: "Update"
+ update: 'Aktualizuj',
+
+ // Original text: "Refresh"
+ refresh: 'Odśwież',
+
+ // Original text: "Upgrade"
+ upgrade: 'Aktualizuj',
+
+ // Original text: "No updater available for Community Edition"
+ 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',
+
+ // 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',
+
+ // Original text: "Current version:"
+ currentVersion: 'Obecna wersja:',
+
+ // Original text: "Register"
+ register: 'Rejestruj',
+
+ // Original text: "Edit registration"
+ 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.',
+
+ // 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.',
+
+ // 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ę',
+
+ // 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.',
+
+ // Original text: "No update information available"
+ noUpdateInfo: 'No update information available',
+
+ // Original text: "Update information may be available"
+ waitingUpdateInfo: 'Update information may be available',
+
+ // Original text: "Your XOA is up-to-date"
+ upToDate: 'Twoje XOA jest aktualne',
+
+ // Original text: "You need to update your XOA (new version is available)"
+ mustUpgrade: 'You need to update your XOA (new version is available)',
+
+ // Original text: "Your XOA is not registered for updates"
+ registerNeeded: 'Your XOA is not registered for updates',
+
+ // Original text: "Can't fetch update information"
+ updaterError: 'Nie mogę pobrać aktualizacji',
+
+ // Original text: "Upgrade successful"
+ 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 ?',
+
+ // 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',
+
+ // 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:",
+
+ // 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.',
+
+ // Original text: "Connect PIF"
+ connectPif: 'Connect PIF',
+
+ // Original text: "Are you sure you want to connect this PIF?"
+ connectPifConfirm: 'Are you sure you want to connect this PIF?',
+
+ // Original text: "Disconnect PIF"
+ disconnectPif: 'Disconnect PIF',
+
+ // Original text: "Are you sure you want to disconnect this PIF?"
+ disconnectPifConfirm: 'Are you sure you want to disconnect this PIF ?',
+
+ // Original text: "Delete PIF"
+ deletePif: 'Delete PIF',
+
+ // Original text: "Are you sure you want to delete this PIF?"
+ deletePifConfirm: 'Are you sure you want to delete this PIF?',
+
+ // Original text: "Username"
+ username: 'Użytkownik',
+
+ // Original text: "Password"
+ password: 'Hasło',
+
+ // Original text: "Language"
+ language: 'Język',
+
+ // Original text: "Old password"
+ oldPasswordPlaceholder: 'Stare hasło',
+
+ // Original text: "New password"
+ newPasswordPlaceholder: 'Nowe hasło',
+
+ // Original text: "Confirm new password"
+ confirmPasswordPlaceholder: 'Potwierdź nowe hasło',
+
+ // Original text: "Confirmation password incorrect"
+ confirmationPasswordError: 'Potwierdzenie hasła niepoprawne',
+
+ // Original text: "Password does not match the confirm password."
+ confirmationPasswordErrorBody: 'Hasło nie zgadza się z potwierdzeniem',
+
+ // Original text: "Password changed"
+ pwdChangeSuccess: 'Hasło zmienione',
+
+ // Original text: "Your password has been successfully changed."
+ pwdChangeSuccessBody: 'Twoje hasło zostało pomyślnie zmienione',
+
+ // Original text: "Incorrect password"
+ 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.',
+
+ // Original text: "OK"
+ changePasswordOk: 'OK',
+
+ // Original text: "SSH keys"
+ sshKeys: 'Klucze SSH',
+
+ // Original text: "New SSH key"
+ newSshKey: 'Nowy klucz SSH',
+
+ // Original text: "Delete"
+ deleteSshKey: 'Usuń',
+
+ // Original text: "No SSH keys"
+ noSshKeys: 'Brak kluczy SSH',
+
+ // Original text: "New SSH key"
+ newSshKeyModalTitle: 'Nowy klucz SSH',
+
+ // Original text: "Invalid key"
+ sshKeyErrorTitle: 'Nieprawidłowy klucz',
+
+ // Original text: "An SSH key requires both a title and a key."
+ sshKeyErrorMessage: 'An SSH key requires both a title and a key.',
+
+ // Original text: "Title"
+ title: 'Title',
+
+ // Original text: "Key"
+ key: 'Klucz',
+
+ // Original text: "Delete SSH key"
+ 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}?',
+
+ // Original text: "Others"
+ others: 'Inne',
+
+ // Original text: "Loading logs…"
+ loadingLogs: 'Ładowanie logów…',
+
+ // Original text: "User"
+ logUser: 'Użytkownik',
+
+ // Original text: "Method"
+ logMethod: 'Metoda',
+
+ // Original text: "Params"
+ logParams: 'Params',
+
+ // Original text: "Message"
+ logMessage: 'Wiadomość',
+
+ // Original text: "Error"
+ logError: 'Błąd',
+
+ // Original text: "Display details"
+ logDisplayDetails: 'Wyświetl szczegóły',
+
+ // Original text: "Date"
+ logTime: 'Data',
+
+ // Original text: "No stack trace"
+ logNoStackTrace: 'No stack trace',
+
+ // Original text: "No params"
+ logNoParams: 'No params',
+
+ // Original text: "Delete log"
+ logDelete: 'Usuń logi',
+
+ // Original text: "Delete all logs"
+ logDeleteAll: 'Usuń wszystkie logi',
+
+ // Original text: "Delete all logs"
+ logDeleteAllTitle: 'Usuń wszystkie logi',
+
+ // Original text: "Are you sure you want to delete all the logs?"
+ logDeleteAllMessage: 'Are you sure you want to delete all the logs?',
+
+ // Original text: "Name"
+ ipPoolName: 'Nazwa',
+
+ // Original text: "IPs"
+ ipPoolIps: 'IPs',
+
+ // Original text: "IPs (e.g.: 1.0.0.12-1.0.0.17;1.0.0.23)"
+ ipPoolIpsPlaceholder: 'IPs (np.: 1.0.0.12-1.0.0.17;1.0.0.23)',
+
+ // Original text: "Networks"
+ ipPoolNetworks: 'Sieci',
+
+ // Original text: "No IP pools"
+ ipsNoIpPool: 'Brak puli IP',
+
+ // Original text: "Create"
+ ipsCreate: 'Utwórz',
+
+ // Original text: "Delete all IP pools"
+ ipsDeleteAllTitle: 'Usuń wszystkie pule IP',
+
+ // Original text: "Are you sure you want to delete all the IP pools?"
+ ipsDeleteAllMessage: 'Jesteś pewien że chcesz usunąć wszystkie pule IP?',
+
+ // Original text: "VIFs"
+ ipsVifs: 'VIFs',
+
+ // Original text: "Not used"
+ ipsNotUsed: 'Nieużywany',
+
+ // Original text: "Keyboard shortcuts"
+ shortcutModalTitle: 'Skróty klawiszowe',
+
+ // Original text: "Global"
+ shortcut_XoApp: 'Global',
+
+ // Original text: "Go to hosts list"
+ shortcut_GO_TO_HOSTS: 'Idź do listy hostów',
+
+ // Original text: "Go to pools list"
+ shortcut_GO_TO_POOLS: 'Idź do listy pul',
+
+ // Original text: "Go to VMs list"
+ shortcut_GO_TO_VMS: 'Idź do listy VMs',
+
+ // Original text: "Create a new VM"
+ shortcut_CREATE_VM: 'Utwórz nową VM',
+
+ // Original text: "Unfocus field"
+ shortcut_UNFOCUS: 'Niepodświetlone pole',
+
+ // Original text: "Show shortcuts key bindings"
+ shortcut_HELP: 'Show shortcuts key bindings',
+
+ // Original text: "Home"
+ shortcut_Home: 'Home',
+
+ // Original text: "Focus search bar"
+ shortcut_SEARCH: 'Focus search bar',
+
+ // Original text: "Next item"
+ shortcut_NAV_DOWN: 'Next item',
+
+ // Original text: "Previous item"
+ shortcut_NAV_UP: 'Previous item',
+
+ // Original text: "Select item"
+ shortcut_SELECT: 'Select item',
+
+ // Original text: "Open"
+ shortcut_JUMP_INTO: 'Otwarte',
+
+ // Original text: "VM"
+ settingsAclsButtonTooltipVM: 'VM',
+
+ // Original text: "Hosts"
+ settingsAclsButtonTooltiphost: 'Hosty',
+
+ // Original text: "Pool"
+ settingsAclsButtonTooltippool: 'Pule',
+
+ // Original text: "SR"
+ settingsAclsButtonTooltipSR: 'SR',
+
+ // Original text: "Network"
+ settingsAclsButtonTooltipnetwork: 'Sieć',
+}
diff --git a/packages/xo-web/src/common/intl/locales/pt.js b/packages/xo-web/src/common/intl/locales/pt.js
new file mode 100644
index 000000000..bace43b65
--- /dev/null
+++ b/packages/xo-web/src/common/intl/locales/pt.js
@@ -0,0 +1,3170 @@
+// See http://momentjs.com/docs/#/use-it/browserify/
+import 'moment/locale/pt'
+
+import reactIntlData from 'react-intl/locale-data/pt'
+import { addLocaleData } from 'react-intl'
+addLocaleData(reactIntlData)
+
+// ===================================================================
+
+export default {
+ // Original text: 'Connecting'
+ statusConnecting: undefined,
+
+ // Original text: 'Disconnected'
+ statusDisconnected: undefined,
+
+ // Original text: 'Loading…'
+ statusLoading: undefined,
+
+ // Original text: 'Page not found'
+ errorPageNotFound: undefined,
+
+ // Original text: 'no such item'
+ errorNoSuchItem: undefined,
+
+ // Original text: "Long click to edit"
+ editableLongClickPlaceholder: 'Longo clique para editar',
+
+ // Original text: "Click to edit"
+ editableClickPlaceholder: 'Clique para editar',
+
+ // Original text: "OK"
+ alertOk: 'OK',
+
+ // Original text: "OK"
+ confirmOk: 'Confirmar',
+
+ // Original text: "Cancel"
+ confirmCancel: 'Cancelar',
+
+ // Original text: 'On error'
+ onError: undefined,
+
+ // Original text: 'Successful'
+ successful: undefined,
+
+ // Original text: 'Copy to clipboard'
+ copyToClipboard: undefined,
+
+ // Original text: 'Master'
+ pillMaster: undefined,
+
+ // Original text: "Home"
+ homePage: 'Principal',
+
+ // Original text: 'VMs'
+ homeVmPage: undefined,
+
+ // Original text: 'Hosts'
+ homeHostPage: undefined,
+
+ // Original text: 'Pools'
+ homePoolPage: undefined,
+
+ // Original text: 'Templates'
+ homeTemplatePage: undefined,
+
+ // Original text: "Dashboard"
+ dashboardPage: 'Painel de Controle',
+
+ // Original text: "Overview"
+ overviewDashboardPage: 'Visão Geral',
+
+ // Original text: "Visualizations"
+ overviewVisualizationDashboardPage: 'Visualizações',
+
+ // Original text: "Statistics"
+ overviewStatsDashboardPage: 'Estatisticas',
+
+ // Original text: "Health"
+ overviewHealthDashboardPage: 'Diagnóstico',
+
+ // Original text: "Self service"
+ selfServicePage: 'Auto-Serviço',
+
+ // Original text: "Backup"
+ backupPage: 'Backup',
+
+ // Original text: "Jobs"
+ jobsPage: 'Tarefas',
+
+ // Original text: "Updates"
+ updatePage: 'Atualizações',
+
+ // Original text: "Settings"
+ settingsPage: 'Configurações',
+
+ // Original text: "Servers"
+ settingsServersPage: 'Servidores',
+
+ // Original text: "Users"
+ settingsUsersPage: 'Usuários',
+
+ // Original text: "Groups"
+ settingsGroupsPage: 'Grupos',
+
+ // Original text: "ACLs"
+ settingsAclsPage: 'Controle de Acessos',
+
+ // Original text: "Plugins"
+ settingsPluginsPage: 'Plugins',
+
+ // Original text: 'Logs'
+ settingsLogsPage: undefined,
+
+ // Original text: 'IPs'
+ settingsIpsPage: undefined,
+
+ // Original text: "About"
+ aboutPage: 'Sobre',
+
+ // Original text: "New"
+ newMenu: 'Novo(a)',
+
+ // Original text: "Tasks"
+ taskMenu: 'Tarefas',
+
+ // Original text: 'Tasks'
+ taskPage: undefined,
+
+ // Original text: "VM"
+ newVmPage: 'VM',
+
+ // Original text: "Storage"
+ newSrPage: 'Armazenamento (Storage)',
+
+ // Original text: "Server"
+ newServerPage: 'Servidor',
+
+ // Original text: "Import"
+ newImport: 'Importar',
+
+ // Original text: "Overview"
+ backupOverviewPage: 'Visão Geral',
+
+ // Original text: "New"
+ backupNewPage: 'Novo(a)',
+
+ // Original text: "Remotes"
+ backupRemotesPage: 'Armazenamento a distância',
+
+ // Original text: "Restore"
+ backupRestorePage: 'Recuperar',
+
+ // Original text: "Schedule"
+ schedule: 'Agendamento',
+
+ // Original text: "New VM backup"
+ newVmBackup: 'Criar novo backup VM',
+
+ // Original text: "Edit VM backup"
+ editVmBackup: 'Editar backup VM',
+
+ // Original text: "Backup"
+ backup: 'Backup',
+
+ // Original text: "Rolling Snapshot"
+ rollingSnapshot: 'Snapshots ativos',
+
+ // Original text: "Delta Backup"
+ deltaBackup: 'Backup Diferencial',
+
+ // Original text: "Disaster Recovery"
+ disasterRecovery: 'Recuperação de Desastres',
+
+ // Original text: "Continuous Replication"
+ continuousReplication: 'Replicação Contínua',
+
+ // Original text: "Overview"
+ jobsOverviewPage: 'Visão Geral',
+
+ // Original text: "New"
+ jobsNewPage: 'Novo(a)',
+
+ // Original text: "Scheduling"
+ jobsSchedulingPage: 'Agendamentos',
+
+ // Original text: "Custom Job"
+ customJob: 'Personalização do Trabalho',
+
+ // Original text: 'User'
+ userPage: undefined,
+
+ // Original text: 'No support'
+ noSupport: undefined,
+
+ // Original text: 'Free upgrade!'
+ freeUpgrade: undefined,
+
+ // Original text: "Sign out"
+ signOut: 'Sair',
+
+ // Original text: 'Edit my settings {username}'
+ editUserProfile: undefined,
+
+ // Original text: "Fetching data…"
+ homeFetchingData: 'Obtendo dados…',
+
+ // Original text: "Welcome on Xen Orchestra!"
+ homeWelcome: 'Bem-vindo ao Xen Orchestra',
+
+ // Original text: "Add your XenServer hosts or pools"
+ homeWelcomeText: 'Adicione seu XenServer hosts e pools',
+
+ // Original text: "Want some help?"
+ homeHelp: 'Posso te ajudar?',
+
+ // Original text: "Add server"
+ homeAddServer: 'Adicionar Servidor',
+
+ // Original text: "Online Doc"
+ homeOnlineDoc: 'Documentação Online',
+
+ // Original text: "Pro Support"
+ homeProSupport: 'Suporte Especializado',
+
+ // Original text: "There are no VMs!"
+ homeNoVms: 'Não foram encontradas VMs!',
+
+ // Original text: "Or…"
+ homeNoVmsOr: 'Ou…',
+
+ // Original text: "Import VM"
+ homeImportVm: 'Importar VM',
+
+ // Original text: "Import an existing VM in xva format"
+ homeImportVmMessage: 'Importar uma VM existente no formato XVA',
+
+ // Original text: "Restore a backup"
+ homeRestoreBackup: 'Restaurar um backup',
+
+ // Original text: "Restore a backup from a remote store"
+ homeRestoreBackupMessage: 'Restaurar um backup remoto',
+
+ // Original text: "This will create a new VM"
+ homeNewVmMessage: 'Pronto para criar uma nova VM?',
+
+ // Original text: "Filters"
+ homeFilters: 'Filtros',
+
+ // Original text: 'No results! Click here to reset your filters'
+ homeNoMatches: undefined,
+
+ // Original text: "Pool"
+ homeTypePool: 'Pool',
+
+ // Original text: "Host"
+ homeTypeHost: 'Host',
+
+ // Original text: "VM"
+ homeTypeVm: 'VM',
+
+ // Original text: "SR"
+ homeTypeSr: 'SR',
+
+ // Original text: 'Template'
+ homeTypeVmTemplate: undefined,
+
+ // Original text: "Sort"
+ homeSort: 'Classificar',
+
+ // Original text: "Pools"
+ homeAllPools: 'Pools',
+
+ // Original text: "Hosts"
+ homeAllHosts: 'Hosts',
+
+ // Original text: "Tags"
+ homeAllTags: 'Etiquetas',
+
+ // Original text: "New VM"
+ homeNewVm: 'Criar nova VM',
+
+ // Original text: "Running hosts"
+ homeFilterRunningHosts: 'Hosts Ativos',
+
+ // Original text: "Disabled hosts"
+ homeFilterDisabledHosts: 'Hosts Desativados',
+
+ // Original text: "Running VMs"
+ homeFilterRunningVms: 'VMs Ativas',
+
+ // Original text: "Non running VMs"
+ homeFilterNonRunningVms: 'VMs Paradas',
+
+ // Original text: "Pending VMs"
+ homeFilterPendingVms: 'VMs pendentes',
+
+ // Original text: "HVM guests"
+ homeFilterHvmGuests: 'HVM guests',
+
+ // Original text: "Tags"
+ homeFilterTags: 'Etiquetas',
+
+ // Original text: "Sort by"
+ homeSortBy: 'Ordenar por',
+
+ // Original text: "Name"
+ homeSortByName: 'Nome',
+
+ // Original text: "Power state"
+ homeSortByPowerstate: 'Estado de energia',
+
+ // Original text: "RAM"
+ homeSortByRAM: 'RAM',
+
+ // Original text: "vCPUs"
+ homeSortByvCPUs: 'vCPUs',
+
+ // Original text: 'CPUs'
+ homeSortByCpus: undefined,
+
+ // Original text: '{displayed, number}x {icon} (on {total, number})'
+ homeDisplayedItems: undefined,
+
+ // Original text: '{selected, number}x {icon} selected (on {total, number})'
+ homeSelectedItems: undefined,
+
+ // Original text: "More"
+ homeMore: 'Mais',
+
+ // Original text: "Migrate to…"
+ homeMigrateTo: 'Migrar para…',
+
+ // Original text: 'Missing patches'
+ homeMissingPaths: undefined,
+
+ // Original text: 'Master:'
+ homePoolMaster: undefined,
+
+ // Original text: 'High Availability'
+ highAvailability: undefined,
+
+ // Original text: "Add"
+ add: 'Adicionar',
+
+ // Original text: "Remove"
+ remove: 'Remover',
+
+ // Original text: "Preview"
+ preview: 'Pré-visualização',
+
+ // Original text: "Item"
+ item: 'Item',
+
+ // Original text: "No selected value"
+ noSelectedValue: 'Valor não selecionado',
+
+ // Original text: "Choose user(s) and/or group(s)"
+ selectSubjects: 'Escolha um usuário(s) e/ou grupo(s)',
+
+ // Original text: "Select Object(s)…"
+ selectObjects: 'Selecionar Objeto(s)…',
+
+ // Original text: "Choose a role"
+ selectRole: 'Escolha uma função',
+
+ // Original text: "Select Host(s)…"
+ selectHosts: 'Selecionar Host(s)…',
+
+ // Original text: "Select object(s)…"
+ selectHostsVms: 'Selecionar Objeto(s)…',
+
+ // Original text: "Select Network(s)…"
+ selectNetworks: 'Selecionar Rede(s)…',
+
+ // Original text: "Select PIF(s)…"
+ selectPifs: 'Selecionar PIF(s)…',
+
+ // Original text: "Select Pool(s)…"
+ selectPools: 'Selecionar Pool(s)…',
+
+ // Original text: "Select Remote(s)…"
+ selectRemotes: 'Selecionar Remote(s)…',
+
+ // Original text: 'Select resource set(s)…'
+ selectResourceSets: undefined,
+
+ // Original text: 'Select template(s)…'
+ selectResourceSetsVmTemplate: undefined,
+
+ // Original text: 'Select SR(s)…'
+ selectResourceSetsSr: undefined,
+
+ // Original text: 'Select network(s)…'
+ selectResourceSetsNetwork: undefined,
+
+ // Original text: 'Select disk(s)…'
+ selectResourceSetsVdi: undefined,
+
+ // Original text: 'Select SSH key(s)…'
+ selectSshKey: undefined,
+
+ // Original text: "Select SR(s)…"
+ selectSrs: 'Selecionar SR(s)…',
+
+ // Original text: "Select VM(s)…"
+ selectVms: 'Selecionar VM(s)…',
+
+ // Original text: "Select VM template(s)…"
+ selectVmTemplates: 'Selecionar VM(s) modelo(s)…',
+
+ // Original text: "Select tag(s)…"
+ selectTags: 'Selecionar etiqueta(s)…',
+
+ // Original text: "Select disk(s)…"
+ selectVdis: 'Selecionar disco(s)…',
+
+ // Original text: 'Select timezone…'
+ selectTimezone: undefined,
+
+ // Original text: 'Select IP(s)…'
+ selectIp: undefined,
+
+ // Original text: 'Select IP pool(s)…'
+ selectIpPool: undefined,
+
+ // Original text: "Fill required informations."
+ fillRequiredInformations: 'Preencha as informações necessárias.',
+
+ // Original text: "Fill informations (optional)"
+ fillOptionalInformations: 'Preencha as informações (opcional)',
+
+ // Original text: "Reset"
+ selectTableReset: 'Reiniciar',
+
+ // Original text: "Month"
+ schedulingMonth: 'Agendamento Mensal',
+
+ // Original text: "Each selected month"
+ schedulingEachSelectedMonth: 'Agendamento escolhido por mês',
+
+ // Original text: "Day of the month"
+ schedulingMonthDay: 'Dia do mês',
+
+ // Original text: "Each selected day"
+ schedulingEachSelectedMonthDay: 'Agendamento por dia selecionado',
+
+ // Original text: "Day of the week"
+ schedulingWeekDay: 'Dia da semana',
+
+ // Original text: "Each selected day"
+ schedulingEachSelectedWeekDay: 'Cada dia selecionado',
+
+ // Original text: "Hour"
+ schedulingHour: 'Hora',
+
+ // Original text: "Every N hour"
+ schedulingEveryNHour: 'Todas N horas',
+
+ // Original text: "Each selected hour"
+ schedulingEachSelectedHour: 'Cada hora selecionada',
+
+ // Original text: "Minute"
+ schedulingMinute: 'Minuto',
+
+ // Original text: "Every N minute"
+ schedulingEveryNMinute: 'Todos N minutos',
+
+ // Original text: "Each selected minute"
+ schedulingEachSelectedMinute: 'Cada minuto selecionado',
+
+ // Original text: "Reset"
+ schedulingReset: 'Reiniciar',
+
+ // Original text: "Unknown"
+ unknownSchedule: 'Desconhecido',
+
+ // Original text: 'Xo-server timezone:'
+ timezonePickerServerValue: undefined,
+
+ // Original text: 'Web browser timezone'
+ timezonePickerUseLocalTime: undefined,
+
+ // Original text: 'Xo-server timezone'
+ timezonePickerUseServerTime: undefined,
+
+ // Original text: 'Server timezone ({value})'
+ serverTimezoneOption: undefined,
+
+ // Original text: 'Cron Pattern:'
+ cronPattern: undefined,
+
+ // Original text: 'Cannot edit backup'
+ backupEditNotFoundTitle: undefined,
+
+ // Original text: 'Missing required info for edition'
+ backupEditNotFoundMessage: undefined,
+
+ // Original text: "Job"
+ job: 'Tarefa',
+
+ // Original text: "Job ID"
+ jobId: 'ID tarefa',
+
+ // Original text: "Name"
+ jobName: 'Nome',
+
+ // Original text: 'Name of your job (forbidden: "_")'
+ jobNamePlaceholder: undefined,
+
+ // Original text: "Start"
+ jobStart: 'Inicia',
+
+ // Original text: "End"
+ jobEnd: 'Termina',
+
+ // Original text: "Duration"
+ jobDuration: 'Duração',
+
+ // Original text: "Status"
+ jobStatus: 'Status',
+
+ // Original text: "Action"
+ jobAction: 'Ação',
+
+ // Original text: "Tag"
+ jobTag: 'Etiqueta',
+
+ // Original text: "Scheduling"
+ jobScheduling: 'Agendamento',
+
+ // Original text: "State"
+ jobState: 'Estado',
+
+ // Original text: 'Timezone'
+ jobTimezone: undefined,
+
+ // Original text: 'xo-server'
+ jobServerTimezone: undefined,
+
+ // Original text: "Run job"
+ 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',
+
+ // Original text: "Started"
+ jobStarted: 'Iniciado',
+
+ // Original text: "Finished"
+ jobFinished: 'Terminado',
+
+ // Original text: "Save"
+ saveBackupJob: 'Salvar',
+
+ // Original text: "Remove backup job"
+ 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?',
+
+ // 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.',
+
+ // 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.',
+
+ // Original text: "No scheduled jobs."
+ noScheduledJobs: 'Sem agendamentos',
+
+ // Original text: "No jobs found."
+ noJobs: 'Tarefas não encontradas',
+
+ // Original text: "No schedules found"
+ noSchedules: 'Nenhum agendamento foi encontrado',
+
+ // Original text: "Select a xo-server API command"
+ jobActionPlaceHolder: 'Selecione um comando para xo-server API',
+
+ // Original text: 'Schedules'
+ jobSchedules: undefined,
+
+ // Original text: 'Name of your schedule'
+ jobScheduleNamePlaceHolder: undefined,
+
+ // Original text: 'Select a Job'
+ jobScheduleJobPlaceHolder: undefined,
+
+ // Original text: "Select your backup type:"
+ newBackupSelection: 'Selecione seu tipo de backup',
+
+ // Original text: 'Select backup mode:'
+ smartBackupModeSelection: undefined,
+
+ // Original text: 'Normal backup'
+ normalBackup: undefined,
+
+ // Original text: 'Smart backup'
+ smartBackup: undefined,
+
+ // Original text: 'Local remote selected'
+ localRemoteWarningTitle: undefined,
+
+ // Original text: 'Warning: local remotes will use limited XOA disk space. Only for advanced users.'
+ localRemoteWarningMessage: undefined,
+
+ // Original text: 'VMs'
+ editBackupVmsTitle: undefined,
+
+ // Original text: 'VMs statuses'
+ editBackupSmartStatusTitle: undefined,
+
+ // Original text: 'Resident on'
+ editBackupSmartResidentOn: undefined,
+
+ // Original text: 'VMs Tags'
+ editBackupSmartTagsTitle: undefined,
+
+ // Original text: 'Tag'
+ editBackupTagTitle: undefined,
+
+ // Original text: 'Report'
+ editBackupReportTitle: undefined,
+
+ // Original text: 'Enable immediately after creation'
+ editBackupScheduleEnabled: undefined,
+
+ // Original text: 'Depth'
+ editBackupDepthTitle: undefined,
+
+ // Original text: 'Remote'
+ editBackupRemoteTitle: undefined,
+
+ // Original text: "Remote stores for backup"
+ remoteList: 'Backups remotos',
+
+ // Original text: "New File System Remote"
+ newRemote: 'Novo Arquivo de Sistema Remoto',
+
+ // Original text: "Local"
+ remoteTypeLocal: 'Local',
+
+ // Original text: "NFS"
+ remoteTypeNfs: 'NFS',
+
+ // Original text: "SMB"
+ remoteTypeSmb: 'SMB',
+
+ // Original text: "Type"
+ remoteType: 'Type',
+
+ // Original text: 'Test your remote'
+ remoteTestTip: undefined,
+
+ // Original text: 'Test Remote'
+ testRemote: undefined,
+
+ // Original text: 'Test failed for {name}'
+ remoteTestFailure: undefined,
+
+ // Original text: 'Test passed for {name}'
+ remoteTestSuccess: undefined,
+
+ // Original text: 'Error'
+ remoteTestError: undefined,
+
+ // Original text: 'Test Step'
+ remoteTestStep: undefined,
+
+ // Original text: 'Test file'
+ remoteTestFile: undefined,
+
+ // Original text: 'The remote appears to work correctly'
+ remoteTestSuccessMessage: undefined,
+
+ // Original text: 'Name'
+ remoteName: undefined,
+
+ // Original text: 'Path'
+ remotePath: undefined,
+
+ // Original text: 'State'
+ remoteState: undefined,
+
+ // Original text: 'Device'
+ remoteDevice: undefined,
+
+ // Original text: 'Share'
+ remoteShare: undefined,
+
+ // Original text: 'Auth'
+ remoteAuth: undefined,
+
+ // Original text: 'Mounted'
+ remoteMounted: undefined,
+
+ // Original text: 'Unmounted'
+ remoteUnmounted: undefined,
+
+ // Original text: 'Connect'
+ remoteConnectTip: undefined,
+
+ // Original text: 'Disconnect'
+ remoteDisconnectTip: undefined,
+
+ // Original text: 'Delete'
+ remoteDeleteTip: undefined,
+
+ // Original text: 'remote name *'
+ remoteNamePlaceHolder: undefined,
+
+ // Original text: 'Name *'
+ remoteMyNamePlaceHolder: undefined,
+
+ // Original text: '/path/to/backup'
+ remoteLocalPlaceHolderPath: undefined,
+
+ // Original text: 'host *'
+ remoteNfsPlaceHolderHost: undefined,
+
+ // Original text: '/path/to/backup'
+ remoteNfsPlaceHolderPath: undefined,
+
+ // Original text: 'subfolder [path\\to\\backup]'
+ remoteSmbPlaceHolderRemotePath: undefined,
+
+ // Original text: 'Username'
+ remoteSmbPlaceHolderUsername: undefined,
+
+ // Original text: 'Password'
+ remoteSmbPlaceHolderPassword: undefined,
+
+ // Original text: 'Domain'
+ remoteSmbPlaceHolderDomain: undefined,
+
+ // Original text: '\\ *'
+ remoteSmbPlaceHolderAddressShare: undefined,
+
+ // Original text: 'password(fill to edit)'
+ remotePlaceHolderPassword: undefined,
+
+ // Original text: 'Create a new SR'
+ newSrTitle: undefined,
+
+ // Original text: "General"
+ newSrGeneral: 'Geral',
+
+ // Original text: "Select Storage Type:"
+ newSrTypeSelection: 'Selecionar o tipo de armazenamento (storage)',
+
+ // Original text: "Settings"
+ newSrSettings: 'Configurações',
+
+ // Original text: "Storage Usage"
+ newSrUsage: 'Uso de armazenamento (storage)',
+
+ // Original text: "Summary"
+ newSrSummary: 'Sumário',
+
+ // Original text: "Host"
+ newSrHost: 'Host',
+
+ // Original text: "Type"
+ newSrType: 'Tipo',
+
+ // Original text: "Name"
+ newSrName: 'Nome',
+
+ // Original text: "Description"
+ newSrDescription: 'Descrição',
+
+ // Original text: "Server"
+ newSrServer: 'Servidor',
+
+ // Original text: "Path"
+ newSrPath: 'Caminho',
+
+ // Original text: "IQN"
+ newSrIqn: 'IQN',
+
+ // Original text: "LUN"
+ newSrLun: 'LUN',
+
+ // Original text: "with auth."
+ newSrAuth: 'Com autenticação',
+
+ // Original text: "User Name"
+ newSrUsername: 'Nome de Usuário',
+
+ // Original text: "Password"
+ newSrPassword: 'Senha',
+
+ // Original text: "Device"
+ newSrDevice: 'Dispositivo',
+
+ // Original text: "in use"
+ newSrInUse: 'Em uso',
+
+ // Original text: "Size"
+ newSrSize: 'Tamanho',
+
+ // Original text: "Create"
+ newSrCreate: 'Criar',
+
+ // Original text: 'Storage name'
+ newSrNamePlaceHolder: undefined,
+
+ // Original text: 'Storage description'
+ newSrDescPlaceHolder: undefined,
+
+ // Original text: 'Address'
+ newSrAddressPlaceHolder: undefined,
+
+ // Original text: '[port]'
+ newSrPortPlaceHolder: undefined,
+
+ // Original text: 'Username'
+ newSrUsernamePlaceHolder: undefined,
+
+ // Original text: 'Password'
+ newSrPasswordPlaceHolder: undefined,
+
+ // Original text: 'Device, e.g /dev/sda…'
+ newSrLvmDevicePlaceHolder: undefined,
+
+ // Original text: '/path/to/directory'
+ newSrLocalPathPlaceHolder: undefined,
+
+ // Original text: "Users/Groups"
+ subjectName: 'Usuários/Grupos',
+
+ // Original text: "Object"
+ objectName: 'Objeto',
+
+ // Original text: 'No acls found'
+ aclNoneFound: undefined,
+
+ // Original text: "Role"
+ roleName: 'Função',
+
+ // Original text: 'Create'
+ aclCreate: undefined,
+
+ // Original text: "New Group Name"
+ newGroupName: 'Novo Nome de Grupo',
+
+ // Original text: "Create Group"
+ createGroup: 'Criar Grupo',
+
+ // Original text: "Create"
+ createGroupButton: 'Criar',
+
+ // Original text: "Delete Group"
+ deleteGroup: 'Deletar Grupo',
+
+ // Original text: "Are you sure you want to delete this group?"
+ deleteGroupConfirm: 'Você tem certeza que deseja deletar este grupo?',
+
+ // Original text: "Remove user from Group"
+ removeUserFromGroup: 'Remover usuário do Grupo',
+
+ // Original text: "Are you sure you want to delete this user?"
+ deleteUserConfirm: 'Você tem certeza que deseja deletar este usuário?',
+
+ // Original text: 'Delete User'
+ deleteUser: undefined,
+
+ // Original text: 'no user'
+ noUser: undefined,
+
+ // Original text: "unknown user"
+ unknownUser: 'Usuário desconhecido',
+
+ // Original text: "No group found"
+ noGroupFound: 'Grupo não encontrado',
+
+ // Original text: "Name"
+ groupNameColumn: 'Nome',
+
+ // Original text: "Users"
+ groupUsersColumn: 'Usuários',
+
+ // Original text: "Add User"
+ addUserToGroupColumn: 'Adicionar Usuário',
+
+ // Original text: "Email"
+ userNameColumn: 'e-mail',
+
+ // Original text: "Permissions"
+ userPermissionColumn: 'Permissões',
+
+ // Original text: "Password"
+ userPasswordColumn: 'Senha',
+
+ // Original text: "Email"
+ userName: 'e-mail',
+
+ // Original text: "Password"
+ userPassword: 'Senha',
+
+ // Original text: "Create"
+ createUserButton: 'Criar',
+
+ // Original text: "No user found"
+ noUserFound: 'Usuário não encontrado',
+
+ // Original text: "User"
+ userLabel: 'Usuário',
+
+ // Original text: "Admin"
+ adminLabel: 'Administrador',
+
+ // Original text: "No user in group"
+ noUserInGroup: 'Nenhum usuário neste grupo',
+
+ // Original text: "{users} user{users, plural, one {} other {s}}"
+ countUsers: '{users} user{users, plural, one {} other {s}}',
+
+ // Original text: "Select Permission"
+ selectPermission: 'Selecionar Permissão',
+
+ // Original text: "Auto-load at server start"
+ autoloadPlugin: 'Carregamento automático na inicialização do servidor',
+
+ // Original text: "Save configuration"
+ savePluginConfiguration: 'Salvar configuração',
+
+ // Original text: "Delete configuration"
+ deletePluginConfiguration: 'Deletar configuração',
+
+ // Original text: "Plugin error"
+ pluginError: 'Erro Plugin',
+
+ // Original text: "Unknown error"
+ unknownPluginError: 'Erro desconhecido',
+
+ // Original text: "Purge plugin configuration"
+ 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?',
+
+ // Original text: "Edit"
+ editPluginConfiguration: 'Editar',
+
+ // Original text: "Cancel"
+ cancelPluginEdition: 'Cancelar',
+
+ // Original text: "Plugin configuration"
+ pluginConfigurationSuccess: 'Configuração do Plugin',
+
+ // Original text: "Plugin configuration successfully saved!"
+ pluginConfigurationChanges:
+ 'Configuração do plugin foi efetuada com sucesso!',
+
+ // Original text: 'Predefined configuration'
+ pluginConfigurationPresetTitle: undefined,
+
+ // Original text: 'Choose a predefined configuration.'
+ pluginConfigurationChoosePreset: undefined,
+
+ // Original text: 'Apply'
+ applyPluginPreset: undefined,
+
+ // Original text: 'Save filter error'
+ saveNewUserFilterErrorTitle: undefined,
+
+ // Original text: 'Bad parameter: name must be given.'
+ saveNewUserFilterErrorBody: undefined,
+
+ // Original text: 'Name:'
+ filterName: undefined,
+
+ // Original text: 'Value:'
+ filterValue: undefined,
+
+ // Original text: 'Save new filter'
+ saveNewFilterTitle: undefined,
+
+ // Original text: 'Set custom filters'
+ setUserFiltersTitle: undefined,
+
+ // Original text: 'Are you sure you want to set custom filters?'
+ setUserFiltersBody: undefined,
+
+ // Original text: 'Remove custom filter'
+ removeUserFilterTitle: undefined,
+
+ // Original text: 'Are you sure you want to remove custom filter?'
+ removeUserFilterBody: undefined,
+
+ // Original text: 'Default filter'
+ defaultFilter: undefined,
+
+ // Original text: 'Default filters'
+ defaultFilters: undefined,
+
+ // Original text: 'Custom filters'
+ customFilters: undefined,
+
+ // Original text: 'Customize filters'
+ customizeFilters: undefined,
+
+ // Original text: 'Save custom filters'
+ saveCustomFilters: undefined,
+
+ // Original text: "Start"
+ startVmLabel: 'Iniciar',
+
+ // Original text: "Recovery start"
+ recoveryModeLabel: 'Iniciar recuperação',
+
+ // Original text: "Suspend"
+ suspendVmLabel: 'Suspender',
+
+ // Original text: "Stop"
+ stopVmLabel: 'Parar',
+
+ // Original text: "Force shutdown"
+ forceShutdownVmLabel: 'Forçar desligamento',
+
+ // Original text: "Reboot"
+ rebootVmLabel: 'Reiniciar',
+
+ // Original text: "Force reboot"
+ forceRebootVmLabel: 'Forçar reinicialização',
+
+ // Original text: "Delete"
+ deleteVmLabel: 'Deletar',
+
+ // Original text: "Migrate"
+ migrateVmLabel: 'Migrar',
+
+ // Original text: "Snapshot"
+ snapshotVmLabel: 'Snapshot',
+
+ // Original text: "Export"
+ exportVmLabel: 'Exportar',
+
+ // Original text: "Resume"
+ resumeVmLabel: 'Continuar',
+
+ // Original text: "Copy"
+ copyVmLabel: 'Copiar',
+
+ // Original text: "Clone"
+ cloneVmLabel: 'Clonar',
+
+ // Original text: "Fast clone"
+ fastCloneVmLabel: 'Clonagem rápida',
+
+ // Original text: "Convert to template"
+ convertVmToTemplateLabel: 'Convertir para template',
+
+ // Original text: "Console"
+ vmConsoleLabel: 'Console',
+
+ // Original text: "Rescan all disks"
+ srRescan: 'Examinar novamente todos os discos',
+
+ // Original text: "Connect to all hosts"
+ srReconnectAll: 'Conectar-se a todos os hosts',
+
+ // Original text: "Disconnect to all hosts"
+ srDisconnectAll: 'Desconectar-se de todos os hosts',
+
+ // Original text: "Forget this SR"
+ srForget: 'Esquecer esta SR',
+
+ // Original text: "Remove this SR"
+ srRemoveButton: 'Remover esta SR',
+
+ // Original text: "No VDIs in this storage"
+ srNoVdis: 'Nenhuma VDI neste armazenamento',
+
+ // Original text: 'Pool RAM usage:'
+ poolTitleRamUsage: undefined,
+
+ // Original text: '{used} used on {total}'
+ poolRamUsage: undefined,
+
+ // Original text: 'Master:'
+ poolMaster: undefined,
+
+ // Original text: "Hosts"
+ hostsTabName: 'Hosts',
+
+ // Original text: "High Availability"
+ poolHaStatus: 'Alta Disponibilidade',
+
+ // Original text: "Enabled"
+ poolHaEnabled: 'Habilitado',
+
+ // Original text: "Disabled"
+ poolHaDisabled: 'Desativado',
+
+ // Original text: "Name"
+ hostNameLabel: 'Nome',
+
+ // Original text: "Description"
+ hostDescription: 'Descrição',
+
+ // Original text: "Memory"
+ hostMemory: 'Memória',
+
+ // Original text: "No hosts"
+ noHost: 'Nenhum host',
+
+ // Original text: '{used}% used ({free} free)'
+ memoryLeftTooltip: undefined,
+
+ // Original text: "Name"
+ poolNetworkNameLabel: 'Nome',
+
+ // Original text: "Description"
+ poolNetworkDescription: 'Descrição',
+
+ // Original text: "PIFs"
+ poolNetworkPif: 'PIFs',
+
+ // Original text: "No networks"
+ poolNoNetwork: 'Nenhuma Rede',
+
+ // Original text: "MTU"
+ poolNetworkMTU: 'MTU',
+
+ // Original text: "Connected"
+ poolNetworkPifAttached: 'Conectado',
+
+ // Original text: "Disconnected"
+ poolNetworkPifDetached: 'Desconectado',
+
+ // Original text: 'Show PIFs'
+ showPifs: undefined,
+
+ // Original text: 'Hide PIFs'
+ hidePifs: undefined,
+
+ // Original text: "Add SR"
+ addSrLabel: 'Adicionar SR',
+
+ // Original text: "Add VM"
+ addVmLabel: 'Adicionar VM',
+
+ // Original text: "Add Host"
+ addHostLabel: 'Adicionar Host',
+
+ // Original text: "Disconnect"
+ disconnectServer: 'Desconectar',
+
+ // Original text: "Start"
+ startHostLabel: 'Iniciar',
+
+ // Original text: "Stop"
+ stopHostLabel: 'Parar',
+
+ // Original text: "Enable"
+ enableHostLabel: 'Habilitar',
+
+ // Original text: "Disable"
+ disableHostLabel: 'Desabilitar',
+
+ // Original text: "Restart toolstack"
+ restartHostAgent: 'Reiniciar toolstack',
+
+ // Original text: "Force reboot"
+ forceRebootHostLabel: 'Forçar reinicialização',
+
+ // Original text: "Reboot"
+ rebootHostLabel: 'Reinicializar',
+
+ // Original text: 'Reboot to apply updates'
+ rebootUpdateHostLabel: undefined,
+
+ // Original text: "Emergency mode"
+ emergencyModeLabel: 'Modo de emergência',
+
+ // Original text: "Storage"
+ storageTabName: 'Armazenamento',
+
+ // Original text: "Patches"
+ patchesTabName: 'Correções',
+
+ // Original text: "Load average"
+ statLoad: 'Carregar média',
+
+ // Original text: "Hardware"
+ hardwareHostSettingsLabel: 'Hardware',
+
+ // Original text: "Address"
+ hostAddress: 'Endereço',
+
+ // Original text: "Status"
+ hostStatus: 'Status',
+
+ // Original text: "Build number"
+ hostBuildNumber: 'Número de compilação',
+
+ // Original text: "iSCSI name"
+ hostIscsiName: 'Nome iSCSI',
+
+ // Original text: "Version"
+ hostXenServerVersion: 'Versão',
+
+ // Original text: "Enabled"
+ hostStatusEnabled: 'Ativado',
+
+ // Original text: "Disabled"
+ hostStatusDisabled: 'Desativado',
+
+ // Original text: "Power on mode"
+ hostPowerOnMode: 'Modo de energia',
+
+ // Original text: "Host uptime"
+ hostStartedSince: 'Tempo de atividade do Host',
+
+ // Original text: "Toolstack uptime"
+ hostStackStartedSince: 'Tempo de atividade Toolstack',
+
+ // Original text: "CPU model"
+ hostCpusModel: 'Modelo CPU',
+
+ // Original text: "Core (socket)"
+ hostCpusNumber: 'Núcleo (soquete)',
+
+ // Original text: "Manufacturer info"
+ hostManufacturerinfo: 'Informações do Fabricante',
+
+ // Original text: "BIOS info"
+ hostBiosinfo: 'Informações BIOS',
+
+ // Original text: "Licence"
+ licenseHostSettingsLabel: 'Licença',
+
+ // Original text: "Type"
+ hostLicenseType: 'Tipo',
+
+ // Original text: "Socket"
+ hostLicenseSocket: 'Soquete',
+
+ // Original text: "Expiry"
+ hostLicenseExpiry: 'Expiração',
+
+ // Original text: "Add a network"
+ networkCreateButton: 'Adicionar a Rede',
+
+ // Original text: 'Add a bonded network'
+ networkCreateBondedButton: undefined,
+
+ // Original text: "Device"
+ pifDeviceLabel: 'Dispositivo',
+
+ // Original text: "Network"
+ pifNetworkLabel: 'Rede',
+
+ // Original text: "VLAN"
+ pifVlanLabel: 'VLAN',
+
+ // Original text: "Address"
+ pifAddressLabel: 'Endereço',
+
+ // Original text: 'Mode'
+ pifModeLabel: undefined,
+
+ // Original text: "MAC"
+ pifMacLabel: 'MAC',
+
+ // Original text: "MTU"
+ pifMtuLabel: 'MTU',
+
+ // Original text: "Status"
+ pifStatusLabel: 'Status',
+
+ // Original text: "Connected"
+ pifStatusConnected: 'Conectado',
+
+ // Original text: "Disconnected"
+ pifStatusDisconnected: 'Desconectado',
+
+ // Original text: "No physical interface detected"
+ pifNoInterface: 'Nenhuma interface física foi detectada',
+
+ // Original text: 'This interface is currently in use'
+ pifInUse: undefined,
+
+ // Original text: 'Default locking mode'
+ defaultLockingMode: undefined,
+
+ // Original text: 'Configure IP address'
+ pifConfigureIp: undefined,
+
+ // Original text: 'Invalid parameters'
+ configIpErrorTitle: undefined,
+
+ // Original text: 'IP address and netmask required'
+ configIpErrorMessage: undefined,
+
+ // Original text: 'Static IP address'
+ staticIp: undefined,
+
+ // Original text: 'Netmask'
+ netmask: undefined,
+
+ // Original text: 'DNS'
+ dns: undefined,
+
+ // Original text: 'Gateway'
+ gateway: undefined,
+
+ // Original text: "Add a storage"
+ addSrDeviceButton: 'Adicionar um armazenamento',
+
+ // Original text: "Name"
+ srNameLabel: 'Nome',
+
+ // Original text: "Type"
+ srType: 'Tipo',
+
+ // Original text: "Status"
+ pbdStatus: 'Status',
+
+ // Original text: "Connected"
+ pbdStatusConnected: 'Conectado',
+
+ // Original text: "Disconnected"
+ pbdStatusDisconnected: 'Desconectado',
+
+ // Original text: 'Connect'
+ pbdConnect: undefined,
+
+ // Original text: 'Disconnect'
+ pbdDisconnect: undefined,
+
+ // Original text: 'Forget'
+ pbdForget: undefined,
+
+ // Original text: "Shared"
+ srShared: 'Compartilhado',
+
+ // Original text: "Not shared"
+ srNotShared: 'Não compartilhado',
+
+ // Original text: "No storage detected"
+ pbdNoSr: 'Nenhum armazenamento detectado',
+
+ // Original text: "Name"
+ patchNameLabel: 'Nome',
+
+ // Original text: "Install all patches"
+ patchUpdateButton: 'Instalar todas as correções',
+
+ // Original text: "Description"
+ patchDescription: 'Descrição',
+
+ // Original text: "Applied date"
+ patchApplied: 'Data de lançamento',
+
+ // Original text: "Size"
+ patchSize: 'Tamanho',
+
+ // Original text: "Status"
+ patchStatus: 'Status',
+
+ // Original text: "Applied"
+ patchStatusApplied: 'Aplicado',
+
+ // Original text: "Missing patches"
+ patchStatusNotApplied: 'Correções faltando',
+
+ // Original text: "No patch detected"
+ patchNothing: 'Nenhuma correção foi detectada',
+
+ // Original text: "Release date"
+ patchReleaseDate: 'Data de lançamento',
+
+ // Original text: "Guidance"
+ patchGuidance: 'Direção',
+
+ // Original text: "Action"
+ patchAction: 'Ação',
+
+ // Original text: 'Applied patches'
+ hostAppliedPatches: undefined,
+
+ // Original text: "Missing patches"
+ hostMissingPatches: 'Correções faltando',
+
+ // Original text: "Host up-to-date!"
+ hostUpToDate: 'Host pronto para atualizar!',
+
+ // Original text: 'Refresh patches'
+ refreshPatches: undefined,
+
+ // Original text: 'Install pool patches'
+ installPoolPatches: undefined,
+
+ // Original text: 'Default SR'
+ defaultSr: undefined,
+
+ // Original text: 'Set as default SR'
+ setAsDefaultSr: undefined,
+
+ // Original text: "General"
+ generalTabName: 'Geral',
+
+ // Original text: "Stats"
+ statsTabName: 'Estatísticas',
+
+ // Original text: "Console"
+ consoleTabName: 'Console',
+
+ // Original text: 'Container'
+ containersTabName: undefined,
+
+ // Original text: "Snapshots"
+ snapshotsTabName: 'Snapshots',
+
+ // Original text: "Logs"
+ logsTabName: 'Logs',
+
+ // Original text: "Advanced"
+ advancedTabName: 'Avançado',
+
+ // Original text: "Network"
+ networkTabName: 'Rede',
+
+ // Original text: "Disk{disks, plural, one {} other {s}}"
+ disksTabName: 'Disco{disks, plural, one {} other {s}}',
+
+ // Original text: "halted"
+ powerStateHalted: 'Interrompido',
+
+ // Original text: "running"
+ powerStateRunning: 'Executando',
+
+ // Original text: "suspended"
+ powerStateSuspended: 'Suspendido',
+
+ // Original text: "No Xen tools detected"
+ vmStatus: 'Nenhum Xen tools foi detectado',
+
+ // Original text: "No IPv4 record"
+ vmName: 'Nenhum registro IPv4',
+
+ // Original text: "No IP record"
+ vmDescription: 'Nenhum registro IP',
+
+ // Original text: "Started {ago}"
+ vmSettings: 'Iniciado {ago}',
+
+ // Original text: "Current status:"
+ vmCurrentStatus: 'Status atual',
+
+ // Original text: "Not running"
+ vmNotRunning: 'Parado',
+
+ // Original text: "No Xen tools detected"
+ noToolsDetected: 'Nenhum Xen tools foi detectado',
+
+ // Original text: "No IPv4 record"
+ noIpv4Record: 'Nenhum registro IPv4',
+
+ // Original text: "No IP record"
+ noIpRecord: 'Nenhum registro IP',
+
+ // Original text: "Started {ago}"
+ started: 'Iniciado {ago}',
+
+ // Original text: "Paravirtualization (PV)"
+ paraVirtualizedMode: 'Paravirtualização',
+
+ // Original text: "Hardware virtualization (HVM)"
+ hardwareVirtualizedMode: 'Virtualização de Hadware (HVM)',
+
+ // Original text: "CPU usage"
+ statsCpu: 'Uso de CPU',
+
+ // Original text: "Memory usage"
+ statsMemory: 'Uso de Memória',
+
+ // Original text: "Network throughput"
+ statsNetwork: 'Taxa de transferência de Rede',
+
+ // Original text: 'Stacked values'
+ useStackedValuesOnStats: undefined,
+
+ // Original text: "Disk throughput"
+ statDisk: 'Taxa de transferência de Disco',
+
+ // Original text: "Last 10 minutes"
+ statLastTenMinutes: 'Últimos 10 minutos',
+
+ // Original text: "Last 2 hours"
+ statLastTwoHours: 'Últimas 2 horas',
+
+ // Original text: "Last week"
+ statLastWeek: 'Semana passada',
+
+ // Original text: "Last year"
+ statLastYear: 'Ano passado',
+
+ // Original text: "Copy"
+ copyToClipboardLabel: 'Copiar',
+
+ // Original text: "Ctrl+Alt+Del"
+ ctrlAltDelButtonLabel: 'Ctrl+Alt+Del',
+
+ // Original text: "Tip:"
+ 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!',
+
+ // Original text: 'Hide infos'
+ hideHeaderTooltip: undefined,
+
+ // Original text: 'Show infos'
+ showHeaderTooltip: undefined,
+
+ // Original text: 'Name'
+ containerName: undefined,
+
+ // Original text: 'Command'
+ containerCommand: undefined,
+
+ // Original text: 'Creation date'
+ containerCreated: undefined,
+
+ // Original text: 'Status'
+ containerStatus: undefined,
+
+ // Original text: 'Action'
+ containerAction: undefined,
+
+ // Original text: 'No existing containers'
+ noContainers: undefined,
+
+ // Original text: 'Stop this container'
+ containerStop: undefined,
+
+ // Original text: 'Start this container'
+ containerStart: undefined,
+
+ // Original text: 'Pause this container'
+ containerPause: undefined,
+
+ // Original text: 'Resume this container'
+ containerResume: undefined,
+
+ // Original text: 'Restart this container'
+ containerRestart: undefined,
+
+ // Original text: "Action"
+ vdiAction: 'Ação',
+
+ // Original text: "Attach disk"
+ vdiAttachDeviceButton: 'Anexar disco',
+
+ // Original text: "New disk"
+ vbdCreateDeviceButton: 'Novo disco',
+
+ // Original text: "Boot order"
+ vdiBootOrder: 'Ordem de boot',
+
+ // Original text: "Name"
+ vdiNameLabel: 'Nome',
+
+ // Original text: "Description"
+ vdiNameDescription: 'Descrição',
+
+ // Original text: "Tags"
+ vdiTags: 'Etiquetas',
+
+ // Original text: "Size"
+ vdiSize: 'Tamanho',
+
+ // Original text: "SR"
+ vdiSr: 'SR',
+
+ // Original text: 'VM'
+ vdiVm: undefined,
+
+ // Original text: 'Migrate VDI'
+ vdiMigrate: undefined,
+
+ // Original text: 'Destination SR:'
+ vdiMigrateSelectSr: undefined,
+
+ // Original text: 'Migrate all VDIs'
+ vdiMigrateAll: undefined,
+
+ // Original text: 'No SR'
+ vdiMigrateNoSr: undefined,
+
+ // Original text: 'A target SR is required to migrate a VDI'
+ vdiMigrateNoSrMessage: undefined,
+
+ // Original text: 'Forget'
+ vdiForget: undefined,
+
+ // Original text: 'Remove VDI'
+ vdiRemove: undefined,
+
+ // Original text: "Boot flag"
+ vbdBootableStatus: 'Indicador de inicialização',
+
+ // Original text: "Status"
+ vbdStatus: 'Status',
+
+ // Original text: "Connected"
+ vbdStatusConnected: 'Conectado',
+
+ // Original text: "Disconnected"
+ vbdStatusDisconnected: 'Desconectado',
+
+ // Original text: "No disks"
+ vbdNoVbd: 'Nenhum disco encontrado',
+
+ // Original text: 'Connect VBD'
+ vbdConnect: undefined,
+
+ // Original text: 'Disconnect VBD'
+ vbdDisconnect: undefined,
+
+ // Original text: 'Bootable'
+ vbdBootable: undefined,
+
+ // Original text: 'Readonly'
+ vbdReadonly: undefined,
+
+ // Original text: 'Create'
+ vbdCreate: undefined,
+
+ // Original text: 'Disk name'
+ vbdNamePlaceHolder: undefined,
+
+ // Original text: 'Size'
+ vbdSizePlaceHolder: undefined,
+
+ // Original text: 'Save'
+ saveBootOption: undefined,
+
+ // Original text: 'Reset'
+ resetBootOption: undefined,
+
+ // Original text: "New device"
+ vifCreateDeviceButton: 'Novo dispositivo',
+
+ // Original text: "No interface"
+ vifNoInterface: 'Nenhuma interface encontrada',
+
+ // Original text: "Device"
+ vifDeviceLabel: 'Dispositivo',
+
+ // Original text: "MAC address"
+ vifMacLabel: 'Endereço MAC',
+
+ // Original text: "MTU"
+ vifMtuLabel: 'MTU',
+
+ // Original text: "Network"
+ vifNetworkLabel: 'Rede',
+
+ // Original text: "Status"
+ vifStatusLabel: 'Status',
+
+ // Original text: "Connected"
+ vifStatusConnected: 'Conectado',
+
+ // Original text: "Disconnected"
+ vifStatusDisconnected: 'Desconectado',
+
+ // Original text: 'Connect'
+ vifConnect: undefined,
+
+ // Original text: 'Disconnect'
+ vifDisconnect: undefined,
+
+ // Original text: 'Remove'
+ vifRemove: undefined,
+
+ // Original text: "IP addresses"
+ vifIpAddresses: 'Endereço IP',
+
+ // Original text: 'Auto-generated if empty'
+ vifMacAutoGenerate: undefined,
+
+ // Original text: 'Allowed IPs'
+ vifAllowedIps: undefined,
+
+ // Original text: 'No IPs'
+ vifNoIps: undefined,
+
+ // Original text: 'Network locked'
+ vifLockedNetwork: undefined,
+
+ // Original text: 'Network locked and no IPs are allowed for this interface'
+ vifLockedNetworkNoIps: undefined,
+
+ // Original text: 'Network not locked'
+ vifUnLockedNetwork: undefined,
+
+ // Original text: 'Unknown network'
+ vifUnknownNetwork: undefined,
+
+ // Original text: 'Create'
+ vifCreate: undefined,
+
+ // Original text: "No snapshots"
+ noSnapshots: 'Nenhum snapshot encontrado',
+
+ // Original text: "New snapshot"
+ snapshotCreateButton: 'Novo snapshot',
+
+ // Original text: "Just click on the snapshot button to create one!"
+ tipCreateSnapshotLabel: 'Clique sobre o botão snapshop para criar!',
+
+ // Original text: 'Revert VM to this snapshot'
+ revertSnapshot: undefined,
+
+ // Original text: 'Remove this snapshot'
+ deleteSnapshot: undefined,
+
+ // Original text: 'Create a VM from this snapshot'
+ copySnapshot: undefined,
+
+ // Original text: 'Export this snapshot'
+ exportSnapshot: undefined,
+
+ // Original text: "Creation date"
+ snapshotDate: 'Data de criação',
+
+ // Original text: "Name"
+ snapshotName: 'Nome',
+
+ // Original text: "Action"
+ snapshotAction: 'Ação',
+
+ // Original text: "Remove all logs"
+ logRemoveAll: 'Remover todos os logs',
+
+ // Original text: "No logs so far"
+ noLogs: 'Sem registros até o momento',
+
+ // Original text: "Creation date"
+ logDate: 'Data de criação',
+
+ // Original text: "Name"
+ logName: 'Nome',
+
+ // Original text: "Content"
+ logContent: 'Conteúdo',
+
+ // Original text: "Action"
+ logAction: 'Ação',
+
+ // Original text: "Remove"
+ vmRemoveButton: 'Remover',
+
+ // Original text: "Convert"
+ vmConvertButton: 'Converter',
+
+ // Original text: "Xen settings"
+ xenSettingsLabel: 'Configurações Xen',
+
+ // Original text: "Guest OS"
+ guestOsLabel: 'Convidado OS',
+
+ // Original text: "Misc"
+ miscLabel: 'Misc',
+
+ // Original text: "UUID"
+ uuid: 'UUID',
+
+ // Original text: "Virtualization mode"
+ virtualizationMode: 'Modo de virtualização',
+
+ // Original text: "CPU weight"
+ cpuWeightLabel: 'Carga da CPU',
+
+ // Original text: "Default ({value, number})"
+ defaultCpuWeight: 'Padrão',
+
+ // Original text: 'CPU cap'
+ cpuCapLabel: undefined,
+
+ // Original text: 'Default ({value, number})'
+ defaultCpuCap: undefined,
+
+ // Original text: "PV args"
+ pvArgsLabel: 'PV argos',
+
+ // Original text: "Xen tools status"
+ xenToolsStatus: 'Status de ferramentas Xen',
+
+ // Original text: "{status}"
+ xenToolsStatusValue: '{status}',
+
+ // Original text: "OS name"
+ osName: 'Nome OS',
+
+ // Original text: "OS kernel"
+ osKernel: 'OS kernel (núcleo)',
+
+ // Original text: "Auto power on"
+ autoPowerOn: 'Ligar automaticamente',
+
+ // Original text: "HA"
+ ha: 'HA',
+
+ // Original text: "Original template"
+ originalTemplate: 'Modelo original (template)',
+
+ // Original text: "Unknown"
+ unknownOsName: 'Desconhecido',
+
+ // Original text: "Unknown"
+ unknownOsKernel: 'Desconhecido',
+
+ // Original text: "Unknown"
+ unknownOriginalTemplate: 'Desconhecido',
+
+ // Original text: "VM limits"
+ vmLimitsLabel: 'Limites de VM',
+
+ // Original text: "CPU limits"
+ vmCpuLimitsLabel: 'Limites de CPU',
+
+ // Original text: "Memory limits (min/max)"
+ vmMemoryLimitsLabel: 'Limites de memória (min/max)',
+
+ // Original text: "vCPUs max:"
+ vmMaxVcpus: 'máximo',
+
+ // Original text: "Memory max:"
+ vmMaxRam: 'Limite máximo de memória',
+
+ // Original text: "Long click to add a name"
+ 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',
+
+ // Original text: "Click to add a name"
+ vmViewNamePlaceholder: 'Clique para adicionar um nome',
+
+ // Original text: "Click to add a description"
+ vmViewDescriptionPlaceholder: 'Clique para adicionar uma descrição',
+
+ // Original text: 'Click to add a name'
+ templateHomeNamePlaceholder: undefined,
+
+ // Original text: 'Click to add a description'
+ templateHomeDescriptionPlaceholder: undefined,
+
+ // Original text: 'Delete template'
+ templateDelete: undefined,
+
+ // Original text: 'Delete VM template{templates, plural, one {} other {s}}'
+ templateDeleteModalTitle: undefined,
+
+ // Original text: 'Are you sure you want to delete {templates, plural, one {this} other {these}} template{templates, plural, one {} other {s}}?'
+ templateDeleteModalBody: undefined,
+
+ // Original text: "Pool{pools, plural, one {} other {s}}"
+ poolPanel: 'Pool{pools, plural, one {} other {s}}',
+
+ // Original text: "Host{hosts, plural, one {} other {s}}"
+ hostPanel: 'Host{hosts, plural, one {} other {s}}',
+
+ // Original text: "VM{vms, plural, one {} other {s}}"
+ vmPanel: 'VM{vms, plural, one {} other {s}}',
+
+ // Original text: "RAM Usage"
+ memoryStatePanel: 'Utilização RAM',
+
+ // Original text: "CPUs Usage"
+ cpuStatePanel: 'Utilização de CPU',
+
+ // Original text: "VMs Power state"
+ vmStatePanel: 'Estado de energia das VMs',
+
+ // Original text: "Pending tasks"
+ taskStatePanel: 'Tarefas pendentes',
+
+ // Original text: "Users"
+ usersStatePanel: 'Usuários',
+
+ // Original text: "Storage state"
+ srStatePanel: 'Data do armazenamento (storage)',
+
+ // Original text: "{usage} (of {total})"
+ ofUsage: 'de',
+
+ // Original text: "No storage"
+ noSrs: 'Nenhum armazenamento (storage)',
+
+ // Original text: "Name"
+ srName: 'Nome',
+
+ // Original text: "Pool"
+ srPool: 'Pool',
+
+ // Original text: "Host"
+ srHost: 'Host',
+
+ // Original text: "Type"
+ srFormat: 'Tipo',
+
+ // Original text: "Size"
+ srSize: 'Tamanho',
+
+ // Original text: "Usage"
+ srUsage: 'Utilização',
+
+ // Original text: "used"
+ srUsed: 'Usado',
+
+ // Original text: "free"
+ srFree: 'Livre',
+
+ // Original text: "Storage Usage"
+ srUsageStatePanel: 'Utilização atual de armazenamento',
+
+ // Original text: "Top 5 SR Usage (in %)"
+ srTopUsageStatePanel: 'Top 5 de Utilização SR (em %)',
+
+ // Original text: '{running} running ({halted} halted)'
+ vmsStates: undefined,
+
+ // Original text: 'Clear selection'
+ dashboardStatsButtonRemoveAll: undefined,
+
+ // Original text: 'Add all hosts'
+ dashboardStatsButtonAddAllHost: undefined,
+
+ // Original text: 'Add all VMs'
+ dashboardStatsButtonAddAllVM: undefined,
+
+ // Original text: "{value} {date, date, medium}"
+ weekHeatmapData: '{value} {date, date, medium}',
+
+ // Original text: "No data."
+ weekHeatmapNoData: 'Nenhum dado encontrado',
+
+ // Original text: 'Weekly Heatmap'
+ weeklyHeatmap: undefined,
+
+ // Original text: 'Weekly Charts'
+ weeklyCharts: undefined,
+
+ // Original text: 'Synchronize scale:'
+ weeklyChartsScaleInfo: undefined,
+
+ // Original text: "Stats error"
+ statsDashboardGenericErrorTitle: 'Erro de estatísticas',
+
+ // Original text: "There is no stats available for:"
+ statsDashboardGenericErrorMessage: 'Não há estatísticas disponíveis para:',
+
+ // Original text: "No selected metric"
+ noSelectedMetric: 'Nenhuma métrica selecionada',
+
+ // Original text: "Select"
+ statsDashboardSelectObjects: 'Selecionar',
+
+ // Original text: "Loading…"
+ metricsLoading: 'Carregando…',
+
+ // Original text: "Coming soon!"
+ comingSoon: 'Em breve!',
+
+ // Original text: "Orphaned snapshot VDIs"
+ orphanedVdis: 'VDI órfãs',
+
+ // Original text: "Orphaned VMs snapshot"
+ orphanedVms: 'VMs órfãs',
+
+ // Original text: "No orphans"
+ noOrphanedObject: 'Sem órfãs',
+
+ // Original text: "Remove all orphaned snapshot VDIs"
+ removeAllOrphanedObject: 'Remover todos as VDIs órfãs',
+
+ // Original text: "Name"
+ vmNameLabel: 'Nome',
+
+ // Original text: "Description"
+ vmNameDescription: 'Descrição',
+
+ // Original text: "Resident on"
+ vmContainer: 'Residente em',
+
+ // Original text: "Alarms"
+ alarmMessage: 'Alarmes',
+
+ // Original text: "No alarms"
+ noAlarms: 'Sem alarmes',
+
+ // Original text: "Date"
+ alarmDate: 'Data',
+
+ // Original text: "Content"
+ alarmContent: 'Conteúdo',
+
+ // Original text: "Issue on"
+ alarmObject: 'Tipo de alarme',
+
+ // Original text: "Pool"
+ alarmPool: 'Pool',
+
+ // Original text: "Remove all alarms"
+ alarmRemoveAll: 'Remover todos os alarmes',
+
+ // Original text: '{used}% used ({free} left)'
+ spaceLeftTooltip: undefined,
+
+ // Original text: "Create a new VM on {select}"
+ newVmCreateNewVmOn: 'Criar uma nova VM em {pool}',
+
+ // Original text: 'Create a new VM on {select1} or {select2}'
+ newVmCreateNewVmOn2: undefined,
+
+ // Original text: 'You have no permission to create a VM'
+ newVmCreateNewVmNoPermission: undefined,
+
+ // Original text: "Infos"
+ newVmInfoPanel: 'Informações',
+
+ // Original text: "Name"
+ newVmNameLabel: 'Nome',
+
+ // Original text: "Template"
+ newVmTemplateLabel: 'Modelo (Template)',
+
+ // Original text: "Description"
+ newVmDescriptionLabel: 'Descrição',
+
+ // Original text: "Performances"
+ newVmPerfPanel: 'Desempenho',
+
+ // Original text: "vCPUs"
+ newVmVcpusLabel: 'vCPUs',
+
+ // Original text: "RAM"
+ newVmRamLabel: 'RAM',
+
+ // Original text: 'Static memory max'
+ newVmStaticMaxLabel: undefined,
+
+ // Original text: 'Dynamic memory min'
+ newVmDynamicMinLabel: undefined,
+
+ // Original text: 'Dynamic memory max'
+ newVmDynamicMaxLabel: undefined,
+
+ // Original text: "Install settings"
+ newVmInstallSettingsPanel: 'Definições de instalação',
+
+ // Original text: "ISO/DVD"
+ newVmIsoDvdLabel: 'ISO/DVD',
+
+ // Original text: "Network"
+ newVmNetworkLabel: 'Rede',
+
+ // Original text: 'e.g: http://httpredir.debian.org/debian'
+ newVmInstallNetworkPlaceHolder: undefined,
+
+ // Original text: "PV Args"
+ newVmPvArgsLabel: 'PV argos',
+
+ // Original text: "PXE"
+ newVmPxeLabel: 'PXE',
+
+ // Original text: "Interfaces"
+ newVmInterfacesPanel: 'Interfaces',
+
+ // Original text: "MAC"
+ newVmMacLabel: 'MAC',
+
+ // Original text: "Add interface"
+ newVmAddInterface: 'Adicionar uma interface',
+
+ // Original text: "Disks"
+ newVmDisksPanel: 'Discos',
+
+ // Original text: "SR"
+ newVmSrLabel: 'SR',
+
+ // Original text: "Bootable"
+ newVmBootableLabel: 'Inicializável',
+
+ // Original text: "Size"
+ newVmSizeLabel: 'Tamanho',
+
+ // Original text: "Add disk"
+ newVmAddDisk: 'Adicionar disco',
+
+ // Original text: "Summary"
+ newVmSummaryPanel: 'Sumário',
+
+ // Original text: "Create"
+ newVmCreate: 'Criar',
+
+ // Original text: "Reset"
+ newVmReset: 'Reiniciar',
+
+ // Original text: "Select template"
+ newVmSelectTemplate: 'Selecionar modelo (template)',
+
+ // Original text: "SSH key"
+ newVmSshKey: 'Chave SSH',
+
+ // Original text: "Config drive"
+ newVmConfigDrive: 'Configuração do drive',
+
+ // Original text: "Custom config"
+ newVmCustomConfig: 'Configuração personalizada',
+
+ // Original text: "Boot VM after creation"
+ newVmBootAfterCreate: 'Inicializar VM após sua criação',
+
+ // Original text: "Auto-generated if empty"
+ newVmMacPlaceholder: 'Auto-gerada se vazio',
+
+ // Original text: "CPU weight"
+ newVmCpuWeightLabel: 'Carga da CPU',
+
+ // Original text: 'Default: {value, number}'
+ newVmDefaultCpuWeight: undefined,
+
+ // Original text: 'CPU cap'
+ newVmCpuCapLabel: undefined,
+
+ // Original text: 'Default: {value, number}'
+ newVmDefaultCpuCap: undefined,
+
+ // Original text: "Cloud config"
+ newVmCloudConfig: 'Configuração do Cloud',
+
+ // Original text: "Create VMs"
+ newVmCreateVms: 'Criar VMs',
+
+ // Original text: "Are you sure you want to create {nbVms} VMs?"
+ newVmCreateVmsConfirm: 'Você tem certeza que deseja criar {nbVms} VMs?',
+
+ // Original text: "Multiple VMs:"
+ newVmMultipleVms: 'Multiplas VMs',
+
+ // Original text: 'Select a resource set:'
+ newVmSelectResourceSet: undefined,
+
+ // Original text: 'Name pattern:'
+ newVmMultipleVmsPattern: undefined,
+
+ // Original text: 'e.g.: \\{name\\}_%'
+ newVmMultipleVmsPatternPlaceholder: undefined,
+
+ // Original text: 'First index:'
+ newVmFirstIndex: undefined,
+
+ // Original text: 'Recalculate VMs number'
+ newVmNumberRecalculate: undefined,
+
+ // Original text: 'Refresh VMs name'
+ newVmNameRefresh: undefined,
+
+ // Original text: 'Advanced'
+ newVmAdvancedPanel: undefined,
+
+ // Original text: 'Show advanced settings'
+ newVmShowAdvanced: undefined,
+
+ // Original text: 'Hide advanced settings'
+ newVmHideAdvanced: undefined,
+
+ // Original text: "Resource sets"
+ resourceSets: 'Ajustes de recursos',
+
+ // Original text: 'No resource sets.'
+ noResourceSets: undefined,
+
+ // Original text: 'Loading resource sets'
+ loadingResourceSets: undefined,
+
+ // Original text: "Resource set name"
+ resourceSetName: 'Ajuste de nome do recurso',
+
+ // Original text: 'Recompute all limits'
+ recomputeResourceSets: undefined,
+
+ // Original text: "Save"
+ saveResourceSet: 'Salvar',
+
+ // Original text: "Reset"
+ resetResourceSet: 'Redefinir',
+
+ // Original text: "Edit"
+ editResourceSet: 'Editar',
+
+ // Original text: "Delete"
+ deleteResourceSet: 'Deletar',
+
+ // Original text: "Delete resource set"
+ deleteResourceSetWarning: 'Deletar grupo de recurso',
+
+ // Original text: "Are you sure you want to delete this resource set?"
+ deleteResourceSetQuestion: 'Você tem certeza que deseja deletar este ajuste?',
+
+ // Original text: "Missing objects:"
+ resourceSetMissingObjects: 'Objetos faltando',
+
+ // Original text: "vCPUs"
+ resourceSetVcpus: 'vCPUs',
+
+ // Original text: "Memory"
+ resourceSetMemory: 'Memória',
+
+ // Original text: "Storage"
+ resourceSetStorage: 'Armazenamento (Storage)',
+
+ // Original text: "Unknown"
+ unknownResourceSetValue: 'Desconhecido',
+
+ // Original text: "Available hosts"
+ availableHosts: 'Hosts disponiveis',
+
+ // Original text: "Excluded hosts"
+ excludedHosts: 'Hosts excluídos',
+
+ // Original text: "No hosts available."
+ 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.',
+
+ // Original text: "Maximum CPUs"
+ maxCpus: 'Limite de CPUs',
+
+ // Original text: "Maximum RAM (GiB)"
+ maxRam: 'Limite de RAM (GiB)',
+
+ // Original text: "Maximum disk space"
+ maxDiskSpace: 'Limite de espaço de disco',
+
+ // Original text: 'IP pool'
+ ipPool: undefined,
+
+ // Original text: 'Quantity'
+ quantity: undefined,
+
+ // Original text: "No limits."
+ noResourceSetLimits: 'Sem limites',
+
+ // Original text: "Total:"
+ totalResource: 'Total',
+
+ // Original text: "Remaining:"
+ remainingResource: 'Restando;',
+
+ // Original text: "Used:"
+ usedResource: 'Usado:',
+
+ // Original text: 'New'
+ 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.',
+
+ // Original text: "No selected VMs."
+ noSelectedVms: 'Nenhuma VM selecionada',
+
+ // Original text: "To Pool:"
+ vmImportToPool: 'Enviar para Pool:',
+
+ // Original text: "To SR:"
+ vmImportToSr: 'Enviar para SR:',
+
+ // Original text: "VMs to import"
+ vmsToImport: 'Importar VMs',
+
+ // Original text: "Reset"
+ importVmsCleanList: 'Reiniciar',
+
+ // Original text: "VM import success"
+ vmImportSuccess: 'Importação feita com sucesso',
+
+ // Original text: "VM import failed"
+ vmImportFailed: 'Falha na importação',
+
+ // Original text: "Import starting…"
+ startVmImport: 'Iniciando importação…',
+
+ // Original text: "Export starting…"
+ startVmExport: 'Iniciando exportação…',
+
+ // Original text: 'N CPUs'
+ nCpus: undefined,
+
+ // Original text: 'Memory'
+ vmMemory: undefined,
+
+ // Original text: 'Disk {position} ({capacity})'
+ diskInfo: undefined,
+
+ // Original text: 'Disk description'
+ diskDescription: undefined,
+
+ // Original text: 'No disks.'
+ noDisks: undefined,
+
+ // Original text: 'No networks.'
+ noNetworks: undefined,
+
+ // Original text: 'Network {name}'
+ networkInfo: undefined,
+
+ // Original text: 'No description available'
+ noVmImportErrorDescription: undefined,
+
+ // Original text: 'Error:'
+ vmImportError: undefined,
+
+ // Original text: '{type} file:'
+ vmImportFileType: undefined,
+
+ // Original text: 'Please to check and/or modify the VM configuration.'
+ vmImportConfigAlert: undefined,
+
+ // Original text: "No pending tasks"
+ noTasks: 'Nenhuma tarefa pendente',
+
+ // Original text: "Currently, there are not any pending XenServer tasks"
+ xsTasks: 'Atualmente nenhuma tarefa esta pendente no XenServer',
+
+ // Original text: 'Schedules'
+ backupSchedules: undefined,
+
+ // Original text: 'Get remote'
+ getRemote: undefined,
+
+ // Original text: 'List Remote'
+ listRemote: undefined,
+
+ // Original text: 'simple'
+ simpleBackup: undefined,
+
+ // Original text: "delta"
+ delta: 'delta',
+
+ // Original text: "Restore Backups"
+ restoreBackups: 'Recuperação de Backups',
+
+ // Original text: 'Click on a VM to display restore options'
+ restoreBackupsInfo: undefined,
+
+ // Original text: "Enabled"
+ remoteEnabled: 'Habilitado',
+
+ // Original text: "Error"
+ remoteError: 'erro',
+
+ // Original text: "No backup available"
+ noBackup: 'Nenhum backup disponível',
+
+ // Original text: 'VM Name'
+ backupVmNameColumn: undefined,
+
+ // Original text: 'Tags'
+ backupTags: undefined,
+
+ // Original text: 'Last Backup'
+ lastBackupColumn: undefined,
+
+ // Original text: 'Available Backups'
+ availableBackupsColumn: undefined,
+
+ // Original text: 'Missing parameters'
+ backupRestoreErrorTitle: undefined,
+
+ // Original text: 'Choose a SR and a backup'
+ backupRestoreErrorMessage: undefined,
+
+ // Original text: 'Display backups'
+ displayBackup: undefined,
+
+ // Original text: 'Import VM'
+ importBackupTitle: undefined,
+
+ // Original text: 'Starting your backup import'
+ importBackupMessage: undefined,
+
+ // Original text: 'VMs to backup'
+ vmsToBackup: undefined,
+
+ // Original text: 'Emergency shutdown Host{nHosts, plural, one {} other {s}}'
+ emergencyShutdownHostsModalTitle: undefined,
+
+ // Original text: 'Are you sure you want to shutdown {nHosts} Host{nHosts, plural, one {} other {s}}?'
+ emergencyShutdownHostsModalMessage: undefined,
+
+ // Original text: "Shutdown host"
+ 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?',
+
+ // Original text: 'Add host'
+ addHostModalTitle: undefined,
+
+ // Original text: 'Are you sure you want to add {host} to {pool}?'
+ addHostModalMessage: undefined,
+
+ // Original text: "Restart host"
+ 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?',
+
+ // Original text: 'Restart Host{nHosts, plural, one {} other {s}} agent{nHosts, plural, one {} other {s}}'
+ restartHostsAgentsModalTitle: undefined,
+
+ // Original text: 'Are you sure you want to restart {nHosts} Host{nHosts, plural, one {} other {s}} agent{nHosts, plural, one {} other {s}}?'
+ restartHostsAgentsModalMessage: undefined,
+
+ // Original text: 'Restart Host{nHosts, plural, one {} other {s}}'
+ restartHostsModalTitle: undefined,
+
+ // Original text: 'Are you sure you want to restart {nHosts} Host{nHosts, plural, one {} other {s}}?'
+ restartHostsModalMessage: undefined,
+
+ // Original text: "Start VM{vms, plural, one {} other {s}}"
+ 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}}?',
+
+ // Original text: 'Stop Host{nHosts, plural, one {} other {s}}'
+ stopHostsModalTitle: undefined,
+
+ // Original text: 'Are you sure you want to stop {nHosts} Host{nHosts, plural, one {} other {s}}?'
+ stopHostsModalMessage: undefined,
+
+ // Original text: "Stop VM{vms, plural, one {} other {s}}"
+ 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}}?',
+
+ // Original text: "Restart VM"
+ restartVmModalTitle: 'Reiniciar VM',
+
+ // Original text: "Are you sure you want to restart {name}?"
+ restartVmModalMessage: 'Você tem certeza que deseja reiniciar {name}?',
+
+ // Original text: "Stop VM"
+ stopVmModalTitle: 'Parar VM',
+
+ // Original text: "Are you sure you want to stop {name}?"
+ stopVmModalMessage: 'Você tem certeza que deseja parar {name}?',
+
+ // Original text: "Restart VM{vms, plural, one {} other {s}}"
+ 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}}?',
+
+ // 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}}?',
+
+ // 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',
+
+ // 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',
+
+ // Original text: "Migrate VM"
+ migrateVmModalTitle: 'Migrar VM',
+
+ // Original text: 'Select a destination host:'
+ migrateVmSelectHost: undefined,
+
+ // Original text: 'Select a migration network:'
+ migrateVmSelectMigrationNetwork: undefined,
+
+ // Original text: 'For each VDI, select an SR:'
+ migrateVmSelectSrs: undefined,
+
+ // Original text: 'For each VIF, select a network:'
+ migrateVmSelectNetworks: undefined,
+
+ // Original text: 'Select a destination SR:'
+ migrateVmsSelectSr: undefined,
+
+ // Original text: 'Select a destination SR for local disks:'
+ migrateVmsSelectSrIntraPool: undefined,
+
+ // Original text: 'Select a network on which to connect each VIF:'
+ migrateVmsSelectNetwork: undefined,
+
+ // Original text: 'Smart mapping'
+ migrateVmsSmartMapping: undefined,
+
+ // Original text: 'Name'
+ migrateVmName: undefined,
+
+ // Original text: 'SR'
+ migrateVmSr: undefined,
+
+ // Original text: 'VIF'
+ migrateVmVif: undefined,
+
+ // Original text: 'Network'
+ migrateVmNetwork: undefined,
+
+ // Original text: 'No target host'
+ migrateVmNoTargetHost: undefined,
+
+ // Original text: 'A target host is required to migrate a VM'
+ migrateVmNoTargetHostMessage: undefined,
+
+ // Original text: 'Delete VDI'
+ deleteVdiModalTitle: undefined,
+
+ // Original text: 'Are you sure you want to delete this disk? ALL DATA ON THIS DISK WILL BE LOST'
+ deleteVdiModalMessage: undefined,
+
+ // Original text: 'Revert your VM'
+ revertVmModalTitle: undefined,
+
+ // Original text: 'Delete snapshot'
+ deleteSnapshotModalTitle: undefined,
+
+ // Original text: 'Are you sure you want to delete this snapshot?'
+ deleteSnapshotModalMessage: undefined,
+
+ // Original text: 'Are you sure you want to revert this VM to the snapshot state? This operation is irreversible.'
+ revertVmModalMessage: undefined,
+
+ // Original text: 'Snapshot before'
+ revertVmModalSnapshotBefore: undefined,
+
+ // Original text: "Import a {name} Backup"
+ importBackupModalTitle: 'Importar este Backup: {name}',
+
+ // Original text: "Start VM after restore"
+ importBackupModalStart: 'Iniciar VM após restauração',
+
+ // Original text: "Select your 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?',
+
+ // 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?',
+
+ // Original text: "This operation is definitive."
+ definitiveMessageModal: 'Esta operação é definitiva.',
+
+ // Original text: "Previous SR Usage"
+ 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.',
+
+ // 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.',
+
+ // 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?',
+
+ // 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!',
+
+ // Original text: "Host"
+ serverHost: 'Host',
+
+ // Original text: "Username"
+ serverUsername: 'Nome de Usuário',
+
+ // Original text: "Password"
+ serverPassword: 'Senha',
+
+ // Original text: "Action"
+ serverAction: 'Ação',
+
+ // Original text: "Read Only"
+ serverReadOnly: 'Modo Leitura',
+
+ // Original text: 'Disconnect server'
+ serverDisconnect: undefined,
+
+ // Original text: 'username'
+ serverPlaceHolderUser: undefined,
+
+ // Original text: 'password'
+ serverPlaceHolderPassword: undefined,
+
+ // Original text: 'address[:port]'
+ serverPlaceHolderAddress: undefined,
+
+ // Original text: 'Connect'
+ serverConnect: undefined,
+
+ // Original text: "Copy VM"
+ copyVm: 'Copiar VM',
+
+ // Original text: "Are you sure you want to copy this VM to {SR}?"
+ copyVmConfirm: 'Você tem certeza que deseja copiar esta VM para {SR}?',
+
+ // Original text: "Name"
+ copyVmName: 'Nome',
+
+ // Original text: 'Name pattern'
+ copyVmNamePattern: undefined,
+
+ // Original text: "If empty: name of the copied VM"
+ copyVmNamePlaceholder: 'Se vazio: Nome da VM copiada',
+
+ // Original text: 'e.g.: "\\{name\\}_COPY"'
+ copyVmNamePatternPlaceholder: undefined,
+
+ // Original text: "Select SR"
+ copyVmSelectSr: 'Selecionar SR',
+
+ // Original text: "Use compression"
+ copyVmCompress: 'Compressão',
+
+ // Original text: 'No target SR'
+ copyVmsNoTargetSr: undefined,
+
+ // Original text: 'A target SR is required to copy a VM'
+ copyVmsNoTargetSrMessage: undefined,
+
+ // Original text: 'Detach host'
+ detachHostModalTitle: undefined,
+
+ // 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: undefined,
+
+ // Original text: 'Detach'
+ detachHost: undefined,
+
+ // Original text: "Create network"
+ newNetworkCreate: 'Criar rede',
+
+ // Original text: 'Create bonded network'
+ newBondedNetworkCreate: undefined,
+
+ // Original text: "Interface"
+ newNetworkInterface: 'Inerface',
+
+ // Original text: "Name"
+ newNetworkName: 'Nome',
+
+ // Original text: "Description"
+ newNetworkDescription: 'Descrição',
+
+ // Original text: "VLAN"
+ newNetworkVlan: 'VLAN',
+
+ // Original text: "No VLAN if empty"
+ newNetworkDefaultVlan: 'Sem VLAN, caso esteja vazia',
+
+ // Original text: "MTU"
+ newNetworkMtu: 'MTU',
+
+ // Original text: "Default: 1500"
+ newNetworkDefaultMtu: 'Padrão: 1500',
+
+ // Original text: 'Name required'
+ newNetworkNoNameErrorTitle: undefined,
+
+ // Original text: 'A name is required to create a network'
+ newNetworkNoNameErrorMessage: undefined,
+
+ // Original text: 'Bond mode'
+ newNetworkBondMode: undefined,
+
+ // Original text: "Delete network"
+ deleteNetwork: 'Deletar rede',
+
+ // Original text: "Are you sure you want to delete this network?"
+ deleteNetworkConfirm: 'Você tem certeza que deseja deletar esta rede?',
+
+ // Original text: 'This network is currently in use'
+ networkInUse: undefined,
+
+ // Original text: 'Bonded'
+ pillBonded: undefined,
+
+ // Original text: 'Host'
+ addHostSelectHost: undefined,
+
+ // Original text: 'No host'
+ addHostNoHost: undefined,
+
+ // Original text: 'No host selected to be added'
+ addHostNoHostMessage: undefined,
+
+ // Original text: "Xen Orchestra"
+ xenOrchestra: 'Xen Orchestra',
+
+ // Original text: "server"
+ xenOrchestraServer: 'servidor',
+
+ // Original text: "web client"
+ xenOrchestraWeb: 'cliente web',
+
+ // Original text: "No pro support provided!"
+ noProSupport: 'Nenhum suporte pro fornecido!',
+
+ // Original text: "Use in production at your own risks"
+ noProductionUse: 'O uso deste em produção é por sua conta e risco',
+
+ // Original text: "You can download our turnkey appliance at"
+ downloadXoa: 'Você pode baixar nosso turnkey appliance em',
+
+ // Original text: "Bug Tracker"
+ bugTracker: 'Rastreador de bug',
+
+ // Original text: "Issues? Report it!"
+ bugTrackerText: 'Problemas? Envie agora!',
+
+ // Original text: "Community"
+ community: 'Comunidade',
+
+ // Original text: "Join our community forum!"
+ communityText: 'Participe do nosso forum e de nossa comunidade!',
+
+ // Original text: "Free Trial for Premium Edition!"
+ freeTrial: 'Versão Premium Edition disponível para período de teste (Trial)',
+
+ // Original text: "Request your trial now!"
+ freeTrialNow: 'Peça já seu período de teste (Trial)',
+
+ // Original text: "Any issue?"
+ issues: 'Algum problema encontrado?',
+
+ // Original text: "Problem? Contact us!"
+ issuesText: 'Problemas? Entre em contato conosco',
+
+ // Original text: "Documentation"
+ documentation: 'Documentação',
+
+ // Original text: "Read our official doc"
+ documentationText: 'Leia nossa documentação oficial (Em inglês)',
+
+ // Original text: "Pro support included"
+ proSupportIncluded: 'Suporte Pro incluído',
+
+ // Original text: "Acces your XO Account"
+ xoAccount: 'Acesse sua conta XO',
+
+ // Original text: "Report a problem"
+ openTicket: 'Enviar um problema',
+
+ // Original text: "Problem? Open a ticket!"
+ openTicketText: 'Algum problema? Abra um ticket agora!',
+
+ // Original text: "Upgrade needed"
+ upgradeNeeded: 'Atualização necessária',
+
+ // Original text: "Upgrade now!"
+ upgradeNow: 'Atualize agora!',
+
+ // Original text: "Or"
+ or: 'Ou',
+
+ // Original text: "Try it for free!"
+ tryIt: 'Teste agora, é grátis!',
+
+ // Original text: "This feature is available starting from {plan} Edition"
+ availableIn: 'Este recurso é disponível a partir da versão {plan}',
+
+ // Original text: 'This feature is not available in your version, contact your administrator to know more.'
+ notAvailable: undefined,
+
+ // Original text: 'Updates'
+ updateTitle: undefined,
+
+ // Original text: "Registration"
+ registration: 'Inscrição',
+
+ // Original text: "Trial"
+ trial: 'Teste (Trial)',
+
+ // Original text: "Settings"
+ settings: 'Configurações',
+
+ // Original text: 'Proxy settings'
+ proxySettings: undefined,
+
+ // Original text: 'Host (myproxy.example.org)'
+ proxySettingsHostPlaceHolder: undefined,
+
+ // Original text: 'Port (eg: 3128)'
+ proxySettingsPortPlaceHolder: undefined,
+
+ // Original text: 'Username'
+ proxySettingsUsernamePlaceHolder: undefined,
+
+ // Original text: 'Password'
+ proxySettingsPasswordPlaceHolder: undefined,
+
+ // Original text: 'Your email account'
+ updateRegistrationEmailPlaceHolder: undefined,
+
+ // Original text: 'Your password'
+ updateRegistrationPasswordPlaceHolder: undefined,
+
+ // Original text: "Update"
+ update: 'Atualizar (Update)',
+
+ // Original text: 'Refresh'
+ refresh: undefined,
+
+ // Original text: "Upgrade"
+ upgrade: 'Atualização (Upgrade)',
+
+ // Original text: "No updater available for 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!',
+
+ // 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!',
+
+ // Original text: "Current version:"
+ currentVersion: 'Versão atual:',
+
+ // Original text: "Register"
+ register: 'Registrar',
+
+ // Original text: 'Edit registration'
+ 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)',
+
+ // 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!',
+
+ // 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',
+
+ // 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.',
+
+ // Original text: 'No update information available'
+ noUpdateInfo: undefined,
+
+ // Original text: 'Update information may be available'
+ waitingUpdateInfo: undefined,
+
+ // Original text: 'Your XOA is up-to-date'
+ upToDate: undefined,
+
+ // Original text: 'You need to update your XOA (new version is available)'
+ mustUpgrade: undefined,
+
+ // Original text: 'Your XOA is not registered for updates'
+ registerNeeded: undefined,
+
+ // Original text: "Can't fetch update information"
+ updaterError: undefined,
+
+ // Original text: 'Upgrade successful'
+ promptUpgradeReloadTitle: undefined,
+
+ // Original text: 'Your XOA has successfully upgraded, and your browser must reload the application. Do you want to reload now ?'
+ promptUpgradeReloadMessage: undefined,
+
+ // Original text: "Xen Orchestra from the sources"
+ 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.',
+
+ // 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:',
+
+ // 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.',
+
+ // Original text: "Connect PIF"
+ connectPif: 'Conectar PIF',
+
+ // Original text: "Are you sure you want to connect this PIF?"
+ connectPifConfirm: 'Você tem certeza que deseja conectar este PIF?',
+
+ // Original text: "Disconnect PIF"
+ disconnectPif: 'Desconectar PIF',
+
+ // Original text: "Are you sure you want to disconnect this PIF?"
+ disconnectPifConfirm: 'Você tem certeza que deseja desconectar este PIF?',
+
+ // Original text: "Delete PIF"
+ deletePif: 'Deletar PIF',
+
+ // Original text: "Are you sure you want to delete this PIF?"
+ deletePifConfirm: 'Você tem certeza que deseja conectar este PIF?',
+
+ // Original text: 'Username'
+ username: undefined,
+
+ // Original text: 'Password'
+ password: undefined,
+
+ // Original text: 'Language'
+ language: undefined,
+
+ // Original text: 'Old password'
+ oldPasswordPlaceholder: undefined,
+
+ // Original text: 'New password'
+ newPasswordPlaceholder: undefined,
+
+ // Original text: 'Confirm new password'
+ confirmPasswordPlaceholder: undefined,
+
+ // Original text: 'Confirmation password incorrect'
+ confirmationPasswordError: undefined,
+
+ // Original text: 'Password does not match the confirm password.'
+ confirmationPasswordErrorBody: undefined,
+
+ // Original text: 'Password changed'
+ pwdChangeSuccess: undefined,
+
+ // Original text: 'Your password has been successfully changed.'
+ pwdChangeSuccessBody: undefined,
+
+ // Original text: 'Incorrect password'
+ pwdChangeError: undefined,
+
+ // Original text: 'The old password provided is incorrect. Your password has not been changed.'
+ pwdChangeErrorBody: undefined,
+
+ // Original text: 'OK'
+ changePasswordOk: undefined,
+
+ // Original text: 'SSH keys'
+ sshKeys: undefined,
+
+ // Original text: 'New SSH key'
+ newSshKey: undefined,
+
+ // Original text: 'Delete'
+ deleteSshKey: undefined,
+
+ // Original text: 'No SSH keys'
+ noSshKeys: undefined,
+
+ // Original text: 'New SSH key'
+ newSshKeyModalTitle: undefined,
+
+ // Original text: 'Invalid key'
+ sshKeyErrorTitle: undefined,
+
+ // Original text: 'An SSH key requires both a title and a key.'
+ sshKeyErrorMessage: undefined,
+
+ // Original text: 'Title'
+ title: undefined,
+
+ // Original text: 'Key'
+ key: undefined,
+
+ // Original text: 'Delete SSH key'
+ deleteSshKeyConfirm: undefined,
+
+ // Original text: 'Are you sure you want to delete the SSH key {title}?'
+ deleteSshKeyConfirmMessage: undefined,
+
+ // Original text: 'Others'
+ others: undefined,
+
+ // Original text: 'Loading logs…'
+ loadingLogs: undefined,
+
+ // Original text: 'User'
+ logUser: undefined,
+
+ // Original text: 'Method'
+ logMethod: undefined,
+
+ // Original text: 'Params'
+ logParams: undefined,
+
+ // Original text: 'Message'
+ logMessage: undefined,
+
+ // Original text: 'Error'
+ logError: undefined,
+
+ // Original text: 'Display details'
+ logDisplayDetails: undefined,
+
+ // Original text: 'Date'
+ logTime: undefined,
+
+ // Original text: 'No stack trace'
+ logNoStackTrace: undefined,
+
+ // Original text: 'No params'
+ logNoParams: undefined,
+
+ // Original text: 'Delete log'
+ logDelete: undefined,
+
+ // Original text: 'Delete all logs'
+ logDeleteAll: undefined,
+
+ // Original text: 'Delete all logs'
+ logDeleteAllTitle: undefined,
+
+ // Original text: 'Are you sure you want to delete all the logs?'
+ logDeleteAllMessage: undefined,
+
+ // Original text: 'Name'
+ ipPoolName: undefined,
+
+ // Original text: 'IPs'
+ ipPoolIps: undefined,
+
+ // Original text: 'IPs (e.g.: 1.0.0.12-1.0.0.17;1.0.0.23)'
+ ipPoolIpsPlaceholder: undefined,
+
+ // Original text: 'Networks'
+ ipPoolNetworks: undefined,
+
+ // Original text: 'No IP pools'
+ ipsNoIpPool: undefined,
+
+ // Original text: 'Create'
+ ipsCreate: undefined,
+
+ // Original text: 'Delete all IP pools'
+ ipsDeleteAllTitle: undefined,
+
+ // Original text: 'Are you sure you want to delete all the IP pools?'
+ ipsDeleteAllMessage: undefined,
+
+ // Original text: 'VIFs'
+ ipsVifs: undefined,
+
+ // Original text: 'Not used'
+ ipsNotUsed: undefined,
+
+ // Original text: 'Keyboard shortcuts'
+ shortcutModalTitle: undefined,
+
+ // Original text: 'Global'
+ shortcut_XoApp: undefined,
+
+ // Original text: 'Go to hosts list'
+ shortcut_GO_TO_HOSTS: undefined,
+
+ // Original text: 'Go to pools list'
+ shortcut_GO_TO_POOLS: undefined,
+
+ // Original text: 'Go to VMs list'
+ shortcut_GO_TO_VMS: undefined,
+
+ // Original text: 'Create a new VM'
+ shortcut_CREATE_VM: undefined,
+
+ // Original text: 'Unfocus field'
+ shortcut_UNFOCUS: undefined,
+
+ // Original text: 'Show shortcuts key bindings'
+ shortcut_HELP: undefined,
+
+ // Original text: 'Home'
+ shortcut_Home: undefined,
+
+ // Original text: 'Focus search bar'
+ shortcut_SEARCH: undefined,
+
+ // Original text: 'Next item'
+ shortcut_NAV_DOWN: undefined,
+
+ // Original text: 'Previous item'
+ shortcut_NAV_UP: undefined,
+
+ // Original text: 'Select item'
+ shortcut_SELECT: undefined,
+
+ // Original text: 'Open'
+ shortcut_JUMP_INTO: undefined,
+
+ // Original text: 'VM'
+ settingsAclsButtonTooltipVM: undefined,
+
+ // Original text: 'Hosts'
+ settingsAclsButtonTooltiphost: undefined,
+
+ // Original text: 'Pool'
+ settingsAclsButtonTooltippool: undefined,
+
+ // Original text: 'SR'
+ settingsAclsButtonTooltipSR: undefined,
+
+ // Original text: 'Network'
+ settingsAclsButtonTooltipnetwork: undefined,
+}
diff --git a/packages/xo-web/src/common/intl/locales/zh.js b/packages/xo-web/src/common/intl/locales/zh.js
new file mode 100644
index 000000000..2d6badb0a
--- /dev/null
+++ b/packages/xo-web/src/common/intl/locales/zh.js
@@ -0,0 +1,2300 @@
+// See http://momentjs.com/docs/#/use-it/browserify/
+import 'moment/locale/zh-cn'
+
+import reactIntlData from 'react-intl/locale-data/zh'
+import { addLocaleData } from 'react-intl'
+addLocaleData(reactIntlData)
+
+// ===================================================================
+
+export default {
+ // Original text: "Long click to edit"
+ editableLongClickPlaceholder: '长按编辑',
+
+ // Original text: "Click to edit"
+ editableClickPlaceholder: '点击编辑',
+
+ // Original text: "OK"
+ alertOk: '确认',
+
+ // Original text: "OK"
+ confirmOk: '确认',
+
+ // Original text: "Cancel"
+ confirmCancel: '取消',
+
+ // Original text: "On error"
+ onError: '出现错误',
+
+ // Original text: "Successful"
+ successful: '成功',
+
+ // Original text: "Home"
+ homePage: '主页',
+
+ // Original text: "Dashboard"
+ dashboardPage: '仪表盘',
+
+ // Original text: "Overview"
+ overviewDashboardPage: '概览',
+
+ // Original text: "Visualizations"
+ overviewVisualizationDashboardPage: '虚拟化',
+
+ // Original text: "Statistics"
+ overviewStatsDashboardPage: '状态统计',
+
+ // Original text: "Health"
+ overviewHealthDashboardPage: '健康状态',
+
+ // Original text: "Self service"
+ selfServicePage: '自助服务',
+
+ // Original text: "Dashboard"
+ selfServiceDashboardPage: '仪表盘',
+
+ // Original text: "Administration"
+ selfServiceAdminPage: '管理',
+
+ // Original text: "Backup"
+ backupPage: '备份',
+
+ // Original text: "Jobs"
+ jobsPage: '任务',
+
+ // Original text: "Updates"
+ updatePage: '更新',
+
+ // Original text: "Settings"
+ settingsPage: '设置',
+
+ // Original text: "Servers"
+ settingsServersPage: '服务器',
+
+ // Original text: "Users"
+ settingsUsersPage: '用户',
+
+ // Original text: "Groups"
+ settingsGroupsPage: '组',
+
+ // Original text: "ACLs"
+ settingsAclsPage: '访问控制',
+
+ // Original text: "Plugins"
+ settingsPluginsPage: '插件',
+
+ // Original text: "About"
+ aboutPage: '关于',
+
+ // Original text: "New"
+ newMenu: '新建',
+
+ // Original text: "Tasks"
+ taskMenu: '任务',
+
+ // Original text: "Tasks"
+ taskPage: '任务',
+
+ // Original text: "VM"
+ newVmPage: '虚拟机',
+
+ // Original text: "Storage"
+ newSrPage: '存储',
+
+ // Original text: "Server"
+ newServerPage: '服务器',
+
+ // Original text: "Import"
+ newImport: '导入',
+
+ // Original text: "Overview"
+ backupOverviewPage: '概览',
+
+ // Original text: "New"
+ backupNewPage: '新建',
+
+ // Original text: "Remotes"
+ backupRemotesPage: '远程',
+
+ // Original text: "Restore"
+ backupRestorePage: '恢复',
+
+ // Original text: "Schedule"
+ schedule: '计划',
+
+ // Original text: "New VM backup"
+ newVmBackup: '新建虚拟机备份',
+
+ // Original text: "Edit VM backup"
+ editVmBackup: '编辑虚拟机备份',
+
+ // Original text: "Backup"
+ backup: '备份',
+
+ // Original text: "Rolling Snapshot"
+ rollingSnapshot: '滚动快照',
+
+ // Original text: "Delta Backup"
+ deltaBackup: '差异备份',
+
+ // Original text: "Disaster Recovery"
+ disasterRecovery: '灾难恢复',
+
+ // Original text: "Continuous Replication"
+ continuousReplication: '持续复制',
+
+ // Original text: "Overview"
+ jobsOverviewPage: '概览',
+
+ // Original text: "New"
+ jobsNewPage: '新建',
+
+ // Original text: "Scheduling"
+ jobsSchedulingPage: '计划',
+
+ // Original text: "Custom Job"
+ customJob: '自定义任务',
+
+ // Original text: "User"
+ userPage: '用户',
+
+ // Original text: "Sign out"
+ signOut: '注销',
+
+ // Original text: "Fetching data…"
+ homeFetchingData: '获取数据',
+
+ // Original text: "Welcome on Xen Orchestra!"
+ homeWelcome: '欢迎使用Xen Orchestra',
+
+ // Original text: "Add your XenServer hosts or pools"
+ homeWelcomeText: '添加您的XenServer主机或资源池',
+
+ // Original text: "Want some help?"
+ homeHelp: '需要帮助?',
+
+ // Original text: "Add server"
+ homeAddServer: '添加服务器',
+
+ // Original text: "Online Doc"
+ homeOnlineDoc: '在线文档',
+
+ // Original text: "Pro Support"
+ homeProSupport: '专业支持',
+
+ // Original text: "There are no VMs!"
+ homeNoVms: '没有可用的虚拟机',
+
+ // Original text: "Or…"
+ homeNoVmsOr: '或',
+
+ // Original text: "Import VM"
+ homeImportVm: '导入虚拟机',
+
+ // Original text: "Import an existing VM in xva format"
+ homeImportVmMessage: '导入一个XVA格式的虚拟机',
+
+ // Original text: "Restore a backup"
+ homeRestoreBackup: '恢复到备份',
+
+ // Original text: "Restore a backup from a remote store"
+ homeRestoreBackupMessage: '恢复到远程存储上的备份',
+
+ // Original text: "This will create a new VM"
+ homeNewVmMessage: '将创建一个新的虚拟机',
+
+ // Original text: "Filters"
+ homeFilters: '过滤器',
+
+ // Original text: "Pool"
+ homeTypePool: '资源池',
+
+ // Original text: "Host"
+ homeTypeHost: '主机',
+
+ // Original text: "VM"
+ homeTypeVm: '虚拟机',
+
+ // Original text: "SR"
+ homeTypeSr: '数据存储',
+
+ // Original text: "VDI"
+ homeTypeVdi: '虚拟硬盘',
+
+ // Original text: "Sort"
+ homeSort: '排序',
+
+ // Original text: "Pools"
+ homeAllPools: '资源池',
+
+ // Original text: "Hosts"
+ homeAllHosts: '主机',
+
+ // Original text: "Tags"
+ homeAllTags: '标签',
+
+ // Original text: "New VM"
+ homeNewVm: '新建虚拟机',
+
+ // Original text: "Running hosts"
+ homeFilterRunningHosts: '正在运行的主机',
+
+ // Original text: "Disabled hosts"
+ homeFilterDisabledHosts: '不可用的主机',
+
+ // Original text: "Running VMs"
+ homeFilterRunningVms: '正在运行的虚拟机',
+
+ // Original text: "Non running VMs"
+ homeFilterNonRunningVms: '未运行的虚拟机',
+
+ // Original text: "Pending VMs"
+ homeFilterPendingVms: '正在创建的虚拟机',
+
+ // Original text: "HVM guests"
+ homeFilterHvmGuests: 'HVM客户机',
+
+ // Original text: "Tags"
+ homeFilterTags: '标签',
+
+ // Original text: "Sort by"
+ homeSortBy: '排序方式',
+
+ // Original text: "Name"
+ homeSortByName: '名称',
+
+ // Original text: "Power state"
+ homeSortByPowerstate: '电源状态',
+
+ // Original text: "RAM"
+ homeSortByRAM: '内存',
+
+ // Original text: "vCPUs"
+ homeSortByvCPUs: '虚拟机CPU',
+
+ // Original text: "CPUs"
+ homeSortByCpus: 'CPU',
+
+ // Original text: "{displayed, number}x {icon} (on {total, number})"
+ homeDisplayedItems: undefined,
+
+ // Original text: "{selected, number}x {icon} selected (on {total, number})"
+ homeSelectedItems: undefined,
+
+ // Original text: "More"
+ homeMore: '更多',
+
+ // Original text: "Migrate to…"
+ homeMigrateTo: '迁移至…',
+
+ // Original text: "Missing patches"
+ homeMissingPaths: '缺少补丁',
+
+ // Original text: "High Availability"
+ highAvailability: '高可用',
+
+ // Original text: "Add"
+ add: '添加',
+
+ // Original text: "Remove"
+ remove: '删除',
+
+ // Original text: "Preview"
+ preview: '预览',
+
+ // Original text: "Item"
+ item: '项',
+
+ // Original text: "No selected value"
+ noSelectedValue: '没有选择的值',
+
+ // Original text: "Choose user(s) and/or group(s)"
+ selectSubjects: '选择用户和/或用户组',
+
+ // Original text: "Select Object(s)…"
+ selectObjects: '选择对象',
+
+ // Original text: "Choose a role"
+ selectRole: '选择一个角色',
+
+ // Original text: "Select Host(s)…"
+ selectHosts: '选择主机',
+
+ // Original text: "Select object(s)…"
+ selectHostsVms: '选择虚拟机',
+
+ // Original text: "Select Network(s)…"
+ selectNetworks: '选择网络',
+
+ // Original text: "Select PIF(s)…"
+ selectPifs: '选择网卡',
+
+ // Original text: "Select Pool(s)…"
+ selectPools: '选择资源池',
+
+ // Original text: "Select Remote(s)…"
+ selectRemotes: '选择远程',
+
+ // Original text: "Select resource set(s)…"
+ selectResourceSets: '选择资源集',
+
+ // Original text: "Select template(s)…"
+ selectResourceSetsVmTemplate: '选择模板',
+
+ // Original text: "Select SR(s)…"
+ selectResourceSetsSr: '选择数据存储',
+
+ // Original text: "Select network(s)…"
+ selectResourceSetsNetwork: '选择网络',
+
+ // Original text: "Select disk(s)…"
+ selectResourceSetsVdi: '选择硬盘',
+
+ // Original text: "Select SR(s)…"
+ selectSrs: '选择数据存储',
+
+ // Original text: "Select VM(s)…"
+ selectVms: '选择虚拟机',
+
+ // Original text: "Select VM template(s)…"
+ selectVmTemplates: '选择虚拟机模板',
+
+ // Original text: "Select tag(s)…"
+ selectTags: '选择标签',
+
+ // Original text: "Select disk(s)…"
+ selectVdis: '选择硬盘',
+
+ // Original text: "Fill required informations."
+ fillRequiredInformations: '填写需要的信息',
+
+ // Original text: "Fill informations (optional)"
+ fillOptionalInformations: '填写信息',
+
+ // Original text: "Reset"
+ selectTableReset: '重置',
+
+ // Original text: "Month"
+ schedulingMonth: '月',
+
+ // Original text: "Every month"
+ schedulingEveryMonth: '每月',
+
+ // Original text: "Each selected month"
+ schedulingEachSelectedMonth: '每个选定月份',
+
+ // Original text: "Day of the month"
+ schedulingMonthDay: '本月的一天',
+
+ // Original text: "Every day"
+ schedulingEveryMonthDay: '每天',
+
+ // Original text: "Each selected day"
+ schedulingEachSelectedMonthDay: '每个选定天',
+
+ // Original text: "Day of the week"
+ schedulingWeekDay: '本周的一天',
+
+ // Original text: "Every day"
+ schedulingEveryWeekDay: '每天',
+
+ // Original text: "Each selected day"
+ schedulingEachSelectedWeekDay: '每个选定天',
+
+ // Original text: "Hour"
+ schedulingHour: '小时',
+
+ // Original text: "Every hour"
+ schedulingEveryHour: '每小时',
+
+ // Original text: "Every N hour"
+ schedulingEveryNHour: '每N小时',
+
+ // Original text: "Each selected hour"
+ schedulingEachSelectedHour: '每个选定小时',
+
+ // Original text: "Minute"
+ schedulingMinute: '分钟',
+
+ // Original text: "Every minute"
+ schedulingEveryMinute: '每分钟',
+
+ // Original text: "Every N minute"
+ schedulingEveryNMinute: '每N分钟',
+
+ // Original text: "Each selected minute"
+ schedulingEachSelectedMinute: '每个选定分钟',
+
+ // Original text: "Reset"
+ schedulingReset: '重置',
+
+ // Original text: "Unknown"
+ unknownSchedule: '未知',
+
+ // Original text: "Cannot edit backup"
+ backupEditNotFoundTitle: '不能编辑备份',
+
+ // Original text: "Missing required info for edition"
+ backupEditNotFoundMessage: '缺少版本所需要的信息',
+
+ // Original text: "Job"
+ job: '任务',
+
+ // Original text: "Job ID"
+ jobId: '任务ID',
+
+ // Original text: "Name"
+ jobName: '名称',
+
+ // Original text: "Start"
+ jobStart: '开始',
+
+ // Original text: "End"
+ jobEnd: '结束',
+
+ // Original text: "Duration"
+ jobDuration: '周期',
+
+ // Original text: "Status"
+ jobStatus: '状态',
+
+ // Original text: "Action"
+ jobAction: '行为',
+
+ // Original text: "Tag"
+ jobTag: '标签',
+
+ // Original text: "Scheduling"
+ jobScheduling: '计划',
+
+ // Original text: "State"
+ jobState: '状态',
+
+ // Original text: "Run job"
+ runJob: '运行任务',
+
+ // Original text: "One shot running started. See overview for logs."
+ runJobVerbose: '开始一次运行,可查看概要日志',
+
+ // Original text: "Started"
+ jobStarted: '已经开始',
+
+ // Original text: "Finished"
+ jobFinished: '已完成',
+
+ // Original text: "Save"
+ saveBackupJob: '保存',
+
+ // Original text: "Remove backup job"
+ deleteBackupSchedule: '删除备份任务',
+
+ // Original text: "Are you sure you want to delete this backup job?"
+ deleteBackupScheduleQuestion: '你确认你要删除这个备份任务吗?',
+
+ // Original text: "Enable immediately after creation"
+ scheduleEnableAfterCreation: '创建后立即启用',
+
+ // Original text: "You are editing Schedule {name} ({id}). Saving will override previous schedule state."
+ scheduleEditMessage: '你正在编辑计划{name} ({id}).保存将覆盖前一个计划状态.',
+
+ // Original text: "You are editing job {name} ({id}). Saving will override previous job state."
+ jobEditMessage: '你正在编辑任务{name} ({id}).保存将覆盖前一个任务状态',
+
+ // Original text: "No scheduled jobs."
+ noScheduledJobs: '没有计划任务',
+
+ // Original text: "No jobs found."
+ noJobs: '未找到任务',
+
+ // Original text: "No schedules found"
+ noSchedules: '未找到计划',
+
+ // Original text: "Select a xo-server API command"
+ jobActionPlaceHolder: '选择一个xo-server API 命令',
+
+ // Original text: "Select your backup type:"
+ newBackupSelection: '选择你的备份类型',
+
+ // Original text: "Remote stores for backup"
+ remoteList: '远程备份存储',
+
+ // Original text: "New File System Remote"
+ newRemote: '新建远程文件系统',
+
+ // Original text: "Local"
+ remoteTypeLocal: '本地',
+
+ // Original text: "NFS"
+ remoteTypeNfs: 'NFS',
+
+ // Original text: "SMB"
+ remoteTypeSmb: 'SMB',
+
+ // Original text: "Type"
+ remoteType: '类型',
+
+ // Original text: "Test your remote"
+ remoteTestTip: '测试你的远程配置',
+
+ // Original text: "Test Remote"
+ testRemote: '测试远程配置',
+
+ // Original text: "Test failed for {name}"
+ remoteTestFailure: '失败的测试项 {name}',
+
+ // Original text: "Test passed for {name}"
+ remoteTestSuccess: '通过的测试项{name}',
+
+ // Original text: "Error"
+ remoteTestError: '错误',
+
+ // Original text: "Test Step"
+ remoteTestStep: '测试步骤',
+
+ // Original text: "Test file"
+ remoteTestFile: '测试文件',
+
+ // Original text: "The remote appears to work correctly"
+ remoteTestSuccessMessage: '远程配置运行正常',
+
+ // Original text: "Create a new SR"
+ newSrTitle: '创建一个新的数据存储',
+
+ // Original text: "General"
+ newSrGeneral: '常规',
+
+ // Original text: "Select Strorage Type:"
+ newSrTypeSelection: '选择存储类型',
+
+ // Original text: "Settings"
+ newSrSettings: '设置',
+
+ // Original text: "Storage Usage"
+ newSrUsage: '存储利用率',
+
+ // Original text: "Summary"
+ newSrSummary: '综述',
+
+ // Original text: "Host"
+ newSrHost: '主机',
+
+ // Original text: "Type"
+ newSrType: '类型',
+
+ // Original text: "Name"
+ newSrName: '名称',
+
+ // Original text: "Description"
+ newSrDescription: '描述',
+
+ // Original text: "Server"
+ newSrServer: '服务器',
+
+ // Original text: "Path"
+ newSrPath: '路径',
+
+ // Original text: "IQN"
+ newSrIqn: 'IQN',
+
+ // Original text: "LUN"
+ newSrLun: 'LUN',
+
+ // Original text: "with auth."
+ newSrAuth: '启用认证',
+
+ // Original text: "User Name"
+ newSrUsername: '用户名',
+
+ // Original text: "Password"
+ newSrPassword: '密码',
+
+ // Original text: "Device"
+ newSrDevice: '设备',
+
+ // Original text: "in use"
+ newSrInUse: '使用中',
+
+ // Original text: "Size"
+ newSrSize: '大小',
+
+ // Original text: "Create"
+ newSrCreate: '创建',
+
+ // Original text: "Users/Groups"
+ subjectName: '用户/组',
+
+ // Original text: "Object"
+ objectName: '对象',
+
+ // Original text: "Role"
+ roleName: '角色',
+
+ // Original text: "New Group Name"
+ newGroupName: '新建组名',
+
+ // Original text: "Create Group"
+ createGroup: '创建组',
+
+ // Original text: "Create"
+ createGroupButton: '创建',
+
+ // Original text: "Delete Group"
+ deleteGroup: '删除组',
+
+ // Original text: "Are you sure you want to delete this group?"
+ deleteGroupConfirm: '你确定要删除该组?',
+
+ // Original text: "Remove user from Group"
+ removeUserFromGroup: '从组中删除用户',
+
+ // Original text: "Are you sure you want to delete this user?"
+ deleteUserConfirm: '你确定要删除该用户?',
+
+ // Original text: "Delete User"
+ deleteUser: '删除用户',
+
+ // Original text: "unknown user"
+ unknownUser: '未知用户',
+
+ // Original text: "No group found"
+ noGroupFound: '没有找到组',
+
+ // Original text: "Name"
+ groupNameColumn: '名称',
+
+ // Original text: "Users"
+ groupUsersColumn: '用户',
+
+ // Original text: "Add User"
+ addUserToGroupColumn: '增加用户',
+
+ // Original text: "Email"
+ userNameColumn: '邮件',
+
+ // Original text: "Permissions"
+ userPermissionColumn: '权限',
+
+ // Original text: "Password"
+ userPasswordColumn: '密码',
+
+ // Original text: "Email"
+ userName: '邮件',
+
+ // Original text: "Password"
+ userPassword: '密码',
+
+ // Original text: "Create"
+ createUserButton: '创建',
+
+ // Original text: "No user found"
+ noUserFound: '没有找到用户',
+
+ // Original text: "User"
+ userLabel: '用户',
+
+ // Original text: "Admin"
+ adminLabel: '管理',
+
+ // Original text: "No user in group"
+ noUserInGroup: '组中没有用户',
+
+ // Original text: "{users} user{users, plural, one {} other {s}}"
+ countUsers: '{users} 用户{users, plural, one {} 其他 {s}}',
+
+ // Original text: "Select Permission"
+ selectPermission: '选择权限',
+
+ // Original text: "Auto-load at server start"
+ autoloadPlugin: '服务器启动时自动加载',
+
+ // Original text: "Save configuration"
+ savePluginConfiguration: '保存配置',
+
+ // Original text: "Delete configuration"
+ deletePluginConfiguration: '删除配置',
+
+ // Original text: "Plugin error"
+ pluginError: '插件错误',
+
+ // Original text: "Unknown error"
+ unknownPluginError: '未知错误',
+
+ // Original text: "Purge plugin configuration"
+ purgePluginConfiguration: '清除插件配置',
+
+ // Original text: "Are you sure you want to purge this configuration ?"
+ purgePluginConfigurationQuestion: '你确定要清除此配置?',
+
+ // Original text: "Edit"
+ editPluginConfiguration: '编辑',
+
+ // Original text: "Cancel"
+ cancelPluginEdition: '取消',
+
+ // Original text: "Plugin configuration"
+ pluginConfigurationSuccess: '插件配置',
+
+ // Original text: "Plugin configuration successfully saved!"
+ pluginConfigurationChanges: '插件配置保存成功',
+
+ // Original text: "Start"
+ startVmLabel: '启动',
+
+ // Original text: "Recovery start"
+ recoveryModeLabel: '恢复启动',
+
+ // Original text: "Suspend"
+ suspendVmLabel: '暂停',
+
+ // Original text: "Stop"
+ stopVmLabel: '关机',
+
+ // Original text: "Force shutdown"
+ forceShutdownVmLabel: '强制关机',
+
+ // Original text: "Reboot"
+ rebootVmLabel: '重启',
+
+ // Original text: "Force reboot"
+ forceRebootVmLabel: '强制重启',
+
+ // Original text: "Delete"
+ deleteVmLabel: '删除',
+
+ // Original text: "Migrate"
+ migrateVmLabel: '迁移',
+
+ // Original text: "Snapshot"
+ snapshotVmLabel: '快照',
+
+ // Original text: "Export"
+ exportVmLabel: '导出',
+
+ // Original text: "Resume"
+ resumeVmLabel: '恢复',
+
+ // Original text: "Copy"
+ copyVmLabel: '复制',
+
+ // Original text: "Clone"
+ cloneVmLabel: '克隆',
+
+ // Original text: "Fast clone"
+ fastCloneVmLabel: '快速克隆',
+
+ // Original text: "Convert to template"
+ convertVmToTemplateLabel: '转换成模板',
+
+ // Original text: "Console"
+ vmConsoleLabel: '控制台',
+
+ // Original text: "Rescan all disks"
+ srRescan: '重新扫描所有磁盘',
+
+ // Original text: "Connect to all hosts"
+ srReconnectAll: '连接所有主机',
+
+ // Original text: "Disconnect to all hosts"
+ srDisconnectAll: '断开所有主机',
+
+ // Original text: "Forget this SR"
+ srForget: '移除此数据存储',
+
+ // Original text: "Remove this SR"
+ srRemoveButton: '删除此数据存储',
+
+ // Original text: "No VDIs in this storage"
+ srNoVdis: '此存储中没有VDI',
+
+ // Original text: "Hosts"
+ hostsTabName: '主机',
+
+ // Original text: "High Availability"
+ poolHaStatus: '高可用',
+
+ // Original text: "Enabled"
+ poolHaEnabled: '启用',
+
+ // Original text: "Disabled"
+ poolHaDisabled: '禁用',
+
+ // Original text: "Name"
+ hostNameLabel: '名称',
+
+ // Original text: "Description"
+ hostDescription: '描述',
+
+ // Original text: "Memory"
+ hostMemory: '内存',
+
+ // Original text: "No hosts"
+ noHost: '没有主机',
+
+ // Original text: "Name"
+ poolNetworkNameLabel: '名称',
+
+ // Original text: "Description"
+ poolNetworkDescription: '描述',
+
+ // Original text: "PIFs"
+ poolNetworkPif: 'PIFs',
+
+ // Original text: "No networks"
+ poolNoNetwork: '没有网络',
+
+ // Original text: "MTU"
+ poolNetworkMTU: 'MTU',
+
+ // Original text: "Connected"
+ poolNetworkPifAttached: '已连接',
+
+ // Original text: "Disconnected"
+ poolNetworkPifDetached: '未连接',
+
+ // Original text: "Add SR"
+ addSrLabel: '添加数据存储',
+
+ // Original text: "Add VM"
+ addVmLabel: '添加虚拟机',
+
+ // Original text: "Add Host"
+ addHostLabel: '添加主机',
+
+ // Original text: "Disconnect"
+ disconnectServer: '断开',
+
+ // Original text: "Start"
+ startHostLabel: '启动',
+
+ // Original text: "Stop"
+ stopHostLabel: '关机',
+
+ // Original text: "Enable"
+ enableHostLabel: '启用',
+
+ // Original text: "Disable"
+ disableHostLabel: '禁用',
+
+ // Original text: "Restart toolstack"
+ restartHostAgent: '重启toolstack',
+
+ // Original text: "Force reboot"
+ forceRebootHostLabel: '强制重启',
+
+ // Original text: "Reboot"
+ rebootHostLabel: '重启',
+
+ // Original text: "Emergency mode"
+ emergencyModeLabel: '紧急模式',
+
+ // Original text: "Storage"
+ storageTabName: '存储',
+
+ // Original text: "Patches"
+ patchesTabName: '补丁',
+
+ // Original text: "Load average"
+ statLoad: '负载平衡',
+
+ // Original text: "Hardware"
+ hardwareHostSettingsLabel: '硬件',
+
+ // Original text: "Address"
+ hostAddress: '地址',
+
+ // Original text: "Status"
+ hostStatus: '状态',
+
+ // Original text: "Build number"
+ hostBuildNumber: '版本号',
+
+ // Original text: "iSCSI name"
+ hostIscsiName: 'iSCSI名称',
+
+ // Original text: "Version"
+ hostXenServerVersion: '版本',
+
+ // Original text: "Enabled"
+ hostStatusEnabled: '启用',
+
+ // Original text: "Disabled"
+ hostStatusDisabled: '禁用',
+
+ // Original text: "Power on mode"
+ hostPowerOnMode: '开机模式',
+
+ // Original text: "Host uptime"
+ hostStartedSince: '系统启动时间',
+
+ // Original text: "Toolstack uptime"
+ hostStackStartedSince: 'Toolstack启动时间',
+
+ // Original text: "CPU model"
+ hostCpusModel: 'CPU型号',
+
+ // Original text: "Core (socket)"
+ hostCpusNumber: '核 (socket)',
+
+ // Original text: "Manufacturer info"
+ hostManufacturerinfo: '制造商信息',
+
+ // Original text: "BIOS info"
+ hostBiosinfo: 'BIOS 信息',
+
+ // Original text: "Licence"
+ licenseHostSettingsLabel: '授权',
+
+ // Original text: "Type"
+ hostLicenseType: '类型',
+
+ // Original text: "Socket"
+ hostLicenseSocket: '插槽',
+
+ // Original text: "Expiry"
+ hostLicenseExpiry: '过期',
+
+ // Original text: "Add a network"
+ networkCreateButton: '新建一个网络',
+
+ // Original text: "Device"
+ pifDeviceLabel: '设备',
+
+ // Original text: "Network"
+ pifNetworkLabel: '网络',
+
+ // Original text: "VLAN"
+ pifVlanLabel: 'VLAN',
+
+ // Original text: "Address"
+ pifAddressLabel: '地址',
+
+ // Original text: "MAC"
+ pifMacLabel: 'MAC',
+
+ // Original text: "MTU"
+ pifMtuLabel: 'MTU',
+
+ // Original text: "Status"
+ pifStatusLabel: '状态',
+
+ // Original text: "Connected"
+ pifStatusConnected: '已连接',
+
+ // Original text: "Disconnected"
+ pifStatusDisconnected: '未连接',
+
+ // Original text: "No physical interface detected"
+ pifNoInterface: '没有检测到物理接口',
+
+ // Original text: "Add a storage"
+ addSrDeviceButton: '新建存储',
+
+ // Original text: "Name"
+ srNameLabel: '名称',
+
+ // Original text: "Type"
+ srType: '类型',
+
+ // Original text: "Status"
+ pdbStatus: '状态',
+
+ // Original text: "Connected"
+ pbdStatusConnected: '已连接',
+
+ // Original text: "Disconnected"
+ pbdStatusDisconnected: '未连接',
+
+ // Original text: "Shared"
+ srShared: '已共享',
+
+ // Original text: "Not shared"
+ srNotShared: '未共享',
+
+ // Original text: "No storage detected"
+ pbdNoSr: '未检测到存储',
+
+ // Original text: "Name"
+ patchNameLabel: '名称',
+
+ // Original text: "Install all patches"
+ patchUpdateButton: '安装所有补丁',
+
+ // Original text: "Description"
+ patchDescription: '描述',
+
+ // Original text: "Applied date"
+ patchApplied: '应用日期',
+
+ // Original text: "Size"
+ patchSize: '大小',
+
+ // Original text: "Status"
+ patchStatus: '状态',
+
+ // Original text: "Applied"
+ patchStatusApplied: '已应用',
+
+ // Original text: "Missing patches"
+ patchStatusNotApplied: '缺少补丁',
+
+ // Original text: "No patch detected"
+ patchNothing: '未检测到补丁',
+
+ // Original text: "Release date"
+ patchReleaseDate: '发布日期',
+
+ // Original text: "Guidance"
+ patchGuidance: '导航',
+
+ // Original text: "Action"
+ patchAction: '操作',
+
+ // Original text: "Applied patches"
+ hostAppliedPatches: '已应用补丁',
+
+ // Original text: "Missing patches"
+ hostMissingPatches: '缺少补丁',
+
+ // Original text: "Host up-to-date!"
+ hostUpToDate: '主机补丁为最新',
+
+ // Original text: "Refresh patches"
+ refreshPatches: '刷新补丁包',
+
+ // Original text: "Install pool patches"
+ installPoolPatches: '安装池补丁',
+
+ // Original text: "General"
+ generalTabName: '常规',
+
+ // Original text: "Stats"
+ statsTabName: '状态',
+
+ // Original text: "Console"
+ consoleTabName: '控制台',
+
+ // Original text: "Snapshots"
+ snapshotsTabName: '快照',
+
+ // Original text: "Logs"
+ logsTabName: '日志',
+
+ // Original text: "Advanced"
+ advancedTabName: '高级',
+
+ // Original text: "Network"
+ networkTabName: '网络',
+
+ // Original text: "Disk{disks, plural, one {} other {s}}"
+ disksTabName: '磁盘{disks, plural, one {} 其他 {s}}',
+
+ // Original text: "halted"
+ powerStateHalted: '已停止',
+
+ // Original text: "running"
+ powerStateRunning: '正在运行',
+
+ // Original text: "suspended"
+ powerStateSuspended: '已暂停',
+
+ // Original text: "No Xen tools detected"
+ vmStatus: '没有检测到Xen Tools',
+
+ // Original text: "No IPv4 record"
+ vmName: '没有IPv4记录',
+
+ // Original text: "No IP record"
+ vmDescription: '没有IP记录',
+
+ // Original text: "Started {ago}"
+ vmSettings: '已启动 {ago}',
+
+ // Original text: "Current status:"
+ vmCurrentStatus: '当前状态',
+
+ // Original text: "Not running"
+ vmNotRunning: '没有运行',
+
+ // Original text: "No Xen tools detected"
+ noToolsDetected: '没有检测到Xen Tools',
+
+ // Original text: "No IPv4 record"
+ noIpv4Record: '没有IPv4记录',
+
+ // Original text: "No IP record"
+ noIpRecord: '没有IP记录',
+
+ // Original text: "Started {ago}"
+ started: '已启动 {ago}',
+
+ // Original text: "Paravirtualization (PV)"
+ paraVirtualizedMode: '半虚拟化 (PV)',
+
+ // Original text: "Hardware virtualization (HVM)"
+ hardwareVirtualizedMode: '硬件虚拟化 (HVM)',
+
+ // Original text: "CPU usage"
+ statsCpu: 'CPU利用率',
+
+ // Original text: "Memory usage"
+ statsMemory: '内存利用率',
+
+ // Original text: "Network throughput"
+ statsNetwork: '网络流量',
+
+ // Original text: "Stacked values"
+ useStackedValuesOnStats: 'Stacked 值',
+
+ // Original text: "Disk throughput"
+ statDisk: '磁盘吞吐',
+
+ // Original text: "Last 10 minutes"
+ statLastTenMinutes: '最近10分钟',
+
+ // Original text: "Last 2 hours"
+ statLastTwoHours: '最近两小时',
+
+ // Original text: "Last week"
+ statLastWeek: '最近一周',
+
+ // Original text: "Last year"
+ statLastYear: '最近一年',
+
+ // Original text: "Copy"
+ copyToClipboardLabel: '复制',
+
+ // Original text: "Ctrl+Alt+Del"
+ ctrlAltDelButtonLabel: '发送Ctrl+Alt+Del',
+
+ // Original text: "Tip:"
+ tipLabel: '提示',
+
+ // Original text: "non-US keyboard could have issues with console: switch your own layout to US."
+ tipConsoleLabel: '非美式键盘操作控制台可能出现问题:请切换至美式键盘模式',
+
+ // Original text: "Action"
+ vdiAction: '操作',
+
+ // Original text: "Attach disk"
+ vdiAttachDeviceButton: '附加磁盘',
+
+ // Original text: "New disk"
+ vbdCreateDeviceButton: '新建磁盘',
+
+ // Original text: "Boot order"
+ vdiBootOrder: '启动顺序',
+
+ // Original text: "Name"
+ vdiNameLabel: '名称',
+
+ // Original text: "Description"
+ vdiNameDescription: '描述',
+
+ // Original text: "Tags"
+ vdiTags: '标签',
+
+ // Original text: "Size"
+ vdiSize: '磁盘大小',
+
+ // Original text: "SR"
+ vdiSr: '数据存储',
+
+ // Original text: "VM"
+ vdiVm: '虚拟机',
+
+ // Original text: "Boot flag"
+ vbdBootableStatus: '启动标识',
+
+ // Original text: "Status"
+ vbdStatus: '状态',
+
+ // Original text: "Connected"
+ vbdStatusConnected: '已连接',
+
+ // Original text: "Disconnected"
+ vbdStatusDisconnected: '未连接',
+
+ // Original text: "No disks"
+ vbdNoVbd: '没有磁盘',
+
+ // Original text: "New device"
+ vifCreateDeviceButton: '新建设备',
+
+ // Original text: "No interface"
+ vifNoInterface: '没有网卡',
+
+ // Original text: "Device"
+ vifDeviceLabel: '设备',
+
+ // Original text: "MAC address"
+ vifMacLabel: 'MAC地址',
+
+ // Original text: "MTU"
+ vifMtuLabel: 'MTU',
+
+ // Original text: "Network"
+ vifNetworkLabel: '网络',
+
+ // Original text: "Status"
+ vifStatusLabel: '状态',
+
+ // Original text: "Connected"
+ vifStatusConnected: '已连接',
+
+ // Original text: "Disconnected"
+ vifStatusDisconnected: '未连接',
+
+ // Original text: "IP addresses"
+ vifIpAddresses: 'IP地址',
+
+ // Original text: "Auto-generated if empty"
+ vifMacAutoGenerate: '如果没有自动创建',
+
+ // Original text: "No snapshots"
+ noSnapshots: '没有快照',
+
+ // Original text: "New snapshot"
+ snapshotCreateButton: '新建快照',
+
+ // Original text: "Just click on the snapshot button to create one!"
+ tipCreateSnapshotLabel: '点击快照按钮来创建快照',
+
+ // Original text: "Creation date"
+ snapshotDate: '创建日期',
+
+ // Original text: "Name"
+ snapshotName: '名称',
+
+ // Original text: "Action"
+ snapshotAction: '操作',
+
+ // Original text: "Remove all logs"
+ logRemoveAll: '删除所有日志',
+
+ // Original text: "No logs so far"
+ noLogs: '目前没有日志',
+
+ // Original text: "Creation date"
+ logDate: '创建日期',
+
+ // Original text: "Name"
+ logName: '名称',
+
+ // Original text: "Content"
+ logContent: '目录',
+
+ // Original text: "Action"
+ logAction: '操作',
+
+ // Original text: "Remove"
+ vmRemoveButton: '删除',
+
+ // Original text: "Convert"
+ vmConvertButton: '转换',
+
+ // Original text: "Xen settings"
+ xenSettingsLabel: 'Xen 设置',
+
+ // Original text: "Guest OS"
+ guestOsLabel: '客户操作系统',
+
+ // Original text: "Misc"
+ miscLabel: 'Misc',
+
+ // Original text: "UUID"
+ uuid: 'UUID',
+
+ // Original text: "Virtualization mode"
+ virtualizationMode: '虚拟化模式',
+
+ // Original text: "CPU weight"
+ cpuWeightLabel: 'CPU权重',
+
+ // Original text: "Default"
+ defaultCpuWeight: '默认',
+
+ // Original text: "PV args"
+ pvArgsLabel: 'PV参数',
+
+ // Original text: "Xen tools status"
+ xenToolsStatus: 'Xen tools状态',
+
+ // Original text: "{status}"
+ xenToolsStatusValue: '{status}',
+
+ // Original text: "OS name"
+ osName: '操作系统名称',
+
+ // Original text: "OS kernel"
+ osKernel: '操作系统内核',
+
+ // Original text: "Auto power on"
+ autoPowerOn: '自动卡机',
+
+ // Original text: "HA"
+ ha: '高可用',
+
+ // Original text: "Original template"
+ originalTemplate: '来源模板',
+
+ // Original text: "Unknown"
+ unknownOsName: '未知',
+
+ // Original text: "Unknown"
+ unknownOsKernel: '未知',
+
+ // Original text: "Unknown"
+ unknownOriginalTemplate: '未知',
+
+ // Original text: "VM limits"
+ vmLimitsLabel: '虚拟机限制',
+
+ // Original text: "CPU limits"
+ vmCpuLimitsLabel: 'CPU限制',
+
+ // Original text: "Memory limits (min/max)"
+ vmMemoryLimitsLabel: '内存限制(min/max)',
+
+ // Original text: "vCPUs max:"
+ vmMaxVcpus: '最大虚拟CPU数',
+
+ // Original text: "Memory max:"
+ vmMaxRam: '最大内存',
+
+ // Original text: "Long click to add a name"
+ vmHomeNamePlaceholder: '长按来添加名称',
+
+ // Original text: "Long click to add a description"
+ vmHomeDescriptionPlaceholder: '长按来添加描述',
+
+ // Original text: "Click to add a name"
+ vmViewNamePlaceholder: '点击添加名称',
+
+ // Original text: "Click to add a description"
+ vmViewDescriptionPlaceholder: '点击添加描述',
+
+ // Original text: "Pool{pools, plural, one {} other {s}}"
+ poolPanel: '池{pools, plural, one {} 其他 {s}}',
+
+ // Original text: "Host{hosts, plural, one {} other {s}}"
+ hostPanel: '主机{hosts, plural, one {} 其他 {s}}',
+
+ // Original text: "VM{vms, plural, one {} other {s}}"
+ vmPanel: '虚拟机{vms, plural, one {} 其他 {s}}',
+
+ // Original text: "RAM Usage"
+ memoryStatePanel: '内容使用率',
+
+ // Original text: "CPUs Usage"
+ cpuStatePanel: 'CPU使用率',
+
+ // Original text: "VMs Power state"
+ vmStatePanel: '虚拟机电源状态',
+
+ // Original text: "Pending tasks"
+ taskStatePanel: '正在运行的任务',
+
+ // Original text: "Users"
+ usersStatePanel: '用户',
+
+ // Original text: "Storage state"
+ srStatePanel: '存储状态',
+
+ // Original text: "{usage} (of {total})"
+ ofUsage: '{usage} (of {total})',
+
+ // Original text: "No storage"
+ noSrs: '没有存储',
+
+ // Original text: "Name"
+ srName: '名称',
+
+ // Original text: "Pool"
+ srPool: '资源池',
+
+ // Original text: "Host"
+ srHost: '主机',
+
+ // Original text: "Type"
+ srFormat: '类型',
+
+ // Original text: "Size"
+ srSize: '大小',
+
+ // Original text: "Usage"
+ srUsage: '利用率',
+
+ // Original text: "used"
+ srUsed: '已使用',
+
+ // Original text: "free"
+ srFree: '剩余空间',
+
+ // Original text: "Storage Usage"
+ srUsageStatePanel: '存储利用率',
+
+ // Original text: "Top 5 SR Usage (in %)"
+ srTopUsageStatePanel: '数据存储使用率前5名(in %)',
+
+ // Original text: "{running} running ({halted} halted)"
+ vmsStates: '{running} 正在运行 ({halted} 已停止)',
+
+ // Original text: "{value} {date, date, medium}"
+ weekHeatmapData: '{value} {date, date, medium}',
+
+ // Original text: "No data."
+ weekHeatmapNoData: '没有数据',
+
+ // Original text: "Weekly Heatmap"
+ weeklyHeatmap: '每周热图',
+
+ // Original text: "Weekly Charts"
+ weeklyCharts: '每周图表',
+
+ // Original text: "Synchronize scale:"
+ weeklyChartsScaleInfo: '同步范围',
+
+ // Original text: "Stats error"
+ statsDashboardGenericErrorTitle: '状态错误',
+
+ // Original text: "There is no stats available for:"
+ statsDashboardGenericErrorMessage: '没有可用的状态:',
+
+ // Original text: "No selected metric"
+ noSelectedMetric: '没有选择度量标准',
+
+ // Original text: "Select"
+ statsDashboardSelectObjects: '选择',
+
+ // Original text: "Loading…"
+ metricsLoading: '加载中….',
+
+ // Original text: "Coming soon!"
+ comingSoon: '即将呈现',
+
+ // Original text: "Orphaned VDIs"
+ orphanedVdis: '孤立的VDI',
+
+ // Original text: "Orphaned VMs"
+ orphanedVms: '孤立的虚拟机',
+
+ // Original text: "No orphans"
+ noOrphanedObject: '没有孤立的内容',
+
+ // Original text: "Remove all orphaned VDIs"
+ removeAllOrphanedObject: '删除所有孤立的VDI',
+
+ // Original text: "Name"
+ vmNameLabel: '名称',
+
+ // Original text: "Description"
+ vmNameDescription: '描述',
+
+ // Original text: "Resident on"
+ vmContainer: '位于',
+
+ // Original text: "Alarms"
+ alarmMessage: '警告',
+
+ // Original text: "No alarms"
+ noAlarms: '没有警告',
+
+ // Original text: "Date"
+ alarmDate: '日期',
+
+ // Original text: "Content"
+ alarmContent: '内容',
+
+ // Original text: "Issue on"
+ alarmObject: '问题',
+
+ // Original text: "Pool"
+ alarmPool: '资源池',
+
+ // Original text: "Remove all alarms"
+ alarmRemoveAll: '删除所有警告',
+
+ // Original text: "Create a new VM on {select}"
+ newVmCreateNewVmOn: '创建一个新的位于{select}的虚拟机',
+
+ // Original text: "Create a new VM on {select1} or {select2}"
+ newVmCreateNewVmOn2: '创建一个新的位于{select1} 或 {select2}的虚拟机',
+
+ // Original text: "You have no permission to create a VM"
+ newVmCreateNewVmNoPermission: '你没有权限创建虚拟机',
+
+ // Original text: "Infos"
+ newVmInfoPanel: '信息',
+
+ // Original text: "Name"
+ newVmNameLabel: '名称',
+
+ // Original text: "Template"
+ newVmTemplateLabel: '模板',
+
+ // Original text: "Description"
+ newVmDescriptionLabel: '描述',
+
+ // Original text: "Performances"
+ newVmPerfPanel: '性能',
+
+ // Original text: "vCPUs"
+ newVmVcpusLabel: '虚拟CPU',
+
+ // Original text: "RAM"
+ newVmRamLabel: '内存',
+
+ // Original text: "Install settings"
+ newVmInstallSettingsPanel: '安装设置',
+
+ // Original text: "ISO/DVD"
+ newVmIsoDvdLabel: 'ISO/DVD',
+
+ // Original text: "Network"
+ newVmNetworkLabel: '网络',
+
+ // Original text: "PV Args"
+ newVmPvArgsLabel: 'PV参数',
+
+ // Original text: "PXE"
+ newVmPxeLabel: 'PXE',
+
+ // Original text: "Interfaces"
+ newVmInterfacesPanel: '网络接口',
+
+ // Original text: "MAC"
+ newVmMacLabel: 'MAC',
+
+ // Original text: "Add interface"
+ newVmAddInterface: '添加网络接口',
+
+ // Original text: "Disks"
+ newVmDisksPanel: '磁盘',
+
+ // Original text: "SR"
+ newVmSrLabel: '数据存储',
+
+ // Original text: "Bootable"
+ newVmBootableLabel: '启动项',
+
+ // Original text: "Size"
+ newVmSizeLabel: '大小',
+
+ // Original text: "Add disk"
+ newVmAddDisk: '添加磁盘',
+
+ // Original text: "Summary"
+ newVmSummaryPanel: '概述',
+
+ // Original text: "Create"
+ newVmCreate: '创建',
+
+ // Original text: "Reset"
+ newVmReset: '重置',
+
+ // Original text: "Select template"
+ newVmSelectTemplate: '选择模板',
+
+ // Original text: "SSH key"
+ newVmSshKey: 'SSH Key',
+
+ // Original text: "Config drive"
+ newVmConfigDrive: '配置驱动器',
+
+ // Original text: "Custom config"
+ newVmCustomConfig: '自定义配置',
+
+ // Original text: "Boot VM after creation"
+ newVmBootAfterCreate: '创建后启动',
+
+ // Original text: "Auto-generated if empty"
+ newVmMacPlaceholder: '如果为空自动创建',
+
+ // Original text: "CPU weight"
+ newVmCpuWeightLabel: 'CPU权重',
+
+ // Original text: "Quarter (1/4)"
+ newVmCpuWeightQuarter: '四分之一 (1/4)',
+
+ // Original text: "Half (1/2)"
+ newVmCpuWeightHalf: '二分之一 (1/2)',
+
+ // Original text: "Normal"
+ newVmCpuWeightNormal: '普通',
+
+ // Original text: "Double (x2)"
+ newVmCpuWeightDouble: '双倍(x2)',
+
+ // Original text: "Cloud config"
+ newVmCloudConfig: '云配置',
+
+ // Original text: "Create VMs"
+ newVmCreateVms: '创建虚拟机',
+
+ // Original text: "Are you sure you want to create {nbVms} VMs?"
+ newVmCreateVmsConfirm: '你确定要创建 {nbVms} 虚拟机?',
+
+ // Original text: "Multiple VMs:"
+ newVmMultipleVms: '多个虚拟机',
+
+ // Original text: "Select a resource set:"
+ newVmSelectResourceSet: '选择资源集',
+
+ // Original text: "Name pattern:"
+ newVmMultipleVmsPattern: '命名模式',
+
+ // Original text: "e.g.: \\{name\\}_%"
+ newVmMultipleVmsPatternPlaceholder: '例如: \\{name\\}_%',
+
+ // Original text: "First index:"
+ newVmFirstIndex: '首要标识',
+
+ // Original text: "Resource sets"
+ resourceSets: '资源集',
+
+ // Original text: "No resource sets."
+ noResourceSets: '没有资源集',
+
+ // Original text: "Resource set name"
+ resourceSetName: '资源集名称',
+
+ // Original text: "Creation and edition"
+ resourceSetCreation: '创建并编辑',
+
+ // Original text: "Save"
+ saveResourceSet: '保存',
+
+ // Original text: "Reset"
+ resetResourceSet: '重置',
+
+ // Original text: "Edit"
+ editResourceSet: '编辑',
+
+ // Original text: "Delete"
+ deleteResourceSet: '删除',
+
+ // Original text: "Delete resource set"
+ deleteResourceSetWarning: '删除资源集',
+
+ // Original text: "Are you sure you want to delete this resource set?"
+ deleteResourceSetQuestion: '你确定要删除此资源集',
+
+ // Original text: "Missing objects:"
+ resourceSetMissingObjects: '缺少对象',
+
+ // Original text: "vCPUs"
+ resourceSetVcpus: '虚拟CPU',
+
+ // Original text: "Memory"
+ resourceSetMemory: '内存',
+
+ // Original text: "Storage"
+ resourceSetStorage: '存储',
+
+ // Original text: "Unknown"
+ unknownResourceSetValue: '未知',
+
+ // Original text: "Available hosts"
+ availableHosts: '可用主机',
+
+ // Original text: "Excluded hosts"
+ excludedHosts: '被排除的主机',
+
+ // Original text: "No hosts available."
+ noHostsAvailable: '没有可用主机',
+
+ // Original text: "VMs created from this resource set shall run on the following hosts."
+ availableHostsDescription: '从这些资源中创建的虚拟机将运行在以下主机上',
+
+ // Original text: "Maximum CPUs"
+ maxCpus: '最大CPU',
+
+ // Original text: "Maximum RAM (GiB)"
+ maxRam: '最大内存',
+
+ // Original text: "Maximum disk space"
+ maxDiskSpace: '最大磁盘空间',
+
+ // Original text: "No limits."
+ noResourceSetLimits: '没有限制',
+
+ // Original text: "Total:"
+ totalResource: '合计',
+
+ // Original text: "Remaining:"
+ remainingResource: '剩余',
+
+ // Original text: "Used:"
+ usedResource: '已使用',
+
+ // Original text: "Try dropping some backups here, or click to select backups to upload. Accept only .xva files."
+ importVmsList:
+ '尝试将备份文件拖拽到这里,或点击选择备份文件上传,仅支持.xva格式的文件',
+
+ // Original text: "No selected VMs."
+ noSelectedVms: '没有选择虚拟机',
+
+ // Original text: "To Pool:"
+ vmImportToPool: '到资源池',
+
+ // Original text: "To SR:"
+ vmImportToSr: '到存储库',
+
+ // Original text: "VMs to import"
+ vmsToImport: '导入虚拟机',
+
+ // Original text: "Reset"
+ importVmsCleanList: '重置',
+
+ // Original text: "VM import success"
+ vmImportSuccess: '虚拟机导入成功',
+
+ // Original text: "VM import failed"
+ vmImportFailed: '虚拟机导入失败',
+
+ // Original text: "Import starting…"
+ startVmImport: '开始导入',
+
+ // Original text: "Export starting…"
+ startVmExport: '开始导出',
+
+ // Original text: "No pending tasks"
+ noTasks: '没有等待中的任务',
+
+ // Original text: "Currently, there are not any pending XenServer tasks"
+ xsTasks: '当前,没有任何等待中的XenServer任务',
+
+ // Original text: "List Remote"
+ listRemote: '列出远程',
+
+ // Original text: "simple"
+ simpleBackup: '简单',
+
+ // Original text: "delta"
+ delta: '增量',
+
+ // Original text: "Restore Backups"
+ restoreBackups: '恢复备份',
+
+ // Original text: "No remotes"
+ noRemotes: '没有远程',
+
+ // Original text: "enabled"
+ remoteEnabled: '启用',
+
+ // Original text: "error"
+ remoteError: '错误',
+
+ // Original text: "No backup available"
+ noBackup: '没有可用的备份',
+
+ // Original text: "VM Name"
+ backupVmNameColumn: '虚拟机名称',
+
+ // Original text: "Backup Tag"
+ backupTagColumn: '备份标识',
+
+ // Original text: "Last Backup"
+ lastBackupColumn: '最后备份',
+
+ // Original text: "Available Backups"
+ availableBackupsColumn: '可用的备份',
+
+ // Original text: "Restore"
+ restoreColumn: '恢复',
+
+ // Original text: "Restore VM"
+ restoreTip: '恢复虚拟机',
+
+ // Original text: "Import VM"
+ importBackupTitle: '导入虚拟机',
+
+ // Original text: "Starting your backup import"
+ importBackupMessage: '开始你的备份导入',
+
+ // Original text: "Emergency shutdown Host{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}}?',
+
+ // Original text: "Shutdown host"
+ stopHostModalTitle: '关闭主机',
+
+ // Original text: "This will shutdown your host. Do you want to continue?"
+ stopHostModalMessage: '此操作将关闭你的主机,你确定要继续吗?',
+
+ // Original text: "Restart host"
+ restartHostModalTitle: '重启主机',
+
+ // Original text: "This will restart your host. Do you want to continue?"
+ 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}}',
+
+ // 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}}?',
+
+ // 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}}?',
+
+ // 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}}?',
+
+ // 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}}?',
+
+ // 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}}?',
+
+ // Original text: "Restart VM"
+ restartVmModalTitle: '重新启动虚拟机',
+
+ // Original text: "Are you sure you want to restart {name}?"
+ restartVmModalMessage: '你确定要重新启动{name}?',
+
+ // Original text: "Stop VM"
+ stopVmModalTitle: '停止虚拟机',
+
+ // Original text: "Are you sure you want to stop {name}?"
+ stopVmModalMessage: '你确定要停止 {name}?',
+
+ // Original text: "Restart VM{vms, plural, one {} other {s}}"
+ 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}}?',
+
+ // 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}}?',
+
+ // Original text: "Delete VM"
+ deleteVmModalTitle: '删除虚拟机',
+
+ // Original text: "Delete VM{vms, plural, one {} other {s}}"
+ deleteVmsModalTitle: '删除虚拟机{vms, plural, one {} other {s}}',
+
+ // Original text: "Are you sure you want to delete this VM? ALL VM DISKS WILL BE REMOVED"
+ 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}}?所有的虚拟机磁盘将被删除',
+
+ // Original text: "Migrate VM"
+ migrateVmModalTitle: '迁移虚拟机',
+
+ // Original text: "Select a destination host:"
+ migrateVmSelectHost: '选择一个目标主机',
+
+ // Original text: "Select a migration network:"
+ migrateVmSelectMigrationNetwork: '选择一个迁移网络',
+
+ // Original text: "For each VDI, select an SR:"
+ migrateVmSelectSrs: '为每个虚拟磁盘,选择存储库',
+
+ // Original text: "For each VIF, select a network:"
+ migrateVmSelectNetworks: '为每个虚拟网卡,选择一个网络',
+
+ // Original text: "Select a destination SR:"
+ migrateVmsSelectSr: '选择一个目标存储库',
+
+ // Original text: "Select a destination SR for local disks:"
+ migrateVmsSelectSrIntraPool: '为本地磁盘选择一个目标存储库',
+
+ // Original text: "Select a network on which to connect each VIF:"
+ migrateVmsSelectNetwork: '选择一个网络来连接每个虚拟网卡',
+
+ // Original text: "Smart mapping"
+ migrateVmsSmartMapping: '智能映射',
+
+ // Original text: "Name"
+ migrateVmName: '名称',
+
+ // Original text: "SR"
+ migrateVmSr: '存储库',
+
+ // Original text: "VIF"
+ migrateVmVif: '虚拟网卡',
+
+ // Original text: "Network"
+ migrateVmNetwork: '网络',
+
+ // Original text: "No target host"
+ migrateVmNoTargetHost: '没有目标主机',
+
+ // Original text: "A target host is required to migrate a VM"
+ migrateVmNoTargetHostMessage: '需要一个目标主机来迁移一个虚拟机',
+
+ // Original text: "Import a {name} Backup"
+ importBackupModalTitle: '导入一个{name}备份',
+
+ // Original text: "Start VM after restore"
+ importBackupModalStart: '恢复后启动虚拟机',
+
+ // Original text: "Select your backup…"
+ importBackupModalSelectBackup: '选择你的备份…',
+
+ // Original text: "Are you sure you want to remove all orphaned VDIs?"
+ removeAllOrphanedModalWarning: '你确定要删除所有孤立的虚拟磁盘?',
+
+ // Original text: "Remove all logs"
+ removeAllLogsModalTitle: '删除所有日志',
+
+ // Original text: "Are you sure you want to remove all logs?"
+ removeAllLogsModalWarning: '你确定要删除所有日志?',
+
+ // Original text: "This operation is definitive."
+ definitiveMessageModal: '这个操作是不可更改的',
+
+ // Original text: "Previous SR Usage"
+ 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主机用来连接存储。如果你选择继续创建存储库,所有的数据将丢失。',
+
+ // 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主机使用。如果你选择继续创建存储库,所有的数据将丢失。',
+
+ // 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},你确定要删除并替换这个注册信息?',
+
+ // 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连接才能正常使用,如果您正式付费将不受此限制',
+
+ // Original text: "Host"
+ serverHost: '主机',
+
+ // Original text: "Username"
+ serverUsername: '用户名',
+
+ // Original text: "Password"
+ serverPassword: '密码',
+
+ // Original text: "Action"
+ serverAction: '操作',
+
+ // Original text: "Read Only"
+ serverReadOnly: '只读',
+
+ // Original text: "Copy VM"
+ copyVm: '复制虚拟机',
+
+ // Original text: "Are you sure you want to copy this VM to {SR}?"
+ copyVmConfirm: '你确定要复制此虚拟机到{SR}',
+
+ // Original text: "Name"
+ copyVmName: '名称',
+
+ // Original text: "Name pattern"
+ copyVmNamePattern: '命名规范',
+
+ // Original text: "If empty: name of the copied VM"
+ copyVmNamePlaceholder: '如果复制虚拟机名称为空',
+
+ // Original text: "e.g.: \"\\{name\\}_COPY\""
+ copyVmNamePatternPlaceholder: 'e.g.: "\\{name\\}_COPY"',
+
+ // Original text: "Select SR"
+ copyVmSelectSr: '选择存储库',
+
+ // Original text: "Use compression"
+ copyVmCompress: '使用压缩',
+
+ // Original text: "No target SR"
+ copyVmsNoTargetSr: '没有目标存储库',
+
+ // Original text: "A target SR is required to copy a VM"
+ copyVmsNoTargetSrMessage: '复制虚拟机需要选择一个目标存储库',
+
+ // Original text: "Create network"
+ newNetworkCreate: '创建网络',
+
+ // Original text: "Interface"
+ newNetworkInterface: '接口',
+
+ // Original text: "Name"
+ newNetworkName: '名称',
+
+ // Original text: "Description"
+ newNetworkDescription: '描述',
+
+ // Original text: "VLAN"
+ newNetworkVlan: 'VLAN',
+
+ // Original text: "No VLAN if empty"
+ newNetworkDefaultVlan: '如果为空则没有VLAN',
+
+ // Original text: "MTU"
+ newNetworkMtu: 'MTU',
+
+ // Original text: "Default: 1500"
+ newNetworkDefaultMtu: '默认1500',
+
+ // Original text: "Delete network"
+ deleteNetwork: '删除网络',
+
+ // Original text: "Are you sure you want to delete this network?"
+ deleteNetworkConfirm: '你确定要删除此网络',
+
+ // Original text: "Xen Orchestra"
+ xenOrchestra: 'Xen Orchestra',
+
+ // Original text: "server"
+ xenOrchestraServer: '服务器',
+
+ // Original text: "web client"
+ xenOrchestraWeb: 'Web客户端',
+
+ // Original text: "No pro support provided!"
+ noProSupport: '不提供专业支持!',
+
+ // Original text: "Use in production at your own risks"
+ noProductionUse: '在生产环境中使用将存在风险',
+
+ // Original text: "You can download our turnkey appliance at"
+ downloadXoa: '您可以在这里下载我们的整套设备',
+
+ // Original text: "Bug Tracker"
+ bugTracker: '问题跟踪器',
+
+ // Original text: "Issues? Report it!"
+ bugTrackerText: '出现问题?报告!',
+
+ // Original text: "Community"
+ community: '社区',
+
+ // Original text: "Join our community forum!"
+ communityText: '加入我们的社区论坛',
+
+ // Original text: "Free Trial for Premium Edition!"
+ freeTrial: '铂金版免费试用',
+
+ // Original text: "Request your trial now!"
+ freeTrialNow: '立即请求试用',
+
+ // Original text: "Any issue?"
+ issues: '出现任何问题?',
+
+ // Original text: "Problem? Contact us!"
+ issuesText: '有问题?联系我们!',
+
+ // Original text: "Documentation"
+ documentation: '文档',
+
+ // Original text: "Read our official doc"
+ documentationText: '阅读我们官方文档',
+
+ // Original text: "Pro support included"
+ proSupportIncluded: '包含专业支持',
+
+ // Original text: "Acces your XO Account"
+ xoAccount: '进入你的XO账户',
+
+ // Original text: "Report a problem"
+ openTicket: '报告一个问题',
+
+ // Original text: "Problem? Open a ticket !"
+ openTicketText: '存在问题?开个Case!',
+
+ // Original text: "Upgrade needed"
+ upgradeNeeded: '需要升级',
+
+ // Original text: "Upgrade now!"
+ upgradeNow: '立即升级',
+
+ // Original text: "Or"
+ or: '或',
+
+ // Original text: "Try it for free!"
+ tryIt: '免费试用',
+
+ // Original text: "This feature is available starting from {plan} Edition"
+ availableIn: '这个功能将在{plan}版本中可用',
+
+ // Original text: "Updates"
+ updateTitle: '更新',
+
+ // Original text: "Registration"
+ registration: '注册',
+
+ // Original text: "Trial"
+ trial: '试用',
+
+ // Original text: "Settings"
+ settings: '设置',
+
+ // Original text: "Update"
+ update: '更新',
+
+ // Original text: "Upgrade"
+ upgrade: '升级',
+
+ // Original text: "No updater available for Community Edition"
+ noUpdaterCommunity: '社区版本没有可用的升级',
+
+ // Original text: "Please consider subscribe and try it with all features for free during 15 days on"
+ noUpdaterSubscribe: '请考虑订购或在15天内免费试用所有功能',
+
+ // Original text: "Manual update could break your current installation due to dependencies issues, do it with caution"
+ noUpdaterWarning:
+ '由于相关依赖关系的问题,手动更新将跑坏你当前的安全,请小心使用',
+
+ // Original text: "Current version:"
+ currentVersion: '当前版本',
+
+ // Original text: "Register"
+ register: '注册',
+
+ // Original text: "Please, take time to register in order to enjoy your trial."
+ trialRegistration: '为了您的正常使用,请考虑花时间注册',
+
+ // Original text: "Start trial"
+ trialStartButton: '开始试用',
+
+ // Original text: "You can use a trial version until {date, date, medium}. Upgrade your appliance to get it."
+ trialAvailableUntil:
+ '你可以使用试用版本直到{date, date, medium}。更新你的设备来获取',
+
+ // Original text: "Your trial has been ended. Contact us or downgrade to Free version"
+ trialConsumed: '你的使用已经结束,联系我们或下载免费版本',
+
+ // Original text: "Your xoa-updater service appears to be down. Your XOA cannot run fully without reaching this service."
+ trialLocked: '你的xoa-更新服务已停止。没有此服务你的XOA不能完全正常运行',
+
+ // Original text: "No update information available"
+ noUpdateInfo: '没有更新信息可用',
+
+ // Original text: "Update information may be available"
+ waitingUpdateInfo: '更新信息可能可用',
+
+ // Original text: "Your XOA is up-to-date"
+ upToDate: '你的XOA是最新的',
+
+ // Original text: "You need to update your XOA (new version is available)"
+ mustUpgrade: '你需要更新你的XOA(有新版本可用)',
+
+ // Original text: "Your XOA is not registered for updates"
+ registerNeeded: '你的XOA没有注册更新',
+
+ // Original text: "Can't fetch update information"
+ updaterError: '不能获取更新信息',
+
+ // Original text: "Upgrade successful"
+ promptUpgradeReloadTitle: '更新成功',
+
+ // Original text: "Your XOA has successfully upgraded, and your browser must reload the application. Do you want to reload now ?"
+ promptUpgradeReloadMessage:
+ '你的XOA已经成功更新,你的浏览器必须重新加载,你要现在重新加载吗?',
+
+ // Original text: "Xen Orchestra from the sources"
+ disclaimerTitle: 'Xen Orchestra 源码版',
+
+ // Original text: "You are using XO from the sources! That's great for a personal/non-profit usage."
+ disclaimerText1: '你在使用XO的源码版!这非常适合个人/非商业用途',
+
+ // Original text: "If you are a company, it's better to use it with our appliance + pro support included:"
+ disclaimerText2: '如果你是一个公司,建议使用我们的设备结合专业的支持',
+
+ // Original text: "This version is not bundled with any support nor updates. Use it with caution for critical tasks."
+ disclaimerText3: '这个版本没有绑定任何支持或更新,在紧急任务下,请谨慎使用',
+
+ // Original text: "Connect PIF"
+ connectPif: '连接物理网卡',
+
+ // Original text: "Are you sure you want to connect this PIF?"
+ connectPifConfirm: '你确定要连接这个物理网卡?',
+
+ // Original text: "Disconnect PIF"
+ disconnectPif: '断开物理网卡',
+
+ // Original text: "Are you sure you want to disconnect this PIF?"
+ disconnectPifConfirm: '你确定要断开这个网卡网卡?',
+
+ // Original text: "Delete PIF"
+ deletePif: '删除物理网卡',
+
+ // Original text: "Are you sure you want to delete this PIF?"
+ deletePifConfirm: '你确定要删除这个物理网卡?',
+
+ // Original text: "Username"
+ username: '用户名',
+
+ // Original text: "Password"
+ password: '密码',
+
+ // Original text: "Language"
+ language: '语言',
+
+ // Original text: "Old password"
+ oldPasswordPlaceholder: '原密码',
+
+ // Original text: "New password"
+ newPasswordPlaceholder: '新密码',
+
+ // Original text: "Confirm new password"
+ confirmPasswordPlaceholder: '确认新密码',
+
+ // Original text: "Confirmation password incorrect"
+ confirmationPasswordError: '确认密码不正确',
+
+ // Original text: "Password does not match the confirm password."
+ confirmationPasswordErrorBody: '确认密码不匹配',
+
+ // Original text: "Password changed"
+ pwdChangeSuccess: '密码已修改',
+
+ // Original text: "Your password has been successfully changed."
+ pwdChangeSuccessBody: '你的密码已成功修改',
+
+ // Original text: "Incorrect password"
+ pwdChangeError: '密码错误',
+
+ // Original text: "The old password provided is incorrect. Your password has not been changed."
+ pwdChangeErrorBody: '原密码错误,你的密码未更改',
+
+ // Original text: "OK"
+ changePasswordOk: '确认',
+
+ // Original text: "Others"
+ others: '其他',
+}
diff --git a/packages/xo-web/src/common/intl/messages.js b/packages/xo-web/src/common/intl/messages.js
new file mode 100644
index 000000000..724ed6a15
--- /dev/null
+++ b/packages/xo-web/src/common/intl/messages.js
@@ -0,0 +1,1775 @@
+// This file is coded in ES5 and CommonJS to be compatible with
+// `create-locale`.
+
+const forEach = require('lodash/forEach')
+const isString = require('lodash/isString')
+
+const messages = {
+ keyValue: '{key}: {value}',
+
+ statusConnecting: 'Connecting',
+ statusDisconnected: 'Disconnected',
+ statusLoading: 'Loading…',
+ errorPageNotFound: 'Page not found',
+ errorNoSuchItem: 'no such item',
+
+ editableLongClickPlaceholder: 'Long click to edit',
+ editableClickPlaceholder: 'Click to edit',
+ browseFiles: 'Browse files',
+ showLogs: 'Show logs',
+
+ // ----- Modals -----
+ alertOk: 'OK',
+ confirmOk: 'OK',
+ genericCancel: 'Cancel',
+ enterConfirmText: 'Enter the following text to confirm:',
+
+ // ----- Filters -----
+ onError: 'On error',
+ successful: 'Successful',
+ filterOnlyManaged: 'Managed disks',
+ filterOnlyOrphaned: 'Orphaned disks',
+ filterOnlyRegular: 'Normal disks',
+ filterOnlySnapshots: 'Snapshot disks',
+ filterOnlyUnmanaged: 'Unmanaged disks',
+ filterSaveAs: 'Save…',
+ filterSyntaxLinkTooltip: 'Explore the search syntax in the documentation',
+ filterVifsOnlyConnected: 'Connected VIFs',
+ filterVifsOnlyDisconnected: 'Disconnected VIFs',
+ filterRemotesOnlyConnected: 'Connected remotes',
+ filterRemotesOnlyDisconnected: 'Disconnected remotes',
+
+ // ----- Copiable component -----
+ copyToClipboard: 'Copy to clipboard',
+
+ // ----- Pills -----
+ pillMaster: 'Master',
+
+ // ----- Titles -----
+ homePage: 'Home',
+ homeVmPage: 'VMs',
+ homeHostPage: 'Hosts',
+ homePoolPage: 'Pools',
+ homeTemplatePage: 'Templates',
+ homeSrPage: 'Storages',
+ dashboardPage: 'Dashboard',
+ overviewDashboardPage: 'Overview',
+ overviewVisualizationDashboardPage: 'Visualizations',
+ overviewStatsDashboardPage: 'Statistics',
+ overviewHealthDashboardPage: 'Health',
+ selfServicePage: 'Self service',
+ backupPage: 'Backup',
+ jobsPage: 'Jobs',
+ xoaPage: 'XOA',
+ updatePage: 'Updates',
+ licensesPage: 'Licenses',
+ settingsPage: 'Settings',
+ settingsServersPage: 'Servers',
+ settingsUsersPage: 'Users',
+ settingsGroupsPage: 'Groups',
+ settingsAclsPage: 'ACLs',
+ settingsPluginsPage: 'Plugins',
+ settingsLogsPage: 'Logs',
+ settingsIpsPage: 'IPs',
+ settingsConfigPage: 'Config',
+ aboutPage: 'About',
+ aboutXoaPlan: 'About XO {xoaPlan}',
+ newMenu: 'New',
+ taskMenu: 'Tasks',
+ taskPage: 'Tasks',
+ newVmPage: 'VM',
+ newSrPage: 'Storage',
+ newServerPage: 'Server',
+ newImport: 'Import',
+ xosan: 'XOSAN',
+ backupOverviewPage: 'Overview',
+ backupNewPage: 'New',
+ backupRemotesPage: 'Remotes',
+ backupRestorePage: 'Restore',
+ backupFileRestorePage: 'File restore',
+ schedule: 'Schedule',
+ newVmBackup: 'New VM backup',
+ editVmBackup: 'Edit VM backup',
+ backup: 'Backup',
+ rollingSnapshot: 'Rolling Snapshot',
+ deltaBackup: 'Delta Backup',
+ disasterRecovery: 'Disaster Recovery',
+ continuousReplication: 'Continuous Replication',
+ jobsOverviewPage: 'Overview',
+ jobsNewPage: 'New',
+ jobsSchedulingPage: 'Scheduling',
+ customJob: 'Custom Job',
+ userPage: 'User',
+ xoa: 'XOA',
+
+ // ----- Support -----
+ noSupport: 'No support',
+ freeUpgrade: 'Free upgrade!',
+
+ // ----- Sign out -----
+ signOut: 'Sign out',
+
+ // ----- User Profile -----
+ editUserProfile: 'Edit my settings {username}',
+
+ // ----- Home view ------
+ homeFetchingData: 'Fetching data…',
+ homeWelcome: 'Welcome to Xen Orchestra!',
+ homeWelcomeText: 'Add your XenServer hosts or pools',
+ homeConnectServerText:
+ 'Some XenServers have been registered but are not connected',
+ homeHelp: 'Want some help?',
+ homeAddServer: 'Add server',
+ homeConnectServer: 'Connect servers',
+ homeOnlineDoc: 'Online Doc',
+ homeProSupport: 'Pro Support',
+ homeNoVms: 'There are no VMs!',
+ homeNoVmsOr: 'Or…',
+ homeImportVm: 'Import VM',
+ homeImportVmMessage: 'Import an existing VM in xva format',
+ homeRestoreBackup: 'Restore a backup',
+ homeRestoreBackupMessage: 'Restore a backup from a remote store',
+ homeNewVmMessage: 'This will create a new VM',
+ homeFilters: 'Filters',
+ homeNoMatches: 'No results! Click here to reset your filters',
+ homeTypePool: 'Pool',
+ homeTypeHost: 'Host',
+ homeTypeVm: 'VM',
+ homeTypeSr: 'SR',
+ homeTypeVmTemplate: 'Template',
+ homeSort: 'Sort',
+ homeAllPools: 'Pools',
+ homeAllHosts: 'Hosts',
+ homeAllTags: 'Tags',
+ homeAllResourceSets: 'Resource sets',
+ homeNewVm: 'New VM',
+ homeFilterNone: 'None',
+ homeFilterRunningHosts: 'Running hosts',
+ homeFilterDisabledHosts: 'Disabled hosts',
+ homeFilterRunningVms: 'Running VMs',
+ homeFilterNonRunningVms: 'Non running VMs',
+ homeFilterPendingVms: 'Pending VMs',
+ homeFilterHvmGuests: 'HVM guests',
+ homeFilterTags: 'Tags',
+ homeSortBy: 'Sort by',
+ homeSortByCpus: 'CPUs',
+ homeSortByName: 'Name',
+ homeSortByPowerstate: 'Power state',
+ homeSortByRAM: 'RAM',
+ homeSortByShared: 'Shared/Not shared',
+ homeSortBySize: 'Size',
+ 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',
+ homeMigrateTo: 'Migrate to…',
+ homeMissingPaths: 'Missing patches',
+ homePoolMaster: 'Master:',
+ homeResourceSet: 'Resource set: {resourceSet}',
+ highAvailability: 'High Availability',
+ 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',
+ remove: 'Remove',
+ preview: 'Preview',
+ action: 'Action',
+ item: 'Item',
+ noSelectedValue: 'No selected value',
+ selectSubjects: 'Choose user(s) and/or group(s)',
+ selectObjects: 'Select Object(s)…',
+ selectRole: 'Choose a role',
+ selectHosts: 'Select Host(s)…',
+ selectHostsVms: 'Select object(s)…',
+ selectNetworks: 'Select Network(s)…',
+ selectPifs: 'Select PIF(s)…',
+ selectPools: 'Select Pool(s)…',
+ selectRemotes: 'Select Remote(s)…',
+ selectResourceSets: 'Select resource set(s)…',
+ selectResourceSetsVmTemplate: 'Select template(s)…',
+ selectResourceSetsSr: 'Select SR(s)…',
+ selectResourceSetsNetwork: 'Select network(s)…',
+ selectResourceSetsVdi: 'Select disk(s)…',
+ selectSshKey: 'Select SSH key(s)…',
+ selectSrs: 'Select SR(s)…',
+ selectVms: 'Select VM(s)…',
+ selectVmTemplates: 'Select VM template(s)…',
+ selectTags: 'Select tag(s)…',
+ selectVdis: 'Select disk(s)…',
+ 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',
+
+ // --- Dates/Scheduler ---
+
+ schedulingMonth: 'Month',
+ schedulingEveryNMonth: 'Every N month',
+ schedulingEachSelectedMonth: 'Each selected month',
+ schedulingDay: 'Day',
+ schedulingEveryNDay: 'Every N day',
+ schedulingEachSelectedDay: 'Each selected day',
+ schedulingSetWeekDayMode: 'Switch to week days',
+ schedulingSetMonthDayMode: 'Switch to month days',
+ schedulingHour: 'Hour',
+ schedulingEachSelectedHour: 'Each selected hour',
+ schedulingEveryNHour: 'Every N hour',
+ schedulingMinute: 'Minute',
+ schedulingEachSelectedMinute: 'Each selected minute',
+ schedulingEveryNMinute: 'Every N minute',
+ selectTableAllMonth: 'Every month',
+ selectTableAllDay: 'Every day',
+ selectTableAllHour: 'Every hour',
+ selectTableAllMinute: 'Every minute',
+ schedulingReset: 'Reset',
+ unknownSchedule: 'Unknown',
+ timezonePickerUseLocalTime: 'Web browser timezone',
+ serverTimezoneOption: 'Server timezone ({value})',
+ cronPattern: 'Cron Pattern:',
+ backupEditNotFoundTitle: 'Cannot edit backup',
+ backupEditNotFoundMessage: 'Missing required info for edition',
+ successfulJobCall: 'Successful',
+ failedJobCall: 'Failed',
+ jobCallInProgess: 'In progress',
+ jobTransferredDataSize: 'Transfer size:',
+ jobTransferredDataSpeed: 'Transfer speed:',
+ jobMergedDataSize: 'Merge size:',
+ jobMergedDataSpeed: 'Merge speed:',
+ allJobCalls: 'All',
+ job: 'Job',
+ jobModalTitle: 'Job {job}',
+ jobId: 'ID',
+ jobType: 'Type',
+ jobName: 'Name',
+ jobNamePlaceholder: 'Name of your job (forbidden: "_")',
+ jobStart: 'Start',
+ jobEnd: 'End',
+ jobDuration: 'Duration',
+ jobStatus: 'Status',
+ jobAction: 'Action',
+ jobTag: 'Tag',
+ jobScheduling: 'Scheduling',
+ jobState: 'State',
+ jobStateEnabled: 'Enabled',
+ jobStateDisabled: 'Disabled',
+ jobTimezone: 'Timezone',
+ jobServerTimezone: 'Server',
+ runJob: 'Run job',
+ runJobVerbose: 'One shot running started. See overview for logs.',
+ jobStarted: 'Started',
+ jobFinished: 'Finished',
+ saveBackupJob: 'Save',
+ deleteBackupSchedule: 'Remove 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.',
+ scheduleEdit: 'Edit',
+ scheduleDelete: 'Delete',
+ deleteSelectedSchedules: 'Delete selected schedules',
+ noScheduledJobs: 'No scheduled jobs.',
+ newSchedule: 'New schedule',
+ noJobs: 'No jobs found.',
+ noSchedules: 'No schedules found',
+ jobActionPlaceHolder: 'Select a xo-server API command',
+ jobTimeoutPlaceHolder:
+ 'Timeout (number of seconds after which a VM is considered failed)',
+ jobSchedules: 'Schedules',
+ jobScheduleNamePlaceHolder: 'Name of your schedule',
+ jobScheduleJobPlaceHolder: 'Select a Job',
+ jobOwnerPlaceholder: 'Job owner',
+ jobUserNotFound: "This job's creator no longer exists",
+ backupUserNotFound: "This backup's creator no longer exists",
+ redirectToMatchingVms: 'Click here to see the matching VMs',
+ noMatchingVms: 'There are no matching VMs!',
+ allMatchingVms: '{icon} See the matching VMs ({nMatchingVms, number})',
+ backupOwner: 'Backup owner',
+
+ // ------ New backup -----
+ newBackupSelection: 'Select your backup type:',
+ smartBackupModeSelection: 'Select backup mode:',
+ 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.',
+ editBackupVmsTitle: 'VMs',
+ editBackupSmartStatusTitle: 'VMs statuses',
+ editBackupSmartResidentOn: 'Resident on',
+ editBackupSmartPools: 'Pools',
+ editBackupSmartTags: 'Tags',
+ sampleOfMatchingVms: 'Sample of matching Vms',
+ editBackupSmartTagsTitle: 'VMs Tags',
+ editBackupNot: 'Reverse',
+ editBackupTagTitle: 'Tag',
+ editBackupReportTitle: 'Report',
+ editBackupScheduleEnabled: 'Automatically run as scheduled',
+ editBackupRetentionTitle: 'Retention',
+ editBackupRemoteTitle: 'Remote',
+ deleteOldBackupsFirst: 'Delete the old backups first',
+
+ // ------ New Remote -----
+ remoteList: 'Remote stores for backup',
+ newRemote: 'New File System Remote',
+ remoteTypeLocal: 'Local',
+ 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}',
+ remoteTestSuccess: 'Test passed for {name}',
+ remoteTestError: 'Error',
+ remoteTestStep: 'Test Step',
+ remoteTestFile: 'Test file',
+ remoteTestName: 'Test name',
+ remoteTestNameFailure: 'Remote name already exists!',
+ remoteTestSuccessMessage: 'The remote appears to work correctly',
+ remoteConnectionFailed: 'Connection failed',
+
+ // ------ Remote -----
+ remoteName: 'Name',
+ remotePath: 'Path',
+ remoteState: 'State',
+ remoteDevice: 'Device',
+ remoteShare: 'Share',
+ remoteAction: 'Action',
+ remoteAuth: 'Auth',
+ remoteMounted: 'Mounted',
+ remoteUnmounted: 'Unmounted',
+ remoteConnectTip: 'Connect',
+ remoteDisconnectTip: 'Disconnect',
+ remoteConnected: 'Connected',
+ remoteDisconnected: 'Disconnected',
+ remoteDeleteTip: 'Delete',
+ remoteDeleteSelected: 'Delete selected remotes',
+ remoteNamePlaceHolder: 'remote name *',
+ remoteMyNamePlaceHolder: 'Name *',
+ remoteLocalPlaceHolderPath: '/path/to/backup',
+ remoteNfsPlaceHolderHost: 'host *',
+ remoteNfsPlaceHolderPath: 'path/to/backup',
+ remoteSmbPlaceHolderRemotePath: 'subfolder [path\\\\to\\\\backup]',
+ remoteSmbPlaceHolderUsername: 'Username',
+ remoteSmbPlaceHolderPassword: 'Password',
+ remoteSmbPlaceHolderDomain: 'Domain',
+ remoteSmbPlaceHolderAddressShare: '\\\\ *',
+ remotePlaceHolderPassword: 'password(fill to edit)',
+
+ // ------ New Storage -----
+ newSrTitle: 'Create a new SR',
+ newSrGeneral: 'General',
+ newSrTypeSelection: 'Select Storage Type:',
+ newSrSettings: 'Settings',
+ newSrUsage: 'Storage Usage',
+ newSrSummary: 'Summary',
+ newSrHost: 'Host',
+ newSrType: 'Type',
+ newSrName: 'Name',
+ newSrDescription: 'Description',
+ newSrServer: 'Server',
+ newSrPath: 'Path',
+ newSrIqn: 'IQN',
+ newSrLun: 'LUN',
+ newSrAuth: 'with auth.',
+ newSrUsername: 'User Name',
+ newSrPassword: 'Password',
+ newSrDevice: 'Device',
+ newSrInUse: 'in use',
+ newSrSize: 'Size',
+ newSrCreate: 'Create',
+ newSrNamePlaceHolder: 'Storage name',
+ newSrDescPlaceHolder: 'Storage description',
+ newSrAddressPlaceHolder: 'Address',
+ newSrPortPlaceHolder: '[port]',
+ newSrUsernamePlaceHolder: 'Username',
+ newSrPasswordPlaceHolder: 'Password',
+ newSrLvmDevicePlaceHolder: 'Device, e.g /dev/sda…',
+ newSrLocalPathPlaceHolder: '/path/to/directory',
+
+ // ----- Acls, Users, Groups ------
+ subjectName: 'Users/Groups',
+ objectName: 'Object',
+ aclNoneFound: 'No acls found',
+ roleName: 'Role',
+ aclCreate: 'Create',
+ newGroupName: 'New Group Name',
+ createGroup: 'Create Group',
+ createGroupButton: 'Create',
+ deleteGroup: 'Delete Group',
+ deleteGroupConfirm: 'Are you sure you want to delete this group?',
+ removeUserFromGroup: 'Remove user from Group',
+ deleteUserConfirm: 'Are you sure you want to delete this user?',
+ deleteUser: 'Delete User',
+ noUser: 'no user',
+ unknownUser: 'unknown user',
+ noGroupFound: 'No group found',
+ groupNameColumn: 'Name',
+ groupUsersColumn: 'Users',
+ addUserToGroupColumn: 'Add User',
+ userNameColumn: 'Email',
+ userPermissionColumn: 'Permissions',
+ userPasswordColumn: 'Password',
+ userName: 'Email',
+ userPassword: 'Password',
+ createUserButton: 'Create',
+ noUserFound: 'No user found',
+ userLabel: 'User',
+ adminLabel: 'Admin',
+ noUserInGroup: 'No user in group',
+ countUsers: '{users, number} user{users, plural, one {} other {s}}',
+ selectPermission: 'Select Permission',
+
+ // ----- Plugins ------
+ noPlugins: 'No plugins found',
+ autoloadPlugin: 'Auto-load at server start',
+ savePluginConfiguration: 'Save configuration',
+ deletePluginConfiguration: 'Delete configuration',
+ pluginError: 'Plugin error',
+ unknownPluginError: 'Unknown error',
+ purgePluginConfiguration: 'Purge plugin configuration',
+ purgePluginConfigurationQuestion:
+ 'Are you sure you want to purge this configuration ?',
+ editPluginConfiguration: 'Edit',
+ cancelPluginEdition: 'Cancel',
+ pluginConfigurationSuccess: 'Plugin configuration',
+ pluginConfigurationChanges: 'Plugin configuration successfully saved!',
+ pluginConfigurationPresetTitle: 'Predefined configuration',
+ pluginConfigurationChoosePreset: 'Choose a predefined configuration.',
+ applyPluginPreset: 'Apply',
+
+ // ----- User preferences -----
+ saveNewUserFilterErrorTitle: 'Save filter error',
+ saveNewUserFilterErrorBody: 'Bad parameter: name must be given.',
+ filterName: 'Name:',
+ filterValue: 'Value:',
+ saveNewFilterTitle: 'Save new filter',
+ setUserFiltersTitle: 'Set custom filters',
+ setUserFiltersBody: 'Are you sure you want to set custom filters?',
+ removeUserFilterTitle: 'Remove custom filter',
+ removeUserFilterBody: 'Are you sure you want to remove custom filter?',
+ defaultFilter: 'Default filter',
+ defaultFilters: 'Default filters',
+ customFilters: 'Custom filters',
+ customizeFilters: 'Customize filters',
+ saveCustomFilters: 'Save custom filters',
+
+ // ----- VM actions ------
+ startVmLabel: 'Start',
+ recoveryModeLabel: 'Recovery start',
+ suspendVmLabel: 'Suspend',
+ stopVmLabel: 'Stop',
+ forceShutdownVmLabel: 'Force shutdown',
+ rebootVmLabel: 'Reboot',
+ forceRebootVmLabel: 'Force reboot',
+ deleteVmLabel: 'Delete',
+ migrateVmLabel: 'Migrate',
+ snapshotVmLabel: 'Snapshot',
+ exportVmLabel: 'Export',
+ resumeVmLabel: 'Resume',
+ copyVmLabel: 'Copy',
+ cloneVmLabel: 'Clone',
+ fastCloneVmLabel: 'Fast clone',
+ convertVmToTemplateLabel: 'Convert to template',
+ vmConsoleLabel: 'Console',
+
+ // ----- 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',
+ srDisconnectAll: 'Disconnect from all hosts',
+ srForget: 'Forget this SR',
+ srsForget: 'Forget SRs',
+ srRemoveButton: 'Remove this SR',
+ srNoVdis: 'No VDIs in this storage',
+ // ----- Pool general -----
+ poolTitleRamUsage: 'Pool RAM usage:',
+ poolRamUsage: '{used} used on {total}',
+ poolMaster: 'Master:',
+ displayAllHosts: 'Display all hosts of this pool',
+ displayAllStorages: 'Display all storages of this pool',
+ displayAllVMs: 'Display all VMs of this pool',
+ // ----- Pool tabs -----
+ hostsTabName: 'Hosts',
+ vmsTabName: 'Vms',
+ srsTabName: 'Srs',
+ // ----- Pool advanced tab -----
+ poolHaStatus: 'High Availability',
+ poolHaEnabled: 'Enabled',
+ poolHaDisabled: 'Disabled',
+ setpoolMaster: 'Master',
+ poolGpuGroups: 'GPU groups',
+ // ----- Pool host tab -----
+ hostNameLabel: 'Name',
+ hostDescription: 'Description',
+ hostMemory: 'Memory',
+ noHost: 'No hosts',
+ memoryLeftTooltip: '{used}% used ({free} free)',
+ // ----- Pool network tab -----
+ pif: 'PIF',
+ poolNetworkNameLabel: 'Name',
+ poolNetworkDescription: 'Description',
+ poolNetworkPif: 'PIFs',
+ poolNoNetwork: 'No networks',
+ poolNetworkMTU: 'MTU',
+ poolNetworkPifAttached: 'Connected',
+ poolNetworkPifDetached: 'Disconnected',
+ showPifs: 'Show PIFs',
+ hidePifs: 'Hide PIFs',
+ showDetails: 'Show details',
+ hideDetails: 'Hide details',
+ // ----- Pool stats tab -----
+ poolNoStats: 'No stats',
+ poolAllHosts: 'All hosts',
+ // ----- Pool actions ------
+ addSrLabel: 'Add SR',
+ addVmLabel: 'Add VM',
+ 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.",
+ addHostErrorTitle: 'Adding host failed',
+ addHostNotHomogeneousErrorMessage: 'Host patches could not be homogenized.',
+ disconnectServer: 'Disconnect',
+
+ // ----- Host actions ------
+ startHostLabel: 'Start',
+ stopHostLabel: 'Stop',
+ enableHostLabel: 'Enable',
+ disableHostLabel: 'Disable',
+ restartHostAgent: 'Restart toolstack',
+ forceRebootHostLabel: 'Force reboot',
+ rebootHostLabel: 'Reboot',
+ noHostsAvailableErrorTitle: 'Error while restarting host',
+ noHostsAvailableErrorMessage:
+ 'Some VMs cannot be migrated before restarting this host. Please try force reboot.',
+ failHostBulkRestartTitle: 'Error while restarting hosts',
+ failHostBulkRestartMessage:
+ '{failedHosts, 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 -----
+ statLoad: 'Load average',
+ // ----- host advanced tab -----
+ memoryHostState: 'RAM Usage: {memoryUsed}',
+ hardwareHostSettingsLabel: 'Hardware',
+ hostAddress: 'Address',
+ hostStatus: 'Status',
+ hostBuildNumber: 'Build number',
+ hostIscsiName: 'iSCSI name',
+ hostXenServerVersion: 'Version',
+ hostStatusEnabled: 'Enabled',
+ hostStatusDisabled: 'Disabled',
+ hostPowerOnMode: 'Power on mode',
+ hostStartedSince: 'Host uptime',
+ hostStackStartedSince: 'Toolstack uptime',
+ hostCpusModel: 'CPU model',
+ hostGpus: 'GPUs',
+ hostCpusNumber: 'Core (socket)',
+ hostManufacturerinfo: 'Manufacturer info',
+ hostBiosinfo: 'BIOS info',
+ licenseHostSettingsLabel: 'License',
+ hostLicenseType: 'Type',
+ hostLicenseSocket: 'Socket',
+ hostLicenseExpiry: 'Expiry',
+ supplementalPacks: 'Installed supplemental packs',
+ supplementalPackNew: 'Install new supplemental pack',
+ supplementalPackPoolNew: 'Install supplemental pack on every host',
+ supplementalPackTitle: '{name} (by {author})',
+ supplementalPackInstallStartedTitle: 'Installation started',
+ supplementalPackInstallStartedMessage: 'Installing new supplemental pack…',
+ supplementalPackInstallErrorTitle: 'Installation error',
+ supplementalPackInstallErrorMessage:
+ 'The installation of the supplemental pack failed.',
+ supplementalPackInstallSuccessTitle: 'Installation success',
+ supplementalPackInstallSuccessMessage:
+ 'Supplemental pack successfully installed.',
+ // ----- Host net tabs -----
+ networkCreateButton: 'Add a network',
+ networkCreateBondedButton: 'Add a bonded network',
+ pifDeviceLabel: 'Device',
+ pifNetworkLabel: 'Network',
+ pifVlanLabel: 'VLAN',
+ pifAddressLabel: 'Address',
+ pifModeLabel: 'Mode',
+ pifMacLabel: 'MAC',
+ pifMtuLabel: 'MTU',
+ pifStatusLabel: 'Status',
+ pifStatusConnected: 'Connected',
+ pifStatusDisconnected: 'Disconnected',
+ pifNoInterface: 'No physical interface detected',
+ pifInUse: 'This interface is currently in use',
+ pifAction: 'Action',
+ defaultLockingMode: 'Default locking mode',
+ pifConfigureIp: 'Configure IP address',
+ configIpErrorTitle: 'Invalid parameters',
+ configIpErrorMessage: 'IP address and netmask required',
+ staticIp: 'Static IP address',
+ netmask: 'Netmask',
+ dns: 'DNS',
+ gateway: 'Gateway',
+ // ----- Host storage tabs -----
+ addSrDeviceButton: 'Add a storage',
+ srNameLabel: 'Name',
+ srType: 'Type',
+ pbdAction: 'Action',
+ pbdStatus: 'Status',
+ pbdStatusConnected: 'Connected',
+ pbdStatusDisconnected: 'Disconnected',
+ pbdConnect: 'Connect',
+ pbdDisconnect: 'Disconnect',
+ pbdForget: 'Forget',
+ srShared: 'Shared',
+ srNotShared: 'Not shared',
+ pbdNoSr: 'No storage detected',
+ // ----- Host patch tabs -----
+ patchNameLabel: 'Name',
+ patchUpdateButton: 'Install all patches',
+ patchDescription: 'Description',
+ patchApplied: 'Applied date',
+ patchSize: 'Size',
+ patchStatus: 'Status',
+ patchStatusApplied: 'Applied',
+ patchStatusNotApplied: 'Missing patches',
+ patchNothing: 'No patches detected',
+ patchReleaseDate: 'Release date',
+ patchGuidance: 'Guidance',
+ patchAction: 'Action',
+ hostAppliedPatches: 'Applied patches',
+ 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',
+ installPatchWarningReject: 'Go to pool',
+ installPatchWarningResolve: 'Install',
+ // ----- Pool patch tabs -----
+ refreshPatches: 'Refresh patches',
+ installPoolPatches: 'Install pool patches',
+ // ----- Pool storage tabs -----
+ defaultSr: 'Default SR',
+ setAsDefaultSr: 'Set as default SR',
+
+ // ----- VM tabs -----
+ generalTabName: 'General',
+ statsTabName: 'Stats',
+ consoleTabName: 'Console',
+ containersTabName: 'Container',
+ snapshotsTabName: 'Snapshots',
+ logsTabName: 'Logs',
+ advancedTabName: 'Advanced',
+ networkTabName: 'Network',
+ disksTabName: 'Disk{disks, plural, one {} other {s}}',
+
+ powerStateHalted: 'halted',
+ powerStateRunning: 'running',
+ powerStateSuspended: 'suspended',
+
+ // ----- VM home -----
+ vmStatus: 'No Xen tools detected',
+ vmName: 'No IPv4 record',
+ vmDescription: 'No IP record',
+ vmSettings: 'Started {ago}',
+ vmCurrentStatus: 'Current status:',
+ vmNotRunning: 'Not running',
+ vmHaltedSince: 'Halted {ago}',
+
+ // ----- VM general tab -----
+ noToolsDetected: 'No Xen tools detected',
+ noIpv4Record: 'No IPv4 record',
+ noIpRecord: 'No IP record',
+ started: 'Started {ago}',
+ paraVirtualizedMode: 'Paravirtualization (PV)',
+ hardwareVirtualizedMode: 'Hardware virtualization (HVM)',
+
+ // ----- VM stat tab -----
+ statsCpu: 'CPU usage',
+ statsMemory: 'Memory usage',
+ statsNetwork: 'Network throughput',
+ useStackedValuesOnStats: 'Stacked values',
+ statDisk: 'Disk throughput',
+ statLastTenMinutes: 'Last 10 minutes',
+ statLastTwoHours: 'Last 2 hours',
+ statLastWeek: 'Last week',
+ statLastYear: 'Last year',
+
+ // ----- VM console tab -----
+ copyToClipboardLabel: 'Copy',
+ ctrlAltDelButtonLabel: 'Ctrl+Alt+Del',
+ tipLabel: 'Tip:',
+ hideHeaderTooltip: 'Hide infos',
+ showHeaderTooltip: 'Show infos',
+
+ // ----- VM container tab -----
+ containerName: 'Name',
+ containerCommand: 'Command',
+ containerCreated: 'Creation date',
+ containerStatus: 'Status',
+ containerAction: 'Action',
+ noContainers: 'No existing containers',
+ containerStop: 'Stop this container',
+ containerStart: 'Start this container',
+ containerPause: 'Pause this container',
+ containerResume: 'Resume this container',
+ containerRestart: 'Restart this container',
+
+ // ----- VM disk tab -----
+ vdiAction: 'Action',
+ vdiAttachDeviceButton: 'Attach disk',
+ vbdCreateDeviceButton: 'New disk',
+ vdiBootOrder: 'Boot order',
+ vdiNameLabel: 'Name',
+ vdiNameDescription: 'Description',
+ vdiPool: 'Pool',
+ vdiDisconnect: 'Disconnect',
+ vdiTags: 'Tags',
+ vdiSize: 'Size',
+ vdiSr: 'SR',
+ vdiVm: 'VM',
+ vdiMigrate: 'Migrate VDI',
+ vdiMigrateSelectSr: 'Destination SR:',
+ vdiMigrateAll: 'Migrate all VDIs',
+ vdiMigrateNoSr: 'No SR',
+ vdiMigrateNoSrMessage: 'A target SR is required to migrate a VDI',
+ vdiForget: 'Forget',
+ vdiRemove: 'Remove VDI',
+ noControlDomainVdis: 'No VDIs attached to Control Domain',
+ vbdBootableStatus: 'Boot flag',
+ vbdStatus: 'Status',
+ vbdStatusConnected: 'Connected',
+ vbdStatusDisconnected: 'Disconnected',
+ vbdNoVbd: 'No disks',
+ vbdConnect: 'Connect VBD',
+ vbdDisconnect: 'Disconnect VBD',
+ vbdBootable: 'Bootable',
+ vbdReadonly: 'Readonly',
+ vbdAction: 'Action',
+ vbdCreate: 'Create',
+ vbdAttach: 'Attach',
+ vbdNamePlaceHolder: 'Disk name',
+ vbdSizePlaceHolder: 'Size',
+ cdDriveNotInstalled: 'CD drive not completely installed',
+ 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',
+ vifNoInterface: 'No interface',
+ vifDeviceLabel: 'Device',
+ vifMacLabel: 'MAC address',
+ vifMtuLabel: 'MTU',
+ vifNetworkLabel: 'Network',
+ vifStatusLabel: 'Status',
+ vifStatusConnected: 'Connected',
+ vifStatusDisconnected: 'Disconnected',
+ vifConnect: 'Connect',
+ vifDisconnect: 'Disconnect',
+ vifRemove: 'Remove',
+ vifsRemove: 'Remove selected VIFs',
+ vifIpAddresses: 'IP addresses',
+ vifMacAutoGenerate: 'Auto-generated if empty',
+ vifAllowedIps: 'Allowed IPs',
+ vifNoIps: 'No IPs',
+ vifLockedNetwork: 'Network locked',
+ vifLockedNetworkNoIps:
+ 'Network locked and no IPs are allowed for this interface',
+ vifUnLockedNetwork: 'Network not locked',
+ vifUnknownNetwork: 'Unknown network',
+ vifAction: 'Action',
+ vifCreate: 'Create',
+
+ // ----- VM snapshot tab -----
+ noSnapshots: 'No snapshots',
+ snapshotCreateButton: 'New snapshot',
+ tipCreateSnapshotLabel: 'Just click on the snapshot button to create one!',
+ revertSnapshot: 'Revert VM to this snapshot',
+ deleteSnapshot: 'Remove this snapshot',
+ deleteSnapshots: 'Remove selected snapshots',
+ copySnapshot: 'Create a VM from this snapshot',
+ exportSnapshot: 'Export this snapshot',
+ snapshotDate: 'Creation date',
+ snapshotName: 'Name',
+ snapshotDescription: 'Description',
+ snapshotAction: 'Action',
+ snapshotQuiesce: 'Quiesced snapshot',
+
+ // ----- VM log tab -----
+ logRemoveAll: 'Remove all logs',
+ noLogs: 'No logs so far',
+ logDate: 'Creation date',
+ logName: 'Name',
+ logContent: 'Content',
+ logAction: 'Action',
+
+ // ----- VM advanced tab -----
+ vmRemoveButton: 'Remove',
+ vmConvertButton: 'Convert',
+ xenSettingsLabel: 'Xen settings',
+ guestOsLabel: 'Guest OS',
+ miscLabel: 'Misc',
+ uuid: 'UUID',
+ virtualizationMode: 'Virtualization mode',
+ cpuWeightLabel: 'CPU weight',
+ defaultCpuWeight: 'Default ({value, number})',
+ cpuCapLabel: 'CPU cap',
+ defaultCpuCap: 'Default ({value, number})',
+ pvArgsLabel: 'PV args',
+ xenToolsStatus: 'Xen tools status',
+ xenToolsStatusValue: {
+ defaultMessage: '{status}',
+ description:
+ 'status can be `not-installed`, `unknown`, `out-of-date` & `up-to-date`',
+ },
+ osName: 'OS name',
+ osKernel: 'OS kernel',
+ autoPowerOn: 'Auto power on',
+ ha: 'HA',
+ vmAffinityHost: 'Affinity host',
+ vmVga: 'VGA',
+ vmVideoram: 'Video RAM',
+ noAffinityHost: 'None',
+ originalTemplate: 'Original template',
+ unknownOsName: 'Unknown',
+ unknownOsKernel: 'Unknown',
+ unknownOriginalTemplate: 'Unknown',
+ vmLimitsLabel: 'VM limits',
+ resourceSet: 'Resource set',
+ resourceSetNone: 'None',
+ 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',
+ vmCoresPerSocketNone: 'None',
+ vmCoresPerSocketIncorrectValue: 'Incorrect cores per socket value',
+ 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 -----
+
+ vmHomeNamePlaceholder: 'Long click to add a name',
+ vmHomeDescriptionPlaceholder: 'Long click to add a description',
+ vmViewNamePlaceholder: 'Click to add a name',
+ vmViewDescriptionPlaceholder: 'Click to add a description',
+
+ // ----- Templates -----
+
+ 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}}?',
+
+ // ----- Dashboard -----
+ poolPanel: 'Pool{pools, plural, one {} other {s}}',
+ hostPanel: 'Host{hosts, plural, one {} other {s}}',
+ vmPanel: 'VM{vms, plural, one {} other {s}}',
+ memoryStatePanel: 'RAM Usage:',
+ usedMemory: 'Used Memory',
+ totalMemory: 'Total Memory',
+ totalCpus: 'CPUs Total',
+ usedVCpus: 'Used vCPUs',
+ usedSpace: 'Used Space',
+ totalSpace: 'Total Space',
+ cpuStatePanel: 'CPUs Usage',
+ vmStatePanel: 'VMs Power state',
+ vmStateHalted: 'Halted',
+ vmStateOther: 'Other',
+ vmStateRunning: 'Running',
+ taskStatePanel: 'Pending tasks',
+ usersStatePanel: 'Users',
+ srStatePanel: 'Storage state',
+ ofUsage: '{usage} (of {total})',
+ ofCpusUsage:
+ '{nVcpus, number} vCPU{nVcpus, plural, one {} other {s}} (of {nCpus, number} CPU{nCpus, plural, one {} other {s}})',
+ noSrs: 'No storage',
+ srName: 'Name',
+ srPool: 'Pool',
+ srHost: 'Host',
+ srFormat: 'Type',
+ srSize: 'Size',
+ srUsage: 'Usage',
+ srUsed: 'used',
+ srFree: 'free',
+ srUsageStatePanel: 'Storage Usage',
+ srTopUsageStatePanel: 'Top 5 SR Usage (in %)',
+ notEnoughPermissionsError: 'Not enough permissions!',
+ vmsStates: '{running, number} running ({halted, number} halted)',
+ dashboardStatsButtonRemoveAll: 'Clear selection',
+ dashboardStatsButtonAddAllHost: 'Add all hosts',
+ dashboardStatsButtonAddAllVM: 'Add all VMs',
+
+ // --- Stats board --
+ weekHeatmapData: '{value} {date, date, medium}',
+ weekHeatmapNoData: 'No data.',
+ weeklyHeatmap: 'Weekly Heatmap',
+ weeklyCharts: 'Weekly Charts',
+ weeklyChartsScaleInfo: 'Synchronize scale:',
+ statsDashboardGenericErrorTitle: 'Stats error',
+ statsDashboardGenericErrorMessage: 'There is no stats available for:',
+ noSelectedMetric: 'No selected metric',
+ statsDashboardSelectObjects: 'Select',
+ metricsLoading: 'Loading…',
+
+ // ----- Visualizations -----
+ comingSoon: 'Coming soon!',
+
+ // ----- Health -----
+ orphanedVdis: 'Orphaned snapshot VDIs',
+ orphanedVms: 'Orphaned VMs snapshot',
+ noOrphanedObject: 'No orphans',
+ removeAllOrphanedObject: 'Remove all orphaned snapshot VDIs',
+ vdisOnControlDomain: 'VDIs attached to Control Domain',
+ vmNameLabel: 'Name',
+ vmNameDescription: 'Description',
+ vmContainer: 'Resident on',
+ alarmMessage: 'Alarms',
+ noAlarms: 'No alarms',
+ alarmDate: 'Date',
+ alarmContent: 'Content',
+ alarmObject: 'Issue on',
+ alarmPool: 'Pool',
+ alarmRemoveAll: 'Remove all alarms',
+ spaceLeftTooltip: '{used}% used ({free} left)',
+
+ // ----- New VM -----
+ newVmCreateNewVmOn: 'Create a new VM on {select}',
+ newVmCreateNewVmNoPermission: 'You have no permission to create a VM',
+ newVmInfoPanel: 'Infos',
+ newVmNameLabel: 'Name',
+ newVmTemplateLabel: 'Template',
+ newVmDescriptionLabel: 'Description',
+ newVmPerfPanel: 'Performances',
+ newVmVcpusLabel: 'vCPUs',
+ newVmRamLabel: 'RAM',
+ newVmStaticMaxLabel: 'Static memory max',
+ newVmDynamicMinLabel: 'Dynamic memory min',
+ newVmDynamicMaxLabel: 'Dynamic memory max',
+ newVmInstallSettingsPanel: 'Install settings',
+ newVmIsoDvdLabel: 'ISO/DVD',
+ newVmNetworkLabel: 'Network',
+ newVmInstallNetworkPlaceHolder: 'e.g: http://httpredir.debian.org/debian',
+ newVmPvArgsLabel: 'PV Args',
+ newVmPxeLabel: 'PXE',
+ newVmInterfacesPanel: 'Interfaces',
+ newVmMacLabel: 'MAC',
+ newVmAddInterface: 'Add interface',
+ newVmDisksPanel: 'Disks',
+ newVmSrLabel: 'SR',
+ newVmSizeLabel: 'Size',
+ newVmAddDisk: 'Add disk',
+ newVmSummaryPanel: 'Summary',
+ newVmCreate: 'Create',
+ newVmReset: 'Reset',
+ newVmSelectTemplate: 'Select template',
+ newVmSshKey: 'SSH key',
+ newVmConfigDrive: 'Config drive',
+ newVmCustomConfig: 'Custom config',
+ newVmBootAfterCreate: 'Boot VM after creation',
+ newVmMacPlaceholder: 'Auto-generated if empty',
+ newVmCpuWeightLabel: 'CPU weight',
+ newVmDefaultCpuWeight: 'Default: {value, number}',
+ newVmCpuCapLabel: 'CPU cap',
+ newVmDefaultCpuCap: 'Default: {value, number}',
+ newVmCloudConfig: 'Cloud config',
+ newVmCreateVms: 'Create VMs',
+ newVmCreateVmsConfirm: 'Are you sure you want to create {nbVms, number} VMs?',
+ newVmMultipleVms: 'Multiple VMs:',
+ newVmSelectResourceSet: 'Select a resource set:',
+ newVmMultipleVmsPattern: 'Name pattern:',
+ newVmMultipleVmsPatternPlaceholder: 'e.g.: \\{name\\}_%',
+ newVmFirstIndex: 'First index:',
+ newVmNumberRecalculate: 'Recalculate VMs number',
+ newVmNameRefresh: 'Refresh VMs name',
+ newVmAffinityHost: 'Affinity host',
+ newVmAdvancedPanel: 'Advanced',
+ newVmShowAdvanced: 'Show advanced settings',
+ newVmHideAdvanced: 'Hide advanced settings',
+ newVmShare: 'Share this VM',
+
+ // ----- Self -----
+ resourceSets: 'Resource sets',
+ noResourceSets: 'No resource sets.',
+ loadingResourceSets: 'Loading resource sets',
+ resourceSetName: 'Resource set name',
+ resourceSetUsers: 'Users',
+ resourceSetPools: 'Pools',
+ resourceSetTemplates: 'Templates',
+ resourceSetSrs: 'SRs',
+ resourceSetNetworks: 'Networks',
+ recomputeResourceSets: 'Recompute all limits',
+ saveResourceSet: 'Save',
+ resetResourceSet: 'Reset',
+ editResourceSet: 'Edit',
+ deleteResourceSet: 'Delete',
+ deleteResourceSetWarning: 'Delete resource set',
+ deleteResourceSetQuestion:
+ 'Are you sure you want to delete this resource set?',
+ resourceSetMissingObjects: 'Missing objects:',
+ resourceSetVcpus: 'vCPUs',
+ resourceSetMemory: 'Memory',
+ resourceSetStorage: 'Storage',
+ unknownResourceSetValue: 'Unknown',
+ availableHosts: 'Available hosts',
+ excludedHosts: 'Excluded hosts',
+ noHostsAvailable: 'No hosts available.',
+ availableHostsDescription:
+ 'VMs created from this resource set shall run on the following hosts.',
+ maxCpus: 'Maximum CPUs',
+ maxRam: 'Maximum RAM',
+ maxDiskSpace: 'Maximum disk space',
+ ipPool: 'IP pool',
+ quantity: 'Quantity',
+ noResourceSetLimits: 'No limits.',
+ remainingResource: 'Remaining:',
+ usedResourceLabel: 'Used',
+ availableResourceLabel: 'Available',
+ resourceSetQuota: 'Used: {usage} (Total: {total})',
+ resourceSetNew: 'New',
+
+ // ---- VM import ---
+ 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:',
+ vmsToImport: 'VMs to import',
+ importVmsCleanList: 'Reset',
+ vmImportSuccess: 'VM import success',
+ vmImportFailed: 'VM import failed',
+ startVmImport: 'Import starting…',
+ startVmExport: 'Export starting…',
+ nCpus: 'N CPUs',
+ vmMemory: 'Memory',
+ diskInfo: 'Disk {position} ({capacity})',
+ diskDescription: 'Disk description',
+ noDisks: 'No disks.',
+ noNetworks: 'No networks.',
+ networkInfo: 'Network {name}',
+ noVmImportErrorDescription: 'No description available',
+ vmImportError: 'Error:',
+ vmImportFileType: '{type} file:',
+ vmImportConfigAlert: 'Please to check and/or modify the VM configuration.',
+
+ // ---- Tasks ---
+ noTasks: 'No pending tasks',
+ xsTasks: 'Currently, there are not any pending XenServer tasks',
+ cancelTask: 'Cancel',
+ destroyTask: 'Destroy',
+ cancelTasks: 'Cancel selected tasks',
+ destroyTasks: 'Destroy selected tasks',
+ pool: 'Pool',
+ task: 'Task',
+ progress: 'Progress',
+
+ // ---- Backup views ---
+ backupSchedules: 'Schedules',
+ getRemote: 'Get remote',
+ listRemote: 'List Remote',
+ simpleBackup: 'simple',
+ 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',
+ remoteEnabled: 'Enabled',
+ remoteError: 'Error',
+ noBackup: 'No backup available',
+ backupVmNameColumn: 'VM Name',
+ backupTags: 'Tags',
+ lastBackupColumn: 'Last Backup',
+ availableBackupsColumn: 'Available Backups',
+ backupRestoreErrorTitle: 'Missing parameters',
+ backupRestoreErrorMessage: 'Choose a SR and a backup',
+ backupRestoreSelectDefaultSr: 'Select default SR…',
+ backupRestoreChooseSrForEachVdis: 'Choose a SR for each VDI',
+ backupRestoreVdiLabel: 'VDI',
+ backupRestoreSrLabel: 'SR',
+ displayBackup: 'Display backups',
+ importBackupTitle: 'Import VM',
+ importBackupMessage: 'Starting your backup import',
+ vmsToBackup: 'VMs to backup',
+
+ // ----- Restore files view -----
+ listRemoteBackups: 'List remote backups',
+ restoreFiles: 'Restore backup files',
+ restoreFilesError: 'Invalid options',
+ restoreFilesFromBackup: 'Restore file from {name}',
+ restoreFilesSelectBackup: 'Select a backup…',
+ restoreFilesSelectDisk: 'Select a disk…',
+ restoreFilesSelectPartition: 'Select a partition…',
+ restoreFilesSelectFolderPath: 'Folder path',
+ restoreFilesSelectFiles: 'Select a file…',
+ restoreFileContentNotFound: 'Content not found',
+ restoreFilesNoFilesSelected: 'No files selected',
+ restoreFilesSelectedFiles: 'Selected files ({files}):',
+ restoreFilesDiskError: 'Error while scanning disk',
+ restoreFilesSelectAllFiles: "Select all this folder's files",
+ restoreFilesUnselectAll: 'Unselect all files',
+
+ // ----- Modals -----
+ emergencyShutdownHostsModalTitle:
+ 'Emergency shutdown Host{nHosts, plural, one {} other {s}}',
+ emergencyShutdownHostsModalMessage:
+ 'Are you sure you want to shutdown {nHosts, 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",
+ 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}}?',
+ restartHostsModalTitle: 'Restart 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',
+ 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}}?',
+ stopVmsModalTitle: 'Stop 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}?',
+ suspendVmsModalTitle: 'Suspend VM{vms, plural, one {} other {s}}',
+ suspendVmsModalMessage:
+ 'Are you sure you want to suspend {vms, number} VM{vms, plural, one {} other {s}}?',
+ restartVmsModalTitle: 'Restart 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}}?',
+ 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',
+ deleteVmsConfirmText:
+ 'delete {nVms, number} vm{nVms, plural, one {} other {s}}',
+ deleteVmModalTitle: 'Delete VM',
+ deleteVmModalMessage:
+ 'Are you sure you want to delete this VM? ALL VM DISKS WILL BE REMOVED',
+ deleteVmBlockedModalTitle: 'Blocked operation',
+ deleteVmBlockedModalMessage:
+ 'Removing the VM is a blocked operation. Would you like to remove it anyway?',
+ migrateVmModalTitle: 'Migrate VM',
+ migrateVmSelectHost: 'Select a destination host:',
+ migrateVmSelectMigrationNetwork: 'Select a migration network:',
+ migrateVmSelectNetworks: 'For each VIF, select a network:',
+ migrateVmsSelectSr: 'Select a destination SR:',
+ migrateVmsSelectSrIntraPool: 'Select a destination SR for local disks:',
+ migrateVmsSelectNetwork: 'Select a network on which to connect each VIF:',
+ migrateVmsSmartMapping: 'Smart mapping',
+ migrateVmVif: 'VIF',
+ migrateVmNetwork: 'Network',
+ migrateVmNoTargetHost: 'No target host',
+ migrateVmNoTargetHostMessage: 'A target host is required to migrate a VM',
+ migrateVmNoDefaultSrError: 'No default SR',
+ migrateVmNotConnectedDefaultSrError: 'Default SR not connected to host',
+ chooseSrForEachVdisModalSelectSr: 'For each VDI, select an SR:',
+ chooseSrForEachVdisModalMainSr: 'Select main SR…',
+ chooseSrForEachVdisModalVdiLabel: 'VDI',
+ 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',
+ 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',
+ deleteSchedulesModalTitle:
+ 'Delete schedule{nSchedules, plural, one {} other {s}}',
+ deleteSchedulesModalMessage:
+ 'Are you sure you want to delete {nSchedules, number} schedule{nSchedules, plural, one {} other {s}}?',
+ deleteRemotesModalTitle: 'Delete remote{nRemotes, plural, one {} other {s}}',
+ deleteRemotesModalMessage:
+ 'Are you sure you want to delete {nRemotes, number} remote{nRemotes, plural, one {} other {s}}?',
+ revertVmModalTitle: 'Revert your VM',
+ deleteVifsModalTitle: 'Delete VIF{nVifs, plural, one {} other {s}}',
+ deleteVifsModalMessage:
+ 'Are you sure you want to delete {nVifs, number} VIF{nVifs, plural, one {} other {s}}?',
+ deleteSnapshotModalTitle: 'Delete snapshot',
+ deleteSnapshotModalMessage: 'Are you sure you want to delete this snapshot?',
+ deleteSnapshotsModalTitle: 'Delete snapshot{nVms, plural, one {} other {s}}',
+ deleteSnapshotsModalMessage:
+ 'Are you sure you want to delete {nVms, number} snapshot{nVms, plural, one {} other {s}}?',
+ 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?',
+ 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.',
+ 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.',
+ alreadyRegisteredModal: 'Replace current 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!',
+ cancelTasksModalTitle: 'Cancel task{nTasks, plural, one {} other {s}}',
+ cancelTasksModalMessage:
+ 'Are you sure you want to cancel {nTasks, number} task{nTasks, plural, one {} other {s}}?',
+ destroyTasksModalTitle: 'Destroy task{nTasks, plural, one {} other {s}}',
+ destroyTasksModalMessage:
+ 'Are you sure you want to destroy {nTasks, number} task{nTasks, plural, one {} other {s}}?',
+
+ // ----- Servers -----
+ serverLabel: 'Label',
+ serverHost: 'Host',
+ serverUsername: 'Username',
+ serverPassword: 'Password',
+ serverAction: 'Action',
+ 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.",
+ serverDisconnect: 'Disconnect server',
+ serverPlaceHolderUser: 'username',
+ serverPlaceHolderPassword: 'password',
+ serverPlaceHolderAddress: 'address[:port]',
+ serverPlaceHolderLabel: 'label',
+ serverConnect: 'Connect',
+ serverError: 'Error',
+ serverAddFailed: 'Adding server failed',
+ serverStatus: 'Status',
+ serverConnectionFailed: 'Connection failed. Click for more information.',
+ 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?',
+
+ // ----- Copy VM -----
+ copyVm: 'Copy VM',
+ copyVmConfirm: 'Are you sure you want to copy this VM to {SR}?',
+ copyVmName: 'Name',
+ copyVmNamePattern: 'Name pattern',
+ copyVmNamePlaceholder: 'If empty: name of the copied VM',
+ copyVmNamePatternPlaceholder: 'e.g.: "\\{name\\}_COPY"',
+ copyVmSelectSr: 'Select SR',
+ copyVmCompress: 'Use compression',
+ copyVmsNoTargetSr: 'No target SR',
+ copyVmsNoTargetSrMessage: 'A target SR is required to copy a VM',
+
+ // ----- 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.',
+ 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.",
+ 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',
+ newNetworkInterface: 'Interface',
+ newNetworkName: 'Name',
+ newNetworkDescription: 'Description',
+ newNetworkVlan: 'VLAN',
+ newNetworkDefaultVlan: 'No VLAN if empty',
+ newNetworkMtu: 'MTU',
+ newNetworkDefaultMtu: 'Default: 1500',
+ newNetworkNoNameErrorTitle: 'Name required',
+ newNetworkNoNameErrorMessage: 'A name is required to create a network',
+ newNetworkBondMode: 'Bond mode',
+ deleteNetwork: 'Delete network',
+ deleteNetworkConfirm: 'Are you sure you want to delete this network?',
+ networkInUse: 'This network is currently in use',
+ pillBonded: 'Bonded',
+
+ // ----- Add host -----
+ addHostSelectHost: 'Host',
+ addHostNoHost: 'No host',
+ addHostNoHostMessage: 'No host selected to be added',
+
+ // ----- About View -----
+ xenOrchestra: 'Xen Orchestra',
+ xenOrchestraServer: 'Xen Orchestra server',
+ xenOrchestraWeb: 'Xen Orchestra web client',
+ noProSupport: 'No pro support provided!',
+ noProductionUse: 'Use in production at your own risks',
+ downloadXoaFromWebsite: 'You can download our turnkey appliance at {website}',
+ bugTracker: 'Bug Tracker',
+ bugTrackerText: 'Issues? Report it!',
+ community: 'Community',
+ communityText: 'Join our community forum!',
+ freeTrial: 'Free Trial for Premium Edition!',
+ freeTrialNow: 'Request your trial now!',
+ issues: 'Any issue?',
+ issuesText: 'Problem? Contact us!',
+ documentation: 'Documentation',
+ documentationText: 'Read our official doc',
+ proSupportIncluded: 'Pro support included',
+ xoAccount: 'Access your XO Account',
+ openTicket: 'Report a problem',
+ openTicketText: 'Problem? Open a ticket!',
+
+ // ----- Upgrade Panel -----
+ upgradeNeeded: 'Upgrade needed',
+ upgradeNow: 'Upgrade now!',
+ 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.',
+
+ // ----- Updates View -----
+ updateTitle: 'Updates',
+ registration: 'Registration',
+ trial: 'Trial',
+ settings: 'Settings',
+ proxySettings: 'Proxy settings',
+ proxySettingsHostPlaceHolder: 'Host (myproxy.example.org)',
+ proxySettingsPortPlaceHolder: 'Port (eg: 3128)',
+ proxySettingsUsernamePlaceHolder: 'Username',
+ proxySettingsPasswordPlaceHolder: 'Password',
+ updateRegistrationEmailPlaceHolder: 'Your email account',
+ updateRegistrationPasswordPlaceHolder: 'Your password',
+ updaterTroubleshootingLink: 'Troubleshooting documentation',
+ update: 'Update',
+ refresh: 'Refresh',
+ upgrade: 'Upgrade',
+ noUpdaterCommunity: 'No updater available for Community Edition',
+ 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.',
+ 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.',
+ 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",
+ promptUpgradeReloadTitle: 'Upgrade successful',
+ 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.',
+
+ // ----- PIF -----
+ connectPif: 'Connect PIF',
+ connectPifConfirm: 'Are you sure you want to connect this PIF?',
+ disconnectPif: 'Disconnect PIF',
+ disconnectPifConfirm: 'Are you sure you want to disconnect this PIF?',
+ deletePif: 'Delete PIF',
+ deletePifConfirm: 'Are you sure you want to delete this PIF?',
+ deletePifs: 'Delete PIFs',
+ deletePifsConfirm:
+ 'Are you sure you want to delete {nPifs, number} PIF{nPifs, plural, one {} other {s}}?',
+ pifConnected: 'Connected',
+ pifDisconnected: 'Disconnected',
+ pifPhysicallyConnected: 'Physically connected',
+ pifPhysicallyDisconnected: 'Physically disconnected',
+
+ // ----- User -----
+ username: 'Username',
+ password: 'Password',
+ language: 'Language',
+ oldPasswordPlaceholder: 'Old password',
+ newPasswordPlaceholder: 'New password',
+ confirmPasswordPlaceholder: 'Confirm new password',
+ confirmationPasswordError: 'Confirmation password incorrect',
+ 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.',
+ changePasswordOk: 'OK',
+ sshKeys: 'SSH keys',
+ newSshKey: 'New SSH key',
+ deleteSshKey: 'Delete',
+ deleteSshKeys: 'Delete selected SSH keys',
+ noSshKeys: 'No SSH keys',
+ newSshKeyModalTitle: 'New SSH key',
+ sshKeyErrorTitle: 'Invalid key',
+ sshKeyErrorMessage: 'An SSH key requires both a title and a key.',
+ title: 'Title',
+ key: 'Key',
+ deleteSshKeyConfirm: 'Delete SSH key',
+ deleteSshKeyConfirmMessage:
+ 'Are you sure you want to delete the SSH key {title}?',
+ deleteSshKeysConfirm: 'Delete SSH key{nKeys, plural, one {} other {s}}',
+ deleteSshKeysConfirmMessage:
+ 'Are you sure you want to delete {nKeys, number} SSH key{nKeys, plural, one {} other {s}}?',
+
+ // ----- Usage -----
+ others: 'Others',
+
+ // ----- Logs -----
+ loadingLogs: 'Loading logs…',
+ logUser: 'User',
+ logMethod: 'Method',
+ logParams: 'Params',
+ logMessage: 'Message',
+ logError: 'Error',
+ logDisplayDetails: 'Display details',
+ logTime: 'Date',
+ logNoStackTrace: 'No stack trace',
+ logNoParams: 'No params',
+ logDelete: 'Delete log',
+ logsDelete: 'Delete logs',
+ logDeleteMultiple: 'Delete log{nLogs, plural, one {} other {s}}',
+ logDeleteMultipleMessage:
+ 'Are you sure you want to delete {nLogs, number} log{nLogs, plural, one {} other {s}}?',
+ logDeleteAll: 'Delete all logs',
+ logDeleteAllTitle: 'Delete all logs',
+ logDeleteAllMessage: 'Are you sure you want to delete all the logs?',
+ logIndicationToEnable: 'Click to enable',
+ logIndicationToDisable: 'Click to disable',
+ reportBug: 'Report a bug',
+ unhealthyVdiChainError: 'Job canceled to protect the VDI chain',
+ clickForMoreInformation: 'Click for more information',
+
+ // ----- IPs ------
+ ipPoolName: 'Name',
+ ipPoolIps: 'IPs',
+ ipPoolIpsPlaceholder: 'IPs (e.g.: 1.0.0.12-1.0.0.17;1.0.0.23)',
+ ipPoolNetworks: 'Networks',
+ ipsNoIpPool: 'No IP pools',
+ ipsCreate: 'Create',
+ ipsDeleteAllTitle: 'Delete all IP pools',
+ ipsDeleteAllMessage: 'Are you sure you want to delete all the IP pools?',
+ ipsVifs: 'VIFs',
+ ipsNotUsed: 'Not used',
+ ipPoolUnknownVif: 'unknown VIF',
+ ipPoolNameAlreadyExists: 'Name already exists',
+
+ // ----- Shortcuts -----
+ shortcutModalTitle: 'Keyboard shortcuts',
+ shortcut_XoApp: 'Global',
+ 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_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',
+ settingsAclsButtonTooltiphost: 'Hosts',
+ settingsAclsButtonTooltippool: 'Pool',
+ settingsAclsButtonTooltipSR: 'SR',
+ settingsAclsButtonTooltipnetwork: 'Network',
+
+ // ----- Config -----
+ noConfigFile: 'No config file selected',
+ importTip:
+ 'Try dropping a config file here, or click to select a config file to upload.',
+ config: 'Config',
+ importConfig: 'Import',
+ importConfigSuccess: 'Config file successfully imported',
+ importConfigError: 'Error while importing config file',
+ exportConfig: 'Export',
+ downloadConfig: 'Download current config',
+ noConfigImportCommunity: 'No config import available for Community Edition',
+
+ // ----- SR -----
+ srReconnectAllModalTitle: 'Reconnect all hosts',
+ srReconnectAllModalMessage: 'This will reconnect this SR to all its hosts.',
+ srsReconnectAllModalMessage:
+ 'This will reconnect each selected SR to its host (local SR) or to every hosts of its pool (shared SR).',
+ srDisconnectAllModalTitle: 'Disconnect all hosts',
+ srDisconnectAllModalMessage:
+ 'This will disconnect this SR from all its hosts.',
+ srsDisconnectAllModalMessage:
+ 'This will disconnect each selected SR from its host (local SR) or from every hosts of its pool (shared SR).',
+ srForgetModalTitle: 'Forget SR',
+ srsForgetModalTitle: 'Forget selected SRs',
+ srForgetModalMessage:
+ "Are you sure you want to forget this SR? VDIs on this storage won't be removed.",
+ srsForgetModalMessage:
+ "Are you sure you want to forget all the selected SRs? VDIs on these storages won't be removed.",
+ srAllDisconnected: 'Disconnected',
+ srSomeConnected: 'Partially connected',
+ srAllConnected: 'Connected',
+
+ // ----- XOSAN -----
+ xosanTitle: 'XOSAN',
+ xosanSrTitle: 'Xen Orchestra SAN SR',
+ xosanAvailableSrsTitle: 'Select local SRs (lvm)',
+ xosanSuggestions: 'Suggestions',
+ xosanDisperseWarning:
+ 'Warning: using disperse layout is not recommended right now. Please read {link}.',
+ xosanName: 'Name',
+ xosanHost: 'Host',
+ xosanHosts: 'Connected Hosts',
+ xosanPool: 'Pool',
+ xosanVolumeId: 'Volume ID',
+ xosanSize: 'Size',
+ xosanUsedSpace: 'Used space',
+ xosanLicense: 'License',
+ xosanMultipleLicenses: 'This XOSAN has more than 1 license!',
+ 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',
+ xosanRestartAgents: 'Restart toolstacks',
+ xosanMasterOffline: 'Pool master is not running',
+ xosanInstallPackTitle: 'Install XOSAN pack on {pool}',
+ xosanSelect2Srs: 'Select at least 2 SRs',
+ xosanLayout: 'Layout',
+ xosanRedundancy: 'Redundancy',
+ xosanCapacity: 'Capacity',
+ xosanAvailableSpace: 'Available space',
+ xosanDiskLossLegend: '* Can fail without data loss',
+ xosanCreate: 'Create',
+ 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',
+ xosanRegister: 'Register your appliance first',
+ xosanLoading: 'Loading…',
+ xosanNotAvailable: 'XOSAN is not available at the moment',
+ 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:',
+ // 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.',
+
+ // Licenses
+ licensesTitle: 'Licenses',
+ xosanUnregisteredDisclaimer:
+ 'You are not registered and therefore will not be able to create or manage your XOSAN SRs. {link}',
+ xosanSourcesDisclaimer:
+ 'In order to create a XOSAN SR, you need to use the Xen Orchestra Appliance and buy a XOSAN license on {link}.',
+ registerNow: 'Register now!',
+ licensesUnregisteredDisclaimer:
+ 'You need to register your appliance to manage your licenses.',
+ licenseProduct: 'Product',
+ licenseBoundObject: 'Attached to',
+ licensePurchaser: 'Purchaser',
+ licenseExpires: 'Expires',
+ licensePurchaserYou: 'You',
+ productSupport: 'Support',
+ licenseNotBoundXosan: 'No XOSAN attached',
+ licenseBoundUnknownXosan: 'License attached to an unknown XOSAN',
+ licensesManage: 'Manage the licenses',
+ newLicense: 'New license',
+ refreshLicenses: 'Refresh',
+ xosanLicenseRestricted: 'Limited size because XOSAN is in trial',
+ xosanAdminNoLicenseDisclaimer:
+ 'You need a license on this SR to manage the XOSAN.',
+ xosanAdminExpiredLicenseDisclaimer:
+ 'Your XOSAN license has expired. You can still use the SR but cannot administrate it anymore.',
+ xosanCheckLicenseError: 'Could not check the license on this XOSAN SR',
+ xosanGetLicensesError: 'Could not fetch licenses',
+ xosanLicenseHasExpired: 'License has expired.',
+ xosanLicenseExpiresDate: 'License expires on {date}.',
+ xosanUpdateLicenseMessage: 'Update the license now!',
+ xosanUnknownSr: 'Unknown XOSAN SR.',
+ contactUs: 'Contact us!',
+ xosanNoLicense: 'No license.',
+ xosanUnlockNow: 'Unlock now!',
+ xosanBetaOverMessage:
+ 'XOSAN Beta is over. You may now delete and recreate previous existing XOSAN SRs.',
+ selectLicense: 'Select a license',
+ bindLicense: 'Bind license',
+ expiresOn: 'expires on {date}',
+ xosanInstallXoaPlugin: 'Install XOA plugin first',
+ xosanLoadXoaPlugin: 'Load XOA plugin first',
+
+ // ----- Utils -----
+ durationFormat:
+ '{days, plural, =0 {} one {# day } other {# days }}{hours, plural, =0 {} one {# hour } other {# hours }}{minutes, plural, =0 {} one {# minute } other {# minutes }}{seconds, plural, =0 {} one {# second} other {# seconds}}',
+}
+forEach(messages, function (message, id) {
+ if (isString(message)) {
+ messages[id] = {
+ id,
+ defaultMessage: message,
+ }
+ } else if (!message.id) {
+ message.id = id
+ }
+})
+
+module.exports = messages
diff --git a/packages/xo-web/src/common/invoke.js b/packages/xo-web/src/common/invoke.js
new file mode 100644
index 000000000..af9e6f35e
--- /dev/null
+++ b/packages/xo-web/src/common/invoke.js
@@ -0,0 +1,33 @@
+// Invoke a function and returns it result.
+// All parameters are forwarded.
+//
+// Why using `invoke()`?
+// - avoid tedious IIFE syntax
+// - avoid declaring variables in the common scope
+// - monkey-patching
+//
+// ```js
+// const sum = invoke(1, 2, (a, b) => a + b)
+//
+// eventEmitter.emit = invoke(eventEmitter.emit, emit => function (event) {
+// if (event === 'foo') {
+// throw new Error('event foo is disabled')
+// }
+//
+// return emit.apply(this, arguments)
+// })
+// ```
+export default function invoke (fn) {
+ const n = arguments.length - 1
+ if (!n) {
+ return fn()
+ }
+
+ fn = arguments[n]
+ const args = new Array(n)
+ for (let i = 0; i < n; ++i) {
+ args[i] = arguments[i]
+ }
+
+ return fn.apply(undefined, args)
+}
diff --git a/packages/xo-web/src/common/ip.js b/packages/xo-web/src/common/ip.js
new file mode 100644
index 000000000..6278287d9
--- /dev/null
+++ b/packages/xo-web/src/common/ip.js
@@ -0,0 +1,136 @@
+import forEachRight from 'lodash/forEachRight'
+import forEach from 'lodash/forEach'
+import isArray from 'lodash/isArray'
+import isIp from 'is-ip'
+import some from 'lodash/some'
+
+export { isIp }
+export const isIpV4 = isIp.v4
+export const isIpV6 = isIp.v6
+
+// Source: https://github.com/ezpaarse-project/ip-range-generator/blob/master/index.js
+
+const ipv4 = /^(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(?:\.(?!$)|$)){4}$/
+
+function ip2hex (ip) {
+ const parts = ip.split('.').map(str => parseInt(str, 10))
+ let n = 0
+
+ n += parts[3]
+ n += parts[2] * 256 // 2^8
+ n += parts[1] * 65536 // 2^16
+ n += parts[0] * 16777216 // 2^24
+
+ return n
+}
+
+function assertIpv4 (str, msg) {
+ if (!ipv4.test(str)) {
+ throw new Error(msg)
+ }
+}
+
+function * range (ip1, ip2) {
+ assertIpv4(ip1, 'argument "ip1" must be a valid IPv4 address')
+ assertIpv4(ip2, 'argument "ip2" must be a valid IPv4 address')
+
+ let hex = ip2hex(ip1)
+ let hex2 = ip2hex(ip2)
+
+ if (hex > hex2) {
+ const tmp = hex
+ hex = hex2
+ hex2 = tmp
+ }
+
+ for (let i = hex; i <= hex2; i++) {
+ yield `${(i >> 24) & 0xff}.${(i >> 16) & 0xff}.${(i >> 8) & 0xff}.${i &
+ 0xff}`
+ }
+}
+
+// -----------------------------------------------------------------------------
+
+export const getNextIpV4 = ip => {
+ const splitIp = ip.split('.')
+ if (
+ splitIp.length !== 4 ||
+ some(splitIp, value => value < 0 || value > 255)
+ ) {
+ return
+ }
+ let index
+ forEachRight(splitIp, (value, i) => {
+ if (value < 255) {
+ index = i
+ return false
+ }
+ splitIp[i] = 1
+ })
+ if (index === 0 && +splitIp[0] === 255) {
+ return 0
+ }
+ splitIp[index]++
+
+ return splitIp.join('.')
+}
+
+export const formatIps = ips => {
+ if (!isArray(ips)) {
+ throw new Error('ips must be an array')
+ }
+ if (ips.length === 0) {
+ return []
+ }
+ const sortedIps = ips.sort((ip1, ip2) => {
+ const splitIp1 = ip1.split('.')
+ const splitIp2 = ip2.split('.')
+ if (splitIp1.length !== 4) {
+ return 1
+ }
+ if (splitIp2.length !== 4) {
+ return -1
+ }
+ return (
+ splitIp1[3] -
+ splitIp2[3] +
+ (splitIp1[2] - splitIp2[2]) * 256 +
+ (splitIp1[1] - splitIp2[1]) * 256 * 256 +
+ (splitIp1[0] - splitIp2[0]) * 256 * 256 * 256
+ )
+ })
+ const range = { first: '', last: '' }
+ const formattedIps = []
+ let index = 0
+ forEach(sortedIps, ip => {
+ if (ip !== getNextIpV4(range.last)) {
+ if (range.first) {
+ formattedIps[index] =
+ range.first === range.last ? range.first : { ...range }
+ index++
+ }
+ range.first = range.last = ip
+ } else {
+ range.last = ip
+ }
+ })
+ formattedIps[index] = range.first === range.last ? range.first : range
+
+ return formattedIps
+}
+
+export const parseIpPattern = pattern => {
+ const ips = []
+ forEach(pattern.split(';'), rawIpRange => {
+ const ipRange = rawIpRange.split('-')
+ if (ipRange.length < 2) {
+ ips.push(ipRange[0])
+ } else if (!isIpV4(ipRange[0]) || !isIpV4(ipRange[1])) {
+ ips.push(rawIpRange)
+ } else {
+ ips.push(...range(ipRange[0], ipRange[1]))
+ }
+ })
+
+ return ips
+}
diff --git a/packages/xo-web/src/common/iso-device.js b/packages/xo-web/src/common/iso-device.js
new file mode 100644
index 000000000..0966434dc
--- /dev/null
+++ b/packages/xo-web/src/common/iso-device.js
@@ -0,0 +1,103 @@
+import React from 'react'
+
+import _ from 'intl'
+import ActionButton from './action-button'
+import Component from './base-component'
+import Icon from 'icon'
+import propTypes from './prop-types-decorator'
+import Tooltip from 'tooltip'
+import { alert } from 'modal'
+import { connectStore } from './utils'
+import { SelectVdi } from './select-objects'
+import {
+ createGetObjectsOfType,
+ createFinder,
+ createGetObject,
+ createSelector,
+} from './selectors'
+import { ejectCd, insertCd } from './xo'
+
+@propTypes({
+ vm: propTypes.object.isRequired,
+})
+@connectStore(() => {
+ const getCdDrive = createFinder(
+ 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
+ }
+ })
+
+ return {
+ cdDrive: getCdDrive,
+ mountedIso: getMountedIso,
+ }
+})
+export default class IsoDevice extends Component {
+ _getPredicate = createSelector(
+ () => this.props.vm.$pool,
+ () => this.props.vm.$container,
+ (vmPool, vmContainer) => sr => {
+ const vmRunning = vmContainer !== vmPool
+ const sameHost = vmContainer === sr.$container
+ const samePool = vmPool === sr.$pool
+
+ return (
+ samePool &&
+ (vmRunning ? sr.shared || sameHost : true) &&
+ (sr.SR_type === 'iso' || (sr.SR_type === 'udev' && sr.size))
+ )
+ }
+ )
+
+ _handleInsert = iso => {
+ const { vm } = this.props
+
+ if (iso) {
+ insertCd(vm, iso.id, true)
+ } else {
+ ejectCd(vm)
+ }
+ }
+
+ _handleEject = () => ejectCd(this.props.vm)
+
+ _showWarning = () => alert(_('cdDriveNotInstalled'), _('cdDriveInstallation'))
+
+ render () {
+ const { cdDrive, mountedIso } = this.props
+
+ return (
+
+
+
+
+
+ {mountedIso &&
+ !cdDrive.device && (
+
+
+
+
+
+ )}
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/common/json-schema-input/array-input.js b/packages/xo-web/src/common/json-schema-input/array-input.js
new file mode 100644
index 000000000..30f5c9577
--- /dev/null
+++ b/packages/xo-web/src/common/json-schema-input/array-input.js
@@ -0,0 +1,126 @@
+import React from 'react'
+import uncontrollableInput from 'uncontrollable-input'
+import { filter, map } from 'lodash'
+
+import _ from '../intl'
+import Button from '../button'
+import Component from '../base-component'
+import propTypes from '../prop-types-decorator'
+import { EMPTY_ARRAY } from '../utils'
+
+import GenericInput from './generic-input'
+import { descriptionRender, forceDisplayOptionalAttr } from './helpers'
+
+@propTypes({
+ depth: propTypes.number,
+ disabled: propTypes.bool,
+ label: propTypes.any.isRequired,
+ required: propTypes.bool,
+ schema: propTypes.object.isRequired,
+ uiSchema: propTypes.object,
+})
+@uncontrollableInput()
+export default class ObjectInput extends Component {
+ state = {
+ use: this.props.required || forceDisplayOptionalAttr(this.props),
+ }
+
+ _onAddItem = () => {
+ const { props } = this
+ props.onChange((props.value || EMPTY_ARRAY).concat(undefined))
+ }
+
+ _onChangeItem = (value, name) => {
+ const key = Number(name)
+
+ const { props } = this
+ const newValue = (props.value || EMPTY_ARRAY).slice()
+ newValue[key] = value
+ props.onChange(newValue)
+ }
+
+ _onRemoveItem = key => {
+ const { props } = this
+ props.onChange(filter(props.value, (_, i) => i !== key))
+ }
+
+ render () {
+ const {
+ props: {
+ depth = 0,
+ disabled,
+ label,
+ required,
+ schema,
+ uiSchema,
+ value = EMPTY_ARRAY,
+ },
+ state: { use },
+ } = this
+
+ const childDepth = depth + 2
+ const itemSchema = schema.items
+ const itemUiSchema = uiSchema && uiSchema.items
+
+ const itemLabel = itemSchema.title || _('item')
+
+ return (
+
+
{label}
+ {descriptionRender(schema.description)}
+
+ {!required && (
+
+
+ {' '}
+ {_('fillOptionalInformations')}
+
+
+ )}
+ {use && (
+
+
+ {map(value, (value, key) => (
+
+
+ this._onRemoveItem(key)}
+ >
+ {_('remove')}
+
+
+ ))}
+
+
+ {_('add')}
+
+
+ )}
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/common/json-schema-input/boolean-input.js b/packages/xo-web/src/common/json-schema-input/boolean-input.js
new file mode 100644
index 000000000..701d0d67b
--- /dev/null
+++ b/packages/xo-web/src/common/json-schema-input/boolean-input.js
@@ -0,0 +1,24 @@
+import React from 'react'
+
+import uncontrollableInput from 'uncontrollable-input'
+import Component from '../base-component'
+import { Toggle } from '../form'
+
+import { PrimitiveInputWrapper } from './helpers'
+
+// ===================================================================
+
+@uncontrollableInput()
+export default class BooleanInput extends Component {
+ render () {
+ const { disabled, onChange, value, ...props } = this.props
+
+ return (
+
+
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/common/json-schema-input/enum-input.js b/packages/xo-web/src/common/json-schema-input/enum-input.js
new file mode 100644
index 000000000..db900b185
--- /dev/null
+++ b/packages/xo-web/src/common/json-schema-input/enum-input.js
@@ -0,0 +1,56 @@
+import _ from 'intl'
+import uncontrollableInput from 'uncontrollable-input'
+import Component from 'base-component'
+import React from 'react'
+import { createSelector } from 'reselect'
+import { findIndex, map } from 'lodash'
+
+import { PrimitiveInputWrapper } from './helpers'
+
+// ===================================================================
+
+@uncontrollableInput()
+export default class EnumInput extends Component {
+ _getSelectedIndex = createSelector(
+ () => this.props.schema.enum,
+ () => {
+ const { schema, value = schema.default } = this.props
+ return value
+ },
+ (enumValues, value) => {
+ const index = findIndex(enumValues, current => current === value)
+ return index === -1 ? '' : index
+ }
+ )
+
+ _onChange = event => {
+ this.props.onChange(this.props.schema.enum[event.target.value])
+ }
+
+ render () {
+ const {
+ disabled,
+ schema: { enum: enumValues, enumNames = enumValues },
+ required,
+ } = this.props
+
+ return (
+
+
+ {_('noSelectedValue', message => {message} )}
+ {map(enumNames, (name, index) => (
+
+ {name}
+
+ ))}
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/common/json-schema-input/generic-input.js b/packages/xo-web/src/common/json-schema-input/generic-input.js
new file mode 100644
index 000000000..b3c3ccb11
--- /dev/null
+++ b/packages/xo-web/src/common/json-schema-input/generic-input.js
@@ -0,0 +1,76 @@
+import React, { Component } from 'react'
+
+import getEventValue from '../get-event-value'
+import propTypes from '../prop-types-decorator'
+import uncontrollableInput from 'uncontrollable-input'
+import { EMPTY_OBJECT } from '../utils'
+
+import ArrayInput from './array-input'
+import BooleanInput from './boolean-input'
+import EnumInput from './enum-input'
+import IntegerInput from './integer-input'
+import NumberInput from './number-input'
+import ObjectInput from './object-input'
+import StringInput from './string-input'
+
+import { getType } from './helpers'
+
+// ===================================================================
+
+const InputByType = {
+ array: ArrayInput,
+ boolean: BooleanInput,
+ integer: IntegerInput,
+ number: NumberInput,
+ object: ObjectInput,
+ string: StringInput,
+}
+
+// ===================================================================
+
+@propTypes({
+ depth: propTypes.number,
+ disabled: propTypes.bool,
+ label: propTypes.any.isRequired,
+ required: propTypes.bool,
+ schema: propTypes.object.isRequired,
+ uiSchema: propTypes.object,
+})
+@uncontrollableInput()
+export default class GenericInput extends Component {
+ _onChange = event => {
+ const { name, onChange } = this.props
+ onChange && onChange(getEventValue(event), name)
+ }
+
+ render () {
+ const {
+ schema,
+ value = schema.default,
+ uiSchema = EMPTY_OBJECT,
+ ...opts
+ } = this.props
+
+ const props = {
+ ...opts,
+ onChange: this._onChange,
+ schema,
+ uiSchema,
+ value,
+ }
+
+ // Enum, special case.
+ if (schema.enum) {
+ return
+ }
+
+ const type = getType(schema)
+ const Input = uiSchema.widget || InputByType[type.toLowerCase()]
+
+ if (!Input) {
+ throw new Error(`Unsupported type: ${type}.`)
+ }
+
+ return
+ }
+}
diff --git a/packages/xo-web/src/common/json-schema-input/helpers.js b/packages/xo-web/src/common/json-schema-input/helpers.js
new file mode 100644
index 000000000..ca9bb91a8
--- /dev/null
+++ b/packages/xo-web/src/common/json-schema-input/helpers.js
@@ -0,0 +1,90 @@
+import React from 'react'
+import includes from 'lodash/includes'
+import isArray from 'lodash/isArray'
+import marked from 'marked'
+
+import { Col, Row } from 'grid'
+
+// ===================================================================
+
+export const getType = schema => {
+ if (!schema) {
+ return
+ }
+
+ const type = schema.type
+
+ if (isArray(type)) {
+ if (includes(type, 'integer')) {
+ return 'integer'
+ }
+ if (includes(type, 'number')) {
+ return 'number'
+ }
+
+ return 'string'
+ }
+
+ return type
+}
+
+export const getXoType = schema => {
+ const type = schema && (schema['xo:type'] || schema.$type)
+
+ if (type) {
+ return type.toLowerCase()
+ }
+}
+
+// ===================================================================
+
+export const descriptionRender = description => (
+
+)
+
+// ===================================================================
+
+export const PrimitiveInputWrapper = ({
+ label,
+ required = false,
+ schema,
+ children,
+}) => (
+
+
+
+
+ {label}
+ {required && * }
+
+ {children}
+
+
+ {descriptionRender(schema.description)}
+
+)
+
+// ===================================================================
+
+export const forceDisplayOptionalAttr = ({ schema, value }) => {
+ if (!schema || !value) {
+ return false
+ }
+
+ // Array
+ if (schema.items && Array.isArray(value)) {
+ return true
+ }
+
+ // Object
+ for (const key in schema.properties) {
+ if (value[key]) {
+ return true
+ }
+ }
+
+ return false
+}
diff --git a/packages/xo-web/src/common/json-schema-input/index.js b/packages/xo-web/src/common/json-schema-input/index.js
new file mode 100644
index 000000000..9007a6379
--- /dev/null
+++ b/packages/xo-web/src/common/json-schema-input/index.js
@@ -0,0 +1 @@
+export default from './generic-input'
diff --git a/packages/xo-web/src/common/json-schema-input/integer-input.js b/packages/xo-web/src/common/json-schema-input/integer-input.js
new file mode 100644
index 000000000..dbe566ba7
--- /dev/null
+++ b/packages/xo-web/src/common/json-schema-input/integer-input.js
@@ -0,0 +1,46 @@
+import React from 'react'
+
+import uncontrollableInput from 'uncontrollable-input'
+import Combobox from '../combobox'
+import Component from '../base-component'
+import getEventValue from '../get-event-value'
+
+import { PrimitiveInputWrapper } from './helpers'
+
+// ===================================================================
+
+@uncontrollableInput()
+export default class IntegerInput extends Component {
+ _onChange = event => {
+ const value = getEventValue(event)
+ this.props.onChange(value ? +value : undefined)
+ }
+
+ render () {
+ const { required, schema } = this.props
+ const {
+ disabled,
+ onChange, // eslint-disable-line no-unused-vars
+ placeholder = schema.default,
+ value,
+ ...props
+ } = this.props
+
+ return (
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/common/json-schema-input/number-input.js b/packages/xo-web/src/common/json-schema-input/number-input.js
new file mode 100644
index 000000000..7b2d52ff2
--- /dev/null
+++ b/packages/xo-web/src/common/json-schema-input/number-input.js
@@ -0,0 +1,46 @@
+import React from 'react'
+
+import uncontrollableInput from 'uncontrollable-input'
+import Combobox from '../combobox'
+import Component from '../base-component'
+import getEventValue from '../get-event-value'
+
+import { PrimitiveInputWrapper } from './helpers'
+
+// ===================================================================
+
+@uncontrollableInput()
+export default class NumberInput extends Component {
+ _onChange = event => {
+ const value = getEventValue(event)
+ this.props.onChange(value ? +value : undefined)
+ }
+
+ render () {
+ const { required, schema } = this.props
+ const {
+ disabled,
+ onChange, // eslint-disable-line no-unused-vars
+ placeholder = schema.default,
+ value,
+ ...props
+ } = this.props
+
+ return (
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/common/json-schema-input/object-input.js b/packages/xo-web/src/common/json-schema-input/object-input.js
new file mode 100644
index 000000000..4aafa9a5f
--- /dev/null
+++ b/packages/xo-web/src/common/json-schema-input/object-input.js
@@ -0,0 +1,98 @@
+import React from 'react'
+import uncontrollableInput from 'uncontrollable-input'
+import { createSelector } from 'reselect'
+import { keyBy, map } from 'lodash'
+
+import _ from '../intl'
+import Component from '../base-component'
+import propTypes from '../prop-types-decorator'
+import { EMPTY_OBJECT } from '../utils'
+
+import GenericInput from './generic-input'
+import { descriptionRender, forceDisplayOptionalAttr } from './helpers'
+
+@propTypes({
+ depth: propTypes.number,
+ disabled: propTypes.bool,
+ label: propTypes.any.isRequired,
+ required: propTypes.bool,
+ schema: propTypes.object.isRequired,
+ uiSchema: propTypes.object,
+})
+@uncontrollableInput()
+export default class ObjectInput extends Component {
+ state = {
+ use: this.props.required || forceDisplayOptionalAttr(this.props),
+ }
+
+ _onChildChange = (value, key) => {
+ this.props.onChange({
+ ...this.props.value,
+ [key]: value,
+ })
+ }
+
+ _getRequiredProps = createSelector(
+ () => this.props.schema.required,
+ required => (required ? keyBy(required) : EMPTY_OBJECT)
+ )
+
+ render () {
+ const {
+ props: {
+ depth = 0,
+ disabled,
+ label,
+ required,
+ schema,
+ uiSchema,
+ value = EMPTY_OBJECT,
+ },
+ state: { use },
+ } = this
+
+ const childDepth = depth + 2
+ const properties = (uiSchema != null && uiSchema.properties) || EMPTY_OBJECT
+ const requiredProps = this._getRequiredProps()
+
+ return (
+
+
{label}
+ {descriptionRender(schema.description)}
+
+ {!required && (
+
+
+ {' '}
+ {_('fillOptionalInformations')}
+
+
+ )}
+ {use && (
+
+ {map(schema.properties, (childSchema, key) => (
+
+
+
+ ))}
+
+ )}
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/common/json-schema-input/string-input.js b/packages/xo-web/src/common/json-schema-input/string-input.js
new file mode 100644
index 000000000..5aa441093
--- /dev/null
+++ b/packages/xo-web/src/common/json-schema-input/string-input.js
@@ -0,0 +1,50 @@
+import React from 'react'
+import uncontrollableInput from 'uncontrollable-input'
+
+import Combobox from '../combobox'
+import Component from '../base-component'
+import getEventValue from '../get-event-value'
+import propTypes from '../prop-types-decorator'
+
+import { PrimitiveInputWrapper } from './helpers'
+
+// ===================================================================
+
+@propTypes({
+ password: propTypes.bool,
+})
+@uncontrollableInput()
+export default class StringInput extends Component {
+ // the value of this input is undefined not '' when empty to make
+ // it homogenous with when the user has never touched this input
+ _onChange = event => {
+ const value = getEventValue(event)
+ this.props.onChange(value !== '' ? value : undefined)
+ }
+
+ render () {
+ const { required, schema } = this.props
+ const {
+ disabled,
+ password,
+ placeholder = schema.default,
+ value,
+ ...props
+ } = this.props
+ delete props.onChange
+
+ return (
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/common/link.js b/packages/xo-web/src/common/link.js
new file mode 100644
index 000000000..1df3612d9
--- /dev/null
+++ b/packages/xo-web/src/common/link.js
@@ -0,0 +1,71 @@
+import Link from 'react-router/lib/Link'
+import React from 'react'
+import { routerShape } from 'react-router/lib/PropTypes'
+
+import Component from './base-component'
+import propTypes from './prop-types-decorator'
+
+// ===================================================================
+
+export { Link as default }
+
+// -------------------------------------------------------------------
+
+const _IGNORED_TAGNAMES = {
+ A: true,
+ BUTTON: true,
+ INPUT: true,
+ SELECT: true,
+}
+
+@propTypes({
+ className: propTypes.string,
+ tagName: propTypes.string,
+})
+export class BlockLink extends Component {
+ static contextTypes = {
+ router: routerShape,
+ }
+
+ _style = { cursor: 'pointer' }
+ _onClickCapture = event => {
+ const { currentTarget } = event
+ let element = event.target
+ while (element !== currentTarget) {
+ if (_IGNORED_TAGNAMES[element.tagName]) {
+ return
+ }
+ element = element.parentNode
+ }
+ event.stopPropagation()
+ if (event.ctrlKey || event.button === 1) {
+ window.open(this.context.router.createHref(this.props.to))
+ } else {
+ this.context.router.push(this.props.to)
+ }
+ }
+
+ _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', className } = this.props
+ const Component = tagName
+ return (
+
+ {children}
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/common/log-error.js b/packages/xo-web/src/common/log-error.js
new file mode 100644
index 000000000..11121c0e7
--- /dev/null
+++ b/packages/xo-web/src/common/log-error.js
@@ -0,0 +1,13 @@
+// Logs an error properly, correctly use the source map for the stack.
+//
+// This is achieved by throwing the error asynchronously.
+const logError = (error, ...args) => {
+ setTimeout(() => {
+ if (args.length) {
+ console.error(...args)
+ }
+
+ throw error
+ }, 0)
+}
+export { logError as default }
diff --git a/packages/xo-web/src/common/modal.js b/packages/xo-web/src/common/modal.js
new file mode 100644
index 000000000..6b9574811
--- /dev/null
+++ b/packages/xo-web/src/common/modal.js
@@ -0,0 +1,286 @@
+import isArray from 'lodash/isArray'
+import isString from 'lodash/isString'
+import map from 'lodash/map'
+import React, { Component, cloneElement } from 'react'
+import { createSelector } from 'selectors'
+import { injectIntl } from 'react-intl'
+import { Modal as ReactModal } from 'react-bootstrap-4/lib'
+
+import _, { messages } from './intl'
+import Button from './button'
+import Icon from './icon'
+import propTypes from './prop-types-decorator'
+import Tooltip from './tooltip'
+import {
+ disable as disableShortcuts,
+ enable as enableShortcuts,
+} from './shortcuts'
+
+// -----------------------------------------------------------------------------
+
+let instance
+const modal = (content, onClose) => {
+ if (!instance) {
+ throw new Error('No modal instance.')
+ } else if (instance.state.showModal) {
+ throw new Error('Other modal still open.')
+ }
+ instance.setState({ content, onClose, showModal: true }, disableShortcuts)
+}
+
+const _addRef = (component, ref) => {
+ if (isString(component) || isArray(component)) {
+ return component
+ }
+
+ try {
+ return cloneElement(component, { ref })
+ } catch (_) {} // Stateless component.
+ return component
+}
+
+// -----------------------------------------------------------------------------
+
+@propTypes({
+ buttons: propTypes.arrayOf(
+ propTypes.shape({
+ btnStyle: propTypes.string,
+ icon: propTypes.string,
+ label: propTypes.node.isRequired,
+ tooltip: propTypes.node,
+ value: propTypes.any,
+ })
+ ).isRequired,
+ children: propTypes.node.isRequired,
+ icon: propTypes.string,
+ title: propTypes.node.isRequired,
+})
+class GenericModal extends Component {
+ _getBodyValue = () => {
+ const { body } = this.refs
+ if (body !== undefined) {
+ return body.getWrappedInstance === undefined
+ ? body.value
+ : body.getWrappedInstance().value
+ }
+ }
+
+ _resolve = (value = this._getBodyValue()) => {
+ this.props.resolve(value)
+ instance.close()
+ }
+
+ _reject = () => {
+ this.props.reject()
+ instance.close()
+ }
+
+ render () {
+ const { buttons, icon, title } = this.props
+
+ const body = _addRef(this.props.children, 'body')
+
+ return (
+
+
+
+ {icon ? (
+
+ {title}
+
+ ) : (
+ title
+ )}
+
+
+ {body}
+
+ {map(buttons, ({ label, tooltip, value, icon, ...props }, key) => {
+ const button = (
+ this._resolve(value)} {...props}>
+ {icon !== undefined && }
+ {label}
+
+ )
+ return (
+
+ {tooltip !== undefined ? (
+ {button}
+ ) : (
+ button
+ )}{' '}
+
+ )
+ })}
+ {this.props.reject !== undefined && (
+ {_('genericCancel')}
+ )}
+
+
+ )
+ }
+}
+
+export const chooseAction = ({ body, buttons, icon, title }) => {
+ return new Promise((resolve, reject) => {
+ modal(
+
+ {body}
+ ,
+ reject
+ )
+ })
+}
+
+@propTypes({
+ body: propTypes.node,
+ strongConfirm: propTypes.object.isRequired,
+ icon: propTypes.string,
+ reject: propTypes.func,
+ resolve: propTypes.func,
+ title: propTypes.node.isRequired,
+})
+@injectIntl
+class StrongConfirm extends Component {
+ state = {
+ buttons: [{ btnStyle: 'danger', label: _('confirmOk'), disabled: true }],
+ }
+
+ _getStrongConfirmString = createSelector(
+ () => this.props.intl.formatMessage,
+ () => this.props.strongConfirm,
+ (format, { messageId, values }) => format(messages[messageId], values)
+ )
+
+ _onInputChange = event => {
+ const userInput = event.target.value
+ const strongConfirmString = this._getStrongConfirmString()
+ const confirmButton = this.state.buttons[0]
+
+ let disabled
+ if (
+ (userInput.toLowerCase() === strongConfirmString.toLowerCase()) ^
+ (disabled = !confirmButton.disabled)
+ ) {
+ this.setState({
+ buttons: [{ ...confirmButton, disabled }],
+ })
+ }
+ }
+
+ render () {
+ const {
+ body,
+ strongConfirm: { messageId, values },
+ icon,
+ reject,
+ resolve,
+ title,
+ } = this.props
+
+ return (
+
+ {body}
+
+
+ {_('enterConfirmText')}{' '}
+ {_(messageId, values)}
+
+
+
+
+
+ )
+ }
+}
+
+// -----------------------------------------------------------------------------
+
+const ALERT_BUTTONS = [{ label: _('alertOk'), value: 'ok' }]
+
+export const alert = (title, body) =>
+ new Promise(resolve => {
+ modal(
+
+ {body}
+ ,
+ resolve
+ )
+ })
+
+// -----------------------------------------------------------------------------
+
+const CONFIRM_BUTTONS = [{ btnStyle: 'primary', label: _('confirmOk') }]
+
+export const confirm = ({ body, icon = 'alarm', title, strongConfirm }) =>
+ strongConfirm
+ ? new Promise((resolve, reject) => {
+ modal(
+
+ )
+ })
+ : chooseAction({
+ body,
+ buttons: CONFIRM_BUTTONS,
+ icon,
+ title,
+ })
+
+// -----------------------------------------------------------------------------
+
+export default class Modal extends Component {
+ constructor () {
+ super()
+
+ this.state = { showModal: false }
+ }
+
+ componentDidMount () {
+ if (instance) {
+ throw new Error('Modal is a singleton!')
+ }
+ instance = this
+ }
+
+ componentWillUnmount () {
+ instance = undefined
+ }
+
+ close () {
+ this.setState({ showModal: false }, enableShortcuts)
+ }
+
+ _onHide = () => {
+ this.close()
+
+ const { onClose } = this.state
+ onClose && onClose()
+ }
+
+ render () {
+ return (
+
+ {this.state.content}
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/common/nav.js b/packages/xo-web/src/common/nav.js
new file mode 100644
index 000000000..66b2f3eba
--- /dev/null
+++ b/packages/xo-web/src/common/nav.js
@@ -0,0 +1,18 @@
+import classNames from 'classnames'
+import React from 'react'
+
+import Link from './link'
+
+export const NavLink = ({ children, to }) => (
+
+
+ {children}
+
+
+)
+
+export const NavTabs = ({ children, className }) => (
+
+)
diff --git a/packages/xo-web/src/common/no-objects.js b/packages/xo-web/src/common/no-objects.js
new file mode 100644
index 000000000..b3560c3ea
--- /dev/null
+++ b/packages/xo-web/src/common/no-objects.js
@@ -0,0 +1,41 @@
+import React from 'react'
+import { isEmpty } from 'lodash'
+
+import propTypes from './prop-types-decorator'
+
+// This component returns :
+// - A loading icon when the objects are not fetched
+// - A default message if the objects are fetched and the collection is empty
+// - The children if the objects are fetched and the collection is not empty
+//
+// ```js
+//
+// {children}
+//
+// ````
+const NoObjects = props => {
+ const { collection } = props
+
+ if (collection == null) {
+ return
+ }
+
+ if (isEmpty(collection)) {
+ return {props.emptyMessage}
+ }
+
+ const { children, component: Component, ...otherProps } = props
+ return children !== undefined ? (
+ children(otherProps)
+ ) : (
+
+ )
+}
+
+propTypes(NoObjects)({
+ children: propTypes.func,
+ collection: propTypes.oneOfType([propTypes.array, propTypes.object]),
+ component: propTypes.func,
+ emptyMessage: propTypes.node.isRequired,
+})
+export default NoObjects
diff --git a/packages/xo-web/src/common/notification.js b/packages/xo-web/src/common/notification.js
new file mode 100644
index 000000000..985d655f3
--- /dev/null
+++ b/packages/xo-web/src/common/notification.js
@@ -0,0 +1,88 @@
+import _ from 'intl'
+import ButtonLink from 'button-link'
+import Icon from 'icon'
+import React, { Component } from 'react'
+import ReactNotify from 'react-notify'
+import { connectStore } from 'utils'
+import { isAdmin } from 'selectors'
+
+let instance
+
+export let error
+export let info
+export let success
+
+@connectStore({
+ isAdmin,
+})
+export class Notification extends Component {
+ componentDidMount () {
+ if (instance) {
+ throw new Error('Notification is a singleton!')
+ }
+ instance = this
+ }
+
+ componentWillUnmount () {
+ instance = undefined
+ }
+
+ // This special component never have to rerender!
+ shouldComponentUpdate () {
+ return false
+ }
+
+ render () {
+ return (
+ {
+ if (!notification) {
+ return
+ }
+
+ error = (title, body) =>
+ notification.error(
+ title,
+ this.props.isAdmin ? (
+
+
{body}
+
+ {_('showLogs')}
+
+
+ ) : (
+ body
+ ),
+ 6e3
+ )
+ info = (title, body) => notification.info(title, body, 3e3)
+ success = (title, body) => notification.success(title, body, 3e3)
+ }}
+ />
+ )
+ }
+}
+
+export { info as default }
+
+/* Example:
+
+import info, { success, error } from 'notification'
+
+ info('Info', 'This is an info notification')}>
+ Info notification
+
+
+ success('Success', 'This is a success notification')}>
+ Success notification
+
+
+ error('Error', 'This is an error notification')}>
+ Error notification
+
+*/
diff --git a/packages/xo-web/src/common/object-name.js b/packages/xo-web/src/common/object-name.js
new file mode 100644
index 000000000..eca61df28
--- /dev/null
+++ b/packages/xo-web/src/common/object-name.js
@@ -0,0 +1,15 @@
+/** EXPERIMENT: this is here to avoid a littel code dupplication, but is not admitted as a highly recommendable component */
+import { connectStore } from 'utils'
+import { createGetObject } from 'selectors'
+import React, { Component } from 'react'
+
+@connectStore(() => {
+ const object = createGetObject()
+ return (state, props) => ({ object: object(state, props) })
+})
+export default class ObjectName extends Component {
+ render () {
+ const { object } = this.props
+ return {object && object.name_label}
+ }
+}
diff --git a/packages/xo-web/src/common/pagination.js b/packages/xo-web/src/common/pagination.js
new file mode 100644
index 000000000..22ded2c66
--- /dev/null
+++ b/packages/xo-web/src/common/pagination.js
@@ -0,0 +1,125 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+
+const PageItem = ({ active, children, disabled, onClick, value }) =>
+ active ? (
+
+ {children}
+
+ ) : disabled ? (
+
+ {children}
+
+ ) : (
+
+
+ {children}
+
+
+ )
+
+export default class Pagination extends React.PureComponent {
+ static defaultProps = {
+ ellipsis: true,
+ maxButtons: 7,
+ next: true,
+ prev: true,
+ }
+
+ static propTypes = {
+ ariaLabel: PropTypes.string,
+ ellipsis: PropTypes.bool,
+ maxButtons: PropTypes.number,
+ next: PropTypes.bool,
+ onChange: PropTypes.func.isRequired,
+ pages: PropTypes.number.isRequired,
+ prev: PropTypes.bool,
+ value: PropTypes.number.isRequired,
+ }
+
+ _onClick (event) {
+ event.preventDefault()
+ this.props.onChange(+event.currentTarget.dataset.value)
+ }
+ _onClick = this._onClick.bind(this)
+
+ render () {
+ const {
+ ariaLabel,
+ ellipsis,
+ maxButtons,
+ next,
+ pages,
+ prev,
+ value,
+ } = this.props
+ const onClick = this._onClick
+
+ let min, max
+ if (pages <= maxButtons) {
+ min = 1
+ max = pages
+ } else {
+ min = Math.max(
+ 1,
+ Math.min(value - Math.floor(maxButtons / 2), pages - maxButtons + 1)
+ )
+ max = min + maxButtons - 1
+ }
+
+ const pageButtons = []
+ if (ellipsis && min !== 1) {
+ pageButtons.push(
+
+ …
+
+ )
+ }
+ for (let page = min; page <= max; ++page) {
+ pageButtons.push(
+
+ {page}
+
+ )
+ }
+ if (ellipsis && max !== pages) {
+ pageButtons.push(
+
+ …
+
+ )
+ }
+ return (
+
+
+ {prev && (
+
+ ‹
+
+ )}
+ {pageButtons}
+ {next && (
+
+ ›
+
+ )}
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/common/prop-types-decorator.js b/packages/xo-web/src/common/prop-types-decorator.js
new file mode 100644
index 000000000..b2981deaf
--- /dev/null
+++ b/packages/xo-web/src/common/prop-types-decorator.js
@@ -0,0 +1,45 @@
+import assign from 'lodash/assign'
+import PropTypes from 'prop-types'
+
+// Deprecated because :
+// - unnecessary
+// - not standard in the React ecosystem
+if (__DEV__) {
+ console.warn(`DEPRECATED: use prop-types directly:
+class MyComponent extends React.Component {
+ static propTypes = {
+ foo: PropTypes.string.isRequired
+ }
+}`)
+}
+
+// Decorators to help declaring properties and context types on React
+// components without using the tedious static properties syntax.
+//
+// ```js
+// @propTypes({
+// children: propTypes.node.isRequired
+// }, {
+// store: propTypes.object.isRequired
+// })
+// class MyComponent extends React.Component {}
+// ```
+const propTypes = (propTypes, contextTypes) => target => {
+ if (propTypes !== undefined) {
+ target.propTypes = {
+ ...target.propTypes,
+ ...propTypes,
+ }
+ }
+ if (contextTypes !== undefined) {
+ target.contextTypes = {
+ ...target.contextTypes,
+ ...contextTypes,
+ }
+ }
+
+ return target
+}
+assign(propTypes, PropTypes)
+
+export { propTypes as default }
diff --git a/packages/xo-web/src/common/react-novnc.js b/packages/xo-web/src/common/react-novnc.js
new file mode 100644
index 000000000..c83d7ffcc
--- /dev/null
+++ b/packages/xo-web/src/common/react-novnc.js
@@ -0,0 +1,169 @@
+import React, { Component } from 'react'
+import RFB from '@nraynaud/novnc/lib/rfb'
+import URL from 'url-parse'
+import { createBackoff } from 'jsonrpc-websocket-client'
+import {
+ enable as enableShortcuts,
+ disable as disableShortcuts,
+} from 'shortcuts'
+
+import propTypes from './prop-types-decorator'
+
+const PROTOCOL_ALIASES = {
+ 'http:': 'ws:',
+ 'https:': 'wss:',
+}
+const fixProtocol = url => {
+ const protocol = PROTOCOL_ALIASES[url.protocol]
+ if (protocol) {
+ url.protocol = protocol
+ }
+}
+
+@propTypes({
+ onClipboardChange: propTypes.func,
+ url: propTypes.string.isRequired,
+})
+export default class NoVnc extends Component {
+ constructor (props) {
+ super(props)
+ this._rfb = null
+ this._retryGen = createBackoff(Infinity)
+
+ this._onUpdateState = (rfb, state) => {
+ if (state === 'normal') {
+ if (this._retryTimeout) {
+ clearTimeout(this._retryTimeout)
+ this._retryTimeout = undefined
+ this._retryGen = createBackoff(Infinity)
+ }
+ }
+
+ if (state !== 'disconnected' || this.refs.canvas == null) {
+ return
+ }
+
+ clearTimeout(this._retryTimeout)
+ this._retryTimeout = setTimeout(
+ this._connect,
+ this._retryGen.next().value
+ )
+ }
+ }
+
+ sendCtrlAltDel () {
+ const rfb = this._rfb
+ if (rfb) {
+ rfb.sendCtrlAltDel()
+ }
+ }
+
+ setClipboard (text) {
+ const rfb = this._rfb
+ if (rfb) {
+ rfb.clipboardPasteFrom(text)
+ }
+ }
+
+ _clean () {
+ const rfb = this._rfb
+ if (rfb) {
+ this._rfb = null
+ rfb.disconnect()
+ }
+ enableShortcuts()
+ }
+
+ _connect = () => {
+ this._clean()
+
+ const { canvas } = this.refs
+ if (!canvas) {
+ return
+ }
+
+ const url = new URL(this.props.url)
+ fixProtocol(url)
+
+ const isSecure = url.protocol === 'wss:'
+
+ const { onClipboardChange } = this.props
+ const rfb = (this._rfb = new RFB({
+ encrypt: isSecure,
+ target: this.refs.canvas,
+ onClipboard:
+ onClipboardChange &&
+ ((_, text) => {
+ onClipboardChange(text)
+ }),
+ onUpdateState: this._onUpdateState,
+ }))
+
+ // remove leading slashes from the path
+ //
+ // a leading slassh will be added by noVNC
+ const clippedPath = url.pathname.replace(/^\/+/, '')
+
+ // a port is required
+ //
+ // if not available from the URL, use the default ones
+ const port = url.port || (isSecure ? 443 : 80)
+
+ rfb.connect(url.hostname, port, null, clippedPath)
+ disableShortcuts()
+ }
+
+ componentDidMount () {
+ this._connect()
+ }
+
+ componentWillUnmount () {
+ this._clean()
+ }
+
+ componentWillReceiveProps (props) {
+ const rfb = this._rfb
+ if (rfb && this.props.scale !== props.scale) {
+ rfb.get_display().set_scale(props.scale || 1)
+ rfb.get_mouse().set_scale(props.scale || 1)
+ }
+ }
+
+ _focus = () => {
+ const rfb = this._rfb
+ if (rfb) {
+ const { activeElement } = document
+ if (activeElement) {
+ activeElement.blur()
+ }
+
+ rfb.get_keyboard().grab()
+ rfb.get_mouse().grab()
+
+ disableShortcuts()
+ }
+ }
+
+ _unfocus = () => {
+ const rfb = this._rfb
+ if (rfb) {
+ rfb.get_keyboard().ungrab()
+ rfb.get_mouse().ungrab()
+
+ enableShortcuts()
+ }
+ }
+
+ render () {
+ return (
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/common/render-xo-item.js b/packages/xo-web/src/common/render-xo-item.js
new file mode 100644
index 000000000..4ade1211d
--- /dev/null
+++ b/packages/xo-web/src/common/render-xo-item.js
@@ -0,0 +1,265 @@
+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'
+
+// ===================================================================
+
+const OBJECT_TYPE_TO_ICON = {
+ 'VM-template': 'vm',
+ host: 'host',
+ network: 'network',
+}
+
+// Host, Network, VM-template.
+const PoolObjectItem = propTypes({
+ object: propTypes.object.isRequired,
+})(
+ connectStore(() => {
+ const getPool = createGetObject((_, props) => props.object.$pool)
+
+ return (state, props) => ({
+ pool: getPool(state, props),
+ })
+ })(({ object, pool }) => {
+ const icon = OBJECT_TYPE_TO_ICON[object.type]
+ const { id } = object
+
+ return (
+
+ {`${object.name_label || id} `}
+ {pool && `(${pool.name_label || pool.id})`}
+
+ )
+ })
+)
+
+// SR.
+const SrItem = propTypes({
+ sr: propTypes.object.isRequired,
+})(
+ connectStore(() => {
+ const getContainer = createGetObject((_, props) => props.sr.$container)
+
+ 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 (
+
+ {label}
+
+ )
+ })
+)
+
+// VM.
+const VmItem = propTypes({
+ vm: propTypes.object.isRequired,
+})(
+ connectStore(() => {
+ const getContainer = createGetObject((_, props) => props.vm.$container)
+
+ return (state, props) => ({
+ container: getContainer(state, props),
+ })
+ })(({ vm, container }) => (
+
+ {' '}
+ {vm.name_label || vm.id}
+ {container && ` (${container.name_label || container.id})`}
+
+ ))
+)
+
+const VgpuItem = connectStore(() => ({
+ vgpuType: createGetObject((_, props) => props.vgpu.vgpuType),
+}))(({ vgpu, vgpuType }) => (
+
+ {vgpuType.modelName}
+
+))
+
+// ===================================================================
+
+const xoItemToRender = {
+ // Subscription objects.
+ group: group => (
+
+ {group.name}
+
+ ),
+ remote: remote => (
+
+ {remote.value.name}
+
+ ),
+ role: role => {role.name} ,
+ user: user => (
+
+ {user.email}
+
+ ),
+ resourceSet: resourceSet => (
+
+
+ {resourceSet.name}
+ {' '}
+ ({resourceSet.id})
+
+ ),
+ sshKey: key => (
+
+ {key.label}
+
+ ),
+ ipPool: ipPool => (
+
+ {ipPool.name}
+
+ ),
+ ipAddress: ({ label, used }) => {
+ if (used) {
+ return {label}
+ }
+ return {label}
+ },
+
+ // XO objects.
+ pool: pool => (
+
+ {pool.name_label || pool.id}
+
+ ),
+
+ VDI: vdi => (
+
+ {vdi.name_label}{' '}
+ {vdi.name_description && ({vdi.name_description}) }
+
+ ),
+
+ // Pool objects.
+ 'VM-template': vmTemplate => ,
+ host: host => ,
+ network: network => ,
+
+ // SR.
+ SR: sr => ,
+
+ // VM.
+ VM: vm => ,
+ 'VM-snapshot': vm => ,
+ 'VM-controller': vm => (
+
+
+
+ ),
+
+ // PIF.
+ PIF: pif => (
+
+ {' '}
+ {pif.device} ({pif.deviceName})
+
+ ),
+
+ // Tags.
+ tag: tag => (
+
+ {tag.value}
+
+ ),
+
+ // GPUs
+
+ vgpu: vgpu => ,
+
+ vgpuType: type => (
+
+ {type.modelName} ({type.vendorName}){' '}
+ {type.maxResolutionX}x{type.maxResolutionY}
+
+ ),
+
+ gpuGroup: group => (
+
+ {startsWith(group.name_label, 'Group of ')
+ ? group.name_label.slice(9)
+ : group.name_label}
+
+ ),
+}
+
+const renderXoItem = (item, { className } = {}) => {
+ const { id, type, label } = item
+
+ if (item.removed) {
+ return (
+
+ {' '}
+ {id}
+
+ )
+ }
+
+ if (!type) {
+ if (process.env.NODE_ENV !== 'production' && !label) {
+ throw new Error(`an item must have at least either a type or a label`)
+ }
+ return (
+
+ {label}
+
+ )
+ }
+
+ const Component = xoItemToRender[type]
+
+ if (process.env.NODE_ENV !== 'production' && !Component) {
+ throw new Error(`no available component for type ${type}`)
+ }
+
+ if (Component) {
+ return (
+
+
+
+ )
+ }
+}
+
+export { renderXoItem as default }
+
+const GenericXoItem = connectStore(() => {
+ const getObject = createGetObject()
+
+ return (state, props) => ({
+ xoItem: getObject(state, props),
+ })
+})(
+ ({ xoItem, ...props }) =>
+ xoItem ? renderXoItem(xoItem, props) : renderXoUnknownItem()
+)
+
+export const renderXoItemFromId = (id, props) => (
+
+)
+
+export const renderXoUnknownItem = () => (
+ {_('errorNoSuchItem')}
+)
diff --git a/packages/xo-web/src/common/resource-set-quotas.js b/packages/xo-web/src/common/resource-set-quotas.js
new file mode 100644
index 000000000..b19da2c96
--- /dev/null
+++ b/packages/xo-web/src/common/resource-set-quotas.js
@@ -0,0 +1,127 @@
+import _, { messages } from 'intl'
+import ChartistGraph from 'react-chartist'
+import PropTypes from 'prop-types'
+import React from 'react'
+import { Card, CardBlock, CardHeader } from 'card'
+import { Container, Row, Col } from 'grid'
+import { forEach, map } from 'lodash'
+import { injectIntl } from 'react-intl'
+
+import Component from './base-component'
+import Icon from './icon'
+import { createSelector } from './selectors'
+import { formatSize } from './utils'
+
+// ===================================================================
+
+const RESOURCES = ['disk', 'memory', 'cpus']
+
+// ===================================================================
+
+@injectIntl
+export default class ResourceSetQuotas extends Component {
+ static propTypes = {
+ limits: PropTypes.object.isRequired,
+ }
+
+ _getQuotas = createSelector(
+ () => this.props.limits,
+ limits => {
+ const quotas = {}
+
+ forEach(RESOURCES, resource => {
+ if (limits[resource] != null) {
+ const { available, total } = limits[resource]
+ quotas[resource] = {
+ available,
+ total,
+ usage: total - available,
+ }
+ }
+ })
+
+ return quotas
+ }
+ )
+
+ render () {
+ const { intl: { formatMessage } } = this.props
+ const labels = [
+ formatMessage(messages.availableResourceLabel),
+ formatMessage(messages.usedResourceLabel),
+ ]
+ const { cpus, disk, memory } = this._getQuotas()
+ const quotas = [
+ {
+ header: (
+
+ {_('cpuStatePanel')}
+
+ ),
+ validFormat: true,
+ quota: cpus,
+ },
+ {
+ header: (
+
+ {_('memoryStatePanel')}
+
+ ),
+ validFormat: false,
+ quota: memory,
+ },
+ {
+ header: (
+
+ {_('srUsageStatePanel')}
+
+ ),
+ validFormat: false,
+ quota: disk,
+ },
+ ]
+ return (
+
+
+ {map(quotas, ({ header, validFormat, quota }, key) => (
+
+
+ {header}
+
+ {quota !== undefined ? (
+
+
+
+ {_('resourceSetQuota', {
+ total: validFormat
+ ? quota.total.toString()
+ : formatSize(quota.total),
+ usage: validFormat
+ ? quota.usage.toString()
+ : formatSize(quota.usage),
+ })}
+
+
+ ) : (
+ ∞
+ )}
+
+
+
+ ))}
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/common/scheduling.js b/packages/xo-web/src/common/scheduling.js
new file mode 100644
index 000000000..a98bedec5
--- /dev/null
+++ b/packages/xo-web/src/common/scheduling.js
@@ -0,0 +1,582 @@
+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 _ from './intl'
+import Button from './button'
+import Component from './base-component'
+import propTypes from './prop-types-decorator'
+import TimezonePicker from './timezone-picker'
+import Icon from './icon'
+import Tooltip from './tooltip'
+import { Card, CardHeader, CardBlock } from './card'
+import { Col, Row } from './grid'
+import { Range, Toggle } from './form'
+
+// ===================================================================
+
+// By default, later uses UTC but we use this line for future versions.
+later.date.UTC()
+
+// ===================================================================
+
+const CLICKABLE = { cursor: 'pointer' }
+const PREVIEW_SLIDER_STYLE = { width: '400px' }
+
+// ===================================================================
+
+const UNITS = ['minute', 'hour', 'monthDay', 'month', 'weekDay']
+
+const MINUTES_RANGE = [2, 30]
+const HOURS_RANGE = [2, 12]
+const MONTH_DAYS_RANGE = [2, 15]
+const MONTHS_RANGE = [2, 6]
+
+const MIN_PREVIEWS = 5
+const MAX_PREVIEWS = 20
+
+const MONTHS = [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10, 11]]
+
+const DAYS = (() => {
+ const days = []
+
+ for (let i = 0; i < 4; i++) {
+ days[i] = []
+
+ for (let j = 1; j < 8; j++) {
+ days[i].push(7 * i + j)
+ }
+ }
+
+ days.push([29, 30, 31])
+
+ return days
+})()
+
+const WEEK_DAYS = [[0, 1, 2], [3, 4, 5], [6]]
+
+const HOURS = (() => {
+ const hours = []
+
+ for (let i = 0; i < 4; i++) {
+ hours[i] = []
+
+ for (let j = 0; j < 6; j++) {
+ hours[i].push(6 * i + j)
+ }
+ }
+
+ return hours
+})()
+
+const MINS = (() => {
+ const minutes = []
+
+ for (let i = 0; i < 6; i++) {
+ minutes[i] = []
+
+ for (let j = 0; j < 10; j++) {
+ minutes[i].push(10 * i + j)
+ }
+ }
+
+ return minutes
+})()
+
+const PICKTIME_TO_ID = {
+ minute: 0,
+ hour: 1,
+ monthDay: 2,
+ month: 3,
+ weekDay: 4,
+}
+
+const TIME_FORMAT = {
+ weekday: 'long',
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ hour: 'numeric',
+ minute: 'numeric',
+
+ // The timezone is not significant for displaying the date previews
+ // as long as it is the same used to generate the next occurrences
+ // from the cron patterns.
+
+ // Therefore we can use UTC everywhere and say to the user that the
+ // previews are in the configured timezone.
+ timeZone: 'UTC',
+}
+
+// ===================================================================
+
+// monthNum: [ 0 : 11 ]
+const getMonthName = monthNum => (
+
+)
+
+// dayNum: [ 0 : 6 ]
+const getDayName = dayNum => (
+ // January, 1970, 5th => Monday
+
+)
+
+// ===================================================================
+
+@propTypes({
+ cronPattern: propTypes.string.isRequired,
+})
+export class SchedulePreview extends Component {
+ render () {
+ const { cronPattern } = this.props
+ 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 (
+
+
+ {_('cronPattern')} {cronPattern}
+
+
+
+
+
+ {map(dates, (date, id) => (
+
+
+
+ ))}
+ ...
+
+
+ )
+ }
+}
+
+// ===================================================================
+
+@propTypes({
+ children: propTypes.any.isRequired,
+ onChange: propTypes.func.isRequired,
+ tdId: propTypes.number.isRequired,
+ value: propTypes.bool.isRequired,
+})
+class ToggleTd extends Component {
+ _onClick = () => {
+ const { props } = this
+ props.onChange(props.tdId, !props.value)
+ }
+
+ render () {
+ const { props } = this
+ return (
+
+ {props.children}
+
+ )
+ }
+}
+
+// ===================================================================
+
+@propTypes({
+ labelId: propTypes.string.isRequired,
+ options: propTypes.array.isRequired,
+ optionRenderer: propTypes.func,
+ onChange: propTypes.func.isRequired,
+ value: propTypes.array.isRequired,
+})
+class TableSelect extends Component {
+ static defaultProps = {
+ optionRenderer: value => value,
+ }
+
+ _reset = () => {
+ this.props.onChange([])
+ }
+
+ _handleChange = (tdId, tdValue) => {
+ const { props } = this
+
+ const newValue = props.value.slice()
+ const index = sortedIndex(newValue, tdId)
+
+ if (tdValue) {
+ // Add
+ if (newValue[index] !== tdId) {
+ newValue.splice(index, 0, tdId)
+ }
+ } else {
+ // Remove
+ if (newValue[index] === tdId) {
+ newValue.splice(index, 1)
+ }
+ }
+
+ props.onChange(newValue)
+ }
+
+ render () {
+ const { labelId, options, optionRenderer, value } = this.props
+
+ return (
+
+
+
+ {map(options, (line, i) => (
+
+ {map(line, tdOption => (
+
+ ))}
+
+ ))}
+
+
+
+ {_(`selectTableAll${labelId}`)}{' '}
+ {value && !value.length && }
+
+
+ )
+ }
+}
+
+// ===================================================================
+
+// "2,7" => [2,7] "*/2" => 2 "*" => []
+const cronToValue = (cron, range) => {
+ if (cron.indexOf('/') === 1) {
+ return +cron.split('/')[1]
+ }
+
+ if (cron === '*') {
+ return []
+ }
+
+ return map(cron.split(','), Number)
+}
+
+// [2,7] => "2,7" 2 => "*/2" [] => "*"
+const valueToCron = value => {
+ if (!isArray(value)) {
+ return `*/${value}`
+ }
+
+ if (!value.length) {
+ return '*'
+ }
+
+ return value.join(',')
+}
+
+@propTypes({
+ headerAddon: propTypes.node,
+ optionRenderer: propTypes.func,
+ onChange: propTypes.func.isRequired,
+ range: propTypes.array,
+ labelId: propTypes.string.isRequired,
+ value: propTypes.any.isRequired,
+})
+class TimePicker extends Component {
+ _update = cron => {
+ const { tableValue, rangeValue } = this.state
+
+ const newValue = cronToValue(cron)
+ const periodic = !isArray(newValue)
+
+ this.setState({
+ periodic,
+ tableValue: periodic ? tableValue : newValue,
+ rangeValue: periodic ? newValue : rangeValue,
+ })
+ }
+
+ componentWillReceiveProps (props) {
+ if (props.value !== this.props.value) {
+ this._update(props.value)
+ }
+ }
+
+ componentDidMount () {
+ this._update(this.props.value)
+ }
+
+ _onChange = value => {
+ this.props.onChange(valueToCron(value))
+ }
+
+ _tableTab = () => this._onChange(this.state.tableValue || [])
+ _periodicTab = () =>
+ this._onChange(this.state.rangeValue || this.props.range[0])
+
+ render () {
+ const { headerAddon, labelId, options, optionRenderer, range } = this.props
+
+ const { periodic, tableValue, rangeValue } = this.state
+
+ return (
+
+
+ {_(`scheduling${labelId}`)}
+ {headerAddon}
+
+
+ {range && (
+
+ )}
+ {periodic ? (
+
+ ) : (
+
+ )}
+
+
+ )
+ }
+}
+
+const isWeekDayMode = ({ monthDayPattern, weekDayPattern }) => {
+ if (monthDayPattern === '*' && weekDayPattern === '*') {
+ return
+ }
+
+ return weekDayPattern !== '*'
+}
+
+@propTypes({
+ monthDayPattern: propTypes.string.isRequired,
+ weekDayPattern: propTypes.string.isRequired,
+})
+class DayPicker extends Component {
+ state = {
+ weekDayMode: isWeekDayMode(this.props),
+ }
+
+ componentWillReceiveProps (props) {
+ const weekDayMode = isWeekDayMode(props)
+
+ if (weekDayMode !== undefined) {
+ this.setState({ weekDayMode })
+ }
+ }
+
+ _setWeekDayMode = weekDayMode => {
+ this.props.onChange(['*', '*'])
+ this.setState({ weekDayMode })
+ }
+
+ _onChange = cron => {
+ const isMonthDayPattern = !this.state.weekDayMode || includes(cron, '/')
+
+ this.props.onChange([
+ isMonthDayPattern ? cron : '*',
+ isMonthDayPattern ? '*' : cron,
+ ])
+ }
+
+ render () {
+ const { monthDayPattern, weekDayPattern } = this.props
+ const { weekDayMode } = this.state
+
+ const dayModeToggle = (
+
+
+
+
+
+ )
+
+ return (
+
+ )
+ }
+}
+
+// ===================================================================
+
+@propTypes({
+ cronPattern: propTypes.string,
+ onChange: propTypes.func,
+ timezone: propTypes.string,
+ value: propTypes.shape({
+ cronPattern: propTypes.string.isRequired,
+ timezone: propTypes.string,
+ }),
+})
+export default class Scheduler extends Component {
+ constructor (props) {
+ super(props)
+
+ this._onCronChange = newCrons => {
+ const cronPattern = this._getCronPattern().split(' ')
+ forEach(newCrons, (cron, unit) => {
+ cronPattern[PICKTIME_TO_ID[unit]] = cron
+ })
+
+ this.props.onChange({
+ cronPattern: cronPattern.join(' '),
+ timezone: this._getTimezone(),
+ })
+ }
+
+ forEach(UNITS, unit => {
+ this[`_${unit}Change`] = cron => this._onCronChange({ [unit]: cron })
+ })
+ this._dayChange = ([monthDay, weekDay]) =>
+ this._onCronChange({ monthDay, weekDay })
+ }
+
+ _onTimezoneChange = timezone => {
+ this.props.onChange({
+ cronPattern: this._getCronPattern(),
+ timezone,
+ })
+ }
+
+ _getCronPattern = () => {
+ const { value, cronPattern = value.cronPattern } = this.props
+ return cronPattern
+ }
+
+ _getTimezone = () => {
+ const { value, timezone = value && value.timezone } = this.props
+ return timezone
+ }
+
+ render () {
+ const cronPatternArr = this._getCronPattern().split(' ')
+ const timezone = this._getTimezone()
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/common/select-files.js b/packages/xo-web/src/common/select-files.js
new file mode 100644
index 000000000..8a8846626
--- /dev/null
+++ b/packages/xo-web/src/common/select-files.js
@@ -0,0 +1,34 @@
+import _ from 'intl'
+import Component from 'base-component'
+import Icon from 'icon'
+import propTypes from 'prop-types-decorator'
+import React from 'react'
+import { omit } from 'lodash'
+
+@propTypes({
+ multi: propTypes.bool,
+ label: propTypes.node,
+ onChange: propTypes.func.isRequired,
+})
+export default class SelectFiles extends Component {
+ _onChange = e => {
+ const { multi, onChange } = this.props
+ const { files } = e.target
+
+ onChange(multi ? files : files[0])
+ }
+
+ render () {
+ return (
+
+ {this.props.label || _('browseFiles')}
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/common/select-objects.js b/packages/xo-web/src/common/select-objects.js
new file mode 100644
index 000000000..32be7d416
--- /dev/null
+++ b/packages/xo-web/src/common/select-objects.js
@@ -0,0 +1,1017 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import { parse as parseRemote } from 'xo-remote-parser'
+import {
+ assign,
+ filter,
+ flatten,
+ forEach,
+ groupBy,
+ includes,
+ isArray,
+ isEmpty,
+ isInteger,
+ isString,
+ keyBy,
+ keys,
+ map,
+ mapValues,
+ pick,
+ sortBy,
+ toArray,
+} from 'lodash'
+
+import _ from './intl'
+import Button from './button'
+import Icon from './icon'
+import renderXoItem from './render-xo-item'
+import Select from './form/select'
+import store from './store'
+import Tooltip from './tooltip'
+import uncontrollableInput from 'uncontrollable-input'
+import {
+ createCollectionWrapper,
+ createFilter,
+ createGetObjectsOfType,
+ createGetTags,
+ createSelector,
+ getObject,
+} from './selectors'
+import { addSubscriptions, connectStore, resolveResourceSets } from './utils'
+import {
+ isSrWritable,
+ subscribeCurrentUser,
+ subscribeGroups,
+ subscribeIpPools,
+ subscribeRemotes,
+ subscribeResourceSets,
+ subscribeRoles,
+ subscribeUsers,
+} from './xo'
+
+// ===================================================================
+
+// react-select's line-height is 1.4
+// https://github.com/JedWatson/react-select/blob/916ab0e62fc7394be8e24f22251c399a68de8b1c/less/multi.less#L33
+// while bootstrap button's line-height is 1.25
+// https://github.com/twbs/bootstrap/blob/959c4e527c6ef69623928db638267ba1c370479d/scss/_variables.scss#L342
+const ADDON_BUTTON_STYLE = { lineHeight: '1.4' }
+
+const getIds = value =>
+ value == null || isString(value) || isInteger(value)
+ ? value
+ : isArray(value) ? map(value, getIds) : value.id
+
+const getOption = (object, container) => ({
+ label: container
+ ? `${getLabel(object)} ${getLabel(container)}`
+ : getLabel(object),
+ value: object.id,
+ xoItem: object,
+})
+
+const getLabel = object =>
+ object.name_label ||
+ object.name ||
+ object.email ||
+ (object.value && object.value.name) ||
+ object.value ||
+ object.label
+
+const options = props => ({
+ defaultValue: props.multi ? [] : undefined,
+})
+
+// ===================================================================
+
+/*
+ * WITHOUT xoContainers :
+ *
+ * xoObjects: [
+ * { type: 'myType', id: 'abc', label: 'First object' },
+ * { type: 'myType', id: 'def', label: 'Second object' }
+ * ]
+ *
+ *
+ * WITH xoContainers :
+ *
+ * xoContainers: [
+ * { type: 'containerType', id: 'ghi', label: 'First container' },
+ * { type: 'containerType', id: 'jkl', label: 'Second container' }
+ * ]
+ *
+ * xoObjects: {
+ * ghi: [
+ * { type: 'objectType', id: 'mno', label: 'First object' }
+ * { type: 'objectType', id: 'pqr', label: 'Second object' }
+ * ],
+ * jkl: [
+ * { type: 'objectType', id: 'stu', label: 'Third object' }
+ * ]
+ * }
+ */
+class GenericSelect extends React.Component {
+ static propTypes = {
+ hasSelectAll: PropTypes.bool,
+ multi: PropTypes.bool,
+ onChange: PropTypes.func.isRequired,
+ xoContainers: PropTypes.array,
+ xoObjects: PropTypes.oneOfType([
+ PropTypes.array,
+ PropTypes.objectOf(PropTypes.array),
+ ]).isRequired,
+ }
+
+ _getObjectsById = createSelector(
+ () => this.props.xoObjects,
+ objects =>
+ keyBy(isArray(objects) ? objects : flatten(toArray(objects)), 'id')
+ )
+
+ _getOptions = createSelector(
+ () => this.props.xoContainers,
+ () => this.props.xoObjects,
+ (containers, objects) => {
+ // createCollectionWrapper with a depth?
+ const { name } = this.constructor
+
+ let options
+ if (containers === undefined) {
+ if (__DEV__ && !isArray(objects)) {
+ throw new Error(
+ `${name}: without xoContainers, xoObjects must be an array`
+ )
+ }
+
+ options = map(objects, getOption)
+ } else {
+ if (__DEV__ && isArray(objects)) {
+ throw new Error(
+ `${name}: with xoContainers, xoObjects must be an object`
+ )
+ }
+
+ options = []
+ forEach(containers, container => {
+ options.push({
+ disabled: true,
+ xoItem: container,
+ })
+
+ forEach(objects[container.id], object => {
+ options.push(getOption(object, container))
+ })
+ })
+ }
+
+ const objectsById = this._getObjectsById()
+ const addIfMissing = val => {
+ if (val != null && !(val in objectsById)) {
+ options.push({
+ disabled: true,
+ id: val,
+ label: val,
+ value: val,
+ xoItem: {
+ id: val,
+ removed: true,
+ },
+ })
+ }
+ }
+
+ const values = this._getSelectedIds()
+ if (isArray(values)) {
+ forEach(values, addIfMissing)
+ } else {
+ addIfMissing(values)
+ }
+
+ return options
+ }
+ )
+
+ _getSelectedIds = createSelector(
+ () => this.props.value,
+ createCollectionWrapper(getIds)
+ )
+
+ _getSelectedObjects = (() => {
+ const helper = createSelector(
+ this._getObjectsById,
+ value => value,
+ (objectsById, value) =>
+ isArray(value)
+ ? map(value, value => objectsById[value.value])
+ : objectsById[value.value]
+ )
+ return value => (value == null ? value : helper(value))
+ })()
+
+ _onChange = value => {
+ this.props.onChange(this._getSelectedObjects(value))
+ }
+
+ _selectAll = () => {
+ this._onChange(filter(this._getOptions(), ({ disabled }) => !disabled))
+ }
+
+ // GroupBy: Display option with margin if not disabled and containers exists.
+ _renderOption = option => (
+
+ {renderXoItem(option.xoItem)}
+
+ )
+
+ render () {
+ const { hasSelectAll, xoContainers, xoObjects, ...props } = this.props
+
+ const select = (
+
+ )
+
+ if (!props.multi || !hasSelectAll) {
+ return select
+ }
+
+ // `hasSelectAll` should be provided by react-select after this pull request has been merged:
+ // https://github.com/JedWatson/react-select/pull/748
+ // TODO: remove once it has been merged upstream.
+ return (
+
+ {select}
+
+
+
+
+
+
+
+
+ )
+ }
+}
+
+const makeStoreSelect = (createSelectors, defaultProps) =>
+ uncontrollableInput(options)(
+ connectStore(createSelectors)(props => (
+
+ ))
+ )
+
+const makeSubscriptionSelect = (subscribe, props) =>
+ uncontrollableInput(options)(
+ class extends React.PureComponent {
+ state = {}
+
+ _getFilteredXoContainers = createFilter(
+ () => this.state.xoContainers,
+ () => this.props.containerPredicate
+ )
+
+ _getFilteredXoObjects = createSelector(
+ () => this.state.xoObjects,
+ () => this.state.xoContainers && this._getFilteredXoContainers(),
+ () => this.props.predicate,
+ (xoObjects, xoContainers, predicate) => {
+ if (xoContainers == null) {
+ return filter(xoObjects, predicate)
+ } else {
+ // Filter xoObjects with `predicate`...
+ const filteredObjects = mapValues(xoObjects, xoObjectsGroup =>
+ filter(xoObjectsGroup, predicate)
+ )
+ // ...and keep only those whose xoContainer hasn't been filtered out
+ return pick(
+ filteredObjects,
+ map(xoContainers, container => container.id)
+ )
+ }
+ }
+ )
+
+ componentWillMount () {
+ this.componentWillUnmount = subscribe(::this.setState)
+ }
+
+ render () {
+ return (
+
+ )
+ }
+ }
+ )
+
+// ===================================================================
+// XO objects.
+// ===================================================================
+
+const getPredicate = (state, props) => props.predicate
+
+// ===================================================================
+
+export const SelectHost = makeStoreSelect(
+ () => {
+ const getHostsByPool = createGetObjectsOfType('host')
+ .filter(getPredicate)
+ .sort()
+
+ return {
+ xoObjects: getHostsByPool,
+ }
+ },
+ { placeholder: _('selectHosts') }
+)
+
+// ===================================================================
+
+export const SelectPool = makeStoreSelect(
+ () => ({
+ xoObjects: createGetObjectsOfType('pool')
+ .filter(getPredicate)
+ .sort(),
+ }),
+ { placeholder: _('selectPools') }
+)
+
+// ===================================================================
+
+export const SelectSr = makeStoreSelect(
+ () => {
+ const getSrsByContainer = createGetObjectsOfType('SR')
+ .filter((_, { predicate }) => predicate || isSrWritable)
+ .sort()
+ .groupBy('$container')
+
+ const getContainerIds = createSelector(getSrsByContainer, srsByContainer =>
+ keys(srsByContainer)
+ )
+
+ const getPools = createGetObjectsOfType('pool')
+ .pick(getContainerIds)
+ .sort()
+ const getHosts = createGetObjectsOfType('host')
+ .pick(getContainerIds)
+ .sort()
+
+ const getContainers = createSelector(getPools, getHosts, (pools, hosts) =>
+ pools.concat(hosts)
+ )
+
+ return {
+ xoObjects: getSrsByContainer,
+ xoContainers: getContainers,
+ }
+ },
+ { placeholder: _('selectSrs') }
+)
+
+// ===================================================================
+
+export const SelectVm = makeStoreSelect(
+ () => {
+ const getVmsByContainer = createGetObjectsOfType('VM')
+ .filter(getPredicate)
+ .sort()
+ .groupBy('$container')
+
+ const getContainerIds = createSelector(getVmsByContainer, vmsByContainer =>
+ keys(vmsByContainer)
+ )
+
+ const getPools = createGetObjectsOfType('pool')
+ .pick(getContainerIds)
+ .sort()
+ const getHosts = createGetObjectsOfType('host')
+ .pick(getContainerIds)
+ .sort()
+
+ const getContainers = createSelector(getPools, getHosts, (pools, hosts) =>
+ pools.concat(hosts)
+ )
+
+ return {
+ xoObjects: getVmsByContainer,
+ xoContainers: getContainers,
+ }
+ },
+ { placeholder: _('selectVms') }
+)
+
+// ===================================================================
+
+export const SelectHostVm = makeStoreSelect(
+ () => {
+ const getHosts = createGetObjectsOfType('host')
+ .filter(getPredicate)
+ .sort()
+ const getVms = createGetObjectsOfType('VM')
+ .filter(getPredicate)
+ .sort()
+
+ const getObjects = createSelector(getHosts, getVms, (hosts, vms) =>
+ hosts.concat(vms)
+ )
+
+ return {
+ xoObjects: getObjects,
+ }
+ },
+ { placeholder: _('selectHostsVms') }
+)
+
+// ===================================================================
+
+export const SelectVmTemplate = makeStoreSelect(
+ () => {
+ const getVmTemplatesByPool = createGetObjectsOfType('VM-template')
+ .filter(getPredicate)
+ .sort()
+ .groupBy('$container')
+ const getPools = createGetObjectsOfType('pool')
+ .pick(
+ createSelector(getVmTemplatesByPool, vmTemplatesByPool =>
+ keys(vmTemplatesByPool)
+ )
+ )
+ .sort()
+
+ return {
+ xoObjects: getVmTemplatesByPool,
+ xoContainers: getPools,
+ }
+ },
+ { placeholder: _('selectVmTemplates') }
+)
+
+// ===================================================================
+
+export const SelectNetwork = makeStoreSelect(
+ () => {
+ const getNetworksByPool = createGetObjectsOfType('network')
+ .filter(getPredicate)
+ .sort()
+ .groupBy('$pool')
+ const getPools = createGetObjectsOfType('pool')
+ .pick(
+ createSelector(getNetworksByPool, networksByPool =>
+ keys(networksByPool)
+ )
+ )
+ .sort()
+
+ return {
+ xoObjects: getNetworksByPool,
+ xoContainers: getPools,
+ }
+ },
+ { placeholder: _('selectNetworks') }
+)
+
+// ===================================================================
+
+export const SelectPif = makeStoreSelect(
+ () => {
+ const getPifsByHost = createGetObjectsOfType('PIF')
+ .filter(getPredicate)
+ .sort()
+ .groupBy('$host')
+ const getHosts = createGetObjectsOfType('host')
+ .pick(
+ createSelector(getPifsByHost, networksByPool => keys(networksByPool))
+ )
+ .sort()
+
+ return {
+ xoObjects: getPifsByHost,
+ xoContainers: getHosts,
+ }
+ },
+ { placeholder: _('selectPifs') }
+)
+
+// ===================================================================
+
+export const SelectTag = makeStoreSelect(
+ (_, props) => ({
+ xoObjects: createSelector(
+ createGetTags(
+ 'objects' in props ? (_, props) => props.objects : undefined
+ )
+ .filter(getPredicate)
+ .sort(),
+ tags => map(tags, tag => ({ id: tag, type: 'tag', value: tag }))
+ ),
+ }),
+ { placeholder: _('selectTags') }
+)
+
+export const SelectHighLevelObject = makeStoreSelect(
+ () => {
+ const getHosts = createGetObjectsOfType('host').filter(getPredicate)
+ const getNetworks = createGetObjectsOfType('network').filter(getPredicate)
+ const getPools = createGetObjectsOfType('pool').filter(getPredicate)
+ const getSrs = createGetObjectsOfType('SR').filter(getPredicate)
+ const getVms = createGetObjectsOfType('VM').filter(getPredicate)
+
+ const getHighLevelObjects = createSelector(
+ getHosts,
+ getNetworks,
+ getPools,
+ getSrs,
+ getVms,
+ (hosts, networks, pools, srs, vms) =>
+ sortBy(assign({}, hosts, networks, pools, srs, vms), [
+ 'type',
+ 'name_label',
+ ])
+ )
+
+ return { xoObjects: getHighLevelObjects }
+ },
+ { placeholder: _('selectObjects') }
+)
+
+// ===================================================================
+
+export const SelectVdi = makeStoreSelect(
+ () => {
+ const getSrs = createGetObjectsOfType('SR').filter(
+ (_, props) => props.srPredicate
+ )
+ const getVdis = createGetObjectsOfType('VDI')
+ .filter(
+ createSelector(
+ getSrs,
+ getPredicate,
+ (srs, predicate) =>
+ predicate
+ ? vdi => srs[vdi.$SR] && predicate(vdi)
+ : vdi => srs[vdi.$SR]
+ )
+ )
+ .sort()
+ .groupBy('$SR')
+
+ return {
+ xoObjects: getVdis,
+ xoContainers: getSrs.sort(),
+ }
+ },
+ { placeholder: _('selectVdis') }
+)
+SelectVdi.propTypes = {
+ srPredicate: PropTypes.func,
+}
+
+// ===================================================================
+
+export const SelectVgpuType = makeStoreSelect(
+ () => {
+ const getVgpuTypes = createSelector(
+ createGetObjectsOfType('vgpuType').filter(getPredicate),
+ vgpuTypes => {
+ const gpuGroups = {}
+ forEach(vgpuTypes, vgpuType => {
+ forEach(vgpuType.gpuGroups, gpuGroup => {
+ if (gpuGroups[gpuGroup] === undefined) {
+ gpuGroups[gpuGroup] = []
+ }
+ gpuGroups[gpuGroup].push({
+ ...vgpuType,
+ gpuGroup,
+ })
+ })
+ })
+
+ return gpuGroups
+ }
+ )
+
+ const getGpuGroups = createGetObjectsOfType('gpuGroup')
+ .pick(createSelector(getVgpuTypes, keys))
+ .sort()
+
+ return {
+ xoObjects: getVgpuTypes,
+ xoContainers: getGpuGroups,
+ }
+ },
+ { placeholder: _('selectVgpuType') }
+)
+
+// ===================================================================
+// Objects from subscriptions.
+// ===================================================================
+
+export const SelectSubject = makeSubscriptionSelect(
+ subscriber => {
+ let subjects = {}
+
+ let usersLoaded, groupsLoaded
+ const set = newSubjects => {
+ subjects = newSubjects
+ /* We must wait for groups AND users options to be loaded,
+ * or a previously setted value belonging to one type or another might be discarded
+ * by the internal
+ */
+ if (usersLoaded && groupsLoaded) {
+ subscriber({
+ xoObjects: subjects,
+ })
+ }
+ }
+
+ const unsubscribeGroups = subscribeGroups(groups => {
+ groupsLoaded = true
+ set([...filter(subjects, subject => subject.type === 'user'), ...groups])
+ })
+
+ const unsubscribeUsers = subscribeUsers(users => {
+ usersLoaded = true
+ set([...filter(subjects, subject => subject.type === 'group'), ...users])
+ })
+
+ return () => {
+ unsubscribeGroups()
+ unsubscribeUsers()
+ }
+ },
+ { placeholder: _('selectSubjects') }
+)
+
+// ===================================================================
+
+export const SelectRole = makeSubscriptionSelect(
+ subscriber => {
+ const unsubscribeRoles = subscribeRoles(roles => {
+ const xoObjects = map(sortBy(roles, 'name'), role => ({
+ ...role,
+ type: 'role',
+ }))
+ subscriber({ xoObjects })
+ })
+ return unsubscribeRoles
+ },
+ { placeholder: _('selectRole') }
+)
+
+// ===================================================================
+
+export const SelectRemote = makeSubscriptionSelect(
+ subscriber => {
+ const unsubscribeRemotes = subscribeRemotes(remotes => {
+ const xoObjects = groupBy(
+ map(sortBy(remotes, 'name'), remote => {
+ remote = { ...remote, ...parseRemote(remote.url) }
+ return { id: remote.id, type: 'remote', value: remote }
+ }),
+ remote => remote.value.type
+ )
+
+ subscriber({
+ xoObjects,
+ xoContainers: map(xoObjects, (remote, type) => ({
+ id: type,
+ label: type,
+ })),
+ })
+ })
+
+ return unsubscribeRemotes
+ },
+ { placeholder: _('selectRemotes') }
+)
+
+// ===================================================================
+
+export const SelectResourceSet = makeSubscriptionSelect(
+ subscriber => {
+ const unsubscribeResourceSets = subscribeResourceSets(resourceSets => {
+ const xoObjects = map(
+ sortBy(resolveResourceSets(resourceSets), 'name'),
+ resourceSet => ({ ...resourceSet, type: 'resourceSet' })
+ )
+
+ subscriber({ xoObjects })
+ })
+
+ return unsubscribeResourceSets
+ },
+ { placeholder: _('selectResourceSets') }
+)
+
+// ===================================================================
+
+export class SelectResourceSetsVmTemplate extends React.PureComponent {
+ get value () {
+ return this.refs.select.value
+ }
+
+ set value (value) {
+ this.refs.select.value = value
+ }
+
+ _getTemplates = createSelector(
+ () => this.props.resourceSet,
+ ({ objectsByType }) => {
+ const { predicate } = this.props
+ const templates = objectsByType['VM-template']
+ return sortBy(
+ predicate ? filter(templates, predicate) : templates,
+ 'name_label'
+ )
+ }
+ )
+
+ render () {
+ return (
+
+ )
+ }
+}
+
+// ===================================================================
+
+export class SelectResourceSetsSr extends React.PureComponent {
+ get value () {
+ return this.refs.select.value
+ }
+
+ set value (value) {
+ this.refs.select.value = value
+ }
+ _getSrs = createSelector(
+ () => this.props.resourceSet,
+ ({ objectsByType }) => {
+ const { predicate } = this.props
+ const srs = objectsByType['SR']
+ return sortBy(predicate ? filter(srs, predicate) : srs, 'name_label')
+ }
+ )
+
+ render () {
+ return (
+
+ )
+ }
+}
+
+// ===================================================================
+
+export class SelectResourceSetsVdi extends React.PureComponent {
+ get value () {
+ return this.refs.select.value
+ }
+
+ set value (value) {
+ this.refs.select.value = value
+ }
+
+ _getObject (id) {
+ return getObject(store.getState(), id, true)
+ }
+
+ _getSrs = createSelector(
+ () => this.props.resourceSet,
+ ({ objectsByType }) => {
+ const { srPredicate } = this.props
+ const srs = objectsByType['SR']
+ return srPredicate ? filter(srs, srPredicate) : srs
+ }
+ )
+
+ _getVdis = createSelector(this._getSrs, srs =>
+ sortBy(map(flatten(map(srs, sr => sr.VDIs)), this._getObject), 'name_label')
+ )
+
+ render () {
+ return (
+
+ )
+ }
+}
+
+// ===================================================================
+
+export class SelectResourceSetsNetwork extends React.PureComponent {
+ get value () {
+ return this.refs.select.value
+ }
+
+ set value (value) {
+ this.refs.select.value = value
+ }
+
+ _getNetworks = createSelector(
+ () => this.props.resourceSet,
+ ({ objectsByType }) => {
+ const { predicate } = this.props
+ const networks = objectsByType['network']
+ return sortBy(
+ predicate ? filter(networks, predicate) : networks,
+ 'name_label'
+ )
+ }
+ )
+
+ render () {
+ return (
+
+ )
+ }
+}
+
+// ===================================================================
+
+// Pass a function to @addSubscriptions to ensure subscribeIpPools and subscribeResourceSets
+// are correctly imported before they are called
+@addSubscriptions(() => ({
+ ipPools: subscribeIpPools,
+ resourceSets: subscribeResourceSets,
+}))
+export class SelectResourceSetIp extends React.Component {
+ static propTypes = {
+ containerPredicate: PropTypes.func,
+ predicate: PropTypes.func,
+ resourceSetId: PropTypes.string.isRequired,
+ }
+
+ get value () {
+ return this.refs.select.value
+ }
+
+ set value (value) {
+ this.refs.select.value = value
+ }
+
+ _getResourceSetIpPools = createSelector(
+ () => this.props.ipPools,
+ () => this.props.resourceSets,
+ () => this.props.resourceSetId,
+ (allIpPools, allResourceSets, resourceSetId) => {
+ const { ipPools } = allResourceSets[resourceSetId]
+ return filter(allIpPools, ({ id }) => includes(ipPools, id))
+ }
+ )
+
+ _getIpPools = createSelector(
+ () => this.props.ipPools,
+ () => this.props.containerPredicate,
+ (ipPools, predicate) => (predicate ? filter(ipPools, predicate) : ipPools)
+ )
+
+ _getIps = createSelector(
+ this._getIpPools,
+ () => this.props.predicate,
+ () => this.props.ipPools,
+ (ipPools, predicate, resolvedIpPools) => {
+ return flatten(
+ map(ipPools, ipPool => {
+ const poolIps = map(ipPool.addresses, (address, ip) => ({
+ ...address,
+ id: ip,
+ label: ip,
+ type: 'ipAddress',
+ used: !isEmpty(address.vifs),
+ }))
+ return predicate ? filter(poolIps, predicate) : poolIps
+ })
+ )
+ }
+ )
+
+ render () {
+ return (
+
+ )
+ }
+}
+
+// ===================================================================
+
+export class SelectSshKey extends React.PureComponent {
+ get value () {
+ return this.refs.select.value
+ }
+
+ set value (value) {
+ this.refs.select.value = value
+ }
+
+ componentWillMount () {
+ this.componentWillUnmount = subscribeCurrentUser(user => {
+ this.setState({
+ sshKeys:
+ user &&
+ user.preferences &&
+ map(user.preferences.sshKeys, (key, id) => ({
+ id,
+ label: key.title,
+ type: 'sshKey',
+ })),
+ })
+ })
+ }
+
+ render () {
+ return (
+
+ )
+ }
+}
+
+// ===================================================================
+
+export const SelectIp = makeSubscriptionSelect(
+ subscriber => {
+ const unsubscribeIpPools = subscribeIpPools(ipPools => {
+ const sortedIpPools = sortBy(ipPools, 'name')
+ const xoObjects = mapValues(groupBy(sortedIpPools, 'id'), ipPools =>
+ map(ipPools[0].addresses, (address, ip) => ({
+ ...address,
+ id: ip,
+ label: ip,
+ type: 'ipAddress',
+ used: !isEmpty(address.vifs),
+ }))
+ )
+ const xoContainers = map(sortedIpPools, ipPool => ({
+ ...ipPool,
+ type: 'ipPool',
+ }))
+ subscriber({ xoObjects, xoContainers })
+ })
+
+ return unsubscribeIpPools
+ },
+ { placeholder: _('selectIp') }
+)
+
+// ===================================================================
+
+export const SelectIpPool = makeSubscriptionSelect(
+ subscriber => {
+ const unsubscribeIpPools = subscribeIpPools(ipPools => {
+ subscriber({
+ xoObjects: map(sortBy(ipPools, 'name'), ipPool => ({
+ ...ipPool,
+ type: 'ipPool',
+ })),
+ })
+ })
+
+ return unsubscribeIpPools
+ },
+ { placeholder: _('selectIpPool') }
+)
diff --git a/packages/xo-web/src/common/selectors.js b/packages/xo-web/src/common/selectors.js
new file mode 100644
index 000000000..39be84d1f
--- /dev/null
+++ b/packages/xo-web/src/common/selectors.js
@@ -0,0 +1,542 @@
+import add from 'lodash/add'
+import checkPermissions from 'xo-acl-resolver'
+import { createSelector as create } from 'reselect'
+import {
+ filter,
+ find,
+ forEach,
+ groupBy,
+ isArray,
+ isArrayLike,
+ isFunction,
+ keys,
+ map,
+ orderBy,
+ pickBy,
+ size,
+ slice,
+} from 'lodash'
+
+import invoke from './invoke'
+import shallowEqual from './shallow-equal'
+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,
+} from 'reselect'
+
+// -------------------------------------------------------------------
+
+// Wraps a function which returns a collection to returns the previous
+// result if the collection has not really changed (ie still has the
+// same items).
+//
+// Use case: in connect, to avoid rerendering a component where the
+// objects are still the same.
+const _createCollectionWrapper = selector => {
+ let cache, previous
+
+ return (...args) => {
+ const value = selector(...args)
+ if (value !== previous) {
+ previous = value
+
+ if (!shallowEqual(value, cache)) {
+ cache = value
+ }
+ }
+ return cache
+ }
+}
+export { _createCollectionWrapper as createCollectionWrapper }
+
+const _SELECTOR_PLACEHOLDER = Symbol('selector placeholder')
+
+// Experimental!
+//
+// Similar to reselect's createSelector() but inputs can be either
+// selectors or plain values.
+//
+// To pass a function as a plain value, simply wrap it with an array.
+const _create2 = (...inputs) => {
+ const resultFn = inputs.pop()
+
+ if (inputs.length === 1 && isArray(inputs[0])) {
+ inputs = inputs[0]
+ }
+
+ const n = inputs.length
+
+ const inputSelectors = []
+ for (let i = 0; i < n; ++i) {
+ const input = inputs[i]
+
+ if (isFunction(input)) {
+ inputSelectors.push(input)
+ inputs[i] = _SELECTOR_PLACEHOLDER
+ } else if (isArray(input) && input.length === 1) {
+ inputs[i] = input[0]
+ }
+ }
+
+ if (!inputSelectors.length) {
+ throw new Error('no input selectors')
+ }
+
+ return create(inputSelectors, function () {
+ 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
+ }
+
+ return resultFn.apply(this, args)
+ })
+}
+
+// ===================================================================
+// 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
+ })
+
+// Creates an object selector from an object selector and a properties
+// selector.
+//
+// Should only be used with a reasonable number of properties.
+export const createPicker = (object, props) =>
+ _create2(
+ object,
+ props,
+ _createCollectionWrapper((object, props) => {
+ const values = {}
+ forEach(props, prop => {
+ const value = object[prop]
+ if (value) {
+ values[prop] = value
+ }
+ })
+ return values
+ })
+ )
+
+// Special cases:
+// - predicate == null → no filtering
+// - predicate === false → everything is filtered out
+export const createFilter = (collection, predicate) =>
+ _create2(
+ collection,
+ predicate,
+ _createCollectionWrapper(
+ (collection, predicate) =>
+ predicate === false
+ ? isArrayLike(collection) ? EMPTY_ARRAY : EMPTY_OBJECT
+ : predicate
+ ? (isArrayLike(collection) ? filter : pickBy)(collection, predicate)
+ : collection
+ )
+ )
+
+export const createFinder = (collection, predicate) =>
+ _create2(collection, predicate, find)
+
+export const createGroupBy = (collection, getter) =>
+ _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)
+ })
+ )
+
+export const createSort = (collection, getter = 'name_label', order = 'asc') =>
+ _create2(collection, getter, order, orderBy)
+
+export const createSumBy = (itemsSelector, iterateeSelector) =>
+ _create2(itemsSelector, iterateeSelector, (items, iteratee) =>
+ map(items, iteratee).reduce(add, 0)
+ )
+
+export const createTop = (collection, iteratee, n) =>
+ _create2(
+ collection,
+ iteratee,
+ n,
+ _createCollectionWrapper((objects, iteratee, n) => {
+ const results = orderBy(objects, iteratee, 'desc')
+ if (n < results.length) {
+ results.length = n
+ }
+ return results
+ })
+ )
+
+// ===================================================================
+// Root-ish selectors (no dependencies).
+
+export const areObjectsFetched = state => state.objects.fetched
+
+const _getId = (state, { routeParams, id }) =>
+ routeParams ? routeParams.id : id
+
+export const getLang = state => state.lang
+
+export const getStatus = state => state.status
+
+export const getUser = state => state.user
+
+export const getCheckPermissions = invoke(() => {
+ const getPredicate = create(
+ state => state.permissions,
+ state => state.objects,
+ (permissions, objects) => {
+ objects = objects.all
+ const getObject = id => objects[id] || EMPTY_OBJECT
+
+ return (id, permission) =>
+ checkPermissions(permissions, getObject, id, permission)
+ }
+ )
+
+ const isTrue = () => true
+ const isFalse = () => false
+
+ return state => {
+ const user = getUser(state)
+
+ if (!user) {
+ return isFalse
+ }
+
+ if (user.permission === 'admin') {
+ return isTrue
+ }
+
+ return getPredicate(state)
+ }
+})
+
+const _getPermissionsPredicate = invoke(() => {
+ const getPredicate = create(
+ state => state.permissions,
+ state => state.objects,
+ (permissions, objects) => {
+ objects = objects.all
+ const getObject = id => objects[id] || EMPTY_OBJECT
+
+ return id => checkPermissions(permissions, getObject, id.id || id, 'view')
+ }
+ )
+
+ return state => {
+ const user = getUser(state)
+ if (!user) {
+ return false
+ }
+
+ if (user.permission === 'admin') {
+ return // No predicate means no filtering.
+ }
+
+ return getPredicate(state)
+ }
+})
+
+export const isAdmin = (...args) => {
+ const user = getUser(...args)
+
+ return user && user.permission === 'admin'
+}
+
+// ===================================================================
+// Common selector creators.
+
+// Creates an object selector from an id selector.
+export const createGetObject = (idSelector = _getId) => (
+ state,
+ props,
+ useResourceSet
+) => {
+ const object = state.objects.all[idSelector(state, props)]
+ if (!object) {
+ return
+ }
+
+ if (useResourceSet) {
+ return object
+ }
+
+ const predicate = _getPermissionsPredicate(state)
+
+ if (!predicate) {
+ if (predicate == null) {
+ return object // no filtering
+ }
+
+ // predicate is false.
+ return
+ }
+
+ if (predicate(object)) {
+ return object
+ }
+}
+
+// Specialized createSort() configured for a given type.
+export const createSortForType = invoke(() => {
+ const iterateesByType = {
+ message: message => message.time,
+ PIF: pif => pif.device,
+ pool: pool => pool.name_label,
+ pool_patch: patch => patch.name,
+ tag: tag => tag,
+ VBD: vbd => vbd.position,
+ 'VDI-snapshot': snapshot => snapshot.snapshot_time,
+ 'VM-snapshot': snapshot => snapshot.snapshot_time,
+ }
+ 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',
+ }
+ const getOrders = type => ordersByType[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)
+ )
+})
+
+// Add utility methods to a collection selector.
+const _extendCollectionSelector = (selector, objectsType) => {
+ // Terminal methods.
+ const _addCount = selector => {
+ selector.count = predicate => createCounter(selector, predicate)
+ return selector
+ }
+ _addCount(selector)
+ const _addGroupBy = selector => {
+ selector.groupBy = getter => createGroupBy(selector, getter)
+ return selector
+ }
+ _addGroupBy(selector)
+ const _addFind = selector => {
+ selector.find = predicate => createFinder(selector, predicate)
+ return selector
+ }
+ _addFind(selector)
+
+ // groupBy can be chained.
+ const _addSort = selector => {
+ // TODO: maybe memoize when no idsSelector.
+ selector.sort = () => _addGroupBy(createSortForType(objectsType, selector))
+ return selector
+ }
+ _addSort(selector)
+
+ // count, groupBy and sort can be chained.
+ const _addFilter = selector => {
+ 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))))
+ )
+
+ return selector
+}
+
+// Creates a collection selector which returns all objects of a given
+// type.
+//
+// The selector as the following methods:
+//
+// - count: returns a selector which returns the number of objects
+// - filter: returns a selector which returns the objects filtered by
+// a predicate (count, groupBy and sort can be chained)
+// - find: returns a selector which returns the first object matching
+// a predicate
+// - groupBy: returns a selector which returns the objects grouped by
+// a value determined by a getter selector
+// - pick: returns a selector which returns only the objects with given
+// ids (filter, find, groupBy and sort can be chained)
+// - sort: returns a selector which returns the objects appropriately
+// sorted (groupBy can be chained)
+export const createGetObjectsOfType = type => {
+ const getObjects = isFunction(type)
+ ? (state, props) => state.objects.byType[type(state, props)] || EMPTY_OBJECT
+ : state => state.objects.byType[type] || EMPTY_OBJECT
+
+ return _extendCollectionSelector(
+ createFilter(getObjects, _getPermissionsPredicate),
+ type
+ )
+}
+
+export const createGetTags = collectionSelectors => {
+ if (!collectionSelectors) {
+ collectionSelectors = [
+ createGetObjectsOfType('host'),
+ createGetObjectsOfType('pool'),
+ createGetObjectsOfType('VM'),
+ ]
+ }
+
+ 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)
+ })
+
+ return _extendCollectionSelector(getTags, 'tag')
+}
+
+export const createGetVmLastShutdownTime = (
+ getVmId = (_, { vm }) => (vm != null ? vm.id : undefined)
+) =>
+ create(getVmId, createGetObjectsOfType('message'), (vmId, messages) => {
+ let max = null
+ forEach(messages, message => {
+ if (
+ message.$object === vmId &&
+ message.name === 'VM_SHUTDOWN' &&
+ (max === null || message.time > max)
+ ) {
+ max = message.time
+ }
+ })
+ return max
+ })
+
+export const createGetObjectMessages = objectSelector =>
+ createGetObjectsOfType('message')
+ .filter(
+ create(
+ (...args) => objectSelector(...args).id,
+ id => message => message.$object === id
+ )
+ )
+ .sort()
+
+// Example of use:
+// import store from 'store'
+// const object = getObject(store.getState(), objectId)
+// ...
+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)
+ )
+ )
+ .find([
+ ({ guidance }) =>
+ find(
+ guidance,
+ action => action === 'restartHost' || action === 'restartXapi'
+ ),
+ ])
+
+ return create(
+ hostSelector,
+ (...args) => args,
+ (host, args) => host.rebootRequired || !!patchRequiresReboot(...args)
+ )
+}
+
+export const createGetHostMetrics = hostSelector =>
+ create(
+ hostSelector,
+ _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 =>
+ createGetObjectsOfType('VDI').pick(
+ create(
+ createGetObjectsOfType('VBD').pick(
+ (state, props) => vmSelector(state, props).$VBDs
+ ),
+ _createCollectionWrapper(vbds =>
+ map(vbds, vbd => (vbd.is_cd_drive ? undefined : vbd.VDI))
+ )
+ )
+ )
diff --git a/packages/xo-web/src/common/shallow-equal.js b/packages/xo-web/src/common/shallow-equal.js
new file mode 100644
index 000000000..de61165ac
--- /dev/null
+++ b/packages/xo-web/src/common/shallow-equal.js
@@ -0,0 +1,49 @@
+import kindOf from 'kindof'
+
+// Tests that two collections (arrays or objects) have strictly equals
+// values (items or properties)
+const shallowEqual = (c1, c2) => {
+ if (c1 === c2) {
+ return true
+ }
+
+ const type = kindOf(c1)
+ if (type !== kindOf(c2)) {
+ return false
+ }
+
+ if (type === 'array') {
+ const { length } = c1
+ if (length !== c2.length) {
+ return false
+ }
+
+ for (let i = 0; i < length; ++i) {
+ if (c1[i] !== c2[i]) {
+ return false
+ }
+ }
+
+ return true
+ }
+
+ if (type !== 'object') {
+ return false
+ }
+
+ let n = 0
+ // eslint-disable-next-line no-unused-vars
+ for (const _ in c2) {
+ ++n
+ }
+
+ for (const key in c1) {
+ if (c1[key] !== c2[key]) {
+ return false
+ }
+ --n
+ }
+
+ return !n
+}
+export { shallowEqual as default }
diff --git a/packages/xo-web/src/common/shortcuts.js b/packages/xo-web/src/common/shortcuts.js
new file mode 100644
index 000000000..84a23b167
--- /dev/null
+++ b/packages/xo-web/src/common/shortcuts.js
@@ -0,0 +1,35 @@
+import Component from 'base-component'
+import forEach from 'lodash/forEach'
+import React from 'react'
+import remove from 'lodash/remove'
+import { Shortcuts as ReactShortcuts } from 'react-shortcuts'
+
+let enabled = true
+const instances = []
+
+const updateInstances = () => {
+ forEach(instances, instance => instance.forceUpdate())
+}
+
+export const enable = () => {
+ enabled = true
+ updateInstances()
+}
+
+export const disable = () => {
+ enabled = false
+ updateInstances()
+}
+
+export default class Shortcuts extends Component {
+ componentDidMount () {
+ instances.push(this)
+ }
+ componentWillUnmount () {
+ remove(instances, this)
+ }
+
+ render () {
+ return enabled ? : null
+ }
+}
diff --git a/packages/xo-web/src/common/single-line-row.js b/packages/xo-web/src/common/single-line-row.js
new file mode 100644
index 000000000..6daff17be
--- /dev/null
+++ b/packages/xo-web/src/common/single-line-row.js
@@ -0,0 +1,18 @@
+import React, { cloneElement } from 'react'
+
+import propTypes from './prop-types-decorator'
+
+const SINGLE_LINE_STYLE = { display: 'flex' }
+const COL_STYLE = { marginTop: 'auto', marginBottom: 'auto' }
+
+const SingleLineRow = propTypes({
+ className: propTypes.string,
+})(({ children, className }) => (
+
+ {React.Children.map(
+ children,
+ child => child && cloneElement(child, { style: COL_STYLE })
+ )}
+
+))
+export { SingleLineRow as default }
diff --git a/packages/xo-web/src/common/smart-backup-pattern.js b/packages/xo-web/src/common/smart-backup-pattern.js
new file mode 100644
index 000000000..d75c4da08
--- /dev/null
+++ b/packages/xo-web/src/common/smart-backup-pattern.js
@@ -0,0 +1,53 @@
+import * as CM from 'complex-matcher'
+import { flatten, identity, map } from 'lodash'
+
+import { EMPTY_OBJECT } from './utils'
+
+export const destructPattern = (pattern, valueTransform = identity) =>
+ pattern && {
+ not: !!pattern.__not,
+ values: valueTransform((pattern.__not || pattern).__or),
+ }
+
+export const constructPattern = (
+ { not, values } = EMPTY_OBJECT,
+ valueTransform = identity
+) => {
+ if (values == null || !values.length) {
+ return
+ }
+
+ const pattern = { __or: valueTransform(values) }
+ return not ? { __not: pattern } : pattern
+}
+
+const parsePattern = pattern => {
+ const patternValues = flatten(
+ pattern.__not !== undefined ? pattern.__not.__or : pattern.__or
+ )
+
+ const queryString = new CM.Or(
+ map(patternValues, array => new CM.String(array))
+ )
+ return pattern.__not !== undefined ? CM.Not(queryString) : queryString
+}
+
+export const constructQueryString = pattern => {
+ const powerState = pattern.power_state
+ const pool = pattern.$pool
+ const tags = pattern.tags
+
+ const filter = []
+
+ if (powerState !== undefined) {
+ filter.push(new CM.Property('power_state', new CM.String(powerState)))
+ }
+ if (pool !== undefined) {
+ filter.push(new CM.Property('$pool', parsePattern(pool)))
+ }
+ if (tags !== undefined) {
+ filter.push(new CM.Property('tags', parsePattern(tags)))
+ }
+
+ return filter.length !== 0 ? new CM.And(filter).toString() : ''
+}
diff --git a/packages/xo-web/src/common/sorted-table/index.css b/packages/xo-web/src/common/sorted-table/index.css
new file mode 100644
index 000000000..7f1d88a00
--- /dev/null
+++ b/packages/xo-web/src/common/sorted-table/index.css
@@ -0,0 +1,17 @@
+.clickableColumn {
+ cursor: pointer;
+}
+
+.clickableColumn:hover {
+ color: #fff;
+ background-color: #96b8d1;
+}
+
+.clickableRow {
+ cursor: pointer;
+}
+
+.highlight {
+ outline: 2px solid #366e98;
+ outline-offset: -2px;
+}
diff --git a/packages/xo-web/src/common/sorted-table/index.js b/packages/xo-web/src/common/sorted-table/index.js
new file mode 100644
index 000000000..1852ac7ac
--- /dev/null
+++ b/packages/xo-web/src/common/sorted-table/index.js
@@ -0,0 +1,916 @@
+import * as CM from 'complex-matcher'
+import _ from 'intl'
+import classNames from 'classnames'
+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 { Input as DebouncedInput } from 'debounce-input-decorator'
+import { Portal } from 'react-overlays'
+import { routerShape } from 'react-router/lib/PropTypes'
+import { Set } from 'immutable'
+import { Dropdown, MenuItem } 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 Pagination from '../pagination'
+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 {
+ createCounter,
+ createFilter,
+ createPager,
+ createSelector,
+ createSort,
+} from '../selectors'
+
+import styles from './index.css'
+
+// ===================================================================
+
+@propTypes({
+ filters: propTypes.object,
+ onChange: propTypes.func.isRequired,
+ value: propTypes.string.isRequired,
+})
+class TableFilter extends Component {
+ _cleanFilter = () => this._setFilter('')
+
+ _setFilter = filterValue => {
+ const filter = this.refs.filter.getWrappedInstance()
+ filter.value = filterValue
+ filter.focus()
+ this.props.onChange(filterValue)
+ }
+
+ _onChange = event => {
+ this.props.onChange(event.target.value)
+ }
+
+ focus () {
+ this.refs.filter.getWrappedInstance().focus()
+ }
+
+ render () {
+ const { props } = this
+
+ return (
+
+ {isEmpty(props.filters) ? (
+
+
+
+ ) : (
+
+
+
+
+
+
+ {map(props.filters, (filter, label) => (
+ this._setFilter(filter)}>
+ {_(label)}
+
+ ))}
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+ }
+}
+
+// ===================================================================
+
+@propTypes({
+ columnId: propTypes.number.isRequired,
+ name: propTypes.node,
+ sort: propTypes.func,
+ sortIcon: propTypes.string,
+})
+class ColumnHead extends Component {
+ _sort = () => {
+ const { props } = this
+ props.sort(props.columnId)
+ }
+
+ render () {
+ const { name, sortIcon, textAlign } = this.props
+
+ if (!this.props.sort) {
+ return {name}
+ }
+
+ const isSelected = sortIcon === 'asc' || sortIcon === 'desc'
+
+ return (
+
+ {name}
+
+
+
+
+ )
+ }
+}
+
+// ===================================================================
+
+@propTypes({
+ 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
+ }
+}
+
+// ===================================================================
+
+const actionsShape = propTypes.arrayOf(
+ propTypes.shape({
+ // groupedActions: the function will be called with an array of the selected items in parameters
+ // individualActions: the function will be called with the related item in parameters
+ disabled: propTypes.oneOfType([propTypes.bool, propTypes.func]),
+ handler: propTypes.func.isRequired,
+ icon: propTypes.string.isRequired,
+ label: propTypes.node.isRequired,
+ level: propTypes.oneOf(['primary', 'warning', 'danger']),
+ })
+)
+
+class IndividualAction extends Component {
+ _getIsDisabled = createSelector(
+ () => this.props.disabled,
+ () => this.props.item,
+ () => this.props.userData,
+ (disabled, item, userData) =>
+ isFunction(disabled) ? disabled(item, userData) : disabled
+ )
+
+ render () {
+ const { icon, label, level, handler, item } = this.props
+
+ return (
+
+ )
+ }
+}
+
+class GroupedAction extends Component {
+ _getIsDisabled = createSelector(
+ () => this.props.disabled,
+ () => this.props.selectedItems,
+ () => this.props.userData,
+ (disabled, selectedItems, userData) =>
+ isFunction(disabled) ? disabled(selectedItems, userData) : disabled
+ )
+
+ render () {
+ const { icon, label, level, handler, selectedItems } = this.props
+
+ return (
+
+ )
+ }
+}
+
+// page number and sort info are optional for backward compatibility
+const URL_STATE_RE = /^(?:(\d+)(?:_(\d+)(_desc)?)?-)?(.*)$/
+
+@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,
+ actions: propTypes.arrayOf(
+ propTypes.shape({
+ // regroup individual actions and grouped actions
+ disabled: propTypes.oneOfType([propTypes.bool, propTypes.func]),
+ handler: propTypes.func.isRequired,
+ icon: propTypes.string.isRequired,
+ individualHandler: propTypes.func,
+ label: propTypes.node.isRequired,
+ level: propTypes.oneOf(['primary', 'warning', 'danger']),
+ })
+ ),
+ 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 {
+ static defaultProps = {
+ itemsPerPage: 10,
+ }
+
+ constructor (props, context) {
+ super(props, context)
+
+ let selectedColumn = props.defaultColumn
+ if (selectedColumn == null) {
+ selectedColumn = findIndex(props.columns, 'default')
+
+ if (selectedColumn === -1) {
+ selectedColumn = 0
+ }
+ }
+
+ const state = (this.state = {
+ all: false, // whether all items are selected (accross pages)
+ filter: defined(() => props.filters[props.defaultFilter], ''),
+ page: 1,
+ selectedColumn,
+ sortOrder:
+ props.columns[selectedColumn].sortOrder === 'desc' ? 'desc' : 'asc',
+ })
+
+ const urlState = get(
+ () => context.router.location.query[props.stateUrlParam]
+ )
+
+ let matches
+ if (
+ urlState !== undefined &&
+ (matches = URL_STATE_RE.exec(urlState)) !== null
+ ) {
+ state.filter = matches[4]
+ const page = matches[1]
+ if (page !== undefined) {
+ state.page = +page
+ }
+ let selectedColumn = matches[2]
+ if (
+ selectedColumn !== undefined &&
+ (selectedColumn = +selectedColumn) < props.columns.length
+ ) {
+ state.selectedColumn = selectedColumn
+ state.sortOrder = matches[3] !== undefined ? 'desc' : 'asc'
+ }
+ }
+
+ this._getSelectedColumn = () =>
+ this.props.columns[this.state.selectedColumn]
+
+ this._getTotalNumberOfItems = createCounter(() => this.props.collection)
+
+ const createMatcher = str => CM.parse(str).createPredicate()
+ this._getItems = createSort(
+ createFilter(
+ () => this.props.collection,
+ createSelector(() => this.state.filter, createMatcher)
+ ),
+ createSelector(
+ () => this._getSelectedColumn().sortCriteria,
+ () => this.props.userData,
+ (sortCriteria, userData) =>
+ typeof sortCriteria === 'function'
+ ? object => sortCriteria(object, userData)
+ : sortCriteria
+ ),
+ () => this.state.sortOrder
+ )
+
+ this._getVisibleItems = createPager(
+ this._getItems,
+ () => this.state.page,
+ () => this.props.itemsPerPage
+ )
+
+ state.selectedItemsIds = new Set()
+
+ this._getSelectedItems = createSelector(
+ () => this.state.all,
+ () => this.state.selectedItemsIds,
+ this._getItems,
+ (all, selectedItemsIds, items) =>
+ all ? items : filter(items, item => selectedItemsIds.has(item.id))
+ )
+
+ this._hasGroupedActions = createSelector(
+ this._getGroupedActions,
+ 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
+ }
+ }
+ )
+ }
+
+ componentDidMount () {
+ this._checkUpdatePage()
+
+ // Force one Portal refresh.
+ // Because Portal cannot see the container reference at first rendering.
+ if (this.props.paginationContainer) {
+ this.forceUpdate()
+ }
+ }
+
+ _sort = columnId => {
+ const { state } = this
+ let sortOrder
+
+ if (state.selectedColumn === columnId) {
+ sortOrder = state.sortOrder === 'desc' ? 'asc' : 'desc'
+ } else {
+ sortOrder =
+ this.props.columns[columnId].sortOrder === 'desc' ? 'desc' : 'asc'
+ }
+
+ this._setVisibleState({
+ selectedColumn: columnId,
+ sortOrder,
+ })
+ }
+
+ componentDidUpdate () {
+ const { selectedItemsIds } = this.state
+
+ // 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 = () => {
+ const { filter, page, selectedColumn, sortOrder } = this.state
+ const { router } = this.context
+ const { location } = router
+ router.replace({
+ ...location,
+ query: {
+ ...location.query,
+ [this.props.stateUrlParam]: `${page}_${selectedColumn}${
+ sortOrder === 'desc' ? '_desc' : ''
+ }-${filter}`,
+ },
+ })
+ }
+
+ // update state in the state and update the URL param
+ _setVisibleState (state) {
+ this.setState(state, this.props.stateUrlParam && this._saveUrlState)
+ }
+
+ _setFilter = filter => {
+ this._setVisibleState({
+ filter,
+ page: 1,
+ highlighted: undefined,
+ })
+ }
+
+ _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)
+ }
+
+ const last = ceil(n / itemsPerPage)
+ if (page > last) {
+ return this._setPage(last)
+ }
+ }
+
+ _setPage (page) {
+ this._setVisibleState({ page })
+ }
+ _setPage = this._setPage.bind(this)
+
+ _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)
+ }
+
+ _getGroupedActions = createSelector(
+ () => this.props.groupedActions,
+ () => this.props.actions,
+ (groupedActions, actions) =>
+ groupedActions !== undefined && actions !== undefined
+ ? groupedActions.concat(actions)
+ : groupedActions || actions
+ )
+
+ _renderItem = (item, i) => {
+ const { props, state } = this
+ const { actions, individualActions, rowAction, rowLink, userData } = props
+
+ const hasGroupedActions = this._hasGroupedActions()
+ const hasIndividualActions =
+ !isEmpty(individualActions) || !isEmpty(actions)
+
+ const columns = map(
+ props.columns,
+ ({ component: Component, itemRenderer, textAlign }, key) => (
+
+ {Component !== undefined ? (
+
+ ) : (
+ itemRenderer(item, userData)
+ )}
+
+ )
+ )
+
+ const { id = i } = item
+
+ const selectionColumn = hasGroupedActions && (
+
+
+
+ )
+ const actionsColumn = hasIndividualActions && (
+
+
+
+ {map(individualActions, (props, key) => (
+
+ ))}
+ {map(actions, (props, key) => (
+
+ ))}
+
+
+
+ )
+
+ return rowLink != null ? (
+
+ {selectionColumn}
+ {columns}
+ {actionsColumn}
+
+ ) : (
+ rowAction(item, userData))}
+ >
+ {selectionColumn}
+ {columns}
+ {actionsColumn}
+
+ )
+ }
+
+ render () {
+ const { props, state } = this
+ const {
+ actions,
+ filterContainer,
+ individualActions,
+ itemsPerPage,
+ paginationContainer,
+ shortcutsTarget,
+ userData,
+ } = props
+ const { all } = state
+ const groupedActions = this._getGroupedActions()
+
+ const nAllItems = this._getTotalNumberOfItems()
+ const nItems = this._getItems().length
+ const nSelectedItems = state.selectedItemsIds.size
+ const nVisibleItems = this._getVisibleItems().length
+
+ const hasGroupedActions = this._hasGroupedActions()
+ const hasIndividualActions =
+ !isEmpty(individualActions) || !isEmpty(actions)
+
+ const nColumns = props.columns.length + (hasIndividualActions ? 2 : 1)
+
+ const displayPagination =
+ paginationContainer === undefined && itemsPerPage < nAllItems
+ const displayFilter = nAllItems !== 0
+
+ const paginationInstance = displayPagination && (
+
+ )
+
+ const filterInstance = displayFilter && (
+
+ )
+
+ return (
+
+ {shortcutsTarget !== undefined && (
+
+ )}
+
+
+
+
+ {nItems === nAllItems
+ ? _('sortedTableNumberOfItems', { nTotal: nItems })
+ : _('sortedTableNumberOfFilteredItems', {
+ nFiltered: nItems,
+ nTotal: nAllItems,
+ })}
+ {all ? (
+
+ {' '}
+ -{' '}
+
+ {_('sortedTableAllItemsSelected')}
+
+
+ ) : (
+ nSelectedItems !== 0 && (
+
+ {' '}
+ -{' '}
+ {_('sortedTableNumberOfSelectedItems', {
+ nSelected: nSelectedItems,
+ })}
+ {nSelectedItems === nVisibleItems &&
+ nSelectedItems < nItems && (
+
+ {_('sortedTableSelectAllItems')}
+
+ )}
+
+ )
+ )}
+ {nSelectedItems !== 0 && (
+
+
+ {map(groupedActions, (props, key) => (
+
+ ))}
+
+
+ )}
+
+
+
+ {hasGroupedActions && (
+
+
+
+ )}
+ {map(props.columns, (column, key) => (
+
+ ))}
+ {hasIndividualActions && }
+
+
+
+ {nVisibleItems !== 0 ? (
+ map(this._getVisibleItems(), this._renderItem)
+ ) : (
+
+
+ {_('sortedTableNoItems')}
+
+
+ )}
+
+
+ {(displayFilter || displayPagination) && (
+
+
+
+ {displayPagination &&
+ (paginationContainer !== undefined ? (
+ // Rebuild container function to refresh Portal component.
+ paginationContainer()}>
+ {paginationInstance}
+
+ ) : (
+ paginationInstance
+ ))}
+
+
+ {displayFilter &&
+ (filterContainer ? (
+ filterContainer()}>
+ {filterInstance}
+
+ ) : (
+ filterInstance
+ ))}
+
+
+
+ )}
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/common/state-button.js b/packages/xo-web/src/common/state-button.js
new file mode 100644
index 000000000..2d9d7f21f
--- /dev/null
+++ b/packages/xo-web/src/common/state-button.js
@@ -0,0 +1,46 @@
+import React from 'react'
+import styled from 'styled-components'
+import { omit } from 'lodash'
+
+import ActionButton from './action-button'
+import propTypes from './prop-types-decorator'
+
+// do not forward `state` to ActionButton
+const Button = styled(p => )`
+ background-color: ${p =>
+ p.theme[`${p.state ? 'enabled' : 'disabled'}StateBg`]};
+ border: 2px solid
+ ${p => p.theme[`${p.state ? 'enabled' : 'disabled'}StateColor`]};
+ color: ${p => p.theme[`${p.state ? 'enabled' : 'disabled'}StateColor`]};
+`
+
+const StateButton = ({
+ disabledHandler,
+ disabledHandlerParam,
+ disabledLabel,
+ disabledTooltip,
+
+ enabledLabel,
+ enabledTooltip,
+ enabledHandler,
+ enabledHandlerParam,
+
+ state,
+ ...props
+}) => (
+
+ {state ? enabledLabel : disabledLabel}
+
+)
+
+export default propTypes({
+ state: propTypes.bool.isRequired,
+})(StateButton)
diff --git a/packages/xo-web/src/common/store/actions.js b/packages/xo-web/src/common/store/actions.js
new file mode 100644
index 000000000..62e16a75a
--- /dev/null
+++ b/packages/xo-web/src/common/store/actions.js
@@ -0,0 +1,52 @@
+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 action
+ })({ type }),
+ 'toString',
+ { value: () => type }
+ )
+})()
+
+// ===================================================================
+
+export const selectLang = createAction('SELECT_LANG', lang => lang)
+
+// ===================================================================
+
+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 signedIn = createAction('SIGNED_IN', user => user)
+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
+)
diff --git a/packages/xo-web/src/common/store/index.js b/packages/xo-web/src/common/store/index.js
new file mode 100644
index 000000000..e606cb1c8
--- /dev/null
+++ b/packages/xo-web/src/common/store/index.js
@@ -0,0 +1,18 @@
+import reduxThunk from 'redux-thunk'
+import { applyMiddleware, combineReducers, createStore } from 'redux'
+
+import { connectStore as connectXo } from '../xo'
+
+import reducer from './reducer'
+
+// ===================================================================
+
+const store = createStore(combineReducers(reducer), applyMiddleware(reduxThunk))
+
+connectXo(store)
+
+if (process.env.XOA_PLAN < 5) {
+ require('xoa-updater').connectStore(store)
+}
+
+export default store
diff --git a/packages/xo-web/src/common/store/reducer.js b/packages/xo-web/src/common/store/reducer.js
new file mode 100644
index 000000000..0cc75dd19
--- /dev/null
+++ b/packages/xo-web/src/common/store/reducer.js
@@ -0,0 +1,166 @@
+import cookies from 'cookies-js'
+
+import invoke from '../invoke'
+
+import * as actions from './actions'
+
+// ===================================================================
+
+const createAsyncHandler = ({ error, next }) => (state, payload, action) => {
+ if (action.error) {
+ if (error) {
+ return error(state, payload, action)
+ }
+ } else {
+ if (next) {
+ return next(state, payload, action)
+ }
+ }
+
+ return state
+}
+
+// Action handlers are reducers but bound to a specific action.
+const combineActionHandlers = invoke(
+ Object.hasOwnProperty,
+ obj => {
+ for (const prop in obj) {
+ return prop
+ }
+ },
+ (has, firstProp) => (initialState, handlers) => {
+ let n = 0
+ for (const actionType in handlers) {
+ if (has.call(handlers, actionType)) {
+ if (actionType === 'undefined') {
+ throw new Error('invalid action type: undefined')
+ }
+
+ ++n
+
+ const handler = handlers[actionType]
+ if (typeof handler === 'object') {
+ handlers[actionType] = createAsyncHandler(handler)
+ }
+ }
+ }
+
+ if (!n) {
+ throw new Error('no action handlers declared')
+ }
+
+ // Optimization for this special case.
+ if (n === 1) {
+ const actionType = firstProp(handlers)
+ const handler = handlers[actionType]
+
+ 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
+ }
+ }
+)
+
+// ===================================================================
+
+export default {
+ lang: combineActionHandlers(cookies.get('lang') || 'en', {
+ [actions.selectLang]: (_, lang) => {
+ cookies.set('lang', lang)
+
+ return lang
+ },
+ }),
+
+ 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
+ }
+
+ for (const id in updates) {
+ const object = updates[id]
+ const previous = all[id]
+
+ if (object) {
+ const { type } = object
+
+ all[id] = object
+ get(type)[id] = object
+
+ if (previous && previous.type !== type) {
+ delete get(previous.type)[id]
+ }
+ } else if (previous) {
+ delete all[id]
+ delete get(previous.type)[id]
+ }
+ }
+
+ return { all, byType, fetched: true }
+ },
+ }
+ ),
+
+ user: combineActionHandlers(null, {
+ [actions.signedIn]: {
+ next: (_, user) => user,
+ },
+ }),
+
+ status: combineActionHandlers('disconnected', {
+ [actions.connected]: () => 'connected',
+ [actions.disconnected]: () => 'disconnected',
+ }),
+
+ xoaUpdaterState: combineActionHandlers('disconnected', {
+ [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
+ },
+ }
+ ),
+}
diff --git a/packages/xo-web/src/common/tab-button.js b/packages/xo-web/src/common/tab-button.js
new file mode 100644
index 000000000..973091fb4
--- /dev/null
+++ b/packages/xo-web/src/common/tab-button.js
@@ -0,0 +1,33 @@
+import React from 'react'
+
+import _ from './intl'
+import ActionButton from './action-button'
+import Icon from './icon'
+import Link from './link'
+
+const STYLE = {
+ marginBottom: '1em',
+ marginLeft: '1em',
+}
+
+const TabButton = ({ labelId, ...props }) => (
+
+ {labelId !== undefined && (
+ {_(labelId)}
+ )}
+
+)
+export { TabButton as default }
+
+export const TabButtonLink = ({ labelId, icon, ...props }) => (
+
+
+ {icon && (
+
+ {' '}
+
+ )}
+ {_(labelId)}
+
+
+)
diff --git a/packages/xo-web/src/common/tags.js b/packages/xo-web/src/common/tags.js
new file mode 100644
index 000000000..1a0fdb21e
--- /dev/null
+++ b/packages/xo-web/src/common/tags.js
@@ -0,0 +1,147 @@
+import filter from 'lodash/filter'
+import includes from 'lodash/includes'
+import map from 'lodash/map'
+import React from 'react'
+
+import Component from './base-component'
+import Icon from './icon'
+import propTypes from './prop-types-decorator'
+
+const INPUT_STYLE = {
+ margin: '2px',
+ maxWidth: '4em',
+}
+const TAG_STYLE = {
+ backgroundColor: '#2598d9',
+ borderRadius: '0.5em',
+ color: 'white',
+ fontSize: '0.6em',
+ margin: '0.2em',
+ marginTop: '-0.1em',
+ padding: '0.3em',
+ verticalAlign: 'middle',
+}
+const LINK_STYLE = {
+ cursor: 'pointer',
+}
+const ADD_TAG_STYLE = {
+ cursor: 'pointer',
+ fontSize: '0.8em',
+ marginLeft: '0.2em',
+}
+const REMOVE_TAG_STYLE = {
+ cursor: 'pointer',
+}
+
+@propTypes({
+ labels: propTypes.arrayOf(React.PropTypes.string).isRequired,
+ onAdd: propTypes.func,
+ onChange: propTypes.func,
+ onClick: propTypes.func,
+ onDelete: propTypes.func,
+})
+export default class Tags extends Component {
+ componentWillMount () {
+ this.setState({ editing: false })
+ }
+
+ _startEdit = () => {
+ this.setState({ editing: true })
+ }
+ _stopEdit = () => {
+ this.setState({ editing: false })
+ }
+
+ _addTag = newTag => {
+ const { labels, onAdd, onChange } = this.props
+
+ if (!includes(labels, newTag)) {
+ onAdd && onAdd(newTag)
+ onChange && onChange([...labels, newTag])
+ }
+ }
+ _deleteTag = tag => {
+ const { onChange, onDelete } = this.props
+
+ onDelete && onDelete(tag)
+ onChange && onChange(filter(this.props.labels, t => t !== tag))
+ }
+
+ _onKeyDown = event => {
+ const { keyCode, target } = event
+
+ if (keyCode === 13) {
+ if (target.value) {
+ this._addTag(target.value)
+ target.value = ''
+ }
+ } else if (keyCode === 27) {
+ this._stopEdit()
+ } else {
+ return
+ }
+
+ event.preventDefault()
+ }
+
+ render () {
+ const { labels, onAdd, onChange, onClick, onDelete } = this.props
+
+ const deleteTag = (onDelete || onChange) && this._deleteTag
+
+ return (
+
+ {' '}
+
+ {map(labels.sort(), (label, index) => (
+
+ ))}
+
+ {(onAdd || onChange) && !this.state.editing ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+ )
+ }
+}
+
+export const Tag = ({ type, label, onDelete, onClick }) => (
+
+ onClick(label))}
+ style={onClick && LINK_STYLE}
+ >
+ {label}
+ {' '}
+ {onDelete ? (
+ onDelete(label))}
+ style={REMOVE_TAG_STYLE}
+ >
+
+
+ ) : (
+ []
+ )}
+
+)
+Tag.propTypes = {
+ label: React.PropTypes.string.isRequired,
+}
diff --git a/packages/xo-web/src/common/themes/.index-modules b/packages/xo-web/src/common/themes/.index-modules
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/xo-web/src/common/themes/base.js b/packages/xo-web/src/common/themes/base.js
new file mode 100644
index 000000000..34f7bcb81
--- /dev/null
+++ b/packages/xo-web/src/common/themes/base.js
@@ -0,0 +1,6 @@
+export default {
+ disabledStateBg: '#fff',
+ disabledStateColor: '#c0392b',
+ enabledStateBg: '#fff',
+ enabledStateColor: '#27ae60',
+}
diff --git a/packages/xo-web/src/common/timezone-picker.js b/packages/xo-web/src/common/timezone-picker.js
new file mode 100644
index 000000000..4a2e213da
--- /dev/null
+++ b/packages/xo-web/src/common/timezone-picker.js
@@ -0,0 +1,99 @@
+import ActionButton from 'action-button'
+import map from 'lodash/map'
+import moment from 'moment-timezone'
+import React from 'react'
+
+import _ from './intl'
+import Component from './base-component'
+import propTypes from './prop-types-decorator'
+import { getXoServerTimezone } from './xo'
+import { Select } from './form'
+
+const SERVER_TIMEZONE_TAG = 'server'
+const LOCAL_TIMEZONE = moment.tz.guess()
+
+@propTypes({
+ defaultValue: propTypes.string,
+ onChange: propTypes.func.isRequired,
+ required: propTypes.bool,
+ value: propTypes.string,
+})
+export default class TimezonePicker extends Component {
+ componentDidMount () {
+ getXoServerTimezone.then(serverTimezone => {
+ this.setState({
+ timezone:
+ this.props.value || this.props.defaultValue || SERVER_TIMEZONE_TAG,
+ options: [
+ ...map(moment.tz.names(), value => ({ label: value, value })),
+ {
+ label: _('serverTimezoneOption', {
+ value: serverTimezone,
+ }),
+ value: SERVER_TIMEZONE_TAG,
+ },
+ ],
+ })
+ })
+ }
+
+ componentWillReceiveProps (props) {
+ if (props.value !== this.props.value) {
+ this.setState({ timezone: props.value || SERVER_TIMEZONE_TAG })
+ }
+ }
+
+ get value () {
+ return this.state.timezone === SERVER_TIMEZONE_TAG
+ ? null
+ : this.state.timezone
+ }
+
+ set value (value) {
+ this.setState({ timezone: value || SERVER_TIMEZONE_TAG })
+ }
+
+ _onChange = option => {
+ if (option && option.value === this.state.timezone) {
+ return
+ }
+
+ this.setState(
+ {
+ timezone: (option != null && option.value) || SERVER_TIMEZONE_TAG,
+ },
+ () =>
+ this.props.onChange(
+ this.state.timezone === SERVER_TIMEZONE_TAG
+ ? null
+ : this.state.timezone
+ )
+ )
+ }
+
+ _useLocalTime = () => {
+ this._onChange({ value: LOCAL_TIMEZONE })
+ }
+
+ render () {
+ const { timezone, options } = this.state
+
+ return (
+
+
+
+
+ {_('timezonePickerUseLocalTime')}
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/common/tooltip/get-position.js b/packages/xo-web/src/common/tooltip/get-position.js
new file mode 100644
index 000000000..151776a94
--- /dev/null
+++ b/packages/xo-web/src/common/tooltip/get-position.js
@@ -0,0 +1,323 @@
+// Source: https://github.com/wwayne/react-tooltip/blob/master/src/utils/getPosition.js
+
+/**
+ * Calculate the position of tooltip
+ *
+ * @params
+ * - `e` {Event} the event of current mouse
+ * - `target` {Element} the currentTarget of the event
+ * - `node` {DOM} the react-tooltip object
+ * - `place` {String} top / right / bottom / left
+ * - `effect` {String} float / solid
+ * - `offset` {Object} the offset to default position
+ *
+ * @return {Object
+ * - `isNewState` {Bool} required
+ * - `newState` {Object}
+ * - `position` {OBject} {left: {Number}, top: {Number}}
+ */
+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 windowWidth = window.innerWidth
+ const windowHeight = window.innerHeight
+
+ const { parentTop, parentLeft } = getParent(target)
+
+ // Get the edge offset of the tooltip
+ const getTipOffsetLeft = place => {
+ const offsetX = defaultOffset[place].l
+ return mouseX + offsetX + extraOffsetX
+ }
+ const getTipOffsetRight = place => {
+ const offsetX = defaultOffset[place].r
+ return mouseX + offsetX + extraOffsetX
+ }
+ const getTipOffsetTop = place => {
+ const offsetY = defaultOffset[place].t
+ return mouseY + offsetY + extraOffsetY
+ }
+ const getTipOffsetBottom = place => {
+ const offsetY = defaultOffset[place].b
+ return mouseY + offsetY + extraOffsetY
+ }
+
+ // Judge if the tooltip has over the window(screen)
+ const outsideVertical = () => {
+ let result = false
+ let newPlace
+ if (
+ getTipOffsetTop('left') < 0 &&
+ getTipOffsetBottom('left') <= windowHeight &&
+ getTipOffsetBottom('bottom') <= windowHeight
+ ) {
+ result = true
+ newPlace = 'bottom'
+ } else if (
+ getTipOffsetBottom('left') > windowHeight &&
+ getTipOffsetTop('left') >= 0 &&
+ getTipOffsetTop('top') >= 0
+ ) {
+ result = true
+ newPlace = 'top'
+ }
+ return { result, newPlace }
+ }
+ const outsideLeft = () => {
+ 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
+ }
+ 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 }
+ }
+ const outsideRight = () => {
+ let { result, newPlace } = outsideVertical()
+ if (result && outsideHorizontal().result) {
+ return { result: false } // No need to change, if change to vertical will out of space
+ }
+ if (
+ !result &&
+ getTipOffsetRight('right') > windowWidth &&
+ getTipOffsetLeft('left') >= 0
+ ) {
+ result = true
+ newPlace = 'left'
+ }
+ return { result, newPlace }
+ }
+
+ const outsideHorizontal = () => {
+ let result = false
+ let newPlace
+ if (
+ getTipOffsetLeft('top') < 0 &&
+ getTipOffsetRight('top') <= windowWidth &&
+ getTipOffsetRight('right') <= windowWidth
+ ) {
+ result = true
+ newPlace = 'right'
+ } else if (
+ getTipOffsetRight('top') > windowWidth &&
+ getTipOffsetLeft('top') >= 0 &&
+ getTipOffsetLeft('left') >= 0
+ ) {
+ result = true
+ newPlace = 'left'
+ }
+ return { result, newPlace }
+ }
+ const outsideTop = () => {
+ let { result, newPlace } = outsideHorizontal()
+ if (result && outsideVertical().result) {
+ return { result: false }
+ }
+ if (
+ !result &&
+ getTipOffsetTop('top') < 0 &&
+ getTipOffsetBottom('bottom') <= windowHeight
+ ) {
+ result = true
+ newPlace = 'bottom'
+ }
+ return { result, newPlace }
+ }
+ const outsideBottom = () => {
+ let { result, newPlace } = outsideHorizontal()
+ if (result && outsideVertical().result) {
+ return { result: false }
+ }
+ if (
+ !result &&
+ getTipOffsetBottom('bottom') > windowHeight &&
+ getTipOffsetTop('top') >= 0
+ ) {
+ result = true
+ newPlace = 'top'
+ }
+ return { result, newPlace }
+ }
+
+ // Return new state to change the placement to the reverse if possible
+ const outsideLeftResult = outsideLeft()
+ const outsideRightResult = outsideRight()
+ const outsideTopResult = outsideTop()
+ const outsideBottomResult = outsideBottom()
+
+ if (place === 'left' && outsideLeftResult.result) {
+ return {
+ isNewState: true,
+ newState: { place: outsideLeftResult.newPlace },
+ }
+ } else if (place === 'right' && outsideRightResult.result) {
+ return {
+ isNewState: true,
+ newState: { place: outsideRightResult.newPlace },
+ }
+ } else if (place === 'top' && outsideTopResult.result) {
+ return {
+ isNewState: true,
+ newState: { place: outsideTopResult.newPlace },
+ }
+ } else if (place === 'bottom' && outsideBottomResult.result) {
+ return {
+ isNewState: true,
+ newState: { place: outsideBottomResult.newPlace },
+ }
+ }
+
+ // Return tooltip offset position
+ return {
+ isNewState: false,
+ position: {
+ left: getTipOffsetLeft(place) - parentLeft,
+ top: getTipOffsetTop(place) - parentTop,
+ },
+ }
+}
+
+// Get current mouse offset
+const getCurrentOffset = (e, currentTarget, effect) => {
+ const boundingClientRect = currentTarget.getBoundingClientRect()
+ const targetTop = boundingClientRect.top
+ const targetLeft = boundingClientRect.left
+ const targetWidth = currentTarget.clientWidth
+ const targetHeight = currentTarget.clientHeight
+
+ if (effect === 'float') {
+ return {
+ mouseX: e.clientX,
+ mouseY: e.clientY,
+ }
+ }
+ return {
+ 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
+) => {
+ let top
+ let right
+ let bottom
+ let left
+ const disToMouse = 3
+ const triangleHeight = 2
+ const cursorHeight = 12 // Optimize for float bottom only, cause the cursor will hide the tooltip
+
+ if (effect === 'float') {
+ top = {
+ l: -(tipWidth / 2),
+ r: tipWidth / 2,
+ t: -(tipHeight + disToMouse + triangleHeight),
+ b: -disToMouse,
+ }
+ bottom = {
+ l: -(tipWidth / 2),
+ r: tipWidth / 2,
+ t: disToMouse + cursorHeight,
+ b: tipHeight + disToMouse + triangleHeight + cursorHeight,
+ }
+ left = {
+ l: -(tipWidth + disToMouse + triangleHeight),
+ r: -disToMouse,
+ t: -(tipHeight / 2),
+ b: tipHeight / 2,
+ }
+ right = {
+ l: disToMouse,
+ r: tipWidth + disToMouse + triangleHeight,
+ t: -(tipHeight / 2),
+ b: tipHeight / 2,
+ }
+ } else if (effect === 'solid') {
+ top = {
+ l: -(tipWidth / 2),
+ r: tipWidth / 2,
+ t: -(targetHeight / 2 + tipHeight + triangleHeight),
+ b: -(targetHeight / 2),
+ }
+ bottom = {
+ l: -(tipWidth / 2),
+ r: tipWidth / 2,
+ t: targetHeight / 2,
+ b: targetHeight / 2 + tipHeight + triangleHeight,
+ }
+ left = {
+ l: -(tipWidth + targetWidth / 2 + triangleHeight),
+ r: -(targetWidth / 2),
+ t: -(tipHeight / 2),
+ b: tipHeight / 2,
+ }
+ right = {
+ l: targetWidth / 2,
+ r: tipWidth + targetWidth / 2 + triangleHeight,
+ t: -(tipHeight / 2),
+ b: tipHeight / 2,
+ }
+ }
+
+ return { top, bottom, left, right }
+}
+
+// Consider additional offset into position calculation
+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 (const key in offset) {
+ if (key === 'top') {
+ extraOffsetY -= parseInt(offset[key], 10)
+ } else if (key === 'bottom') {
+ extraOffsetY += parseInt(offset[key], 10)
+ } else if (key === 'left') {
+ extraOffsetX -= parseInt(offset[key], 10)
+ } else if (key === 'right') {
+ extraOffsetX += parseInt(offset[key], 10)
+ }
+ }
+
+ return { extraOffsetX, extraOffsetY }
+}
+
+// Get the offset of the parent elements
+const getParent = currentTarget => {
+ let currentParent = currentTarget
+ while (currentParent) {
+ if (currentParent.style.transform.length > 0) break
+ currentParent = currentParent.parentElement
+ }
+
+ const parentTop = currentParent && currentParent.getBoundingClientRect().top
+ const parentLeft = currentParent && currentParent.getBoundingClientRect().left
+
+ return { parentTop, parentLeft }
+}
diff --git a/packages/xo-web/src/common/tooltip/index.css b/packages/xo-web/src/common/tooltip/index.css
new file mode 100644
index 000000000..6dafa9cff
--- /dev/null
+++ b/packages/xo-web/src/common/tooltip/index.css
@@ -0,0 +1,20 @@
+.tooltipEnabled {
+ background-color: #222;
+ border-radius: 3px;
+ border: 1px solid #fff;
+ color: #fff;
+ display: inline-block;
+ font-size: 13px;
+ margin-left: 0px;
+ margin-top: 0px;
+ opacity: 0.9;
+ padding: 8px 21px;
+ pointer-events: none;
+ position: fixed;
+ transition: opacity 0.3s ease-out, margin-top 0.3s ease-out, margin-left 0.3s ease-out;
+ z-index: 9999;
+}
+
+.tooltipDisabled {
+ display: none;
+}
diff --git a/packages/xo-web/src/common/tooltip/index.js b/packages/xo-web/src/common/tooltip/index.js
new file mode 100644
index 000000000..9c87d9096
--- /dev/null
+++ b/packages/xo-web/src/common/tooltip/index.js
@@ -0,0 +1,160 @@
+import classNames from 'classnames'
+import isString from 'lodash/isString'
+import React from 'react'
+import ReactDOM from 'react-dom'
+
+import Component from '../base-component'
+import getPosition from './get-position'
+import propTypes from '../prop-types-decorator'
+
+import styles from './index.css'
+
+// ===================================================================
+
+let instance
+
+export class TooltipViewer extends Component {
+ constructor () {
+ super()
+
+ this.state.place = 'top'
+ }
+
+ componentDidMount () {
+ if (instance) {
+ throw new Error('Tooltip viewer is a singleton!')
+ }
+ instance = this
+ }
+
+ componentWillUnmount () {
+ instance = undefined
+ }
+
+ render () {
+ const { className, content, place, show, style } = this.state
+
+ return (
+
+ {content}
+
+ )
+ }
+}
+
+// ===================================================================
+
+@propTypes({
+ children: propTypes.oneOfType([propTypes.element, propTypes.string]),
+ className: propTypes.string,
+ content: propTypes.node,
+ style: propTypes.object,
+ tagName: propTypes.string,
+})
+export default class Tooltip extends Component {
+ componentDidMount () {
+ this._addListeners()
+ }
+
+ componentWillUnmount () {
+ this._removeListeners()
+ }
+
+ componentWillReceiveProps (props) {
+ if (props.children !== this.props.children) {
+ this._removeListeners()
+ }
+ }
+
+ componentDidUpdate (prevProps) {
+ if (prevProps.children !== this.props.children) {
+ this._addListeners()
+ }
+ }
+
+ _addListeners () {
+ const node = (this._node = ReactDOM.findDOMNode(this))
+
+ node.addEventListener('mouseenter', this._showTooltip)
+ node.addEventListener('mouseleave', this._hideTooltip)
+ node.addEventListener('mousemove', this._updateTooltip)
+ }
+
+ _removeListeners () {
+ const node = this._node
+ this._hideTooltip()
+
+ if (!node) {
+ return
+ }
+
+ node.removeEventListener('mouseenter', this._showTooltip)
+ node.removeEventListener('mouseleave', this._hideTooltip)
+ node.removeEventListener('mousemove', this._updateTooltip)
+
+ this._node = null
+ }
+
+ _showTooltip = () => {
+ const { props } = this
+
+ instance.setState({
+ className: props.className,
+ content: props.content,
+ show: true,
+ style: props.style,
+ })
+ }
+
+ _hideTooltip = () => {
+ instance.setState({ show: false })
+ }
+
+ _updateTooltip = event => {
+ const node = ReactDOM.findDOMNode(instance)
+ const result = getPosition(
+ event,
+ event.currentTarget,
+ node,
+ instance.state.place,
+ 'solid',
+ {}
+ )
+
+ if (result.isNewState) {
+ return instance.setState(result.newState, () =>
+ this._updateTooltip(event)
+ )
+ }
+
+ const { position } = result
+ node.style.left = `${position.left}px`
+ node.style.top = `${position.top}px`
+ }
+
+ render () {
+ const { children } = this.props
+
+ if (!children) {
+ return
+ }
+
+ if (isString(children)) {
+ return {children}
+ }
+
+ return children
+ }
+}
diff --git a/packages/xo-web/src/common/usage/index.js b/packages/xo-web/src/common/usage/index.js
new file mode 100644
index 000000000..5e7788884
--- /dev/null
+++ b/packages/xo-web/src/common/usage/index.js
@@ -0,0 +1,78 @@
+import _ from 'intl'
+import classNames from 'classnames'
+import PropTypes from 'prop-types'
+import React, { cloneElement } from 'react'
+import sum from 'lodash/sum'
+
+import Tooltip from '../tooltip'
+
+const Usage = ({ total, children }) => {
+ const limit = total / 400
+ const othersValues = React.Children.map(children, child => {
+ const { value } = child.props
+ return value < limit && value
+ })
+ const othersTotal = sum(othersValues)
+ return (
+
+ {React.Children.map(
+ children,
+ (child, index) =>
+ child.props.value > limit && cloneElement(child, { total })
+ )}
+
+
+ )
+}
+Usage.propTypes = {
+ total: PropTypes.number.isRequired,
+}
+export { Usage as default }
+
+const Element = ({ highlight, href, others, tooltip, total, value }) => (
+
+
+
+)
+Element.propTypes = {
+ highlight: PropTypes.bool,
+ href: PropTypes.string,
+ others: PropTypes.bool,
+ tooltip: PropTypes.node,
+ value: PropTypes.number.isRequired,
+}
+export { Element as UsageElement }
+
+export const Limits = ({ used, toBeUsed, limit }) => {
+ const available = limit - used
+
+ return (
+
+
+ available ? 'limits-over-used' : 'limits-to-be-used'
+ }
+ style={{
+ width: Math.min(toBeUsed || 0, available) / limit * 100 + '%',
+ }}
+ />
+
+ )
+}
+Limits.propTypes = {
+ used: PropTypes.number,
+ toBeUsed: PropTypes.number,
+ limit: PropTypes.number.isRequired,
+}
diff --git a/packages/xo-web/src/common/utils.js b/packages/xo-web/src/common/utils.js
new file mode 100644
index 000000000..8093cc0bc
--- /dev/null
+++ b/packages/xo-web/src/common/utils.js
@@ -0,0 +1,526 @@
+import getStream from 'get-stream'
+import humanFormat from 'human-format'
+import React from 'react'
+import ReadableStream from 'readable-stream'
+import { connect } from 'react-redux'
+import { FormattedDate } from 'react-intl'
+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'
+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 addSubscriptions from './add-subscriptions'
+
+// ===================================================================
+
+export const ensureArray = value => {
+ if (value === undefined) {
+ return []
+ }
+
+ return Array.isArray(value) ? value : [value]
+}
+
+export const propsEqual = (o1, o2, props) => {
+ props = ensureArray(props)
+
+ for (const prop of props) {
+ if (o1[prop] !== o2[prop]) {
+ return false
+ }
+ }
+
+ return true
+}
+
+// ===================================================================
+
+const _normalizeMapStateToProps = mapper => {
+ if (isFunction(mapper)) {
+ const factoryOrMapper = (state, props) => {
+ const result = mapper(state, props)
+
+ // Properly handles factory pattern.
+ if (isFunction(result)) {
+ mapper = result
+ return factoryOrMapper
+ }
+
+ if (isPlainObject(result)) {
+ if (isEmpty(result)) {
+ // Nothing can be determined, wait for it.
+ return result
+ }
+
+ if (every(result, isFunction)) {
+ indirection = (state, props) =>
+ mapValues(result, selector => selector(state, props))
+ return indirection(state, props)
+ }
+ }
+
+ indirection = mapper
+ return result
+ }
+
+ let indirection = factoryOrMapper
+ return (state, props) => indirection(state, props)
+ }
+
+ mapper = mapValues(mapper, _normalizeMapStateToProps)
+ return (state, props) => mapValues(mapper, fn => fn(state, props))
+}
+
+export const connectStore = (mapStateToProps, opts = {}) => {
+ const connector = connect(
+ _normalizeMapStateToProps(mapStateToProps),
+ actions,
+ undefined,
+ opts
+ )
+
+ return Component => {
+ const ConnectedComponent = connector(Component)
+
+ if (opts.withRef && 'value' in Component.prototype) {
+ Object.defineProperty(ConnectedComponent.prototype, 'value', {
+ configurable: true,
+ get () {
+ return this.getWrappedInstance().value
+ },
+ set (value) {
+ this.getWrappedInstance().value = value
+ },
+ })
+ }
+
+ return ConnectedComponent
+ }
+}
+
+// -------------------------------------------------------------------
+
+export { default as Debug } from './debug'
+
+// -------------------------------------------------------------------
+
+// Returns the current XOA Plan or the Plan name if number given
+export const getXoaPlan = plan => {
+ switch (plan || +process.env.XOA_PLAN) {
+ case 1:
+ return 'Free'
+ case 2:
+ return 'Starter'
+ case 3:
+ return 'Enterprise'
+ case 4:
+ return 'Premium'
+ case 5:
+ return 'Community'
+ }
+ return 'Unknown'
+}
+
+// -------------------------------------------------------------------
+
+export const mapPlus = (collection, cb) => {
+ const result = []
+ const push = ::result.push
+ forEach(collection, (value, index) => cb(value, push, index))
+ return result
+}
+
+// -------------------------------------------------------------------
+
+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
+ })
+ })
+
+ return osName => osName && osToFamily[osName.toLowerCase()]
+ }
+)
+
+// -------------------------------------------------------------------
+
+export const formatSize = bytes =>
+ humanFormat(bytes, { scale: 'binary', unit: 'B' })
+
+export const formatSizeShort = bytes =>
+ humanFormat(bytes, { scale: 'binary', unit: 'B', decimals: 0 })
+
+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' })
+ if (bytes.unit && bytes.unit !== 'B') {
+ bytes = humanFormat.parse.raw(size)
+
+ if (bytes.unit && bytes.unit !== 'B') {
+ throw new Error('invalid size: ' + size)
+ }
+ }
+ return Math.floor(bytes.value * bytes.factor)
+}
+
+// -------------------------------------------------------------------
+
+export const normalizeXenToolsStatus = status => {
+ if (status === false) {
+ return 'not-installed'
+ }
+ if (status === undefined) {
+ return 'unknown'
+ }
+ if (status === 'up to date') {
+ return 'up-to-date'
+ }
+ return 'out-of-date'
+}
+
+// -------------------------------------------------------------------
+
+const _NotFound = () => {_('errorPageNotFound')}
+
+// Decorator to declare routes on a component.
+//
+// TODO: add support for function childRoutes (getChildRoutes).
+export const routes = (indexRoute, childRoutes) => target => {
+ if (isArray(indexRoute)) {
+ childRoutes = indexRoute
+ indexRoute = undefined
+ } else if (isFunction(indexRoute)) {
+ indexRoute = {
+ component: indexRoute,
+ }
+ } else if (isString(indexRoute)) {
+ indexRoute = {
+ onEnter: invoke(indexRoute, pathname => (state, replace) => {
+ const current = state.location.pathname
+ replace((current === '/' ? '' : current) + '/' + pathname)
+ }),
+ }
+ }
+
+ if (isPlainObject(childRoutes)) {
+ childRoutes = map(childRoutes, (component, path) => {
+ // The logic can be bypassed by passing a plain object.
+ if (isPlainObject(component)) {
+ return { ...component, path }
+ }
+
+ return { ...component.route, component, path }
+ })
+ }
+
+ if (childRoutes) {
+ childRoutes.push({ component: _NotFound, path: '*' })
+ }
+
+ target.route = {
+ indexRoute,
+ childRoutes,
+ }
+
+ return target
+}
+
+// -------------------------------------------------------------------
+
+// Creates a new function which throws an error.
+//
+// ```js
+// promise.catch(throwFn('an error has occured'))
+//
+// function foo (param = throwFn('param is required')) {}
+// ```
+export const throwFn = error => () => {
+ throw isString(error) ? new Error(error) : error
+}
+
+// ===================================================================
+
+export const resolveResourceSet = resourceSet => {
+ if (!resourceSet) {
+ return
+ }
+
+ const { objects, ipPools, ...attrs } = resourceSet
+ const resolvedObjects = {}
+ const resolvedSet = {
+ ...attrs,
+ missingObjects: [],
+ objectsByType: resolvedObjects,
+ ipPools,
+ }
+ const state = store.getState()
+
+ forEach(objects, id => {
+ const object = getObject(state, id, true) // true: useResourceSet to bypass permissions
+
+ // Error, missing resource.
+ if (!object) {
+ resolvedSet.missingObjects.push(id)
+ return
+ }
+
+ const { type } = object
+
+ if (!resolvedObjects[type]) {
+ resolvedObjects[type] = [object]
+ } else {
+ resolvedObjects[type].push(object)
+ }
+ })
+
+ return resolvedSet
+}
+
+export const resolveResourceSets = resourceSets =>
+ map(resourceSets, resolveResourceSet)
+
+// -------------------------------------------------------------------
+
+// Creates a string replacer based on a pattern and a list of rules
+//
+// ```js
+// const myReplacer = buildTemplate('{name}_COPY_{name}_{id}_%', {
+// '{name}': vm => vm.name_label,
+// '{id}': vm => vm.id,
+// '%': (_, i) => i
+// })
+//
+// const newString = myReplacer({
+// name_label: 'foo',
+// id: 42,
+// }, 32)
+//
+// newString === 'foo_COPY_foo_42_32'
+// ```
+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
+ })
+}
+
+// ===================================================================
+
+export const streamToString = getStream
+
+// ===================================================================
+
+/* global FileReader */
+
+// Creates a readable stream from a HTML file.
+export const htmlFileToStream = file => {
+ const reader = new FileReader()
+ const stream = new ReadableStream()
+ let offset = 0
+
+ reader.onloadend = evt => {
+ stream.push(evt.target.result)
+ }
+ reader.onerror = error => {
+ stream.emit('error', error)
+ }
+
+ stream._read = function (size) {
+ if (offset >= file.size) {
+ stream.push(null)
+ } else {
+ reader.readAsBinaryString(file.slice(offset, offset + size))
+ offset += size
+ }
+ }
+
+ return stream
+}
+
+// ===================================================================
+
+export const resolveId = value =>
+ value != null && typeof value === 'object' && 'id' in value ? value.id : value
+
+export const resolveIds = params => {
+ for (const key in params) {
+ const param = params[key]
+ if (param != null && typeof param === 'object' && 'id' in param) {
+ params[key] = param.id
+ }
+ }
+ return params
+}
+
+// ===================================================================
+
+const OPs = {
+ '<': a => a < 0,
+ '<=': a => a <= 0,
+ '===': a => a === 0,
+ '>': a => a > 0,
+ '>=': a => a >= 0,
+}
+
+const makeNiceCompare = compare =>
+ function () {
+ const { length } = arguments
+ if (length === 2) {
+ return compare(arguments[0], arguments[1])
+ }
+
+ let i = 1
+ let v1 = arguments[0]
+ let op, v2
+ while (i < length) {
+ op = arguments[i++]
+ v2 = arguments[i++]
+ if (!OPs[op](compare(v1, v2))) {
+ return false
+ }
+ v1 = v2
+ }
+ return true
+ }
+
+export const compareVersions = makeNiceCompare((v1, v2) => {
+ v1 = v1.split('.')
+ v2 = v2.split('.')
+
+ for (let i = 0; i < Math.max(v1.length, v2.length); i++) {
+ const n1 = +v1[i] || 0
+ const n2 = +v2[i] || 0
+
+ if (n1 < n2) return -1
+ if (n1 > n2) return 1
+ }
+
+ return 0
+})
+
+export const isXosanPack = ({ name }) => startsWith(name, 'XOSAN')
+
+// ===================================================================
+
+export const getCoresPerSocketPossibilities = (maxCoresPerSocket, vCPUs) => {
+ // According to : https://www.citrix.com/blogs/2014/03/11/citrix-xenserver-setting-more-than-one-vcpu-per-vm-to-improve-application-performance-and-server-consolidation-e-g-for-cad3-d-graphical-applications/
+ const maxVCPUs = 16
+
+ const options = []
+ if (maxCoresPerSocket !== undefined && vCPUs !== '') {
+ const ratio = vCPUs / maxVCPUs
+
+ 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
+//
+// const getProgress = createFakeProgress(120)
+// setInterval(() => console.log(`Progress: ${getProgress() * 100} %`), 1000)
+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
+ }
+ }
+})()
+
+export const ShortDate = ({ timestamp }) => (
+
+)
diff --git a/packages/xo-web/src/common/wizard/index.css b/packages/xo-web/src/common/wizard/index.css
new file mode 100644
index 000000000..d87bda53b
--- /dev/null
+++ b/packages/xo-web/src/common/wizard/index.css
@@ -0,0 +1,64 @@
+/* Wizard */
+@value done: #089944;
+
+.wizard {
+ margin: 1em;
+}
+
+/* Section */
+
+.section {
+ padding-bottom: 1em;
+ display: flex;
+ flex-wrap: wrap;
+}
+
+.title {
+ flex: 0 0 15em;
+}
+.title.success {
+ color: done;
+}
+
+.content {
+ border: solid 2px;
+ flex: 1 1 40em;
+ min-width: 15em;
+ padding: 0.5em;
+}
+.content.success {
+ border-color: done;
+}
+.content.active {
+ box-shadow: 0 0 0 2px;
+}
+.content.success.active {
+ box-shadow: 0 0 0 2px done;
+}
+
+/* Bulleted list */
+
+.bullet {
+ position:relative;
+ margin-bottom: 0;
+}
+.bullet:after {
+ font-family: FontAwesome;
+ content: '\f111';
+ position: absolute;
+ left: -25px;
+ top: 5px;
+}
+.bullet:not(:last-child):before {
+ content: '';
+ position: absolute;
+ left: -19px;
+ border-left: 1px solid;
+ height: calc(100% - 11px);
+ width: 1px;
+ top: 22px;
+}
+.bullet.success:after {
+ content: '\f058';
+ color: done;
+}
diff --git a/packages/xo-web/src/common/wizard/index.js b/packages/xo-web/src/common/wizard/index.js
new file mode 100644
index 000000000..85442c4e0
--- /dev/null
+++ b/packages/xo-web/src/common/wizard/index.js
@@ -0,0 +1,76 @@
+import classNames from 'classnames'
+import every from 'lodash/every'
+import React, { Component, cloneElement } from 'react'
+
+import _ from '../intl'
+import Icon from '../icon'
+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
+ )
+
+ return (
+
+ {React.Children.map(
+ children,
+ (child, key) => child && cloneElement(child, { allDone, key })
+ )}
+
+ )
+}
+export { Wizard as default }
+
+@propTypes({
+ icon: propTypes.string.isRequired,
+ title: propTypes.string.isRequired,
+})
+export class Section extends Component {
+ componentWillMount () {
+ this.setState({ isActive: false })
+ }
+
+ _onFocus = () => this.setState({ isActive: true })
+ _onBlur = () => this.setState({ isActive: false })
+
+ render () {
+ const { allDone, icon, title, done, children } = this.props
+ return (
+
+ {/* TITLE */}
+
+
+ {icon && } {_(title)}
+
+
+ {/* CONTENT */}
+
+ {children}
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/common/xo-defined.js b/packages/xo-web/src/common/xo-defined.js
new file mode 100644
index 000000000..262f4e9eb
--- /dev/null
+++ b/packages/xo-web/src/common/xo-defined.js
@@ -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
diff --git a/packages/xo-web/src/common/xo-json-schema-input/index.js b/packages/xo-web/src/common/xo-json-schema-input/index.js
new file mode 100644
index 000000000..a8beaea02
--- /dev/null
+++ b/packages/xo-web/src/common/xo-json-schema-input/index.js
@@ -0,0 +1,69 @@
+import forEach from 'lodash/forEach'
+
+import XoHighLevelObjectInput from './xo-highlevel-object-input'
+import XoHostInput from './xo-host-input'
+import XoPoolInput from './xo-pool-input'
+import XoRemoteInput from './xo-remote-input'
+import XoRoleInput from './xo-role-input'
+import XoSrInput from './xo-sr-input'
+import XoSubjectInput from './xo-subject-input'
+import XoTagInput from './xo-tag-input'
+import XoVmInput from './xo-vm-input'
+import { getType, getXoType } from '../json-schema-input/helpers'
+
+// ===================================================================
+
+const XO_TYPE_TO_COMPONENT = {
+ host: XoHostInput,
+ pool: XoPoolInput,
+ remote: XoRemoteInput,
+ role: XoRoleInput,
+ sr: XoSrInput,
+ subject: XoSubjectInput,
+ tag: XoTagInput,
+ vm: XoVmInput,
+ xoobject: XoHighLevelObjectInput,
+}
+
+// ===================================================================
+
+const buildStringInput = (uiSchema, key, xoType) => {
+ if (key === 'password') {
+ uiSchema.config = { password: true }
+ }
+
+ uiSchema.widget = XO_TYPE_TO_COMPONENT[xoType]
+}
+
+// ===================================================================
+
+const _generateUiSchema = (schema, uiSchema, key) => {
+ const type = getType(schema)
+
+ if (type === 'object') {
+ const properties = (uiSchema.properties = {})
+
+ forEach(schema.properties, (schema, key) => {
+ const subUiSchema = (properties[key] = {})
+ _generateUiSchema(schema, subUiSchema, key)
+ })
+ } else if (type === 'array') {
+ const widget = XO_TYPE_TO_COMPONENT[getXoType(schema.items)]
+
+ if (widget) {
+ uiSchema.widget = widget
+ uiSchema.config = { multi: true }
+ } else {
+ const subUiSchema = (uiSchema.items = {})
+ _generateUiSchema(schema.items, subUiSchema, key)
+ }
+ } else if (type === 'string') {
+ buildStringInput(uiSchema, key, getXoType(schema))
+ }
+}
+
+export const generateUiSchema = schema => {
+ const uiSchema = {}
+ _generateUiSchema(schema, uiSchema, '')
+ return uiSchema
+}
diff --git a/packages/xo-web/src/common/xo-json-schema-input/xo-abstract-input.js b/packages/xo-web/src/common/xo-json-schema-input/xo-abstract-input.js
new file mode 100644
index 000000000..8bbd864b6
--- /dev/null
+++ b/packages/xo-web/src/common/xo-json-schema-input/xo-abstract-input.js
@@ -0,0 +1,19 @@
+import map from 'lodash/map'
+import { PureComponent } from 'react'
+
+import getEventValue from '../get-event-value'
+
+// ===================================================================
+
+const getId = value => (value != null && value.id) || value
+
+export default class XoAbstractInput extends PureComponent {
+ _onChange = event => {
+ const value = getEventValue(event)
+ const { props } = this
+
+ return props.onChange(
+ props.schema.type === 'array' ? map(value, getId) : getId(value)
+ )
+ }
+}
diff --git a/packages/xo-web/src/common/xo-json-schema-input/xo-highlevel-object-input.js b/packages/xo-web/src/common/xo-json-schema-input/xo-highlevel-object-input.js
new file mode 100644
index 000000000..debcdd87b
--- /dev/null
+++ b/packages/xo-web/src/common/xo-json-schema-input/xo-highlevel-object-input.js
@@ -0,0 +1,27 @@
+import React from 'react'
+import { SelectHighLevelObject } from 'select-objects'
+
+import XoAbstractInput from './xo-abstract-input'
+import { PrimitiveInputWrapper } from '../json-schema-input/helpers'
+
+// ===================================================================
+
+export default class HighLevelObjectInput extends XoAbstractInput {
+ render () {
+ const { props } = this
+
+ return (
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/common/xo-json-schema-input/xo-host-input.js b/packages/xo-web/src/common/xo-json-schema-input/xo-host-input.js
new file mode 100644
index 000000000..b6b6e1bb4
--- /dev/null
+++ b/packages/xo-web/src/common/xo-json-schema-input/xo-host-input.js
@@ -0,0 +1,27 @@
+import React from 'react'
+import { SelectHost } from 'select-objects'
+
+import XoAbstractInput from './xo-abstract-input'
+import { PrimitiveInputWrapper } from '../json-schema-input/helpers'
+
+// ===================================================================
+
+export default class HostInput extends XoAbstractInput {
+ render () {
+ const { props } = this
+
+ return (
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/common/xo-json-schema-input/xo-pool-input.js b/packages/xo-web/src/common/xo-json-schema-input/xo-pool-input.js
new file mode 100644
index 000000000..040e04600
--- /dev/null
+++ b/packages/xo-web/src/common/xo-json-schema-input/xo-pool-input.js
@@ -0,0 +1,27 @@
+import React from 'react'
+import { SelectPool } from 'select-objects'
+
+import XoAbstractInput from './xo-abstract-input'
+import { PrimitiveInputWrapper } from '../json-schema-input/helpers'
+
+// ===================================================================
+
+export default class PoolInput extends XoAbstractInput {
+ render () {
+ const { props } = this
+
+ return (
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/common/xo-json-schema-input/xo-remote-input.js b/packages/xo-web/src/common/xo-json-schema-input/xo-remote-input.js
new file mode 100644
index 000000000..54f98ca57
--- /dev/null
+++ b/packages/xo-web/src/common/xo-json-schema-input/xo-remote-input.js
@@ -0,0 +1,27 @@
+import React from 'react'
+import { SelectRemote } from 'select-objects'
+
+import XoAbstractInput from './xo-abstract-input'
+import { PrimitiveInputWrapper } from '../json-schema-input/helpers'
+
+// ===================================================================
+
+export default class RemoteInput extends XoAbstractInput {
+ render () {
+ const { props } = this
+
+ return (
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/common/xo-json-schema-input/xo-role-input.js b/packages/xo-web/src/common/xo-json-schema-input/xo-role-input.js
new file mode 100644
index 000000000..351eba2a5
--- /dev/null
+++ b/packages/xo-web/src/common/xo-json-schema-input/xo-role-input.js
@@ -0,0 +1,27 @@
+import React from 'react'
+import { SelectRole } from 'select-objects'
+
+import XoAbstractInput from './xo-abstract-input'
+import { PrimitiveInputWrapper } from '../json-schema-input/helpers'
+
+// ===================================================================
+
+export default class RoleInput extends XoAbstractInput {
+ render () {
+ const { props } = this
+
+ return (
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/common/xo-json-schema-input/xo-sr-input.js b/packages/xo-web/src/common/xo-json-schema-input/xo-sr-input.js
new file mode 100644
index 000000000..1ec0ad574
--- /dev/null
+++ b/packages/xo-web/src/common/xo-json-schema-input/xo-sr-input.js
@@ -0,0 +1,27 @@
+import React from 'react'
+import { SelectSr } from 'select-objects'
+
+import XoAbstractInput from './xo-abstract-input'
+import { PrimitiveInputWrapper } from '../json-schema-input/helpers'
+
+// ===================================================================
+
+export default class SrInput extends XoAbstractInput {
+ render () {
+ const { props } = this
+
+ return (
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/common/xo-json-schema-input/xo-subject-input.js b/packages/xo-web/src/common/xo-json-schema-input/xo-subject-input.js
new file mode 100644
index 000000000..eeec2ae0b
--- /dev/null
+++ b/packages/xo-web/src/common/xo-json-schema-input/xo-subject-input.js
@@ -0,0 +1,27 @@
+import React from 'react'
+import { SelectSubject } from 'select-objects'
+
+import XoAbstractInput from './xo-abstract-input'
+import { PrimitiveInputWrapper } from '../json-schema-input/helpers'
+
+// ===================================================================
+
+export default class SubjectInput extends XoAbstractInput {
+ render () {
+ const { props } = this
+
+ return (
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/common/xo-json-schema-input/xo-tag-input.js b/packages/xo-web/src/common/xo-json-schema-input/xo-tag-input.js
new file mode 100644
index 000000000..8dd00b2cb
--- /dev/null
+++ b/packages/xo-web/src/common/xo-json-schema-input/xo-tag-input.js
@@ -0,0 +1,27 @@
+import React from 'react'
+import { SelectTag } from 'select-objects'
+
+import XoAbstractInput from './xo-abstract-input'
+import { PrimitiveInputWrapper } from '../json-schema-input/helpers'
+
+// ===================================================================
+
+export default class TagInput extends XoAbstractInput {
+ render () {
+ const { props } = this
+
+ return (
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/common/xo-json-schema-input/xo-vm-input.js b/packages/xo-web/src/common/xo-json-schema-input/xo-vm-input.js
new file mode 100644
index 000000000..447a5397f
--- /dev/null
+++ b/packages/xo-web/src/common/xo-json-schema-input/xo-vm-input.js
@@ -0,0 +1,27 @@
+import React from 'react'
+import { SelectVm } from 'select-objects'
+
+import XoAbstractInput from './xo-abstract-input'
+import { PrimitiveInputWrapper } from '../json-schema-input/helpers'
+
+// ===================================================================
+
+export default class VmInput extends XoAbstractInput {
+ render () {
+ const { props } = this
+
+ return (
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/common/xo-line-chart/index.css b/packages/xo-web/src/common/xo-line-chart/index.css
new file mode 100644
index 000000000..7b13dcc04
--- /dev/null
+++ b/packages/xo-web/src/common/xo-line-chart/index.css
@@ -0,0 +1,4 @@
+.dashedLine {
+ stroke: black;
+ stroke-dasharray: 4px 2px;
+}
diff --git a/packages/xo-web/src/common/xo-line-chart/index.js b/packages/xo-web/src/common/xo-line-chart/index.js
new file mode 100644
index 000000000..55de5fb07
--- /dev/null
+++ b/packages/xo-web/src/common/xo-line-chart/index.js
@@ -0,0 +1,553 @@
+import ChartistGraph from 'react-chartist'
+import ChartistLegend from 'chartist-plugin-legend'
+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 propTypes from '../prop-types-decorator'
+import { computeArraysSum } from '../xo-stats'
+import { formatSize } from '../utils'
+
+import styles from './index.css'
+
+// Number of labels on axis X.
+const N_LABELS_X = 5
+
+const LABEL_OFFSET_X = 40
+const LABEL_OFFSET_Y = 85
+
+// ===================================================================
+
+// See xo-stats.js, data can be null.
+// Return the size of the first non-null object.
+const getStatsLength = stats => size(find(stats, stats => stats != null))
+
+// ===================================================================
+
+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,
+ },
+ axisY: {
+ labelInterpolationFnc: valueTransform,
+ offset: LABEL_OFFSET_Y,
+ },
+ plugins: [
+ ChartistLegend(),
+ ChartistTooltip({
+ valueTransform: value => valueTransform(+value), // '+value' because tooltip gives a string value...
+ }),
+ ],
+})
+
+// ===================================================================
+
+const makeLabelInterpolationFnc = (intl, nValues, endTimestamp, interval) => {
+ const labelSpace = floor(nValues / N_LABELS_X)
+ let format
+
+ if (interval === 3600) {
+ format = {
+ minute: 'numeric',
+ hour: 'numeric',
+ weekday: 'short',
+ }
+ } else if (interval === 86400) {
+ format = {
+ day: 'numeric',
+ month: 'numeric',
+ year: 'numeric',
+ }
+ }
+
+ return (value, index) =>
+ index % labelSpace === 0
+ ? intl.formatTime(
+ (endTimestamp - (nValues - index - 1) * interval) * 1000,
+ format
+ )
+ : null
+}
+
+// Supported series: xvds, vifs, pifs.
+const buildSeries = ({ stats, label, addSumSeries }) => {
+ const series = []
+
+ for (const io in stats) {
+ const ioData = stats[io]
+ for (const letter in ioData) {
+ const data = ioData[letter]
+
+ // See xo-stats.js, data can be null.
+ if (data) {
+ series.push({
+ name: `${label}${letter} (${io})`,
+ data,
+ })
+ }
+ }
+
+ if (addSumSeries) {
+ series.push({
+ name: `All ${io}`,
+ data: computeArraysSum(values(ioData)),
+ className: styles.dashedLine,
+ })
+ }
+ }
+
+ return series
+}
+
+const templateError = No 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
+ }
+
+ const series = map(stats, (data, id) => ({
+ name: `Cpu${id}`,
+ data,
+ }))
+
+ if (addSumSeries) {
+ series.push({
+ name: 'All Cpus',
+ data: computeArraysSum(stats),
+ className: styles.dashedLine,
+ })
+ }
+
+ return (
+ `${floor(value)}%`,
+ }),
+ high: !addSumSeries ? 100 : stats.length * 100,
+ ...options,
+ }}
+ />
+ )
+ })
+)
+
+export const PoolCpuLineChart = injectIntl(
+ propTypes({
+ addSumSeries: propTypes.bool,
+ data: propTypes.array.isRequired,
+ options: propTypes.object,
+ })(({ addSumSeries, data, options = {}, intl }) => {
+ const firstHostData = data[0]
+ const length = getStatsLength(firstHostData.stats.cpus)
+
+ if (!length) {
+ return templateError
+ }
+
+ const series = map(data, ({ host, stats }) => ({
+ name: host,
+ data: computeArraysSum(stats.cpus),
+ }))
+
+ if (addSumSeries) {
+ series.push({
+ name: intl.formatMessage(messages.poolAllHosts),
+ data: computeArraysSum(map(series, 'data')),
+ className: styles.dashedLine,
+ })
+ }
+
+ const nbCpusByHost = map(data, ({ stats }) => stats.cpus.length)
+
+ return (
+ `${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
+
+ if (!memory || !memoryUsed) {
+ return templateError
+ }
+
+ return (
+
+ )
+ })
+)
+
+export const PoolMemoryLineChart = injectIntl(
+ propTypes({
+ addSumSeries: propTypes.bool,
+ data: propTypes.array.isRequired,
+ options: propTypes.object,
+ })(({ addSumSeries, data, options = {}, intl }) => {
+ const firstHostData = data[0]
+ const { memory, memoryUsed } = firstHostData.stats
+
+ if (!memory || !memoryUsed) {
+ return templateError
+ }
+
+ const series = map(data, ({ host, stats }) => ({
+ name: host,
+ data: stats.memoryUsed,
+ }))
+
+ if (addSumSeries) {
+ series.push({
+ name: intl.formatMessage(messages.poolAllHosts),
+ data: computeArraysSum(map(data, 'stats.memoryUsed')),
+ className: styles.dashedLine,
+ })
+ }
+
+ const currentMemoryByHost = map(
+ data,
+ ({ stats }) => stats.memory[stats.memory.length - 1]
+ )
+
+ return (
+
+ )
+ })
+)
+
+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
+ }
+
+ return (
+
+ )
+ })
+)
+
+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
+ }
+
+ return (
+
+ )
+ })
+)
+
+export const PifLineChart = injectIntl(
+ propTypes({
+ addSumSeries: propTypes.bool,
+ data: propTypes.array.isRequired,
+ options: propTypes.object,
+ })(({ addSumSeries, data, options = {}, intl }) => {
+ const stats = data.stats.pifs
+ const length = stats && getStatsLength(stats.rx)
+
+ if (!length) {
+ return templateError
+ }
+
+ return (
+
+ )
+ })
+)
+
+const ios = ['rx', 'tx']
+export const PoolPifLineChart = injectIntl(
+ propTypes({
+ addSumSeries: propTypes.bool,
+ data: propTypes.array.isRequired,
+ options: propTypes.object,
+ })(({ addSumSeries, data, options = {}, intl }) => {
+ const firstHostData = data[0]
+ const length =
+ firstHostData.stats && getStatsLength(firstHostData.stats.pifs.rx)
+
+ if (!length) {
+ return templateError
+ }
+
+ const series = addSumSeries
+ ? map(ios, io => ({
+ name: `${intl.formatMessage(messages.poolAllHosts)} (${io})`,
+ data: computeArraysSum(
+ map(data, ({ stats }) => computeArraysSum(stats.pifs[io]))
+ ),
+ }))
+ : flatten(
+ map(data, ({ stats, host }) =>
+ map(ios, io => ({
+ name: `${host} (${io})`,
+ data: computeArraysSum(stats.pifs[io]),
+ }))
+ )
+ )
+
+ return (
+
+ )
+ })
+)
+
+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
+ }
+
+ return (
+ `${value.toPrecision(3)}`,
+ }),
+ ...options,
+ }}
+ />
+ )
+ })
+)
+
+export const PoolLoadLineChart = injectIntl(
+ propTypes({
+ addSumSeries: propTypes.bool,
+ data: propTypes.array.isRequired,
+ options: propTypes.object,
+ })(({ addSumSeries, data, options = {}, intl }) => {
+ const firstHostData = data[0]
+ const length = firstHostData.stats && firstHostData.stats.load.length
+
+ if (!length) {
+ return templateError
+ }
+
+ const series = map(data, ({ host, stats }) => ({
+ name: host,
+ data: stats.load,
+ }))
+
+ if (addSumSeries) {
+ series.push({
+ name: intl.formatMessage(messages.poolAllHosts),
+ data: computeArraysSum(map(data, 'stats.load')),
+ className: styles.dashedLine,
+ })
+ }
+
+ return (
+ `${value.toPrecision(3)}`,
+ }),
+ ...options,
+ }}
+ />
+ )
+ })
+)
diff --git a/packages/xo-web/src/common/xo-parallel-chart.js b/packages/xo-web/src/common/xo-parallel-chart.js
new file mode 100644
index 000000000..41764fd8a
--- /dev/null
+++ b/packages/xo-web/src/common/xo-parallel-chart.js
@@ -0,0 +1,315 @@
+import * as d3 from 'd3'
+import React from 'react'
+import forEach from 'lodash/forEach'
+import keys from 'lodash/keys'
+import map from 'lodash/map'
+import times from 'lodash/times'
+
+import Component from './base-component'
+import propTypes from './prop-types-decorator'
+import { setStyles } from './d3-utils'
+
+// ===================================================================
+
+const CHART_WIDTH = 2000
+const CHART_HEIGHT = 800
+
+const TICK_SIZE = CHART_WIDTH / 100
+
+const N_TICKS = 4
+
+const TOOLTIP_PADDING = 10
+
+const DEFAULT_STROKE_WIDTH_FACTOR = 500
+const HIGHLIGHT_STROKE_WIDTH_FACTOR = 200
+
+const BRUSH_SELECTION_WIDTH = 2 * CHART_WIDTH / 100
+
+// ===================================================================
+
+const SVG_STYLE = {
+ display: 'block',
+ height: '100%',
+ left: 0,
+ position: 'absolute',
+ top: 0,
+ width: '100%',
+}
+
+const SVG_CONTAINER_STYLE = {
+ 'padding-bottom': '50%',
+ 'vertical-align': 'middle',
+ overflow: 'hidden',
+ position: 'relative',
+ width: '100%',
+}
+
+const SVG_CONTENT = {
+ 'font-size': `${CHART_WIDTH / 100}px`,
+}
+
+const COLUMN_TITLE_STYLE = {
+ 'font-size': '100%',
+ 'font-weight': 'bold',
+ 'text-anchor': 'middle',
+}
+
+const COLUMN_VALUES_STYLE = {
+ 'font-size': '100%',
+}
+
+const LINES_CONTAINER_STYLE = {
+ 'stroke-opacity': 0.5,
+ 'stroke-width': CHART_WIDTH / DEFAULT_STROKE_WIDTH_FACTOR,
+ fill: 'none',
+ stroke: 'red',
+}
+
+const TOOLTIP_STYLE = {
+ fill: 'white',
+ 'font-size': '125%',
+ 'font-weight': 'bold',
+}
+
+// ===================================================================
+
+@propTypes({
+ dataSet: propTypes.arrayOf(
+ propTypes.shape({
+ data: propTypes.object.isRequired,
+ label: propTypes.string.isRequired,
+ objectId: propTypes.string.isRequired,
+ })
+ ).isRequired,
+ labels: propTypes.object.isRequired,
+ renderers: propTypes.object,
+})
+export default class XoParallelChart extends Component {
+ _line = d3.line()
+
+ _color = d3.scaleOrdinal(d3.schemeCategory10)
+
+ _handleBrush = () => {
+ // 1. Get selected brushes.
+ const brushes = []
+ this._svg
+ .selectAll('.chartColumn')
+ .selectAll('.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')
+
+ lines.each((elem, lineId, lines) => {
+ const { data } = elem
+
+ const res = brushes.every(brush => {
+ const selection = d3.brushSelection(brush)
+ const columnId = brush.__data__
+ const { invert } = this._y[columnId] // Range to domain.
+
+ return (
+ invert(selection[1]) <= data[columnId] &&
+ data[columnId] <= invert(selection[0])
+ )
+ })
+
+ const line = d3.select(lines[lineId])
+
+ if (!res) {
+ line.attr('stroke-opacity', 1.0).attr('stroke', '#e6e6e6')
+ } else {
+ line.attr('stroke-opacity', 0.5).attr('stroke', this._color(elem.label))
+ }
+ })
+ }
+
+ _brush = d3
+ .brushY()
+ // Brush area: (x0, y0), (x1, y1)
+ .extent([
+ [-BRUSH_SELECTION_WIDTH / 2, 0],
+ [BRUSH_SELECTION_WIDTH / 2, CHART_HEIGHT],
+ ])
+ .on('brush', this._handleBrush)
+ .on('end', this._handleBrush)
+
+ _highlight (elem, position) {
+ const svg = this._svg
+
+ // Reset tooltip.
+ svg.selectAll('.objectTooltip').remove()
+
+ // Reset all lines.
+ svg
+ .selectAll('.chartLine')
+ .attr('stroke-width', CHART_WIDTH / DEFAULT_STROKE_WIDTH_FACTOR)
+
+ if (!position) {
+ return
+ }
+
+ // Set stroke on selected line.
+ svg
+ .select('#chartLine-' + elem.objectId)
+ .attr('stroke-width', CHART_WIDTH / HIGHLIGHT_STROKE_WIDTH_FACTOR)
+
+ const { label } = elem
+
+ const tooltip = svg.append('g').attr('class', 'objectTooltip')
+
+ const bbox = tooltip
+ .append('text')
+ .text(label)
+ .attr('x', position[0])
+ .attr('y', position[1] - 30)
+ ::setStyles(TOOLTIP_STYLE)
+ .node()
+ .getBBox()
+
+ tooltip
+ .insert('rect', '*')
+ .attr('x', bbox.x - TOOLTIP_PADDING)
+ .attr('y', bbox.y - TOOLTIP_PADDING)
+ .attr('width', bbox.width + TOOLTIP_PADDING * 2)
+ .attr('height', bbox.height + TOOLTIP_PADDING * 2)
+ .style('fill', this._color(label))
+ }
+
+ _handleMouseOver = (elem, pathId, paths) => {
+ this._highlight(elem, d3.mouse(paths[pathId]))
+ }
+
+ _handleMouseOut = elem => {
+ this._highlight()
+ }
+
+ _draw (props = this.props) {
+ const svg = this._svg
+ const { labels, dataSet } = props
+
+ 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))
+
+ // 1. Remove old nodes.
+ svg.selectAll('.chartColumn').remove()
+
+ svg.selectAll('.linesContainer').remove()
+
+ // 2. Build Ys.
+ const y = (this._y = {})
+ forEach(columnsIds, (columnId, index) => {
+ const max = d3.max(dataSet, elem => elem.data[columnId])
+
+ y[columnId] = d3
+ .scaleLinear()
+ .domain([0, max])
+ .range([CHART_HEIGHT, 0])
+ })
+
+ // 3. Build columns.
+ const columns = svg
+ .selectAll('.chartColumn')
+ .data(columnsIds)
+ .enter()
+ .append('g')
+ .attr('class', 'chartColumn')
+ .attr('transform', d => `translate(${x(d)})`)
+
+ // 4. Draw titles.
+ columns
+ .append('text')
+ .text(columnId => labels[columnId])
+ .attr('y', -50)
+ ::setStyles(COLUMN_TITLE_STYLE)
+
+ // 5. Draw axis.
+ columns
+ .append('g')
+ .each((columnId, axisId, axes) => {
+ const axis = d3
+ .axisLeft()
+ .ticks(N_TICKS, ',f')
+ .tickSize(TICK_SIZE)
+ .scale(y[columnId])
+
+ const renderer = props.renderers[columnId]
+
+ // Add optional renderer like formatSize.
+ if (renderer) {
+ axis.tickFormat(renderer)
+ }
+
+ d3.select(axes[axisId]).call(axis)
+ })
+ ::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')
+ .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)
+
+ // 7. Brushes.
+ columns
+ .append('g')
+ .attr('class', 'brush')
+ .each((_, brushId, brushes) => {
+ d3.select(brushes[brushId]).call(this._brush)
+ })
+ }
+
+ componentDidMount () {
+ 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)
+
+ this._draw()
+ }
+
+ componentWillReceiveProps (nextProps) {
+ this._draw(nextProps)
+ }
+
+ render () {
+ return
+ }
+}
diff --git a/packages/xo-web/src/common/xo-sparklines.js b/packages/xo-web/src/common/xo-sparklines.js
new file mode 100644
index 000000000..6da4fa3c0
--- /dev/null
+++ b/packages/xo-web/src/common/xo-sparklines.js
@@ -0,0 +1,161 @@
+import React from 'react'
+import { Sparklines, SparklinesLine } from 'react-sparklines'
+
+import propTypes from './prop-types-decorator'
+import { computeArraysAvg, computeObjectsAvg } from './xo-stats'
+
+const STYLE = {}
+
+const WIDTH = 120
+const HEIGHT = 20
+const STROKE_WIDTH = 0.5
+
+// ===================================================================
+
+const templateError = No stats.
+
+// ===================================================================
+
+export const CpuSparkLines = propTypes({
+ data: propTypes.object.isRequired,
+})(({ data, width = WIDTH, height = HEIGHT, strokeWidth = STROKE_WIDTH }) => {
+ const { cpus } = data.stats
+
+ if (!cpus) {
+ return templateError
+ }
+
+ return (
+
+
+
+ )
+})
+
+export const MemorySparkLines = propTypes({
+ data: propTypes.object.isRequired,
+})(({ data, width = WIDTH, height = HEIGHT, strokeWidth = STROKE_WIDTH }) => {
+ const { memory, memoryUsed } = data.stats
+
+ if (!memory || !memoryUsed) {
+ return templateError
+ }
+
+ return (
+
+
+
+ )
+})
+
+export const XvdSparkLines = propTypes({
+ data: propTypes.object.isRequired,
+})(({ data, width = WIDTH, height = HEIGHT, strokeWidth = STROKE_WIDTH }) => {
+ const { xvds } = data.stats
+
+ if (!xvds) {
+ return templateError
+ }
+
+ return (
+
+
+
+ )
+})
+
+export const NetworkSparkLines = propTypes({
+ data: propTypes.object.isRequired,
+})(({ data, width = WIDTH, height = HEIGHT, strokeWidth = STROKE_WIDTH }) => {
+ const { pifs, vifs: ifs = pifs } = data.stats
+
+ return ifs === undefined ? (
+ templateError
+ ) : (
+
+
+
+ )
+})
+
+export const LoadSparkLines = propTypes({
+ data: propTypes.object.isRequired,
+})(({ data, width = WIDTH, height = HEIGHT, strokeWidth = STROKE_WIDTH }) => {
+ const { load } = data.stats
+
+ if (!load) {
+ return templateError
+ }
+
+ return (
+
+
+
+ )
+})
diff --git a/packages/xo-web/src/common/xo-stats.js b/packages/xo-web/src/common/xo-stats.js
new file mode 100644
index 000000000..29476ee80
--- /dev/null
+++ b/packages/xo-web/src/common/xo-stats.js
@@ -0,0 +1,80 @@
+// ===================================================================
+// Tools to manipulate rrd stats
+// ===================================================================
+
+import map from 'lodash/map'
+import values from 'lodash/values'
+import { mapPlus } from 'utils'
+
+// Returns a new array with arrays sums.
+// Example: computeArraysSum([[1, 2], [3, 4], [5, 0]) = [9, 6]
+const _computeArraysSum = arrays => {
+ if (!arrays || !arrays.length || !arrays[0].length) {
+ return []
+ }
+
+ const n = arrays[0].length // N items in each array
+ const m = arrays.length // M arrays
+
+ const result = new Array(n)
+
+ for (let i = 0; i < n; i++) {
+ result[i] = 0
+
+ for (let j = 0; j < m; j++) {
+ result[i] += arrays[j][i]
+ }
+ }
+
+ return result
+}
+
+// Returns a new array with arrays avgs.
+// Example: computeArraysAvg([[1, 2], [3, 4], [5, 0]) = [4.5, 2]
+const _computeArraysAvg = arrays => {
+ const sums = _computeArraysSum(arrays)
+
+ if (!arrays[0]) {
+ return []
+ }
+ const n = arrays && arrays[0].length
+ const m = arrays.length
+
+ for (let i = 0; i < n; i++) {
+ sums[i] /= m
+ }
+
+ return sums
+}
+
+// Arrays can be null.
+// See: https://github.com/vatesfr/xo-web/issues/969
+//
+// 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)
+ }
+ })
+
+export const computeArraysSum = arrays =>
+ _computeArraysSum(removeUndefinedArrays(arrays))
+export const computeArraysAvg = arrays =>
+ _computeArraysAvg(removeUndefinedArrays(arrays))
+
+// More complex than computeArraysAvg.
+//
+// Take in parameter one object like:
+// { x: { a: [...], b: [...], c: [...] },
+// y: { d: [...], e: [...], f: [...] } }
+// and returns the avgs between a, b, c, d, e and f.
+// Useful for vifs, pifs, xvds.
+//
+// Note: The parameter can be also an 3D array.
+export const computeObjectsAvg = objects => {
+ return _computeArraysAvg(
+ map(objects, object => computeArraysAvg(values(object)))
+ )
+}
diff --git a/packages/xo-web/src/common/xo-week-charts/index.css b/packages/xo-web/src/common/xo-week-charts/index.css
new file mode 100644
index 000000000..8b953b9ae
--- /dev/null
+++ b/packages/xo-web/src/common/xo-week-charts/index.css
@@ -0,0 +1,3 @@
+.container {
+ width: 100%;
+}
diff --git a/packages/xo-web/src/common/xo-week-charts/index.js b/packages/xo-web/src/common/xo-week-charts/index.js
new file mode 100644
index 000000000..f1d192969
--- /dev/null
+++ b/packages/xo-web/src/common/xo-week-charts/index.js
@@ -0,0 +1,406 @@
+import React from 'react'
+import * as d3 from 'd3'
+import forEach from 'lodash/forEach'
+import map from 'lodash/map'
+
+import Component from '../base-component'
+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 styles from './index.css'
+
+// ===================================================================
+
+const X_AXIS_STYLE = {
+ 'shape-rendering': 'crispEdges',
+ fill: 'none',
+ stroke: '#000',
+}
+
+const X_AXIS_TEXT_STYLE = {
+ 'font-size': '125%',
+ fill: 'black',
+ stroke: 'transparent',
+}
+
+const LABEL_STYLE = {
+ 'font-size': '125%',
+}
+
+const MOUSE_AREA_STYLE = {
+ 'pointer-events': 'all',
+ fill: 'none',
+}
+
+const HOVER_LINE_STYLE = {
+ 'stroke-width': '2px',
+ 'stroke-dasharray': '5 5',
+ stroke: 'red',
+ fill: 'none',
+}
+
+const HOVER_TEXT_STYLE = {
+ fill: 'black',
+}
+
+const HORIZON_AREA_N_STEPS = 4
+const HORIZON_AREA_MARGIN = 20
+const HORIZON_AREA_PATH_STYLE = {
+ 'fill-opacity': 0.25,
+ 'stroke-opacity': 0.3,
+ fill: 'darkgreen',
+ stroke: 'transparent',
+}
+
+// ===================================================================
+
+@propTypes({
+ chartHeight: propTypes.number,
+ chartWidth: propTypes.number,
+ data: propTypes.arrayOf(
+ propTypes.shape({
+ date: 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,
+})
+@connectStore(() => {
+ const label = createSelector(
+ createGetObject((_, props) => props.objectId),
+ object => object.name_label
+ )
+
+ return { label }
+})
+class XoWeekChart extends Component {
+ static defaultProps = {
+ chartHeight: 70,
+ chartWidth: 300,
+ valueRenderer: value => value,
+ }
+
+ _x = d3.scaleTime()
+ _y = d3.scaleLinear()
+
+ _bisectDate = d3.bisector(elem => elem.date).left
+
+ _xAxis = d3.axisBottom().scale(this._x)
+
+ _line = d3
+ .line()
+ .x(elem => this._x(elem.date))
+ .y(elem => this._y(elem.value))
+
+ _drawHorizonArea (data, max = d3.max(data, elem => elem.value)) {
+ const intervalSize = max / HORIZON_AREA_N_STEPS
+ const splittedData = []
+
+ // Start.
+ let date = new Date(data[0].date)
+ for (let i = 0; i < HORIZON_AREA_N_STEPS; i++) {
+ splittedData[i] = [
+ {
+ date,
+ value: 0,
+ },
+ ]
+ }
+
+ // Middle.
+ forEach(data, elem => {
+ const date = new Date(elem.date)
+ 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
+ ),
+ })
+ }
+ })
+
+ // End.
+ date = new Date(data[data.length - 1].date)
+ for (let i = 0; i < HORIZON_AREA_N_STEPS; i++) {
+ splittedData[i].push({
+ date,
+ value: 0,
+ })
+ }
+
+ this._x.domain(d3.extent(splittedData[0], elem => elem.date))
+ this._y.domain([0, max / HORIZON_AREA_N_STEPS])
+
+ const svg = this._svg
+
+ svg
+ .select('.horizon-area')
+ .selectAll('path')
+ .remove()
+ forEach(splittedData, data => {
+ ;svg
+ .select('.horizon-area')
+ .append('path')
+ .datum(data)
+ .attr('d', this._line)
+ ::setStyles(HORIZON_AREA_PATH_STYLE)
+ })
+ }
+
+ _draw (props = this.props) {
+ const svg = this._svg
+
+ // 1. Update dimensions.
+ const width = props.chartWidth
+ const horizonAreaWidth = width - HORIZON_AREA_MARGIN * 2
+
+ const horizonAreaHeight = props.chartHeight
+ const height = horizonAreaHeight + HORIZON_AREA_MARGIN
+
+ this._x.range([0, horizonAreaWidth])
+ this._y.range([horizonAreaHeight, 0])
+
+ svg
+ .attr('width', width)
+ .attr('height', height)
+ .select('.mouse-area')
+ .attr('width', horizonAreaWidth)
+ .attr('height', horizonAreaHeight)
+
+ svg
+ .select('.hover-container')
+ .select('.hover-line')
+ .attr('y2', horizonAreaHeight)
+
+ // 2. Draw horizon area.
+ this._drawHorizonArea(props.data, props.maxValue)
+
+ // 3. Update x axis.
+ svg
+ .select('.x-axis')
+ .call(this._xAxis)
+ .attr('transform', `translate(0, ${props.chartHeight})`)
+ .selectAll('text')
+ ::setStyles(X_AXIS_TEXT_STYLE)
+
+ // 4. Update label.
+ svg
+ .select('.label')
+ .attr('dx', 5)
+ .attr('dy', 20)
+ .text(props.label)
+ }
+
+ _handleMouseMove = () => {
+ this.props.onTooltipChange(
+ d3.mouse(this.refs.chart)[0] - HORIZON_AREA_MARGIN
+ )
+ }
+
+ // Update hover area position and text.
+ _updateTooltip (tooltipX) {
+ const date = this._x.invert(tooltipX)
+ const { data } = this.props
+ const index = this._bisectDate(data, date, 1)
+
+ const d0 = data[index - 1]
+ const d1 = data[index]
+
+ // Outside limits.
+ if (d1 === undefined) {
+ return
+ }
+
+ const elem = date - d0.date > d1.date - date ? d1 : d0
+ const x = this._x(elem.date)
+
+ const { props } = this
+ const hover = this._svg.select('.hover-container')
+
+ hover
+ .select('.hover-line')
+ .attr('x1', x)
+ .attr('x2', x)
+
+ hover
+ .select('.hover-text')
+ .attr('dx', x + 5)
+ .attr('dy', props.chartHeight / 2)
+ .text(props.valueRenderer(elem.value))
+ }
+
+ componentDidMount () {
+ // Horizon area ----------------------------------------
+
+ const svg = (this._svg = d3
+ .select(this.refs.chart)
+ .append('svg')
+ .attr('transform', `translate(${HORIZON_AREA_MARGIN}, 0)`))
+ svg
+ .append('g')
+ .attr('class', 'x-axis')
+ ::setStyles(X_AXIS_STYLE)
+
+ svg.append('g').attr('class', 'horizon-area')
+ svg
+ .append('text')
+ .attr('class', 'label')
+ ::setStyles(LABEL_STYLE)
+
+ // Tooltip ---------------------------------------------
+ svg
+ .append('rect')
+ .attr('class', 'mouse-area')
+ .on('mousemove', this._handleMouseMove)
+ ::setStyles(MOUSE_AREA_STYLE)
+
+ const hover = svg
+ .append('g')
+ .attr('class', 'hover-container')
+ ::setStyles('pointer-events', 'none')
+ hover
+ .append('line')
+ .attr('class', 'hover-line')
+ .attr('y1', 0)
+ ::setStyles(HOVER_LINE_STYLE)
+ hover
+ .append('text')
+ .attr('class', 'hover-text')
+ ::setStyles(HOVER_TEXT_STYLE)
+
+ this._draw()
+ }
+
+ componentWillReceiveProps (nextProps) {
+ const { props } = this
+
+ if (
+ !propsEqual(props, nextProps, [
+ 'chartHeight',
+ 'chartWidth',
+ 'data',
+ 'maxValue',
+ ])
+ ) {
+ this._draw(nextProps)
+ }
+
+ if (props.tooltipX !== nextProps.tooltipX) {
+ this._updateTooltip(nextProps.tooltipX)
+ }
+ }
+
+ render () {
+ return
+ }
+}
+
+// ===================================================================
+
+@propTypes({
+ chartHeight: propTypes.number,
+ series: propTypes.arrayOf(
+ propTypes.shape({
+ data: propTypes.arrayOf(
+ propTypes.shape({
+ date: propTypes.number.isRequired,
+ value: propTypes.number.isRequired,
+ })
+ ).isRequired,
+ objectId: propTypes.string.isRequired,
+ })
+ ).isRequired,
+ valueRenderer: propTypes.func,
+})
+export default class XoWeekCharts extends Component {
+ _handleResize = () => {
+ const { container } = this.refs
+ this.setState({
+ chartsWidth: container && container.offsetWidth,
+ })
+ }
+
+ _handleTooltipChange = x => {
+ this.setState({ tooltipX: x })
+ }
+
+ _updateScale = (useScale, series = this.props.series) => {
+ let max
+
+ if (useScale) {
+ max = 0
+ forEach(series, series => {
+ forEach(series.data, elem => {
+ max = Math.max(elem.value, max)
+ })
+ })
+ }
+
+ this.setState({
+ maxValue: max,
+ })
+ }
+
+ componentDidMount () {
+ window.addEventListener('resize', this._handleResize)
+ this._handleResize()
+ }
+
+ componentWillMount () {
+ this.setState({
+ tooltipX: 0,
+ })
+ }
+
+ componentWillUnmount () {
+ window.removeEventListener('resize', this._handleResize)
+ }
+
+ componentWillReceiveProps (nextProps) {
+ const { series } = nextProps
+
+ if (this.props.series !== series) {
+ this.setState({ tooltipX: 0 })
+ this._updateScale(this.state.maxValue !== undefined, series)
+ }
+ }
+
+ render () {
+ const { props, state: { chartsWidth, maxValue, tooltipX } } = this
+
+ return (
+
+
+
+ {_('weeklyChartsScaleInfo')}{' '}
+
+
+
+
+ {chartsWidth &&
+ map(props.series, (series, key) => (
+
+ ))}
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/common/xo-week-heatmap/index.css b/packages/xo-web/src/common/xo-week-heatmap/index.css
new file mode 100644
index 000000000..3e6a74544
--- /dev/null
+++ b/packages/xo-web/src/common/xo-week-heatmap/index.css
@@ -0,0 +1,13 @@
+.cell {
+ border-radius: 4px;
+ height: 2em;
+ padding: 1px;
+ transition: background-color 300ms linear;
+}
+
+.table {
+ border-spacing: 2px;
+ border-collapse: separate;
+ table-layout: fixed;
+ width: 100%;
+}
diff --git a/packages/xo-web/src/common/xo-week-heatmap/index.js b/packages/xo-web/src/common/xo-week-heatmap/index.js
new file mode 100644
index 000000000..bac82534a
--- /dev/null
+++ b/packages/xo-web/src/common/xo-week-heatmap/index.js
@@ -0,0 +1,188 @@
+import React from 'react'
+import forEach from 'lodash/forEach'
+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 { FormattedTime } from 'react-intl'
+
+import _ from '../intl'
+import Component from '../base-component'
+import propTypes from '../prop-types-decorator'
+import Tooltip from '../tooltip'
+
+import styles from './index.css'
+
+// ===================================================================
+
+const DAY_TIME_FORMAT = {
+ day: 'numeric',
+ month: 'numeric',
+}
+
+// ===================================================================
+
+const computeColorGen = days => {
+ let min = Number.MAX_VALUE
+ let max = Number.MIN_VALUE
+
+ forEach(days, day => {
+ const [_min, _max] = extent(day.hours, value => value && value.value)
+
+ if (_min < min) {
+ min = _min
+ }
+
+ if (_max > max) {
+ max = _max
+ }
+ })
+
+ return scaleSequential(interpolateViridis).domain([max, min])
+}
+
+const computeMissingDays = days => {
+ const correctedDays = days.slice()
+ const end = days.length - 1
+ const hours = new Array(24)
+
+ let a = moment(days[end].timestamp)
+ let b
+
+ for (let i = end; i > 0; i--) {
+ b = moment(days[i - 1].timestamp)
+
+ const diff = a.diff(b, 'days')
+
+ if (diff > 1) {
+ const missingDays = times(diff - 1, () => ({
+ hours,
+ timestamp: a.subtract(1, 'days').valueOf(),
+ })).reverse()
+
+ correctedDays.splice.apply(correctedDays, [i, 0].concat(missingDays))
+ }
+
+ a = b
+ }
+
+ return correctedDays
+}
+
+// ===================================================================
+
+@propTypes({
+ cellRenderer: propTypes.func,
+ data: propTypes.arrayOf(
+ propTypes.shape({
+ date: propTypes.number.isRequired,
+ value: propTypes.number.isRequired,
+ })
+ ).isRequired,
+})
+export default class XoWeekHeatmap extends Component {
+ static defaultProps = {
+ cellRenderer: value => value,
+ }
+
+ componentWillReceiveProps (nextProps) {
+ this._updateDays(nextProps.data)
+ }
+
+ componentWillMount () {
+ this._updateDays(this.props.data)
+ }
+
+ _updateDays (data) {
+ const days = {}
+
+ // 1. Compute average per day.
+ forEach(data, elem => {
+ const date = new Date(elem.date)
+ const dayId = moment(date).format('YYYYMMDD')
+ const hourId = date.getHours()
+
+ const { value } = elem
+
+ if (!days[dayId]) {
+ days[dayId] = {
+ hours: new Array(24),
+ timestamp: elem.date,
+ }
+ }
+
+ const { hours } = days[dayId]
+
+ if (!hours[hourId]) {
+ hours[hourId] = {
+ date,
+ nb: 1,
+ value,
+ }
+ } else {
+ const hour = hours[hourId]
+ hour.value = (hour.value * hour.nb + value) / (hour.nb + 1)
+ hour.nb++
+ }
+ })
+
+ // 2. Compute color gen.
+ const colorGen = computeColorGen(days)
+
+ // 3. Define color cells.
+ forEach(days, day => {
+ forEach(day.hours, hour => {
+ if (hour) {
+ hour.color = colorGen(hour.value)
+ }
+ })
+ })
+
+ this.setState({
+ days: computeMissingDays(sortBy(days, 'timestamp')),
+ })
+ }
+
+ render () {
+ return (
+
+
+
+
+ {times(24, hour => (
+
+ {hour}
+
+ ))}
+
+ {map(this.state.days, (day, key) => (
+
+
+
+
+ {map(day.hours, (hour, key) => (
+
+
+
+ ))}
+
+ ))}
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/common/xo/add-host-modal/index.js b/packages/xo-web/src/common/xo/add-host-modal/index.js
new file mode 100644
index 000000000..a1bc79210
--- /dev/null
+++ b/packages/xo-web/src/common/xo/add-host-modal/index.js
@@ -0,0 +1,103 @@
+import _ from 'intl'
+import BaseComponent from 'base-component'
+import Icon from 'icon'
+import React from 'react'
+import SingleLineRow from 'single-line-row'
+import { Col } from 'grid'
+import { connectStore } from 'utils'
+import {
+ createCollectionWrapper,
+ createGetObjectsOfType,
+ createSelector,
+ createGetObject,
+} from 'selectors'
+import { SelectHost } from 'select-objects'
+import { differenceBy, forEach } from 'lodash'
+
+@connectStore(
+ () => ({
+ singleHosts: createSelector(
+ (_, { pool }) => pool && pool.id,
+ createGetObjectsOfType('host'),
+ createCollectionWrapper((poolId, hosts) => {
+ const visitedPools = {}
+ const singleHosts = {}
+ forEach(hosts, host => {
+ const { $pool } = host
+ if ($pool !== poolId) {
+ const previousHost = visitedPools[$pool]
+ if (previousHost) {
+ delete singleHosts[previousHost]
+ } else {
+ const { id } = host
+ singleHosts[id] = true
+ visitedPools[$pool] = id
+ }
+ }
+ })
+ return singleHosts
+ })
+ ),
+ poolMasterPatches: createSelector(
+ createGetObject((_, props) => props.pool.master),
+ ({ patches }) => patches
+ ),
+ }),
+ { withRef: true }
+)
+export default class AddHostModal extends BaseComponent {
+ get value () {
+ if (process.env.XOA_PLAN < 2 && this.state.nMissingPatches) {
+ return {}
+ }
+
+ return this.state
+ }
+
+ _getHostPredicate = createSelector(
+ () => this.props.singleHosts,
+ singleHosts => host => singleHosts[host.id]
+ )
+
+ _onChangeHost = host => {
+ this.setState({
+ host,
+ nMissingPatches: host
+ ? differenceBy(this.props.poolMasterPatches, host.patches, 'name')
+ .length
+ : undefined,
+ })
+ }
+
+ render () {
+ const { nMissingPatches } = this.state
+
+ return (
+
+
+ {_('addHostSelectHost')}
+
+
+
+
+
+ {nMissingPatches > 0 && (
+
+
+
+ {' '}
+ {process.env.XOA_PLAN > 1
+ ? _('hostNeedsPatchUpdate', { patches: nMissingPatches })
+ : _('hostNeedsPatchUpdateNoInstall')}
+
+
+
+ )}
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/common/xo/add-user-filter-modal/index.js b/packages/xo-web/src/common/xo/add-user-filter-modal/index.js
new file mode 100644
index 000000000..bf3c84055
--- /dev/null
+++ b/packages/xo-web/src/common/xo/add-user-filter-modal/index.js
@@ -0,0 +1,60 @@
+import keys from 'lodash/keys'
+import React from 'react'
+
+import * as FormGrid from '../../form-grid'
+import _ from '../../intl'
+import Combobox from '../../combobox'
+import Component from '../../base-component'
+import propTypes from '../../prop-types-decorator'
+import { createSelector } from '../../selectors'
+
+@propTypes({
+ type: propTypes.string.isRequired,
+ user: propTypes.object.isRequired,
+ value: propTypes.string.isRequired,
+})
+export default class SaveNewUserFilterModalBody extends Component {
+ get value () {
+ return this.state.name || ''
+ }
+
+ _getFilterOptions = createSelector(
+ tmp =>
+ (tmp = this.props.user) &&
+ (tmp = tmp.preferences) &&
+ (tmp = tmp.filters) &&
+ tmp[this.props.type],
+ keys
+ )
+
+ render () {
+ const { value } = this.props
+ const options = this._getFilterOptions()
+
+ return (
+
+
+ {_('filterName')}
+
+
+
+
+
+ {_('filterValue')}
+
+
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/common/xo/choose-sr-for-each-vdis-modal/index.js b/packages/xo-web/src/common/xo/choose-sr-for-each-vdis-modal/index.js
new file mode 100644
index 000000000..5537771e0
--- /dev/null
+++ b/packages/xo-web/src/common/xo/choose-sr-for-each-vdis-modal/index.js
@@ -0,0 +1,107 @@
+import Collapse from 'collapse'
+import Component from 'base-component'
+import React from 'react'
+import { map } from 'lodash'
+
+import _ from '../../intl'
+import propTypes from '../../prop-types-decorator'
+import SingleLineRow from '../../single-line-row'
+import { Container, Col } from 'grid'
+import { isSrWritable } from 'xo'
+import { SelectSr } from '../../select-objects'
+
+const Collapsible = ({ collapsible, children, ...props }) =>
+ collapsible ? (
+ {children}
+ ) : (
+
+ {props.buttonText}
+
+ {children}
+
+ )
+
+Collapsible.propTypes = {
+ collapsible: propTypes.bool.isRequired,
+ children: propTypes.node.isRequired,
+}
+
+@propTypes({
+ mainSrPredicate: propTypes.func,
+ onChange: propTypes.func.isRequired,
+ srPredicate: propTypes.func,
+ value: propTypes.objectOf(
+ propTypes.shape({
+ mainSr: propTypes.object,
+ mapVdisSrs: propTypes.object,
+ })
+ ).isRequired,
+ vdis: propTypes.object.isRequired,
+})
+export default class ChooseSrForEachVdisModal extends Component {
+ _onChange = newValues => {
+ this.props.onChange({
+ ...this.props.value,
+ ...newValues,
+ })
+ }
+
+ _onChangeMainSr = mainSr => this._onChange({ mainSr })
+
+ render () {
+ const { props } = this
+ const {
+ mainSrPredicate = isSrWritable,
+ srPredicate = mainSrPredicate,
+ value: { mainSr, mapVdisSrs },
+ } = props
+
+ return (
+
+
+
+ {props.vdis != null &&
+ mainSr != null && (
+ = 3}
+ >
+
+
+
+
+ {_('chooseSrForEachVdisModalVdiLabel')}
+
+
+ {_('chooseSrForEachVdisModalSrLabel')}
+
+
+ {map(props.vdis, vdi => (
+
+ {vdi.name_label || vdi.name}
+
+
+ this._onChange({
+ mapVdisSrs: { ...mapVdisSrs, [vdi.uuid]: sr },
+ })
+ }
+ predicate={srPredicate}
+ value={mapVdisSrs !== undefined && mapVdisSrs[vdi.uuid]}
+ />
+
+
+ ))}
+ {_('chooseSrForEachVdisModalOptionalEntry')}
+
+
+ )}
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/common/xo/copy-vm-modal/index.js b/packages/xo-web/src/common/xo/copy-vm-modal/index.js
new file mode 100644
index 000000000..5e27e1272
--- /dev/null
+++ b/packages/xo-web/src/common/xo/copy-vm-modal/index.js
@@ -0,0 +1,64 @@
+import React, { Component } from 'react'
+
+import _, { messages } from '../../intl'
+import SingleLineRow from '../../single-line-row'
+import Upgrade from 'xoa-upgrade'
+import { Col } from '../../grid'
+import { SelectSr } from '../../select-objects'
+import { Toggle } from '../../form'
+import { injectIntl } from 'react-intl'
+
+class CopyVmModalBody extends Component {
+ state = { compress: false }
+
+ get value () {
+ const { state } = this
+ return {
+ compress: state.compress,
+ name: this.state.name || this.props.vm.name_label,
+ sr: state.sr.id,
+ }
+ }
+
+ _onChangeSr = sr => this.setState({ sr })
+ _onChangeName = event => this.setState({ name: event.target.value })
+ _onChangeCompress = compress => this.setState({ compress })
+
+ render () {
+ const { formatMessage } = this.props.intl
+ return process.env.XOA_PLAN > 2 ? (
+
+
+ {_('copyVmSelectSr')}
+
+
+
+
+
+
+ {_('copyVmName')}
+
+
+
+
+
+
+ {_('copyVmCompress')}
+
+
+
+
+
+ ) : (
+
+
+
+ )
+ }
+}
+export default injectIntl(CopyVmModalBody, { withRef: true })
diff --git a/packages/xo-web/src/common/xo/copy-vms-modal/index.js b/packages/xo-web/src/common/xo/copy-vms-modal/index.js
new file mode 100644
index 000000000..286bbcfee
--- /dev/null
+++ b/packages/xo-web/src/common/xo/copy-vms-modal/index.js
@@ -0,0 +1,100 @@
+import _, { messages } from 'intl'
+import map from 'lodash/map'
+import React from 'react'
+import { injectIntl } from 'react-intl'
+
+import BaseComponent from 'base-component'
+import SingleLineRow from 'single-line-row'
+import Upgrade from 'xoa-upgrade'
+import { Col } from 'grid'
+import { createGetObjectsOfType } from 'selectors'
+import { SelectSr } from 'select-objects'
+import { Toggle } from 'form'
+import { buildTemplate, connectStore } from 'utils'
+
+@connectStore(
+ () => {
+ const getVms = createGetObjectsOfType('VM').pick((_, props) => props.vms)
+ return {
+ vms: getVms,
+ }
+ },
+ { withRef: true }
+)
+class CopyVmsModalBody extends BaseComponent {
+ get value () {
+ const { state } = this
+ if (!state || !state.sr) {
+ return {}
+ }
+ const { vms } = this.props
+ const { namePattern } = state
+
+ const names = namePattern
+ ? map(
+ vms,
+ buildTemplate(namePattern, {
+ '{name}': vm => vm.name_label,
+ '{id}': vm => vm.id,
+ })
+ )
+ : map(vms, vm => vm.name_label)
+ return {
+ compress: state.compress,
+ names,
+ sr: state.sr.id,
+ }
+ }
+
+ componentWillMount () {
+ this.setState({
+ compress: false,
+ namePattern: '{name}_COPY',
+ })
+ }
+
+ _onChangeSr = sr => this.setState({ sr })
+ _onChangeNamePattern = event =>
+ this.setState({ namePattern: event.target.value })
+ _onChangeCompress = compress => this.setState({ compress })
+
+ render () {
+ const { formatMessage } = this.props.intl
+ const { compress, namePattern, sr } = this.state
+ return process.env.XOA_PLAN > 2 ? (
+
+
+ {_('copyVmSelectSr')}
+
+
+
+
+
+
+ {_('copyVmName')}
+
+
+
+
+
+
+ {_('copyVmCompress')}
+
+
+
+
+
+ ) : (
+
+
+
+ )
+ }
+}
+export default injectIntl(CopyVmsModalBody, { withRef: true })
diff --git a/packages/xo-web/src/common/xo/create-bonded-network-modal/index.js b/packages/xo-web/src/common/xo/create-bonded-network-modal/index.js
new file mode 100644
index 000000000..61fe462ef
--- /dev/null
+++ b/packages/xo-web/src/common/xo/create-bonded-network-modal/index.js
@@ -0,0 +1,113 @@
+import Component from 'base-component'
+import map from 'lodash/map'
+import React from 'react'
+import { createGetObject, createSelector } from 'selectors'
+import { getBondModes } from 'xo'
+import { injectIntl } from 'react-intl'
+
+import _, { messages } from '../../intl'
+import { Col } from '../../grid'
+import { connectStore } from '../../utils'
+import { SelectPif } from '../../select-objects'
+import SingleLineRow from '../../single-line-row'
+
+@connectStore(
+ () => ({
+ poolMaster: createSelector(
+ createGetObject((_, props) => props.pool),
+ pool => pool.master
+ ),
+ }),
+ { withRef: true }
+)
+class CreateBondedNetworkModalBody extends Component {
+ componentWillMount () {
+ getBondModes().then(bondModes =>
+ this.setState({ bondModes, bondMode: bondModes[0] })
+ )
+ }
+
+ _getPifPredicate = createSelector(
+ () => this.props.poolMaster,
+ hostId => pif => pif.$host === hostId && pif.vlan === -1
+ )
+
+ get value () {
+ const { name, description, pifs, mtu, bondMode } = this.state
+ return {
+ pool: this.props.pool,
+ name,
+ description,
+ pifs: map(pifs, pif => pif.id),
+ mtu,
+ bondMode,
+ }
+ }
+
+ render () {
+ const { formatMessage } = this.props.intl
+ return (
+
+
+ {_('newNetworkInterface')}
+
+
+
+
+
+
+ {_('newNetworkName')}
+
+
+
+
+
+
+ {_('newNetworkDescription')}
+
+
+
+
+
+
+ {_('newNetworkMtu')}
+
+
+
+
+
+
+ {_('newNetworkBondMode')}
+
+
+ {map(this.state.bondModes, mode => (
+ {mode}
+ ))}
+
+
+
+
+ )
+ }
+}
+export default injectIntl(CreateBondedNetworkModalBody, { withRef: true })
diff --git a/packages/xo-web/src/common/xo/create-network-modal/index.js b/packages/xo-web/src/common/xo/create-network-modal/index.js
new file mode 100644
index 000000000..f8e0a27f6
--- /dev/null
+++ b/packages/xo-web/src/common/xo/create-network-modal/index.js
@@ -0,0 +1,84 @@
+import React, { Component } from 'react'
+import { injectIntl } from 'react-intl'
+import { createSelector } from 'selectors'
+
+import SingleLineRow from '../../single-line-row'
+import _, { messages } from '../../intl'
+import { SelectPif } from '../../select-objects'
+import { Col } from '../../grid'
+
+class CreateNetworkModalBody extends Component {
+ _getPifPredicate = createSelector(
+ () => {
+ const { container } = this.props
+ return container.type === 'pool' ? container.master : container.id
+ },
+ hostId => pif => pif.$host === hostId && pif.vlan === -1
+ )
+
+ get value () {
+ const { refs } = this
+ const { container } = this.props
+ return {
+ pool: container.$pool,
+ name: refs.name.value,
+ description: refs.description.value,
+ pif: refs.pif.value.id,
+ mtu: refs.mtu.value,
+ vlan: refs.vlan.value,
+ }
+ }
+
+ render () {
+ const { formatMessage } = this.props.intl
+ return (
+
+
+ {_('newNetworkInterface')}
+
+
+
+
+
+
+ {_('newNetworkName')}
+
+
+
+
+
+
+ {_('newNetworkDescription')}
+
+
+
+
+
+
+ {_('newNetworkVlan')}
+
+
+
+
+
+
+ {_('newNetworkMtu')}
+
+
+
+
+
+ )
+ }
+}
+export default injectIntl(CreateNetworkModalBody, { withRef: true })
diff --git a/packages/xo-web/src/common/xo/index.js b/packages/xo-web/src/common/xo/index.js
new file mode 100644
index 000000000..4cd9b37ac
--- /dev/null
+++ b/packages/xo-web/src/common/xo/index.js
@@ -0,0 +1,2193 @@
+import asap from 'asap'
+import cookies from 'cookies-js'
+import fpSortBy from 'lodash/fp/sortBy'
+import React from 'react'
+import URL from 'url-parse'
+import Xo from 'xo-lib'
+import { createBackoff } from 'jsonrpc-websocket-client'
+import {
+ assign,
+ filter,
+ forEach,
+ includes,
+ isEmpty,
+ isEqual,
+ map,
+ once,
+ size,
+ sortBy,
+ throttle,
+} from 'lodash'
+import { lastly, reflect, tap } from 'promise-toolbox'
+import { forbiddenOperation, noHostsAvailable } from 'xo-common/api-errors'
+
+import _ from '../intl'
+import invoke from '../invoke'
+import logError from '../log-error'
+import store from 'store'
+import { alert, chooseAction, confirm } from '../modal'
+import { error, info, success } from '../notification'
+import { getObject } from 'selectors'
+import { post } from '../fetch'
+import { noop, resolveId, resolveIds } from '../utils'
+import {
+ connected,
+ disconnected,
+ signedIn,
+ signedOut,
+ updateObjects,
+ updatePermissions,
+} from '../store/actions'
+
+// ===================================================================
+
+export const XEN_DEFAULT_CPU_WEIGHT = 256
+export const XEN_DEFAULT_CPU_CAP = 0
+
+// ===================================================================
+
+export const XEN_VIDEORAM_VALUES = [1, 2, 4, 8, 16]
+
+// ===================================================================
+
+export const isSrWritable = sr => sr && sr.content_type !== 'iso' && sr.size > 0
+export const isSrShared = sr => sr && sr.shared
+export const isVmRunning = vm => vm && vm.power_state === 'Running'
+
+// ===================================================================
+
+export const signOut = () => {
+ cookies.expire('token')
+ window.location.reload(true)
+}
+
+export const connect = () => {
+ xo.open(createBackoff()).catch(error => {
+ logError(error, 'failed to connect to xo-server')
+ })
+}
+
+const xo = invoke(() => {
+ const token = cookies.get('token')
+ if (!token) {
+ signOut()
+ throw new Error('no valid token')
+ }
+
+ const xo = new Xo({
+ credentials: { token },
+ })
+
+ xo.on('authenticationFailure', signOut)
+ xo.on('scheduledAttempt', ({ delay }) => {
+ console.warn('next attempt in %s ms', delay)
+ })
+
+ xo.on('closed', connect)
+
+ return xo
+})
+connect()
+
+const _signIn = new Promise(resolve => xo.once('authenticated', resolve))
+
+const _call = (method, params) => {
+ let promise = _signIn.then(() => xo.call(method, params))
+
+ if (process.env.NODE_ENV !== 'production') {
+ promise = promise::tap(null, error => {
+ console.error('XO error', {
+ method,
+ params,
+ code: error.code,
+ message: error.message,
+ data: error.data,
+ })
+ })
+ }
+
+ return promise
+}
+
+// ===================================================================
+
+export const connectStore = store => {
+ let updates = {}
+ const sendUpdates = throttle(() => {
+ store.dispatch(updateObjects(updates))
+ updates = {}
+ }, 5e2)
+
+ xo.on('open', () => store.dispatch(connected()))
+ xo.on('closed', () => {
+ store.dispatch(signedOut())
+ store.dispatch(disconnected())
+ })
+ xo.on('authenticated', () => {
+ store.dispatch(signedIn(xo.user))
+
+ _call('xo.getAllObjects').then(objects =>
+ store.dispatch(updateObjects(objects))
+ )
+ })
+ xo.on('notification', notification => {
+ if (notification.method !== 'all') {
+ return
+ }
+
+ assign(updates, notification.params.items)
+ sendUpdates()
+ })
+ subscribePermissions(permissions =>
+ store.dispatch(updatePermissions(permissions))
+ )
+
+ // work around to keep the user in Redux store up to date
+ //
+ // FIXME: store subscriptions data directly in Redux
+ subscribeUsers(user => {
+ store.dispatch(signedIn(xo.user))
+ })
+}
+
+// -------------------------------------------------------------------
+
+export const resolveUrl = invoke(
+ xo._url, // FIXME: accessing private prop
+ baseUrl => to => new URL(to, baseUrl).toString()
+)
+
+// -------------------------------------------------------------------
+
+const createSubscription = cb => {
+ const delay = 5e3
+
+ const subscribers = Object.create(null)
+ let cache
+ let n = 0
+ let nextId = 0
+ let timeout
+
+ let running = false
+
+ const uninstall = () => {
+ clearTimeout(timeout)
+ cache = undefined
+ }
+
+ const loop = () => {
+ if (running) {
+ return
+ }
+
+ running = true
+ _signIn.then(() => cb()).then(
+ result => {
+ running = false
+
+ if (n === 0) {
+ return uninstall()
+ }
+
+ timeout = setTimeout(loop, delay)
+
+ if (!isEqual(result, cache)) {
+ cache = result
+
+ forEach(subscribers, subscriber => {
+ // A subscriber might have disappeared during iteration.
+ //
+ // E.g.: if a subscriber triggers the subscription of another.
+ if (subscriber) {
+ subscriber(result)
+ }
+ })
+ }
+ },
+ error => {
+ running = false
+
+ if (n === 0) {
+ return uninstall()
+ }
+
+ console.error(error)
+ }
+ )
+ }
+
+ const subscribe = cb => {
+ const id = nextId++
+ subscribers[id] = cb
+
+ if (n++ !== 0) {
+ if (cache !== undefined) {
+ asap(() => cb(cache))
+ }
+ } else {
+ loop()
+ }
+
+ return once(() => {
+ delete subscribers[id]
+
+ if (--n === 0) {
+ uninstall()
+ }
+ })
+ }
+
+ subscribe.forceRefresh = () => {
+ if (n) {
+ clearTimeout(timeout)
+ loop()
+ }
+ }
+
+ return subscribe
+}
+
+// Subscriptions -----------------------------------------------------
+
+export const subscribeCurrentUser = createSubscription(() => xo.refreshUser())
+
+export const subscribeAcls = createSubscription(() => _call('acl.get'))
+
+export const subscribeJobs = createSubscription(() => _call('job.getAll'))
+
+export const subscribeJobsLogs = createSubscription(() =>
+ _call('log.get', { namespace: 'jobs' })
+)
+
+export const subscribeApiLogs = createSubscription(() =>
+ _call('log.get', { namespace: 'api' })
+)
+
+export const subscribePermissions = createSubscription(() =>
+ _call('acl.getCurrentPermissions')
+)
+
+export const subscribePlugins = createSubscription(() => _call('plugin.get'))
+
+export const subscribeRemotes = createSubscription(() => _call('remote.getAll'))
+
+export const subscribeResourceSets = createSubscription(() =>
+ _call('resourceSet.getAll')
+)
+
+export const subscribeScheduleTable = createSubscription(() =>
+ _call('scheduler.getScheduleTable')
+)
+
+export const subscribeSchedules = createSubscription(() =>
+ _call('schedule.getAll')
+)
+
+export const subscribeServers = createSubscription(
+ invoke(fpSortBy('host'), sort => () => _call('server.getAll').then(sort))
+)
+
+export const subscribeUsers = createSubscription(() =>
+ _call('user.getAll').then(users => {
+ forEach(users, user => {
+ user.type = 'user'
+ })
+
+ return sortBy(users, 'email')
+ })
+)
+
+export const subscribeGroups = createSubscription(() =>
+ _call('group.getAll').then(groups => {
+ forEach(groups, group => {
+ group.type = 'group'
+ })
+
+ return sortBy(groups, 'name')
+ })
+)
+
+export const subscribeRoles = createSubscription(
+ invoke(sortBy('name'), sort => () => _call('role.getAll').then(sort))
+)
+
+export const subscribeIpPools = createSubscription(() => _call('ipPool.getAll'))
+
+export const subscribeResourceCatalog = createSubscription(() =>
+ _call('cloud.getResourceCatalog')
+)
+
+const checkSrCurrentStateSubscriptions = {}
+export const subscribeCheckSrCurrentState = (pool, cb) => {
+ const poolId = resolveId(pool)
+
+ if (!checkSrCurrentStateSubscriptions[poolId]) {
+ checkSrCurrentStateSubscriptions[poolId] = createSubscription(() =>
+ _call('xosan.checkSrCurrentState', { poolId })
+ )
+ }
+
+ return checkSrCurrentStateSubscriptions[poolId](cb)
+}
+subscribeCheckSrCurrentState.forceRefresh = pool => {
+ if (pool === undefined) {
+ forEach(checkSrCurrentStateSubscriptions, subscription =>
+ subscription.forceRefresh()
+ )
+ return
+ }
+
+ const subscription = checkSrCurrentStateSubscriptions[resolveId(pool)]
+ if (subscription !== undefined) {
+ subscription.forceRefresh()
+ }
+}
+
+const missingPatchesByHost = {}
+export const subscribeHostMissingPatches = (host, cb) => {
+ const hostId = resolveId(host)
+
+ if (missingPatchesByHost[hostId] == null) {
+ missingPatchesByHost[hostId] = createSubscription(() =>
+ getHostMissingPatches(host)
+ )
+ }
+
+ return missingPatchesByHost[hostId](cb)
+}
+subscribeHostMissingPatches.forceRefresh = host => {
+ if (host === undefined) {
+ forEach(missingPatchesByHost, subscription => subscription.forceRefresh())
+ return
+ }
+
+ const subscription = missingPatchesByHost[resolveId(host)]
+ if (subscription !== undefined) {
+ subscription.forceRefresh()
+ }
+}
+
+const volumeInfoBySr = {}
+export const subscribeVolumeInfo = ({ sr, infoType }, cb) => {
+ sr = resolveId(sr)
+
+ if (volumeInfoBySr[sr] == null) {
+ volumeInfoBySr[sr] = {}
+ }
+
+ if (volumeInfoBySr[sr][infoType] == null) {
+ volumeInfoBySr[sr][infoType] = createSubscription(() =>
+ _call('xosan.getVolumeInfo', { sr, infoType })
+ )
+ }
+
+ return volumeInfoBySr[sr][infoType](cb)
+}
+subscribeVolumeInfo.forceRefresh = (() => {
+ const refreshSrVolumeInfo = volumeInfo => {
+ forEach(volumeInfo, subscription => subscription.forceRefresh())
+ }
+
+ return sr => {
+ if (sr === undefined) {
+ forEach(volumeInfoBySr, refreshSrVolumeInfo)
+ } else {
+ refreshSrVolumeInfo(volumeInfoBySr[sr])
+ }
+ }
+})()
+
+const unhealthyVdiChainsLengthSubscriptionsBySr = {}
+export const createSrUnhealthyVdiChainsLengthSubscription = sr => {
+ sr = resolveId(sr)
+ let subscription = unhealthyVdiChainsLengthSubscriptionsBySr[sr]
+ if (subscription === undefined) {
+ subscription = createSubscription(() =>
+ _call('sr.getUnhealthyVdiChainsLength', { sr })
+ )
+ unhealthyVdiChainsLengthSubscriptionsBySr[sr] = subscription
+ }
+ return subscription
+}
+
+// System ============================================================
+
+export const apiMethods = _call('system.getMethodsInfo')
+
+export const serverVersion = _call('system.getServerVersion')
+
+export const getXoServerTimezone = _call('system.getServerTimezone')
+
+// XO --------------------------------------------------------------------------
+
+export const importConfig = config =>
+ _call('xo.importConfig').then(({ $sendTo }) =>
+ post($sendTo, config).then(response => {
+ if (response.status !== 200) {
+ throw new Error('config import failed')
+ }
+ })
+ )
+
+export const exportConfig = () =>
+ _call('xo.exportConfig').then(({ $getFrom: url }) => {
+ window.location = `.${url}`
+ })
+
+// Server ------------------------------------------------------------
+
+export const addServer = (host, username, password, label) =>
+ _call('server.add', { host, label, password, username })::tap(
+ subscribeServers.forceRefresh,
+ () => error(_('serverError'), _('serverAddFailed'))
+ )
+
+export const editServer = (server, props) =>
+ _call('server.set', { ...props, id: resolveId(server) })::tap(
+ subscribeServers.forceRefresh
+ )
+
+export const connectServer = server =>
+ _call('server.connect', { id: resolveId(server) })::lastly(
+ subscribeServers.forceRefresh
+ )
+
+export const disconnectServer = server =>
+ _call('server.disconnect', { id: resolveId(server) })::tap(
+ subscribeServers.forceRefresh
+ )
+
+export const removeServer = server =>
+ _call('server.remove', { id: resolveId(server) })::tap(
+ subscribeServers.forceRefresh
+ )
+
+// Pool --------------------------------------------------------------
+
+export const editPool = (pool, props) =>
+ _call('pool.set', { id: resolveId(pool), ...props })
+
+import AddHostModalBody from './add-host-modal' // eslint-disable-line import/first
+export const addHostToPool = (pool, host) => {
+ if (host) {
+ return confirm({
+ icon: 'add',
+ title: _('addHostModalTitle'),
+ body: _('addHostModalMessage', {
+ pool: pool.name_label,
+ host: host.name_label,
+ }),
+ }).then(() =>
+ _call('pool.mergeInto', {
+ source: host.$pool,
+ target: pool.id,
+ force: true,
+ })
+ )
+ }
+
+ return confirm({
+ icon: 'add',
+ title: _('addHostModalTitle'),
+ body: ,
+ }).then(params => {
+ if (!params.host) {
+ error(_('addHostNoHost'), _('addHostNoHostMessage'))
+ return
+ }
+ return _call('pool.mergeInto', {
+ source: params.host.$pool,
+ target: pool.id,
+ force: true,
+ }).catch(error => {
+ if (error.code !== 'HOSTS_NOT_HOMOGENEOUS') {
+ throw error
+ }
+
+ error(_('addHostErrorTitle'), _('addHostNotHomogeneousErrorMessage'))
+ })
+ }, noop)
+}
+
+export const detachHost = host =>
+ confirm({
+ icon: 'host-eject',
+ title: _('detachHostModalTitle'),
+ body: _('detachHostModalMessage', {
+ host: {host.name_label} ,
+ }),
+ }).then(() => _call('host.detach', { host: host.id }))
+
+export const forgetHost = host =>
+ confirm({
+ icon: 'host-forget',
+ title: _('forgetHostModalTitle'),
+ body: _('forgetHostModalMessage', {
+ host: {host.name_label} ,
+ }),
+ }).then(() => _call('host.forget', { host: resolveId(host) }))
+
+export const setDefaultSr = sr =>
+ _call('pool.setDefaultSr', { sr: resolveId(sr) })
+
+export const setPoolMaster = host =>
+ confirm({
+ title: _('setPoolMasterModalTitle'),
+ body: _('setPoolMasterModalMessage', {
+ host: {host.name_label} ,
+ }),
+ }).then(() => _call('pool.setPoolMaster', { host: resolveId(host) }), noop)
+
+// Host --------------------------------------------------------------
+
+export const editHost = (host, props) =>
+ _call('host.set', { ...props, id: resolveId(host) })
+
+export const fetchHostStats = (host, granularity) =>
+ _call('host.stats', { host: resolveId(host), granularity })
+
+export const restartHost = (host, force = false) =>
+ confirm({
+ title: _('restartHostModalTitle'),
+ body: _('restartHostModalMessage'),
+ }).then(
+ () =>
+ _call('host.restart', { id: resolveId(host), force }).catch(error => {
+ if (noHostsAvailable.is(error)) {
+ alert(
+ _('noHostsAvailableErrorTitle'),
+ _('noHostsAvailableErrorMessage')
+ )
+ }
+ }),
+ noop
+ )
+
+export const restartHosts = (hosts, force = false) => {
+ const nHosts = size(hosts)
+ return confirm({
+ title: _('restartHostsModalTitle', { nHosts }),
+ body: _('restartHostsModalMessage', { nHosts }),
+ }).then(
+ () =>
+ Promise.all(
+ map(hosts, host =>
+ _call('host.restart', { id: resolveId(host), force })::reflect()
+ )
+ ).then(results => {
+ const nbErrors = filter(results, result => !result.isFulfilled()).length
+ if (nbErrors) {
+ return alert(
+ _('failHostBulkRestartTitle'),
+ _('failHostBulkRestartMessage', {
+ failedHosts: nbErrors,
+ totalHosts: results.length,
+ })
+ )
+ }
+ }),
+ noop
+ )
+}
+
+export const restartHostAgent = host =>
+ _call('host.restart_agent', { id: resolveId(host) })
+
+export const restartHostsAgents = hosts => {
+ const nHosts = size(hosts)
+ return confirm({
+ title: _('restartHostsAgentsModalTitle', { nHosts }),
+ body: _('restartHostsAgentsModalMessage', { nHosts }),
+ }).then(() => Promise.all(map(hosts, restartHostAgent)), noop)
+}
+
+export const startHost = host => _call('host.start', { id: resolveId(host) })
+
+export const stopHost = host =>
+ confirm({
+ title: _('stopHostModalTitle'),
+ body: _('stopHostModalMessage'),
+ }).then(() => _call('host.stop', { id: resolveId(host) }), noop)
+
+export const stopHosts = hosts => {
+ const nHosts = size(hosts)
+ return confirm({
+ title: _('stopHostsModalTitle', { nHosts }),
+ body: _('stopHostsModalMessage', { nHosts }),
+ }).then(
+ () => map(hosts, host => _call('host.stop', { id: resolveId(host) })),
+ noop
+ )
+}
+
+export const enableHost = host => _call('host.enable', { id: resolveId(host) })
+
+export const disableHost = host =>
+ _call('host.disable', { id: resolveId(host) })
+
+export const getHostMissingPatches = host =>
+ _call('host.listMissingPatches', { host: resolveId(host) }).then(
+ patches =>
+ // Hide paid patches to XS-free users
+ host.license_params.sku_type !== 'free'
+ ? patches
+ : filter(patches, ['paid', false])
+ )
+
+export const emergencyShutdownHost = host =>
+ _call('host.emergencyShutdownHost', { host: resolveId(host) })
+
+export const emergencyShutdownHosts = hosts => {
+ const nHosts = size(hosts)
+ return confirm({
+ title: _('emergencyShutdownHostsModalTitle', { nHosts }),
+ body: _('emergencyShutdownHostsModalMessage', { nHosts }),
+ }).then(() => map(hosts, host => emergencyShutdownHost(host)), noop)
+}
+
+export const installHostPatch = (host, { uuid }) =>
+ _call('host.installPatch', { host: resolveId(host), patch: uuid })::tap(() =>
+ subscribeHostMissingPatches.forceRefresh(host)
+ )
+
+export const installAllHostPatches = host =>
+ _call('host.installAllPatches', { host: resolveId(host) })::tap(() =>
+ subscribeHostMissingPatches.forceRefresh(host)
+ )
+
+export const installAllPatchesOnPool = pool =>
+ _call('pool.installAllPatches', { pool: resolveId(pool) })::tap(() =>
+ subscribeHostMissingPatches.forceRefresh()
+ )
+
+export const installSupplementalPack = (host, file) => {
+ info(
+ _('supplementalPackInstallStartedTitle'),
+ _('supplementalPackInstallStartedMessage')
+ )
+
+ return _call('host.installSupplementalPack', { host: resolveId(host) }).then(
+ ({ $sendTo }) =>
+ post($sendTo, file)
+ .then(res => {
+ if (res.status !== 200) {
+ throw new Error('installing supplemental pack failed')
+ }
+
+ success(
+ _('supplementalPackInstallSuccessTitle'),
+ _('supplementalPackInstallSuccessMessage')
+ )
+ })
+ .catch(err => {
+ error(
+ _('supplementalPackInstallErrorTitle'),
+ _('supplementalPackInstallErrorMessage')
+ )
+ throw err
+ })
+ )
+}
+
+export const installSupplementalPackOnAllHosts = (pool, file) => {
+ info(
+ _('supplementalPackInstallStartedTitle'),
+ _('supplementalPackInstallStartedMessage')
+ )
+
+ return _call('pool.installSupplementalPack', { pool: resolveId(pool) }).then(
+ ({ $sendTo }) =>
+ post($sendTo, file)
+ .then(res => {
+ if (res.status !== 200) {
+ throw new Error('installing supplemental pack failed')
+ }
+
+ success(
+ _('supplementalPackInstallSuccessTitle'),
+ _('supplementalPackInstallSuccessMessage')
+ )
+ })
+ .catch(err => {
+ error(
+ _('supplementalPackInstallErrorTitle'),
+ _('supplementalPackInstallErrorMessage')
+ )
+ throw err
+ })
+ )
+}
+
+// Containers --------------------------------------------------------
+
+export const pauseContainer = (vm, container) =>
+ _call('docker.pause', { vm: resolveId(vm), container })
+
+export const restartContainer = (vm, container) =>
+ _call('docker.restart', { vm: resolveId(vm), container })
+
+export const startContainer = (vm, container) =>
+ _call('docker.start', { vm: resolveId(vm), container })
+
+export const stopContainer = (vm, container) =>
+ _call('docker.stop', { vm: resolveId(vm), container })
+
+export const unpauseContainer = (vm, container) =>
+ _call('docker.unpause', { vm: resolveId(vm), container })
+
+// VM ----------------------------------------------------------------
+
+const chooseActionToUnblockForbiddenStartVm = props =>
+ chooseAction({
+ icon: 'alarm',
+ buttons: [
+ { label: _('cloneAndStartVM'), value: 'clone', btnStyle: 'success' },
+ { label: _('forceStartVm'), value: 'force', btnStyle: 'danger' },
+ ],
+ ...props,
+ })
+
+const cloneAndStartVM = async vm => _call('vm.start', { id: await cloneVm(vm) })
+
+export const startVm = vm =>
+ _call('vm.start', { id: resolveId(vm) }).catch(async reason => {
+ if (!forbiddenOperation.is(reason)) {
+ throw reason
+ }
+
+ const choice = await chooseActionToUnblockForbiddenStartVm({
+ body: _('blockedStartVmModalMessage'),
+ title: _('forceStartVmModalTitle'),
+ })
+
+ if (choice === 'clone') {
+ return cloneAndStartVM(vm)
+ }
+
+ return _call('vm.start', { id: resolveId(vm), force: true })
+ })
+
+export const startVms = vms =>
+ confirm({
+ title: _('startVmsModalTitle', { vms: vms.length }),
+ body: _('startVmsModalMessage', { vms: vms.length }),
+ }).then(async () => {
+ const forbiddenStart = []
+ let nErrors = 0
+
+ await Promise.all(
+ map(vms, id =>
+ _call('vm.start', { id }).catch(reason => {
+ if (forbiddenOperation.is(reason)) {
+ forbiddenStart.push(id)
+ } else {
+ nErrors++
+ }
+ })
+ )
+ )
+
+ if (forbiddenStart.length === 0) {
+ if (nErrors === 0) {
+ return
+ }
+
+ return error(
+ _('failedVmsErrorTitle'),
+ _('failedVmsErrorMessage', { nVms: nErrors })
+ )
+ }
+
+ const choice = await chooseActionToUnblockForbiddenStartVm({
+ body: _('blockedStartVmsModalMessage', { nVms: forbiddenStart.length }),
+ title: _('forceStartVmModalTitle'),
+ }).catch(noop)
+
+ if (nErrors !== 0) {
+ error(
+ _('failedVmsErrorTitle'),
+ _('failedVmsErrorMessage', { nVms: nErrors })
+ )
+ }
+
+ if (choice === 'clone') {
+ return Promise.all(
+ map(forbiddenStart, async id =>
+ cloneAndStartVM(getObject(store.getState(), id))
+ )
+ )
+ }
+
+ if (choice === 'force') {
+ return Promise.all(
+ map(forbiddenStart, id => _call('vm.start', { id, force: true }))
+ )
+ }
+ }, noop)
+
+export const stopVm = (vm, force = false) =>
+ confirm({
+ title: _('stopVmModalTitle'),
+ body: _('stopVmModalMessage', { name: vm.name_label }),
+ }).then(() => _call('vm.stop', { id: resolveId(vm), force }), noop)
+
+export const stopVms = (vms, force = false) =>
+ confirm({
+ title: _('stopVmsModalTitle', { vms: vms.length }),
+ body: _('stopVmsModalMessage', { vms: vms.length }),
+ }).then(
+ () => map(vms, vm => _call('vm.stop', { id: resolveId(vm), force })),
+ noop
+ )
+
+export const suspendVm = vm => _call('vm.suspend', { id: resolveId(vm) })
+
+export const suspendVms = vms =>
+ confirm({
+ title: _('suspendVmsModalTitle', { nVms: vms.length }),
+ body: _('suspendVmsModalMessage', { nVms: vms.length }),
+ }).then(
+ () =>
+ Promise.all(map(vms, vm => _call('vm.suspend', { id: resolveId(vm) }))),
+ noop
+ )
+
+export const resumeVm = vm => _call('vm.resume', { id: resolveId(vm) })
+
+export const recoveryStartVm = vm =>
+ _call('vm.recoveryStart', { id: resolveId(vm) })
+
+export const restartVm = (vm, force = false) =>
+ confirm({
+ title: _('restartVmModalTitle'),
+ body: _('restartVmModalMessage', { name: vm.name_label }),
+ }).then(() => _call('vm.restart', { id: resolveId(vm), force }), noop)
+
+export const restartVms = (vms, force = false) =>
+ confirm({
+ title: _('restartVmsModalTitle', { vms: vms.length }),
+ body: _('restartVmsModalMessage', { vms: vms.length }),
+ }).then(
+ () =>
+ Promise.all(
+ map(vms, vmId => _call('vm.restart', { id: resolveId(vmId), force }))
+ ),
+ noop
+ )
+
+export const cloneVm = ({ id, name_label: nameLabel }, fullCopy = false) =>
+ _call('vm.clone', {
+ id,
+ name: `${nameLabel}_clone`,
+ full_copy: fullCopy,
+ })
+
+import CopyVmModalBody from './copy-vm-modal' // eslint-disable-line import/first
+export const copyVm = (vm, sr, name, compress) => {
+ const vmId = resolveId(vm)
+ return sr !== undefined
+ ? confirm({
+ title: _('copyVm'),
+ body: _('copyVmConfirm', { SR: sr.name_label }),
+ }).then(() =>
+ _call('vm.copy', {
+ vm: vmId,
+ sr: sr.id,
+ name: name || vm.name_label + '_COPY',
+ compress,
+ })
+ )
+ : confirm({
+ title: _('copyVm'),
+ body: ,
+ }).then(params => {
+ if (!params.sr) {
+ error('copyVmsNoTargetSr', 'copyVmsNoTargetSrMessage')
+ return
+ }
+ return _call('vm.copy', { vm: vmId, ...params })
+ }, noop)
+}
+
+import CopyVmsModalBody from './copy-vms-modal' // eslint-disable-line import/first
+export const copyVms = vms => {
+ const _vms = resolveIds(vms)
+ return confirm({
+ title: _('copyVm'),
+ body: ,
+ }).then(({ compress, names, sr }) => {
+ if (sr !== undefined) {
+ return Promise.all(
+ map(_vms, (vm, index) =>
+ _call('vm.copy', { vm, sr, compress, name: names[index] })
+ )
+ )
+ }
+ error(_('copyVmsNoTargetSr'), _('copyVmsNoTargetSrMessage'))
+ }, noop)
+}
+
+export const convertVmToTemplate = vm =>
+ confirm({
+ title: 'Convert to template',
+ body: (
+
+
Are you sure you want to convert this VM into a template?
+
This operation is definitive.
+
+ ),
+ }).then(() => _call('vm.convert', { id: resolveId(vm) }), noop)
+
+export const deleteTemplates = templates =>
+ confirm({
+ title: _('templateDeleteModalTitle', { templates: templates.length }),
+ body: _('templateDeleteModalBody', { templates: templates.length }),
+ }).then(
+ () =>
+ Promise.all(map(resolveIds(templates), id => _call('vm.delete', { id }))),
+ noop
+ )
+
+export const snapshotVm = vm => _call('vm.snapshot', { id: resolveId(vm) })
+
+export const snapshotVms = vms =>
+ confirm({
+ title: _('snapshotVmsModalTitle', { vms: vms.length }),
+ body: _('snapshotVmsModalMessage', { vms: vms.length }),
+ }).then(() => map(vms, vmId => snapshotVm({ id: vmId })), noop)
+
+export const deleteSnapshot = vm =>
+ confirm({
+ title: _('deleteSnapshotModalTitle'),
+ body: _('deleteSnapshotModalMessage'),
+ }).then(() => _call('vm.delete', { id: resolveId(vm) }), noop)
+
+export const deleteSnapshots = vms =>
+ confirm({
+ title: _('deleteSnapshotsModalTitle', { nVms: vms.length }),
+ body: _('deleteSnapshotsModalMessage', { nVms: vms.length }),
+ }).then(
+ () =>
+ Promise.all(map(vms, vm => _call('vm.delete', { id: resolveId(vm) }))),
+ noop
+ )
+
+import MigrateVmModalBody from './migrate-vm-modal' // eslint-disable-line import/first
+export const migrateVm = (vm, host) =>
+ confirm({
+ title: _('migrateVmModalTitle'),
+ body: ,
+ }).then(params => {
+ if (!params.targetHost) {
+ return error(
+ _('migrateVmNoTargetHost'),
+ _('migrateVmNoTargetHostMessage')
+ )
+ }
+ _call('vm.migrate', { vm: vm.id, ...params })
+ }, noop)
+
+import MigrateVmsModalBody from './migrate-vms-modal' // eslint-disable-line import/first
+export const migrateVms = vms =>
+ confirm({
+ title: _('migrateVmModalTitle'),
+ body: ,
+ }).then(params => {
+ if (isEmpty(params.vms)) {
+ return
+ }
+ if (!params.targetHost) {
+ return error(
+ _('migrateVmNoTargetHost'),
+ _('migrateVmNoTargetHostMessage')
+ )
+ }
+
+ const {
+ mapVmsMapVdisSrs,
+ mapVmsMapVifsNetworks,
+ mapVmsMigrationNetwork,
+ targetHost,
+ vms,
+ } = params
+ Promise.all(
+ map(vms, ({ id }) =>
+ _call('vm.migrate', {
+ mapVdisSrs: mapVmsMapVdisSrs[id],
+ mapVifsNetworks: mapVmsMapVifsNetworks[id],
+ migrationNetwork: mapVmsMigrationNetwork[id],
+ targetHost,
+ vm: id,
+ })
+ )
+ )
+ }, noop)
+
+export const createVm = args => _call('vm.create', args)
+
+export const createVms = (args, nameLabels) =>
+ confirm({
+ title: _('newVmCreateVms'),
+ body: _('newVmCreateVmsConfirm', { nbVms: nameLabels.length }),
+ }).then(
+ () =>
+ Promise.all(
+ map(nameLabels, (
+ name_label // eslint-disable-line camelcase
+ ) => _call('vm.create', { ...args, name_label }))
+ ),
+ noop
+ )
+
+export const getCloudInitConfig = template =>
+ _call('vm.getCloudInitConfig', { template })
+
+export const deleteVm = (vm, retryWithForce = true) =>
+ confirm({
+ title: _('deleteVmModalTitle'),
+ body: _('deleteVmModalMessage'),
+ })
+ .then(() => _call('vm.delete', { id: resolveId(vm) }), noop)
+ .catch(error => {
+ if (forbiddenOperation.is(error) || !retryWithForce) {
+ throw error
+ }
+
+ return confirm({
+ title: _('deleteVmBlockedModalTitle'),
+ body: _('deleteVmBlockedModalMessage'),
+ }).then(
+ () => _call('vm.delete', { id: resolveId(vm), force: true }),
+ noop
+ )
+ })
+
+export const deleteVms = vms =>
+ confirm({
+ title: _('deleteVmsModalTitle', { vms: vms.length }),
+ body: _('deleteVmsModalMessage', { vms: vms.length }),
+ strongConfirm: vms.length > 1 && {
+ messageId: 'deleteVmsConfirmText',
+ values: { nVms: vms.length },
+ },
+ }).then(
+ () =>
+ Promise.all(
+ map(vms, vmId => _call('vm.delete', { id: resolveId(vmId) }))
+ ),
+ noop
+ )
+
+export const importBackup = ({ remote, file, sr }) =>
+ _call('vm.importBackup', resolveIds({ remote, file, sr }))
+
+export const importDeltaBackup = ({ remote, file, sr, mapVdisSrs }) =>
+ _call(
+ 'vm.importDeltaBackup',
+ resolveIds({
+ remote,
+ filePath: file,
+ sr,
+ mapVdisSrs: resolveIds(mapVdisSrs),
+ })
+ )
+
+import RevertSnapshotModalBody from './revert-snapshot-modal' // eslint-disable-line import/first
+export const revertSnapshot = vm =>
+ confirm({
+ title: _('revertVmModalTitle'),
+ body: ,
+ }).then(
+ snapshotBefore => _call('vm.revert', { id: resolveId(vm), snapshotBefore }),
+ noop
+ )
+
+export const editVm = (vm, props) =>
+ _call('vm.set', { ...props, id: resolveId(vm) })
+
+export const fetchVmStats = (vm, granularity) =>
+ _call('vm.stats', { id: resolveId(vm), granularity })
+
+export const importVm = (file, type = 'xva', data = undefined, sr) => {
+ const { name } = file
+
+ info(_('startVmImport'), name)
+
+ return _call('vm.import', { type, data, sr: resolveId(sr) }).then(
+ ({ $sendTo }) =>
+ post($sendTo, file)
+ .then(res => {
+ if (res.status !== 200) {
+ throw res.status
+ }
+ success(_('vmImportSuccess'), name)
+ })
+ .catch(() => {
+ error(_('vmImportFailed'), name)
+ })
+ )
+}
+
+export const importVms = (vms, sr) =>
+ Promise.all(
+ map(vms, ({ file, type, data }) =>
+ importVm(file, type, data, sr).catch(noop)
+ )
+ )
+
+export const exportVm = vm => {
+ info(_('startVmExport'), vm.id)
+ return _call('vm.export', { vm: resolveId(vm) }).then(({ $getFrom: url }) => {
+ window.location = `.${url}`
+ })
+}
+
+export const insertCd = (vm, cd, force = false) =>
+ _call('vm.insertCd', {
+ id: resolveId(vm),
+ cd_id: resolveId(cd),
+ force,
+ })
+
+export const ejectCd = vm => _call('vm.ejectCd', { id: resolveId(vm) })
+
+export const setVmBootOrder = (vm, order) =>
+ _call('vm.setBootOrder', {
+ vm: resolveId(vm),
+ order,
+ })
+
+export const attachDiskToVm = (vdi, vm, { bootable, mode, position }) =>
+ _call('vm.attachDisk', {
+ bootable,
+ mode,
+ position: (position && String(position)) || undefined,
+ vdi: resolveId(vdi),
+ vm: resolveId(vm),
+ })
+
+export const createVgpu = (vm, { gpuGroup, vgpuType }) =>
+ _call('vm.createVgpu', resolveIds({ vm, gpuGroup, vgpuType }))
+
+export const deleteVgpu = vgpu => _call('vm.deleteVgpu', resolveIds({ vgpu }))
+
+// DISK ---------------------------------------------------------------
+
+export const createDisk = (name, size, sr, { vm, bootable, mode, position }) =>
+ _call('disk.create', {
+ bootable,
+ mode,
+ name,
+ position,
+ size,
+ sr: resolveId(sr),
+ vm: resolveId(vm),
+ })
+
+// VDI ---------------------------------------------------------------
+
+export const editVdi = (vdi, props) =>
+ _call('vdi.set', { ...props, id: resolveId(vdi) })
+
+export const deleteVdi = vdi =>
+ confirm({
+ title: _('deleteVdiModalTitle'),
+ body: _('deleteVdiModalMessage'),
+ }).then(() => _call('vdi.delete', { id: resolveId(vdi) }), noop)
+
+export const deleteVdis = vdis =>
+ confirm({
+ title: _('deleteVdisModalTitle', { nVdis: vdis.length }),
+ body: _('deleteVdisModalMessage', { nVdis: vdis.length }),
+ }).then(
+ () =>
+ Promise.all(
+ map(vdis, vdi => _call('vdi.delete', { id: resolveId(vdi) }))
+ ),
+ noop
+ )
+
+export const deleteOrphanedVdis = vdis =>
+ confirm({
+ title: _('removeAllOrphanedObject'),
+ body: (
+
+
{_('removeAllOrphanedModalWarning')}
+
{_('definitiveMessageModal')}
+
+ ),
+ }).then(
+ () => Promise.all(map(resolveIds(vdis), id => _call('vdi.delete', { id }))),
+ noop
+ )
+
+export const migrateVdi = (vdi, sr) =>
+ _call('vdi.migrate', { id: resolveId(vdi), sr_id: resolveId(sr) })
+
+// VBD ---------------------------------------------------------------
+
+export const connectVbd = vbd => _call('vbd.connect', { id: resolveId(vbd) })
+
+export const disconnectVbd = vbd =>
+ _call('vbd.disconnect', { id: resolveId(vbd) })
+
+export const deleteVbd = vbd => _call('vbd.delete', { id: resolveId(vbd) })
+
+export const editVbd = (vbd, props) =>
+ _call('vbd.set', { ...props, id: resolveId(vbd) })
+
+export const setBootableVbd = (vbd, bootable) =>
+ _call('vbd.setBootable', { vbd: resolveId(vbd), bootable })
+
+// VIF ---------------------------------------------------------------
+
+export const createVmInterface = (vm, network, mac) =>
+ _call('vm.createInterface', resolveIds({ vm, network, mac }))
+
+export const connectVif = vif => _call('vif.connect', { id: resolveId(vif) })
+
+export const disconnectVif = vif =>
+ _call('vif.disconnect', { id: resolveId(vif) })
+
+export const deleteVif = vif => _call('vif.delete', { id: resolveId(vif) })
+
+export const deleteVifs = vifs =>
+ confirm({
+ title: _('deleteVifsModalTitle', { nVifs: vifs.length }),
+ body: _('deleteVifsModalMessage', { nVifs: vifs.length }),
+ }).then(
+ () => map(vifs, vif => _call('vif.delete', { id: resolveId(vif) })),
+ noop
+ )
+
+export const setVif = (
+ vif,
+ { network, mac, allowedIpv4Addresses, allowedIpv6Addresses }
+) =>
+ _call('vif.set', {
+ id: resolveId(vif),
+ network: resolveId(network),
+ mac,
+ allowedIpv4Addresses,
+ allowedIpv6Addresses,
+ })
+
+// Network -----------------------------------------------------------
+
+export const editNetwork = (network, props) =>
+ _call('network.set', { ...props, id: resolveId(network) })
+
+import CreateNetworkModalBody from './create-network-modal' // eslint-disable-line import/first
+export const createNetwork = container =>
+ confirm({
+ icon: 'network',
+ title: _('newNetworkCreate'),
+ body: ,
+ }).then(params => {
+ if (!params.name) {
+ return error(
+ _('newNetworkNoNameErrorTitle'),
+ _('newNetworkNoNameErrorMessage')
+ )
+ }
+ return _call('network.create', params)
+ }, noop)
+
+export const getBondModes = () => _call('network.getBondModes')
+
+import CreateBondedNetworkModalBody from './create-bonded-network-modal' // eslint-disable-line import/first
+export const createBondedNetwork = container =>
+ confirm({
+ icon: 'network',
+ title: _('newBondedNetworkCreate'),
+ body: ,
+ }).then(params => {
+ if (!params.name) {
+ return error(
+ _('newNetworkNoNameErrorTitle'),
+ _('newNetworkNoNameErrorMessage')
+ )
+ }
+ return _call('network.createBonded', params)
+ }, noop)
+
+export const deleteNetwork = network =>
+ confirm({
+ title: _('deleteNetwork'),
+ body: _('deleteNetworkConfirm'),
+ }).then(() => _call('network.delete', { network: resolveId(network) }), noop)
+
+// PIF ---------------------------------------------------------------
+
+export const connectPif = pif =>
+ confirm({
+ title: _('connectPif'),
+ body: _('connectPifConfirm'),
+ }).then(() => _call('pif.connect', { pif: resolveId(pif) }), noop)
+
+export const disconnectPif = pif =>
+ confirm({
+ title: _('disconnectPif'),
+ body: _('disconnectPifConfirm'),
+ }).then(() => _call('pif.disconnect', { pif: resolveId(pif) }), noop)
+
+export const deletePif = pif =>
+ confirm({
+ title: _('deletePif'),
+ body: _('deletePifConfirm'),
+ }).then(() => _call('pif.delete', { pif: resolveId(pif) }), noop)
+
+export const deletePifs = pifs =>
+ confirm({
+ title: _('deletePifs'),
+ body: _('deletePifsConfirm', { nPifs: pifs.length }),
+ }).then(
+ () =>
+ Promise.all(
+ map(pifs, pif => _call('pif.delete', { pif: resolveId(pif) }))
+ ),
+ noop
+ )
+
+export const reconfigurePifIp = (pif, { mode, ip, netmask, gateway, dns }) =>
+ _call('pif.reconfigureIp', {
+ pif: resolveId(pif),
+ mode,
+ ip,
+ netmask,
+ gateway,
+ dns,
+ })
+
+export const getIpv4ConfigModes = () => _call('pif.getIpv4ConfigurationModes')
+
+export const editPif = (pif, { vlan }) =>
+ _call('pif.editPif', { pif: resolveId(pif), vlan })
+
+// SR ----------------------------------------------------------------
+
+export const deleteSr = sr =>
+ confirm({
+ title: 'Delete SR',
+ body: (
+
+
Are you sure you want to remove this SR?
+
This operation is definitive, and ALL DISKS WILL BE LOST FOREVER.
+
+ ),
+ }).then(() => _call('sr.destroy', { id: resolveId(sr) }), noop)
+
+export const forgetSr = sr =>
+ confirm({
+ title: _('srForgetModalTitle'),
+ body: _('srForgetModalMessage'),
+ }).then(() => _call('sr.forget', { id: resolveId(sr) }), noop)
+export const forgetSrs = srs =>
+ confirm({
+ title: _('srsForgetModalTitle'),
+ body: _('srsForgetModalMessage'),
+ }).then(
+ () => Promise.all(map(resolveIds(srs), id => _call('sr.forget', { id }))),
+ noop
+ )
+
+export const reconnectAllHostsSr = sr =>
+ confirm({
+ title: _('srReconnectAllModalTitle'),
+ body: _('srReconnectAllModalMessage'),
+ }).then(() => _call('sr.connectAllPbds', { id: resolveId(sr) }), noop)
+export const reconnectAllHostsSrs = srs =>
+ confirm({
+ title: _('srReconnectAllModalTitle'),
+ body: _('srReconnectAllModalMessage'),
+ }).then(
+ () =>
+ Promise.all(resolveIds(srs), id => _call('sr.connectAllPbds', { id })),
+ noop
+ )
+
+export const disconnectAllHostsSr = sr =>
+ confirm({
+ title: _('srDisconnectAllModalTitle'),
+ body: _('srDisconnectAllModalMessage'),
+ }).then(() => _call('sr.disconnectAllPbds', { id: resolveId(sr) }), noop)
+export const disconnectAllHostsSrs = srs =>
+ confirm({
+ title: _('srDisconnectAllModalTitle'),
+ body: _('srsDisconnectAllModalMessage'),
+ }).then(
+ () =>
+ Promise.all(resolveIds(srs), id => _call('sr.disconnectAllPbds', { id })),
+ noop
+ )
+
+export const editSr = (sr, { nameDescription, nameLabel }) =>
+ _call('sr.set', {
+ id: resolveId(sr),
+ name_description: nameDescription,
+ name_label: nameLabel,
+ })
+
+export const rescanSr = sr => _call('sr.scan', { id: resolveId(sr) })
+export const rescanSrs = srs =>
+ Promise.all(map(resolveIds(srs), id => _call('sr.scan', { id })))
+
+// PBDs --------------------------------------------------------------
+
+export const connectPbd = pbd => _call('pbd.connect', { id: resolveId(pbd) })
+
+export const disconnectPbd = pbd =>
+ _call('pbd.disconnect', { id: resolveId(pbd) })
+
+export const deletePbd = pbd => _call('pbd.delete', { id: resolveId(pbd) })
+
+// Messages ----------------------------------------------------------
+
+export const deleteMessage = message =>
+ _call('message.delete', { id: resolveId(message) })
+
+export const deleteMessages = logs =>
+ confirm({
+ title: _('logDeleteMultiple', { nLogs: logs.length }),
+ body: _('logDeleteMultipleMessage', { nLogs: logs.length }),
+ }).then(() => Promise.all(map(logs, deleteMessage)), noop)
+
+// Tags --------------------------------------------------------------
+
+export const addTag = (object, tag) =>
+ _call('tag.add', { id: resolveId(object), tag })
+
+export const removeTag = (object, tag) =>
+ _call('tag.remove', { id: resolveId(object), tag })
+
+// Tasks --------------------------------------------------------------
+
+export const cancelTask = task => _call('task.cancel', { id: resolveId(task) })
+
+export const cancelTasks = tasks =>
+ confirm({
+ title: _('cancelTasksModalTitle', { nTasks: tasks.length }),
+ body: _('cancelTasksModalMessage', { nTasks: tasks.length }),
+ }).then(
+ () =>
+ Promise.all(
+ map(tasks, task => _call('task.cancel', { id: resolveId(task) }))
+ ),
+ noop
+ )
+
+export const destroyTask = task =>
+ _call('task.destroy', { id: resolveId(task) })
+
+export const destroyTasks = tasks =>
+ confirm({
+ title: _('destroyTasksModalTitle', { nTasks: tasks.length }),
+ body: _('destroyTasksModalMessage', { nTasks: tasks.length }),
+ }).then(
+ () =>
+ Promise.all(
+ map(tasks, task => _call('task.destroy', { id: resolveId(task) }))
+ ),
+ noop
+ )
+
+// Jobs -------------------------------------------------------------
+
+export const createJob = job =>
+ _call('job.create', { job })::tap(subscribeJobs.forceRefresh)
+
+export const deleteJob = job =>
+ _call('job.delete', { id: resolveId(job) })::tap(subscribeJobs.forceRefresh)
+
+export const editJob = job =>
+ _call('job.set', { job })::tap(subscribeJobs.forceRefresh)
+
+export const getJob = id => _call('job.get', { id })
+
+export const runJob = id => {
+ info(_('runJob'), _('runJobVerbose'))
+ return _call('job.runSequence', { idSequence: [id] })
+}
+
+// Backup/Schedule ---------------------------------------------------------
+
+export const createSchedule = (
+ jobId,
+ { cron, enabled, name = undefined, timezone = undefined }
+) =>
+ _call('schedule.create', { jobId, cron, enabled, name, timezone })::tap(
+ subscribeSchedules.forceRefresh
+ )
+
+export const deleteBackupSchedule = async schedule => {
+ await confirm({
+ title: _('deleteBackupSchedule'),
+ body: _('deleteBackupScheduleQuestion'),
+ })
+ await _call('schedule.delete', { id: schedule.id })
+ await _call('job.delete', { id: schedule.job })
+
+ subscribeSchedules.forceRefresh()
+ subscribeJobs.forceRefresh()
+}
+
+export const deleteSchedule = schedule =>
+ _call('schedule.delete', { id: resolveId(schedule) })::tap(
+ subscribeSchedules.forceRefresh
+ )
+
+export const deleteSchedules = schedules =>
+ confirm({
+ title: _('deleteSchedulesModalTitle', { nSchedules: schedules.length }),
+ body: _('deleteSchedulesModalMessage', { nSchedules: schedules.length }),
+ }).then(() =>
+ map(schedules, schedule =>
+ _call('schedule.delete', { id: resolveId(schedule) })::tap(
+ subscribeSchedules.forceRefresh
+ )
+ )
+ )
+
+export const disableSchedule = id =>
+ _call('scheduler.disable', { id })::tap(subscribeScheduleTable.forceRefresh)
+
+export const editSchedule = ({
+ id,
+ job: jobId,
+ cron,
+ enabled,
+ name,
+ timezone,
+}) =>
+ _call('schedule.set', { id, jobId, cron, enabled, name, timezone })::tap(
+ subscribeSchedules.forceRefresh
+ )
+
+export const enableSchedule = id =>
+ _call('scheduler.enable', { id })::tap(subscribeScheduleTable.forceRefresh)
+
+export const getSchedule = id => _call('schedule.get', { id })
+
+// Plugins -----------------------------------------------------------
+
+export const loadPlugin = async id =>
+ _call('plugin.load', { id })::tap(subscribePlugins.forceRefresh, err =>
+ error(_('pluginError'), (err && err.message) || _('unknownPluginError'))
+ )
+
+export const unloadPlugin = id =>
+ _call('plugin.unload', { id })::tap(subscribePlugins.forceRefresh, err =>
+ error(_('pluginError'), (err && err.message) || _('unknownPluginError'))
+ )
+
+export const enablePluginAutoload = id =>
+ _call('plugin.enableAutoload', { id })::tap(subscribePlugins.forceRefresh)
+
+export const disablePluginAutoload = id =>
+ _call('plugin.disableAutoload', { id })::tap(subscribePlugins.forceRefresh)
+
+export const configurePlugin = (id, configuration) =>
+ _call('plugin.configure', { id, configuration })::tap(
+ () => {
+ info(_('pluginConfigurationSuccess'), _('pluginConfigurationChanges'))
+ subscribePlugins.forceRefresh()
+ },
+ err =>
+ error(
+ _('pluginError'),
+ JSON.stringify(err.data) || _('unknownPluginError')
+ )
+ )
+
+export const purgePluginConfiguration = async id => {
+ await confirm({
+ title: _('purgePluginConfiguration'),
+ body: _('purgePluginConfigurationQuestion'),
+ })
+ await _call('plugin.purgeConfiguration', { id })
+
+ subscribePlugins.forceRefresh()
+}
+
+export const testPlugin = async (id, data) => _call('plugin.test', { id, data })
+
+// Resource set ------------------------------------------------------
+
+export const createResourceSet = (name, { subjects, objects, limits } = {}) =>
+ _call('resourceSet.create', { name, subjects, objects, limits })::tap(
+ subscribeResourceSets.forceRefresh
+ )
+
+export const editResourceSet = (
+ id,
+ { name, subjects, objects, limits, ipPools } = {}
+) =>
+ _call('resourceSet.set', {
+ id,
+ name,
+ subjects,
+ objects,
+ limits,
+ ipPools,
+ })::tap(subscribeResourceSets.forceRefresh)
+
+export const deleteResourceSet = async id => {
+ await confirm({
+ title: _('deleteResourceSetWarning'),
+ body: _('deleteResourceSetQuestion'),
+ })
+ await _call('resourceSet.delete', { id: resolveId(id) })
+
+ subscribeResourceSets.forceRefresh()
+}
+
+export const recomputeResourceSetsLimits = () =>
+ _call('resourceSet.recomputeAllLimits')
+
+// Remote ------------------------------------------------------------
+
+export const getRemote = remote =>
+ _call('remote.get', resolveIds({ id: remote }))::tap(null, err =>
+ error(_('getRemote'), err.message || String(err))
+ )
+
+export const createRemote = (name, url) =>
+ _call('remote.create', { name, url })::tap(subscribeRemotes.forceRefresh)
+
+export const deleteRemote = remote =>
+ _call('remote.delete', { id: resolveId(remote) })::tap(
+ subscribeRemotes.forceRefresh
+ )
+
+export const deleteRemotes = remotes =>
+ confirm({
+ title: _('deleteRemotesModalTitle', { nRemotes: remotes.length }),
+ body: _('deleteRemotesModalMessage', { nRemotes: remotes.length }),
+ }).then(
+ () =>
+ Promise.all(
+ map(remotes, remote =>
+ _call('remote.delete', { id: resolveId(remote) })
+ )
+ )::tap(subscribeRemotes.forceRefresh),
+ noop
+ )
+
+export const enableRemote = remote =>
+ _call('remote.set', { id: resolveId(remote), enabled: true })::tap(
+ subscribeRemotes.forceRefresh
+ )
+
+export const disableRemote = remote =>
+ _call('remote.set', { id: resolveId(remote), enabled: false })::tap(
+ subscribeRemotes.forceRefresh
+ )
+
+export const editRemote = (remote, { name, url }) =>
+ _call('remote.set', resolveIds({ remote, name, url }))::tap(
+ subscribeRemotes.forceRefresh
+ )
+
+export const listRemote = remote =>
+ _call('remote.list', resolveIds({ id: remote }))::tap(
+ subscribeRemotes.forceRefresh,
+ err => error(_('listRemote'), err.message || String(err))
+ )
+
+export const listRemoteBackups = remote =>
+ _call('backup.list', resolveIds({ remote }))::tap(null, err =>
+ error(_('listRemote'), err.message || String(err))
+ )
+
+export const testRemote = remote =>
+ _call('remote.test', resolveIds({ id: remote }))::tap(null, err =>
+ error(_('testRemote'), err.message || String(err))
+ )
+
+// File restore ----------------------------------------------------
+
+export const scanDisk = (remote, disk) =>
+ _call('backup.scanDisk', resolveIds({ remote, disk }))
+
+export const scanFiles = (remote, disk, path, partition) =>
+ _call('backup.scanFiles', resolveIds({ remote, disk, path, partition }))
+
+export const fetchFiles = (remote, disk, partition, paths, format) =>
+ _call(
+ 'backup.fetchFiles',
+ resolveIds({ remote, disk, partition, paths, format })
+ ).then(({ $getFrom: url }) => {
+ window.location = `.${url}`
+ })
+
+// -------------------------------------------------------------------
+
+export const probeSrNfs = (host, server) =>
+ _call('sr.probeNfs', { host, server })
+
+export const probeSrNfsExists = (host, server, serverPath) =>
+ _call('sr.probeNfsExists', { host, server, serverPath })
+
+export const probeSrIscsiIqns = (
+ host,
+ target,
+ port = undefined,
+ chapUser = undefined,
+ chapPassword
+) => {
+ const params = { host, target }
+ port && (params.port = port)
+ chapUser && (params.chapUser = chapUser)
+ chapPassword && (params.chapPassword = chapPassword)
+ return _call('sr.probeIscsiIqns', params)
+}
+
+export const probeSrIscsiLuns = (
+ host,
+ target,
+ targetIqn,
+ chapUser = undefined,
+ chapPassword
+) => {
+ const params = { host, target, targetIqn }
+ chapUser && (params.chapUser = chapUser)
+ chapPassword && (params.chapPassword = chapPassword)
+ return _call('sr.probeIscsiLuns', params)
+}
+
+export const probeSrIscsiExists = (
+ host,
+ target,
+ targetIqn,
+ scsiId,
+ port = undefined,
+ chapUser = undefined,
+ chapPassword
+) => {
+ const params = { host, target, targetIqn, scsiId }
+ port && (params.port = port)
+ chapUser && (params.chapUser = chapUser)
+ chapPassword && (params.chapPassword = chapPassword)
+ return _call('sr.probeIscsiExists', params)
+}
+
+export const reattachSr = (host, uuid, nameLabel, nameDescription, type) =>
+ _call('sr.reattach', { host, uuid, nameLabel, nameDescription, type })
+
+export const reattachSrIso = (host, uuid, nameLabel, nameDescription, type) =>
+ _call('sr.reattachIso', { host, uuid, nameLabel, nameDescription, type })
+
+export const createSrNfs = (
+ host,
+ nameLabel,
+ nameDescription,
+ server,
+ serverPath,
+ nfsVersion = undefined
+) => {
+ const params = { host, nameLabel, nameDescription, server, serverPath }
+ nfsVersion && (params.nfsVersion = nfsVersion)
+ return _call('sr.createNfs', params)
+}
+
+export const createSrIscsi = (
+ host,
+ nameLabel,
+ nameDescription,
+ target,
+ targetIqn,
+ scsiId,
+ port = undefined,
+ chapUser = undefined,
+ chapPassword = undefined
+) => {
+ const params = { host, nameLabel, nameDescription, target, targetIqn, scsiId }
+ port && (params.port = port)
+ chapUser && (params.chapUser = chapUser)
+ chapPassword && (params.chapPassword = chapPassword)
+ return _call('sr.createIscsi', params)
+}
+
+export const createSrIso = (
+ host,
+ nameLabel,
+ nameDescription,
+ path,
+ type,
+ user = undefined,
+ password = undefined
+) => {
+ const params = { host, nameLabel, nameDescription, path, type }
+ user && (params.user = user)
+ password && (params.password = password)
+ return _call('sr.createIso', params)
+}
+
+export const createSrLvm = (host, nameLabel, nameDescription, device) =>
+ _call('sr.createLvm', { host, nameLabel, nameDescription, device })
+
+// Job logs ----------------------------------------------------------
+
+export const deleteJobsLog = id =>
+ _call('log.delete', { namespace: 'jobs', id })::tap(
+ subscribeJobsLogs.forceRefresh
+ )
+
+// Logs
+
+export const deleteApiLog = id =>
+ _call('log.delete', { namespace: 'api', id })::tap(
+ subscribeApiLogs.forceRefresh
+ )
+
+// Acls, users, groups ----------------------------------------------------------
+
+export const addAcl = ({ subject, object, action }) =>
+ _call('acl.add', resolveIds({ subject, object, action }))::tap(
+ subscribeAcls.forceRefresh,
+ err => error('Add ACL', err.message || String(err))
+ )
+
+export const removeAcl = ({ subject, object, action }) =>
+ _call('acl.remove', resolveIds({ subject, object, action }))::tap(
+ subscribeAcls.forceRefresh,
+ err => error('Remove ACL', err.message || String(err))
+ )
+
+export const editAcl = (
+ { subject, object, action },
+ {
+ subject: newSubject = subject,
+ object: newObject = object,
+ action: newAction = action,
+ }
+) =>
+ _call('acl.remove', resolveIds({ subject, object, action }))
+ .then(() =>
+ _call(
+ 'acl.add',
+ resolveIds({
+ subject: newSubject,
+ object: newObject,
+ action: newAction,
+ })
+ )
+ )
+ ::tap(subscribeAcls.forceRefresh, err =>
+ error('Edit ACL', err.message || String(err))
+ )
+
+export const createGroup = name =>
+ _call('group.create', { name })::tap(subscribeGroups.forceRefresh, err =>
+ error(_('createGroup'), err.message || String(err))
+ )
+
+export const setGroupName = (group, name) =>
+ _call('group.set', resolveIds({ group, name }))::tap(
+ subscribeGroups.forceRefresh
+ )
+
+export const deleteGroup = group =>
+ confirm({
+ title: _('deleteGroup'),
+ body: {_('deleteGroupConfirm')}
,
+ }).then(
+ () =>
+ _call('group.delete', resolveIds({ id: group }))::tap(
+ subscribeGroups.forceRefresh,
+ err => error(_('deleteGroup'), err.message || String(err))
+ ),
+ noop
+ )
+
+export const removeUserFromGroup = (user, group) =>
+ _call('group.removeUser', resolveIds({ id: group, userId: user }))::tap(
+ subscribeGroups.forceRefresh,
+ err => error(_('removeUserFromGroup'), err.message || String(err))
+ )
+
+export const addUserToGroup = (user, group) =>
+ _call('group.addUser', resolveIds({ id: group, userId: user }))::tap(
+ subscribeGroups.forceRefresh,
+ err => error('Add User', err.message || String(err))
+ )
+
+export const createUser = (email, password, permission) =>
+ _call('user.create', { email, password, permission })::tap(
+ subscribeUsers.forceRefresh,
+ err => error('Create user', err.message || String(err))
+ )
+
+export const deleteUser = user =>
+ confirm({
+ title: _('deleteUser'),
+ body: {_('deleteUserConfirm')}
,
+ }).then(() =>
+ _call('user.delete', { id: resolveId(user) })::tap(
+ subscribeUsers.forceRefresh,
+ err => error(_('deleteUser'), err.message || String(err))
+ )
+ )
+
+export const editUser = (user, { email, password, permission }) =>
+ _call('user.set', { id: resolveId(user), email, password, permission })::tap(
+ subscribeUsers.forceRefresh
+ )
+
+export const changePassword = (oldPassword, newPassword) =>
+ _call('user.changePassword', {
+ oldPassword,
+ newPassword,
+ }).then(
+ () => success(_('pwdChangeSuccess'), _('pwdChangeSuccessBody')),
+ () => error(_('pwdChangeError'), _('pwdChangeErrorBody'))
+ )
+
+const _setUserPreferences = preferences =>
+ _call('user.set', {
+ id: xo.user.id,
+ preferences,
+ })::tap(subscribeCurrentUser.forceRefresh)
+
+import NewSshKeyModalBody from './new-ssh-key-modal' // eslint-disable-line import/first
+export const addSshKey = key => {
+ const { preferences } = xo.user
+ const otherKeys = (preferences && preferences.sshKeys) || []
+ if (key) {
+ return _setUserPreferences({
+ sshKeys: [...otherKeys, key],
+ })
+ }
+ return confirm({
+ icon: 'ssh-key',
+ title: _('newSshKeyModalTitle'),
+ body: ,
+ }).then(newKey => {
+ if (!newKey.title || !newKey.key) {
+ error(_('sshKeyErrorTitle'), _('sshKeyErrorMessage'))
+ return
+ }
+ return _setUserPreferences({
+ sshKeys: [...otherKeys, newKey],
+ })
+ }, noop)
+}
+
+export const deleteSshKey = key =>
+ confirm({
+ title: _('deleteSshKeyConfirm'),
+ body: _('deleteSshKeyConfirmMessage', {
+ title: {key.title} ,
+ }),
+ }).then(() => {
+ const { preferences } = xo.user
+ return _setUserPreferences({
+ sshKeys: filter(
+ preferences && preferences.sshKeys,
+ k => k.key !== resolveId(key)
+ ),
+ })
+ }, noop)
+
+export const deleteSshKeys = keys =>
+ confirm({
+ title: _('deleteSshKeysConfirm', { nKeys: keys.length }),
+ body: _('deleteSshKeysConfirmMessage', {
+ nKeys: keys.length,
+ }),
+ }).then(() => {
+ const { preferences } = xo.user
+ const keyIds = resolveIds(keys)
+ return _setUserPreferences({
+ sshKeys: filter(
+ preferences && preferences.sshKeys,
+ sshKey => !includes(keyIds, sshKey.key)
+ ),
+ })
+ }, noop)
+
+// User filters --------------------------------------------------
+
+import AddUserFilterModalBody from './add-user-filter-modal' // eslint-disable-line import/first
+export const addCustomFilter = (type, value) => {
+ const { user } = xo
+ return confirm({
+ title: _('saveNewFilterTitle'),
+ body: ,
+ }).then(name => {
+ if (name.length === 0) {
+ return error(
+ _('saveNewUserFilterErrorTitle'),
+ _('saveNewUserFilterErrorBody')
+ )
+ }
+
+ const { preferences } = user
+ const filters = (preferences && preferences.filters) || {}
+
+ return _setUserPreferences({
+ filters: {
+ ...filters,
+ [type]: {
+ ...filters[type],
+ [name]: value,
+ },
+ },
+ })
+ })
+}
+
+export const removeCustomFilter = (type, name) =>
+ confirm({
+ title: _('removeUserFilterTitle'),
+ body: {_('removeUserFilterBody')}
,
+ }).then(() => {
+ const { user } = xo
+ const { filters } = user.preferences
+
+ return _setUserPreferences({
+ filters: {
+ ...filters,
+ [type]: {
+ ...filters[type],
+ [name]: undefined,
+ },
+ },
+ })
+ })
+
+export const editCustomFilter = (type, name, { newName = name, newValue }) => {
+ const { filters } = xo.user.preferences
+ return _setUserPreferences({
+ filters: {
+ ...filters,
+ [type]: {
+ ...filters[type],
+ [name]: undefined,
+ [newName]: newValue || filters[type][name],
+ },
+ },
+ })
+}
+
+export const setDefaultHomeFilter = (type, name) => {
+ const { user } = xo
+ const { preferences } = user
+ const defaultFilters = (preferences && preferences.defaultHomeFilters) || {}
+
+ return _setUserPreferences({
+ defaultHomeFilters: {
+ ...defaultFilters,
+ [type]: name,
+ },
+ })
+}
+
+// IP pools --------------------------------------------------------------------
+
+export const createIpPool = ({ name, ips, networks }) => {
+ const addresses = {}
+ forEach(ips, ip => {
+ addresses[ip] = {}
+ })
+ return _call('ipPool.create', {
+ name,
+ addresses,
+ networks: resolveIds(networks),
+ })::tap(subscribeIpPools.forceRefresh)
+}
+
+export const deleteIpPool = ipPool =>
+ _call('ipPool.delete', { id: resolveId(ipPool) })::tap(
+ subscribeIpPools.forceRefresh
+ )
+
+export const setIpPool = (ipPool, { name, addresses, networks }) =>
+ _call('ipPool.set', {
+ id: resolveId(ipPool),
+ name,
+ addresses,
+ networks: resolveIds(networks),
+ })::tap(subscribeIpPools.forceRefresh)
+
+// XO SAN ----------------------------------------------------------------------
+
+export const getVolumeInfo = (xosanSr, infoType) =>
+ _call('xosan.getVolumeInfo', { sr: xosanSr, infoType })
+
+export const createXosanSR = ({
+ template,
+ pif,
+ vlan,
+ srs,
+ glusterType,
+ redundancy,
+ brickSize,
+ memorySize,
+ ipRange,
+}) => {
+ const promise = _call('xosan.createSR', {
+ template,
+ pif: pif.id,
+ vlan: String(vlan),
+ srs: resolveIds(srs),
+ glusterType,
+ redundancy: Number.parseInt(redundancy),
+ brickSize,
+ memorySize,
+ ipRange,
+ })
+
+ // Force refresh in parallel to get the creation progress sooner
+ subscribeCheckSrCurrentState.forceRefresh()
+
+ return promise
+}
+
+export const addXosanBricks = (xosansr, lvmsrs, brickSize) =>
+ _call('xosan.addBricks', { xosansr, lvmsrs, brickSize })
+
+export const replaceXosanBrick = (
+ xosansr,
+ previousBrick,
+ newLvmSr,
+ brickSize,
+ onSameVM = false
+) =>
+ _call(
+ 'xosan.replaceBrick',
+ resolveIds({ xosansr, previousBrick, newLvmSr, brickSize, onSameVM })
+ )
+
+export const removeXosanBricks = (xosansr, bricks) =>
+ _call('xosan.removeBricks', { xosansr, bricks })
+
+export const computeXosanPossibleOptions = (lvmSrs, brickSize) =>
+ _call('xosan.computeXosanPossibleOptions', { lvmSrs, brickSize })
+
+import InstallXosanPackModal from './install-xosan-pack-modal' // eslint-disable-line import/first
+export const downloadAndInstallXosanPack = pool =>
+ confirm({
+ title: _('xosanInstallPackTitle', { pool: pool.name_label }),
+ icon: 'export',
+ body: ,
+ }).then(pack =>
+ _call('xosan.downloadAndInstallXosanPack', {
+ id: pack.id,
+ version: pack.version,
+ pool: resolveId(pool),
+ })
+ )
+
+export const fixHostNotInXosanNetwork = (xosanSr, host) =>
+ _call('xosan.fixHostNotInNetwork', { xosanSr, host })
+
+// Licenses --------------------------------------------------------------------
+
+export const getLicenses = productId => _call('xoa.getLicenses', { productId })
+
+export const getLicense = (productId, boundObjectId) =>
+ _call('xoa.getLicense', { productId, boundObjectId })
+
+export const unlockXosan = (licenseId, srId) =>
+ _call('xosan.unlock', { licenseId, sr: srId })
diff --git a/packages/xo-web/src/common/xo/install-xosan-pack-modal/index.js b/packages/xo-web/src/common/xo/install-xosan-pack-modal/index.js
new file mode 100644
index 000000000..c5c1c7202
--- /dev/null
+++ b/packages/xo-web/src/common/xo/install-xosan-pack-modal/index.js
@@ -0,0 +1,130 @@
+import _ from 'intl'
+import Component from 'base-component'
+import React from 'react'
+import { connectStore, compareVersions, isXosanPack } from 'utils'
+import { subscribeResourceCatalog, subscribePlugins } from 'xo'
+import {
+ createGetObjectsOfType,
+ createSelector,
+ createCollectionWrapper,
+} from 'selectors'
+import { satisfies as versionSatisfies } from 'semver'
+import { every, filter, forEach, map, some } from 'lodash'
+
+const findLatestPack = (packs, hostsVersions) => {
+ const checkVersion = version =>
+ every(hostsVersions, hostVersion => versionSatisfies(hostVersion, version))
+
+ let latestPack = { version: '0' }
+ forEach(packs, pack => {
+ const xsVersionRequirement =
+ pack.requirements && pack.requirements.xenserver
+
+ if (
+ pack.type === 'iso' &&
+ compareVersions(pack.version, latestPack.version) > 0 &&
+ (!xsVersionRequirement || checkVersion(xsVersionRequirement))
+ ) {
+ latestPack = pack
+ }
+ })
+
+ if (latestPack.version === '0') {
+ // No compatible pack was found
+ return
+ }
+
+ return latestPack
+}
+
+@connectStore(
+ () => ({
+ hosts: createGetObjectsOfType('host').filter(
+ createSelector(
+ (_, { pool }) => pool != null && pool.id,
+ poolId =>
+ poolId
+ ? host =>
+ host.$pool === poolId &&
+ !some(host.supplementalPacks, isXosanPack)
+ : false
+ )
+ ),
+ }),
+ { withRef: true }
+)
+export default class InstallXosanPackModal extends Component {
+ componentDidMount () {
+ this._unsubscribePlugins = subscribePlugins(plugins =>
+ this.setState({ plugins })
+ )
+ this._unsubscribeResourceCatalog = subscribeResourceCatalog(catalog =>
+ this.setState({ catalog })
+ )
+ }
+
+ componentWillUnmount () {
+ this._unsubscribePlugins()
+ this._unsubscribeResourceCatalog()
+ }
+
+ _getXosanLatestPack = createSelector(
+ () => this.state.catalog && this.state.catalog.xosan,
+ createSelector(
+ () => this.props.hosts,
+ createCollectionWrapper(hosts => map(hosts, 'version'))
+ ),
+ findLatestPack
+ )
+
+ _getXosanPacks = createSelector(
+ () => this.state.catalog && this.state.catalog.xosan,
+ packs => filter(packs, ({ type }) => type === 'iso')
+ )
+
+ get value () {
+ return this._getXosanLatestPack()
+ }
+
+ render () {
+ const { hosts } = this.props
+ const latestPack = this._getXosanLatestPack()
+
+ return (
+
+ {latestPack ? (
+
+ {_('xosanInstallPackOnHosts')}
+
+ {map(hosts, host => {host.name_label} )}
+
+
+ {_('xosanInstallPack', {
+ pack: latestPack.name,
+ version: latestPack.version,
+ })}
+
+
+ ) : (
+
+ {_('xosanNoPackFound')}
+
+ {_('xosanPackRequirements')}
+
+ {map(this._getXosanPacks(), ({ name, requirements }, key) => (
+
+ {_.keyValue(
+ name,
+ requirements && requirements.xenserver
+ ? requirements.xenserver
+ : '/'
+ )}
+
+ ))}
+
+
+ )}
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/common/xo/migrate-vm-modal/index.css b/packages/xo-web/src/common/xo/migrate-vm-modal/index.css
new file mode 100644
index 000000000..013f70502
--- /dev/null
+++ b/packages/xo-web/src/common/xo/migrate-vm-modal/index.css
@@ -0,0 +1,18 @@
+.listTitle {
+ font-weight: bold;
+ font-size: 1.1em
+}
+
+.listItem {
+ margin-top: 2px;
+}
+
+.block {
+ padding-bottom: 1em;
+}
+
+.groupBlock {
+ padding-bottom: 1em;
+ padding-top: 1em;
+ border-top: 1px solid #e5e5e5;
+}
diff --git a/packages/xo-web/src/common/xo/migrate-vm-modal/index.js b/packages/xo-web/src/common/xo/migrate-vm-modal/index.js
new file mode 100644
index 000000000..0ae9d17c0
--- /dev/null
+++ b/packages/xo-web/src/common/xo/migrate-vm-modal/index.js
@@ -0,0 +1,315 @@
+import BaseComponent from 'base-component'
+import every from 'lodash/every'
+import find from 'lodash/find'
+import forEach from 'lodash/forEach'
+import map from 'lodash/map'
+import React from 'react'
+import store from 'store'
+
+import _ from '../../intl'
+import ChooseSrForEachVdisModal from '../choose-sr-for-each-vdis-modal'
+import invoke from '../../invoke'
+import SingleLineRow from '../../single-line-row'
+import { Col } from '../../grid'
+import { connectStore, mapPlus, resolveId, resolveIds } from '../../utils'
+import { getDefaultNetworkForVif } from '../utils'
+import { SelectHost, SelectNetwork } from '../../select-objects'
+import {
+ createGetObjectsOfType,
+ createPicker,
+ createSelector,
+ getObject,
+} from '../../selectors'
+
+import { isSrShared, isSrWritable } from '../'
+
+import styles from './index.css'
+
+@connectStore(
+ () => {
+ const getVm = (_, props) => props.vm
+
+ const getVbds = createGetObjectsOfType('VBD')
+ .pick((state, props) => getVm(state, props).$VBDs)
+ .sort()
+
+ const getVdis = createGetObjectsOfType('VDI').pick(
+ createSelector(getVbds, vbds =>
+ mapPlus(vbds, (vbd, push) => {
+ if (!vbd.is_cd_drive && vbd.VDI) {
+ push(vbd.VDI)
+ }
+ })
+ )
+ )
+
+ const getVifs = createGetObjectsOfType('VIF')
+ .pick(createSelector(getVm, vm => vm.VIFs))
+ .sort()
+
+ const getPifs = createGetObjectsOfType('PIF')
+ const getNetworks = createGetObjectsOfType('network')
+ const getPools = createGetObjectsOfType('pool')
+
+ return {
+ networks: getNetworks,
+ pifs: getPifs,
+ pools: getPools,
+ vbds: getVbds,
+ vdis: getVdis,
+ vifs: getVifs,
+ }
+ },
+ { withRef: true }
+)
+export default class MigrateVmModalBody extends BaseComponent {
+ constructor (props) {
+ super(props)
+
+ this.state = {
+ mapVifsNetworks: {},
+ targetSrs: {},
+ }
+
+ this._getHostPredicate = createSelector(
+ () => this.props.vm,
+ ({ $container }) => host => host.id !== $container
+ )
+
+ this._getSrPredicate = createSelector(
+ () => this.state.host,
+ host =>
+ host
+ ? sr =>
+ isSrWritable(sr) &&
+ (sr.$container === host.id || sr.$container === host.$pool)
+ : false
+ )
+
+ this._getTargetNetworkPredicate = createSelector(
+ createPicker(() => this.props.pifs, () => this.state.host.$PIFs),
+ pifs => {
+ if (!pifs) {
+ return false
+ }
+
+ const networks = {}
+ forEach(pifs, pif => {
+ networks[pif.$network] = true
+ })
+
+ return network => networks[network.id]
+ }
+ )
+
+ this._getMigrationNetworkPredicate = createSelector(
+ createPicker(() => this.props.pifs, () => this.state.host.$PIFs),
+ pifs => {
+ if (!pifs) {
+ return false
+ }
+
+ const networks = {}
+ forEach(pifs, pif => {
+ pif.ip && (networks[pif.$network] = true)
+ })
+
+ return network => networks[network.id]
+ }
+ )
+ }
+
+ componentDidMount () {
+ this._selectHost(this.props.host)
+ }
+
+ get value () {
+ return {
+ mapVdisSrs: resolveIds(this.state.targetSrs.mapVdisSrs),
+ mapVifsNetworks: this.state.mapVifsNetworks,
+ migrationNetwork: this.state.migrationNetworkId,
+ sr: resolveId(this.state.targetSrs.mainSr),
+ targetHost: this.state.host && this.state.host.id,
+ }
+ }
+
+ _getObject (id) {
+ return getObject(store.getState(), id)
+ }
+
+ _selectHost = host => {
+ // No host selected
+ if (!host) {
+ this.setState({
+ host: undefined,
+ intraPool: undefined,
+ })
+ return
+ }
+
+ const { vbds, vm } = this.props
+ const intraPool = vm.$pool === host.$pool
+
+ // Intra-pool
+ if (intraPool) {
+ let doNotMigrateVdis
+ if (vm.$container === host.id) {
+ doNotMigrateVdis = true
+ } else {
+ const _doNotMigrateVdi = {}
+ forEach(vbds, vbd => {
+ if (vbd.VDI != null) {
+ _doNotMigrateVdi[vbd.VDI] = isSrShared(
+ this._getObject(this._getObject(vbd.VDI).$SR)
+ )
+ }
+ })
+ doNotMigrateVdis = every(_doNotMigrateVdi)
+ }
+
+ this.setState({
+ doNotMigrateVdis,
+ host,
+ intraPool,
+ mapVifsNetworks: undefined,
+ migrationNetwork: undefined,
+ targetSrs: {},
+ })
+ return
+ }
+
+ // Inter-pool
+ const { networks, pifs, vifs } = this.props
+ const defaultMigrationNetworkId = find(
+ pifs,
+ pif => pif.$host === host.id && pif.management
+ ).$network
+
+ const defaultNetwork = invoke(() => {
+ // First PIF with an IP.
+ const pifId = find(host.$PIFs, pif => pifs[pif].ip)
+ const pif = pifId && pifs[pifId]
+
+ return pif && pif.$network
+ })
+
+ const defaultNetworksForVif = {}
+ forEach(vifs, vif => {
+ defaultNetworksForVif[vif.id] =
+ getDefaultNetworkForVif(vif, host, pifs, networks) || defaultNetwork
+ })
+
+ this.setState({
+ doNotMigrateVdis: false,
+ host,
+ intraPool,
+ mapVifsNetworks: defaultNetworksForVif,
+ migrationNetworkId: defaultMigrationNetworkId,
+ targetSrs: {},
+ })
+ }
+
+ _selectMigrationNetwork = migrationNetwork =>
+ this.setState({ migrationNetworkId: migrationNetwork.id })
+
+ render () {
+ const { vdis, vifs, networks } = this.props
+ const {
+ doNotMigrateVdis,
+ host,
+ intraPool,
+ mapVifsNetworks,
+ migrationNetworkId,
+ targetSrs,
+ } = this.state
+ return (
+
+
+
+ {_('migrateVmSelectHost')}
+
+
+
+
+
+ {host &&
+ !doNotMigrateVdis && (
+
+
+
+
+
+
+
+ )}
+ {intraPool !== undefined &&
+ (!intraPool && (
+
+
+
+ {_('migrateVmSelectMigrationNetwork')}
+
+
+
+
+
+
+
+ {_('migrateVmSelectNetworks')}
+
+
+
+
+
+ {_('migrateVmVif')}
+
+
+
+
+ {_('migrateVmNetwork')}
+
+
+
+ {map(vifs, vif => (
+
+
+
+ {vif.MAC} ({networks[vif.$network].name_label})
+
+
+
+ this.setState({
+ mapVifsNetworks: {
+ ...mapVifsNetworks,
+ [vif.id]: network.id,
+ },
+ })
+ }
+ predicate={this._getTargetNetworkPredicate()}
+ value={mapVifsNetworks[vif.id]}
+ />
+
+
+
+ ))}
+
+
+ ))}
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/common/xo/migrate-vms-modal/index.js b/packages/xo-web/src/common/xo/migrate-vms-modal/index.js
new file mode 100644
index 000000000..7f5117265
--- /dev/null
+++ b/packages/xo-web/src/common/xo/migrate-vms-modal/index.js
@@ -0,0 +1,357 @@
+import BaseComponent from 'base-component'
+import every from 'lodash/every'
+import flatten from 'lodash/flatten'
+import forEach from 'lodash/forEach'
+import filter from 'lodash/filter'
+import find from 'lodash/find'
+import isEmpty from 'lodash/isEmpty'
+import map from 'lodash/map'
+import mapValues from 'lodash/mapValues'
+import React from 'react'
+import some from 'lodash/some'
+import store from 'store'
+
+import _ from '../../intl'
+import Icon from 'icon'
+import invoke from '../../invoke'
+import SingleLineRow from '../../single-line-row'
+import Tooltip from '../../tooltip'
+import { Col } from '../../grid'
+import { getDefaultNetworkForVif } from '../utils'
+import { SelectHost, SelectNetwork, SelectSr } from '../../select-objects'
+import { connectStore } from '../../utils'
+import {
+ createGetObjectsOfType,
+ createPicker,
+ createSelector,
+ getObject,
+} from '../../selectors'
+import { isSrShared } from 'xo'
+
+import { isSrWritable } from '../'
+
+const LINE_STYLE = { paddingBottom: '1em' }
+
+@connectStore(
+ () => {
+ const getNetworks = createGetObjectsOfType('network')
+ const getPifs = createGetObjectsOfType('PIF')
+ const getPools = createGetObjectsOfType('pool')
+
+ const getVms = createGetObjectsOfType('VM').pick((_, props) => props.vms)
+
+ const getVbdsByVm = createGetObjectsOfType('VBD')
+ .pick(createSelector(getVms, vms => flatten(map(vms, vm => vm.$VBDs))))
+ .groupBy('VM')
+
+ const getVifsByVM = createGetObjectsOfType('VIF')
+ .pick(createSelector(getVms, vms => flatten(map(vms, vm => vm.VIFs))))
+ .groupBy('$VM')
+
+ return {
+ networks: getNetworks,
+ pifs: getPifs,
+ pools: getPools,
+ vbdsByVm: getVbdsByVm,
+ vifsByVm: getVifsByVM,
+ vms: getVms,
+ }
+ },
+ { withRef: true }
+)
+export default class MigrateVmsModalBody extends BaseComponent {
+ constructor (props) {
+ super(props)
+
+ this._getHostPredicate = createSelector(
+ () => this.props.vms,
+ vms => host => some(vms, vm => host.id !== vm.$container)
+ )
+
+ this._getSrPredicate = createSelector(
+ () => this.state.host,
+ host =>
+ host
+ ? sr =>
+ isSrWritable(sr) &&
+ (sr.$container === host.id || sr.$container === host.$pool)
+ : false
+ )
+
+ this._getTargetNetworkPredicate = createSelector(
+ createPicker(() => this.props.pifs, () => this.state.host.$PIFs),
+ pifs => {
+ if (!pifs) {
+ return false
+ }
+
+ const networks = {}
+ forEach(pifs, pif => {
+ networks[pif.$network] = true
+ })
+
+ return network => networks[network.id]
+ }
+ )
+
+ this._getMigrationNetworkPredicate = createSelector(
+ createPicker(() => this.props.pifs, () => this.state.host.$PIFs),
+ pifs => {
+ if (!pifs) {
+ return false
+ }
+
+ const networks = {}
+ forEach(pifs, pif => {
+ pif.ip && (networks[pif.$network] = true)
+ })
+
+ return network => networks[network.id]
+ }
+ )
+ }
+
+ componentDidMount () {
+ this._selectHost(this.props.host)
+ }
+
+ get value () {
+ const { host } = this.state
+ const vms = filter(this.props.vms, vm => vm.$container !== host.id)
+ if (!host || isEmpty(vms)) {
+ return { vms }
+ }
+ const { networks, pifs, vbdsByVm, vifsByVm } = this.props
+ const {
+ intraPool,
+ doNotMigrateVdi,
+ doNotMigrateVmVdis,
+ migrationNetworkId,
+ networkId,
+ smartVifMapping,
+ srId,
+ } = this.state
+
+ // Map VM --> ( Map VDI --> SR )
+ const mapVmsMapVdisSrs = {}
+ forEach(vbdsByVm, (vbds, vm) => {
+ if (doNotMigrateVmVdis[vm]) {
+ return
+ }
+ const mapVdisSrs = {}
+ forEach(vbds, vbd => {
+ const vdi = vbd.VDI
+ if (!vbd.is_cd_drive && vdi) {
+ mapVdisSrs[vdi] =
+ intraPool && doNotMigrateVdi[vdi] ? this._getObject(vdi).SR : srId
+ }
+ })
+ mapVmsMapVdisSrs[vm] = mapVdisSrs
+ })
+
+ const defaultNetwork =
+ smartVifMapping &&
+ invoke(() => {
+ // First PIF with an IP.
+ const pifId = find(host.$PIFs, pif => pifs[pif].ip)
+ const pif = pifId && pifs[pifId]
+
+ return pif && pif.$network
+ })
+
+ // Map VM --> ( Map VIF --> network )
+ const mapVmsMapVifsNetworks = {}
+ forEach(vms, vm => {
+ if (vm.$pool === host.$pool) {
+ return
+ }
+ const mapVifsNetworks = {}
+ forEach(vifsByVm[vm.id], vif => {
+ mapVifsNetworks[vif.id] = smartVifMapping
+ ? getDefaultNetworkForVif(vif, host, pifs, networks) || defaultNetwork
+ : networkId
+ })
+ mapVmsMapVifsNetworks[vm.id] = mapVifsNetworks
+ })
+
+ // Map VM --> migration network
+ const mapVmsMigrationNetwork = mapValues(
+ doNotMigrateVmVdis,
+ doNotMigrateVdis => (doNotMigrateVdis ? undefined : migrationNetworkId)
+ )
+
+ return {
+ mapVmsMapVdisSrs,
+ mapVmsMapVifsNetworks,
+ mapVmsMigrationNetwork,
+ targetHost: host.id,
+ vms,
+ }
+ }
+
+ _getObject (id) {
+ return getObject(store.getState(), id)
+ }
+
+ _selectHost = host => {
+ if (!host) {
+ this.setState({ targetHost: undefined })
+ return
+ }
+ const { pools, pifs } = this.props
+ const defaultMigrationNetworkId = find(
+ pifs,
+ pif => pif.$host === host.id && pif.management
+ ).$network
+ const defaultSrId = pools[host.$pool].default_SR
+ const defaultSrConnectedToHost = some(
+ host.$PBDs,
+ pbd => this._getObject(pbd).SR === defaultSrId
+ )
+ const doNotMigrateVmVdis = {}
+ const doNotMigrateVdi = {}
+ forEach(this.props.vbdsByVm, (vbds, vm) => {
+ if (this._getObject(vm).$container === host.id) {
+ doNotMigrateVmVdis[vm] = true
+ return
+ }
+ const _doNotMigrateVdi = {}
+ forEach(vbds, vbd => {
+ if (vbd.VDI != null) {
+ doNotMigrateVdi[vbd.VDI] = _doNotMigrateVdi[vbd.VDI] = isSrShared(
+ this._getObject(this._getObject(vbd.VDI).$SR)
+ )
+ }
+ })
+ doNotMigrateVmVdis[vm] = every(_doNotMigrateVdi)
+ })
+ const noVdisMigration = every(doNotMigrateVmVdis)
+ this.setState({
+ defaultSrConnectedToHost,
+ defaultSrId,
+ host,
+ intraPool: every(this.props.vms, vm => vm.$pool === host.$pool),
+ doNotMigrateVdi,
+ doNotMigrateVmVdis,
+ migrationNetworkId: defaultMigrationNetworkId,
+ networkId: defaultMigrationNetworkId,
+ noVdisMigration,
+ smartVifMapping: true,
+ srId: defaultSrConnectedToHost ? defaultSrId : undefined,
+ })
+ }
+ _selectMigrationNetwork = migrationNetwork =>
+ this.setState({ migrationNetworkId: migrationNetwork.id })
+ _selectNetwork = network => this.setState({ networkId: network.id })
+ _selectSr = sr => this.setState({ srId: sr.id })
+ _toggleSmartVifMapping = () =>
+ this.setState({ smartVifMapping: !this.state.smartVifMapping })
+
+ render () {
+ const {
+ defaultSrConnectedToHost,
+ defaultSrId,
+ host,
+ intraPool,
+ migrationNetworkId,
+ networkId,
+ noVdisMigration,
+ smartVifMapping,
+ srId,
+ } = this.state
+ return (
+
+
+
+ {_('migrateVmSelectHost')}
+
+
+
+
+
+ {intraPool === false && (
+
+
+ {_('migrateVmSelectMigrationNetwork')}
+
+
+
+
+
+ )}
+ {host &&
+ (!intraPool || !noVdisMigration) && (
+
+
+
+ {!intraPool
+ ? _('migrateVmsSelectSr')
+ : _('migrateVmsSelectSrIntraPool')}{' '}
+ {(defaultSrId === undefined || !defaultSrConnectedToHost) && (
+
+
+
+ )}
+
+
+
+
+
+
+ )}
+ {host &&
+ !intraPool && (
+
+
+ {_('migrateVmsSelectNetwork')}
+
+
+
+
+
+
+ {' '}
+ {_('migrateVmsSmartMapping')}
+
+
+
+ )}
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/common/xo/new-ssh-key-modal/index.js b/packages/xo-web/src/common/xo/new-ssh-key-modal/index.js
new file mode 100644
index 000000000..2ee728a78
--- /dev/null
+++ b/packages/xo-web/src/common/xo/new-ssh-key-modal/index.js
@@ -0,0 +1,57 @@
+import BaseComponent from 'base-component'
+import React from 'react'
+
+import _ from '../../intl'
+import SingleLineRow from '../../single-line-row'
+import { Col } from '../../grid'
+import getEventValue from '../../get-event-value'
+
+export default class NewSshKeyModalBody extends BaseComponent {
+ get value () {
+ return this.state
+ }
+
+ _onKeyChange = event => {
+ const key = getEventValue(event)
+ const splitKey = key.split(' ')
+ if (!this.state.title && splitKey.length === 3) {
+ this.setState({ title: splitKey[2].split('\n')[0] })
+ }
+ this.setState({ key })
+ }
+
+ render () {
+ const { key, title } = this.state
+
+ return (
+
+
+
+ {_('title')}
+
+
+
+
+
+
+
+ {_('key')}
+
+
+
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/common/xo/revert-snapshot-modal/index.js b/packages/xo-web/src/common/xo/revert-snapshot-modal/index.js
new file mode 100644
index 000000000..614165c3d
--- /dev/null
+++ b/packages/xo-web/src/common/xo/revert-snapshot-modal/index.js
@@ -0,0 +1,28 @@
+import _ from 'intl'
+import BaseComponent from 'base-component'
+import React from 'react'
+
+export default class RevertSnapshotModalBody extends BaseComponent {
+ state = { snapshotBefore: true }
+
+ get value () {
+ return this.state.snapshotBefore
+ }
+
+ render () {
+ return (
+
+
{_('revertVmModalMessage')}
+
+
+ {' '}
+ {_('revertVmModalSnapshotBefore')}
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/common/xo/utils.js b/packages/xo-web/src/common/xo/utils.js
new file mode 100644
index 000000000..a66006d55
--- /dev/null
+++ b/packages/xo-web/src/common/xo/utils.js
@@ -0,0 +1,24 @@
+import { forEach, includes, map } from 'lodash'
+
+export const getDefaultNetworkForVif = (vif, destHost, pifs, networks) => {
+ const originNetwork = networks[vif.$network]
+ const originVlans = map(originNetwork.PIFs, pifId => pifs[pifId].vlan)
+
+ let destNetworkId = pifs[destHost.$PIFs[0]].$network
+
+ forEach(destHost.$PIFs, pifId => {
+ const { $network, vlan } = pifs[pifId]
+
+ if (networks[$network].name_label === originNetwork.name_label) {
+ destNetworkId = $network
+
+ return false
+ }
+
+ if (vlan !== -1 && includes(originVlans, vlan)) {
+ destNetworkId = $network
+ }
+ })
+
+ return destNetworkId
+}
diff --git a/packages/xo-web/src/common/xoa-updater.js b/packages/xo-web/src/common/xoa-updater.js
new file mode 100644
index 000000000..684e00e4e
--- /dev/null
+++ b/packages/xo-web/src/common/xoa-updater.js
@@ -0,0 +1,434 @@
+import assign from 'lodash/assign'
+import Client, {
+ AbortedConnection,
+ ConnectionError,
+} from 'jsonrpc-websocket-client'
+import eventToPromise from 'event-to-promise'
+import forEach from 'lodash/forEach'
+import makeError from 'make-error'
+import map from 'lodash/map'
+import { EventEmitter } from 'events'
+import {
+ xoaConfiguration,
+ xoaRegisterState,
+ xoaTrialState,
+ xoaUpdaterLog,
+ xoaUpdaterState,
+} from 'store/actions'
+
+// ===================================================================
+
+const states = [
+ 'disconnected',
+ 'updating',
+ 'upgrading',
+ 'upToDate',
+ 'upgradeNeeded',
+ 'registerNeeded',
+ 'error',
+]
+
+// ===================================================================
+
+export function isTrialRunning (trial) {
+ return trial && trial.end && Date.now() < trial.end
+}
+
+export function exposeTrial (trial) {
+ // We won't suggest trial if any trial is running now, or if premium was enjoyed in any past trial
+ return !(trial && (isTrialRunning(trial) || trial.plan === 'premium'))
+}
+
+export function blockXoaAccess (xoaState) {
+ let block = xoaState.state === 'untrustedTrial'
+ if (process.env.XOA_PLAN > 1 && process.env.XOA_PLAN < 5) {
+ block = block || xoaState.state === 'ERROR'
+ }
+ return block
+}
+
+function getCurrentUrl () {
+ if (typeof window === 'undefined') {
+ throw new Error('cannot get current URL')
+ }
+ return String(window.location)
+}
+
+function adaptUrl (url, port = null) {
+ const matches = /^http(s?):\/\/([^/:]*(?::[^/]*)?)(?:[^:]*)?$/.exec(url)
+ if (!matches || !matches[2]) {
+ throw new Error('current URL not recognized')
+ }
+ return 'ws' + matches[1] + '://' + matches[2] + '/api/updater'
+}
+
+// ===================================================================
+
+export const NotRegistered = makeError('NotRegistered')
+
+class XoaUpdater extends EventEmitter {
+ constructor () {
+ super()
+ this._waiting = false
+ this._log = []
+ this._lastRun = 0
+ this._lowState = null
+ this.state('disconnected')
+ this.registerError = ''
+ this._configuration = {}
+ }
+
+ state (state) {
+ this._state = state
+ this.emit(state, this._lowState && this._lowState.source)
+ }
+
+ async update () {
+ if (this._waiting) {
+ return
+ }
+ this._waiting = true
+ this.state('updating')
+ this._update(false)
+ }
+
+ async upgrade () {
+ if (this._waiting) {
+ return
+ }
+ this._waiting = true
+ this.state('upgrading')
+ await this._update(true)
+ }
+
+ _upgradeSuccessful () {
+ this.emit('upgradeSuccessful', this._lowState && this._lowState.source)
+ }
+
+ async _open () {
+ const openFailure = error => {
+ switch (true) {
+ case error instanceof AbortedConnection:
+ this.log('error', 'AbortedConnection')
+ break
+ case error instanceof ConnectionError:
+ this.log('error', 'ConnectionError')
+ break
+ default:
+ this.log('error', error)
+ }
+ delete this._client
+ this.state('disconnected')
+ throw error
+ }
+
+ const handleOpen = c => {
+ const middle = new EventEmitter()
+ const handleError = error => {
+ this.log('error', error.message)
+ this._lowState = error
+ this.state('error')
+ this._waiting = false
+ this.emit('error', error)
+ }
+
+ c.on('notification', n => middle.emit(n.method, n.params))
+ c.on('closed', () => middle.emit('disconnected'))
+
+ middle.on('print', ({ content }) => {
+ Array.isArray(content) || (content = [content])
+ content.forEach(elem => this.log('info', elem))
+ this.emit('print', content)
+ })
+ middle.on('end', end => {
+ this._lowState = end
+ switch (this._lowState.state) {
+ case 'xoa-up-to-date':
+ case 'xoa-upgraded':
+ case 'updater-upgraded':
+ case 'installer-upgraded':
+ this.state('upToDate')
+ break
+ case 'xoa-upgrade-needed':
+ case 'updater-upgrade-needed':
+ case 'installer-upgrade-needed':
+ this.state('upgradeNeeded')
+ break
+ case 'register-needed':
+ this.state('registerNeeded')
+ break
+ default:
+ this.state('error')
+ }
+ this.log(end.level, end.message)
+ this._lastRun = Date.now()
+ this._waiting = false
+ this.emit('end', end)
+ if (this._lowState === 'register-needed') {
+ this.isRegistered()
+ }
+ if (
+ this._lowState.state === 'updater-upgraded' ||
+ this._lowState.state === 'installer-upgraded'
+ ) {
+ this.update()
+ } else if (this._lowState.state === 'xoa-upgraded') {
+ this._upgradeSuccessful()
+ }
+ this.xoaState()
+ })
+ middle.on('warning', warning => {
+ this.log('warning', warning.message)
+ this.emit('warning', warning)
+ })
+ middle.on('server-error', handleError)
+ middle.on('disconnected', () => {
+ this._lowState = null
+ this.state('disconnected')
+ this._waiting = false
+ this.log('warning', 'Lost connection with xoa-updater')
+ middle.emit('reconnect_failed') // No reconnecting attempts implemented so far
+ })
+ middle.on('reconnect_failed', () => {
+ this._waiting = false
+ middle.removeAllListeners()
+ this._client.removeAllListeners()
+ if (this._client.status !== 'closed') {
+ this._client.close()
+ }
+ delete this._client
+ const message = 'xoa-updater could not be reached'
+ this._xoaStateError({ message })
+ this.log('error', message)
+ this.emit('disconnected')
+ })
+
+ this.update()
+ this.isRegistered()
+ this.getConfiguration()
+ return c
+ }
+
+ if (!this._client) {
+ try {
+ this._client = new Client(adaptUrl(getCurrentUrl()))
+ await this._client.open()
+ handleOpen(this._client)
+ } catch (error) {
+ openFailure(error)
+ }
+ }
+ const c = this._client
+ if (c.status === 'open') {
+ return c
+ } else {
+ return eventToPromise
+ .multi(c, ['open'], ['closed', 'error'])
+ .then(() => c)
+ }
+ }
+
+ async isRegistered () {
+ try {
+ const token = await this._call('isRegistered')
+ if (token.registrationToken === undefined) {
+ throw new NotRegistered(
+ 'Your Xen Orchestra Appliance is not registered'
+ )
+ } else {
+ this.registerState = 'registered'
+ this.token = token
+ return token
+ }
+ } catch (error) {
+ delete this.token
+ if (error instanceof NotRegistered) {
+ this.registerState = 'unregistered'
+ } else {
+ this.registerError = error.message
+ this.registerState = 'error'
+ }
+ } finally {
+ this.emit('registerState', {
+ state: this.registerState,
+ email: (this.token && this.token.registrationEmail) || '',
+ error: this.registerError,
+ })
+ }
+ }
+
+ async register (email, password, renew = false) {
+ try {
+ const token = await this._call('register', { email, password, renew })
+ this.registerState = 'registered'
+ this.registerError = ''
+ this.token = token
+ return token
+ } catch (error) {
+ if (!renew) {
+ delete this.token
+ }
+ if (error.code && error.code === 1) {
+ this.registerError = 'Authentication failed'
+ } else {
+ this.registerError = error.message
+ this.registerState = 'error'
+ }
+ } finally {
+ this.emit('registerState', {
+ state: this.registerState,
+ email: (this.token && this.token.registrationEmail) || '',
+ error: this.registerError,
+ })
+ if (this.registerState === 'registered') {
+ this.update()
+ }
+ }
+ }
+
+ async requestTrial () {
+ const state = await this.xoaState()
+ if (!state.state === 'ERROR') {
+ throw new Error(state.message)
+ }
+ if (isTrialRunning(state.trial)) {
+ throw new Error('You are already under trial')
+ }
+ try {
+ return this._call('requestTrial', { trialPlan: 'premium' })
+ } finally {
+ this.xoaState()
+ }
+ }
+
+ async xoaState () {
+ try {
+ const state = await this._call('xoaState')
+ this._xoaState = state
+ return state
+ } catch (error) {
+ return this._xoaStateError(error)
+ } finally {
+ this.emit('trialState', assign({}, this._xoaState))
+ }
+ }
+
+ _xoaStateError (error) {
+ const message = error.message || String(error)
+ this._xoaState = {
+ state: 'ERROR',
+ message,
+ }
+ return this._xoaState
+ }
+
+ async _update (upgrade = false) {
+ try {
+ const c = await this._open()
+ this.log('info', 'Start ' + (upgrade ? 'upgrading' : 'updating' + '...'))
+ c.notify('update', { upgrade })
+ } catch (error) {
+ this._waiting = false
+ }
+ }
+
+ async start () {
+ if (this.isStarted()) {
+ return
+ }
+ await this.xoaState()
+ await this.isRegistered()
+ this._interval = setInterval(() => this.run(), 60 * 60 * 1000)
+ this.run()
+ }
+
+ stop () {
+ if (this._interval) {
+ clearInterval(this._interval)
+ delete this._interval
+ }
+ if (this._client) {
+ this._client.removeAllListeners()
+ if (this._client.status !== 'closed') {
+ this._client.close()
+ }
+ delete this._client
+ }
+ this.state('disconnected')
+ }
+
+ run () {
+ if (Date.now() - this._lastRun >= 24 * 60 * 60 * 1000) {
+ this.update()
+ }
+ }
+
+ isStarted () {
+ return this._interval
+ }
+
+ log (level, message) {
+ message = (message != null && message.message) || String(message)
+ const date = new Date()
+ this._log.unshift({
+ date: date.toLocaleString(),
+ level,
+ message,
+ })
+ while (this._log.length > 10) {
+ this._log.pop()
+ }
+ this.emit('log', map(this._log, item => assign({}, item)))
+ }
+
+ async getConfiguration () {
+ try {
+ this._configuration = await this._call('getConfiguration')
+ return this._configuration
+ } catch (error) {
+ this._configuration = {}
+ } finally {
+ this.emit('configuration', assign({}, this._configuration))
+ }
+ }
+
+ async _call (...args) {
+ const c = await this._open()
+ try {
+ return await c.call(...args)
+ } catch (error) {
+ this.log('error', error)
+ throw error
+ }
+ }
+
+ async configure (config) {
+ try {
+ this._configuration = await this._call('configure', config)
+ this.update()
+ return this._configuration
+ } catch (error) {
+ this._configuration = {}
+ } finally {
+ this.emit('configuration', assign({}, this._configuration))
+ }
+ }
+}
+
+const xoaUpdater = new XoaUpdater()
+
+export default xoaUpdater
+
+export const connectStore = store => {
+ forEach(states, state =>
+ xoaUpdater.on(state, () => store.dispatch(xoaUpdaterState(state)))
+ )
+ xoaUpdater.on('trialState', state => store.dispatch(xoaTrialState(state)))
+ xoaUpdater.on('log', log => store.dispatch(xoaUpdaterLog(log)))
+ xoaUpdater.on('registerState', registration =>
+ store.dispatch(xoaRegisterState(registration))
+ )
+ xoaUpdater.on('configuration', configuration =>
+ store.dispatch(xoaConfiguration(configuration))
+ )
+}
diff --git a/packages/xo-web/src/common/xoa-upgrade.js b/packages/xo-web/src/common/xoa-upgrade.js
new file mode 100644
index 000000000..947bed8f1
--- /dev/null
+++ b/packages/xo-web/src/common/xoa-upgrade.js
@@ -0,0 +1,51 @@
+import React from 'react'
+
+import _ from './intl'
+import Icon from './icon'
+import Link from './link'
+import propTypes from './prop-types-decorator'
+import { Card, CardHeader, CardBlock } from './card'
+import { connectStore, getXoaPlan } from './utils'
+import { isAdmin } from 'selectors'
+
+const Upgrade = propTypes({
+ available: propTypes.number,
+ place: propTypes.string.isRequired,
+ required: propTypes.number,
+})(
+ connectStore({
+ isAdmin,
+ })
+)(
+ ({ available, children, isAdmin, place, required = available }) =>
+ process.env.XOA_PLAN < required ? (
+
+ {_('upgradeNeeded')}
+ {isAdmin ? (
+
+ {_('availableIn', { plan: getXoaPlan(required) })}
+
+
+ {_('upgradeNow')}
+ {' '}
+ {_('or')}
+
+ {_('tryIt')}
+
+
+
+ ) : (
+
+ {_('notAvailable')}
+
+ )}
+
+ ) : (
+ children
+ )
+)
+
+export { Upgrade as default }
diff --git a/packages/xo-web/src/favicon.ico b/packages/xo-web/src/favicon.ico
new file mode 100644
index 000000000..adc2a4447
Binary files /dev/null and b/packages/xo-web/src/favicon.ico differ
diff --git a/packages/xo-web/src/icons.scss b/packages/xo-web/src/icons.scss
new file mode 100644
index 000000000..87fbaa55b
--- /dev/null
+++ b/packages/xo-web/src/icons.scss
@@ -0,0 +1,1011 @@
+.xo-icon {
+ &-pool {
+ @extend .fa;
+ @extend .fa-cloud;
+ }
+ &-host {
+ @extend .fa;
+ @extend .fa-server;
+ }
+ &-vm {
+ @extend .fa;
+ @extend .fa-desktop;
+ }
+ &-remote {
+ @extend .fa;
+ @extend .fa-plug;
+ }
+ &-task {
+ @extend .fa;
+ @extend .fa-tasks;
+ }
+ &-template {
+ @extend .fa;
+ @extend .fa-thumb-tack;
+ }
+ &-message {
+ @extend .fa;
+ @extend .fa-envelope-o;
+ }
+ &-logs {
+ @extend .fa;
+ @extend .fa-list;
+ }
+ &-alarm {
+ @extend .fa;
+ @extend .fa-exclamation-triangle;
+ }
+ &-error {
+ @extend .fa;
+ @extend .fa-exclamation-triangle;
+ @extend .text-danger;
+ }
+ &-success {
+ @extend .fa;
+ @extend .fa-check;
+ @extend .text-success;
+ }
+ &-undo {
+ @extend .fa;
+ @extend .fa-undo;
+ @extend .text-warning;
+ }
+ &-edition {
+ @extend .fa;
+ @extend .fa-pencil-square-o;
+ }
+ &-edit {
+ @extend .fa;
+ @extend .fa-pencil;
+ }
+ &-refresh {
+ @extend .fa;
+ @extend .fa-refresh;
+ }
+ &-administration {
+ @extend .fa;
+ @extend .fa-wrench;
+ }
+ &-diagnosis {
+ @extend .fa;
+ @extend .fa-medkit;
+ }
+ &-chevron-up {
+ @extend .fa;
+ @extend .fa-chevron-up;
+ }
+ &-chevron-down {
+ @extend .fa;
+ @extend .fa-chevron-down;
+ }
+
+ &-grab {
+ @extend .fa;
+ @extend .fa-ellipsis-v;
+ },
+ &-previous {
+ @extend .fa;
+ @extend .fa-chevron-left;
+ },
+ &-next {
+ @extend .fa;
+ @extend .fa-chevron-right;
+ },
+ &-caret {
+ @extend .fa;
+ @extend .fa-caret-down;
+ }
+ &-caret-up {
+ @extend .fa;
+ @extend .fa-caret-up;
+ }
+ &-loading {
+ @extend .fa;
+ @extend .fa-spinner;
+ @extend .fa-pulse;
+ }
+ &-clipboard {
+ @extend .fa;
+ @extend .fa-clipboard;
+ }
+ &-shortcuts {
+ @extend .fa;
+ @extend .fa-keyboard-o;
+ }
+ &-info {
+ @extend .fa;
+ @extend .fa-info-circle;
+ }
+ &-search {
+ @extend .fa;
+ @extend .fa-search;
+ }
+ &-settings {
+ @extend .fa;
+ @extend .fa-cog;
+ }
+ &-summary {
+ @extend .fa;
+ @extend .fa-flag-checkered;
+ }
+ &-run {
+ @extend .fa;
+ @extend .fa-play;
+ }
+ &-ssh-key {
+ @extend .fa;
+ @extend .fa-key;
+ }
+ &-ip {
+ @extend .fa;
+ @extend .fa-map-marker;
+ }
+ &-file {
+ @extend .fa;
+ @extend .fa-file-o;
+ }
+
+ &-shown {
+ @extend .fa;
+ @extend .fa-eye;
+ }
+ &-hidden {
+ @extend .fa;
+ @extend .fa-eye-slash;
+ }
+
+ &-password {
+ @extend .fa;
+ @extend .fa-key;
+ }
+
+ &-toggle-on {
+ @extend .fa;
+ @extend .fa-toggle-on;
+ }
+
+ &-toggle-off {
+ @extend .fa;
+ @extend .fa-toggle-off;
+ }
+
+ &-scale {
+ @extend .fa;
+ @extend .fa-balance-scale;
+ }
+
+ &-asc {
+ @extend .fa;
+ @extend .fa-arrow-down;
+ }
+ &-desc {
+ @extend .fa;
+ @extend .fa-arrow-up;
+ }
+ &-sort {
+ @extend .fa;
+ @extend .fa-sort;
+ }
+
+ &-connect {
+ @extend .fa;
+ @extend .fa-link;
+ }
+ &-disconnect {
+ @extend .fa;
+ @extend .fa-chain-broken;
+ }
+
+ &-lock {
+ @extend .fa;
+ @extend .fa-lock;
+ }
+ &-unlock {
+ @extend .fa;
+ @extend .fa-unlock;
+ }
+ &-unknown-status {
+ @extend .fa;
+ @extend .fa-question-circle;
+ }
+
+ &-cpu {
+ @extend .fa;
+ @extend .fa-microchip;
+ }
+ &-gpu {
+ @extend .fa;
+ @extend .fa-microchip;
+ }
+ &-vgpu {
+ @extend .fa;
+ @extend .fa-microchip;
+ }
+ &-memory {
+ @extend .fa;
+ @extend .fa-sliders;
+ }
+ &-disk {
+ @extend .fa;
+ @extend .fa-hdd-o;
+ }
+ &-network {
+ @extend .fa;
+ @extend .fa-sitemap;
+ }
+ &-sr {
+ @extend .fa;
+ @extend .fa-database;
+ }
+ &-delete {
+ @extend .fa;
+ @extend .fa-trash;
+ }
+ &-self-service {
+ @extend .fa;
+ @extend .fa-cloud;
+ }
+ &-resource-set {
+ @extend .fa;
+ @extend .fa-list-alt;
+ }
+ &-trial {
+ @extend .fa;
+ @extend .fa-thumbs-up;
+ }
+
+ // Backups
+ &-backup {
+ @extend .fa;
+ @extend .fa-download;
+ }
+ &-rolling-snapshot {
+ @extend .fa;
+ @extend .fa-camera;
+ }
+ &-delta-backup {
+ @extend .fa;
+ @extend .fa-code-fork;
+ }
+ &-disaster-recovery {
+ @extend .fa;
+ @extend .fa-medkit;
+ }
+ &-continuous-replication {
+ @extend .fa;
+ @extend .fa-map-signs;
+ }
+
+ // Jobs
+ &-jobs {
+ @extend .fa;
+ @extend .fa-cogs;
+ }
+
+ // VM
+ &-vm {
+ // States
+ &-running {
+ @extend .fa;
+ @extend .fa-desktop;
+ @extend .text-success;
+ }
+ &-suspended {
+ @extend .fa;
+ @extend .fa-desktop;
+ @extend .text-primary;
+ }
+ &-halted {
+ @extend .fa;
+ @extend .fa-desktop;
+ @extend .text-danger;
+ }
+ &-busy {
+ @extend .fa;
+ @extend .fa-desktop;
+ @extend .text-warning;
+ }
+
+ // Actions
+ &-create-template {
+ @extend .fa;
+ @extend .fa-thumb-tack;
+ }
+ &-copy {
+ @extend .fa;
+ @extend .fa-clone;
+ }
+ &-console {
+ @extend .fa;
+ @extend .fa-terminal;
+ }
+ &-migrate {
+ @extend .fa;
+ @extend .fa-share;
+ }
+ &-snapshot {
+ @extend .fa;
+ @extend .fa-camera;
+ }
+ &-fast-clone {
+ @extend .fa;
+ @extend .fa-code-fork;
+ }
+ &-clone {
+ @extend .fa;
+ @extend .fa-copy;
+ }
+ &-suspend {
+ @extend .fa;
+ @extend .fa-pause;
+ }
+ &-force-reboot {
+ @extend .fa;
+ @extend .fa-bolt;
+ }
+ &-recovery-mode {
+ @extend .fa;
+ @extend .fa-forward;
+ }
+ &-force-shutdown {
+ @extend .fa;
+ @extend .fa-power-off;
+ }
+ &-docker {
+ @extend .fa;
+ @extend .text-primary;
+ @extend .fa-ship;
+ }
+ }
+
+ // Generic states
+ &-running {
+ @extend .fa;
+ @extend .fa-circle;
+ @extend .xo-status-running;
+ }
+
+ &-halted {
+ @extend .fa;
+ @extend .fa-circle;
+ @extend .xo-status-halted;
+ }
+
+ &-suspended {
+ @extend .fa;
+ @extend .fa-circle;
+ @extend .xo-status-suspended;
+ }
+
+ &-paused {
+ @extend .fa;
+ @extend .fa-circle;
+ @extend .xo-status-suspended;
+ }
+
+ &-unknown {
+ @extend .fa;
+ @extend .fa-circle;
+ @extend .xo-status-unknown;
+ }
+
+ &-busy {
+ @extend .fa;
+ @extend .fa-circle;
+ @extend .xo-status-busy;
+ }
+
+ &-disabled {
+ @extend .fa;
+ @extend .fa-circle;
+ @extend .xo-status-busy;
+ }
+
+ &-all-connected {
+ @extend .fa;
+ @extend .fa-circle;
+ @extend .xo-status-running;
+ }
+
+ &-some-connected {
+ @extend .fa;
+ @extend .fa-circle;
+ @extend .xo-status-busy;
+ }
+
+ &-all-disconnected {
+ @extend .fa;
+ @extend .fa-circle;
+ @extend .xo-status-halted;
+ }
+
+ &-connected {
+ @extend .fa;
+ @extend .fa-link;
+ }
+
+ &-disconnected {
+ @extend .fa;
+ @extend .fa-unlink;
+ }
+
+ // Task
+ &-task {
+ &-cancel {
+ @extend .fa;
+ @extend .fa-ban;
+ }
+ &-destroy {
+ @extend .fa;
+ @extend .fa-trash;
+ }
+ }
+
+ // SR
+ &-sr, &-vdi {
+ &-reconnect-all {
+ @extend .fa;
+ @extend .fa-retweet;
+ }
+ &-disconnect-all {
+ @extend .fa;
+ @extend .fa-power-off;
+ }
+ &-forget {
+ @extend .fa;
+ @extend .fa-ban;
+ }
+ &-remove {
+ @extend .fa;
+ @extend .fa-trash;
+ }
+ &-migrate {
+ @extend .fa;
+ @extend .fa-share;
+ }
+ }
+ // Host
+ &-host {
+ // States
+ &-running {
+ @extend .fa;
+ @extend .fa-server;
+ @extend .text-success;
+ }
+ &-halted {
+ @extend .fa;
+ @extend .fa-server;
+ @extend .text-danger;
+ }
+ &-disabled {
+ @extend .fa;
+ @extend .fa-server;
+ @extend .text-warning;
+ }
+ &-forget {
+ @extend .fa;
+ @extend .fa-ban;
+ }
+ &-working {
+ @extend .fa;
+ @extend .fa-circle;
+ @extend .text-warning;
+ }
+
+ // Actions
+ &-enable {
+ @extend .fa;
+ @extend .fa-check-circle;
+ }
+ &-disable {
+ @extend .fa;
+ @extend .fa-times-circle;
+ }
+ &-restart-agent {
+ @extend .fa;
+ @extend .fa-retweet;
+ }
+ &-emergency-shutdown {
+ @extend .fa;
+ @extend .fa-exclamation-triangle;
+ }
+ &-patch-update {
+ @extend .fa;
+ @extend .fa-download;
+ }
+ }
+
+ // Host and VM actions
+ &-host, &-vm {
+ &-start {
+ @extend .fa;
+ @extend .fa-play;
+ }
+ &-delete {
+ @extend .fa;
+ @extend .fa-trash;
+ }
+ &-stop {
+ @extend .fa;
+ @extend .fa-stop;
+ }
+ &-force-reboot {
+ @extend .fa;
+ @extend .fa-bolt;
+ }
+ &-reboot {
+ @extend .fa;
+ @extend .fa-refresh;
+ }
+ &-eject {
+ @extend .fa;
+ @extend .fa-eject;
+ }
+ &-keyboard {
+ @extend .fa;
+ @extend .fa-keyboard-o;
+ }
+ &-new {
+ @extend .fa;
+ @extend .fa-plus;
+ }
+ }
+
+ &-snapshot {
+ &-revert {
+ @extend .fa;
+ @extend .fa-refresh;
+ }
+ &-delete {
+ @extend .fa;
+ @extend .fa-trash;
+ }
+ }
+
+ &-filters {
+ @extend .fa;
+ @extend .fa-filter
+ }
+
+ &-tags {
+ @extend .fa;
+ @extend .fa-tags
+ }
+
+ &-remove-tag {
+ @extend .fa;
+ @extend .fa-times;
+ }
+
+ &-add {
+ @extend .fa;
+ @extend .fa-plus;
+ }
+
+ &-add-tag {
+ @extend .fa;
+ @extend .fa-plus;
+ }
+
+ &-sort {
+ @extend .fa;
+ @extend .fa-sort;
+ }
+ &-reset {
+ @extend .fa;
+ @extend .fa-undo;
+ }
+ &-save {
+ @extend .fa;
+ @extend .fa-floppy-o;
+ }
+ &-delete {
+ @extend .fa;
+ @extend .fa-trash;
+ }
+ &-remove {
+ @extend .fa;
+ @extend .fa-times;
+ }
+ &-minus {
+ @extend .fa;
+ @extend .fa-minus
+ }
+ &-plus {
+ @extend .fa;
+ @extend .fa-plus
+ }
+ &-clear-search {
+ @extend .fa;
+ @extend .fa-times-circle;
+ }
+
+ &-preview {
+ @extend .fa;
+ @extend .fa-eye;
+ }
+ &-backup {
+ @extend .fa;
+ @extend .fa-archive;
+ }
+ &-import {
+ @extend .fa;
+ @extend .fa-file-archive-o;
+ }
+ &-export {
+ @extend .fa;
+ @extend .fa-download;
+ }
+ &-schedule {
+ @extend .fa;
+ @extend .fa-clock-o;
+ }
+ &-time {
+ @extend .fa;
+ @extend .fa-clock-o;
+ }
+ &-database {
+ @extend .fa;
+ @extend .fa-database;
+ }
+ &-arrow-right {
+ @extend .fa;
+ @extend .fa-arrow-right;
+ }
+
+ &-run-schedule {
+ @extend .fa;
+ @extend .fa-play;
+ }
+
+ &-user {
+ @extend .fa;
+ @extend .fa-user;
+ }
+ &-group {
+ @extend .fa;
+ @extend .fa-users;
+ }
+ &-sign-out {
+ @extend .fa;
+ @extend .fa-sign-out;
+ }
+
+ // Menu
+ &-menu-collapse {
+ @extend .fa;
+ @extend .fa-bars;
+ }
+ &-menu-home {
+ @extend .fa;
+ @extend .fa-home;
+ }
+ &-menu-dashboard {
+ @extend .fa;
+ @extend .fa-dashboard;
+ &-overview {
+ @extend .fa;
+ @extend .fa-eye;
+ }
+ &-visualization {
+ @extend .fa;
+ @extend .fa-pie-chart;
+ }
+ &-stats {
+ @extend .fa;
+ @extend .fa-bar-chart;
+ }
+ &-health {
+ @extend .fa;
+ @extend .fa-heartbeat;
+ }
+ }
+ &-menu-self-service {
+ @extend .fa;
+ @extend .fa-cloud;
+ }
+ &-menu-backup {
+ @extend .fa;
+ @extend .fa-archive;
+ &-overview {
+ @extend .fa;
+ @extend .fa-eye;
+ }
+ &-new {
+ @extend .fa;
+ @extend .fa-plus;
+ }
+ &-remotes {
+ @extend .fa;
+ @extend .fa-plug;
+ }
+ &-restore {
+ @extend .fa;
+ @extend .fa-upload;
+ }
+ &-file-restore {
+ @extend .fa;
+ @extend .fa-file-o;
+ }
+ }
+ &-menu-jobs {
+ @extend .fa;
+ @extend .fa-cogs;
+ &-overview {
+ @extend .fa;
+ @extend .fa-eye;
+ }
+ &-new {
+ @extend .fa;
+ @extend .fa-plus;
+ }
+ &-schedule {
+ @extend .fa;
+ @extend .fa-clock-o;
+ }
+ }
+ &-menu-xoa {
+ @extend .fa;
+ @extend .fa-cube;
+ }
+ &-menu-update {
+ @extend .fa;
+ @extend .fa-refresh;
+ }
+ &-menu-license {
+ @extend .fa;
+ @extend .fa-file-text-o;
+ }
+ &-menu-settings {
+ @extend .fa;
+ @extend .fa-cog;
+ &-servers {
+ @extend .fa;
+ @extend .fa-cloud;
+ }
+ &-users {
+ @extend .fa;
+ @extend .fa-user;
+ }
+ &-groups {
+ @extend .fa;
+ @extend .fa-users;
+ }
+ &-acls {
+ @extend .fa;
+ @extend .fa-key;
+ }
+ &-plugins {
+ @extend .fa;
+ @extend .fa-puzzle-piece;
+ }
+ &-logs {
+ @extend .fa;
+ @extend .fa-list;
+ }
+ &-config {
+ @extend .fa;
+ @extend .fa-file-o;
+ }
+ }
+ &-menu-about {
+ @extend .fa;
+ @extend .fa-info;
+ }
+ &-menu-new {
+ @extend .fa;
+ @extend .fa-plus;
+ &-vm {
+ @extend .fa;
+ @extend .fa-desktop;
+ }
+ &-sr {
+ @extend .fa;
+ @extend .fa-database;
+ }
+ &-import {
+ @extend .fa;
+ @extend .fa-file-archive-o;
+ }
+ }
+ &-menu-xosan {
+ @extend .fa;
+ @extend .fa-database;
+ }
+ // New VM
+ &-new-vm {
+ &-infos {
+ @extend .fa;
+ @extend .fa-info-circle
+ }
+ &-perf {
+ @extend .fa;
+ @extend .fa-dashboard;
+ }
+ &-install-settings {
+ @extend .fa;
+ @extend .fa-download;
+ }
+ &-interfaces {
+ @extend .fa;
+ @extend .fa-sitemap;
+ }
+ &-disks {
+ @extend .fa;
+ @extend .fa-hdd-o;
+ }
+ &-summary {
+ @extend .fa;
+ @extend .fa-flag-checkered;
+ }
+ &-create {
+ @extend .fa;
+ @extend .fa-play;
+ }
+ &-reset {
+ @extend .fa;
+ @extend .fa-undo;
+ }
+ &-add {
+ @extend .fa;
+ @extend .fa-plus;
+ }
+ &-remove {
+ @extend .fa;
+ @extend .fa-times;
+ }
+ }
+ // OS Icons
+ &-centos {
+ @extend .fa;
+ @extend .icon-centos;
+ }
+ &-debian {
+ @extend .fa;
+ @extend .icon-debian;
+ }
+ &-docker {
+ @extend .fa;
+ @extend .icon-docker;
+ }
+ &-fedora {
+ @extend .fa;
+ @extend .icon-fedora;
+ }
+ &-freebsd {
+ @extend .fa;
+ @extend .icon-freebsd;
+ }
+ &-gentoo {
+ @extend .fa;
+ @extend .icon-gentoo;
+ }
+ &-linux {
+ @extend .fa;
+ @extend .fa-linux;
+ }
+ &-linux-mint {
+ @extend .fa;
+ @extend .icon-linux-mint;
+ }
+ &-netbsd {
+ @extend .fa;
+ @extend .icon-netbsd;
+ }
+ &-oracle {
+ @extend .fa;
+ @extend .icon-oracle;
+ }
+ &-osx {
+ @extend .fa;
+ @extend .icon-osx;
+ }
+ &-redhat {
+ @extend .fa;
+ @extend .icon-redhat;
+ }
+ &-solaris {
+ @extend .fa;
+ @extend .icon-solaris;
+ }
+ &-suse {
+ @extend .fa;
+ @extend .icon-suse;
+ }
+ &-ubuntu {
+ @extend .fa;
+ @extend .icon-ubuntu;
+ }
+ &-windows {
+ @extend .fa;
+ @extend .fa-windows;
+ }
+
+ // Home
+ &-nav {
+ @extend .fa;
+ @extend .fa-bars;
+ color: #ccc;
+ }
+
+ // About
+ &-bug {
+ @extend .fa;
+ @extend .fa-bug;
+ }
+ &-help {
+ @extend .fa;
+ @extend .fa-life-ring;
+ }
+
+ // Updates
+ &-upgrade {
+ @extend .fa;
+ @extend .fa-cog;
+ }
+ &-update-unknown {
+ @extend .fa;
+ @extend .fa-question-circle;
+ }
+ &-update-ready {
+ @extend .fa;
+ @extend .fa-bell;
+ }
+ &-not-registered {
+ @extend .fa;
+ @extend .fa-bell-slash;
+ }
+
+ // Generic actions
+
+ &-add-sr {
+ @extend .fa;
+ @extend .fa-database;
+ }
+ &-add-vm {
+ @extend .fa;
+ @extend .fa-desktop;
+ }
+ &-add-host {
+ @extend .fa;
+ @extend .fa-server;
+ }
+ &-connect {
+ @extend .fa;
+ @extend .fa-link;
+ }
+ &-disconnect {
+ @extend .fa;
+ @extend .fa-unlink;
+ }
+ &-refresh {
+ @extend .fa;
+ @extend .fa-refresh;
+ }
+
+ // XOA related
+
+ &-plan-upgrade {
+ @extend .fa;
+ @extend .fa-cloud-upload;
+ }
+ &-plan-trial {
+ @extend .fa;
+ @extend .fa-star;
+ }
+ &-support {
+ @extend .fa;
+ @extend .fa-support;
+ }
+
+ // XOSAN related
+
+ &-health {
+ @extend .fa;
+ @extend .fa-heartbeat;
+ }
+ &-fix {
+ @extend .fa;
+ @extend .fa-wrench;
+ }
+}
diff --git a/packages/xo-web/src/index.js b/packages/xo-web/src/index.js
new file mode 100644
index 000000000..0a7891125
--- /dev/null
+++ b/packages/xo-web/src/index.js
@@ -0,0 +1,24 @@
+import './patch-react'
+
+import hashHistory from 'react-router/lib/hashHistory'
+import React from 'react'
+import Router from 'react-router/lib/Router'
+import store from 'store'
+import { Provider } from 'react-redux'
+import { render } from 'react-dom'
+
+import XoApp from './xo-app'
+
+render(
+
+
+ ,
+ document.getElementById('xo-app')
+)
diff --git a/packages/xo-web/src/index.pug b/packages/xo-web/src/index.pug
new file mode 100644
index 000000000..a848afa68
--- /dev/null
+++ b/packages/xo-web/src/index.pug
@@ -0,0 +1,64 @@
+//- HTML 5 Doctype
+doctype html
+
+//- The `no-js` class will be automatically removed if JavaScript is
+//- available.
+html.no-js(
+ dir = 'ltr'
+ lang = 'en'
+)
+
+ head
+
+ meta(charset = 'utf-8')
+
+ //- Makes sure IE is using the last engine available.
+ meta(
+ http-equiv = 'x-ua-compatible'
+ content = 'ie=edge,chrome=1'
+ )
+
+ //- .visible-js to display content only when JavaScript is ENABLED.
+ //- .hidden-js to display content only when JavaScript is DISABLED.
+ script !function(d){d.className=d.className.replace(/\bno-js\b/,'js')}(document.documentElement)
+
+ style .no-js .visible-js,.js .hidden-js{display:none}
+
+ //- (TODO: confirm) For smartphones and tablets: sets the page
+ //- width to the device width and prevents the page from being
+ //- zoomed in when going to landscape mode.
+ meta(
+ name = 'viewport'
+ content = 'width=device-width, initial-scale=1'
+ )
+
+ title Xen Orchestra
+
+ link(
+ rel = 'stylesheet'
+ href = 'index.css'
+ )
+ link(
+ rel = 'stylesheet'
+ href = 'modules.css'
+ )
+
+ link(
+ rel = 'shortcut icon'
+ href = 'favicon.ico'
+ )
+
+ //- Styles required for a proper display while loading.
+ style.
+ html, body, #xo-app { height: 100% }
+
+ body
+
+ #xo-app
+ div(style = 'display: flex; height: 100%')
+ div(style = 'margin: auto')
+ h1.hidden-js.text-danger JavaScript is required for Xen Orchestra!
+ h1.visible-js.text-muted
+ img(src='assets/loading.svg')
+
+ script(src = 'index.js')
diff --git a/packages/xo-web/src/index.scss b/packages/xo-web/src/index.scss
new file mode 100644
index 000000000..47ff4fe4c
--- /dev/null
+++ b/packages/xo-web/src/index.scss
@@ -0,0 +1,247 @@
+// http://v4-alpha.getbootstrap.com/getting-started/flexbox/#how-it-works
+// $enable-flex: true;
+
+$nav-pills-border-radius: 0;
+$nav-pills-active-link-color: white;
+$nav-pills-active-link-bg: #366e98;
+$brand-primary: #366e98;
+$brand-secondary: #047f75;
+$brand-success: #089944;
+$brand-danger: #990822;
+$brand-warning: #eca649;
+$brand-info: #044b7f;
+
+@import "../node_modules/bootstrap/scss/bootstrap";
+
+// -------------------------------------------------------------------
+
+$fa-font-path: "./";
+
+@import "../node_modules/font-awesome/scss/font-awesome";
+
+// Replace Bootstrap's glyphicons by Font Awesome.
+.glyphicon {
+ @extend .fa;
+}
+
+// -------------------------------------------------------------------
+
+@import "../node_modules/font-mfizz/dist/font-mfizz";
+
+// -------------------------------------------------------------------
+
+@import "./chartist";
+@import "./meter";
+@import "./icons";
+@import "./usage";
+
+// ROOT STYLES =================================================================
+
+$side-menu-bg: #044b7f;
+$side-menu-color: white;
+
+@include media-breakpoint-down(md) {
+ html {
+ font-size: 1.5vw;
+ }
+}
+
+@include media-breakpoint-down(sm) {
+ html {
+ font-size: 3vmin;
+ }
+}
+
+@include media-breakpoint-down(xs) {
+ html {
+ font-size: 2.95vmin;
+ }
+}
+
+// REACT-VIRTUALIZED ===========================================================
+
+@import '../node_modules/react-virtualized/styles';
+
+// REACT-SELECT ================================================================
+
+$select-input-height: 40px; // Bootstrap input height
+@import '../node_modules/react-select/scss/default';
+
+// Boostrap hack...
+.is-searchable {
+ width: 100%;
+}
+
+.Select-value-label {
+ color: #373a3c;
+}
+
+.Select-control {
+ border-radius: unset;
+}
+
+// Disabled option style.
+.Select-menu-outer .Select-option.is-disabled {
+ cursor: default;
+ font-weight: bold;
+ color: #777;
+}
+
+.Select-placeholder {
+ color: #999;
+}
+
+.Select--single > .Select-control .Select-value {
+ color: #333;
+}
+
+// COLORS ======================================================================
+
+.xo-status-running {
+ @extend .text-success;
+}
+
+.xo-status-halted {
+ @extend .text-danger;
+}
+
+.xo-status-suspended {
+ @extend .text-info;
+}
+
+.xo-status-unknown, .xo-status-paused {
+ @extend .text-muted;
+}
+
+.xo-status-busy {
+ @extend .text-warning;
+}
+
+// HEADER CONTENT STYLE=========================================================
+
+.header-title {
+ margin-bottom: 1em;
+}
+
+.nav-tabs {
+ font-size: 1.2em;
+}
+
+// CONTENT TAB STYLE ===========================================================
+
+.btn-huge {
+ font-size: 4em;
+}
+
+.console {
+ margin-top: 1em;
+ text-align: center;
+}
+
+// GENERAL STYLES ==============================================================
+
+.tag-ip {
+ margin-left: 1em;
+}
+
+// MENU STYLE ==================================================================
+
+.xo-menu, .xo-sub-menu {
+ background: $side-menu-bg;
+ color: $side-menu-color;
+}
+
+.xo-menu {
+ a {
+ color: inherit;
+ }
+
+ button {
+ background-color: inherit;
+ color: inherit;
+ }
+}
+
+.xo-menu-item {
+ min-width: 100%;
+ position: relative;
+ width: max-content;
+
+ &:hover {
+ background-color: $nav-pills-active-link-bg;
+ color: $nav-pills-active-link-color;
+ }
+}
+
+.xo-sub-menu {
+ left: 100%;
+ opacity: 0;
+ position: absolute;
+ top: 0;
+ transition: opacity .3s;
+ visibility: hidden;
+ width: max-content;
+ z-index: 1000;
+}
+
+.xo-menu-item:hover > .xo-sub-menu {
+ opacity: 1;
+ visibility: visible;
+}
+// PAGE HEADER STYLE ===========================================================
+
+.page-header {
+ background: $gray-lighter;
+}
+
+// NOTIFICATIONS STYLE =========================================================
+
+.notify-container {
+ align-content: flex-start;
+ align-items: flex-start;
+ display: flex;
+ flex-direction: column;
+ flex-wrap: wrap;
+ justify-content: flex-end;
+ position: absolute;
+ right: 10px;
+ top: 10px;
+}
+
+.notify-item {
+ border-radius: 5px;
+ border: 1px solid black;
+ margin: 5px 10px;
+ width: 250px;
+ &.success {
+ background: $alert-success-bg;
+ border-color: $alert-success-border;
+ color: $alert-success-text;
+ }
+ &.info {
+ background: $alert-info-bg;
+ border-color: $alert-info-border;
+ color: $alert-info-text;
+ }
+ &.error {
+ background: $alert-danger-bg;
+ border-color: $alert-danger-border;
+ color: $alert-danger-text;
+ }
+}
+
+.notify-item > p {
+ margin: 10px;
+}
+
+.notify-title {
+ font-weight: 700;
+}
+
+// =============================================================================
+
+.no-text-selection {
+ cursor: not-allowed;
+ -moz-user-select: none; /* Firefox */
+ user-select: none; /* Chrome */
+}
diff --git a/packages/xo-web/src/keymap.js b/packages/xo-web/src/keymap.js
new file mode 100644
index 000000000..fbd741f6c
--- /dev/null
+++ b/packages/xo-web/src/keymap.js
@@ -0,0 +1,37 @@
+import _ from 'intl'
+import mapValues from 'lodash/mapValues'
+
+const keymap = {
+ XoApp: {
+ GO_TO_HOSTS: 'g h',
+ GO_TO_POOLS: 'g p',
+ GO_TO_VMS: 'g v',
+ GO_TO_SRS: 'g s',
+ CREATE_VM: 'c v',
+ UNFOCUS: 'esc',
+ HELP: ['?', 'h'],
+ },
+ Home: {
+ SEARCH: '/',
+ NAV_DOWN: 'j',
+ NAV_UP: 'k',
+ SELECT: 'x',
+ JUMP_INTO: 'enter',
+ },
+ SortedTable: {
+ SEARCH: '/',
+ NAV_DOWN: 'j',
+ NAV_UP: 'k',
+ SELECT: 'x',
+ ROW_ACTION: 'enter',
+ },
+}
+export { keymap as default }
+
+export const help = mapValues(keymap, (shortcuts, contextLabel) => ({
+ name: _(`shortcut_${contextLabel}`),
+ shortcuts: mapValues(shortcuts, (shortcut, label) => ({
+ keys: shortcuts[label],
+ message: _(`shortcut_${contextLabel}_${label}`),
+ })),
+}))
diff --git a/packages/xo-web/src/meter.scss b/packages/xo-web/src/meter.scss
new file mode 100644
index 000000000..a57f57594
--- /dev/null
+++ b/packages/xo-web/src/meter.scss
@@ -0,0 +1,50 @@
+// METER OBJECT ================================================================
+
+// Used for object utilization (eg SR):
+// success for usage < 80%
+// warning for usage between 80% and 89%
+// error for usage > 90%
+
+meter {
+ /* For Firefox */
+ background: #EEE;
+ box-shadow: 0 2px 3px rgba(0,0,0,0.2) inset;
+ border-radius: 3px;
+}
+
+meter::-webkit-meter-bar {
+ background: #EEE;
+ box-shadow: 0 2px 3px rgba(0,0,0,0.2) inset;
+ border-radius: 3px;
+}
+
+meter::-webkit-meter-optimum-value {
+ background: $brand-success;
+ border-radius: 3px;
+}
+
+meter::-webkit-meter-suboptimum-value {
+ background: $brand-warning;
+ border-radius: 3px;
+}
+
+meter::-webkit-meter-even-less-good-value {
+ background: $brand-danger;
+ border-radius: 3px;
+}
+
+meter::-moz-meter-bar {
+ border-radius: 3px;
+}
+
+meter:-moz-meter-optimum::-moz-meter-bar {
+ background: $brand-success;
+}
+
+meter:-moz-meter-sub-optimum::-moz-meter-bar {
+ background: $brand-warning;
+}
+
+meter:-moz-meter-sub-sub-optimum::-moz-meter-bar {
+ background: $brand-danger;
+}
diff --git a/packages/xo-web/src/patch-react.js b/packages/xo-web/src/patch-react.js
new file mode 100644
index 000000000..f96f6f1c5
--- /dev/null
+++ b/packages/xo-web/src/patch-react.js
@@ -0,0 +1,53 @@
+import logError from 'log-error'
+import React from 'react'
+import { assign, isFunction } from 'lodash'
+
+// Avoid global breakage if a component fails to render.
+//
+// Inspired by https://gist.github.com/Aldredcz/4d63b0a9049b00f54439f8780be7f0d8
+React.createElement = (createElement => {
+ const errorComponent = (
+
+ an error has occured
+
+ )
+
+ const wrapRender = render =>
+ function patchedRender () {
+ try {
+ return render.apply(this, arguments)
+ } catch (error) {
+ logError(error)
+
+ return errorComponent
+ }
+ }
+
+ return function (Component) {
+ if (isFunction(Component)) {
+ const patched = Component._patched
+ if (patched) {
+ arguments[0] = patched
+ } else {
+ const { prototype } = Component
+ let render
+ if (prototype && isFunction((render = prototype.render))) {
+ prototype.render = wrapRender(render)
+ Component._patched = Component // itself
+ } else {
+ arguments[0] = Component._patched = assign(
+ wrapRender(Component),
+ Component
+ )
+ }
+ }
+ }
+
+ return createElement.apply(this, arguments)
+ }
+})(React.createElement)
diff --git a/packages/xo-web/src/usage.scss b/packages/xo-web/src/usage.scss
new file mode 100644
index 000000000..dbaa2b868
--- /dev/null
+++ b/packages/xo-web/src/usage.scss
@@ -0,0 +1,62 @@
+// Usage
+
+.usage {
+ @extend .progress;
+ background-color: #eee;
+ height: 2em;
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.25) inset;
+ margin-top: 1em;
+ margin-bottom: 2em;
+}
+
+.usage-element {
+ background-color: #5cb85c;
+ box-shadow: -1px 0 0 0 white;
+ height: 2em;
+ display: inline-block;
+ transition: all 0.3s ease 0s;
+}
+
+.usage-element-highlight {
+ background-color: $brand-warning;
+}
+
+.usage-element-others {
+ background-color: $brand-info;
+}
+
+.usage-element:hover {
+ opacity: 0.6;
+}
+
+// Limits
+
+.limits {
+ @extend .progress;
+ background-color: #eee;
+ height: 1.1em;
+ width: 100%;
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.25) inset;
+}
+
+.limits-element {
+ background-color: #5cb85c;
+ height: 100%;
+ display: inline-block;
+ transition: all 0.3s ease 0s;
+}
+
+.limits-used {
+ @extend .limits-element;
+ background-color: $brand-primary;
+}
+
+.limits-to-be-used {
+ @extend .limits-element;
+ background-color: $brand-success;
+}
+
+.limits-over-used {
+ @extend .limits-element;
+ background-color: $brand-danger;
+}
diff --git a/packages/xo-web/src/xo-app/about/index.js b/packages/xo-web/src/xo-app/about/index.js
new file mode 100644
index 000000000..21f30db66
--- /dev/null
+++ b/packages/xo-web/src/xo-app/about/index.js
@@ -0,0 +1,158 @@
+import _ from 'intl'
+import Component from 'base-component'
+import Copiable from 'copiable'
+import Icon from 'icon'
+import Link from 'link'
+import Page from '../page'
+import React from 'react'
+import { getUser } from 'selectors'
+import { serverVersion } from 'xo'
+import { Container, Row, Col } from 'grid'
+import { connectStore, getXoaPlan } from 'utils'
+
+import pkg from '../../../package'
+
+const HEADER = (
+
+
+
+
+ {' '}
+ {_('aboutXoaPlan', { xoaPlan: getXoaPlan() })}
+
+
+
+
+)
+
+@connectStore(() => ({
+ user: getUser,
+}))
+export default class About extends Component {
+ componentWillMount () {
+ serverVersion.then(serverVersion => {
+ this.setState({ serverVersion })
+ })
+ }
+ render () {
+ const { user } = this.props
+ const isAdmin = user && user.permission === 'admin'
+
+ return (
+
+
+ {isAdmin && (
+
+
+
+
+ xo-server {this.state.serverVersion || 'unknown'}
+
+ {_('xenOrchestraServer')}
+
+
+
+
+ xo-web {pkg.version}
+
+ {_('xenOrchestraWeb')}
+
+
+ )}
+ {process.env.XOA_PLAN > 4 ? (
+
+ ) : +process.env.XOA_PLAN === 1 ? (
+
+ ) : (
+
+ )}
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/backup/edit/index.js b/packages/xo-web/src/xo-app/backup/edit/index.js
new file mode 100644
index 000000000..22f354269
--- /dev/null
+++ b/packages/xo-web/src/xo-app/backup/edit/index.js
@@ -0,0 +1,32 @@
+import _ from 'intl'
+import Component from 'base-component'
+import React from 'react'
+import { getJob, getSchedule } from 'xo'
+
+import New from '../new'
+
+export default class Edit extends Component {
+ componentWillMount () {
+ const { id } = this.props.routeParams
+
+ if (id == null) {
+ return
+ }
+
+ getSchedule(id).then(schedule => {
+ getJob(schedule.job).then(job => {
+ this.setState({ job, schedule })
+ })
+ })
+ }
+
+ render () {
+ const { job, schedule } = this.state
+
+ if (!job || !schedule) {
+ return {_('statusLoading')}
+ }
+
+ return
+ }
+}
diff --git a/packages/xo-web/src/xo-app/backup/file-restore/index.css b/packages/xo-web/src/xo-app/backup/file-restore/index.css
new file mode 100644
index 000000000..5bce73e1e
--- /dev/null
+++ b/packages/xo-web/src/xo-app/backup/file-restore/index.css
@@ -0,0 +1,3 @@
+.listRestoreBackupInfos {
+ list-style-type: none;
+}
diff --git a/packages/xo-web/src/xo-app/backup/file-restore/index.js b/packages/xo-web/src/xo-app/backup/file-restore/index.js
new file mode 100644
index 000000000..5a5bd2ff8
--- /dev/null
+++ b/packages/xo-web/src/xo-app/backup/file-restore/index.js
@@ -0,0 +1,176 @@
+import _ from 'intl'
+import Component from 'base-component'
+import Icon from 'icon'
+import React from 'react'
+import SortedTable from 'sorted-table'
+import Upgrade from 'xoa-upgrade'
+import { confirm } from 'modal'
+import { addSubscriptions, noop } from 'utils'
+import { Container, Row, Col } from 'grid'
+import { error } from 'notification'
+import { FormattedDate } from 'react-intl'
+import {
+ find,
+ filter,
+ forEach,
+ groupBy,
+ isEmpty,
+ map,
+ mapValues,
+ reduce,
+ uniq,
+} from 'lodash'
+import { fetchFiles, listRemoteBackups, subscribeRemotes } from 'xo'
+
+import RestoreFileModalBody from './restore-file-modal'
+import styles from './index.css'
+
+const VM_COLUMNS = [
+ {
+ name: _('backupVmNameColumn'),
+ itemRenderer: ({ last }) => last.name,
+ sortCriteria: ({ last }) => last.name,
+ },
+ {
+ name: _('backupTags'),
+ itemRenderer: ({ tagsByRemote }) => (
+
+ {map(tagsByRemote, ({ tags, remoteName }) => (
+
+
+ {remoteName}
+
+ {tags.join(', ')}
+
+ ))}
+
+ ),
+ },
+ {
+ name: _('lastBackupColumn'),
+ itemRenderer: ({ last }) => (
+
+ ),
+ sortCriteria: ({ last }) => last.datetime,
+ sortOrder: 'desc',
+ },
+ {
+ name: _('availableBackupsColumn'),
+ itemRenderer: ({ count }) => {count} ,
+ sortCriteria: ({ count }) => count,
+ },
+]
+
+const openImportModal = ({ backups }) =>
+ confirm({
+ title: _('restoreFilesFromBackup', { name: backups[0].name }),
+ body: ,
+ }).then(({ remote, disk, partition, paths, format }) => {
+ if (!remote || !disk || !paths || !paths.length) {
+ return error(_('restoreFiles'), _('restoreFilesError'))
+ }
+ return fetchFiles(remote, disk, partition, paths, format)
+ }, noop)
+
+const _listAllBackups = async remotes => {
+ const remotesBackups = await Promise.all(
+ map(remotes, remote => listRemoteBackups(remote))
+ )
+
+ const backupsByVm = {}
+ forEach(remotesBackups, (backups, index) => {
+ forEach(backups, backup => {
+ if (backup.disks) {
+ const remote = remotes[index]
+
+ backupsByVm[backup.name] || (backupsByVm[backup.name] = [])
+ backupsByVm[backup.name].push({
+ ...backup,
+ remoteId: remote.id,
+ remoteName: remote.name,
+ })
+ }
+ })
+ })
+
+ const backupInfoByVm = mapValues(backupsByVm, backups => ({
+ backups,
+ count: backups.length,
+ last: reduce(backups, (last, b) => (b.datetime > last.datetime ? b : last)),
+ tagsByRemote: mapValues(
+ groupBy(backups, 'remoteId'),
+ (backups, remoteId) => ({
+ remoteName: find(remotes, remote => remote.id === remoteId).name,
+ tags: uniq(map(backups, 'tag')),
+ })
+ ),
+ }))
+
+ return backupInfoByVm
+}
+
+@addSubscriptions({
+ backupInfoByVm: cb =>
+ subscribeRemotes(remotes =>
+ _listAllBackups(filter(remotes, 'enabled')).then(cb)
+ ),
+})
+export default class FileRestore extends Component {
+ render () {
+ const { backupInfoByVm } = this.props
+
+ if (!backupInfoByVm) {
+ return {_('statusLoading')}
+ }
+
+ return process.env.XOA_PLAN > 3 ? (
+
+ {_('restoreFiles')}
+ {isEmpty(backupInfoByVm) ? (
+
+
+ {_('restoreDeltaBackupsInfo')}
+
+
+
+ ) : (
+
+
+
+
+ {_('restoreBackupsInfo')}
+
+
+
+
+ {_('restoreDeltaBackupsInfo')}
+
+
+
+
+
+
+ )}
+
+ ) : (
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/backup/file-restore/restore-file-modal.js b/packages/xo-web/src/xo-app/backup/file-restore/restore-file-modal.js
new file mode 100644
index 000000000..8fd6cd292
--- /dev/null
+++ b/packages/xo-web/src/xo-app/backup/file-restore/restore-file-modal.js
@@ -0,0 +1,392 @@
+import _ from 'intl'
+import ActionButton from 'action-button'
+import Component from 'base-component'
+import endsWith from 'lodash/endsWith'
+import Icon from 'icon'
+import React from 'react'
+import replace from 'lodash/replace'
+import Select from 'form/select'
+import Tooltip from 'tooltip'
+import { Container, Col, Row } from 'grid'
+import { createSelector } from 'reselect'
+import { formatSize } from 'utils'
+import { FormattedDate } from 'react-intl'
+import { filter, includes, isEmpty, map } from 'lodash'
+import { scanDisk, scanFiles } from 'xo'
+
+const backupOptionRenderer = backup => (
+
+ {backup.tag} - {backup.remoteName} ( )
+
+)
+
+const partitionOptionRenderer = partition => (
+
+ {partition.name} {partition.type}{' '}
+ {partition.size && `(${formatSize(+partition.size)})`}
+
+)
+
+const diskOptionRenderer = disk => {disk.name}
+
+const fileOptionRenderer = file => {file.name}
+
+const formatFilesOptions = (rawFiles, path) => {
+ const files =
+ path !== '/'
+ ? [
+ {
+ name: '..',
+ id: '..',
+ path: getParentPath(path),
+ content: {},
+ },
+ ]
+ : []
+
+ return files.concat(
+ map(rawFiles, (file, name) => ({
+ name,
+ id: `${path}${name}`,
+ path: `${path}${name}`,
+ content: file,
+ }))
+ )
+}
+
+const getParentPath = path => replace(path, /^(\/+.+)*(\/+.+)/, '$1/')
+
+// -----------------------------------------------------------------------------
+
+export default class RestoreFileModalBody extends Component {
+ state = {
+ format: 'zip',
+ }
+
+ get value () {
+ const { state } = this
+
+ return {
+ disk: state.disk,
+ format: state.format,
+ partition: state.partition,
+ paths: state.selectedFiles && map(state.selectedFiles, 'path'),
+ remote: state.backup.remoteId,
+ }
+ }
+
+ _scanFiles = () => {
+ const { backup, disk, partition, path } = this.state
+ this.setState({ scanningFiles: true })
+
+ return scanFiles(backup.remoteId, disk, path, partition).then(
+ rawFiles =>
+ this.setState({
+ files: formatFilesOptions(rawFiles, path),
+ scanningFiles: false,
+ scanFilesError: false,
+ }),
+ error => {
+ this.setState({
+ scanningFiles: false,
+ scanFilesError: true,
+ })
+ throw error
+ }
+ )
+ }
+
+ _getSelectableFiles = createSelector(
+ () => this.state.files,
+ () => this.state.selectedFiles,
+ (available, selected) =>
+ filter(available, file => !includes(selected, file))
+ )
+
+ _onBackupChange = backup => {
+ this.setState({
+ backup,
+ disk: undefined,
+ partition: undefined,
+ file: undefined,
+ selectedFiles: undefined,
+ scanDiskError: false,
+ scanFilesError: false,
+ })
+ }
+
+ _onDiskChange = disk => {
+ this.setState({
+ partition: undefined,
+ file: undefined,
+ selectedFiles: undefined,
+ scanDiskError: false,
+ scanFilesError: false,
+ })
+
+ if (!disk) {
+ return
+ }
+
+ scanDisk(this.state.backup.remoteId, disk).then(
+ ({ partitions }) => {
+ if (isEmpty(partitions)) {
+ this.setState(
+ {
+ disk,
+ path: '/',
+ },
+ this._scanFiles
+ )
+
+ return
+ }
+
+ this.setState({
+ disk,
+ partitions,
+ })
+ },
+ error => {
+ this.setState({
+ disk,
+ scanDiskError: true,
+ })
+ throw error
+ }
+ )
+ }
+
+ _onPartitionChange = partition => {
+ this.setState(
+ {
+ partition,
+ path: '/',
+ file: undefined,
+ selectedFiles: undefined,
+ },
+ partition && this._scanFiles
+ )
+ }
+
+ _onFileChange = file => {
+ if (file == null) {
+ return
+ }
+
+ // Ugly workaround to keep the ReactSelect open after selecting a folder
+ // FIXME: Remove and use isOpen/alwaysOpen prop once one of these issues is fixed:
+ // https://github.com/JedWatson/react-select/issues/662 -> /pull/817
+ // https://github.com/JedWatson/react-select/issues/962 -> /pull/1015
+ const select = document.activeElement
+ select.blur()
+ select.focus()
+
+ const isFile = file.id !== '..' && !endsWith(file.path, '/')
+ if (isFile) {
+ const { selectedFiles } = this.state
+ if (!includes(selectedFiles, file)) {
+ this.setState({
+ selectedFiles: (selectedFiles || []).concat(file),
+ })
+ }
+ } else {
+ this.setState(
+ {
+ path: file.id === '..' ? getParentPath(this.state.path) : file.path,
+ },
+ this._scanFiles
+ )
+ }
+ }
+
+ _unselectFile = file => {
+ this.setState({
+ selectedFiles: filter(
+ this.state.selectedFiles,
+ ({ id }) => id !== file.id
+ ),
+ })
+ }
+
+ _unselectAllFiles = () => {
+ this.setState({
+ selectedFiles: undefined,
+ })
+ }
+
+ _selectAllFolderFiles = () => {
+ this.setState({
+ selectedFiles: (this.state.selectedFiles || []).concat(
+ filter(this._getSelectableFiles(), ({ path }) => !endsWith(path, '/'))
+ ),
+ })
+ }
+
+ // ---------------------------------------------------------------------------
+
+ render () {
+ const { backups } = this.props
+ const {
+ backup,
+ disk,
+ format,
+ partition,
+ partitions,
+ path,
+ scanDiskError,
+ scanFilesError,
+ scanningFiles,
+ selectedFiles,
+ } = this.state
+ const noPartitions = isEmpty(partitions)
+
+ return (
+
+
+ {backup && [
+
,
+
,
+ ]}
+ {scanDiskError && (
+
+ {_('restoreFilesDiskError')}
+
+ )}
+ {disk &&
+ !scanDiskError &&
+ !noPartitions && [
+
,
+
,
+ ]}
+ {(partition || (disk && !scanDiskError && noPartitions)) && [
+
,
+
+
+
+
+ {path} {scanningFiles && }
+ {scanFilesError && }
+
+
+
+
+
+
+
+
+
+
+ ,
+
,
+
,
+
+
+ {' '}
+ ZIP
+
+
+ {' '}
+ TAR
+
+
,
+
,
+ selectedFiles && selectedFiles.length ? (
+
+
+
+
+ {_('restoreFilesSelectedFiles', {
+ files: selectedFiles.length,
+ })}
+
+
+
+
+
+
+ {map(selectedFiles, file => (
+
+
+ {file.path}
+
+
+
+
+
+ ))}
+
+ ) : (
+
{_('restoreFilesNoFilesSelected')}
+ ),
+ ]}
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/backup/index.js b/packages/xo-web/src/xo-app/backup/index.js
new file mode 100644
index 000000000..22dd25a76
--- /dev/null
+++ b/packages/xo-web/src/xo-app/backup/index.js
@@ -0,0 +1,56 @@
+import _ from 'intl'
+import Icon from 'icon'
+import Page from '../page'
+import React from 'react'
+import { routes } from 'utils'
+import { Container, Row, Col } from 'grid'
+import { NavLink, NavTabs } from 'nav'
+
+import Edit from './edit'
+import New from './new'
+import Overview from './overview'
+import Restore from './restore'
+import FileRestore from './file-restore'
+
+const HEADER = (
+
+
+
+
+ {_('backupPage')}
+
+
+
+
+
+ {_('backupOverviewPage')}
+
+
+ {_('backupNewPage')}
+
+
+ {_('backupRestorePage')}
+
+
+ {' '}
+ {_('backupFileRestorePage')}
+
+
+
+
+
+)
+
+const Backup = routes('overview', {
+ ':id/edit': Edit,
+ new: New,
+ overview: Overview,
+ restore: Restore,
+ 'file-restore': FileRestore,
+})(({ children }) => (
+
+ {children}
+
+))
+
+export default Backup
diff --git a/packages/xo-web/src/xo-app/backup/new/index.js b/packages/xo-web/src/xo-app/backup/new/index.js
new file mode 100644
index 000000000..d7b9fd8ff
--- /dev/null
+++ b/packages/xo-web/src/xo-app/backup/new/index.js
@@ -0,0 +1,828 @@
+import _ from 'intl'
+import ActionButton from 'action-button'
+import Button from 'button'
+import Component from 'base-component'
+import GenericInput from 'json-schema-input'
+import getEventValue from 'get-event-value'
+import Icon from 'icon'
+import Link from 'link'
+import moment from 'moment-timezone'
+import PropTypes from 'prop-types'
+import React from 'react'
+import renderXoItem from 'render-xo-item'
+import Scheduler, { SchedulePreview } from 'scheduling'
+import Tooltip from 'tooltip'
+import uncontrollableInput from 'uncontrollable-input'
+import Upgrade from 'xoa-upgrade'
+import Wizard, { Section } from 'wizard'
+import { confirm } from 'modal'
+import { Card, CardBlock, CardHeader } from 'card'
+import { Container, Row, Col } from 'grid'
+import { createPredicate } from 'value-matcher'
+import { createSelector } from 'reselect'
+import { generateUiSchema } from 'xo-json-schema-input'
+import { SelectSubject } from 'select-objects'
+import { createGetObjectsOfType, getUser } from 'selectors'
+import { connectStore, EMPTY_OBJECT } from 'utils'
+import {
+ constructPattern,
+ destructPattern,
+ constructQueryString,
+} from 'smart-backup-pattern'
+import {
+ filter,
+ forEach,
+ isArray,
+ map,
+ mapValues,
+ noop,
+ pickBy,
+ startsWith,
+} from 'lodash'
+
+import { createJob, createSchedule, getRemote, editJob, editSchedule } from 'xo'
+
+// ===================================================================
+// FIXME: missing most of translation. Can't be done in a dumb way, some of the word are keyword for XO-Server parameters...
+
+const NO_SMART_SCHEMA = {
+ type: 'object',
+ properties: {
+ vms: {
+ type: 'array',
+ items: {
+ type: 'string',
+ 'xo:type': 'vm',
+ },
+ title: _('editBackupVmsTitle'),
+ description: 'Choose VMs to backup.', // FIXME: can't translate
+ },
+ },
+ required: ['vms'],
+}
+const NO_SMART_UI_SCHEMA = generateUiSchema(NO_SMART_SCHEMA)
+
+const SMART_SCHEMA = {
+ type: 'object',
+ properties: {
+ power_state: {
+ default: 'All', // FIXME: can't translate
+ enum: ['All', 'Running', 'Halted'], // FIXME: can't translate
+ title: _('editBackupSmartStatusTitle'),
+ description: 'The statuses of VMs to backup.', // FIXME: can't translate
+ },
+ $pool: {
+ type: 'object',
+ title: _('editBackupSmartPools'),
+ properties: {
+ not: {
+ type: 'boolean',
+ title: _('editBackupNot'),
+ description:
+ 'Toggle on to backup VMs that are NOT resident on these pools',
+ },
+ values: {
+ type: 'array',
+ items: {
+ type: 'string',
+ 'xo:type': 'pool',
+ },
+ title: _('editBackupSmartResidentOn'),
+ description: 'Not used if empty.', // FIXME: can't translate
+ },
+ },
+ },
+ tags: {
+ type: 'object',
+ title: _('editBackupSmartTags'),
+ properties: {
+ not: {
+ type: 'boolean',
+ title: _('editBackupNot'),
+ description: 'Toggle on to backup VMs that do NOT contain these tags',
+ },
+ values: {
+ type: 'array',
+ items: {
+ type: 'string',
+ 'xo:type': 'tag',
+ },
+ title: _('editBackupSmartTagsTitle'),
+ description:
+ 'VMs which contain at least one of these tags. Not used if empty.', // FIXME: can't translate
+ },
+ },
+ },
+ },
+ required: ['power_state', '$pool', 'tags'],
+}
+const SMART_UI_SCHEMA = generateUiSchema(SMART_SCHEMA)
+
+// ===================================================================
+
+const COMMON_SCHEMA = {
+ type: 'object',
+ properties: {
+ tag: {
+ type: 'string',
+ title: _('editBackupTagTitle'),
+ description: 'Back-up tag.', // FIXME: can't translate
+ },
+ _reportWhen: {
+ default: 'failure',
+ enum: ['never', 'always', 'failure'], // FIXME: can't translate
+ title: _('editBackupReportTitle'),
+ description: [
+ 'When to send reports.',
+ '',
+ 'Plugins *tranport-email* and *backup-reports* need to be configured.',
+ ].join('\n'),
+ },
+ enabled: {
+ type: 'boolean',
+ title: _('editBackupScheduleEnabled'),
+ },
+ },
+ required: ['tag', 'vms', '_reportWhen'],
+}
+
+const RETENTION_PROPERTY = {
+ type: 'integer',
+ title: _('editBackupRetentionTitle'),
+ description: 'How many backups to rollover.', // FIXME: can't translate
+ min: 1,
+}
+
+const REMOTE_PROPERTY = {
+ type: 'string',
+ 'xo:type': 'remote',
+ title: _('editBackupRemoteTitle'),
+}
+
+const BACKUP_SCHEMA = {
+ type: 'object',
+ properties: {
+ ...COMMON_SCHEMA.properties,
+ retention: RETENTION_PROPERTY,
+ remoteId: REMOTE_PROPERTY,
+ compress: {
+ type: 'boolean',
+ title: 'Enable compression',
+ default: true,
+ },
+ },
+ required: COMMON_SCHEMA.required.concat(['retention', 'remoteId']),
+}
+
+const ROLLING_SNAPSHOT_SCHEMA = {
+ type: 'object',
+ properties: {
+ ...COMMON_SCHEMA.properties,
+ retention: RETENTION_PROPERTY,
+ },
+ required: COMMON_SCHEMA.required.concat('retention'),
+}
+
+const DELTA_BACKUP_SCHEMA = {
+ type: 'object',
+ properties: {
+ ...COMMON_SCHEMA.properties,
+ retention: RETENTION_PROPERTY,
+ remote: REMOTE_PROPERTY,
+ },
+ required: COMMON_SCHEMA.required.concat(['retention', 'remote']),
+}
+
+const DISASTER_RECOVERY_SCHEMA = {
+ type: 'object',
+ properties: {
+ ...COMMON_SCHEMA.properties,
+ retention: RETENTION_PROPERTY,
+ deleteOldBackupsFirst: {
+ type: 'boolean',
+ title: _('deleteOldBackupsFirst'),
+ description: [
+ 'Delete the old backups before copy the vms.',
+ '',
+ 'If the backup fails, you will lose your old backups.',
+ ].join('\n'),
+ },
+ sr: {
+ type: 'string',
+ 'xo:type': 'sr',
+ title: 'To SR',
+ },
+ },
+ required: COMMON_SCHEMA.required.concat(['retention', 'sr']),
+}
+
+const CONTINUOUS_REPLICATION_SCHEMA = {
+ type: 'object',
+ properties: {
+ ...COMMON_SCHEMA.properties,
+ retention: RETENTION_PROPERTY,
+ sr: {
+ type: 'string',
+ 'xo:type': 'sr',
+ title: 'To SR',
+ },
+ },
+ required: COMMON_SCHEMA.required.concat('sr'),
+}
+
+let REQUIRED_XOA_PLAN
+if (process.env.XOA_PLAN < 4) {
+ REQUIRED_XOA_PLAN = {
+ deltaBackup: 3,
+ disasterRecovery: 3,
+ continuousReplication: 4,
+ }
+}
+
+// ===================================================================
+
+const BACKUP_METHOD_TO_INFO = {
+ 'vm.rollingBackup': {
+ schema: BACKUP_SCHEMA,
+ uiSchema: generateUiSchema(BACKUP_SCHEMA),
+ label: 'backup',
+ icon: 'backup',
+ jobKey: 'rollingBackup',
+ method: 'vm.rollingBackup',
+ },
+ 'vm.rollingSnapshot': {
+ schema: ROLLING_SNAPSHOT_SCHEMA,
+ uiSchema: generateUiSchema(ROLLING_SNAPSHOT_SCHEMA),
+ label: 'rollingSnapshot',
+ icon: 'rolling-snapshot',
+ jobKey: 'rollingSnapshot',
+ method: 'vm.rollingSnapshot',
+ },
+ 'vm.rollingDeltaBackup': {
+ schema: DELTA_BACKUP_SCHEMA,
+ uiSchema: generateUiSchema(DELTA_BACKUP_SCHEMA),
+ label: 'deltaBackup',
+ icon: 'delta-backup',
+ jobKey: 'deltaBackup',
+ method: 'vm.rollingDeltaBackup',
+ },
+ 'vm.rollingDrCopy': {
+ schema: DISASTER_RECOVERY_SCHEMA,
+ uiSchema: generateUiSchema(DISASTER_RECOVERY_SCHEMA),
+ label: 'disasterRecovery',
+ icon: 'disaster-recovery',
+ jobKey: 'disasterRecovery',
+ method: 'vm.rollingDrCopy',
+ },
+ 'vm.deltaCopy': {
+ schema: CONTINUOUS_REPLICATION_SCHEMA,
+ uiSchema: generateUiSchema(CONTINUOUS_REPLICATION_SCHEMA),
+ label: 'continuousReplication',
+ icon: 'continuous-replication',
+ jobKey: 'continuousReplication',
+ method: 'vm.deltaCopy',
+ },
+}
+
+// ===================================================================
+
+const SAMPLE_SIZE_OF_MATCHING_VMS = 3
+
+@connectStore({
+ vms: createGetObjectsOfType('VM'),
+})
+class SmartBackupPreview extends Component {
+ static propTypes = {
+ pattern: PropTypes.object.isRequired,
+ }
+
+ _getMatchingVms = createSelector(
+ () => this.props.vms,
+ createSelector(
+ () => this.props.pattern,
+ pattern => createPredicate(pickBy(pattern, val => val != null))
+ ),
+ (vms, predicate) => filter(vms, predicate)
+ )
+
+ _getSampleOfMatchingVms = createSelector(this._getMatchingVms, vms =>
+ vms.slice(0, SAMPLE_SIZE_OF_MATCHING_VMS)
+ )
+
+ _getQueryString = createSelector(
+ () => this.props.pattern,
+ constructQueryString
+ )
+
+ render () {
+ const nMatchingVms = this._getMatchingVms().length
+ const sampleOfMatchingVms = this._getSampleOfMatchingVms()
+ const queryString = this._getQueryString()
+
+ return (
+
+ {_('sampleOfMatchingVms')}
+
+ {nMatchingVms === 0 ? (
+ {_('noMatchingVms')}
+ ) : (
+
+
+ {map(sampleOfMatchingVms, vm => (
+
+ {renderXoItem(vm)}
+
+ ))}
+
+
+
+
+ {_('allMatchingVms', {
+ icon: ,
+ nMatchingVms,
+ })}
+
+
+
+ )}
+
+
+ )
+ }
+}
+
+// ===================================================================
+
+@uncontrollableInput()
+class TimeoutInput extends Component {
+ _onChange = event => {
+ const value = getEventValue(event).trim()
+ this.props.onChange(value === '' ? null : +value * 1e3)
+ }
+
+ render () {
+ const { props } = this
+ const { value } = props
+
+ return (
+
+ )
+ }
+}
+
+// ===================================================================
+
+const DEFAULT_CRON_PATTERN = '0 0 * * *'
+const DEFAULT_TIMEZONE = moment.tz.guess()
+
+// xo-web v5.7.1 introduced a bug where an extra level
+// ({ id: { id: } }) was introduced for the VM param.
+//
+// This code automatically unbox the ids.
+const extractId = value => {
+ while (typeof value === 'object') {
+ value = value.id
+ }
+ return value
+}
+
+const normalizeMainParams = params => {
+ if (!('retention' in params)) {
+ const { depth, ...rest } = params
+ if (depth != null) {
+ params = rest
+ params.retention = depth
+ }
+ }
+ return params
+}
+
+@connectStore({
+ currentUser: getUser,
+})
+export default class New extends Component {
+ _getParams = createSelector(
+ () => this.props.job,
+ () => this.props.schedule,
+ (job, schedule) => {
+ if (!job) {
+ return { main: {}, vms: { vms: [] } }
+ }
+
+ const { items } = job.paramsVector
+ const enabled = schedule != null && schedule.enabled
+
+ // legacy backup jobs
+ if (items.length === 1) {
+ return {
+ main: normalizeMainParams({
+ enabled,
+ ...items[0].values[0],
+ }),
+ vms: { vms: map(items[0].values.slice(1), extractId) },
+ }
+ }
+
+ // smart backup
+ if (items[1].type === 'map') {
+ const { pattern } = items[1].collection
+ const { $pool, tags } = pattern
+
+ return {
+ main: normalizeMainParams({
+ enabled,
+ ...items[0].values[0],
+ }),
+ vms: {
+ $pool: destructPattern($pool),
+ power_state: pattern.power_state,
+ tags: destructPattern(tags, tags =>
+ map(tags, tag => (isArray(tag) ? tag[0] : tag))
+ ),
+ },
+ }
+ }
+
+ // normal backup
+ return {
+ main: normalizeMainParams({
+ enabled,
+ ...items[1].values[0],
+ }),
+ vms: { vms: map(items[0].values, extractId) },
+ }
+ }
+ )
+
+ _constructPattern = vms => ({
+ $pool: constructPattern(vms.$pool),
+ power_state: vms.power_state === 'All' ? undefined : vms.power_state,
+ tags: constructPattern(vms.tags, tags => map(tags, tag => [tag])),
+ type: 'VM',
+ })
+
+ _getMainParams = () => this.state.mainParams || this._getParams().main
+ _getVmsParam = () => this.state.vmsParam || this._getParams().vms
+
+ _getScheduling = createSelector(
+ () => this.props.schedule,
+ () => this.state.scheduling,
+ (schedule, scheduling) => {
+ if (scheduling !== undefined) {
+ return scheduling
+ }
+
+ const { cron = DEFAULT_CRON_PATTERN, timezone = DEFAULT_TIMEZONE } =
+ schedule || EMPTY_OBJECT
+
+ return {
+ cronPattern: cron,
+ timezone,
+ }
+ }
+ )
+
+ _handleSubmit = async () => {
+ const { props, state } = this
+
+ const method = this._getValue('job', 'method')
+ const backupInfo = BACKUP_METHOD_TO_INFO[method]
+
+ const { enabled, ...mainParams } = this._getMainParams()
+ const vms = this._getVmsParam()
+
+ const job = {
+ ...state.job,
+
+ type: 'call',
+ key: backupInfo.jobKey,
+ paramsVector: {
+ type: 'crossProduct',
+ items: isArray(vms.vms)
+ ? [
+ {
+ type: 'set',
+ values: map(vms.vms, vm => ({ id: extractId(vm) })),
+ },
+ {
+ type: 'set',
+ values: [mainParams],
+ },
+ ]
+ : [
+ {
+ type: 'set',
+ values: [mainParams],
+ },
+ {
+ type: 'map',
+ collection: {
+ type: 'fetchObjects',
+ pattern: this._constructPattern(vms),
+ },
+ iteratee: {
+ type: 'extractProperties',
+ mapping: { id: 'id' },
+ },
+ },
+ ],
+ },
+ }
+
+ const scheduling = this._getScheduling()
+
+ let remoteId
+ if (job.type === 'call') {
+ const { paramsVector } = job
+ if (paramsVector.type === 'crossProduct') {
+ const { items } = paramsVector
+ forEach(items, item => {
+ if (item.type === 'set') {
+ forEach(item.values, value => {
+ if (value.remoteId) {
+ remoteId = value.remoteId
+ return false
+ }
+ })
+ if (remoteId) {
+ return false
+ }
+ }
+ })
+ }
+ }
+
+ if (remoteId) {
+ const remote = await getRemote(remoteId)
+ if (startsWith(remote.url, 'file:')) {
+ await confirm({
+ title: _('localRemoteWarningTitle'),
+ body: _('localRemoteWarningMessage'),
+ })
+ }
+ }
+
+ // Update backup schedule.
+ const oldJob = props.job
+ if (oldJob) {
+ job.id = oldJob.id
+ await editJob(job)
+
+ return editSchedule({
+ id: props.schedule.id,
+ cron: scheduling.cronPattern,
+ enabled,
+ timezone: scheduling.timezone,
+ })
+ }
+
+ if (job.timeout === null) {
+ delete job.timeout // only needed for job edition
+ }
+
+ // Create backup schedule.
+ return createSchedule(await createJob(job), {
+ cron: scheduling.cronPattern,
+ enabled,
+ timezone: scheduling.timezone,
+ })
+ }
+
+ _handleReset = () => {
+ this.setState(mapValues(this.state, noop))
+ }
+
+ _handleSmartBackupMode = event => {
+ this.setState(
+ event.target.value === 'smart'
+ ? { vmsParam: {} }
+ : { vmsParam: { vms: [] } }
+ )
+ }
+
+ _subjectPredicate = ({ type, permission }) =>
+ type === 'user' && permission === 'admin'
+
+ _getValue = (ns, key, defaultValue) => {
+ let tmp
+
+ // look in the state
+ if ((tmp = this.state[ns]) != null && (tmp = tmp[key]) !== undefined) {
+ return tmp
+ }
+
+ // look in the props
+ if ((tmp = this.props[ns]) != null && (tmp = tmp[key]) !== undefined) {
+ return tmp
+ }
+
+ return defaultValue
+ }
+
+ render () {
+ const method = this._getValue('job', 'method', '')
+ const scheduling = this._getScheduling()
+ const vms = this._getVmsParam()
+
+ const backupInfo = BACKUP_METHOD_TO_INFO[method]
+ const smartBackupMode = !isArray(vms.vms)
+
+ return (
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/backup/overview/index.js b/packages/xo-web/src/xo-app/backup/overview/index.js
new file mode 100644
index 000000000..cd8900c6d
--- /dev/null
+++ b/packages/xo-web/src/xo-app/backup/overview/index.js
@@ -0,0 +1,267 @@
+import _ from 'intl'
+import ActionRowButton from 'action-row-button'
+import ButtonGroup from 'button-group'
+import Component from 'base-component'
+import Icon from 'icon'
+import Link from 'link'
+import LogList from '../../logs'
+import NoObjects from 'no-objects'
+import React from 'react'
+import SortedTable from 'sorted-table'
+import StateButton from 'state-button'
+import Tooltip from 'tooltip'
+import { addSubscriptions } from 'utils'
+import { constructQueryString } from 'smart-backup-pattern'
+import { createSelector } from 'selectors'
+import { Card, CardHeader, CardBlock } from 'card'
+import { filter, find, forEach, get, map, orderBy } from 'lodash'
+import {
+ deleteBackupSchedule,
+ disableSchedule,
+ enableSchedule,
+ runJob,
+ subscribeJobs,
+ subscribeSchedules,
+ subscribeScheduleTable,
+ subscribeUsers,
+} from 'xo'
+
+// ===================================================================
+
+const jobKeyToLabel = {
+ continuousReplication: _('continuousReplication'),
+ deltaBackup: _('deltaBackup'),
+ disasterRecovery: _('disasterRecovery'),
+ rollingBackup: _('backup'),
+ rollingSnapshot: _('rollingSnapshot'),
+}
+
+const JOB_COLUMNS = [
+ {
+ name: _('jobId'),
+ itemRenderer: ({ jobId }) => jobId.slice(4, 8),
+ sortCriteria: 'jobId',
+ },
+ {
+ name: _('jobType'),
+ itemRenderer: ({ jobLabel }) => jobLabel,
+ sortCriteria: 'jobLabel',
+ },
+ {
+ name: _('jobTag'),
+ itemRenderer: ({ scheduleTag }) => scheduleTag,
+ default: true,
+ sortCriteria: ({ scheduleTag }) => scheduleTag,
+ },
+ {
+ name: _('jobScheduling'),
+ itemRenderer: ({ schedule }) => schedule.cron,
+ sortCriteria: ({ schedule }) => schedule.cron,
+ },
+ {
+ name: _('jobTimezone'),
+ itemRenderer: ({ schedule }) => schedule.timezone || _('jobServerTimezone'),
+ sortCriteria: ({ schedule }) => schedule.timezone,
+ },
+ {
+ name: _('jobState'),
+ itemRenderer: ({ schedule, scheduleToggleValue }) => (
+
+ ),
+ sortCriteria: 'scheduleToggleValue',
+ },
+ {
+ name: _('jobAction'),
+ itemRenderer: ({ redirect, schedule }, isScheduleUserMissing) => (
+
+ {!isScheduleUserMissing[schedule.id] && (
+
+
+
+ )}
+
+
+
+
+ {redirect && (
+
+ )}
+
+
+
+
+ ),
+ textAlign: 'right',
+ },
+]
+
+// ===================================================================
+
+@addSubscriptions({
+ users: subscribeUsers,
+})
+export default class Overview extends Component {
+ static contextTypes = {
+ router: React.PropTypes.object,
+ }
+
+ constructor (props) {
+ super(props)
+ this.state = {
+ scheduleTable: {},
+ }
+ }
+
+ componentWillMount () {
+ const unsubscribeJobs = subscribeJobs(jobs => {
+ const obj = {}
+ forEach(jobs, job => {
+ obj[job.id] = job
+ })
+
+ this.setState({
+ jobs: obj,
+ })
+ })
+
+ const unsubscribeSchedules = subscribeSchedules(schedules => {
+ // Get only backup jobs.
+ schedules = filter(schedules, schedule => {
+ const job = this.state.jobs && this.state.jobs[schedule.job]
+ return job && jobKeyToLabel[job.key]
+ })
+
+ this.setState({
+ schedules: orderBy(schedules, schedule => +schedule.id.split(':')[1], [
+ 'desc',
+ ]),
+ })
+ })
+
+ const unsubscribeScheduleTable = subscribeScheduleTable(scheduleTable => {
+ this.setState({
+ scheduleTable,
+ })
+ })
+
+ this.componentWillUnmount = () => {
+ unsubscribeJobs()
+ unsubscribeSchedules()
+ unsubscribeScheduleTable()
+ }
+ }
+
+ _redirectToMatchingVms = pattern => {
+ this.context.router.push({
+ pathname: '/home',
+ query: { t: 'VM', s: constructQueryString(pattern) },
+ })
+ }
+
+ _getScheduleCollection = createSelector(
+ () => this.state.schedules,
+ () => this.state.scheduleTable,
+ () => this.state.jobs,
+ (schedules, scheduleTable, jobs) => {
+ if (!schedules || !jobs) {
+ return []
+ }
+
+ return map(schedules, schedule => {
+ const job = jobs[schedule.job]
+ const { items } = job.paramsVector
+ const pattern = get(items, '[1].collection.pattern')
+
+ return {
+ jobId: job.id,
+ jobLabel: jobKeyToLabel[job.key] || _('unknownSchedule'),
+ redirect:
+ pattern !== undefined &&
+ (() => this._redirectToMatchingVms(pattern)),
+ // Old versions of XenOrchestra use items[0]
+ scheduleTag:
+ get(items, '[0].values[0].tag') ||
+ get(items, '[1].values[0].tag') ||
+ schedule.id,
+ schedule,
+ scheduleToggleValue: scheduleTable && scheduleTable[schedule.id],
+ }
+ })
+ }
+ )
+
+ _getIsScheduleUserMissing = createSelector(
+ () => this.state.schedules,
+ () => this.state.jobs,
+ () => this.props.users,
+ (schedules, jobs, users) => {
+ const isScheduleUserMissing = {}
+ forEach(schedules, schedule => {
+ isScheduleUserMissing[schedule.id] = !!(
+ jobs && find(users, user => user.id === jobs[schedule.job].userId)
+ )
+ })
+
+ return isScheduleUserMissing
+ }
+ )
+
+ render () {
+ const { schedules } = this.state
+
+ const isScheduleUserMissing = this._getIsScheduleUserMissing()
+
+ return (
+
+
+
+ {_('backupSchedules')}
+
+
+
+ {() => (
+
+ )}
+
+
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/backup/restore/index.js b/packages/xo-web/src/xo-app/backup/restore/index.js
new file mode 100644
index 000000000..8615fba6e
--- /dev/null
+++ b/packages/xo-web/src/xo-app/backup/restore/index.js
@@ -0,0 +1,367 @@
+import _, { messages } from 'intl'
+import ChooseSrForEachVdisModal from 'xo/choose-sr-for-each-vdis-modal'
+import Component from 'base-component'
+import every from 'lodash/every'
+import filter from 'lodash/filter'
+import find from 'lodash/find'
+import forEach from 'lodash/forEach'
+import groupBy from 'lodash/groupBy'
+import Icon from 'icon'
+import isEmpty from 'lodash/isEmpty'
+import map from 'lodash/map'
+import mapValues from 'lodash/mapValues'
+import moment from 'moment'
+import React from 'react'
+import reduce from 'lodash/reduce'
+import SortedTable from 'sorted-table'
+import uniq from 'lodash/uniq'
+import Upgrade from 'xoa-upgrade'
+import { confirm } from 'modal'
+import { createSelector } from 'selectors'
+import { addSubscriptions, noop } from 'utils'
+import { Container, Row, Col } from 'grid'
+import { FormattedDate, injectIntl } from 'react-intl'
+import { info, error } from 'notification'
+import { Select, Toggle } from 'form'
+
+import {
+ importBackup,
+ importDeltaBackup,
+ isSrWritable,
+ listRemote,
+ listRemoteBackups,
+ startVm,
+ subscribeRemotes,
+} from 'xo'
+
+// Can 2 SRs on the same pool have 2 VDIs used by the same VM
+const areSrsCompatible = (sr1, sr2) =>
+ sr1.shared || sr2.shared || sr1.$container === sr2.$container
+
+const parseDate = date => +moment(date, 'YYYYMMDDTHHmmssZ').format('x')
+
+const backupOptionRenderer = backup => (
+
+ {backup.type === 'delta' && (
+
+ {_('delta')} {' '}
+
+ )}
+ {backup.tag} - {backup.remoteName} ( )
+
+)
+
+const VM_COLUMNS = [
+ {
+ name: _('backupVmNameColumn'),
+ itemRenderer: ({ last }) => last.name,
+ sortCriteria: ({ last }) => last.name,
+ },
+ {
+ name: _('backupTags'),
+ itemRenderer: ({ tagsByRemote }) => (
+
+ {map(tagsByRemote, ({ tags, remoteName }, key) => (
+
+
+ {remoteName}
+
+ {tags.join(', ')}
+
+ ))}
+
+ ),
+ },
+ {
+ name: _('lastBackupColumn'),
+ itemRenderer: ({ last }) => (
+
+ ),
+ sortCriteria: ({ last }) => last.date,
+ sortOrder: 'desc',
+ },
+ {
+ name: _('availableBackupsColumn'),
+ itemRenderer: ({ simpleCount, deltaCount }) => (
+
+ {!!simpleCount && (
+
+ {_('simpleBackup')}{' '}
+ {simpleCount}
+
+ )}
+ {!!simpleCount && !!deltaCount && ', '}
+ {!!deltaCount && (
+
+ {_('delta')}{' '}
+ {deltaCount}
+
+ )}
+
+ ),
+ },
+]
+
+const openImportModal = ({ backups }) =>
+ confirm({
+ title: _('importBackupModalTitle', { name: backups[0].name }),
+ body: ,
+ }).then(doImport)
+
+const doImport = ({ backup, targetSrs, start }) => {
+ if (targetSrs.mainSr === undefined || backup === undefined) {
+ error(_('backupRestoreErrorTitle'), _('backupRestoreErrorMessage'))
+ return
+ }
+ const importMethods = {
+ delta: importDeltaBackup,
+ simple: importBackup,
+ }
+ info(_('importBackupTitle'), _('importBackupMessage'))
+ try {
+ const importPromise = importMethods[backup.type]({
+ file: backup.path,
+ mapVdisSrs: targetSrs.mapVdisSrs,
+ remote: backup.remoteId,
+ sr: targetSrs.mainSr,
+ }).then(id => {
+ return id
+ })
+ if (start) {
+ importPromise.then(id => startVm({ id }))
+ }
+ } catch (err) {
+ error('VM import', err.message || String(err))
+ }
+}
+
+class _ModalBody extends Component {
+ state = {
+ targetSrs: {},
+ }
+
+ get value () {
+ return this.state
+ }
+
+ _getSrPredicate = createSelector(
+ () => this.state.targetSrs.mainSr,
+ () => this.state.targetSrs.mapVdisSrs,
+ (mainSr, mapVdisSrs) => sr =>
+ isSrWritable(sr) &&
+ mainSr.$pool === sr.$pool &&
+ areSrsCompatible(mainSr, sr) &&
+ every(
+ mapVdisSrs,
+ selectedSr => selectedSr == null || areSrsCompatible(selectedSr, sr)
+ )
+ )
+
+ _onSrsChange = props => {
+ const oldMainSr = this.state.targetSrs.mainSr
+ const newMainSr = props.mainSr
+
+ const targetSrs = { ...props }
+
+ // This code fixes the incompatibilities between the mapVdisSrs values
+ if (oldMainSr !== newMainSr) {
+ if (
+ oldMainSr == null ||
+ newMainSr == null ||
+ oldMainSr.$pool !== newMainSr.$pool
+ ) {
+ targetSrs.mapVdisSrs = {}
+ } else if (!newMainSr.shared) {
+ forEach(targetSrs.mapVdisSrs, (sr, vdi) => {
+ if (
+ sr != null &&
+ newMainSr !== sr &&
+ sr.$container !== newMainSr.$container &&
+ !sr.shared
+ ) {
+ delete targetSrs.mapVdisSrs[vdi]
+ }
+ })
+ }
+ }
+
+ this.setState({ targetSrs })
+ }
+
+ render () {
+ const { props, state } = this
+ const vdis = state.backup && state.backup.vdis
+
+ return (
+
+
+
+
+
+ {' '}
+ {_('importBackupModalStart')}
+
+ )
+ }
+}
+
+const ImportModalBody = injectIntl(_ModalBody, { withRef: true })
+
+@addSubscriptions({
+ rawRemotes: subscribeRemotes,
+})
+export default class Restore extends Component {
+ componentWillReceiveProps ({ rawRemotes }) {
+ let filteredRemotes
+ if (
+ (filteredRemotes = filter(rawRemotes, 'enabled')) !==
+ filter(this.props.rawRemotes, 'enabled')
+ ) {
+ this._listAll(filteredRemotes).catch(noop)
+ }
+ }
+
+ _listAll = async remotes => {
+ const remotesInfo = await Promise.all(
+ map(remotes, async remote => ({
+ files: await listRemote(remote.id),
+ backupsInfo: await listRemoteBackups(remote.id),
+ }))
+ )
+
+ const backupInfoByVm = {}
+
+ forEach(remotesInfo, (remoteInfo, index) => {
+ const remote = remotes[index]
+
+ forEach(remoteInfo.files, file => {
+ let backup
+ const deltaInfo = /^vm_delta_(.*)_([^/]+)\/([^_]+)_(.*)$/.exec(file)
+
+ if (deltaInfo) {
+ const [, tag, id, date, name] = deltaInfo
+ const vdis = find(remoteInfo.backupsInfo, {
+ id: `${file}.json`,
+ }).disks
+
+ backup = {
+ type: 'delta',
+ date: parseDate(date),
+ id,
+ name,
+ path: file,
+ tag,
+ remoteId: remote.id,
+ remoteName: remote.name,
+ vdis,
+ }
+ } else {
+ const backupInfo = /^([^_]+)_([^_]+)_(.*)\.xva$/.exec(file)
+ if (backupInfo) {
+ const [, date, tag, name] = backupInfo
+ backup = {
+ type: 'simple',
+ date: parseDate(date),
+ name,
+ path: file,
+ tag,
+ remoteId: remote.id,
+ remoteName: remote.name,
+ }
+ }
+ }
+ if (backup) {
+ backupInfoByVm[backup.name] || (backupInfoByVm[backup.name] = [])
+ backupInfoByVm[backup.name].push(backup)
+ }
+ })
+ })
+ forEach(backupInfoByVm, (backups, vm) => {
+ backupInfoByVm[vm] = {
+ backups,
+ last: reduce(backups, (last, b) => (b.date > last.date ? b : last)),
+ tagsByRemote: mapValues(
+ groupBy(backups, 'remoteId'),
+ (backups, remoteId) => ({
+ remoteName: find(remotes, remote => remote.id === remoteId).name,
+ tags: uniq(map(backups, 'tag')),
+ })
+ ),
+ simpleCount: reduce(
+ backups,
+ (sum, b) => (b.type === 'simple' ? ++sum : sum),
+ 0
+ ),
+ deltaCount: reduce(
+ backups,
+ (sum, b) => (b.type === 'delta' ? ++sum : sum),
+ 0
+ ),
+ }
+ })
+ this.setState({ backupInfoByVm })
+ }
+
+ render () {
+ const { backupInfoByVm } = this.state
+
+ if (!backupInfoByVm) {
+ return {_('statusLoading')}
+ }
+
+ return process.env.XOA_PLAN > 1 ? (
+
+ {_('restoreBackups')}
+ {isEmpty(backupInfoByVm) ? (
+ _('noBackup')
+ ) : (
+
+
+ {_('restoreBackupsInfo')}
+
+
+
+ )}
+
+ ) : (
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/dashboard/health/index.js b/packages/xo-web/src/xo-app/dashboard/health/index.js
new file mode 100644
index 000000000..4770672cc
--- /dev/null
+++ b/packages/xo-web/src/xo-app/dashboard/health/index.js
@@ -0,0 +1,605 @@
+import _ from 'intl'
+import ActionRowButton from 'action-row-button'
+import Component from 'base-component'
+import Icon from 'icon'
+import Link from 'link'
+import NoObjects from 'no-objects'
+import React from 'react'
+import SortedTable from 'sorted-table'
+import TabButton from 'tab-button'
+import Tooltip from 'tooltip'
+import Upgrade from 'xoa-upgrade'
+import xml2js from 'xml2js'
+import { Card, CardHeader, CardBlock } from 'card'
+import { confirm } from 'modal'
+import { Container, Row, Col } from 'grid'
+import { FormattedRelative, FormattedTime } from 'react-intl'
+import { fromCallback } from 'promise-toolbox'
+import {
+ deleteMessage,
+ deleteOrphanedVdis,
+ deleteVbd,
+ deleteVdi,
+ deleteVm,
+ isSrWritable,
+} from 'xo'
+import {
+ areObjectsFetched,
+ createCollectionWrapper,
+ createGetObject,
+ createGetObjectsOfType,
+ createSelector,
+} from 'selectors'
+import { flatten, get, map, mapValues } from 'lodash'
+import { connectStore, formatSize, mapPlus, noop } from 'utils'
+
+const SrColContainer = connectStore(() => ({
+ container: createGetObject(),
+}))(({ container }) => (
+ {container.name_label}
+))
+
+const VdiColSr = connectStore(() => ({
+ sr: createGetObject(),
+}))(({ sr }) => {sr.name_label})
+
+const VmColContainer = connectStore(() => ({
+ container: createGetObject(),
+}))(({ container }) => {container.name_label} )
+
+const AlarmColObject = connectStore(() => ({
+ object: createGetObject(),
+}))(({ object }) => {
+ if (!object) {
+ return null
+ }
+
+ switch (object.type) {
+ case 'VM':
+ return {object.name_label}
+ case 'VM-controller':
+ return {object.name_label}
+ case 'host':
+ return {object.name_label}
+ default:
+ return null
+ }
+})
+
+const AlarmColPool = connectStore(() => ({
+ pool: createGetObject(),
+}))(({ pool }) => {
+ if (!pool) {
+ return null
+ }
+ return {pool.name_label}
+})
+
+const SR_COLUMNS = [
+ {
+ name: _('srName'),
+ itemRenderer: sr => sr.name_label,
+ sortCriteria: sr => sr.name_label,
+ },
+ {
+ name: _('srPool'),
+ itemRenderer: sr => ,
+ },
+ {
+ name: _('srFormat'),
+ itemRenderer: sr => sr.SR_type,
+ sortCriteria: sr => sr.SR_type,
+ },
+ {
+ name: _('srSize'),
+ itemRenderer: sr => formatSize(sr.size),
+ sortCriteria: sr => sr.size,
+ },
+ {
+ default: true,
+ name: _('srUsage'),
+ itemRenderer: sr =>
+ sr.size > 1 && (
+
+
+
+ ),
+ sortCriteria: sr => sr.physical_usage / sr.size,
+ sortOrder: 'desc',
+ },
+]
+
+const ORPHANED_VDI_COLUMNS = [
+ {
+ name: _('snapshotDate'),
+ itemRenderer: vdi => (
+
+ {' '}
+ ( )
+
+ ),
+ sortCriteria: vdi => vdi.snapshot_time,
+ sortOrder: 'desc',
+ },
+ {
+ name: _('vdiNameLabel'),
+ itemRenderer: vdi => vdi.name_label,
+ sortCriteria: vdi => vdi.name_label,
+ },
+ {
+ name: _('vdiNameDescription'),
+ itemRenderer: vdi => vdi.name_description,
+ sortCriteria: vdi => vdi.name_description,
+ },
+ {
+ name: _('vdiSize'),
+ itemRenderer: vdi => formatSize(vdi.size),
+ sortCriteria: vdi => vdi.size,
+ },
+ {
+ name: _('vdiSr'),
+ itemRenderer: vdi => ,
+ },
+ {
+ name: _('logAction'),
+ itemRenderer: vdi => (
+
+ ),
+ },
+]
+
+const CONTROL_DOMAIN_VDI_COLUMNS = [
+ {
+ name: _('vdiNameLabel'),
+ itemRenderer: vdi => vdi && vdi.name_label,
+ sortCriteria: vdi => vdi && vdi.name_label,
+ },
+ {
+ name: _('vdiNameDescription'),
+ itemRenderer: vdi => vdi && vdi.name_description,
+ sortCriteria: vdi => vdi && vdi.name_description,
+ },
+ {
+ name: _('vdiPool'),
+ itemRenderer: vdi =>
+ vdi &&
+ vdi.pool && (
+ {vdi.pool.name_label}
+ ),
+ sortCriteria: vdi => vdi && vdi.pool && vdi.pool.name_label,
+ },
+ {
+ name: _('vdiSize'),
+ itemRenderer: vdi => vdi && formatSize(vdi.size),
+ sortCriteria: vdi => vdi && vdi.size,
+ },
+ {
+ name: _('vdiSr'),
+ itemRenderer: vdi =>
+ vdi && vdi.sr && {vdi.sr.name_label},
+ sortCriteria: vdi => vdi && vdi.sr && vdi.sr.name_label,
+ },
+ {
+ name: _('vdiAction'),
+ itemRenderer: vdi =>
+ vdi &&
+ vdi.vbd && (
+
+ ),
+ },
+]
+
+const VM_COLUMNS = [
+ {
+ name: _('snapshotDate'),
+ itemRenderer: vm => (
+
+ {' '}
+ ( )
+
+ ),
+ sortCriteria: vm => vm.snapshot_time,
+ sortOrder: 'desc',
+ },
+ {
+ name: _('vmNameLabel'),
+ itemRenderer: vm => vm.name_label,
+ sortCriteria: vm => vm.name_label,
+ },
+ {
+ name: _('vmNameDescription'),
+ itemRenderer: vm => vm.name_description,
+ sortCriteria: vm => vm.name_description,
+ },
+ {
+ name: _('vmContainer'),
+ itemRenderer: vm => ,
+ },
+ {
+ name: _('logAction'),
+ itemRenderer: vm => (
+
+ ),
+ },
+]
+
+const ALARM_COLUMNS = [
+ {
+ name: _('alarmDate'),
+ itemRenderer: message => (
+
+ {' '}
+ ( )
+
+ ),
+ sortCriteria: message => message.time,
+ sortOrder: 'desc',
+ },
+ {
+ name: _('alarmContent'),
+ itemRenderer: ({ formatted, body }) =>
+ formatted ? (
+
+
+
+ {formatted.name}
+
+ {formatted.value}
+
+
+ {map(formatted.alarmAttributes, (value, label) => (
+
+ {label}
+ {value}
+
+ ))}
+
+ ) : (
+ {body}
+ ),
+ sortCriteria: message => message.body,
+ },
+ {
+ name: _('alarmObject'),
+ itemRenderer: message => ,
+ },
+ {
+ name: _('alarmPool'),
+ itemRenderer: message => ,
+ },
+ {
+ name: _('logAction'),
+ itemRenderer: message => (
+
+ ),
+ },
+]
+
+@connectStore(() => {
+ const getOrphanVdiSnapshots = createGetObjectsOfType('VDI-snapshot')
+ .filter([_ => !_.$snapshot_of && _.$VBDs.length === 0])
+ .sort()
+ const getControlDomainVbds = createGetObjectsOfType('VBD')
+ .pick(
+ createSelector(
+ createGetObjectsOfType('VM-controller'),
+ createCollectionWrapper(vmControllers =>
+ flatten(map(vmControllers, '$VBDs'))
+ )
+ )
+ )
+ .sort()
+ const getControlDomainVdis = createSelector(
+ getControlDomainVbds,
+ createGetObjectsOfType('VDI'),
+ createGetObjectsOfType('pool'),
+ createGetObjectsOfType('SR'),
+ (vbds, vdis, pools, srs) =>
+ mapPlus(vbds, (vbd, push) => {
+ const vdi = vdis[vbd.VDI]
+
+ if (vdi == null) {
+ return
+ }
+
+ push({
+ ...vdi,
+ pool: pools[vbd.$pool],
+ sr: srs[vdi.$SR],
+ vbd,
+ })
+ })
+ )
+ const getOrphanVmSnapshots = createGetObjectsOfType('VM-snapshot')
+ .filter([snapshot => !snapshot.$snapshot_of])
+ .sort()
+ const getUserSrs = createGetObjectsOfType('SR').filter([isSrWritable])
+ const getVdiSrs = createGetObjectsOfType('SR').pick(
+ createSelector(getOrphanVdiSnapshots, snapshots => map(snapshots, '$SR'))
+ )
+ const getAlertMessages = createGetObjectsOfType('message').filter([
+ message => message.name === 'ALARM',
+ ])
+
+ return {
+ areObjectsFetched,
+ alertMessages: getAlertMessages,
+ controlDomainVdis: getControlDomainVdis,
+ userSrs: getUserSrs,
+ vdiOrphaned: getOrphanVdiSnapshots,
+ vdiSr: getVdiSrs,
+ vmOrphaned: getOrphanVmSnapshots,
+ }
+})
+export default class Health extends Component {
+ componentWillReceiveProps (props) {
+ if (props.alertMessages !== this.props.alertMessages) {
+ this._updateAlarms(props)
+ }
+ }
+
+ componentDidMount () {
+ this._updateAlarms(this.props)
+ }
+
+ _updateAlarms = props => {
+ Promise.all(
+ map(props.alertMessages, ({ body }, id) => {
+ const matches = /^value:\s*([0-9.]+)\s+config:\s*([^]*)$/.exec(body)
+ if (!matches) {
+ return
+ }
+
+ const [, value, xml] = matches
+ return fromCallback(cb => xml2js.parseString(xml, cb)).then(result => {
+ const object = mapValues(result && result.variable, value =>
+ get(value, '[0].$.value')
+ )
+ if (!object || !object.name) {
+ return
+ }
+
+ const { name, ...alarmAttributes } = object
+
+ return { name, value, alarmAttributes, id }
+ }, noop)
+ })
+ ).then(formattedMessages => {
+ this.setState({
+ messages: map(formattedMessages, ({ id, ...formattedMessage }) => ({
+ formatted: formattedMessage,
+ ...props.alertMessages[id],
+ })),
+ })
+ }, noop)
+ }
+
+ _deleteOrphanedVdis = () => deleteOrphanedVdis(this.props.vdiOrphaned)
+
+ _deleteAllLogs = () =>
+ confirm({
+ title: _('removeAllLogsModalTitle'),
+ body: (
+
+
{_('removeAllLogsModalWarning')}
+
{_('definitiveMessageModal')}
+
+ ),
+ }).then(
+ () => Promise.all(map(this.props.alertMessages, deleteMessage)),
+ noop
+ )
+
+ _getSrUrl = sr => `srs/${sr.id}`
+
+ render () {
+ const { props } = this
+
+ return process.env.XOA_PLAN > 3 ? (
+
+
+
+
+
+ {_('srStatePanel')}
+
+
+
+ {() => (
+
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+ {_('orphanedVdis')}
+
+
+
+ {() => (
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+ {_('vdisOnControlDomain')}
+
+
+
+
+
+
+
+
+
+
+
+ {_('orphanedVms')}
+
+
+
+
+
+
+
+
+
+
+
+ {_('alarmMessage')}
+
+
+
+ {() => (
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+
+
+
+
+
+ ) : (
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/dashboard/index.js b/packages/xo-web/src/xo-app/dashboard/index.js
new file mode 100644
index 000000000..ae4f9d638
--- /dev/null
+++ b/packages/xo-web/src/xo-app/dashboard/index.js
@@ -0,0 +1,56 @@
+import _ from 'intl'
+import Icon from 'icon'
+import Page from '../page'
+import React from 'react'
+import { routes } from 'utils'
+import { Container, Row, Col } from 'grid'
+import { NavLink, NavTabs } from 'nav'
+
+import Health from './health'
+import Overview from './overview'
+import Stats from './stats'
+import Visualizations from './visualizations'
+
+const HEADER = (
+
+
+
+
+ {_('dashboardPage')}
+
+
+
+
+
+ {_('overviewDashboardPage')}
+
+
+ {' '}
+ {_('overviewVisualizationDashboardPage')}
+
+
+ {' '}
+ {_('overviewStatsDashboardPage')}
+
+
+ {' '}
+ {_('overviewHealthDashboardPage')}
+
+
+
+
+
+)
+
+const Dashboard = routes('overview', {
+ health: Health,
+ overview: Overview,
+ stats: Stats,
+ visualizations: Visualizations,
+})(({ children }) => (
+
+ {children}
+
+))
+
+export default Dashboard
diff --git a/packages/xo-web/src/xo-app/dashboard/overview/index.css b/packages/xo-web/src/xo-app/dashboard/overview/index.css
new file mode 100644
index 000000000..463ea7b33
--- /dev/null
+++ b/packages/xo-web/src/xo-app/dashboard/overview/index.css
@@ -0,0 +1,4 @@
+.bigCardContent {
+ font-size: 4em;
+ text-align: center;
+}
diff --git a/packages/xo-web/src/xo-app/dashboard/overview/index.js b/packages/xo-web/src/xo-app/dashboard/overview/index.js
new file mode 100644
index 000000000..391e21a73
--- /dev/null
+++ b/packages/xo-web/src/xo-app/dashboard/overview/index.js
@@ -0,0 +1,450 @@
+import _, { messages } from 'intl'
+import ButtonGroup from 'button-group'
+import ChartistGraph from 'react-chartist'
+import Component from 'base-component'
+import HostsPatchesTable from 'hosts-patches-table'
+import Icon from 'icon'
+import Link, { BlockLink } from 'link'
+import PropTypes from 'prop-types'
+import React from 'react'
+import ResourceSetQuotas from 'resource-set-quotas'
+import Upgrade from 'xoa-upgrade'
+import { Card, CardBlock, CardHeader } from 'card'
+import { Container, Row, Col } from 'grid'
+import { forEach, isEmpty, map, size } from 'lodash'
+import { injectIntl } from 'react-intl'
+import {
+ createCollectionWrapper,
+ createCounter,
+ createGetObjectsOfType,
+ createGetHostMetrics,
+ createSelector,
+ createTop,
+ isAdmin,
+} from 'selectors'
+import { addSubscriptions, connectStore, formatSize } from 'utils'
+import {
+ isSrWritable,
+ subscribePermissions,
+ subscribeResourceSets,
+ subscribeUsers,
+} from 'xo'
+
+import styles from './index.css'
+
+// ===================================================================
+
+const PIE_GRAPH_OPTIONS = { donut: true, donutWidth: 40, showLabel: false }
+
+// ===================================================================
+
+class PatchesCard extends Component {
+ static propTypes = {
+ hosts: PropTypes.object.isRequired,
+ }
+
+ _getContainer = () => this.refs.container
+
+ render () {
+ return (
+
+
+ {_('update')}
+
+
+
+
+
+
+ )
+ }
+}
+
+@connectStore(() => {
+ const getHosts = createGetObjectsOfType('host')
+ const getVms = createGetObjectsOfType('VM')
+
+ const getHostMetrics = createGetHostMetrics(getHosts)
+
+ const writableSrs = createGetObjectsOfType('SR').filter([isSrWritable])
+
+ const getSrMetrics = createCollectionWrapper(
+ createSelector(writableSrs, writableSrs => {
+ const metrics = {
+ srTotal: 0,
+ srUsage: 0,
+ }
+ forEach(writableSrs, sr => {
+ metrics.srUsage += sr.physical_usage
+ metrics.srTotal += sr.size
+ })
+ return metrics
+ })
+ )
+ const getVmMetrics = createCollectionWrapper(
+ createSelector(getVms, vms => {
+ const metrics = {
+ vcpus: 0,
+ running: 0,
+ halted: 0,
+ other: 0,
+ }
+ forEach(vms, vm => {
+ if (vm.power_state === 'Running') {
+ metrics.running++
+ metrics.vcpus += vm.CPUs.number
+ } else if (vm.power_state === 'Halted') {
+ metrics.halted++
+ } else metrics.other++
+ })
+ return metrics
+ })
+ )
+ const getNumberOfAlarmMessages = createCounter(
+ createGetObjectsOfType('message'),
+ [message => message.name === 'ALARM']
+ )
+ const getNumberOfHosts = createCounter(getHosts)
+ const getNumberOfPools = createCounter(createGetObjectsOfType('pool'))
+ const getNumberOfTasks = createCounter(
+ createGetObjectsOfType('task').filter([task => task.status === 'pending'])
+ )
+ const getNumberOfVms = createCounter(getVms)
+
+ return {
+ hostMetrics: getHostMetrics,
+ hosts: getHosts,
+ nAlarmMessages: getNumberOfAlarmMessages,
+ nHosts: getNumberOfHosts,
+ nPools: getNumberOfPools,
+ nTasks: getNumberOfTasks,
+ nVms: getNumberOfVms,
+ srMetrics: getSrMetrics,
+ topWritableSrs: createTop(
+ writableSrs,
+ [sr => sr.physical_usage / sr.size],
+ 5
+ ),
+ vmMetrics: getVmMetrics,
+ }
+})
+@injectIntl
+class DefaultCard extends Component {
+ componentWillMount () {
+ this.componentWillUnmount = subscribeUsers(users => {
+ this.setState({ users })
+ })
+ }
+
+ render () {
+ const { props, state } = this
+ const users = state && state.users
+ const nUsers = size(users)
+
+ const { formatMessage } = props.intl
+
+ return (
+
+
+
+
+
+ {_('poolPanel', { pools: props.nPools })}
+
+
+
+ {props.nPools}
+
+
+
+
+
+
+
+ {_('hostPanel', { hosts: props.nHosts })}
+
+
+
+ {props.nHosts}
+
+
+
+
+
+
+
+ {_('vmPanel', { vms: props.nVms })}
+
+
+
+ {props.nVms}
+
+
+
+
+
+
+
+
+
+ {_('memoryStatePanel')}
+
+
+
+
+ {_('ofUsage', {
+ total: formatSize(props.hostMetrics.memoryTotal),
+ usage: formatSize(props.hostMetrics.memoryUsage),
+ })}
+
+
+
+
+
+
+
+ {_('cpuStatePanel')}
+
+
+
+
+
+ {_('ofCpusUsage', {
+ nCpus: props.hostMetrics.cpus,
+ nVcpus: props.vmMetrics.vcpus,
+ })}
+
+
+
+
+
+
+
+
+ {_('srUsageStatePanel')}
+
+
+
+
+
+
+ {_('ofUsage', {
+ total: formatSize(props.srMetrics.srTotal),
+ usage: formatSize(props.srMetrics.srUsage),
+ })}
+
+
+
+
+
+
+
+
+
+
+
+ {_('alarmMessage')}
+
+
+
+ 0 ? 'text-warning' : ''}
+ >
+ {props.nAlarmMessages}
+
+
+
+
+
+
+
+
+ {_('taskStatePanel')}
+
+
+
+ {props.nTasks}
+
+
+
+
+
+
+
+ {_('usersStatePanel')}
+
+
+
+ {props.isAdmin ? (
+ {nUsers}
+ ) : (
+
{nUsers}
+ )}
+
+
+
+
+
+
+
+
+
+ {_('vmStatePanel')}
+
+
+
+
+
+ {_('vmsStates', {
+ running: props.vmMetrics.running,
+ halted: props.vmMetrics.halted,
+ })}
+
+
+
+
+
+
+
+
+ {_('srTopUsageStatePanel')}
+
+
+
+ sr.physical_usage / sr.size * 100
+ ),
+ }}
+ options={{
+ showLabel: false,
+ showGrid: false,
+ distributeSeries: true,
+ high: 100,
+ }}
+ type='Bar'
+ />
+
+
+
+
+
+
+
+
+
+
+
+ )
+ }
+}
+
+// ===================================================================
+
+@addSubscriptions({
+ resourceSets: subscribeResourceSets,
+ permissions: subscribePermissions,
+})
+@connectStore({
+ isAdmin,
+})
+export default class Overview extends Component {
+ render () {
+ const { props } = this
+ const showResourceSets = !isEmpty(props.resourceSets) && !props.isAdmin
+ const authorized = !isEmpty(props.permissions) || props.isAdmin
+
+ if (!authorized && !showResourceSets) {
+ return {_('notEnoughPermissionsError')}
+ }
+
+ return (
+
+
+ {showResourceSets ? (
+ map(props.resourceSets, resourceSet => (
+
+
+
+ {resourceSet.name}
+
+
+
+
+
+
+ ))
+ ) : (
+
+ )}
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/dashboard/stats/index.js b/packages/xo-web/src/xo-app/dashboard/stats/index.js
new file mode 100644
index 000000000..7cb63a5d3
--- /dev/null
+++ b/packages/xo-web/src/xo-app/dashboard/stats/index.js
@@ -0,0 +1,434 @@
+import _ from 'intl'
+import ActionButton from 'action-button'
+import cloneDeep from 'lodash/cloneDeep'
+import Component from 'base-component'
+import forEach from 'lodash/forEach'
+import Icon from 'icon'
+import map from 'lodash/map'
+import propTypes from 'prop-types-decorator'
+import React from 'react'
+import renderXoItem from 'render-xo-item'
+import sortBy from 'lodash/sortBy'
+import Upgrade from 'xoa-upgrade'
+import XoWeekCharts from 'xo-week-charts'
+import XoWeekHeatmap from 'xo-week-heatmap'
+import { Container, Row, Col } from 'grid'
+import { error } from 'notification'
+import { SelectHostVm } from 'select-objects'
+import { createGetObjectsOfType } from 'selectors'
+import { connectStore, formatSize, mapPlus } from 'utils'
+import { fetchHostStats, fetchVmStats } from 'xo'
+
+// ===================================================================
+
+const computeMetricArray = (
+ stats,
+ { metricKey, metrics, objectId, timestampStart, valueRenderer }
+) => {
+ if (!stats) {
+ return
+ }
+
+ if (!metrics[metricKey]) {
+ metrics[metricKey] = {
+ key: metricKey,
+ renderer: valueRenderer,
+ values: {}, // Stats of all object for one metric.
+ }
+ }
+
+ // Stats of one object.
+ metrics[metricKey].values[objectId] = map(stats, (value, i) => ({
+ value: +value,
+ date: timestampStart + 3600000 * i,
+ }))
+}
+
+// ===================================================================
+
+const computeCpusMetric = (cpus, { objectId, ...params }) => {
+ forEach(cpus, (cpu, index) => {
+ computeMetricArray(cpu, {
+ metricKey: `CPU ${index}`,
+ objectId,
+ ...params,
+ })
+ })
+
+ const nCpus = cpus.length
+
+ if (!nCpus) {
+ return
+ }
+
+ const { metrics } = params
+ const cpusAvg = cloneDeep(metrics['CPU 0'].values[objectId])
+
+ for (let i = 1; i < nCpus; i++) {
+ forEach(metrics[`CPU ${i}`].values[objectId], (value, index) => {
+ cpusAvg[index].value += value.value
+ })
+ }
+
+ forEach(cpusAvg, value => {
+ value.value /= nCpus
+ })
+
+ const allCpusKey = 'All CPUs'
+
+ if (!metrics[allCpusKey]) {
+ metrics[allCpusKey] = {
+ key: allCpusKey,
+ values: {},
+ }
+ }
+
+ metrics[allCpusKey].values[objectId] = cpusAvg
+}
+
+const computeVifsMetric = (vifs, params) => {
+ forEach(vifs, (vifs, vifsType) => {
+ const rw = vifsType === 'rx' ? 'out' : 'in'
+
+ forEach(vifs, (vif, index) => {
+ computeMetricArray(vif, {
+ metricKey: `Network ${index} ${rw}`,
+ valueRenderer: formatSize,
+ ...params,
+ })
+ })
+ })
+}
+
+const computePifsMetric = (pifs, params) => {
+ forEach(pifs, (pifs, pifsType) => {
+ const rw = pifsType === 'rx' ? 'out' : 'in'
+
+ forEach(pifs, (pif, index) => {
+ computeMetricArray(pif, {
+ metricKey: `NIC ${index} ${rw}`,
+ valueRenderer: formatSize,
+ ...params,
+ })
+ })
+ })
+}
+
+const computeXvdsMetric = (xvds, params) => {
+ forEach(xvds, (xvds, xvdsType) => {
+ const rw = xvdsType === 'r' ? 'read' : 'write'
+
+ forEach(xvds, (xvd, index) => {
+ computeMetricArray(xvd, {
+ metricKey: `Disk ${index} ${rw}`,
+ valueRenderer: formatSize,
+ ...params,
+ })
+ })
+ })
+}
+
+const computeLoadMetric = (load, params) => {
+ computeMetricArray(load, {
+ metricKey: 'Load',
+ ...params,
+ })
+}
+
+const computeMemoryUsedMetric = (memoryUsed, params) => {
+ computeMetricArray(memoryUsed, {
+ metricKey: 'RAM used',
+ valueRenderer: formatSize,
+ ...params,
+ })
+}
+
+// ===================================================================
+
+const METRICS_LOADING = 1
+const METRICS_LOADED = 2
+
+const runningObjectsPredicate = object => object.power_state === 'Running'
+
+const STATS_TYPE_TO_COMPUTE_FNC = {
+ cpus: computeCpusMetric,
+ vifs: computeVifsMetric,
+ pifs: computePifsMetric,
+ xvds: computeXvdsMetric,
+ load: computeLoadMetric,
+ memoryUsed: computeMemoryUsedMetric,
+}
+
+@propTypes({
+ onChange: propTypes.func.isRequired,
+})
+@connectStore(() => {
+ const getRunningHosts = createGetObjectsOfType('host')
+ .filter([runningObjectsPredicate])
+ .sort()
+ const getRunningVms = createGetObjectsOfType('VM')
+ .filter([runningObjectsPredicate])
+ .sort()
+
+ return {
+ hosts: getRunningHosts,
+ vms: getRunningVms,
+ }
+})
+class SelectMetric extends Component {
+ constructor (props) {
+ super(props)
+ this.state = {
+ objects: [],
+ predicate: runningObjectsPredicate,
+ }
+ }
+
+ _handleSelection = objects => {
+ this.setState({
+ metricsState: undefined,
+ metrics: undefined,
+ objects,
+ predicate: objects.length
+ ? object =>
+ runningObjectsPredicate(object) && object.type === objects[0].type
+ : runningObjectsPredicate,
+ })
+ }
+
+ _resetSelection = () => {
+ this._handleSelection([])
+ }
+
+ _selectAllHosts = () => {
+ this.setState({
+ metricsState: undefined,
+ metrics: undefined,
+ objects: this.props.hosts,
+ predicate: object =>
+ runningObjectsPredicate(object) && object.type === 'host',
+ })
+ }
+
+ _selectAllVms = () => {
+ this.setState({
+ metricsState: undefined,
+ metrics: undefined,
+ objects: this.props.vms,
+ predicate: object =>
+ runningObjectsPredicate(object) && object.type === 'VM',
+ })
+ }
+
+ _validSelection = async () => {
+ this.setState({ metricsState: METRICS_LOADING })
+
+ const { objects } = this.state
+ const getStats =
+ (objects[0].type === 'host' && fetchHostStats) || fetchVmStats
+
+ const metrics = {}
+
+ await Promise.all(
+ map(objects, object => {
+ return getStats(object, 'hours')
+ .then(result => {
+ const { stats } = result
+
+ if (stats === undefined) {
+ throw new Error('No stats')
+ }
+
+ const params = {
+ metrics,
+ objectId: object.id,
+ timestampStart:
+ (result.endTimestamp - 3600 * (stats.memory.length - 1)) * 1000,
+ }
+
+ forEach(stats, (stats, type) => {
+ const fnc = STATS_TYPE_TO_COMPUTE_FNC[type]
+
+ if (fnc) {
+ fnc(stats, params)
+ }
+ })
+ })
+ .catch(() => {
+ error(
+ _('statsDashboardGenericErrorTitle'),
+
+ {_('statsDashboardGenericErrorMessage')}{' '}
+ {object.name_label || object.id}
+
+ )
+ })
+ })
+ )
+
+ this.setState({
+ metricsState: METRICS_LOADED,
+ metrics: sortBy(metrics, metric => metric.key),
+ })
+ }
+
+ _handleSelectedMetric = event => {
+ const { value } = event.target
+ const { state } = this
+
+ this.props.onChange(value !== '' && state.metrics[value], state.objects)
+ }
+
+ render () {
+ const { metricsState, metrics, objects, predicate } = this.state
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {_('statsDashboardSelectObjects')}
+
+
+
+
+ {metricsState === METRICS_LOADING ? (
+
+ {_('metricsLoading')}
+
+ ) : (
+ metricsState === METRICS_LOADED && (
+
+ {_('noSelectedMetric', message => (
+ {message}
+ ))}
+ {map(metrics, (metric, key) => (
+
+ {metric.key}
+
+ ))}
+
+ )
+ )}
+
+
+
+ )
+ }
+}
+
+// ===================================================================
+
+@propTypes({
+ metricRenderer: propTypes.func.isRequired,
+ title: propTypes.any.isRequired,
+})
+class MetricViewer extends Component {
+ _handleSelectedMetric = (selectedMetric, objects) => {
+ this.setState({ selectedMetric, objects })
+ }
+
+ render () {
+ const {
+ props: { metricRenderer, title },
+ state: { selectedMetric, objects },
+ } = this
+
+ return (
+
+
{title}
+
+
+ {selectedMetric && (
+
+
+
+ {map(objects, object =>
+ renderXoItem(object, { className: 'mr-1' })
+ )}
+
+
+
+ {metricRenderer(selectedMetric)}
+
+
+ )}
+
+ )
+ }
+}
+
+// ===================================================================
+
+const weekHeatmapRenderer = metric => (
+
+ {
+ forEach(arr, value => push(value))
+ })}
+ />
+
+
+)
+
+const weekChartsRenderer = metric => (
+ ({
+ data,
+ objectId: id,
+ }))}
+ valueRenderer={metric.renderer}
+ />
+)
+
+const Stats = () =>
+ process.env.XOA_PLAN > 2 ? (
+
+
+
+
+ ) : (
+
+
+
+ )
+
+export { Stats as default }
diff --git a/packages/xo-web/src/xo-app/dashboard/visualizations/index.js b/packages/xo-web/src/xo-app/dashboard/visualizations/index.js
new file mode 100644
index 000000000..080aaf309
--- /dev/null
+++ b/packages/xo-web/src/xo-app/dashboard/visualizations/index.js
@@ -0,0 +1,129 @@
+import Component from 'base-component'
+import React from 'react'
+import XoParallelChart from 'xo-parallel-chart'
+import forEach from 'lodash/forEach'
+import invoke from 'invoke'
+import map from 'lodash/map'
+import mapValues from 'lodash/mapValues'
+import Upgrade from 'xoa-upgrade'
+import { Container, Row, Col } from 'grid'
+import {
+ createFilter,
+ createGetObjectsOfType,
+ createPicker,
+ createSelector,
+} from 'selectors'
+import { connectStore, formatSize } from 'utils'
+
+// ===================================================================
+
+// Columns order is defined by the attributes declaration order.
+// FIXME translation
+const DATA_LABELS = {
+ nVCpus: 'vCPUs number',
+ ram: 'RAM quantity',
+ nVifs: 'VIF number',
+ nVdis: 'VDI number',
+ vdisSize: 'Total space',
+}
+
+const DATA_RENDERERS = {
+ ram: formatSize,
+ vdisSize: formatSize,
+}
+
+// ===================================================================
+
+@connectStore(() => {
+ const getVms = createGetObjectsOfType('VM')
+ const getVdisByVm = invoke(() => {
+ let current = {}
+ const getVdisByVmSelectors = createSelector(
+ vms => vms,
+ vms => {
+ const previous = current
+ current = {}
+
+ forEach(vms, vm => {
+ const { id } = vm
+ current[id] =
+ previous[id] ||
+ createPicker(
+ (vm, vbds, vdis) => vdis,
+ createSelector(
+ createFilter(createPicker((vm, vbds) => vbds, vm => vm.$VBDs), [
+ vbd => !vbd.is_cd_drive && vbd.attached,
+ ]),
+ vbds => map(vbds, vbd => vbd.VDI)
+ )
+ )
+ })
+
+ return current
+ }
+ )
+
+ return createSelector(
+ getVms,
+ createGetObjectsOfType('VBD'),
+ createGetObjectsOfType('VDI'),
+ (vms, vbds, vdis) =>
+ mapValues(getVdisByVmSelectors(vms), (getVdis, vmId) =>
+ getVdis(vms[vmId], vbds, vdis)
+ )
+ )
+ })
+
+ return {
+ vms: getVms,
+ vdisByVm: getVdisByVm,
+ }
+})
+export default class Visualizations extends Component {
+ _getData = createSelector(
+ () => this.props.vms,
+ () => this.props.vdisByVm,
+ (vms, vdisByVm) =>
+ map(vms, (vm, vmId) => {
+ let vdisSize = 0
+ let nVdis = 0
+
+ forEach(vdisByVm[vmId], vdi => {
+ vdisSize += vdi.size
+ nVdis++
+ })
+
+ return {
+ objectId: vmId,
+ label: vm.name_label,
+ data: {
+ nVCpus: vm.CPUs.number,
+ nVdis,
+ nVifs: vm.VIFs.length,
+ ram: vm.memory.size,
+ vdisSize,
+ },
+ }
+ })
+ )
+
+ render () {
+ return process.env.XOA_PLAN > 3 ? (
+
+
+
+
+
+
+
+ ) : (
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/home/host-item.js b/packages/xo-web/src/xo-app/home/host-item.js
new file mode 100644
index 000000000..f7bd10228
--- /dev/null
+++ b/packages/xo-web/src/xo-app/home/host-item.js
@@ -0,0 +1,252 @@
+import _ from 'intl'
+import Component from 'base-component'
+import Ellipsis, { EllipsisContainer } from 'ellipsis'
+import Icon from 'icon'
+import isEmpty from 'lodash/isEmpty'
+import Link, { BlockLink } from 'link'
+import map from 'lodash/map'
+import React from 'react'
+import SingleLineRow from 'single-line-row'
+import HomeTags from 'home-tags'
+import Tooltip from 'tooltip'
+import { Row, Col } from 'grid'
+import { Text } from 'editable'
+import {
+ addTag,
+ editHost,
+ fetchHostStats,
+ removeTag,
+ startHost,
+ stopHost,
+} from 'xo'
+import { connectStore, formatSizeShort, osFamily } from 'utils'
+import {
+ createDoesHostNeedRestart,
+ createGetObject,
+ createGetObjectsOfType,
+ createSelector,
+} from 'selectors'
+
+import MiniStats from './mini-stats'
+import styles from './index.css'
+
+@connectStore(() => ({
+ container: createGetObject((_, props) => props.item.$pool),
+ needsRestart: createDoesHostNeedRestart((_, props) => props.item),
+ nVms: createGetObjectsOfType('VM').count(
+ createSelector(
+ (_, props) => props.item.id,
+ hostId => obj => obj.$container === hostId
+ )
+ ),
+}))
+export default class HostItem extends Component {
+ get _isRunning () {
+ const host = this.props.item
+ return host && host.power_state === 'Running'
+ }
+
+ _addTag = tag => addTag(this.props.item.id, tag)
+ _fetchStats = () => fetchHostStats(this.props.item.id)
+ _removeTag = tag => removeTag(this.props.item.id, tag)
+ _setNameDescription = nameDescription =>
+ editHost(this.props.item, { name_description: nameDescription })
+ _setNameLabel = nameLabel =>
+ editHost(this.props.item, { name_label: nameLabel })
+ _start = () => startHost(this.props.item)
+ _stop = () => stopHost(this.props.item)
+ _toggleExpanded = () => this.setState({ expanded: !this.state.expanded })
+ _onSelect = () => this.props.onSelect(this.props.item.id)
+
+ render () {
+ const { item: host, container, expandAll, selected, nVms } = this.props
+ const toolTipContent =
+ host.power_state === `Running` && !host.enabled
+ ? `disabled`
+ : _(`powerState${host.power_state}`)
+ return (
+
+
+
+
+
+
+
+
+ {toolTipContent}
+ {' ('}
+ {map(host.current_operations)[0]}
+ {')'}
+
+ )
+ }
+ >
+ {!isEmpty(host.current_operations) ? (
+
+ ) : host.power_state === 'Running' && !host.enabled ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+ {container &&
+ host.id === container.master && (
+
+ {_('pillMaster')}
+
+ )}
+
+ {this.props.needsRestart && (
+
+
+
+
+
+ )}
+
+
+
+
+
+
+ {nVms}x {_('vmsTabName')}
+
+ }
+ >
+ {nVms > 0 ? (
+
+
+
+ ) : (
+
+ )}
+
+
+ {this._isRunning ? (
+
+
+
+
+
+
+
+ ) : (
+
+
+
+
+
+
+
+ )}
+
+ {' '}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {host.address}
+
+ {container && (
+
+
+ {container.name_label}
+
+
+ )}
+
+
+
+
+
+
+
+ {(this.state.expanded || expandAll) && (
+
+
+
+ {host.cpus.cores}x {' '}
+ {formatSizeShort(host.memory.size)} {' '}
+ v{host.version.substring(0, 3)}
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/home/index.css b/packages/xo-web/src/xo-app/home/index.css
new file mode 100644
index 000000000..d011e4501
--- /dev/null
+++ b/packages/xo-web/src/xo-app/home/index.css
@@ -0,0 +1,63 @@
+.itemRowHeader {
+ margin-bottom: 0.8em;
+}
+
+.itemContainer {
+ border: 1px solid #eee;
+ border-radius: .3em;
+}
+
+.itemContainerHeader {
+ padding: 0.5em;
+ background: #ededed;
+ border-bottom: 1px solid #eee;
+ margin: 0px;
+}
+
+.item {
+ padding: 0.5em;
+ border-bottom: 1px solid #eee;
+ white-space: nowrap;
+}
+
+.item:hover {
+ background: #f4f4f4;
+}
+
+.item:hover .itemActionButons {
+ opacity: 1;
+}
+
+.itemActionButons {
+ margin-right: 0.6em;
+ color: #999;
+ opacity: 0;
+}
+
+.itemActionButons:hover {
+ color: #111;
+}
+
+.itemExpanded {
+ color: #999;
+ font-size: 1em;
+ text-overflow: ellipsis;
+ white-space:nowrap;
+}
+
+.itemExpandButton {
+ cursor: pointer;
+}
+
+.itemExpandRow {
+ text-align: right;
+ padding-left: 0;
+}
+
+.selectObject {
+ width: 20em;
+}
+
+.highlight {
+ outline: 2px solid #366e98;
+}
diff --git a/packages/xo-web/src/xo-app/home/index.js b/packages/xo-web/src/xo-app/home/index.js
new file mode 100644
index 000000000..f16cc905a
--- /dev/null
+++ b/packages/xo-web/src/xo-app/home/index.js
@@ -0,0 +1,1118 @@
+import * as ComplexMatcher from 'complex-matcher'
+import * as homeFilters from 'home-filters'
+import _ from 'intl'
+import ActionButton from 'action-button'
+import Button from 'button'
+import CenterPanel from 'center-panel'
+import Component from 'base-component'
+import defined, { get } from 'xo-defined'
+import Icon from 'icon'
+import invoke from 'invoke'
+import Link from 'link'
+import Page from '../page'
+import Pagination from 'pagination'
+import propTypes from 'prop-types-decorator'
+import React from 'react'
+import Shortcuts from 'shortcuts'
+import SingleLineRow from 'single-line-row'
+import Tooltip from 'tooltip'
+import { Card, CardHeader, CardBlock } from 'card'
+import {
+ ceil,
+ debounce,
+ filter,
+ find,
+ forEach,
+ identity,
+ includes,
+ isEmpty,
+ isString,
+ keys,
+ map,
+ pick,
+ pickBy,
+ size,
+ some,
+} from 'lodash'
+import {
+ addCustomFilter,
+ copyVms,
+ deleteTemplates,
+ deleteVms,
+ disconnectAllHostsSrs,
+ emergencyShutdownHosts,
+ forgetSrs,
+ isSrShared,
+ migrateVms,
+ reconnectAllHostsSrs,
+ rescanSrs,
+ restartHosts,
+ restartHostsAgents,
+ restartVms,
+ snapshotVms,
+ startVms,
+ stopHosts,
+ stopVms,
+ subscribeResourceSets,
+ subscribeServers,
+ suspendVms,
+} from 'xo'
+import { Container, Row, Col } from 'grid'
+import {
+ SelectHost,
+ SelectPool,
+ SelectResourceSet,
+ SelectTag,
+} from 'select-objects'
+import { addSubscriptions, connectStore, noop } from 'utils'
+import {
+ areObjectsFetched,
+ createCounter,
+ createFilter,
+ createGetObjectsOfType,
+ createPager,
+ createSelector,
+ createSort,
+ getUser,
+ isAdmin,
+} from 'selectors'
+import {
+ DropdownButton,
+ MenuItem,
+ OverlayTrigger,
+ Popover,
+} from 'react-bootstrap-4/lib'
+
+import styles from './index.css'
+import HostItem from './host-item'
+import PoolItem from './pool-item'
+import VmItem from './vm-item'
+import TemplateItem from './template-item'
+import SrItem from './sr-item'
+
+const ITEMS_PER_PAGE = 20
+
+const OPTIONS = {
+ host: {
+ defaultFilter: 'power_state:running ',
+ filters: homeFilters.host,
+ mainActions: [
+ { handler: stopHosts, icon: 'host-stop', tooltip: _('stopHostLabel') },
+ {
+ handler: restartHostsAgents,
+ icon: 'host-restart-agent',
+ tooltip: _('restartHostAgent'),
+ },
+ {
+ handler: emergencyShutdownHosts,
+ icon: 'host-emergency-shutdown',
+ tooltip: _('emergencyModeLabel'),
+ },
+ {
+ handler: restartHosts,
+ icon: 'host-reboot',
+ tooltip: _('rebootHostLabel'),
+ },
+ ],
+ Item: HostItem,
+ showPoolsSelector: true,
+ sortOptions: [
+ { labelId: 'homeSortByName', sortBy: 'name_label', sortOrder: 'asc' },
+ {
+ labelId: 'homeSortByPowerstate',
+ sortBy: 'power_state',
+ sortOrder: 'desc',
+ },
+ { labelId: 'homeSortByRAM', sortBy: 'memory.size', sortOrder: 'desc' },
+ { labelId: 'homeSortByCpus', sortBy: 'CPUs.cpu_count', sortOrder: 'desc' },
+ ],
+ },
+ VM: {
+ defaultFilter: 'power_state:running ',
+ filters: homeFilters.VM,
+ mainActions: [
+ { handler: stopVms, icon: 'vm-stop', tooltip: _('stopVmLabel') },
+ { handler: startVms, icon: 'vm-start', tooltip: _('startVmLabel') },
+ { handler: restartVms, icon: 'vm-reboot', tooltip: _('rebootVmLabel') },
+ { handler: migrateVms, icon: 'vm-migrate', tooltip: _('migrateVmLabel') },
+ { handler: copyVms, icon: 'vm-copy', tooltip: _('copyVmLabel') },
+ ],
+ otherActions: [
+ {
+ handler: suspendVms,
+ icon: 'vm-suspend',
+ labelId: 'suspendVmLabel',
+ },
+ {
+ handler: restartVms,
+ icon: 'vm-force-reboot',
+ labelId: 'forceRebootVmLabel',
+ params: true,
+ },
+ {
+ handler: stopVms,
+ icon: 'vm-force-shutdown',
+ labelId: 'forceShutdownVmLabel',
+ params: true,
+ },
+ {
+ handler: snapshotVms,
+ icon: 'vm-snapshot',
+ labelId: 'snapshotVmLabel',
+ },
+ {
+ handler: deleteVms,
+ icon: 'vm-delete',
+ labelId: 'vmRemoveButton',
+ },
+ ],
+ Item: VmItem,
+ showPoolsSelector: true,
+ showHostsSelector: true,
+ showResourceSetsSelector: true,
+ sortOptions: [
+ { labelId: 'homeSortByCpus', sortBy: 'CPUs.number', sortOrder: 'desc' },
+ { labelId: 'homeSortByName', sortBy: 'name_label', sortOrder: 'asc' },
+ {
+ labelId: 'homeSortByPowerstate',
+ sortBy: 'power_state',
+ sortOrder: 'desc',
+ },
+ { labelId: 'homeSortByRAM', sortBy: 'memory.size', sortOrder: 'desc' },
+ {
+ labelId: 'homeSortVmsBySnapshots',
+ sortBy: 'snapshots.length',
+ sortOrder: 'desc',
+ },
+ ],
+ },
+ pool: {
+ defaultFilter: '',
+ filters: homeFilters.pool,
+ getActions: noop,
+ Item: PoolItem,
+ sortOptions: [
+ { labelId: 'homeSortByName', sortBy: 'name_label', sortOrder: 'asc' },
+ ],
+ },
+ 'VM-template': {
+ defaultFilter: '',
+ filters: homeFilters.vmTemplate,
+ mainActions: [
+ { handler: deleteTemplates, icon: 'delete', tooltip: _('templateDelete') },
+ ],
+ Item: TemplateItem,
+ showPoolsSelector: true,
+ sortOptions: [
+ { labelId: 'homeSortByName', sortBy: 'name_label', sortOrder: 'asc' },
+ { labelId: 'homeSortByRAM', sortBy: 'memory.size', sortOrder: 'desc' },
+ { labelId: 'homeSortByCpus', sortBy: 'CPUs.number', sortOrder: 'desc' },
+ ],
+ },
+ SR: {
+ defaultFilter: '',
+ filters: homeFilters.SR,
+ mainActions: [
+ { handler: rescanSrs, icon: 'refresh', tooltip: _('srRescan') },
+ {
+ handler: reconnectAllHostsSrs,
+ icon: 'sr-reconnect-all',
+ tooltip: _('srReconnectAll'),
+ },
+ {
+ handler: disconnectAllHostsSrs,
+ icon: 'sr-disconnect-all',
+ tooltip: _('srDisconnectAll'),
+ },
+ { handler: forgetSrs, icon: 'sr-forget', tooltip: _('srsForget') },
+ ],
+ Item: SrItem,
+ showPoolsSelector: true,
+ sortOptions: [
+ { labelId: 'homeSortByName', sortBy: 'name_label', sortOrder: 'asc' },
+ {
+ labelId: 'homeSortBySize',
+ sortBy: 'size',
+ sortOrder: 'desc',
+ default: true,
+ },
+ { labelId: 'homeSortByShared', sortBy: isSrShared, sortOrder: 'desc' },
+ {
+ labelId: 'homeSortByUsage',
+ sortBy: 'physical_usage',
+ sortOrder: 'desc',
+ },
+ { labelId: 'homeSortByType', sortBy: 'SR_type', sortOrder: 'asc' },
+ ],
+ },
+}
+
+const TYPES = {
+ VM: _('homeTypeVm'),
+ 'VM-template': _('homeTypeVmTemplate'),
+ host: _('homeTypeHost'),
+ pool: _('homeTypePool'),
+ SR: _('homeSrPage'),
+}
+
+const DEFAULT_TYPE = 'VM'
+
+@addSubscriptions({
+ noRegisteredServers: cb => subscribeServers(data => cb(isEmpty(data))),
+})
+@connectStore(() => {
+ const noServersConnected = invoke(
+ createGetObjectsOfType('host'),
+ hosts => state => isEmpty(hosts(state))
+ )
+
+ return {
+ areObjectsFetched,
+ noServersConnected,
+ }
+})
+@propTypes({
+ isAdmin: propTypes.bool.isRequired,
+ noResourceSets: propTypes.bool.isRequired,
+})
+class NoObjects_ extends Component {
+ render () {
+ const {
+ areObjectsFetched,
+ isAdmin,
+ noRegisteredServers,
+ noResourceSets,
+ noServersConnected,
+ } = this.props
+
+ if (!areObjectsFetched) {
+ return (
+
+
+
+
+
+ )
+ }
+
+ if (noServersConnected && isAdmin) {
+ return (
+
+
+ {_('homeWelcome')}
+
+
+
+
+ {noRegisteredServers
+ ? _('homeAddServer')
+ : _('homeConnectServer')}
+
+
+
+ {noRegisteredServers
+ ? _('homeWelcomeText')
+ : _('homeConnectServerText')}
+
+
+
+ {_('homeHelp')}
+
+
+
+
+ {_('homeOnlineDoc')}
+
+
+
+
+
+ {_('homeProSupport')}
+
+
+
+
+
+
+ )
+ }
+
+ return (
+
+
+ {_('homeNoVms')}
+ {(isAdmin || !noResourceSets) && (
+
+
+
+
+
+ {_('homeNewVm')}
+
+ {_('homeNewVmMessage')}
+
+
+ {isAdmin && (
+
+
{_('homeNoVmsOr')}
+
+
+
+
+ {_('homeImportVm')}
+
+ {_('homeImportVmMessage')}
+
+
+
+
+ {_('homeRestoreBackup')}
+
+
+ {_('homeRestoreBackupMessage')}
+
+
+
+
+ )}
+
+ )}
+
+
+ )
+ }
+}
+
+@addSubscriptions({
+ noResourceSets: cb => subscribeResourceSets(data => cb(isEmpty(data))),
+})
+@connectStore(() => {
+ const type = (_, props) => props.location.query.t || DEFAULT_TYPE
+
+ return {
+ isAdmin,
+ items: createGetObjectsOfType(type),
+ type,
+ user: getUser,
+ }
+})
+export default class Home extends Component {
+ static contextTypes = {
+ router: React.PropTypes.object,
+ }
+
+ state = {
+ selectedItems: {},
+ }
+
+ get page () {
+ return this.state.page
+ }
+ set page (activePage) {
+ this.setState({ activePage })
+ }
+
+ componentWillMount () {
+ this._initFilterAndSortBy(this.props)
+ }
+
+ componentWillReceiveProps (props) {
+ if (this._getFilter() !== this._getFilter(props)) {
+ this._initFilterAndSortBy(props)
+ }
+ if (props.type !== this.props.type) {
+ this.setState({ activePage: undefined, highlighted: undefined })
+ }
+ }
+
+ componentDidUpdate () {
+ const { selectedItems } = this.state
+
+ // Unselect items that are no longer visible
+ if (
+ (this._visibleItemsRecomputations || 0) <
+ (this._visibleItemsRecomputations = this._getVisibleItems.recomputations())
+ ) {
+ const newSelectedItems = pick(
+ selectedItems,
+ map(this._getVisibleItems(), 'id')
+ )
+ if (size(newSelectedItems) < this._getNumberOfSelectedItems()) {
+ this.setState({ selectedItems: newSelectedItems })
+ }
+ }
+ }
+
+ _getNumberOfItems = createCounter(() => this.props.items)
+ _getNumberOfSelectedItems = createCounter(() => this.state.selectedItems, [
+ identity,
+ ])
+
+ _getType () {
+ return this.props.type
+ }
+
+ _setType (type) {
+ const { pathname, query } = this.props.location
+ this.context.router.push({
+ pathname,
+ query: { ...query, t: type, s: undefined },
+ })
+ }
+
+ // Filter and sort -----------------------------------------------------------
+
+ _getDefaultFilter (props = this.props) {
+ const { type } = props
+ const preferences = get(() => props.user.preferences)
+ const defaultFilterName = get(() => preferences.defaultHomeFilters[type])
+ return defined(
+ defaultFilterName &&
+ defined(
+ () => homeFilters[type][defaultFilterName],
+ () => preferences.filters[type][defaultFilterName]
+ ),
+ OPTIONS[type].defaultFilter
+ )
+ }
+
+ _getDefaultSort (props = this.props) {
+ const sortOption = find(OPTIONS[props.type].sortOptions, 'default')
+
+ return {
+ sortBy: defined(() => sortOption.sortBy, 'name_label'),
+ sortOrder: defined(() => sortOption.sortOrder, 'asc'),
+ }
+ }
+
+ _initFilterAndSortBy (props) {
+ const filter = this._getFilter(props)
+
+ // If filter is null, set a default filter.
+ if (filter == null) {
+ const defaultFilter = this._getDefaultFilter(props)
+
+ if (defaultFilter != null) {
+ this._setFilter(defaultFilter, props, true)
+ }
+ return
+ }
+
+ // If the filter is already set, do nothing.
+ if (filter === this.props.filter) {
+ return
+ }
+
+ const parsed = ComplexMatcher.parse(filter)
+ const properties = ComplexMatcher.getPropertyClausesStrings(parsed)
+
+ const sort = this._getDefaultSort(props)
+
+ this.setState({
+ selectedHosts: properties.$container,
+ selectedPools: properties.$pool,
+ selectedTags: properties.tags,
+ selectedResourceSets: properties.resourceSet,
+ ...sort,
+ })
+
+ const { filterInput } = this.refs
+ if (filterInput && filterInput.value !== filter) {
+ filterInput.value = filter
+ }
+ }
+
+ // Optionally can take the props to be able to use it in
+ // componentWillReceiveProps().
+ _getFilter (props = this.props) {
+ return props.location.query.s
+ }
+
+ _getParsedFilter = createSelector(
+ props => this._getFilter(),
+ filter => ComplexMatcher.parse(filter)
+ )
+
+ _getFilterFunction = createSelector(
+ this._getParsedFilter,
+ filter => filter !== undefined && filter.createPredicate()
+ )
+
+ // Optionally can take the props to be able to use it in
+ // componentWillReceiveProps().
+ _setFilter (filter, props = this.props, replace) {
+ if (!isString(filter)) {
+ filter = filter.toString()
+ }
+
+ const { pathname, query } = props.location
+ this.context.router[replace ? 'replace' : 'push']({
+ pathname,
+ query: { ...query, s: filter },
+ })
+
+ this.page = 1
+ }
+
+ _clearFilter = () => this._setFilter('')
+
+ _onFilterChange = invoke(() => {
+ const setFilter = debounce(filter => {
+ this._setFilter(filter)
+ }, 500)
+
+ return event => setFilter(event.target.value)
+ })
+
+ _getFilteredItems = createSort(
+ createFilter(() => this.props.items, this._getFilterFunction),
+ () => this.state.sortBy,
+ () => this.state.sortOrder
+ )
+
+ _getVisibleItems = createPager(
+ this._getFilteredItems,
+ () => this.state.activePage || 1,
+ ITEMS_PER_PAGE
+ )
+
+ _expandAll = () => this.setState({ expandAll: !this.state.expandAll })
+
+ _onPageSelection = page => {
+ this.page = page
+ }
+
+ _tick = isCriteria => (
+
+ )
+
+ // High level filters --------------------------------------------------------
+
+ _typesDropdownItems = map(TYPES, (label, type) => (
+ this._setType(type)}>
+ {label}
+
+ ))
+ _updateSelectedPools = pools => {
+ const filter = this._getParsedFilter()
+
+ this._setFilter(
+ pools.length
+ ? ComplexMatcher.setPropertyClause(
+ filter,
+ '$pool',
+ new ComplexMatcher.Or(
+ map(pools, pool => new ComplexMatcher.String(pool.id))
+ )
+ )
+ : ComplexMatcher.setPropertyClause(filter, '$pool', undefined)
+ )
+ }
+ _updateSelectedHosts = hosts => {
+ const filter = this._getParsedFilter()
+
+ this._setFilter(
+ hosts.length
+ ? ComplexMatcher.setPropertyClause(
+ filter,
+ '$container',
+ new ComplexMatcher.Or(
+ map(hosts, host => new ComplexMatcher.String(host.id))
+ )
+ )
+ : ComplexMatcher.setPropertyClause(filter, '$container', undefined)
+ )
+ }
+ _updateSelectedTags = tags => {
+ const filter = this._getParsedFilter()
+
+ this._setFilter(
+ tags.length
+ ? ComplexMatcher.setPropertyClause(
+ filter,
+ 'tags',
+ new ComplexMatcher.Or(
+ map(tags, tag => new ComplexMatcher.String(tag.id))
+ )
+ )
+ : ComplexMatcher.setPropertyClause(filter, 'tags', undefined)
+ )
+ }
+ _updateSelectedResourceSets = resourceSets => {
+ const filter = this._getParsedFilter()
+
+ this._setFilter(
+ resourceSets.length
+ ? ComplexMatcher.setPropertyClause(
+ filter,
+ 'resourceSet',
+ new ComplexMatcher.Or(
+ map(resourceSets, set => new ComplexMatcher.String(set.id))
+ )
+ )
+ : ComplexMatcher.setPropertyClause(filter, 'resourceSet', undefined)
+ )
+ }
+ _addCustomFilter = () => {
+ return addCustomFilter(this._getType(), this._getFilter())
+ }
+ _getCustomFilters () {
+ const { preferences } = this.props.user || {}
+
+ if (!preferences) {
+ return
+ }
+
+ const customFilters = preferences.filters || {}
+ return customFilters[this._getType()]
+ }
+
+ // Checkboxes ----------------------------------------------------------------
+
+ _getIsAllSelected = createSelector(
+ () => this.state.selectedItems,
+ this._getVisibleItems,
+ (selectedItems, visibleItems) =>
+ size(visibleItems) > 0 &&
+ size(filter(selectedItems)) === size(visibleItems)
+ )
+ _getIsSomeSelected = createSelector(() => this.state.selectedItems, some)
+ _toggleMaster = () => {
+ const selectedItems = {}
+ if (!this._getIsAllSelected()) {
+ forEach(this._getVisibleItems(), ({ id }) => {
+ selectedItems[id] = true
+ })
+ }
+ this.setState({ selectedItems })
+ }
+ _getSelectedItemsIds = createSelector(
+ () => this.state.selectedItems,
+ items => keys(pickBy(items))
+ )
+
+ // Shortcuts -----------------------------------------------------------------
+
+ _getShortcutsHandler = createSelector(
+ () => this._getVisibleItems(),
+ items => (command, event) => {
+ event.preventDefault()
+ switch (command) {
+ case 'SEARCH':
+ this.refs.filterInput.focus()
+ break
+ case 'NAV_DOWN':
+ this.setState({
+ highlighted: (this.state.highlighted + 1) % items.length || 0,
+ })
+ break
+ case 'NAV_UP':
+ this.setState({
+ highlighted: (this.state.highlighted - 1) % items.length || 0,
+ })
+ break
+ case 'SELECT':
+ const itemId = items[this.state.highlighted].id
+ this.setState({
+ selectedItems: {
+ ...this.state.selectedItems,
+ [itemId]: !this.state.selectedItems[itemId],
+ },
+ })
+ break
+ case 'JUMP_INTO':
+ const item = items[this.state.highlighted]
+ if (includes(['VM', 'host', 'pool', 'SR'], item && item.type)) {
+ this.context.router.push({
+ pathname: `${item.type.toLowerCase()}s/${item.id}`,
+ })
+ }
+ }
+ }
+ )
+
+ // Header --------------------------------------------------------------------
+
+ _renderHeader () {
+ const { isAdmin, noResourceSets, type } = this.props
+ const { filters } = OPTIONS[type]
+ const customFilters = this._getCustomFilters()
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {_('filterSaveAs')}
+
+
+ {!isEmpty(customFilters) && [
+ map(customFilters, (filter, name) => (
+ this._setFilter(filter)}
+ >
+ {name}
+
+ )),
+ ,
+ ]}
+ {map(filters, (filter, label) => (
+ this._setFilter(filter)}
+ >
+ {_(label)}
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {(isAdmin || !noResourceSets) && (
+
+
+ {_('homeNewVm')}
+
+
+ )}
+
+
+ )
+ }
+
+ // ---------------------------------------------------------------------------
+
+ render () {
+ const { isAdmin, noResourceSets } = this.props
+
+ const nItems = this._getNumberOfItems()
+
+ if (nItems < 1) {
+ return
+ }
+
+ const filteredItems = this._getFilteredItems()
+ const visibleItems = this._getVisibleItems()
+
+ const {
+ activePage,
+ expandAll,
+ highlighted,
+ selectedHosts,
+ selectedItems,
+ selectedPools,
+ selectedResourceSets,
+ selectedTags,
+ sortBy,
+ } = this.state
+ const { items, type } = this.props
+
+ const options = OPTIONS[type]
+ const {
+ Item,
+ mainActions,
+ otherActions,
+ showHostsSelector,
+ showPoolsSelector,
+ showResourceSetsSelector,
+ } = options
+
+ // Necessary because indeterminate cannot be used as an attribute
+ if (this.refs.masterCheckbox) {
+ this.refs.masterCheckbox.indeterminate =
+ this._getIsSomeSelected() && !this._getIsAllSelected()
+ }
+
+ return (
+
+
+
+
+
+
+ {' '}
+
+ {this._getNumberOfSelectedItems()
+ ? _('homeSelectedItems', {
+ icon: ,
+ selected: this._getNumberOfSelectedItems(),
+ total: nItems,
+ })
+ : _('homeDisplayedItems', {
+ displayed: filteredItems.length,
+ icon: ,
+ total: nItems,
+ })}
+
+
+
+ {this._getNumberOfSelectedItems() ? (
+
+ {mainActions && (
+
+ {map(mainActions, (action, key) => (
+
+
+
+ ))}
+
+ )}
+ {otherActions && (
+
+ {map(otherActions, (action, key) => (
+ {
+ action.handler(
+ this._getSelectedItemsIds(),
+ action.params
+ )
+ }}
+ >
+ {' '}
+ {_(action.labelId)}
+
+ ))}
+
+ )}
+
+ ) : (
+
+ {showPoolsSelector && (
+
+
+
+ }
+ >
+
+ {_('homeAllPools')}
+
+
+ )}
+ {showHostsSelector && (
+
+
+
+ }
+ >
+
+ {_('homeAllHosts')}
+
+
+ )}
+
+
+
+ }
+ >
+
+ {_('homeAllTags')}
+
+
+ {showResourceSetsSelector &&
+ isAdmin &&
+ !noResourceSets && (
+
+
+
+ }
+ >
+
+ {' '}
+ {_('homeAllResourceSets')}
+
+
+ )}
+
+ {map(
+ options.sortOptions,
+ ({ labelId, sortBy: _sortBy, sortOrder }, key) => (
+
+ this.setState({ sortBy: _sortBy, sortOrder })
+ }
+ >
+ {this._tick(_sortBy === sortBy)}
+ {_sortBy === sortBy ? (
+ {_(labelId)}
+ ) : (
+ _(labelId)
+ )}
+
+ )
+ )}
+
+
+ )}
+
+
+
+
+
+
+
+ {isEmpty(filteredItems) ? (
+
+
+ {_('homeNoMatches')}
+
+
+ ) : (
+ map(visibleItems, (item, index) => (
+
+
+
+ ))
+ )}
+
+ {filteredItems.length > ITEMS_PER_PAGE && (
+
+
+
+ )}
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/home/mini-stats.js b/packages/xo-web/src/xo-app/home/mini-stats.js
new file mode 100644
index 000000000..0941b8bab
--- /dev/null
+++ b/packages/xo-web/src/xo-app/home/mini-stats.js
@@ -0,0 +1,65 @@
+import Component from 'base-component'
+import Icon from 'icon'
+import propTypes from 'prop-types-decorator'
+import React from 'react'
+import { Col, Row } from 'grid'
+import {
+ CpuSparkLines,
+ LoadSparkLines,
+ NetworkSparkLines,
+ XvdSparkLines,
+} from 'xo-sparklines'
+
+import styles from './index.css'
+
+const MINI_STATS_PROPS = {
+ height: 10,
+ strokeWidth: 0.2,
+ width: 50,
+}
+
+@propTypes({
+ fetchStats: propTypes.func.isRequired,
+})
+export default class MiniStats extends Component {
+ _fetch = () => {
+ this.props.fetch().then(stats => {
+ this.setState({ stats })
+ this._timeout = setTimeout(this._fetch, 5e3)
+ })
+ }
+
+ componentWillMount () {
+ this._fetch()
+ }
+
+ componentWillUnmount () {
+ clearTimeout(this._timeout)
+ }
+
+ render () {
+ const { stats } = this.state
+
+ if (stats === undefined) {
+ return
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ {stats.stats.load !== undefined ? (
+
+ ) : (
+
+ )}
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/home/pool-item.js b/packages/xo-web/src/xo-app/home/pool-item.js
new file mode 100644
index 000000000..4951de34a
--- /dev/null
+++ b/packages/xo-web/src/xo-app/home/pool-item.js
@@ -0,0 +1,263 @@
+import _ from 'intl'
+import Component from 'base-component'
+import Ellipsis, { EllipsisContainer } from 'ellipsis'
+import flatMap from 'lodash/flatMap'
+import Icon from 'icon'
+import map from 'lodash/map'
+import React from 'react'
+import SingleLineRow from 'single-line-row'
+import size from 'lodash/size'
+import HomeTags from 'home-tags'
+import Tooltip from 'tooltip'
+import Link, { BlockLink } from 'link'
+import { Col } from 'grid'
+import { Text } from 'editable'
+import { addTag, editPool, getHostMissingPatches, removeTag } from 'xo'
+import { connectStore, formatSizeShort } from 'utils'
+import {
+ createGetObjectsOfType,
+ createGetHostMetrics,
+ createSelector,
+} from 'selectors'
+
+import styles from './index.css'
+
+@connectStore(() => {
+ const getPoolHosts = createGetObjectsOfType('host').filter(
+ createSelector(
+ (_, props) => props.item.id,
+ poolId => host => host.$pool === poolId
+ )
+ )
+
+ const getMissingPatches = createSelector(getPoolHosts, hosts => {
+ return Promise.all(map(hosts, host => getHostMissingPatches(host))).then(
+ patches => flatMap(patches)
+ )
+ })
+
+ const getHostMetrics = createGetHostMetrics(getPoolHosts)
+
+ const getNumberOfSrs = createGetObjectsOfType('SR').count(
+ createSelector(
+ (_, props) => props.item.id,
+ poolId => obj => obj.$pool === poolId
+ )
+ )
+
+ const getNumberOfVms = createGetObjectsOfType('VM').count(
+ createSelector(
+ (_, props) => props.item.id,
+ poolId => obj => obj.$pool === poolId
+ )
+ )
+
+ return {
+ hostMetrics: getHostMetrics,
+ missingPaths: getMissingPatches,
+ poolHosts: getPoolHosts,
+ nSrs: getNumberOfSrs,
+ nVms: getNumberOfVms,
+ }
+})
+export default class PoolItem extends Component {
+ _addTag = tag => addTag(this.props.item.id, tag)
+ _removeTag = tag => removeTag(this.props.item.id, tag)
+ _setNameDescription = nameDescription =>
+ editPool(this.props.item, { name_description: nameDescription })
+ _setNameLabel = nameLabel =>
+ editPool(this.props.item, { name_label: nameLabel })
+ _toggleExpanded = () => this.setState({ expanded: !this.state.expanded })
+ _onSelect = () => this.props.onSelect(this.props.item.id)
+
+ componentWillMount () {
+ this.props.missingPaths.then(patches =>
+ this.setState({ missingPatchCount: size(patches) })
+ )
+ }
+
+ render () {
+ const {
+ item: pool,
+ expandAll,
+ selected,
+ hostMetrics,
+ poolHosts,
+ nSrs,
+ nVms,
+ } = this.props
+ const { missingPatchCount } = this.state
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {missingPatchCount > 0 && (
+
+
+
+
+ {missingPatchCount}
+
+
+
+ )}
+ {pool.HA_enabled && (
+
+
+
+
+
+
+
+
+
+ )}
+
+
+
+
+
+
+ {hostMetrics.count}x {_('hostsTabName')}
+
+ }
+ >
+ {hostMetrics.count > 0 ? (
+
+
+
+ ) : (
+
+ )}
+
+
+
+ {nVms}x {_('vmsTabName')}
+
+ }
+ >
+ {nVms > 0 ? (
+
+
+
+ ) : (
+
+ )}
+
+
+
+ {nSrs}x {_('srsTabName')}
+
+ }
+ >
+ {nSrs > 0 ? (
+
+
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {(this.state.expanded || expandAll) && (
+
+
+
+ {hostMetrics.count}x {nVms}x{' '}
+ {nSrs}x {hostMetrics.cpus}x{' '}
+ {formatSizeShort(hostMetrics.memoryTotal)}
+
+
+
+
+ {_('homePoolMaster')}{' '}
+
+ {poolHosts && poolHosts[pool.master].name_label}
+
+
+
+
+
+
+
+
+
+ )}
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/home/sr-item.js b/packages/xo-web/src/xo-app/home/sr-item.js
new file mode 100644
index 000000000..fa25ae63d
--- /dev/null
+++ b/packages/xo-web/src/xo-app/home/sr-item.js
@@ -0,0 +1,217 @@
+import _ from 'intl'
+import Component from 'base-component'
+import sum from 'lodash/sum'
+import Ellipsis, { EllipsisContainer } from 'ellipsis'
+import Icon from 'icon'
+import Link, { BlockLink } from 'link'
+import map from 'lodash/map'
+import React from 'react'
+import SingleLineRow from 'single-line-row'
+import size from 'lodash/size'
+import Tooltip from 'tooltip'
+import HomeTags from 'home-tags'
+import { Col } from 'grid'
+import { Text } from 'editable'
+import {
+ createGetObject,
+ createGetObjectsOfType,
+ createSelector,
+} from 'selectors'
+import {
+ addTag,
+ editPool,
+ isSrShared,
+ reconnectAllHostsSr,
+ removeTag,
+ setDefaultSr,
+} from 'xo'
+import { connectStore, formatSizeShort } from 'utils'
+
+import styles from './index.css'
+
+@connectStore({
+ container: createGetObject((_, props) => props.item.$container),
+ isDefaultSr: createSelector(
+ createGetObjectsOfType('pool').find((_, props) => pool =>
+ props.item.$pool === pool.id
+ ),
+ (_, props) => props.item,
+ (pool, sr) => pool && pool.default_SR === sr.id
+ ),
+ isShared: createSelector((_, props) => props.item, isSrShared),
+ status: createSelector(
+ createGetObjectsOfType('PBD').filter((_, props) => pbd =>
+ pbd.SR === props.item.id
+ ),
+ pbds => {
+ const nbAttached = sum(map(pbds, pbd => (pbd.attached ? 1 : 0)))
+ const nbPbds = size(pbds)
+ if (!nbPbds) {
+ return -1
+ }
+ if (!nbAttached) {
+ return 0
+ }
+
+ return nbAttached < nbPbds ? 1 : 2
+ }
+ ),
+})
+export default class SrItem extends Component {
+ _addTag = tag => addTag(this.props.item.id, tag)
+ _removeTag = tag => removeTag(this.props.item.id, tag)
+ _setNameDescription = nameDescription =>
+ editPool(this.props.item, { name_description: nameDescription })
+ _setNameLabel = nameLabel =>
+ editPool(this.props.item, { name_label: nameLabel })
+ _toggleExpanded = () => this.setState({ expanded: !this.state.expanded })
+ _onSelect = () => this.props.onSelect(this.props.item.id)
+
+ _reconnectAllHostSr = () => reconnectAllHostsSr(this.props.item)
+ _setDefaultSr = () => setDefaultSr(this.props.item)
+
+ _getStatusPill = () => {
+ switch (this.props.status) {
+ case -1:
+ return (
+
+
+
+ )
+ case 0:
+ return (
+
+
+
+ )
+ case 1:
+ return (
+
+
+
+ )
+ case 2:
+ return (
+
+
+
+ )
+ }
+ }
+
+ render () {
+ const {
+ container,
+ expandAll,
+ isDefaultSr,
+ isShared,
+ item: sr,
+ selected,
+ } = this.props
+
+ return (
+
+
+
+
+
+
+
+ {this._getStatusPill()}
+
+
+
+
+ {isDefaultSr && (
+
+ {_('defaultSr')}
+
+ )}
+
+
+
+
+
+
+
+
+
+ {' '}
+
+
+
+
+
+
+
+
+
+ {isShared ? _('srSharedType', { type: sr.SR_type }) : sr.SR_type}
+
+
+ {formatSizeShort(sr.size)}
+
+
+ {sr.size > 0 && (
+
+
+
+ )}
+
+
+ {container && (
+
+ {container.name_label}
+
+ )}
+
+
+
+
+
+
+
+
+ {(this.state.expanded || expandAll) && (
+
+
+ {sr.VDIs.length}x
+
+
+
+
+
+
+
+ )}
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/home/template-item.js b/packages/xo-web/src/xo-app/home/template-item.js
new file mode 100644
index 000000000..8bc1cc8e9
--- /dev/null
+++ b/packages/xo-web/src/xo-app/home/template-item.js
@@ -0,0 +1,130 @@
+import _ from 'intl'
+import Component from 'base-component'
+import defined from 'xo-defined'
+import Ellipsis, { EllipsisContainer } from 'ellipsis'
+import Icon from 'icon'
+import Link from 'link'
+import map from 'lodash/map'
+import React from 'react'
+import SingleLineRow from 'single-line-row'
+import HomeTags from 'home-tags'
+import Tooltip from 'tooltip'
+import { Row, Col } from 'grid'
+import { Number, Size, Text } from 'editable'
+import { addTag, editVm, removeTag } from 'xo'
+import { connectStore, osFamily } from 'utils'
+import { createGetObject } from 'selectors'
+
+import styles from './index.css'
+
+@connectStore({
+ container: createGetObject((_, props) => props.item.$container),
+})
+export default class TemplateItem extends Component {
+ _addTag = tag => addTag(this.props.item.id, tag)
+ _onSelect = () => this.props.onSelect(this.props.item.id)
+ _removeTag = tag => removeTag(this.props.item.id, tag)
+ _setNameDescription = nameDescription =>
+ editVm(this.props.item, { name_description: nameDescription })
+ _setNameLabel = nameLabel =>
+ editVm(this.props.item, { name_label: nameLabel })
+ _setCpus = nCpus => editVm(this.props.item, { CPUs: nCpus })
+ _setMemory = memory => editVm(this.props.item, { memory })
+
+ render () {
+ const { item: vm, container, expandAll, selected } = this.props
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {' '}
+
+
+
+
+
+
+ {container && (
+
+ {container.name_label}
+
+ )}
+
+
+
+
+
+
+
+ {(this.state.expanded || expandAll) && (
+
+
+
+ x{' '}
+
+ {' '}
+
+
+
+
+ {map(vm.addresses, address => (
+
+ {address}
+
+ ))}
+
+
+
+
+
+
+
+ )}
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/home/vm-item.js b/packages/xo-web/src/xo-app/home/vm-item.js
new file mode 100644
index 000000000..e5e764819
--- /dev/null
+++ b/packages/xo-web/src/xo-app/home/vm-item.js
@@ -0,0 +1,256 @@
+import _ from 'intl'
+import Component from 'base-component'
+import Ellipsis, { EllipsisContainer } from 'ellipsis'
+import Icon from 'icon'
+import Link, { BlockLink } from 'link'
+import React from 'react'
+import SingleLineRow from 'single-line-row'
+import HomeTags from 'home-tags'
+import Tooltip from 'tooltip'
+import { Row, Col } from 'grid'
+import { Text, XoSelect } from 'editable'
+import { isEmpty, map } from 'lodash'
+import {
+ addTag,
+ editVm,
+ fetchVmStats,
+ migrateVm,
+ removeTag,
+ startVm,
+ stopVm,
+ subscribeResourceSets,
+} from 'xo'
+import {
+ addSubscriptions,
+ connectStore,
+ formatSizeShort,
+ osFamily,
+} from 'utils'
+import {
+ createFinder,
+ createGetObject,
+ createGetVmDisks,
+ createSelector,
+ createSumBy,
+} from 'selectors'
+
+import MiniStats from './mini-stats'
+import styles from './index.css'
+
+@addSubscriptions({
+ resourceSets: subscribeResourceSets,
+})
+@connectStore(() => ({
+ container: createGetObject((_, props) => props.item.$container),
+ totalDiskSize: createSumBy(createGetVmDisks((_, props) => props.item), 'size'),
+}))
+export default class VmItem extends Component {
+ get _isRunning () {
+ const vm = this.props.item
+ return vm && vm.power_state === 'Running'
+ }
+
+ _getResourceSet = createFinder(
+ () => this.props.resourceSets,
+ createSelector(
+ () => this.props.item.resourceSet,
+ id => resourceSet => resourceSet.id === id
+ )
+ )
+
+ _addTag = tag => addTag(this.props.item.id, tag)
+ _fetchStats = () => fetchVmStats(this.props.item.id)
+ _migrateVm = host => migrateVm(this.props.item, host)
+ _removeTag = tag => removeTag(this.props.item.id, tag)
+ _setNameDescription = nameDescription =>
+ editVm(this.props.item, { name_description: nameDescription })
+ _setNameLabel = nameLabel =>
+ editVm(this.props.item, { name_label: nameLabel })
+ _start = () => startVm(this.props.item)
+ _stop = () => stopVm(this.props.item)
+ _toggleExpanded = () => this.setState({ expanded: !this.state.expanded })
+ _onSelect = () => this.props.onSelect(this.props.item.id)
+
+ render () {
+ const { item: vm, container, expandAll, selected } = this.props
+ const resourceSet = this._getResourceSet()
+
+ return (
+
+
+
+
+
+
+
+
+ {_(`powerState${vm.power_state}`)}
+ {' ('}
+ {map(vm.current_operations)[0]}
+ {')'}
+
+ )
+ }
+ >
+ {isEmpty(vm.current_operations) ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ {this._isRunning ? (
+
+
+
+
+
+
+
+
+
+
+
+
+ ) : (
+
+
+
+
+
+
+
+
+ )}
+
+
+
+ {' '}
+
+
+
+
+
+
+ {this._isRunning && container ? (
+
+
+ {container.name_label}
+
+
+ ) : (
+ container && (
+
+ {container.name_label}
+
+ )
+ )}
+
+
+
+
+
+
+
+
+ {(this.state.expanded || expandAll) && (
+
+
+
+ {vm.CPUs.number}x {' '}
+ {formatSizeShort(vm.memory.size)} {' '}
+ {formatSizeShort(this.props.totalDiskSize)} {' '}
+ {' '}
+ {isEmpty(vm.snapshots) ? null : (
+
+ {vm.snapshots.length}x
+
+ )}
+ {vm.docker ? : null}
+
+
+
+ {resourceSet && (
+
+ {_('homeResourceSet', {
+ resourceSet: (
+
+ {resourceSet.name}
+
+ ),
+ })}
+
+ )}
+
+
+ {map(vm.addresses, address => (
+
+ {address}
+
+ ))}
+
+
+
+
+
+
+
+ {this._isRunning && }
+
+
+ )}
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/host/action-bar.js b/packages/xo-web/src/xo-app/host/action-bar.js
new file mode 100644
index 000000000..f398f1d9e
--- /dev/null
+++ b/packages/xo-web/src/xo-app/host/action-bar.js
@@ -0,0 +1,54 @@
+import _ from 'intl'
+import ActionBar, { Action } from 'action-bar'
+import React from 'react'
+import {
+ // disableHost,
+ emergencyShutdownHost,
+ restartHost,
+ restartHostAgent,
+ startHost,
+ stopHost,
+} from 'xo'
+
+const hostActionBarByState = {
+ Running: ({ host }) => (
+
+
+
+
+
+
+ ),
+ Halted: ({ host }) => (
+
+
+
+ ),
+}
+
+const HostActionBar = ({ host }) => {
+ const ActionBar = hostActionBarByState[host.power_state]
+
+ if (!ActionBar) {
+ return No action bar for state {host.power_state}
+ }
+
+ return
+}
+export default HostActionBar
diff --git a/packages/xo-web/src/xo-app/host/index.js b/packages/xo-web/src/xo-app/host/index.js
new file mode 100644
index 000000000..898c09301
--- /dev/null
+++ b/packages/xo-web/src/xo-app/host/index.js
@@ -0,0 +1,348 @@
+import _ from 'intl'
+import HostActionBar from './action-bar'
+import Icon from 'icon'
+import Link from 'link'
+import { NavLink, NavTabs } from 'nav'
+import Page from '../page'
+import React, { cloneElement, Component } from 'react'
+import Tooltip from 'tooltip'
+import { Text } from 'editable'
+import { Container, Row, Col } from 'grid'
+import {
+ editHost,
+ fetchHostStats,
+ installAllHostPatches,
+ installHostPatch,
+ subscribeHostMissingPatches,
+} from 'xo'
+import { connectStore, routes } from 'utils'
+import {
+ createDoesHostNeedRestart,
+ createGetObject,
+ createGetObjectsOfType,
+ createSelector,
+} from 'selectors'
+import { assign, isEmpty, isString, map, pick, sortBy, sum } from 'lodash'
+
+import TabAdvanced from './tab-advanced'
+import TabConsole from './tab-console'
+import TabGeneral from './tab-general'
+import TabLogs from './tab-logs'
+import TabNetwork from './tab-network'
+import TabPatches from './tab-patches'
+import TabStats from './tab-stats'
+import TabStorage from './tab-storage'
+
+const isRunning = host => host && host.power_state === 'Running'
+
+// ===================================================================
+
+@routes('general', {
+ advanced: TabAdvanced,
+ console: TabConsole,
+ general: TabGeneral,
+ logs: TabLogs,
+ network: TabNetwork,
+ patches: TabPatches,
+ stats: TabStats,
+ storage: TabStorage,
+})
+@connectStore(() => {
+ const getHost = createGetObject()
+
+ const getPool = createGetObject((state, props) => getHost(state, props).$pool)
+
+ const getVmController = createGetObjectsOfType('VM-controller').find(
+ createSelector(getHost, ({ id }) => obj => obj.$container === id)
+ )
+
+ const getHostVms = createGetObjectsOfType('VM').filter(
+ createSelector(getHost, ({ id }) => obj => obj.$container === id)
+ )
+
+ const getNumberOfVms = getHostVms.count()
+
+ const getLogs = createGetObjectsOfType('message')
+ .filter(
+ createSelector(
+ getHost,
+ getVmController,
+ (host, controller) => ({ $object }) =>
+ $object === host.id || $object === controller.id
+ )
+ )
+ .sort()
+
+ const getPifs = createGetObjectsOfType('PIF')
+ .pick(createSelector(getHost, host => host.$PIFs))
+ .sort()
+
+ const getNetworks = createGetObjectsOfType('network').pick(
+ createSelector(getPifs, pifs => map(pifs, pif => pif.$network))
+ )
+
+ const getHostPatches = createSelector(
+ createGetObjectsOfType('pool_patch'),
+ createGetObjectsOfType('host_patch').pick(
+ createSelector(
+ getHost,
+ host => (isString(host.patches[0]) ? host.patches : [])
+ )
+ ),
+ (poolsPatches, hostsPatches) =>
+ map(hostsPatches, hostPatch => ({
+ ...hostPatch,
+ poolPatch: poolsPatches[hostPatch.pool_patch],
+ }))
+ )
+
+ const doesNeedRestart = createDoesHostNeedRestart(getHost)
+
+ const getMemoryUsed = createSelector(getHostVms, vms =>
+ sum(map(vms, vm => vm.memory.size))
+ )
+
+ return (state, props) => {
+ const host = getHost(state, props)
+ if (!host) {
+ return {}
+ }
+
+ return {
+ host,
+ hostPatches: getHostPatches(state, props),
+ logs: getLogs(state, props),
+ memoryUsed: getMemoryUsed(state, props),
+ needsRestart: doesNeedRestart(state, props),
+ networks: getNetworks(state, props),
+ nVms: getNumberOfVms(state, props),
+ pifs: getPifs(state, props),
+ pool: getPool(state, props),
+ vmController: getVmController(state, props),
+ vms: getHostVms(state, props),
+ }
+ }
+})
+export default class Host extends Component {
+ static contextTypes = {
+ router: React.PropTypes.object,
+ }
+
+ loop (host = this.props.host) {
+ if (host == null) {
+ return
+ }
+
+ if (this.cancel) {
+ this.cancel()
+ }
+
+ if (!isRunning(host)) {
+ return
+ }
+
+ let cancelled = false
+ this.cancel = () => {
+ cancelled = true
+ }
+
+ fetchHostStats(host).then(stats => {
+ if (cancelled) {
+ return
+ }
+ this.cancel = null
+
+ clearTimeout(this.timeout)
+ this.setState(
+ {
+ statsOverview: stats,
+ },
+ () => {
+ this.timeout = setTimeout(this.loop, stats.interval * 1000)
+ }
+ )
+ })
+ }
+ loop = ::this.loop
+
+ componentDidMount () {
+ this.loop()
+ this._subscribePatches(this.props.host)
+ }
+
+ componentWillUnmount () {
+ clearTimeout(this.timeout)
+ this.unsubscribeHostMissingPatches()
+ }
+
+ componentWillReceiveProps (props) {
+ const hostNext = props.host
+ const hostCur = this.props.host
+
+ if (hostCur && !hostNext) {
+ return this.context.router.push('/')
+ }
+
+ if (!hostNext) {
+ return
+ }
+
+ this._subscribePatches(hostNext)
+
+ if (!isRunning(hostCur) && isRunning(hostNext)) {
+ this.loop(hostNext)
+ } else if (isRunning(hostCur) && !isRunning(hostNext)) {
+ this.setState({
+ statsOverview: undefined,
+ })
+ }
+ }
+
+ _subscribePatches (host) {
+ if (host === undefined) {
+ return
+ }
+
+ this.unsubscribeHostMissingPatches = subscribeHostMissingPatches(
+ host,
+ missingPatches =>
+ this.setState({
+ missingPatches: sortBy(missingPatches, patch => -patch.time),
+ })
+ )
+ }
+
+ _installAllPatches = () => {
+ const { host } = this.props
+ return installAllHostPatches(host)
+ }
+
+ _installPatch = patch => {
+ const { host } = this.props
+ return installHostPatch(host, patch)
+ }
+
+ _setNameDescription = nameDescription =>
+ editHost(this.props.host, { name_description: nameDescription })
+ _setNameLabel = nameLabel =>
+ editHost(this.props.host, { name_label: nameLabel })
+
+ header () {
+ const { host, pool } = this.props
+ const { missingPatches } = this.state || {}
+ if (!host) {
+ return
+ }
+ return (
+
+
+
+
+ {' '}
+
+
+
+
+ {pool && (
+
+ {' '}
+ - {pool.name_label}
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+ {_('generalTabName')}
+
+
+ {_('statsTabName')}
+
+
+ {_('consoleTabName')}
+
+
+ {_('networkTabName')}
+
+
+ {_('storageTabName')}
+
+
+ {_('patchesTabName')}{' '}
+ {isEmpty(missingPatches) ? null : (
+
+ {missingPatches.length}
+
+ )}
+ {this.props.needsRestart &&
+ isEmpty(missingPatches) && (
+
+
+
+ )}
+
+
+ {_('logsTabName')}
+
+
+ {_('advancedTabName')}
+
+
+
+
+
+ )
+ }
+
+ render () {
+ const { host, pool } = this.props
+ if (!host) {
+ return {_('statusLoading')}
+ }
+ const childProps = assign(
+ pick(this.props, [
+ 'host',
+ 'hostPatches',
+ 'logs',
+ 'memoryUsed',
+ 'networks',
+ 'nVms',
+ 'pbds',
+ 'pifs',
+ 'srs',
+ 'vmController',
+ 'vms',
+ ]),
+ pick(this.state, ['missingPatches', 'statsOverview']),
+ {
+ installAllPatches: this._installAllPatches,
+ installPatch: this._installPatch,
+ }
+ )
+ return (
+
+ {cloneElement(this.props.children, childProps)}
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/host/tab-advanced.js b/packages/xo-web/src/xo-app/host/tab-advanced.js
new file mode 100644
index 000000000..cf015f268
--- /dev/null
+++ b/packages/xo-web/src/xo-app/host/tab-advanced.js
@@ -0,0 +1,250 @@
+import _ from 'intl'
+import Copiable from 'copiable'
+import React from 'react'
+import TabButton from 'tab-button'
+import SelectFiles from 'select-files'
+import Upgrade from 'xoa-upgrade'
+import { connectStore } from 'utils'
+import { Toggle } from 'form'
+import {
+ enableHost,
+ detachHost,
+ disableHost,
+ forgetHost,
+ restartHost,
+ installSupplementalPack,
+} from 'xo'
+import { FormattedRelative, FormattedTime } from 'react-intl'
+import { Container, Row, Col } from 'grid'
+import { createGetObjectsOfType, createSelector } from 'selectors'
+import { map, noop } from 'lodash'
+
+const ALLOW_INSTALL_SUPP_PACK = process.env.XOA_PLAN > 1
+
+const forceReboot = host => restartHost(host, true)
+
+const formatPack = ({ name, author, description, version }, key) => (
+
+ {_('supplementalPackTitle', { author, name })}
+ {description}
+ {version}
+
+)
+
+export default connectStore(() => {
+ const getPgpus = createGetObjectsOfType('PGPU')
+ .pick((_, { host }) => host.$PGPUs)
+ .sort()
+
+ const getPcis = createGetObjectsOfType('PCI').pick(
+ createSelector(getPgpus, pgpus => map(pgpus, 'pci'))
+ )
+
+ return {
+ pcis: getPcis,
+ pgpus: getPgpus,
+ }
+})(({ host, pcis, pgpus }) => (
+
+
+
+ {host.power_state === 'Running' && (
+
+ )}
+ {host.enabled ? (
+
+ ) : (
+
+ )}
+
+ {host.power_state !== 'Running' && (
+
+ )}
+
+
+
+
+ {_('xenSettingsLabel')}
+
+
+
+ {_('uuid')}
+ {host.uuid}
+
+
+ {_('hostAddress')}
+ {host.address}
+
+
+ {_('hostStatus')}
+
+ {host.enabled
+ ? _('hostStatusEnabled')
+ : _('hostStatusDisabled')}
+
+
+
+ {_('hostPowerOnMode')}
+
+
+
+
+
+ {_('hostStartedSince')}
+
+ {_('started', {
+ ago: ,
+ })}
+
+
+
+ {_('hostStackStartedSince')}
+
+ {_('started', {
+ ago: ,
+ })}
+
+
+
+ {_('hostXenServerVersion')}
+
+ {host.license_params.sku_marketing_name} {host.version} ({
+ host.license_params.sku_type
+ })
+
+
+
+ {_('hostBuildNumber')}
+ {host.build}
+
+
+ {_('hostIscsiName')}
+ {host.iSCSI_name}
+
+
+
+
+ {_('hardwareHostSettingsLabel')}
+
+
+
+ {_('hostCpusModel')}
+ {host.CPUs.modelname}
+
+
+ {_('hostGpus')}
+
+ {map(pgpus, pgpu => pcis[pgpu.pci].device_name).join(', ')}
+
+
+
+ {_('hostCpusNumber')}
+
+ {host.cpus.cores} ({host.cpus.sockets})
+
+
+
+ {_('hostManufacturerinfo')}
+
+ {host.bios_strings['system-manufacturer']} ({
+ host.bios_strings['system-product-name']
+ })
+
+
+
+ {_('hostBiosinfo')}
+
+ {host.bios_strings['bios-vendor']} ({
+ host.bios_strings['bios-version']
+ })
+
+
+
+
+
+ {_('licenseHostSettingsLabel')}
+
+
+
+ {_('hostLicenseType')}
+ {host.license_params.sku_type}
+
+
+ {_('hostLicenseSocket')}
+ {host.license_params.sockets}
+
+
+ {_('hostLicenseExpiry')}
+
+
+
+
+
+
+
+ {_('supplementalPacks')}
+
+
+ {map(host.supplementalPacks, formatPack)}
+ {ALLOW_INSTALL_SUPP_PACK && (
+
+ {_('supplementalPackNew')}
+
+ installSupplementalPack(host, file)}
+ />
+
+
+ )}
+
+
+ {!ALLOW_INSTALL_SUPP_PACK && [
+ {_('supplementalPackNew')} ,
+
+
+ ,
+ ]}
+
+
+
+))
diff --git a/packages/xo-web/src/xo-app/host/tab-console.js b/packages/xo-web/src/xo-app/host/tab-console.js
new file mode 100644
index 000000000..58764a36a
--- /dev/null
+++ b/packages/xo-web/src/xo-app/host/tab-console.js
@@ -0,0 +1,101 @@
+import _ from 'intl'
+import Button from 'button'
+import Component from 'base-component'
+import CopyToClipboard from 'react-copy-to-clipboard'
+import debounce from 'lodash/debounce'
+import Icon from 'icon'
+import invoke from 'invoke'
+import NoVnc from 'react-novnc'
+import React from 'react'
+import { resolveUrl } from 'xo'
+import { Container, Row, Col } from 'grid'
+
+import {
+ CpuSparkLines,
+ MemorySparkLines,
+ NetworkSparkLines,
+ LoadSparkLines,
+} from 'xo-sparklines'
+
+export default class extends Component {
+ _sendCtrlAltDel = () => {
+ this.refs.noVnc.sendCtrlAltDel()
+ }
+
+ _getRemoteClipboard = clipboard => {
+ this.setState({ clipboard })
+ this.refs.clipboard.value = clipboard
+ }
+ _setRemoteClipboard = invoke(() => {
+ const setRemoteClipboard = debounce(value => {
+ this.setState({ clipboard: value })
+ this.refs.noVnc.setClipboard(value)
+ }, 200)
+ return event => setRemoteClipboard(event.target.value)
+ })
+
+ _getClipboardContent = () => this.refs.clipboard && this.refs.clipboard.value
+
+ render () {
+ const { vmController, statsOverview } = this.props
+
+ return (
+
+ {statsOverview && (
+
+
+ {' '}
+
+
+
+ {' '}
+
+
+
+ {' '}
+
+
+
+ {' '}
+
+
+
+ )}
+
+
+
+
+
+
+
+
+ {_('copyToClipboardLabel')}
+
+
+
+
+
+
+
+ {_('ctrlAltDelButtonLabel')}
+
+
+
+
+
+
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/host/tab-general.js b/packages/xo-web/src/xo-app/host/tab-general.js
new file mode 100644
index 000000000..0eea7cbd3
--- /dev/null
+++ b/packages/xo-web/src/xo-app/host/tab-general.js
@@ -0,0 +1,164 @@
+import * as CM from 'complex-matcher'
+import _ from 'intl'
+import Copiable from 'copiable'
+import Icon from 'icon'
+import map from 'lodash/map'
+import React from 'react'
+import store from 'store'
+import HomeTags from 'home-tags'
+import { addTag, removeTag } from 'xo'
+import { BlockLink } from 'link'
+import { Container, Row, Col } from 'grid'
+import { FormattedRelative } from 'react-intl'
+import { formatSize } from 'utils'
+import Usage, { UsageElement } from 'usage'
+import { getObject } from 'selectors'
+import {
+ CpuSparkLines,
+ MemorySparkLines,
+ NetworkSparkLines,
+ LoadSparkLines,
+} from 'xo-sparklines'
+
+export default ({
+ statsOverview,
+ host,
+ memoryUsed,
+ nVms,
+ vmController,
+ vms,
+}) => {
+ const pool = getObject(store.getState(), host.$pool)
+ const vmsFilter = encodeURIComponent(
+ new CM.Property('$container', new CM.String(host.id)).toString()
+ )
+
+ return (
+
+
+
+
+
+ {host.CPUs.cpu_count}x
+
+
+ {statsOverview && }
+
+
+
+
+ {formatSize(host.memory.size)}
+
+
+ {statsOverview && }
+
+
+
+
+
+ {host.$PIFs.length}x
+
+
+
+ {statsOverview && }
+
+
+
+
+
+ {host.$PBDs.length}x
+
+
+
+ {statsOverview && }
+
+
+
+
+
+
+
+ {_('started', {
+ ago: ,
+ })}
+
+
+
+
+ {host.license_params.sku_marketing_name} {host.version} ({
+ host.license_params.sku_type
+ })
+
+
+
+ {host.address}
+
+
+
+ {host.bios_strings['system-manufacturer']}{' '}
+ {host.bios_strings['system-product-name']}
+
+
+
+
+
+
+
+
+ {nVms}x
+
+
+
+
+
+
+
+
+ {_('memoryHostState', { memoryUsed: formatSize(memoryUsed) })}
+
+
+
+
+
+
+
+ {map(vms, vm => (
+
+ ))}
+
+
+
+ {pool &&
+ host.id === pool.master && (
+
+
+
+ {_('pillMaster')}
+
+
+
+ )}
+
+
+
+ removeTag(host.id, tag)}
+ onAdd={tag => addTag(host.id, tag)}
+ />
+
+
+
+
+ )
+}
diff --git a/packages/xo-web/src/xo-app/host/tab-logs.js b/packages/xo-web/src/xo-app/host/tab-logs.js
new file mode 100644
index 000000000..85686a171
--- /dev/null
+++ b/packages/xo-web/src/xo-app/host/tab-logs.js
@@ -0,0 +1,105 @@
+import _ from 'intl'
+import ActionRowButton from 'action-row-button'
+import Component from 'base-component'
+import isEmpty from 'lodash/isEmpty'
+import map from 'lodash/map'
+import React from 'react'
+import SortedTable from 'sorted-table'
+import TabButton from 'tab-button'
+import { deleteMessage } from 'xo'
+import { createPager } from 'selectors'
+import { FormattedRelative, FormattedTime } from 'react-intl'
+import { Container, Row, Col } from 'grid'
+
+const LOG_COLUMNS = [
+ {
+ name: _('logDate'),
+ itemRenderer: log => (
+
+ {' '}
+ ( )
+
+ ),
+ sortCriteria: log => log.time,
+ sortOrder: 'desc',
+ },
+ {
+ name: _('logName'),
+ itemRenderer: log => log.name,
+ sortCriteria: log => log.name,
+ },
+ {
+ name: _('logContent'),
+ itemRenderer: log => log.body,
+ sortCriteria: log => log.body,
+ },
+ {
+ name: _('logAction'),
+ itemRenderer: log => (
+
+ ),
+ },
+]
+
+export default class TabLogs extends Component {
+ constructor () {
+ super()
+
+ this.getLogs = createPager(() => this.props.logs, () => this.state.page, 10)
+
+ this.state = {
+ page: 1,
+ }
+ }
+
+ _deleteAllLogs = () => map(this.props.logs, deleteMessage)
+ _nextPage = () => this.setState({ page: this.state.page + 1 })
+ _previousPage = () => this.setState({ page: this.state.page - 1 })
+
+ render () {
+ const logs = this.getLogs()
+
+ if (isEmpty(logs)) {
+ return (
+
+
+
+ {_('noLogs')}
+
+
+ )
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/host/tab-network.js b/packages/xo-web/src/xo-app/host/tab-network.js
new file mode 100644
index 000000000..523ca71ef
--- /dev/null
+++ b/packages/xo-web/src/xo-app/host/tab-network.js
@@ -0,0 +1,334 @@
+import _ from 'intl'
+import Component from 'base-component'
+import React from 'react'
+import Icon from 'icon'
+import pick from 'lodash/pick'
+import SingleLineRow from 'single-line-row'
+import some from 'lodash/some'
+import SortedTable from 'sorted-table'
+import StateButton from 'state-button'
+import TabButton from 'tab-button'
+import Tooltip from 'tooltip'
+import { confirm } from 'modal'
+import { connectStore, noop } from 'utils'
+import { Container, Row, Col } from 'grid'
+import { createGetObjectsOfType } from 'selectors'
+import { error } from 'notification'
+import { Select, Number } from 'editable'
+import { Toggle } from 'form'
+import {
+ connectPif,
+ createNetwork,
+ deletePif,
+ deletePifs,
+ disconnectPif,
+ editNetwork,
+ editPif,
+ getIpv4ConfigModes,
+ reconfigurePifIp,
+} from 'xo'
+
+const EDIT_BUTTON_STYLE = { color: '#999', cursor: 'pointer' }
+
+const _toggleDefaultLockingMode = (component, tooltip) =>
+ tooltip ? {component} : component
+
+class ConfigureIpModal extends Component {
+ constructor (props) {
+ super(props)
+
+ const { pif } = props
+ if (pif) {
+ this.state = pick(pif, ['ip', 'netmask', 'dns', 'gateway'])
+ }
+ }
+
+ get value () {
+ return this.state
+ }
+
+ render () {
+ const { ip, netmask, dns, gateway } = this.state
+
+ return (
+
+
+ {_('staticIp')}
+
+
+
+
+
+
+ {_('netmask')}
+
+
+
+
+
+
+ {_('dns')}
+
+
+
+
+
+
+ {_('gateway')}
+
+
+
+
+
+ )
+ }
+}
+
+class PifItemVlan extends Component {
+ _editPif = vlan => editPif(this.props.item, { vlan })
+ render () {
+ const pif = this.props.item
+ return (
+
+ {pif.vlan === -1 ? (
+ 'None'
+ ) : (
+
+ {pif.vlan}
+
+ )}
+
+ )
+ }
+}
+
+const reconfigureIp = (pif, mode) => {
+ if (mode === 'Static') {
+ return confirm({
+ icon: 'ip',
+ title: _('pifConfigureIp'),
+ body: ,
+ }).then(params => {
+ if (!params.ip || !params.netmask) {
+ error(_('configIpErrorTitle'), _('configIpErrorMessage'))
+ return
+ }
+ return reconfigurePifIp(pif, { mode, ...params })
+ }, noop)
+ }
+ return reconfigurePifIp(pif, { mode })
+}
+
+class PifItemIp extends Component {
+ _onEditIp = () => reconfigureIp(this.props.pif, 'Static')
+
+ render () {
+ const { pif } = this.props
+ const pifIp = pif.ip
+ return (
+
+ {pifIp}{' '}
+ {pifIp && (
+
+
+
+ )}
+
+ )
+ }
+}
+
+class PifItemMode extends Component {
+ state = { configModes: [] }
+
+ componentDidMount () {
+ getIpv4ConfigModes().then(configModes => this.setState({ configModes }))
+ }
+
+ _configIp = mode => reconfigureIp(this.props.pif, mode)
+
+ render () {
+ const { pif } = this.props
+ const { configModes } = this.state
+ return (
+
+ {pif.mode}
+
+ )
+ }
+}
+
+@connectStore(() => ({
+ vifsByNetwork: createGetObjectsOfType('VIF').groupBy('$network'),
+}))
+class PifItemLock extends Component {
+ _editNetwork = () => {
+ const { pif, networks } = this.props
+ return editNetwork(pif.$network, {
+ defaultIsLocked: !networks[pif.$network].defaultIsLocked,
+ })
+ }
+
+ render () {
+ const { networks, pif, vifsByNetwork } = this.props
+ const pifInUse = some(vifsByNetwork[pif.$network], vif => vif.attached)
+ return _toggleDefaultLockingMode(
+ ,
+ pifInUse && _('pifInUse')
+ )
+ }
+}
+
+const COLUMNS = [
+ {
+ default: true,
+ itemRenderer: pif => pif.device,
+ name: _('pifDeviceLabel'),
+ sortCriteria: 'device',
+ },
+ {
+ itemRenderer: (pif, userData) => userData.networks[pif.$network].name_label,
+ name: _('pifNetworkLabel'),
+ sortCriteria: (pif, userData) => userData.networks[pif.$network].name_label,
+ },
+ {
+ component: PifItemVlan,
+ name: _('pifVlanLabel'),
+ sortCriteria: 'vlan',
+ },
+ {
+ itemRenderer: (pif, userData) => (
+
+ ),
+ name: _('pifAddressLabel'),
+ sortCriteria: 'ip',
+ },
+ {
+ itemRenderer: (pif, userData) => (
+
+ ),
+ name: _('pifModeLabel'),
+ sortCriteria: 'mode',
+ },
+ {
+ itemRenderer: pif => pif.mac,
+ name: _('pifMacLabel'),
+ sortCriteria: 'mac',
+ },
+ {
+ itemRenderer: pif => pif.mtu,
+ name: _('pifMtuLabel'),
+ sortCriteria: 'mtu',
+ },
+ {
+ itemRenderer: (pif, userData) => (
+
+ ),
+ name: _('defaultLockingMode'),
+ },
+ {
+ itemRenderer: pif => (
+
+ {' '}
+
+
+
+
+ ),
+ name: _('pifStatusLabel'),
+ },
+]
+
+const INDIVIDUAL_ACTIONS = [
+ {
+ handler: deletePif,
+ icon: 'delete',
+ label: _('deletePif'),
+ level: 'danger',
+ },
+]
+
+const GROUPED_ACTIONS = [
+ {
+ handler: deletePifs,
+ icon: 'delete',
+ label: _('deletePifs'),
+ level: 'danger',
+ },
+]
+
+export default class TabNetwork extends Component {
+ render () {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/host/tab-patches.js b/packages/xo-web/src/xo-app/host/tab-patches.js
new file mode 100644
index 000000000..c4d4f8836
--- /dev/null
+++ b/packages/xo-web/src/xo-app/host/tab-patches.js
@@ -0,0 +1,238 @@
+import _ from 'intl'
+import ActionRowButton from 'action-row-button'
+import React, { Component } from 'react'
+import SortedTable from 'sorted-table'
+import TabButton from 'tab-button'
+import Upgrade from 'xoa-upgrade'
+import { chooseAction } from 'modal'
+import { connectStore, formatSize } from 'utils'
+import { Container, Row, Col } from 'grid'
+import { createDoesHostNeedRestart, createSelector } from 'selectors'
+import { FormattedRelative, FormattedTime } from 'react-intl'
+import { restartHost } from 'xo'
+import { isEmpty, isString } from 'lodash'
+
+const MISSING_PATCH_COLUMNS = [
+ {
+ name: _('patchNameLabel'),
+ itemRenderer: patch => patch.name,
+ sortCriteria: patch => patch.name,
+ },
+ {
+ name: _('patchDescription'),
+ itemRenderer: patch => (
+
+ {patch.description}
+
+ ),
+ sortCriteria: patch => patch.description,
+ },
+ {
+ name: _('patchReleaseDate'),
+ itemRenderer: patch => (
+
+ {' '}
+ ( )
+
+ ),
+ sortCriteria: patch => patch.date,
+ sortOrder: 'desc',
+ },
+ {
+ name: _('patchGuidance'),
+ itemRenderer: patch => patch.guidance,
+ sortCriteria: patch => patch.guidance,
+ },
+ {
+ name: _('patchAction'),
+ itemRenderer: (patch, { installPatch, _installPatchWarning }) => (
+ _installPatchWarning(patch, installPatch)}
+ icon='host-patch-update'
+ />
+ ),
+ },
+]
+
+const INSTALLED_PATCH_COLUMNS = [
+ {
+ name: _('patchNameLabel'),
+ itemRenderer: patch => patch.poolPatch.name,
+ sortCriteria: patch => patch.poolPatch.name,
+ },
+ {
+ name: _('patchDescription'),
+ itemRenderer: patch => patch.poolPatch.description,
+ sortCriteria: patch => patch.poolPatch.description,
+ },
+ {
+ default: true,
+ name: _('patchApplied'),
+ itemRenderer: patch => {
+ const time = patch.time * 1000
+ return (
+
+ {' '}
+ ( )
+
+ )
+ },
+ sortCriteria: patch => patch.time,
+ sortOrder: 'desc',
+ },
+ {
+ name: _('patchSize'),
+ itemRenderer: patch => formatSize(patch.poolPatch.size),
+ sortCriteria: patch => patch.poolPatch.size,
+ },
+]
+
+// support for software_version.platform_version ^2.1.1
+const INSTALLED_PATCH_COLUMNS_2 = [
+ {
+ default: true,
+ name: _('patchNameLabel'),
+ itemRenderer: patch => patch.name,
+ sortCriteria: patch => patch.name,
+ },
+ {
+ name: _('patchDescription'),
+ itemRenderer: patch => patch.description,
+ sortCriteria: patch => patch.description,
+ },
+ {
+ name: _('patchSize'),
+ itemRenderer: patch => formatSize(patch.size),
+ sortCriteria: patch => patch.size,
+ },
+]
+
+@connectStore(() => ({
+ needsRestart: createDoesHostNeedRestart((_, props) => props.host),
+}))
+export default class HostPatches extends Component {
+ static contextTypes = {
+ router: React.PropTypes.object,
+ }
+
+ _chooseActionPatch = async doInstall => {
+ const choice = await chooseAction({
+ body: {_('installPatchWarningContent')}
,
+ buttons: [
+ {
+ label: _('installPatchWarningResolve'),
+ value: 'install',
+ btnStyle: 'primary',
+ },
+ { label: _('installPatchWarningReject'), value: 'goToPool' },
+ ],
+ title: _('installPatchWarningTitle'),
+ })
+
+ return choice === 'install'
+ ? doInstall()
+ : this.context.router.push(`/pools/${this.props.host.$pool}/patches`)
+ }
+
+ _installPatchWarning = (patch, installPatch) =>
+ this._chooseActionPatch(() => installPatch(patch))
+
+ _installAllPatchesWarning = installAllPatches =>
+ this._chooseActionPatch(installAllPatches)
+
+ _getPatches = createSelector(
+ () => this.props.host,
+ () => this.props.hostPatches,
+ (host, hostPatches) => {
+ if (isEmpty(host.patches) && isEmpty(hostPatches)) {
+ return { patches: null }
+ }
+
+ if (isString(host.patches[0])) {
+ return {
+ patches: hostPatches,
+ columns: INSTALLED_PATCH_COLUMNS,
+ }
+ }
+
+ return {
+ patches: host.patches,
+ columns: INSTALLED_PATCH_COLUMNS_2,
+ }
+ }
+ )
+
+ render () {
+ const { host, missingPatches, installAllPatches, installPatch } = this.props
+ const { patches, columns } = this._getPatches()
+ const hasMissingPatches = !isEmpty(missingPatches)
+ return process.env.XOA_PLAN > 1 ? (
+
+
+
+ {this.props.needsRestart &&
+ isEmpty(missingPatches) && (
+
+ )}
+
+
+
+ {hasMissingPatches && (
+
+
+ {_('hostMissingPatches')}
+
+
+
+ )}
+
+
+ {patches ? (
+
+ {_('hostAppliedPatches')}
+
+
+ ) : (
+ {_('patchNothing')}
+ )}
+
+
+
+ ) : (
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/host/tab-stats.js b/packages/xo-web/src/xo-app/host/tab-stats.js
new file mode 100644
index 000000000..fb5b5e70b
--- /dev/null
+++ b/packages/xo-web/src/xo-app/host/tab-stats.js
@@ -0,0 +1,188 @@
+import _ from 'intl'
+import Component from 'base-component'
+import Icon from 'icon'
+import React from 'react'
+import Tooltip from 'tooltip'
+import Upgrade from 'xoa-upgrade'
+import { Container, Row, Col } from 'grid'
+import { Toggle } from 'form'
+import { fetchHostStats } from 'xo'
+import {
+ CpuLineChart,
+ MemoryLineChart,
+ PifLineChart,
+ LoadLineChart,
+} from 'xo-line-chart'
+
+export default class HostStats extends Component {
+ constructor (props) {
+ super(props)
+ this.state.useCombinedValues = false
+ }
+
+ loop (host = this.props.host) {
+ if (this.cancel) {
+ this.cancel()
+ }
+
+ if (host.power_state !== 'Running') {
+ return
+ }
+
+ let cancelled = false
+ this.cancel = () => {
+ cancelled = true
+ }
+
+ fetchHostStats(host, this.state.granularity).then(stats => {
+ if (cancelled) {
+ return
+ }
+ this.cancel = null
+
+ clearTimeout(this.timeout)
+ this.setState(
+ {
+ stats,
+ selectStatsLoading: false,
+ },
+ () => {
+ this.timeout = setTimeout(this.loop, stats.interval * 1000)
+ }
+ )
+ })
+ }
+ loop = ::this.loop
+
+ componentWillMount () {
+ this.loop()
+ }
+
+ componentWillUnmount () {
+ clearTimeout(this.timeout)
+ }
+
+ componentWillReceiveProps (props) {
+ const hostCur = this.props.host
+ const hostNext = props.host
+
+ if (
+ hostCur.power_state !== 'Running' &&
+ hostNext.power_state === 'Running'
+ ) {
+ this.loop(hostNext)
+ } else if (
+ hostCur.power_state === 'Running' &&
+ hostNext.power_state !== 'Running'
+ ) {
+ this.setState({
+ stats: undefined,
+ })
+ }
+ }
+
+ handleSelectStats (event) {
+ const granularity = event.target.value
+ clearTimeout(this.timeout)
+
+ this.setState(
+ {
+ granularity,
+ selectStatsLoading: true,
+ },
+ this.loop
+ )
+ }
+ handleSelectStats = ::this.handleSelectStats
+
+ render () {
+ const {
+ granularity,
+ selectStatsLoading,
+ stats,
+ useCombinedValues,
+ } = this.state
+
+ return !stats ? (
+ No stats.
+ ) : process.env.XOA_PLAN > 2 ? (
+
+
+
+
+
+
+
+
+
+
+ {selectStatsLoading && (
+
+
+
+ )}
+
+
+
+
+ {_('statLastTenMinutes', message => (
+ {message}
+ ))}
+ {_('statLastTwoHours', message => (
+ {message}
+ ))}
+ {_('statLastWeek', message => (
+ {message}
+ ))}
+ {_('statLastYear', message => (
+ {message}
+ ))}
+
+
+
+
+
+
+
+ {_('statsCpu')}
+
+
+
+
+
+ {_('statsMemory')}
+
+
+
+
+
+
+
+
+
+ {_('statsNetwork')}
+
+
+
+
+
+ {_('statLoad')}
+
+
+
+
+
+ ) : (
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/host/tab-storage.js b/packages/xo-web/src/xo-app/host/tab-storage.js
new file mode 100644
index 000000000..11e48fe7d
--- /dev/null
+++ b/packages/xo-web/src/xo-app/host/tab-storage.js
@@ -0,0 +1,150 @@
+import _ from 'intl'
+import ActionRowButton from 'action-row-button'
+import isEmpty from 'lodash/isEmpty'
+import Link from 'link'
+import map from 'lodash/map'
+import React from 'react'
+import SortedTable from 'sorted-table'
+import StateButton from 'state-button'
+import Tooltip from 'tooltip'
+import { connectPbd, disconnectPbd, deletePbd, editSr, isSrShared } from 'xo'
+import { connectStore, formatSize } from 'utils'
+import { Container, Row, Col } from 'grid'
+import { createGetObjectsOfType, createSelector } from 'selectors'
+import { TabButtonLink } from 'tab-button'
+import { Text } from 'editable'
+
+const SR_COLUMNS = [
+ {
+ name: _('srName'),
+ itemRenderer: storage => (
+
+ editSr(storage.id, { nameLabel })}
+ useLongClick
+ value={storage.nameLabel}
+ />
+
+ ),
+ sortCriteria: 'nameLabel',
+ },
+ {
+ name: _('srFormat'),
+ itemRenderer: storage => storage.format,
+ sortCriteria: 'format',
+ },
+ {
+ name: _('srSize'),
+ itemRenderer: storage => formatSize(storage.size),
+ sortCriteria: 'size',
+ },
+ {
+ default: true,
+ name: _('srUsage'),
+ itemRenderer: storage =>
+ storage.size !== 0 && (
+
+
+
+ ),
+ sortCriteria: storage => storage.usagePercentage,
+ sortOrder: 'desc',
+ },
+ {
+ name: _('srType'),
+ itemRenderer: storage =>
+ storage.shared ? _('srShared') : _('srNotShared'),
+ sortCriteria: 'shared',
+ },
+ {
+ name: _('pbdStatus'),
+ itemRenderer: storage => (
+
+ ),
+ },
+ {
+ name: _('pbdAction'),
+ itemRenderer: storage =>
+ !storage.attached && (
+
+ ),
+ textAlign: 'right',
+ },
+]
+
+export default connectStore(() => {
+ const pbds = createGetObjectsOfType('PBD').pick(
+ (_, props) => props.host.$PBDs
+ )
+ const srs = createGetObjectsOfType('SR').pick(
+ createSelector(pbds, pbds => map(pbds, pbd => pbd.SR))
+ )
+
+ const storages = createSelector(pbds, srs, (pbds, srs) =>
+ map(pbds, pbd => {
+ const sr = srs[pbd.SR]
+ const { physical_usage: usage, size } = sr
+
+ return {
+ attached: pbd.attached,
+ format: sr.SR_type,
+ free: size > 0 ? size - usage : 0,
+ id: sr.id,
+ nameLabel: sr.name_label,
+ pbdId: pbd.id,
+ shared: isSrShared(sr),
+ size: size > 0 ? size : 0,
+ usagePercentage: size > 0 && Math.round(100 * usage / size),
+ }
+ })
+ )
+
+ return { storages }
+})(({ host, storages }) => (
+
+
+
+
+
+
+
+
+ {isEmpty(storages) ? (
+ {_('pbdNoSr')}
+ ) : (
+
+ )}
+
+
+
+))
diff --git a/packages/xo-web/src/xo-app/index.js b/packages/xo-web/src/xo-app/index.js
new file mode 100644
index 000000000..0a69d1230
--- /dev/null
+++ b/packages/xo-web/src/xo-app/index.js
@@ -0,0 +1,233 @@
+import Component from 'base-component'
+import cookies from 'cookies-js'
+import DocumentTitle from 'react-document-title'
+import Icon from 'icon'
+import isArray from 'lodash/isArray'
+import map from 'lodash/map'
+import PropTypes from 'prop-types'
+import React from 'react'
+import Shortcuts from 'shortcuts'
+import themes from 'themes'
+import _, { IntlProvider } from 'intl'
+import { blockXoaAccess } from 'xoa-updater'
+import { connectStore, routes } from 'utils'
+import { Notification } from 'notification'
+import { ShortcutManager } from 'react-shortcuts'
+import { ThemeProvider } from 'styled-components'
+import { TooltipViewer } from 'tooltip'
+import { Container, Row, Col } from 'grid'
+// import {
+// keyHandler
+// } from 'react-key-handler'
+
+import About from './about'
+import Backup from './backup'
+import Dashboard from './dashboard'
+import Home from './home'
+import Host from './host'
+import Jobs from './jobs'
+import Menu from './menu'
+import Modal, { alert } from 'modal'
+import New from './new'
+import NewVm from './new-vm'
+import Pool from './pool'
+import Self from './self'
+import Settings from './settings'
+import Sr from './sr'
+import Tasks from './tasks'
+import User from './user'
+import Vm from './vm'
+import VmImport from './vm-import'
+import Xoa from './xoa'
+import XoaUpdates from './xoa/update'
+import Xosan from './xosan'
+
+import keymap, { help } from '../keymap'
+
+const shortcutManager = new ShortcutManager(keymap)
+
+const CONTAINER_STYLE = {
+ display: 'flex',
+ minHeight: '100vh',
+
+ // FIXME: The size of `xo-main` matches the size of the window
+ // thanks to the, flex growing feature.
+ //
+ // Therefore, when there is a scrollbar on the right side, `xo-main`
+ // is too large (since the scrollbar uses a few, pixels) which makes
+ // an almost useless horizontal scrollbar appear.
+ overflow: 'hidden',
+}
+const BODY_WRAPPER_STYLE = {
+ flex: 1,
+ position: 'relative',
+}
+const BODY_STYLE = {
+ height: '100%',
+ left: 0,
+ overflow: 'auto',
+ position: 'absolute',
+ top: 0,
+ width: '100%',
+}
+
+@routes('home', {
+ about: About,
+ backup: Backup,
+ dashboard: Dashboard,
+ home: Home,
+ 'hosts/:id': Host,
+ jobs: Jobs,
+ new: New,
+ 'pools/:id': Pool,
+ self: Self,
+ settings: Settings,
+ 'srs/:id': Sr,
+ tasks: Tasks,
+ user: User,
+ 'vms/import': VmImport,
+ 'vms/new': NewVm,
+ 'vms/:id': Vm,
+ xoa: Xoa,
+ xosan: Xosan,
+})
+@connectStore(state => {
+ return {
+ trial: state.xoaTrialState,
+ signedUp: !!state.user,
+ }
+})
+export default class XoApp extends Component {
+ static contextTypes = {
+ router: PropTypes.object,
+ }
+ static childContextTypes = {
+ shortcuts: PropTypes.object.isRequired,
+ }
+ getChildContext = () => ({ shortcuts: shortcutManager })
+
+ displayOpenSourceDisclaimer () {
+ const previousDisclaimer = cookies.get('previousDisclaimer')
+ const now = Math.floor(Date.now() / 1e3)
+ const oneWeekAgo = now - 7 * 24 * 3600
+ if (!previousDisclaimer || previousDisclaimer < oneWeekAgo) {
+ alert(
+ _('disclaimerTitle'),
+
+ )
+ cookies.set('previousDisclaimer', now)
+ }
+ }
+
+ componentDidMount () {
+ this.refs.bodyWrapper.style.minHeight =
+ this.refs.menu.getWrappedInstance().height + 'px'
+ if (+process.env.XOA_PLAN === 5) {
+ this.displayOpenSourceDisclaimer()
+ }
+ }
+
+ _shortcutsHandler = (command, event) => {
+ event.preventDefault()
+ switch (command) {
+ case 'GO_TO_HOSTS':
+ this.context.router.push('home?t=host')
+ break
+ case 'GO_TO_POOLS':
+ this.context.router.push('home?t=pool')
+ break
+ case 'GO_TO_VMS':
+ this.context.router.push('home?t=VM')
+ break
+ case 'GO_TO_SRS':
+ this.context.router.push('home?t=SR')
+ break
+ case 'CREATE_VM':
+ this.context.router.push('vms/new')
+ break
+ case 'UNFOCUS':
+ if (event.target.tagName === 'INPUT') {
+ event.target.blur()
+ }
+ break
+ case 'HELP':
+ alert(
+
+ {_('shortcutModalTitle')}
+ ,
+
+ {map(
+ help,
+ (context, contextKey) =>
+ context.name && [
+
+
+ {context.name}
+
+
,
+ ...map(
+ context.shortcuts,
+ ({ message, keys }, key) =>
+ message && (
+
+
+ {isArray(keys) ? keys[0] : keys}
+
+ {message}
+
+ )
+ ),
+ ]
+ )}
+
+ )
+ break
+ }
+ }
+
+ render () {
+ const { signedUp, trial } = this.props
+ const blocked = signedUp && blockXoaAccess(trial) // If we are under expired or unstable trial (signed up only)
+
+ return (
+
+
+
+
+
+
+
+
+ {blocked ? (
+
+ ) : signedUp ? (
+ this.props.children
+ ) : (
+
Still loading
+ )}
+
+
+
+
+
+
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/jobs/edit/index.js b/packages/xo-web/src/xo-app/jobs/edit/index.js
new file mode 100644
index 000000000..245b04105
--- /dev/null
+++ b/packages/xo-web/src/xo-app/jobs/edit/index.js
@@ -0,0 +1,4 @@
+import New from '../new'
+import React from 'react'
+
+export default props =>
diff --git a/packages/xo-web/src/xo-app/jobs/index.js b/packages/xo-web/src/xo-app/jobs/index.js
new file mode 100644
index 000000000..a99bcdc79
--- /dev/null
+++ b/packages/xo-web/src/xo-app/jobs/index.js
@@ -0,0 +1,52 @@
+import _ from 'intl'
+import Icon from 'icon'
+import Page from '../page'
+import React from 'react'
+import { Container, Row, Col } from 'grid'
+import { NavLink, NavTabs } from 'nav'
+import { routes } from 'utils'
+
+import Edit from './edit'
+import New from './new'
+import Overview from './overview'
+import Schedules from './schedules'
+import EditSchedule from './schedules/edit'
+
+const HEADER = (
+
+
+
+
+ {_('jobsPage')}
+
+
+
+
+
+ {_('jobsOverviewPage')}
+
+
+ {_('jobsNewPage')}
+
+
+ {_('jobsSchedulingPage')}
+
+
+
+
+
+)
+
+const Jobs = routes('overview', {
+ ':id/edit': Edit,
+ new: New,
+ overview: Overview,
+ schedules: Schedules,
+ 'schedules/:id/edit': EditSchedule,
+})(({ children }) => (
+
+ {children}
+
+))
+
+export default Jobs
diff --git a/packages/xo-web/src/xo-app/jobs/new/index.js b/packages/xo-web/src/xo-app/jobs/new/index.js
new file mode 100644
index 000000000..344e0a5ca
--- /dev/null
+++ b/packages/xo-web/src/xo-app/jobs/new/index.js
@@ -0,0 +1,504 @@
+import _, { messages } from 'intl'
+import ActionButton from 'action-button'
+import ActionRowButton from 'action-row-button'
+import Button from 'button'
+import Component from 'base-component'
+import defined from 'xo-defined'
+import delay from 'lodash/delay'
+import find from 'lodash/find'
+import forEach from 'lodash/forEach'
+import GenericInput from 'json-schema-input'
+import Icon from 'icon'
+import includes from 'lodash/includes'
+import isEmpty from 'lodash/isEmpty'
+import map from 'lodash/map'
+import mapValues from 'lodash/mapValues'
+import React from 'react'
+import Select from 'form/select'
+import size from 'lodash/size'
+import Tooltip from 'tooltip'
+import Upgrade from 'xoa-upgrade'
+import { addSubscriptions } from 'utils'
+import { createSelector } from 'selectors'
+import { error } from 'notification'
+import { generateUiSchema } from 'xo-json-schema-input'
+import { injectIntl } from 'react-intl'
+import { SelectSubject } from 'select-objects'
+
+import {
+ apiMethods,
+ createJob,
+ deleteJob,
+ editJob,
+ runJob,
+ subscribeCurrentUser,
+ subscribeJobs,
+ subscribeUsers,
+} from 'xo'
+
+const JOB_KEY = 'genericTask'
+
+const getType = function (param) {
+ if (!param) {
+ return
+ }
+ if (Array.isArray(param.type)) {
+ if (includes(param.type, 'integer')) {
+ return 'integer'
+ }
+ if (includes(param.type, 'number')) {
+ return 'number'
+ }
+ return 'string'
+ }
+ return param.type
+}
+
+/**
+ * Tries extracting Object targeted property
+ */
+const reduceObject = (value, propertyName = 'id') =>
+ (value != null && value[propertyName]) || value
+
+/**
+ * Adapts all data "arrayed" by UI-multiple-selectors to job's cross-product trick
+ */
+const dataToParamVectorItems = function (params, data) {
+ const items = []
+ forEach(params, (param, name) => {
+ if (Array.isArray(data[name]) && param.items) {
+ // We have an array for building cross product, the "real" type was $type
+ const values = []
+ if (data[name].length === 1) {
+ // One value, no need to engage cross-product
+ data[name] = data[name].pop()
+ } else {
+ forEach(data[name], value => {
+ values.push({ [name]: reduceObject(value, name) })
+ })
+ if (values.length) {
+ items.push({
+ type: 'set',
+ values,
+ })
+ }
+ delete data[name]
+ }
+ }
+ })
+ if (size(data)) {
+ items.push({
+ type: 'set',
+ values: [mapValues(data, reduceObject)],
+ })
+ }
+ return items
+}
+
+@addSubscriptions({
+ users: subscribeUsers,
+ currentUser: subscribeCurrentUser,
+})
+@injectIntl
+export default class Jobs extends Component {
+ constructor (props) {
+ super(props)
+
+ this.state = {
+ action: undefined,
+ actions: undefined,
+ job: undefined,
+ jobs: undefined,
+ }
+ new Promise((resolve, reject) => {
+ this._resolveLoaded = resolve
+ }).then(() => {
+ const { id } = this.props
+ if (id) {
+ this._edit(id)
+ }
+ })
+ }
+
+ componentWillMount () {
+ this.componentWillUnmount = subscribeJobs(jobs => {
+ const j = {}
+ for (const id in jobs) {
+ const job = jobs[id]
+ job && job.key === JOB_KEY && (j[id] = job)
+ }
+ this.setState({ jobs: j }, this._resolveLoaded)
+ })
+
+ const jobCompliantMethods = [
+ 'acl.add',
+ 'acl.remove',
+ 'host.detach',
+ 'host.disable',
+ 'host.enable',
+ 'host.installAllPatches',
+ 'host.restart',
+ 'host.restartAgent',
+ 'host.set',
+ 'host.start',
+ 'host.stop',
+ 'job.runSequence',
+ 'vm.attachDisk',
+ 'vm.backup',
+ 'vm.clone',
+ 'vm.convert',
+ 'vm.copy',
+ 'vm.creatInterface',
+ 'vm.delete',
+ 'vm.migrate',
+ 'vm.migrate',
+ 'vm.restart',
+ 'vm.resume',
+ 'vm.revert',
+ 'vm.rollingBackup',
+ 'vm.rollingDrCopy',
+ 'vm.rollingSnapshot',
+ 'vm.set',
+ 'vm.setBootOrder',
+ 'vm.snapshot',
+ 'vm.start',
+ 'vm.stop',
+ 'vm.suspend',
+ ]
+ apiMethods.then(methods => {
+ const actions = []
+
+ for (const method in methods) {
+ if (includes(jobCompliantMethods, method)) {
+ const [group, command] = method.split('.')
+ const info = { ...methods[method] }
+ info.type = 'object'
+
+ const properties = { ...info.params }
+ delete info.params
+
+ const required = []
+ for (const key in properties) {
+ const property = { ...properties[key] }
+ const type = getType(property)
+
+ const modifyProperty = (prop, type) => {
+ const titles = {
+ Host: 'Host(s)',
+ Pool: 'Pool(s)',
+ Role: 'Role(s)',
+ Sr: 'Storage(s)',
+ Subject: 'Subject(s)',
+ Vm: 'VM(s)',
+ XoObject: 'Object(s)',
+ }
+ prop.type = 'array'
+ prop.items = {
+ type: 'string',
+ $type: type,
+ }
+ prop.title = titles[type]
+ }
+
+ if (type === 'string') {
+ if (group === 'acl') {
+ if (key === 'object') {
+ modifyProperty(property, 'XoObject')
+ } else if (key === 'action') {
+ modifyProperty(property, 'Role')
+ } else if (key === 'subject') {
+ modifyProperty(property, 'Subject')
+ }
+ } else if (group === 'host' && key === 'id') {
+ modifyProperty(property, 'Host')
+ } else if (group === 'vm' && key === 'id') {
+ modifyProperty(property, 'Vm')
+ } else {
+ if (includes(['pool', 'pool_id', 'target_pool_id'], key)) {
+ modifyProperty(property, 'Pool')
+ } else if (includes(['sr', 'sr_id', 'target_sr_id'], key)) {
+ modifyProperty(property, 'Sr')
+ } else if (
+ includes(
+ ['host', 'host_id', 'target_host_id', 'targetHost'],
+ key
+ )
+ ) {
+ modifyProperty(property, 'Host')
+ } else if (includes(['vm'], key)) {
+ modifyProperty(property, 'Vm')
+ }
+ }
+ }
+ if (!property.optional) {
+ required.push(key)
+ }
+ properties[key] = property
+ }
+ !isEmpty(required) && (info.required = required)
+ info.properties = properties
+
+ actions.push({
+ method,
+ group,
+ command,
+ info,
+ uiSchema: generateUiSchema(info),
+ })
+ }
+ }
+
+ this.setState({ actions })
+ })
+ }
+
+ _handleSelectMethod = action => this.setState({ action })
+
+ _handleSubmit = () => {
+ const { name, method, params } = this.refs
+
+ const { job, owner, timeout } = this.state
+ const _job = {
+ type: 'call',
+ name: name.value,
+ key: JOB_KEY,
+ method: method.value.method,
+ paramsVector: {
+ type: 'crossProduct',
+ items: dataToParamVectorItems(
+ method.value.info.properties,
+ params.value
+ ),
+ },
+ userId: owner !== undefined ? owner : this.props.currentUser.id,
+ timeout: timeout ? timeout * 1e3 : undefined,
+ }
+
+ job && (_job.id = job.id)
+ const saveJob = job ? editJob : createJob
+
+ return saveJob(_job)
+ .then(this._reset)
+ .catch(err => error('Create Job', err.message || String(err)))
+ }
+
+ _edit = id => {
+ const { jobs, actions } = this.state
+ const job = find(jobs, job => job.id === id)
+ if (!job) {
+ error('Job edition', 'This job was not found, or may not longer exists.')
+ return
+ }
+
+ const { name, method } = this.refs
+ const action = find(actions, action => action.method === job.method)
+ name.value = job.name
+ method.value = action
+ this.setState(
+ {
+ job,
+ action,
+ },
+ () => delay(this._populateForm, 250, job)
+ ) // Work around.
+ // Without the delay, some selects are not always ready to load a value
+ // Values are displayed, but html5 compliant browsers say the value is required and empty on submit
+ }
+
+ _populateForm = job => {
+ const data = {}
+ const paramsVector = job.paramsVector
+ if (paramsVector) {
+ if (paramsVector.type !== 'crossProduct') {
+ throw new Error(`Unknown parameter-vector type ${paramsVector.type}`)
+ }
+ forEach(paramsVector.items, item => {
+ if (item.type !== 'set') {
+ throw new Error(`Unknown parameter-vector item type ${item.type}`)
+ }
+ forEach(item.values, valueItem => {
+ forEach(valueItem, (value, key) => {
+ if (data[key] === undefined) {
+ data[key] = []
+ }
+ data[key].push(value)
+ })
+ })
+ })
+ }
+ const { params } = this.refs
+ params.value = data
+ this.setState({
+ owner: job.userId,
+ timeout: job.timeout && job.timeout / 1e3,
+ })
+ }
+
+ _reset = () => {
+ const { name, method } = this.refs
+ name.value = ''
+ method.value = undefined
+ this.setState({
+ action: undefined,
+ job: undefined,
+ owner: undefined,
+ timeout: '',
+ })
+ }
+
+ _getIsJobUserMissing = createSelector(
+ () => this.state.jobs,
+ () => this.props.users,
+ (jobs, users) => {
+ const isJobUserMissing = {}
+ forEach(jobs, job => {
+ isJobUserMissing[job.id] = !!find(users, user => user.id === job.userId)
+ })
+
+ return isJobUserMissing
+ }
+ )
+
+ _subjectPredicate = ({ type, permission }) =>
+ type === 'user' && permission === 'admin'
+
+ render () {
+ const { props, state } = this
+ const { action, actions, job, jobs } = state
+ const { formatMessage } = this.props.intl
+
+ const isJobUserMissing = this._getIsJobUserMissing()
+
+ return (
+
+
{_('jobsPage')}
+
+
+
+
+ {_('jobName')}
+ {_('jobAction')}
+
+
+
+
+
+ {isEmpty(jobs) && (
+
+
+ {_('noJobs')}
+
+
+ )}
+ {map(jobs, job => (
+
+
+
+ {job.name}{' '}
+ ({job.id.slice(4, 8)})
+
+
+ {job.method}
+
+
+ {!isJobUserMissing[job.id] && (
+
+
+
+ )}
+
+
+ {' '}
+
+
+
+ ))}
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/jobs/overview/index.js b/packages/xo-web/src/xo-app/jobs/overview/index.js
new file mode 100644
index 000000000..53450b25d
--- /dev/null
+++ b/packages/xo-web/src/xo-app/jobs/overview/index.js
@@ -0,0 +1,225 @@
+import _ from 'intl'
+import ActionRowButton from 'action-row-button'
+import filter from 'lodash/filter'
+import find from 'lodash/find'
+import forEach from 'lodash/forEach'
+import Icon from 'icon'
+import Link from 'link'
+import LogList from '../../logs'
+import map from 'lodash/map'
+import orderBy from 'lodash/orderBy'
+import React, { Component } from 'react'
+import StateButton from 'state-button'
+import Tooltip from 'tooltip'
+import Upgrade from 'xoa-upgrade'
+import { addSubscriptions } from 'utils'
+import { Container } from 'grid'
+import { createSelector } from 'selectors'
+import { Card, CardHeader, CardBlock } from 'card'
+import {
+ deleteSchedule,
+ disableSchedule,
+ enableSchedule,
+ runJob,
+ subscribeJobs,
+ subscribeSchedules,
+ subscribeScheduleTable,
+ subscribeUsers,
+} from 'xo'
+
+// ===================================================================
+
+const jobKeyToLabel = {
+ genericTask: _('customJob'),
+}
+
+// ===================================================================
+
+@addSubscriptions({
+ users: subscribeUsers,
+})
+export default class Overview extends Component {
+ constructor (props) {
+ super(props)
+ this.state = {
+ schedules: [],
+ scheduleTable: {},
+ }
+ }
+
+ componentWillMount () {
+ const unsubscribeJobs = subscribeJobs(jobs => {
+ const obj = {}
+ forEach(jobs, job => {
+ obj[job.id] = job
+ })
+
+ this.setState({
+ jobs: obj,
+ })
+ })
+
+ const unsubscribeSchedules = subscribeSchedules(schedules => {
+ // Get only backup jobs.
+ schedules = filter(schedules, schedule => {
+ const job = this._getScheduleJob(schedule)
+ return job && jobKeyToLabel[job.key]
+ })
+
+ this.setState({
+ schedules: orderBy(schedules, schedule => +schedule.id.split(':')[1], [
+ 'desc',
+ ]),
+ })
+ })
+
+ const unsubscribeScheduleTable = subscribeScheduleTable(scheduleTable => {
+ this.setState({
+ scheduleTable,
+ })
+ })
+
+ this.componentWillUnmount = () => {
+ unsubscribeJobs()
+ unsubscribeSchedules()
+ unsubscribeScheduleTable()
+ }
+ }
+
+ _getScheduleJob (schedule) {
+ const { jobs } = this.state || {}
+ return jobs[schedule.job]
+ }
+
+ _getJobLabel (job = {}) {
+ return `${job.name} - ${job.method} (${job.id.slice(4, 8)})`
+ }
+
+ _getScheduleLabel (schedule) {
+ return `${schedule.name} (${schedule.id.slice(4, 8)})`
+ }
+
+ _getScheduleToggle (schedule) {
+ const { id } = schedule
+
+ return (
+
+ )
+ }
+
+ _getIsScheduleUserMissing = createSelector(
+ () => this.state.schedules,
+ () => this.props.users,
+ (schedules, users) => {
+ const isScheduleUserMissing = {}
+
+ forEach(schedules, schedule => {
+ isScheduleUserMissing[schedule.id] = !!find(
+ users,
+ user => user.id === this._getScheduleJob(schedule).userId
+ )
+ })
+
+ return isScheduleUserMissing
+ }
+ )
+
+ render () {
+ const { schedules } = this.state
+
+ const isScheduleUserMissing = this._getIsScheduleUserMissing()
+
+ return process.env.XOA_PLAN > 3 ? (
+
+
+
+ {_('backupSchedules')}
+
+
+ {schedules.length ? (
+
+
+
+ {_('schedule')}
+ {_('job')}
+ {_('jobScheduling')}
+ {_('jobState')}
+ {_('jobAction')}
+
+
+
+ {map(schedules, (schedule, key) => {
+ const job = this._getScheduleJob(schedule)
+
+ return (
+
+
+ {this._getScheduleLabel(schedule)}
+
+
+
+
+
+ {this._getJobLabel(job)}
+
+
+
+
+ {schedule.cron}
+ {this._getScheduleToggle(schedule)}
+
+
+ {!isScheduleUserMissing[schedule.id] && (
+
+
+
+ )}
+
+
+
+
+
+ )
+ })}
+
+
+ ) : (
+ {_('noScheduledJobs')}
+ )}
+
+
+
+
+ ) : (
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/jobs/schedules/edit/index.js b/packages/xo-web/src/xo-app/jobs/schedules/edit/index.js
new file mode 100644
index 000000000..f4d7e5cc4
--- /dev/null
+++ b/packages/xo-web/src/xo-app/jobs/schedules/edit/index.js
@@ -0,0 +1,4 @@
+import Schedule from '..'
+import React from 'react'
+
+export default props =>
diff --git a/packages/xo-web/src/xo-app/jobs/schedules/index.js b/packages/xo-web/src/xo-app/jobs/schedules/index.js
new file mode 100644
index 000000000..c142557b7
--- /dev/null
+++ b/packages/xo-web/src/xo-app/jobs/schedules/index.js
@@ -0,0 +1,294 @@
+import _, { messages } from 'intl'
+import ActionButton from 'action-button'
+import Button from 'button'
+import find from 'lodash/find'
+import isEmpty from 'lodash/isEmpty'
+import map from 'lodash/map'
+import SortedTable from 'sorted-table'
+import Upgrade from 'xoa-upgrade'
+import React, { Component } from 'react'
+import Scheduler, { SchedulePreview } from 'scheduling'
+import { error } from 'notification'
+import { injectIntl } from 'react-intl'
+import { Select, Toggle } from 'form'
+import {
+ createSchedule,
+ deleteSchedule,
+ deleteSchedules,
+ subscribeJobs,
+ subscribeSchedules,
+ editSchedule,
+} from 'xo'
+
+const JOB_KEY = 'genericTask'
+const DEFAULT_CRON_PATTERN = '0 0 * * *'
+const COLUMNS = [
+ {
+ itemRenderer: schedule => (
+
+ {schedule.name}{' '}
+ ({schedule.id.slice(4, 8)})
+
+ ),
+ name: _('jobName'),
+ sortCriteria: 'name',
+ default: true,
+ },
+ {
+ itemRenderer: (schedule, userData) => {
+ const job = userData.jobs[schedule.job]
+ if (job !== undefined) {
+ return (
+
+ {job.name} - {job.method}{' '}
+ ({schedule.job.slice(4, 8)})
+
+ )
+ }
+ },
+ name: _('job'),
+ sortCriteria: (schedule, userData) => userData.jobs[schedule.job].name,
+ },
+ {
+ itemRenderer: schedule => schedule.cron,
+ name: _('jobScheduling'),
+ },
+ {
+ itemRenderer: schedule => schedule.timezone || _('jobServerTimezone'),
+ name: _('jobTimezone'),
+ sortCriteria: 'timezone',
+ },
+]
+const GROUPED_ACTIONS = [
+ {
+ handler: deleteSchedules,
+ icon: 'delete',
+ label: _('deleteSelectedSchedules'),
+ level: 'danger',
+ },
+]
+
+@injectIntl
+export default class Schedules extends Component {
+ constructor (props) {
+ super(props)
+ this.state = {
+ action: undefined,
+ actions: undefined,
+ cronPattern: DEFAULT_CRON_PATTERN,
+ job: undefined,
+ jobs: undefined,
+ timezone: undefined,
+ }
+ this.loaded = new Promise((resolve, reject) => {
+ this._resolveLoaded = resolve
+ }).then(() => {
+ const { id } = this.props
+ if (id) {
+ this._edit(id)
+ }
+ })
+ }
+
+ componentWillMount () {
+ const unsubscribeJobs = subscribeJobs(jobs => {
+ const j = {}
+ for (const id in jobs) {
+ const job = jobs[id]
+ if (job && job.key === JOB_KEY) {
+ const _job = { ...job }
+ _job.label = `${_job.name} - ${_job.method} (${_job.id})`
+ j[job.id] = _job
+ }
+ }
+ this.setState({ jobs: j })
+ })
+
+ const unsubscribeSchedules = subscribeSchedules(schedules => {
+ const s = {}
+ const { jobs } = this.state
+ if (isEmpty(jobs)) {
+ return
+ }
+ for (const id in schedules) {
+ const schedule = schedules[id]
+ const scheduleJob = find(jobs, job => job.id === schedule.job)
+ if (scheduleJob && scheduleJob.key === JOB_KEY) {
+ s[id] = schedule
+ }
+ }
+ this.setState({ schedules: s }, this._resolveLoaded)
+ })
+
+ this.componentWillUnmount = () => {
+ unsubscribeJobs()
+ unsubscribeSchedules()
+ }
+ }
+
+ _handleSubmit = () => {
+ const { name, job, enabled } = this.refs
+ const { cronPattern, schedule, timezone } = this.state
+ let save
+ if (schedule) {
+ schedule.job = job.value.id
+ schedule.cron = cronPattern
+ schedule.name = name.value
+ schedule.timezone = timezone
+ save = editSchedule(schedule)
+ } else {
+ save = createSchedule(job.value.id, {
+ cron: cronPattern,
+ enabled: enabled.value,
+ name: name.value,
+ })
+ }
+ return save
+ .then(this._reset)
+ .catch(err => error('Save Schedule', err.message || String(err)))
+ }
+
+ _edit = id => {
+ const { schedules, jobs } = this.state
+ const schedule = find(schedules, schedule => schedule.id === id)
+ if (!schedule) {
+ error(
+ 'Schedule edition',
+ 'This schedule was not found, or may not longer exists.'
+ )
+ return
+ }
+
+ const { name, job } = this.refs
+ name.value = schedule.name
+ job.value = jobs[schedule.job]
+ this.setState({
+ cronPattern: schedule.cron,
+ schedule,
+ timezone: schedule.timezone || null,
+ })
+ }
+
+ _reset = () => {
+ this.setState(
+ {
+ cronPattern: DEFAULT_CRON_PATTERN,
+ schedule: undefined,
+ timezone: undefined,
+ },
+ () => {
+ const { name, job, enabled } = this.refs
+ name.value = ''
+ enabled.value = false
+ job.value = undefined
+ }
+ )
+ }
+
+ _updateCronPattern = value => {
+ this.setState(value)
+ }
+
+ individualActions = [
+ {
+ handler: job => this._edit(job.id),
+ icon: 'edit',
+ label: _('scheduleEdit'),
+ level: 'primary',
+ },
+ {
+ handler: deleteSchedule,
+ icon: 'delete',
+ label: _('scheduleDelete'),
+ level: 'danger',
+ },
+ ]
+
+ render () {
+ const { cronPattern, jobs, schedule, schedules, timezone } = this.state
+ const userData = { jobs }
+ return (
+
+
{_('newSchedule')}
+
+
+
+
+
+
+
+ {schedule && (
+
+ {_('scheduleEditMessage', {
+ name: schedule.name,
+ id: schedule.id,
+ })}
+
+ )}
+ {process.env.XOA_PLAN > 3 ? (
+
+
+ {_('saveBackupJob')}
+ {' '}
+ {_('selectTableReset')}
+
+ ) : (
+
+
+
+ )}
+
+ {schedules !== undefined && (
+
+
{_('jobSchedules')}
+
+
+ )}
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/logs/index.js b/packages/xo-web/src/xo-app/logs/index.js
new file mode 100644
index 000000000..a7ed5b935
--- /dev/null
+++ b/packages/xo-web/src/xo-app/logs/index.js
@@ -0,0 +1,472 @@
+import _, { FormattedDuration } from 'intl'
+import ActionButton from 'action-button'
+import ActionRowButton from 'action-row-button'
+import BaseComponent from 'base-component'
+import ButtonGroup from 'button-group'
+import classnames from 'classnames'
+import Icon from 'icon'
+import NoObjects from 'no-objects'
+import propTypes from 'prop-types-decorator'
+import React, { Component } from 'react'
+import renderXoItem from 'render-xo-item'
+import SortedTable from 'sorted-table'
+import Tooltip from 'tooltip'
+import { alert, confirm } from 'modal'
+import { createGetObject } from 'selectors'
+import { FormattedDate } from 'react-intl'
+import { connectStore, formatSize, formatSpeed } from 'utils'
+import { Card, CardHeader, CardBlock } from 'card'
+import { forEach, get, includes, isEmpty, map, orderBy } from 'lodash'
+import { deleteJobsLog, subscribeJobsLogs } from 'xo'
+
+// ===================================================================
+
+const jobKeyToLabel = {
+ continuousReplication: _('continuousReplication'),
+ deltaBackup: _('deltaBackup'),
+ disasterRecovery: _('disasterRecovery'),
+ genericTask: _('customJob'),
+ rollingBackup: _('backup'),
+ rollingSnapshot: _('rollingSnapshot'),
+}
+
+// ===================================================================
+
+@connectStore(() => ({ object: createGetObject() }))
+class JobParam extends Component {
+ render () {
+ const { object, paramKey, id } = this.props
+
+ return object != null
+ ? _.keyValue(object.type || paramKey, renderXoItem(object))
+ : _.keyValue(paramKey, String(id))
+ }
+}
+
+@connectStore(() => ({ object: createGetObject() }))
+class JobReturn extends Component {
+ render () {
+ const { object, id } = this.props
+
+ return (
+
+ {object ? renderXoItem(object) : String(id)}
+
+ )
+ }
+}
+
+const JobCallStateInfos = ({ end, error }) => {
+ const [icon, tooltip] =
+ error !== undefined
+ ? ['halted', 'failedJobCall']
+ : end !== undefined
+ ? ['running', 'successfulJobCall']
+ : ['busy', 'jobCallInProgess']
+
+ return (
+
+
+
+ )
+}
+
+const JobDataInfos = ({
+ jobDuration,
+ size,
+
+ transferDuration = jobDuration,
+ transferSize = size,
+ mergeDuration,
+ mergeSize,
+}) => (
+
+ {transferSize !== undefined && (
+
+ {_('jobTransferredDataSize')} {' '}
+ {formatSize(transferSize)}
+
+ {_('jobTransferredDataSpeed')} {' '}
+ {formatSpeed(transferSize, transferDuration)}
+
+ )}
+ {mergeSize !== undefined && (
+
+ {_('jobMergedDataSize')} {formatSize(mergeSize)}
+
+ {_('jobMergedDataSpeed')} {' '}
+ {formatSpeed(mergeSize, mergeDuration)}
+
+ )}
+
+)
+
+const CALL_FILTER_OPTIONS = [
+ { label: 'successfulJobCall', value: 'success' },
+ { label: 'failedJobCall', value: 'error' },
+ { label: 'jobCallInProgess', value: 'running' },
+ { label: 'allJobCalls', value: 'all' },
+]
+
+const PREDICATES = {
+ all: () => true,
+ error: call => call.error !== undefined,
+ running: call => call.end === undefined && call.error === undefined,
+ success: call => call.end !== undefined && call.error === undefined,
+}
+
+const UNHEALTHY_VDI_CHAIN_ERROR = 'unhealthy VDI chain'
+const UNHEALTHY_VDI_CHAIN_LINK =
+ 'https://xen-orchestra.com/docs/backup_troubleshooting.html#vdi-chain-protection'
+
+class Log extends BaseComponent {
+ state = {
+ filter: 'all',
+ }
+
+ render () {
+ const { props, state } = this
+ const predicate = PREDICATES[state.filter]
+
+ return (
+
+
+ {map(CALL_FILTER_OPTIONS, ({ label, value }) =>
+ _({ key: value }, label, message => (
+ {message}
+ ))
+ )}
+
+
+
+
+ )
+ }
+}
+
+const showCalls = log =>
+ alert(_('jobModalTitle', { job: log.jobId }), )
+
+const LOG_COLUMNS = [
+ {
+ name: _('jobId'),
+ itemRenderer: log => log.jobId,
+ sortCriteria: log => log.jobId,
+ },
+ {
+ name: _('jobType'),
+ itemRenderer: log => jobKeyToLabel[log.key],
+ sortCriteria: log => log.key,
+ },
+ {
+ name: _('jobTag'),
+ itemRenderer: log => get(log, 'calls[0].params.tag'),
+ sortCriteria: log => get(log, 'calls[0].params.tag'),
+ },
+ {
+ name: _('jobStart'),
+ itemRenderer: log =>
+ log.start && (
+
+ ),
+ sortCriteria: log => log.start,
+ sortOrder: 'desc',
+ },
+ {
+ default: true,
+ name: _('jobEnd'),
+ itemRenderer: log =>
+ log.end && (
+
+ ),
+ sortCriteria: log => log.end || log.start,
+ sortOrder: 'desc',
+ },
+ {
+ name: _('jobDuration'),
+ itemRenderer: log =>
+ log.duration && ,
+ sortCriteria: log => log.duration,
+ },
+ {
+ name: _('jobStatus'),
+ itemRenderer: log => (
+
+ {log.status === 'finished' && (
+
+ {_('jobFinished')}
+
+ )}
+ {log.status === 'started' && (
+ {_('jobStarted')}
+ )}
+ {log.status !== 'started' &&
+ log.status !== 'finished' && (
+ {_('jobUnknown')}
+ )}{' '}
+
+
+
+
+
+
+
+
+
+
+
+ ),
+ sortCriteria: log => (log.hasErrors ? ' ' : log.status),
+ },
+]
+
+@propTypes({
+ jobKeys: propTypes.array.isRequired,
+})
+export default class LogList extends Component {
+ constructor (props) {
+ super(props)
+ this.state = {
+ logsToClear: [],
+ }
+ this.filters = {
+ onError: 'hasErrors?',
+ successful: 'status:finished !hasErrors?',
+ }
+ }
+
+ componentWillMount () {
+ this.componentWillUnmount = subscribeJobsLogs(rawLogs => {
+ const logs = {}
+ const logsToClear = []
+ forEach(rawLogs, (log, logKey) => {
+ const data = log.data
+ const { time } = log
+ if (
+ data.event === 'job.start' &&
+ includes(this.props.jobKeys, data.key)
+ ) {
+ logsToClear.push(logKey)
+ logs[logKey] = {
+ logKey,
+ jobId: data.jobId.slice(4, 8),
+ key: data.key,
+ userId: data.userId,
+ start: time,
+ calls: {},
+ time,
+ }
+ } else {
+ const runJobId = data.runJobId
+ const entry = logs[runJobId]
+ if (!entry) {
+ return
+ }
+ logsToClear.push(logKey)
+ if (data.event === 'job.end') {
+ entry.end = time
+ entry.duration = time - entry.start
+ entry.status = 'finished'
+ } else if (data.event === 'jobCall.start') {
+ entry.calls[logKey] = {
+ callKey: logKey,
+ params: data.params,
+ method: data.method,
+ start: time,
+ time,
+ }
+ } else if (data.event === 'jobCall.end') {
+ const call = entry.calls[data.runCallId]
+
+ if (data.error) {
+ call.error = data.error
+ entry.hasErrors = true
+ entry.meta = 'error'
+ } else {
+ call.returnedValue = data.returnedValue
+ call.end = time
+ }
+ }
+ }
+ })
+
+ forEach(logs, log => {
+ if (log.end === undefined) {
+ log.status = 'started'
+ } else if (!log.meta) {
+ log.meta = 'success'
+ }
+ log.calls = orderBy(log.calls, ['time'], ['desc'])
+ })
+
+ this.setState({
+ logs: orderBy(logs, ['time'], ['desc']),
+ logsToClear,
+ })
+ })
+ }
+
+ _deleteAllLogs = () => {
+ return confirm({
+ title: _('removeAllLogsModalTitle'),
+ body: {_('removeAllLogsModalWarning')}
,
+ }).then(() => deleteJobsLog(this.state.logsToClear))
+ }
+
+ render () {
+ const { logs } = this.state
+
+ return (
+
+
+ Logs
+
+
+
+
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/menu/index.css b/packages/xo-web/src/xo-app/menu/index.css
new file mode 100644
index 000000000..400b30302
--- /dev/null
+++ b/packages/xo-web/src/xo-app/menu/index.css
@@ -0,0 +1,20 @@
+.brand {
+ padding: 0.6em;
+ font-size: 1.5em;
+}
+
+.hiddenUncollapsed {
+ display: none;
+}
+
+.collapsed .hiddenCollapsed {
+ display: none;
+}
+
+.collapsed .hiddenUncollapsed {
+ display: inherit;
+}
+
+.collapsed .centerCollapsed {
+ text-align: center;
+}
diff --git a/packages/xo-web/src/xo-app/menu/index.js b/packages/xo-web/src/xo-app/menu/index.js
new file mode 100644
index 000000000..b6b49357f
--- /dev/null
+++ b/packages/xo-web/src/xo-app/menu/index.js
@@ -0,0 +1,489 @@
+import _ from 'intl'
+import classNames from 'classnames'
+import Component from 'base-component'
+import Icon from 'icon'
+import isEmpty from 'lodash/isEmpty'
+import Link from 'link'
+import map from 'lodash/map'
+import React from 'react'
+import Tooltip from 'tooltip'
+import { UpdateTag } from '../xoa/update'
+import { addSubscriptions, connectStore, getXoaPlan, noop } from 'utils'
+import {
+ connect,
+ signOut,
+ subscribePermissions,
+ subscribeResourceSets,
+} from 'xo'
+import {
+ createFilter,
+ createGetObjectsOfType,
+ createSelector,
+ getStatus,
+ getUser,
+ isAdmin,
+} from 'selectors'
+
+import styles from './index.css'
+
+const returnTrue = () => true
+
+@connectStore(
+ () => ({
+ isAdmin,
+ nTasks: createGetObjectsOfType('task').count([
+ task => task.status === 'pending',
+ ]),
+ pools: createGetObjectsOfType('pool'),
+ nHosts: createGetObjectsOfType('host').count(),
+ srs: createGetObjectsOfType('SR'),
+ status: getStatus,
+ user: getUser,
+ }),
+ {
+ withRef: true,
+ }
+)
+@addSubscriptions({
+ permissions: subscribePermissions,
+ resourceSets: subscribeResourceSets,
+})
+export default class Menu extends Component {
+ componentWillMount () {
+ const updateCollapsed = () => {
+ this.setState({ collapsed: window.innerWidth < 1200 })
+ }
+ updateCollapsed()
+
+ window.addEventListener('resize', updateCollapsed)
+ this._removeListener = () => {
+ window.removeEventListener('resize', updateCollapsed)
+ this._removeListener = noop
+ }
+ }
+
+ componentWillUnmount () {
+ this._removeListener()
+ }
+
+ _checkPermissions = createSelector(
+ () => this.props.isAdmin,
+ () => this.props.permissions,
+ (isAdmin, permissions) =>
+ isAdmin
+ ? returnTrue
+ : ({ id }) => permissions && permissions[id] && permissions[id].operate
+ )
+
+ _getNoOperatablePools = createSelector(
+ createFilter(() => this.props.pools, this._checkPermissions),
+ isEmpty
+ )
+
+ _getNoOperatableSrs = createSelector(
+ createFilter(() => this.props.srs, this._checkPermissions),
+ isEmpty
+ )
+
+ _getNoResourceSets = createSelector(() => this.props.resourceSets, isEmpty)
+
+ get height () {
+ return this.refs.content.offsetHeight
+ }
+
+ _toggleCollapsed = event => {
+ event.preventDefault()
+ this._removeListener()
+ this.setState({ collapsed: !this.state.collapsed })
+ }
+
+ _connect = event => {
+ event.preventDefault()
+ return connect()
+ }
+
+ _signOut = event => {
+ event.preventDefault()
+ return signOut()
+ }
+
+ render () {
+ const { isAdmin, nTasks, status, user, pools, nHosts } = this.props
+ const noOperatablePools = this._getNoOperatablePools()
+ const noOperatableSrs = this._getNoOperatableSrs()
+ const noResourceSets = this._getNoResourceSets()
+
+ /* eslint-disable object-property-newline */
+ const items = [
+ {
+ to: '/home',
+ icon: 'menu-home',
+ label: 'homePage',
+ subMenu: [
+ { to: '/home?t=VM', icon: 'vm', label: 'homeVmPage' },
+ nHosts !== 0 && {
+ to: '/home?t=host',
+ icon: 'host',
+ label: 'homeHostPage',
+ },
+ !isEmpty(pools) && {
+ to: '/home?t=pool',
+ icon: 'pool',
+ label: 'homePoolPage',
+ },
+ isAdmin && {
+ to: '/home?t=VM-template',
+ icon: 'template',
+ label: 'homeTemplatePage',
+ },
+ !noOperatableSrs && {
+ to: '/home?t=SR',
+ icon: 'sr',
+ label: 'homeSrPage',
+ },
+ ],
+ },
+ {
+ to: '/dashboard/overview',
+ icon: 'menu-dashboard',
+ label: 'dashboardPage',
+ subMenu: [
+ {
+ to: '/dashboard/overview',
+ icon: 'menu-dashboard-overview',
+ label: 'overviewDashboardPage',
+ },
+ {
+ to: '/dashboard/visualizations',
+ icon: 'menu-dashboard-visualization',
+ label: 'overviewVisualizationDashboardPage',
+ },
+ {
+ to: '/dashboard/stats',
+ icon: 'menu-dashboard-stats',
+ label: 'overviewStatsDashboardPage',
+ },
+ {
+ to: '/dashboard/health',
+ icon: 'menu-dashboard-health',
+ label: 'overviewHealthDashboardPage',
+ },
+ ],
+ },
+ isAdmin && {
+ to: '/self',
+ icon: 'menu-self-service',
+ label: 'selfServicePage',
+ },
+ isAdmin && {
+ to: '/backup/overview',
+ icon: 'menu-backup',
+ label: 'backupPage',
+ subMenu: [
+ {
+ to: '/backup/overview',
+ icon: 'menu-backup-overview',
+ label: 'backupOverviewPage',
+ },
+ {
+ to: '/backup/new',
+ icon: 'menu-backup-new',
+ label: 'backupNewPage',
+ },
+ {
+ to: '/backup/restore',
+ icon: 'menu-backup-restore',
+ label: 'backupRestorePage',
+ },
+ {
+ to: '/backup/file-restore',
+ icon: 'menu-backup-file-restore',
+ label: 'backupFileRestorePage',
+ },
+ ],
+ },
+ isAdmin && {
+ to: 'xoa/update',
+ icon: 'menu-xoa',
+ label: 'xoa',
+ extra: ,
+ subMenu: [
+ { to: 'xoa/update', icon: 'menu-update', label: 'updatePage' },
+ { to: 'xoa/licenses', icon: 'menu-license', label: 'licensesPage' },
+ ],
+ },
+ isAdmin && {
+ to: '/settings/servers',
+ icon: 'menu-settings',
+ label: 'settingsPage',
+ subMenu: [
+ {
+ to: '/settings/servers',
+ icon: 'menu-settings-servers',
+ label: 'settingsServersPage',
+ },
+ {
+ to: '/settings/users',
+ icon: 'menu-settings-users',
+ label: 'settingsUsersPage',
+ },
+ {
+ to: '/settings/groups',
+ icon: 'menu-settings-groups',
+ label: 'settingsGroupsPage',
+ },
+ {
+ to: '/settings/acls',
+ icon: 'menu-settings-acls',
+ label: 'settingsAclsPage',
+ },
+ {
+ to: '/settings/remotes',
+ icon: 'menu-backup-remotes',
+ label: 'backupRemotesPage',
+ },
+ {
+ to: '/settings/plugins',
+ icon: 'menu-settings-plugins',
+ label: 'settingsPluginsPage',
+ },
+ {
+ to: '/settings/logs',
+ icon: 'menu-settings-logs',
+ label: 'settingsLogsPage',
+ },
+ { to: '/settings/ips', icon: 'ip', label: 'settingsIpsPage' },
+ {
+ to: '/settings/config',
+ icon: 'menu-settings-config',
+ label: 'settingsConfigPage',
+ },
+ ],
+ },
+ isAdmin && {
+ to: '/jobs/overview',
+ icon: 'menu-jobs',
+ label: 'jobsPage',
+ subMenu: [
+ {
+ to: '/jobs/overview',
+ icon: 'menu-jobs-overview',
+ label: 'jobsOverviewPage',
+ },
+ { to: '/jobs/new', icon: 'menu-jobs-new', label: 'jobsNewPage' },
+ {
+ to: '/jobs/schedules',
+ icon: 'menu-jobs-schedule',
+ label: 'jobsSchedulingPage',
+ },
+ ],
+ },
+ isAdmin && { to: '/about', icon: 'menu-about', label: 'aboutPage' },
+ { to: '/tasks', icon: 'task', label: 'taskMenu', pill: nTasks },
+ isAdmin && { to: '/xosan', icon: 'menu-xosan', label: 'xosan' },
+ !(noOperatablePools && noResourceSets) && {
+ to: '/vms/new',
+ icon: 'menu-new',
+ label: 'newMenu',
+ subMenu: [
+ (isAdmin || !noResourceSets) && {
+ to: '/vms/new',
+ icon: 'menu-new-vm',
+ label: 'newVmPage',
+ },
+ isAdmin && { to: '/new/sr', icon: 'menu-new-sr', label: 'newSrPage' },
+ isAdmin && {
+ to: '/settings/servers',
+ icon: 'menu-settings-servers',
+ label: 'newServerPage',
+ },
+ !noOperatablePools && {
+ to: '/vms/import',
+ icon: 'menu-new-import',
+ label: 'newImport',
+ },
+ ],
+ },
+ ]
+ /* eslint-enable object-property-newline */
+
+ return (
+
+
+
+
+
+ XO
+ Xen Orchestra
+
+
+
+
+
+
+
+
+ {map(
+ items,
+ (item, index) => item &&
+ )}
+
+
+ {(isAdmin || +process.env.XOA_PLAN === 5) && (
+
+
+ {+process.env.XOA_PLAN === 5 ? (
+
+
+ {' '}
+ {_('noSupport')}
+
+
+
+
+
+ ) : +process.env.XOA_PLAN === 1 ? (
+
+
+ {' '}
+ {_('freeUpgrade')}
+
+
+
+
+
+ ) : (
+
+
+ {getXoaPlan()}
+
+
+
+
+
+ )}
+
+
+ )}
+
+
+
+
+
+ {_('signOut')}
+
+
+
+
+
+
+
+
+
+
+
+ {status === 'connecting' ? (
+ {_('statusConnecting')}
+ ) : (
+ status === 'disconnected' && (
+
+
+ {' '}
+ {_('statusDisconnected')}
+
+
+ )
+ )}
+
+
+ )
+ }
+}
+
+const MenuLinkItem = props => {
+ const { item } = props
+ const { to, icon, label, subMenu, pill, extra } = item
+
+ return (
+
+
+
+ {_(label)}
+ {pill > 0 && {pill} }
+ {extra}
+
+ {subMenu && }
+
+ )
+}
+
+const SubMenu = props => {
+ return (
+
+ {map(
+ props.items,
+ (item, index) =>
+ item && (
+
+
+ {' '}
+ {_(item.label)}
+
+
+ )
+ )}
+
+ )
+}
diff --git a/packages/xo-web/src/xo-app/new-vm/index.css b/packages/xo-web/src/xo-app/new-vm/index.css
new file mode 100644
index 000000000..28f84fcc0
--- /dev/null
+++ b/packages/xo-web/src/xo-app/new-vm/index.css
@@ -0,0 +1,80 @@
+.inlineSelect {
+ display: inline-block;
+ font-size: 1rem;
+ width: 20em;
+}
+
+.button {
+ font-size: 1.8em;
+}
+
+.sizeInput {
+ width: 10rem;
+}
+
+.lineItem {
+ align-items: center;
+ display: flex;
+ flex-wrap: wrap;
+}
+
+.item {
+ align-items: center;
+ display: inline-flex;
+ margin: 0.5em;
+ white-space: nowrap;
+}
+
+.input {
+ flex: 1 0 20rem;
+}
+
+.sectionContent {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: flex-start;
+}
+
+.sectionContentColumn {
+ flex-direction: column;
+}
+
+.summary {
+ align-items: center;
+ display: flex;
+ flex-wrap: wrap;
+ font-size: 2em;
+ justify-content: space-around;
+}
+
+.submitSection {
+ display: flex;
+ justify-content: space-around;
+}
+
+.configDrive {
+ display: flex;
+ background-color: #eee;
+ padding: 1em;
+ margin-bottom: 0.5em;
+}
+
+.configDriveToggle {
+ margin: auto;
+}
+
+.refreshNames {
+ cursor: pointer;
+}
+
+.customConfig {
+ resize: both;
+}
+
+.fixedWidth {
+ width: 20em;
+}
+
+.tags {
+ font-size: 1.5em;
+}
diff --git a/packages/xo-web/src/xo-app/new-vm/index.js b/packages/xo-web/src/xo-app/new-vm/index.js
new file mode 100644
index 000000000..53e08692d
--- /dev/null
+++ b/packages/xo-web/src/xo-app/new-vm/index.js
@@ -0,0 +1,1709 @@
+import _, { messages } from 'intl'
+import ActionButton from 'action-button'
+import BaseComponent from 'base-component'
+import Button from 'button'
+import classNames from 'classnames'
+import defined, { get } from 'xo-defined'
+import Icon from 'icon'
+import isIp from 'is-ip'
+import Page from '../page'
+import PropTypes from 'prop-types'
+import React from 'react'
+import store from 'store'
+import Tags from 'tags'
+import Tooltip from 'tooltip'
+import Wizard, { Section } from 'wizard'
+import { Container, Row, Col } from 'grid'
+import { injectIntl } from 'react-intl'
+import {
+ Input as DebounceInput,
+ Textarea as DebounceTextarea,
+} from 'debounce-input-decorator'
+import { Limits } from 'usage'
+import {
+ clamp,
+ every,
+ filter,
+ find,
+ forEach,
+ includes,
+ isEmpty,
+ join,
+ map,
+ slice,
+ size,
+ sum,
+ sumBy,
+} from 'lodash'
+import {
+ addSshKey,
+ createVm,
+ createVms,
+ getCloudInitConfig,
+ subscribeCurrentUser,
+ subscribePermissions,
+ subscribeResourceSets,
+ XEN_DEFAULT_CPU_CAP,
+ XEN_DEFAULT_CPU_WEIGHT,
+} from 'xo'
+import {
+ SelectHost,
+ SelectIp,
+ SelectNetwork,
+ SelectPool,
+ SelectResourceSet,
+ SelectResourceSetIp,
+ SelectResourceSetsNetwork,
+ SelectResourceSetsSr,
+ SelectResourceSetsVdi,
+ SelectResourceSetsVmTemplate,
+ SelectSr,
+ SelectSshKey,
+ SelectVdi,
+ SelectVgpuType,
+ SelectVmTemplate,
+} from 'select-objects'
+import { SizeInput, Toggle } from 'form'
+import {
+ addSubscriptions,
+ buildTemplate,
+ connectStore,
+ formatSize,
+ getCoresPerSocketPossibilities,
+ generateReadableRandomString,
+ noop,
+ resolveResourceSet,
+} from 'utils'
+import {
+ createSelector,
+ createGetObject,
+ createGetObjectsOfType,
+ getUser,
+} from 'selectors'
+
+import styles from './index.css'
+
+const NB_VMS_MIN = 2
+const NB_VMS_MAX = 100
+
+/* eslint-disable camelcase */
+
+const getObject = createGetObject((_, id) => id)
+
+// Sub-components
+
+const SectionContent = ({ column, children }) => (
+
+ {children}
+
+)
+
+const LineItem = ({ children }) => (
+ {children}
+)
+
+const Item = ({ label, children, className }) => (
+
+ {label && {label} }
+ {children}
+
+)
+
+@injectIntl
+class Vif extends BaseComponent {
+ render () {
+ const {
+ intl: { formatMessage },
+ ipPoolPredicate,
+ networkPredicate,
+ onChangeAddresses,
+ onChangeMac,
+ onChangeNetwork,
+ onDelete,
+ pool,
+ resourceSet,
+ vif,
+ } = this.props
+
+ return (
+
+ -
+
+
+ -
+
+ {pool ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {pool ? (
+
+ ) : (
+
+ )}
+
+
+ -
+
+
+
+
+
+ )
+ }
+}
+
+// =============================================================================
+
+@addSubscriptions({
+ resourceSets: subscribeResourceSets,
+ permissions: subscribePermissions,
+ user: subscribeCurrentUser,
+})
+@connectStore(() => ({
+ isAdmin: createSelector(getUser, user => user && user.permission === 'admin'),
+ networks: createGetObjectsOfType('network').sort(),
+ pool: createGetObject((_, props) => props.location.query.pool),
+ pools: createGetObjectsOfType('pool'),
+ templates: createGetObjectsOfType('VM-template').sort(),
+ userSshKeys: createSelector((_, props) => {
+ const user = props.user
+ return user && user.preferences && user.preferences.sshKeys
+ }, keys => keys),
+ srs: createGetObjectsOfType('SR'),
+}))
+@injectIntl
+export default class NewVm extends BaseComponent {
+ static contextTypes = {
+ router: PropTypes.object,
+ }
+
+ constructor () {
+ super()
+
+ this._uniqueId = 0
+ // NewVm's form's state is stored in this.state.state instead of this.state
+ // so it can be emptied easily with this.setState({ state: {} })
+ this.state = { state: {} }
+ }
+
+ componentDidMount () {
+ this._reset()
+ }
+
+ _getResourceSet = () => {
+ const {
+ location: { query: { resourceSet: resourceSetId } },
+ resourceSets,
+ } = this.props
+ return resourceSets && find(resourceSets, ({ id }) => id === resourceSetId)
+ }
+
+ _getResolvedResourceSet = createSelector(
+ this._getResourceSet,
+ resolveResourceSet
+ )
+
+ // Utils -----------------------------------------------------------------------
+
+ get _isDiskTemplate () {
+ const { template } = this.state.state
+ return (
+ template &&
+ template.template_info.disks.length === 0 &&
+ template.name_label !== 'Other install media'
+ )
+ }
+ _setState = (newValues, callback) => {
+ this.setState(
+ {
+ state: {
+ ...this.state.state,
+ ...newValues,
+ },
+ },
+ callback
+ )
+ }
+ _replaceState = (state, callback) => this.setState({ state }, callback)
+ _linkState = (path, targetPath) => this.linkState(`state.${path}`, targetPath)
+ _toggleState = path => this.toggleState(`state.${path}`)
+
+ // Actions ---------------------------------------------------------------------
+
+ _reset = () => {
+ this._replaceState({
+ bootAfterCreate: true,
+ configDrive: false,
+ CPUs: '',
+ cpuCap: '',
+ cpuWeight: '',
+ existingDisks: {},
+ fastClone: true,
+ multipleVms: false,
+ name_label: '',
+ name_description: '',
+ nameLabels: map(Array(NB_VMS_MIN), (_, index) => `VM_${index + 1}`),
+ namePattern: '{name}_%',
+ nbVms: NB_VMS_MIN,
+ VDIs: [],
+ VIFs: [],
+ seqStart: 1,
+ share: false,
+ tags: [],
+ })
+ }
+
+ _create = () => {
+ const { state } = this.state
+ let installation
+ switch (state.installMethod) {
+ case 'ISO':
+ installation = {
+ method: 'cdrom',
+ repository: state.installIso.id,
+ }
+ break
+ case 'network':
+ const matches = /^(http|ftp|nfs)/i.exec(state.installNetwork)
+ if (!matches) {
+ throw new Error('invalid network URL')
+ }
+ installation = {
+ method: matches[1].toLowerCase(),
+ repository: state.installNetwork,
+ }
+ break
+ case 'PXE':
+ installation = {
+ method: 'network',
+ repository: 'pxe',
+ }
+ }
+
+ let cloudConfig
+ if (state.configDrive) {
+ const hostname = state.name_label
+ .replace(/^\s+|\s+$/g, '')
+ .replace(/\s+/g, '-')
+ if (state.installMethod === 'SSH') {
+ cloudConfig = `#cloud-config\nhostname: ${hostname}\nssh_authorized_keys:\n${join(
+ map(state.sshKeys, keyId => {
+ return this.props.userSshKeys[keyId]
+ ? ` - ${this.props.userSshKeys[keyId].key}\n`
+ : ''
+ }),
+ ''
+ )}`
+ } else {
+ cloudConfig = state.customConfig
+ }
+ } else if (state.template.name_label === 'CoreOS') {
+ cloudConfig = state.cloudConfig
+ }
+
+ // Split allowed IPs into IPv4 and IPv6
+ const { VIFs } = state
+ const _VIFs = map(VIFs, vif => {
+ const _vif = { ...vif }
+ delete _vif.addresses
+ _vif.allowedIpv4Addresses = []
+ _vif.allowedIpv6Addresses = []
+ forEach(vif.addresses, ip => {
+ if (!isIp(ip)) {
+ return
+ }
+ if (isIp.v4(ip)) {
+ _vif.allowedIpv4Addresses.push(ip)
+ } else {
+ _vif.allowedIpv6Addresses.push(ip)
+ }
+ })
+ return _vif
+ })
+
+ const resourceSet = this._getResourceSet()
+
+ const data = {
+ affinityHost: state.affinityHost && state.affinityHost.id,
+ clone: !this.isDiskTemplate && state.fastClone,
+ existingDisks: state.existingDisks,
+ installation,
+ name_label: state.name_label,
+ template: state.template.id,
+ VDIs: state.VDIs,
+ VIFs: _VIFs,
+ resourceSet: resourceSet && resourceSet.id,
+ // vm.set parameters
+ coresPerSocket: state.coresPerSocket,
+ CPUs: state.CPUs,
+ cpuWeight: state.cpuWeight === '' ? null : state.cpuWeight,
+ cpuCap: state.cpuCap === '' ? null : state.cpuCap,
+ name_description: state.name_description,
+ memoryStaticMax: state.memoryStaticMax,
+ memoryMin: state.memoryDynamicMin,
+ memoryMax: state.memoryDynamicMax,
+ pv_args: state.pv_args,
+ autoPoweron: state.autoPoweron,
+ bootAfterCreate: state.bootAfterCreate,
+ share: state.share,
+ cloudConfig,
+ coreOs: state.template.name_label === 'CoreOS',
+ tags: state.tags,
+ vgpuType: get(() => state.vgpuType.id),
+ gpuGroup: get(() => state.vgpuType.gpuGroup),
+ }
+
+ return state.multipleVms
+ ? createVms(data, state.nameLabels)
+ : createVm(data)
+ }
+
+ _initTemplate = template => {
+ if (!template) {
+ return this._reset()
+ }
+
+ this._setState({ template })
+
+ const storeState = store.getState()
+ const isInResourceSet = this._getIsInResourceSet()
+ const { state } = this.state
+ const { pool } = this.props
+ const resourceSet = this._getResolvedResourceSet()
+
+ const existingDisks = {}
+ forEach(template.$VBDs, vbdId => {
+ const vbd = getObject(storeState, vbdId, resourceSet)
+ if (!vbd || vbd.is_cd_drive) {
+ return
+ }
+ const vdi = getObject(storeState, vbd.VDI, resourceSet)
+ if (vdi) {
+ existingDisks[vbd.position] = {
+ name_label: vdi.name_label,
+ name_description: vdi.name_description,
+ size: vdi.size,
+ $SR:
+ pool || isInResourceSet(vdi.$SR)
+ ? vdi.$SR
+ : resourceSet.objectsByType['SR'][0].id,
+ }
+ }
+ })
+
+ const VIFs = []
+ forEach(template.VIFs, vifId => {
+ const vif = getObject(storeState, vifId, resourceSet)
+ VIFs.push({
+ network:
+ pool || isInResourceSet(vif.$network)
+ ? vif.$network
+ : resourceSet.objectsByType['network'][0].id,
+ })
+ })
+ if (VIFs.length === 0) {
+ const networkId = this._getDefaultNetworkId()
+ VIFs.push({
+ network: networkId,
+ })
+ }
+ const name_label =
+ state.name_label === '' || !state.name_labelHasChanged
+ ? template.name_label
+ : state.name_label
+ const name_description =
+ state.name_description === '' || !state.name_descriptionHasChanged
+ ? template.name_description || ''
+ : state.name_description
+ const replacer = this._buildTemplate()
+ this._setState({
+ // infos
+ name_label,
+ template,
+ name_description,
+ nameLabels: map(Array(+state.nbVms), (_, index) =>
+ replacer({ name_label, name_description, template }, index + 1)
+ ),
+ // performances
+ CPUs: template.CPUs.number,
+ cpuCap: '',
+ cpuWeight: '',
+ memoryDynamicMax: template.memory.dynamic[1],
+ // installation
+ installMethod:
+ (template.install_methods != null && template.install_methods[0]) ||
+ 'SSH',
+ sshKeys: this.props.userSshKeys && this.props.userSshKeys.length && [0],
+ customConfig:
+ '#cloud-config\n#hostname: myhostname\n#ssh_authorized_keys:\n# - ssh-rsa \n#packages:\n# - htop\n',
+ // interfaces
+ VIFs,
+ // disks
+ existingDisks,
+ VDIs: map(template.template_info.disks, disk => {
+ return {
+ ...disk,
+ name_description: disk.name_description || 'Created by XO',
+ name_label:
+ (name_label || 'disk') + '_' + generateReadableRandomString(5),
+ SR: pool ? pool.default_SR : resourceSet.objectsByType['SR'][0].id,
+ }
+ }),
+ })
+
+ if (template.name_label === 'CoreOS') {
+ getCloudInitConfig(template.id).then(
+ cloudConfig => this._setState({ cloudConfig }),
+ noop
+ )
+ }
+ }
+
+ // Selectors -------------------------------------------------------------------
+
+ _getIsInPool = createSelector(
+ () => {
+ const { pool } = this.props
+ return pool && pool.id
+ },
+ poolId => ({ $pool }) => $pool === poolId
+ )
+ _getIsInResourceSet = createSelector(
+ () => {
+ const resourceSet = this._getResourceSet()
+ return resourceSet && resourceSet.objects
+ },
+ objectsIds => id => includes(objectsIds, id)
+ )
+
+ _getVmPredicate = createSelector(
+ this._getIsInPool,
+ this._getIsInResourceSet,
+ (isInPool, isInResourceSet) => vm => isInResourceSet(vm.id) || isInPool(vm)
+ )
+ _getSrPredicate = createSelector(
+ this._getIsInPool,
+ this._getIsInResourceSet,
+ (isInPool, isInResourceSet) => disk =>
+ (isInResourceSet(disk.id) || isInPool(disk)) &&
+ disk.content_type !== 'iso' &&
+ disk.size > 0
+ )
+ _getIsoPredicate = createSelector(
+ () => this.props.pool && this.props.pool.id,
+ poolId => sr =>
+ (poolId == null || poolId === sr.$pool) && sr.SR_type === 'iso'
+ )
+ _getIpPoolPredicate = createSelector(
+ () => !!this.props.pool,
+ () => {
+ const { resourceSet } = this.props
+ return resourceSet && resourceSet.ipPools
+ },
+ () => this.props.vif,
+ (pool, ipPools, vif) => ipPool => {
+ if (!ipPool) {
+ return false
+ }
+ return (
+ pool ||
+ (ipPools &&
+ includes(ipPools, ipPool.id) &&
+ find(ipPool.networks, ipPoolNetwork => ipPoolNetwork === vif.network))
+ )
+ }
+ )
+ _getNetworkPredicate = createSelector(
+ this._getIsInPool,
+ this._getIsInResourceSet,
+ (isInPool, isInResourceSet) => network =>
+ isInResourceSet(network.id) || isInPool(network)
+ )
+ _getPoolNetworks = createSelector(
+ () => this.props.networks,
+ () => {
+ const { pool } = this.props
+ return pool && pool.id
+ },
+ (networks, poolId) => filter(networks, network => network.$pool === poolId)
+ )
+
+ _getAffinityHostPredicate = createSelector(
+ () => this.props.pool,
+ () => this.state.state.existingDisks,
+ () => this.state.state.VDIs,
+ () => this.props.srs,
+ (pool, existingDisks, VDIs, srs) => {
+ if (!srs) {
+ return false
+ }
+
+ const containers = [
+ ...map(existingDisks, disk => get(() => srs[disk.$SR].$container)),
+ ...map(VDIs, disk => get(() => srs[disk.SR].$container)),
+ ]
+ return host =>
+ host.$pool === pool.id &&
+ every(
+ containers,
+ container => container === pool.id || container === host.id
+ )
+ }
+ )
+ _getDefaultNetworkId = () => {
+ const resourceSet = this._getResolvedResourceSet()
+ if (resourceSet) {
+ const { network } = resourceSet.objectsByType
+ return !isEmpty(network) && network[0].id
+ }
+ const network = find(this._getPoolNetworks(), network => {
+ const pif = getObject(store.getState(), network.PIFs[0])
+ return pif && pif.management
+ })
+ return network && network.id
+ }
+ _buildTemplate = createSelector(
+ () => this.state.state.namePattern,
+ namePattern =>
+ buildTemplate(namePattern, {
+ '{name}': state => state.name_label || '',
+ '%': (_, i) => i,
+ })
+ )
+
+ _getVgpuTypePredicate = createSelector(
+ () => this.props.pool,
+ pool => vgpuType => pool !== undefined && pool.id === vgpuType.$pool
+ )
+
+ _getCoresPerSocketPossibilities = createSelector(
+ () => {
+ const { pool } = this.props
+ if (pool !== undefined) {
+ return pool.cpus.cores
+ }
+ },
+ () => this.state.state.CPUs,
+ getCoresPerSocketPossibilities
+ )
+
+ // On change -------------------------------------------------------------------
+
+ _onChangeSshKeys = keys =>
+ this._setState({ sshKeys: map(keys, key => key.id) })
+
+ _updateNbVms = () => {
+ const { nbVms, nameLabels, seqStart } = this.state.state
+ const nbVmsClamped = clamp(nbVms, NB_VMS_MIN, NB_VMS_MAX)
+ const newNameLabels = [...nameLabels]
+
+ if (nbVmsClamped < nameLabels.length) {
+ this._setState({ nameLabels: slice(newNameLabels, 0, nbVmsClamped) })
+ } else {
+ const replacer = this._buildTemplate()
+ for (
+ let i = +seqStart + nameLabels.length;
+ i <= +seqStart + nbVmsClamped - 1;
+ i++
+ ) {
+ newNameLabels.push(replacer(this.state.state, i))
+ }
+ this._setState({ nameLabels: newNameLabels })
+ }
+ }
+ _updateNameLabels = () => {
+ const { nameLabels, seqStart } = this.state.state
+ const nbVms = nameLabels.length
+ const newNameLabels = []
+ const replacer = this._buildTemplate()
+
+ for (let i = +seqStart; i <= +seqStart + nbVms - 1; i++) {
+ newNameLabels.push(replacer(this.state.state, i))
+ }
+ this._setState({ nameLabels: newNameLabels })
+ }
+ _selectResourceSet = resourceSet => {
+ const { pathname } = this.props.location
+
+ this.context.router.push({
+ pathname,
+ query: resourceSet && { resourceSet: resourceSet.id },
+ })
+ this._reset()
+ }
+ _selectPool = pool => {
+ const { pathname } = this.props.location
+
+ this.context.router.push({
+ pathname,
+ query: pool && { pool: pool.id },
+ })
+ this._reset()
+ }
+ _addVdi = () => {
+ const { state } = this.state
+ const { pool } = this.props
+
+ this._setState({
+ VDIs: [
+ ...state.VDIs,
+ {
+ name_description: 'Created by XO',
+ name_label:
+ (state.name_label || 'disk') +
+ '_' +
+ generateReadableRandomString(5),
+ SR: pool && pool.default_SR,
+ type: 'system',
+ },
+ ],
+ })
+ }
+ _removeVdi = index => {
+ const { VDIs } = this.state.state
+
+ this._setState({
+ VDIs: [...VDIs.slice(0, index), ...VDIs.slice(index + 1)],
+ })
+ }
+ _addInterface = () => {
+ const networkId = this._getDefaultNetworkId()
+
+ this._setState({
+ VIFs: [
+ ...this.state.state.VIFs,
+ {
+ network: networkId,
+ },
+ ],
+ })
+ }
+ _removeInterface = index => {
+ const { VIFs } = this.state.state
+
+ this._setState({
+ VIFs: [...VIFs.slice(0, index), ...VIFs.slice(index + 1)],
+ })
+ }
+
+ _addNewSshKey = () => {
+ const { newSshKey, sshKeys } = this.state.state
+ const { userSshKeys } = this.props
+ const splitKey = newSshKey.split(' ')
+ const title =
+ splitKey.length === 3
+ ? splitKey[2].split('\n')[0]
+ : newSshKey.substring(newSshKey.length - 10, newSshKey.length)
+
+ // save key
+ addSshKey({
+ title,
+ key: newSshKey,
+ }).then(() => {
+ // select key
+ this._setState({
+ sshKeys: [...(sshKeys || []), userSshKeys ? userSshKeys.length : 0],
+ newSshKey: '',
+ })
+ })
+ }
+
+ _getRedirectionUrl = id =>
+ this.state.state.multipleVms ? '/home' : `/vms/${id}`
+
+ // MAIN ------------------------------------------------------------------------
+
+ _renderHeader = () => {
+ const { isAdmin, pool, resourceSets } = this.props
+ const selectPool = (
+
+
+
+ )
+ const selectResourceSet = (
+
+
+
+ )
+ return (
+
+
+
+
+ {isAdmin || !isEmpty(resourceSets)
+ ? _('newVmCreateNewVmOn', {
+ select: isAdmin ? selectPool : selectResourceSet,
+ })
+ : _('newVmCreateNewVmNoPermission')}
+
+
+
+
+ )
+ }
+
+ render () {
+ const { pool } = this.props
+ return (
+
+ {(pool || this._getResourceSet()) && (
+
+ )}
+
+ )
+ }
+
+ // INFO ------------------------------------------------------------------------
+
+ _renderInfo = () => {
+ const { name_description, name_label, template } = this.state.state
+ return (
+
+
+ -
+
+ {this.props.pool ? (
+
+ ) : (
+
+ )}
+
+
+ -
+
+
+ -
+
+
+
+
+ )
+ }
+ _isInfoDone = () => {
+ const { template, name_label } = this.state.state
+ return name_label && template
+ }
+
+ _renderPerformances = () => {
+ const { CPUs, memoryDynamicMax, coresPerSocket } = this.state.state
+
+ return (
+
+
+ -
+
+
+ -
+
+
+ -
+
+ {_('vmChooseCoresPerSocket', message => (
+ {message}
+ ))}
+ {map(this._getCoresPerSocketPossibilities(), coresPerSocket =>
+ _(
+ 'vmCoresPerSocket',
+ {
+ nSockets: CPUs / coresPerSocket,
+ nCores: coresPerSocket,
+ },
+ message => (
+
+ {message}
+
+ )
+ )
+ )}
+
+
+
+
+ )
+ }
+ _isPerformancesDone = () => {
+ const { CPUs, memoryDynamicMax } = this.state.state
+ return CPUs && memoryDynamicMax != null
+ }
+
+ // INSTALL SETTINGS ------------------------------------------------------------
+
+ _renderInstallSettings = () => {
+ const { template } = this.state.state
+ if (!template) {
+ return
+ }
+ const {
+ cloudConfig,
+ configDrive,
+ customConfig,
+ installIso,
+ installMethod,
+ installNetwork,
+ newSshKey,
+ pv_args,
+ sshKeys,
+ } = this.state.state
+ const { formatMessage } = this.props.intl
+ return (
+
+ )
+ }
+ _isInstallSettingsDone = () => {
+ const {
+ configDrive,
+ customConfig,
+ installIso,
+ installMethod,
+ installNetwork,
+ sshKeys,
+ template,
+ } = this.state.state
+ switch (installMethod) {
+ case 'customConfig':
+ return customConfig || !configDrive
+ case 'ISO':
+ return installIso
+ case 'network':
+ return /^(http|ftp|nfs)/i.exec(installNetwork)
+ case 'PXE':
+ return true
+ case 'SSH':
+ return !isEmpty(sshKeys) || !configDrive
+ default:
+ return template && this._isDiskTemplate && !configDrive
+ }
+ }
+
+ // INTERFACES ------------------------------------------------------------------
+
+ _renderInterfaces = () => {
+ const { state: { VIFs } } = this.state
+
+ return (
+
+
+ {map(VIFs, (vif, index) => (
+
+ this._removeInterface(index)}
+ pool={this.props.pool}
+ resourceSet={this._getResolvedResourceSet()}
+ vif={vif}
+ />
+ {index < VIFs.length - 1 && }
+
+ ))}
+ -
+
+ {_('newVmAddInterface')}
+
+
+
+
+ )
+ }
+ _isInterfacesDone = () => every(this.state.state.VIFs, vif => vif.network)
+
+ // DISKS -----------------------------------------------------------------------
+
+ _renderDisks = () => {
+ const { state: { configDrive, existingDisks, VDIs } } = this.state
+ const { pool } = this.props
+ let i = 0
+ const resourceSet = this._getResolvedResourceSet()
+
+ return (
+
+
+ {/* Existing disks */}
+ {map(existingDisks, (disk, index) => (
+
+
+ -
+
+ {pool ? (
+
+ ) : (
+
+ )}
+
+ {' '}
+ -
+
+
+ -
+
+
+ -
+
+
+
+ {i++ < size(existingDisks) + VDIs.length - 1 &&
}
+
+ ))}
+
+ {/* VDIs */}
+ {map(VDIs, (vdi, index) => (
+
+
+ -
+
+ {pool ? (
+
+ ) : (
+
+ )}
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
this._removeVdi(index)}>
+
+
+
+
+ {index < VDIs.length - 1 &&
}
+
+ ))}
+ -
+
+ {_('newVmAddDisk')}
+
+
+
+
+ )
+ }
+ _isDisksDone = () =>
+ every(
+ this.state.state.VDIs,
+ vdi => vdi.SR && vdi.name_label && vdi.size !== undefined
+ ) &&
+ every(
+ this.state.state.existingDisks,
+ (vdi, index) => vdi.$SR && vdi.name_label && vdi.size !== undefined
+ )
+
+ // ADVANCED --------------------------------------------------------------------
+
+ _renderAdvanced = () => {
+ const {
+ affinityHost,
+ autoPoweron,
+ bootAfterCreate,
+ cpuCap,
+ cpuWeight,
+ memoryDynamicMin,
+ memoryDynamicMax,
+ memoryStaticMax,
+ multipleVms,
+ nameLabels,
+ namePattern,
+ nbVms,
+ seqStart,
+ share,
+ showAdvanced,
+ tags,
+ template,
+ } = this.state.state
+ const { isAdmin } = this.props
+ const { formatMessage } = this.props.intl
+ return (
+
+
+
+ {showAdvanced ? _('newVmHideAdvanced') : _('newVmShowAdvanced')}
+
+
+ {showAdvanced && [
+ ,
+
+ -
+
+
+ {_('newVmBootAfterCreate')}
+
+ -
+
+
+ {_('autoPowerOn')}
+
+ -
+
+
+ ,
+ this._getResourceSet() !== undefined && (
+
+ -
+
+
+ {_('newVmShare')}
+
+
+ ),
+
+ -
+
+
+ -
+
+
+ ,
+
+ -
+
+
+ -
+
+
+ -
+
+
+ ,
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+ {multipleVms && (
+
+ {map(nameLabels, (nameLabel, index) => (
+ -
+
+
+ ))}
+
+ )}
+ ,
+ isAdmin && (
+
+ -
+
+
+
+ ),
+ template &&
+ template.virtualizationMode === 'hvm' && (
+
+ -
+
+
+
+ ),
+ ]}
+
+ )
+ }
+ _isAdvancedDone = () => {
+ const {
+ memoryDynamicMin,
+ memoryDynamicMax,
+ memoryStaticMax,
+ } = this.state.state
+ return (
+ memoryDynamicMax != null &&
+ (memoryDynamicMin == null || memoryDynamicMin <= memoryDynamicMax) &&
+ (memoryStaticMax == null || memoryDynamicMax <= memoryStaticMax)
+ )
+ }
+
+ // SUMMARY ---------------------------------------------------------------------
+
+ _renderSummary = () => {
+ const {
+ CPUs,
+ existingDisks,
+ fastClone,
+ memoryDynamicMax,
+ multipleVms,
+ nameLabels,
+ VDIs,
+ VIFs,
+ } = this.state.state
+
+ const factor = multipleVms ? nameLabels.length : 1
+ const resourceSet = this._getResourceSet()
+ const limits = resourceSet && resourceSet.limits
+ const cpusLimits = limits && limits.cpus
+ const memoryLimits = limits && limits.memory
+ const diskLimits = limits && limits.disk
+
+ return (
+
+ )
+ }
+
+ _availableResources = () => {
+ const resourceSet = this._getResourceSet()
+
+ if (!resourceSet) {
+ return true
+ }
+
+ const {
+ CPUs,
+ existingDisks,
+ memoryDynamicMax,
+ VDIs,
+ multipleVms,
+ nameLabels,
+ } = this.state.state
+ const factor = multipleVms ? nameLabels.length : 1
+
+ return !(
+ CPUs * factor > get(() => resourceSet.limits.cpus.available) ||
+ memoryDynamicMax * factor >
+ get(() => resourceSet.limits.memory.available) ||
+ (sumBy(VDIs, 'size') + sum(map(existingDisks, disk => disk.size))) *
+ factor >
+ get(() => resourceSet.limits.disk.available)
+ )
+ }
+}
+/* eslint-enable camelcase */
diff --git a/packages/xo-web/src/xo-app/new/index.js b/packages/xo-web/src/xo-app/new/index.js
new file mode 100644
index 000000000..3c5568831
--- /dev/null
+++ b/packages/xo-web/src/xo-app/new/index.js
@@ -0,0 +1,9 @@
+import { routes } from 'utils'
+
+import Sr from './sr'
+
+const New = routes('vm', {
+ sr: Sr,
+})(({ children }) => children)
+
+export default New
diff --git a/packages/xo-web/src/xo-app/new/sr/index.js b/packages/xo-web/src/xo-app/new/sr/index.js
new file mode 100644
index 000000000..271a69363
--- /dev/null
+++ b/packages/xo-web/src/xo-app/new/sr/index.js
@@ -0,0 +1,828 @@
+import _, { messages } from 'intl'
+import ActionButton from 'action-button'
+import Component from 'base-component'
+import filter from 'lodash/filter'
+import Icon from 'icon'
+import includes from 'lodash/includes'
+import info, { error } from 'notification'
+import isEmpty from 'lodash/isEmpty'
+import map from 'lodash/map'
+import Page from '../../page'
+import propTypes from 'prop-types-decorator'
+import React from 'react'
+import store from 'store'
+import trim from 'lodash/trim'
+import Wizard, { Section } from 'wizard'
+import { confirm } from 'modal'
+import { connectStore, formatSize } from 'utils'
+import { Container, Row, Col } from 'grid'
+import { injectIntl } from 'react-intl'
+import { Password, Select } from 'form'
+import { SelectHost } from 'select-objects'
+import {
+ createFilter,
+ createGetObjectsOfType,
+ createSelector,
+ getObject,
+} from 'selectors'
+import {
+ createSrIso,
+ createSrIscsi,
+ createSrLvm,
+ createSrNfs,
+ probeSrIscsiExists,
+ probeSrIscsiIqns,
+ probeSrIscsiLuns,
+ probeSrNfs,
+ probeSrNfsExists,
+ reattachSrIso,
+ reattachSr,
+} from 'xo'
+
+// ===================================================================
+
+@propTypes({
+ onChange: propTypes.func.isRequired,
+ options: propTypes.array.isRequired,
+})
+class SelectIqn extends Component {
+ _getOptions = createSelector(
+ () => this.props.options,
+ options =>
+ map(options, ({ ip, iqn }, index) => ({
+ label: `${iqn} (${ip})`,
+ value: index,
+ }))
+ )
+
+ _handleChange = ({ value }) => {
+ const { props } = this
+
+ this.setState({ value }, () => props.onChange(props.options[value]))
+ }
+
+ componentDidMount () {
+ return this.componentDidUpdate()
+ }
+
+ componentDidUpdate () {
+ let options
+ if (
+ this.state.value === null &&
+ (options = this._getOptions()).length === 1
+ ) {
+ this._handleChange(options[0])
+ }
+ }
+
+ state = { value: null }
+
+ render () {
+ return (
+
+ )
+ }
+}
+
+@propTypes({
+ onChange: propTypes.func.isRequired,
+ options: propTypes.array.isRequired,
+})
+class SelectLun extends Component {
+ _getOptions = createSelector(
+ () => this.props.options,
+ options =>
+ map(options, (lun, index) => ({
+ label: `LUN ${lun.id}: ${lun.serial} - ${formatSize(+lun.size)} - (${
+ lun.vendor
+ })`,
+ value: index,
+ }))
+ )
+
+ _handleChange = ({ value }) => {
+ const { props } = this
+ this.setState({ value }, () => props.onChange(props.options[value]))
+ }
+
+ componentDidMount () {
+ return this.componentDidUpdate()
+ }
+
+ componentDidUpdate () {
+ let options
+ if (
+ this.state.value === null &&
+ (options = this._getOptions()).length === 1
+ ) {
+ this._handleChange(options[0])
+ }
+ }
+
+ state = { value: null }
+
+ render () {
+ return (
+
+ )
+ }
+}
+
+// ===================================================================
+
+const SR_TYPE_TO_LABEL = {
+ nfs: 'NFS',
+ iscsi: 'iSCSI',
+ lvm: 'Local LVM',
+ local: 'Local',
+ nfsiso: 'NFS ISO',
+ smb: 'SMB',
+}
+
+const SR_GROUP_TO_LABEL = {
+ vdisr: 'VDI SR',
+ isosr: 'ISO SR',
+}
+
+const typeGroups = {
+ vdisr: ['nfs', 'iscsi', 'lvm'],
+ isosr: ['local', 'nfsiso', 'smb'],
+}
+
+const getSrPath = id => `/srs/${id}`
+
+// ===================================================================
+
+@injectIntl
+@connectStore(() => ({
+ hosts: createGetObjectsOfType('host'),
+ srs: createGetObjectsOfType('SR'),
+}))
+export default class New extends Component {
+ constructor (props) {
+ super(props)
+
+ const hostId = props.location.query.host
+
+ this.state = {
+ description: undefined,
+ host: hostId && getObject(store.getState(), hostId),
+ iqn: undefined,
+ iqns: undefined,
+ loading: 0,
+ lockCreation: undefined,
+ lun: undefined,
+ luns: undefined,
+ name: undefined,
+ path: undefined,
+ paths: undefined,
+ type: undefined,
+ unused: undefined,
+ usage: undefined,
+ used: undefined,
+ }
+ this.getHostSrs = createFilter(
+ () => this.props.srs,
+ createSelector(
+ () => this.state.host,
+ ({ $pool, id }) => sr => sr.$container === $pool || sr.$container === id
+ ),
+ true
+ )
+ }
+
+ _handleSubmit = async () => {
+ const {
+ description,
+ device,
+ localPath,
+ name,
+ password,
+ port,
+ server,
+ username,
+ } = this.refs
+ const { host, iqn, lun, path, type } = this.state
+
+ const createMethodFactories = {
+ nfs: async () => {
+ const previous = await probeSrNfsExists(host.id, server.value, path)
+ if (previous && previous.length > 0) {
+ try {
+ await confirm({
+ title: _('existingSrModalTitle'),
+ body: {_('existingSrModalText')}
,
+ })
+ } catch (error) {
+ return
+ }
+ }
+ return createSrNfs(
+ host.id,
+ name.value,
+ description.value,
+ server.value,
+ path
+ )
+ },
+ iscsi: async () => {
+ const previous = await probeSrIscsiExists(
+ host.id,
+ iqn.ip,
+ iqn.iqn,
+ lun.scsiId,
+ +port.value,
+ username && username.value,
+ password && password.value
+ )
+ if (previous && previous.length > 0) {
+ try {
+ await confirm({
+ title: _('existingLunModalTitle'),
+ body: {_('existingLunModalText')}
,
+ })
+ } catch (error) {
+ return
+ }
+ }
+ return createSrIscsi(
+ host.id,
+ name.value,
+ description.value,
+ iqn.ip,
+ iqn.iqn,
+ lun.scsiId,
+ +port.value,
+ username && username.value,
+ password && password.value
+ )
+ },
+ lvm: () =>
+ createSrLvm(host.id, name.value, description.value, device.value),
+ local: () =>
+ createSrIso(
+ host.id,
+ name.value,
+ description.value,
+ localPath.value,
+ 'local'
+ ),
+ nfsiso: () =>
+ createSrIso(
+ host.id,
+ name.value,
+ description.value,
+ `${server.value}:${path}`,
+ 'nfs',
+ username && username.value,
+ password && password.value
+ ),
+ smb: () =>
+ createSrIso(
+ host.id,
+ name.value,
+ description.value,
+ server.value,
+ 'smb',
+ username && username.value,
+ password && password.value
+ ),
+ }
+
+ try {
+ return await createMethodFactories[type]()
+ } catch (err) {
+ error('SR Creation', err.message || String(err))
+ }
+ }
+
+ _handleSrHostSelection = host => this.setState({ host })
+ _handleNameChange = event => this.setState({ name: event.target.value })
+ _handleDescriptionChange = event =>
+ this.setState({ description: event.target.value })
+
+ _handleSrTypeSelection = event => {
+ const type = event.target.value
+ this.setState({
+ type,
+ paths: undefined,
+ iqns: undefined,
+ usage: undefined,
+ used: undefined,
+ unused: undefined,
+ summary: type === 'lvm' || type === 'local' || type === 'smb',
+ })
+ }
+
+ _handleSrIqnSelection = async iqn => {
+ const { username, password } = this.refs
+ const { host } = this.state
+
+ try {
+ this.setState(({ loading }) => ({ loading: loading + 1 }))
+ const luns = await probeSrIscsiLuns(
+ host.id,
+ iqn.ip,
+ iqn.iqn,
+ username && username.value,
+ password && password.value
+ )
+ this.setState({
+ iqn,
+ luns,
+ })
+ } catch (err) {
+ error('LUNs Detection', err.message || String(err))
+ } finally {
+ this.setState(({ loading }) => ({ loading: loading - 1 }))
+ }
+ }
+
+ _handleSrLunSelection = async lun => {
+ const { password, port, username } = this.refs
+ const { host, iqn } = this.state
+
+ try {
+ this.setState(({ loading }) => ({ loading: loading + 1 }))
+ const list = await probeSrIscsiExists(
+ host.id,
+ iqn.ip,
+ iqn.iqn,
+ lun.scsiId,
+ +port.value,
+ username && username.value,
+ password && password.value
+ )
+ const srIds = map(this.getHostSrs(), sr => sr.id)
+ const used = filter(list, item => includes(srIds, item.id))
+ const unused = filter(list, item => !includes(srIds, item.id))
+ this.setState({
+ lun,
+ usage: true,
+ used,
+ unused,
+ summary: used.length <= 0,
+ })
+ } catch (err) {
+ error('iSCSI Error', err.message || String(err))
+ } finally {
+ this.setState(({ loading }) => ({ loading: loading - 1 }))
+ }
+ }
+
+ _handleAuthChoice = () => {
+ const auth = this.refs['auth'].checked
+ this.setState({
+ auth,
+ })
+ }
+
+ _handleSearchServer = async () => {
+ const { password, port, server, username } = this.refs
+
+ const { host, type } = this.state
+
+ try {
+ if (type === 'nfs' || type === 'nfsiso') {
+ const paths = await probeSrNfs(host.id, server.value)
+ this.setState({
+ usage: undefined,
+ paths,
+ })
+ } else if (type === 'iscsi') {
+ const iqns = await probeSrIscsiIqns(
+ host.id,
+ server.value,
+ +port.value,
+ username && username.value,
+ password && password.value
+ )
+ if (!iqns.length) {
+ info('iSCSI Detection', 'No IQNs found')
+ } else {
+ this.setState({
+ usage: undefined,
+ iqns,
+ })
+ }
+ }
+ } catch (err) {
+ error('Server Detection', err.message || String(err))
+ }
+ }
+
+ _handleSrPathSelection = async path => {
+ const { server } = this.refs
+ const { host } = this.state
+
+ try {
+ this.setState(({ loading }) => ({ loading: loading + 1 }))
+ const list = await probeSrNfsExists(host.id, server.value, path)
+ const srIds = map(this.getHostSrs(), sr => sr.id)
+ const used = filter(list, item => includes(srIds, item.id))
+ const unused = filter(list, item => !includes(srIds, item.id))
+ this.setState({
+ path,
+ usage: true,
+ used,
+ unused,
+ summary: used.length <= 0,
+ })
+ } catch (err) {
+ error('NFS Error', err.message || String(err))
+ } finally {
+ this.setState(({ loading }) => ({ loading: loading - 1 }))
+ }
+ }
+
+ _reattach = async uuid => {
+ const { host, type } = this.state
+
+ let { name, description } = this.refs
+
+ name = trim(name)
+ description = trim(description)
+ if (isEmpty(name) || isEmpty(description)) {
+ error('Missing General Parameters', 'Please complete General Information')
+ }
+
+ const method = type === 'nfsiso' ? reattachSrIso : reattachSr
+ try {
+ await method(host.id, uuid, name, description, type)
+ } catch (err) {
+ error('Reattach', err.message || String(err))
+ }
+ }
+
+ _renderHeader () {
+ return (
+
+
+
+
+ {_('newSrTitle')}
+
+
+
+
+ )
+ }
+
+ render () {
+ const { hosts } = this.props
+ const {
+ auth,
+ host,
+ iqns,
+ loading,
+ lockCreation,
+ lun,
+ luns,
+ path,
+ paths,
+ summary,
+ type,
+ unused,
+ usage,
+ used,
+ } = this.state
+ const { formatMessage } = this.props.intl
+
+ return (
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/page/index.css b/packages/xo-web/src/xo-app/page/index.css
new file mode 100644
index 000000000..e280d6ffc
--- /dev/null
+++ b/packages/xo-web/src/xo-app/page/index.css
@@ -0,0 +1,18 @@
+.container {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ height: 100%;
+}
+
+.header {
+ padding: 0.6em;
+ padding-bottom: 0;
+ flex-shrink: 0;
+}
+
+.content {
+ flex: 1;
+ overflow-y: auto;
+ padding: 1em;
+}
diff --git a/packages/xo-web/src/xo-app/page/index.js b/packages/xo-web/src/xo-app/page/index.js
new file mode 100644
index 000000000..44c98226d
--- /dev/null
+++ b/packages/xo-web/src/xo-app/page/index.js
@@ -0,0 +1,44 @@
+import { messages } from 'intl'
+import DocumentTitle from 'react-document-title'
+import React from 'react'
+import { injectIntl } from 'react-intl'
+
+import styles from './index.css'
+
+const Page = ({
+ children,
+ collapsedHeader,
+ formatTitle,
+ header,
+ intl,
+ title,
+}) => {
+ const { formatMessage } = intl
+
+ const content = (
+
+ {!collapsedHeader && (
+
{header}
+ )}
+
{children}
+
+ )
+
+ return title ? (
+
+ {content}
+
+ ) : (
+ content
+ )
+}
+
+Page.propTypes = {
+ children: React.PropTypes.node,
+ collapsedHeader: React.PropTypes.bool,
+ formatTitle: React.PropTypes.bool,
+ header: React.PropTypes.node,
+ title: React.PropTypes.string,
+}
+
+export default injectIntl(Page)
diff --git a/packages/xo-web/src/xo-app/pool/action-bar.js b/packages/xo-web/src/xo-app/pool/action-bar.js
new file mode 100644
index 000000000..3a489511f
--- /dev/null
+++ b/packages/xo-web/src/xo-app/pool/action-bar.js
@@ -0,0 +1,66 @@
+import _ from 'intl'
+import ActionBar, { Action } from 'action-bar'
+import Component from 'base-component'
+import React from 'react'
+import { createGetObjectsOfType, createSelector } from 'selectors'
+import { find } from 'lodash'
+import { addSubscriptions, connectStore, noop } from 'utils'
+import { addHostToPool, disconnectServer, subscribeServers } from 'xo'
+
+@connectStore({
+ hosts: createGetObjectsOfType('host'),
+})
+@addSubscriptions({
+ servers: subscribeServers,
+})
+export default class PoolActionBar extends Component {
+ _getMasterAddress = createSelector(
+ () => this.props.pool && this.props.pool.master,
+ () => this.props.hosts,
+ (poolMaster, hosts) => {
+ const master = find(hosts, { id: poolMaster })
+
+ return master && master.address
+ }
+ )
+
+ _getServer = createSelector(
+ this._getMasterAddress,
+ () => this.props.servers,
+ (masterAddress, servers) => find(servers, { host: masterAddress })
+ )
+
+ _disconnectServer = () => disconnectServer(this._getServer())
+
+ render () {
+ const { pool } = this.props
+
+ return (
+
+
+
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/pool/index.js b/packages/xo-web/src/xo-app/pool/index.js
new file mode 100644
index 000000000..1347e48e4
--- /dev/null
+++ b/packages/xo-web/src/xo-app/pool/index.js
@@ -0,0 +1,163 @@
+import _ from 'intl'
+import assign from 'lodash/assign'
+import Icon from 'icon'
+import PoolActionBar from './action-bar'
+import Page from '../page'
+import pick from 'lodash/pick'
+import React, { cloneElement, Component } from 'react'
+import { NavLink, NavTabs } from 'nav'
+import { Text } from 'editable'
+import { editPool } from 'xo'
+import { Container, Row, Col } from 'grid'
+import { connectStore, routes } from 'utils'
+import {
+ createGetObject,
+ createGetObjectMessages,
+ createGetObjectsOfType,
+ createSelector,
+} from 'selectors'
+
+import TabAdvanced from './tab-advanced'
+import TabGeneral from './tab-general'
+import TabStats from './tab-stats'
+import TabLogs from './tab-logs'
+import TabNetwork from './tab-network'
+import TabPatches from './tab-patches'
+
+// ===================================================================
+
+@routes('general', {
+ advanced: TabAdvanced,
+ general: TabGeneral,
+ logs: TabLogs,
+ network: TabNetwork,
+ patches: TabPatches,
+ stats: TabStats,
+})
+@connectStore(() => {
+ const getPool = createGetObject()
+
+ const getMaster = createGetObject(
+ (state, props) => getPool(state, props).master
+ )
+
+ const getNetworks = createGetObjectsOfType('network')
+ .filter(
+ createSelector(getPool, ({ id }) => network => network.$pool === id)
+ )
+ .sort()
+
+ const getHosts = createGetObjectsOfType('host')
+ .filter(createSelector(getPool, ({ id }) => obj => obj.$pool === id))
+ .sort()
+
+ const getPoolSrs = createGetObjectsOfType('SR')
+ .filter(createSelector(getPool, ({ id }) => sr => sr.$pool === id))
+ .sort()
+
+ const getNumberOfVms = createGetObjectsOfType('VM').count(
+ createSelector(getPool, ({ id }) => obj => obj.$pool === id)
+ )
+
+ const getLogs = createGetObjectMessages(getPool)
+
+ return (state, props) => {
+ const pool = getPool(state, props)
+ if (!pool) {
+ return {}
+ }
+
+ return {
+ hosts: getHosts(state, props),
+ logs: getLogs(state, props),
+ master: getMaster(state, props),
+ networks: getNetworks(state, props),
+ nVms: getNumberOfVms(state, props),
+ pool,
+ srs: getPoolSrs(state, props),
+ }
+ }
+})
+export default class Pool extends Component {
+ _setNameDescription = nameDescription =>
+ editPool(this.props.pool, { name_description: nameDescription })
+ _setNameLabel = nameLabel =>
+ editPool(this.props.pool, { name_label: nameLabel })
+
+ header () {
+ const { pool } = this.props
+ if (!pool) {
+ return
+ }
+ return (
+
+
+
+
+ {' '}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {_('generalTabName')}
+
+
+ {_('statsTabName')}
+
+
+ {_('networkTabName')}
+
+
+ {_('patchesTabName')}
+
+
+ {_('logsTabName')}
+
+
+ {_('advancedTabName')}
+
+
+
+
+
+ )
+ }
+
+ render () {
+ const { pool } = this.props
+ if (!pool) {
+ return {_('statusLoading')}
+ }
+ const childProps = assign(
+ pick(this.props, [
+ 'hosts',
+ 'logs',
+ 'master',
+ 'networks',
+ 'nVms',
+ 'pool',
+ 'srs',
+ ])
+ )
+ return (
+
+ {cloneElement(this.props.children, childProps)}
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/pool/tab-advanced.js b/packages/xo-web/src/xo-app/pool/tab-advanced.js
new file mode 100644
index 000000000..080891e59
--- /dev/null
+++ b/packages/xo-web/src/xo-app/pool/tab-advanced.js
@@ -0,0 +1,94 @@
+import React from 'react'
+
+import _ from 'intl'
+import Component from 'base-component'
+import Copiable from 'copiable'
+import renderXoItem from 'render-xo-item'
+import SelectFiles from 'select-files'
+import Upgrade from 'xoa-upgrade'
+import { connectStore } from 'utils'
+import { createGetObjectsOfType } from 'selectors'
+import { XoSelect } from 'editable'
+import { installSupplementalPackOnAllHosts, setPoolMaster } from 'xo'
+import { map } from 'lodash'
+import { Container, Row, Col } from 'grid'
+
+@connectStore(() => ({
+ master: createGetObjectsOfType('host').find((_, { pool }) => ({
+ id: pool.master,
+ })),
+}))
+class PoolMaster extends Component {
+ _getPoolMasterPredicate = host => host.$pool === this.props.pool.id
+
+ _onChange = host => setPoolMaster(host)
+
+ render () {
+ const { pool, master } = this.props
+
+ return (
+
+ {master.name_label}
+
+ )
+ }
+}
+
+export default connectStore({
+ gpuGroups: createGetObjectsOfType('gpuGroup'),
+})(({ gpuGroups, pool }) => (
+
+
{_('xenSettingsLabel')}
+
+
+
+ {_('uuid')}
+
+
+ {pool.uuid}
+
+
+
+
+ {_('poolHaStatus')}
+
+
+ {pool.HA_enabled ? _('poolHaEnabled') : _('poolHaDisabled')}
+
+
+
+
+ {_('setpoolMaster')}
+
+
+
+
+
+
+
{_('poolGpuGroups')}
+
+
+
+
+ {map(gpuGroups, gpuGroup => (
+
+ {renderXoItem(gpuGroup)}
+
+ ))}
+
+
+
+
+
{_('supplementalPackPoolNew')}
+
+ installSupplementalPackOnAllHosts(pool, file)}
+ />
+
+
+))
diff --git a/packages/xo-web/src/xo-app/pool/tab-general.js b/packages/xo-web/src/xo-app/pool/tab-general.js
new file mode 100644
index 000000000..e17faabf7
--- /dev/null
+++ b/packages/xo-web/src/xo-app/pool/tab-general.js
@@ -0,0 +1,98 @@
+import _ from 'intl'
+import find from 'lodash/find'
+import Icon from 'icon'
+import map from 'lodash/map'
+import React from 'react'
+import sumBy from 'lodash/sumBy'
+import HomeTags from 'home-tags'
+import { addTag, removeTag } from 'xo'
+import Link, { BlockLink } from 'link'
+import { Container, Row, Col } from 'grid'
+import Usage, { UsageElement } from 'usage'
+import { formatSize } from 'utils'
+import Tooltip from 'tooltip'
+
+export default ({ hosts, nVms, pool, srs }) => (
+
+
+
+
+
+
+
+ {hosts.length}x
+
+
+
+
+
+
+
+
+ {srs.length}x
+
+
+
+
+
+
+
+
+ {nVms}x
+
+
+
+
+
+
+
+
+ {_('poolTitleRamUsage')}
+
+
+
+
+
+ {map(hosts, host => (
+
+ ))}
+
+
+
+
+
+
+ {_('poolRamUsage', {
+ used: formatSize(sumBy(hosts, 'memory.usage')),
+ total: formatSize(sumBy(hosts, 'memory.size')),
+ })}
+
+
+
+
+
+ {_('poolMaster')}{' '}
+
+ {find(hosts, host => host.id === pool.master).name_label}
+
+
+
+
+
+
+ removeTag(pool.id, tag)}
+ onAdd={tag => addTag(pool.id, tag)}
+ />
+
+
+
+
+)
diff --git a/packages/xo-web/src/xo-app/pool/tab-logs.js b/packages/xo-web/src/xo-app/pool/tab-logs.js
new file mode 100644
index 000000000..cd7d88faf
--- /dev/null
+++ b/packages/xo-web/src/xo-app/pool/tab-logs.js
@@ -0,0 +1,68 @@
+import _ from 'intl'
+import React, { Component } from 'react'
+import SortedTable from 'sorted-table'
+import { FormattedRelative, FormattedTime } from 'react-intl'
+import { deleteMessage, deleteMessages } from 'xo'
+
+const LOG_COLUMNS = [
+ {
+ default: true,
+ itemRenderer: log => (
+
+ {' '}
+ ( )
+
+ ),
+ name: _('logDate'),
+ sortCriteria: 'time',
+ },
+ {
+ itemRenderer: log => log.name,
+ name: _('logName'),
+ sortCriteria: 'name',
+ },
+ {
+ itemRenderer: log => log.body,
+ name: _('logContent'),
+ sortCriteria: 'body',
+ },
+]
+
+const INDIVIDUAL_ACTIONS = [
+ {
+ handler: deleteMessage,
+ icon: 'delete',
+ label: _('logDelete'),
+ level: 'danger',
+ },
+]
+
+const GROUPED_ACTIONS = [
+ {
+ handler: deleteMessages,
+ icon: 'delete',
+ label: _('logsDelete'),
+ level: 'danger',
+ },
+]
+
+export default class TabLogs extends Component {
+ render () {
+ return (
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/pool/tab-network.js b/packages/xo-web/src/xo-app/pool/tab-network.js
new file mode 100644
index 000000000..4efff62d7
--- /dev/null
+++ b/packages/xo-web/src/xo-app/pool/tab-network.js
@@ -0,0 +1,380 @@
+import _ from 'intl'
+import ActionRowButton from 'action-row-button'
+import BaseComponent from 'base-component'
+import Button from 'button'
+import ButtonGroup from 'button-group'
+import Icon from 'icon'
+import isEmpty from 'lodash/isEmpty'
+import map from 'lodash/map'
+import React, { Component } from 'react'
+import some from 'lodash/some'
+import SortedTable from 'sorted-table'
+import TabButton from 'tab-button'
+import Tooltip from 'tooltip'
+import { connectStore } from 'utils'
+import { Container, Row, Col } from 'grid'
+import { Text, Number } from 'editable'
+import { Toggle } from 'form'
+import {
+ createFinder,
+ createGetObject,
+ createGetObjectsOfType,
+ createSelector,
+} from 'selectors'
+import {
+ connectPif,
+ createBondedNetwork,
+ createNetwork,
+ deleteNetwork,
+ disconnectPif,
+ editNetwork,
+ editPif,
+} from 'xo'
+
+// =============================================================================
+
+const _conditionalTooltip = (component, tooltip) =>
+ tooltip ? {component} : component
+
+const _createGetPifs = () =>
+ createGetObjectsOfType('PIF').pick((_, props) => props.network.PIFs)
+
+const _createGetDefaultPif = () =>
+ createFinder(
+ _createGetPifs(),
+ createSelector(
+ createSelector(
+ createGetObject((_, props) => props.network.$pool),
+ pool => pool.master
+ ),
+ poolMaster => pif => pif.$host === poolMaster
+ )
+ )
+
+// =============================================================================
+
+@connectStore(() => ({
+ isBonded: createSelector(
+ createGetObjectsOfType('PIF').pick(
+ (_, props) => props && props.network.PIFs
+ ),
+ pifs => some(pifs, 'isBondMaster')
+ ),
+}))
+class Name extends Component {
+ _editName = value => editNetwork(this.props.network, { name_label: value })
+
+ render () {
+ const { isBonded, network } = this.props
+
+ return (
+
+ {' '}
+ {isBonded && (
+ {_('pillBonded')}
+ )}
+
+ )
+ }
+}
+
+// -----------------------------------------------------------------------------
+
+class Description extends Component {
+ _editDescription = value =>
+ editNetwork(this.props.network, { name_description: value })
+
+ render () {
+ const { network } = this.props
+
+ return (
+
+ )
+ }
+}
+
+// -----------------------------------------------------------------------------
+
+@connectStore(() => ({
+ defaultPif: _createGetDefaultPif(),
+}))
+class DefaultPif extends BaseComponent {
+ _editPif = vlan => editPif(this.props.defaultPif, { vlan })
+
+ render () {
+ const { defaultPif } = this.props
+
+ if (!defaultPif) {
+ return null
+ }
+
+ return {defaultPif.device}
+ }
+}
+
+@connectStore(() => ({
+ defaultPif: _createGetDefaultPif(),
+}))
+class Vlan extends BaseComponent {
+ _editPif = vlan => editPif(this.props.defaultPif, { vlan })
+
+ render () {
+ const { defaultPif } = this.props
+
+ if (!defaultPif) {
+ return null
+ }
+
+ return (
+
+ {
+
+ {defaultPif.vlan === -1 ? 'None' : defaultPif.vlan}
+
+ }
+
+ )
+ }
+}
+
+// -----------------------------------------------------------------------------
+
+@connectStore(() => ({
+ isInUse: createSelector(
+ createGetObjectsOfType('VIF').pick(
+ (_, props) => props && props.network.VIFs
+ ),
+ vifs => some(vifs, 'attached')
+ ),
+}))
+class ToggleDefaultLockingMode extends Component {
+ _editDefaultIsLocked = () => {
+ const { network } = this.props
+ editNetwork(network, { defaultIsLocked: !network.defaultIsLocked })
+ }
+
+ render () {
+ const { isInUse, network } = this.props
+ return _conditionalTooltip(
+ ,
+ isInUse && _('networkInUse')
+ )
+ }
+}
+
+// -----------------------------------------------------------------------------
+
+@connectStore(() => {
+ const pif = createGetObject()
+ const host = createGetObject(createSelector(pif, pif => pif.$host))
+ const disableUnplug = createSelector(
+ pif,
+ pif =>
+ pif.attached &&
+ !pif.isBondMaster &&
+ (pif.management || pif.disallowUnplug)
+ )
+
+ return { host, pif, disableUnplug }
+})
+class PifItem extends Component {
+ render () {
+ const { pif, host, disableUnplug } = this.props
+
+ return (
+
+ {pif.device}
+ {host.name_label}
+ {pif.ip}
+ {pif.mac}
+
+ {pif.carrier ? (
+
+ {_('poolNetworkPifAttached')}
+
+ ) : (
+
+ {_('poolNetworkPifDetached')}
+
+ )}
+
+
+
+
+
+
+
+ )
+ }
+}
+
+class PifsItem extends BaseComponent {
+ render () {
+ const { network } = this.props
+ const { showPifs } = this.state
+
+ return (
+
+
+
+
+
+
+ {showPifs && (
+
+
+
+ {_('pifDeviceLabel')}
+ {_('homeTypeHost')}
+ {_('pifAddressLabel')}
+ {_('pifMacLabel')}
+ {_('pifStatusLabel')}
+
+
+
+
+ {map(network.PIFs, pifId => )}
+
+
+ )}
+
+ )
+ }
+}
+
+// -----------------------------------------------------------------------------
+
+@connectStore(() => {
+ const disablePifUnplug = pif =>
+ pif.attached && !pif.isBondMaster && (pif.management || pif.disallowUnplug)
+
+ const getDisableNetworkDelete = createSelector(
+ _createGetPifs(),
+ (_, props) => props && props.network.name_label,
+ (pifs, nameLabel) =>
+ nameLabel === 'Host internal management network' ||
+ some(pifs, disablePifUnplug)
+ )
+
+ return {
+ disableNetworkDelete: getDisableNetworkDelete,
+ }
+})
+class NetworkActions extends Component {
+ render () {
+ const { network, disableNetworkDelete } = this.props
+
+ return (
+
+
+
+ )
+ }
+}
+
+// =============================================================================
+
+const NETWORKS_COLUMNS = [
+ {
+ name: _('poolNetworkNameLabel'),
+ itemRenderer: network => ,
+ sortCriteria: network => network.name_label,
+ },
+ {
+ name: _('poolNetworkDescription'),
+ itemRenderer: network => ,
+ sortCriteria: network => network.name_description,
+ },
+ {
+ name: _('pif'),
+ itemRenderer: network => ,
+ },
+ {
+ name: _('pifVlanLabel'),
+ itemRenderer: network => ,
+ },
+ {
+ name: _('poolNetworkMTU'),
+ itemRenderer: network => network.MTU,
+ },
+ {
+ name: (
+
+
+
+
+
+ ),
+ itemRenderer: network => ,
+ },
+ {
+ name: _('poolNetworkPif'),
+ itemRenderer: network =>
+ !isEmpty(network.PIFs) && ,
+ },
+ {
+ name: '',
+ itemRenderer: network => ,
+ textAlign: 'right',
+ },
+]
+
+// =============================================================================
+
+export default class TabNetworks extends Component {
+ render () {
+ const { networks } = this.props
+
+ return (
+
+
+
+
+
+
+
+
+
+ {!isEmpty(networks) ? (
+
+ ) : (
+ {_('poolNoNetwork')}
+ )}
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/pool/tab-patches.js b/packages/xo-web/src/xo-app/pool/tab-patches.js
new file mode 100644
index 000000000..00a472421
--- /dev/null
+++ b/packages/xo-web/src/xo-app/pool/tab-patches.js
@@ -0,0 +1,45 @@
+import Component from 'base-component'
+import HostsPatchesTable from 'hosts-patches-table'
+import React from 'react'
+import Upgrade from 'xoa-upgrade'
+import { connectStore } from 'utils'
+import { Container, Row, Col } from 'grid'
+import { createGetObjectsOfType } from 'selectors'
+
+// ===================================================================
+
+@connectStore(() => {
+ const getHosts = createGetObjectsOfType('host').filter((_, props) => host =>
+ props.pool.id === host.$pool
+ )
+
+ return {
+ hosts: getHosts,
+ }
+})
+export default class TabPatches extends Component {
+ _getContainer = () => this.refs.container
+
+ render () {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/pool/tab-stats.js b/packages/xo-web/src/xo-app/pool/tab-stats.js
new file mode 100644
index 000000000..bda7f2217
--- /dev/null
+++ b/packages/xo-web/src/xo-app/pool/tab-stats.js
@@ -0,0 +1,195 @@
+import _ from 'intl'
+import Component from 'base-component'
+import getEventValue from 'get-event-value'
+import Icon from 'icon'
+import React from 'react'
+import Tooltip from 'tooltip'
+import Upgrade from 'xoa-upgrade'
+import { Container, Row, Col } from 'grid'
+import { Toggle } from 'form'
+import { fetchHostStats } from 'xo'
+import { createGetObjectsOfType, createSelector } from 'selectors'
+import { map } from 'lodash'
+import { connectStore } from 'utils'
+import {
+ PoolCpuLineChart,
+ PoolMemoryLineChart,
+ PoolPifLineChart,
+ PoolLoadLineChart,
+} from 'xo-line-chart'
+
+@connectStore({
+ hosts: createGetObjectsOfType('host').filter(
+ createSelector(
+ (state, props) => props.pool.id,
+ poolId => host => host.power_state === 'Running' && host.$pool === poolId
+ )
+ ),
+})
+export default class PoolStats extends Component {
+ state = {
+ useCombinedValues: false,
+ }
+
+ _loop = () => {
+ if (this.cancel) {
+ this.cancel()
+ }
+
+ let cancelled = false
+ this.cancel = () => {
+ cancelled = true
+ }
+
+ Promise.all(
+ map(this.props.hosts, host =>
+ fetchHostStats(host, this.state.granularity).then(stats => ({
+ host: host.name_label,
+ ...stats,
+ }))
+ )
+ ).then(stats => {
+ if (cancelled || !stats[0]) {
+ return
+ }
+ this.cancel = null
+
+ clearTimeout(this.timeout)
+ this.setState(
+ {
+ stats,
+ selectStatsLoading: false,
+ },
+ () => {
+ this.timeout = setTimeout(this._loop, stats[0].interval * 1000)
+ }
+ )
+ })
+ }
+
+ componentDidMount () {
+ this._loop()
+ }
+
+ componentWillUnmount () {
+ clearTimeout(this.timeout)
+ }
+
+ _handleSelectStats = event => {
+ const granularity = getEventValue(event)
+ clearTimeout(this.timeout)
+
+ this.setState(
+ {
+ granularity,
+ selectStatsLoading: true,
+ },
+ this._loop
+ )
+ }
+
+ render () {
+ const {
+ granularity,
+ selectStatsLoading,
+ stats,
+ useCombinedValues,
+ } = this.state
+
+ return process.env.XOA_PLAN > 2 ? (
+ stats ? (
+
+
+
+
+
+
+
+
+
+
+ {selectStatsLoading && (
+
+
+
+ )}
+
+
+
+
+ {_('statLastTenMinutes', message => (
+ {message}
+ ))}
+ {_('statLastTwoHours', message => (
+ {message}
+ ))}
+ {_('statLastWeek', message => (
+ {message}
+ ))}
+ {_('statLastYear', message => (
+ {message}
+ ))}
+
+
+
+
+
+
+
+ {_('statsCpu')}
+
+
+
+
+
+ {_('statsMemory')}
+
+
+
+
+
+
+
+
+
+ {_('statsNetwork')}
+
+ {/* key: workaround that unmounts and re-mounts the chart to make sure the legend updates when toggling "stacked values"
+ FIXME: remove key prop once this issue is fixed: https://github.com/CodeYellowBV/chartist-plugin-legend/issues/5 */}
+
+
+
+
+ {_('statLoad')}
+
+
+
+
+
+ ) : (
+ {_('poolNoStats')}
+ )
+ ) : (
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/self/helpers.js b/packages/xo-web/src/xo-app/self/helpers.js
new file mode 100644
index 000000000..c9056d818
--- /dev/null
+++ b/packages/xo-web/src/xo-app/self/helpers.js
@@ -0,0 +1,99 @@
+import _ from 'intl'
+import filter from 'lodash/filter'
+import forEach from 'lodash/forEach'
+import includes from 'lodash/includes'
+import intersection from 'lodash/intersection'
+import keyBy from 'lodash/keyBy'
+import map from 'lodash/map'
+import propTypes from 'prop-types-decorator'
+import React, { Component } from 'react'
+import reduce from 'lodash/reduce'
+import renderXoItem from 'render-xo-item'
+import { resolveIds } from 'utils'
+
+import { subscribeGroups, subscribeUsers } from 'xo'
+
+// ===================================================================
+
+@propTypes({
+ subjects: propTypes.array.isRequired,
+})
+export class Subjects extends Component {
+ constructor (props) {
+ super(props)
+ this.state = {
+ groups: {},
+ users: {},
+ }
+ }
+
+ componentWillMount () {
+ const unsubscribeGroups = subscribeGroups(groups => {
+ this.setState({
+ groups: keyBy(groups, 'id'),
+ })
+ })
+ const unsubscribeUsers = subscribeUsers(users => {
+ this.setState({
+ users: keyBy(users, 'id'),
+ })
+ })
+
+ this.componentWillUnmount = () => {
+ unsubscribeGroups()
+ unsubscribeUsers()
+ }
+ }
+
+ render () {
+ const { state } = this
+
+ return (
+
+ {map(this.props.subjects, id => {
+ if (state.users[id]) {
+ return renderXoItem(
+ { type: 'user', ...state.users[id] },
+ {
+ className: 'mr-1',
+ }
+ )
+ }
+
+ if (state.groups[id]) {
+ return renderXoItem(
+ { type: 'group', ...state.groups[id] },
+ {
+ className: 'mr-1',
+ }
+ )
+ }
+
+ return (
+
+ {_('unknownResourceSetValue')}
+
+ )
+ })}
+
+ )
+ }
+}
+
+export const computeAvailableHosts = (pools, srs, hostsByPool) => {
+ const validHosts = reduce(
+ hostsByPool,
+ (result, hosts, poolId) =>
+ includes(resolveIds(pools), poolId) ? result.concat(hosts) : result,
+ []
+ )
+
+ const availableHosts = filter(validHosts, host => {
+ let kept = false
+
+ forEach(srs, sr => !(kept = intersection(sr.$PBDs, host.$PBDs).length > 0))
+
+ return kept
+ })
+ return availableHosts
+}
diff --git a/packages/xo-web/src/xo-app/self/index.js b/packages/xo-web/src/xo-app/self/index.js
new file mode 100644
index 000000000..1377d7855
--- /dev/null
+++ b/packages/xo-web/src/xo-app/self/index.js
@@ -0,0 +1,760 @@
+import _ from 'intl'
+import ActionButton from 'action-button'
+import Collapse from 'collapse'
+import Component from 'base-component'
+import defined from 'xo-defined'
+import differenceBy from 'lodash/differenceBy'
+import filter from 'lodash/filter'
+import forEach from 'lodash/forEach'
+import get from 'lodash/get'
+import Icon from 'icon'
+import includes from 'lodash/includes'
+import intersection from 'lodash/intersection'
+import isEmpty from 'lodash/isEmpty'
+import keys from 'lodash/keys'
+import map from 'lodash/map'
+import mapKeys from 'lodash/mapKeys'
+import PropTypes from 'prop-types'
+import React from 'react'
+import remove from 'lodash/remove'
+import renderXoItem from 'render-xo-item'
+import ResourceSetQuotas from 'resource-set-quotas'
+import Upgrade from 'xoa-upgrade'
+import { Container, Row, Col } from 'grid'
+import { createGetObjectsOfType, createSelector } from 'selectors'
+import { injectIntl } from 'react-intl'
+import { SizeInput } from 'form'
+
+import {
+ createResourceSet,
+ deleteResourceSet,
+ editResourceSet,
+ recomputeResourceSetsLimits,
+ subscribeIpPools,
+ subscribeResourceSets,
+} from 'xo'
+
+import {
+ addSubscriptions,
+ connectStore,
+ resolveIds,
+ resolveResourceSets,
+} from 'utils'
+
+import {
+ SelectIpPool,
+ SelectNetwork,
+ SelectPool,
+ SelectSr,
+ SelectSubject,
+ SelectVmTemplate,
+} from 'select-objects'
+
+import { computeAvailableHosts, Subjects } from './helpers'
+
+import Page from '../page'
+
+// ===================================================================
+
+const HEADER = (
+
+
+
+
+ {_('selfServicePage')}
+
+
+
+
+)
+
+// ===================================================================
+
+const Hosts = ({ eligibleHosts, excludedHosts }) => (
+
+
+
+ {_('availableHosts')}
+ {_('availableHostsDescription')}
+
+
+ {_('excludedHosts')}
+
+
+
+
+
+ {eligibleHosts.length ? (
+ map(eligibleHosts, (host, key) => (
+
+ {renderXoItem(host)}
+
+ ))
+ ) : (
+ {_('noHostsAvailable')}
+ )}
+
+
+
+
+ {excludedHosts.length ? (
+ map(excludedHosts, (host, key) => (
+
+ {renderXoItem(host)}
+
+ ))
+ ) : (
+
+ {_('noHostsAvailable')}
+
+ )}
+
+
+
+
+)
+
+Hosts.propTypes = {
+ eligibleHosts: PropTypes.array.isRequired,
+ excludedHosts: PropTypes.array.isRequired,
+}
+
+// ===================================================================
+
+@connectStore(() => {
+ const getHosts = createGetObjectsOfType('host').sort()
+ const getHostsByPool = getHosts.groupBy('$pool')
+
+ return {
+ hosts: getHosts,
+ hostsByPool: getHostsByPool,
+ }
+})
+export class Edit extends Component {
+ static propTypes = {
+ onSave: PropTypes.func,
+ resourceSet: PropTypes.object,
+ }
+
+ constructor (props) {
+ super(props)
+
+ this.state = {
+ cpus: '',
+ disk: null,
+ eligibleHosts: [],
+ excludedHosts: props.hosts,
+ ipPools: [],
+ memory: null,
+ name: '',
+ networks: [],
+ pools: [],
+ srs: [],
+ subjects: [],
+ templates: [],
+ }
+ }
+
+ componentDidMount () {
+ const { resourceSet } = this.props
+
+ if (resourceSet) {
+ // Objects
+ const { objectsByType } = resourceSet
+ const pools = {}
+ forEach(objectsByType, objects => {
+ forEach(objects, object => {
+ pools[object.$pool] = true
+ })
+ })
+
+ this._updateSelectedPools(
+ keys(pools),
+ objectsByType.SR,
+ objectsByType.network
+ )
+
+ // Limits and others
+ const { ipPools: rawIpPools, limits } = resourceSet
+
+ const ipPools = []
+ forEach(rawIpPools, ipPool => {
+ ipPools.push({
+ id: ipPool,
+ quantity: get(limits, `[ipPool:${ipPool}].total`),
+ })
+ })
+
+ this.setState({
+ cpus: get(limits, 'cpus.total', ''),
+ disk: get(limits, 'disk.total', null),
+ ipPools,
+ memory: get(limits, 'memory.total', null),
+ name: resourceSet.name,
+ subjects: resourceSet.subjects,
+ templates: objectsByType['VM-template'] || [],
+ })
+ }
+ }
+
+ _save = async () => {
+ const {
+ cpus,
+ disk,
+ ipPools,
+ memory,
+ name,
+ networks,
+ srs,
+ subjects,
+ templates,
+ } = this.state
+
+ const set = this.props.resourceSet || (await createResourceSet(name))
+ const objects = [...templates, ...srs, ...networks]
+
+ const ipPoolsLimits = {}
+ forEach(ipPools, ipPool => {
+ if (ipPool.quantity) {
+ ipPoolsLimits[`ipPool:${ipPool.id}`] = +ipPool.quantity
+ }
+ })
+
+ await editResourceSet(set.id, {
+ name,
+ limits: {
+ cpus: cpus === '' ? undefined : +cpus,
+ memory: memory === null ? undefined : memory,
+ disk: disk === null ? undefined : disk,
+ ...ipPoolsLimits,
+ },
+ objects: resolveIds(objects),
+ subjects: resolveIds(subjects),
+ ipPools: resolveIds(ipPools),
+ })
+
+ this.props.onSave()
+ }
+
+ _reset = () => {
+ this._updateSelectedPools([], [], [])
+
+ this.setState({
+ cpus: '',
+ disk: null,
+ ipPools: [],
+ memory: null,
+ newIpPool: undefined,
+ newIpPoolQuantity: '',
+ subjects: [],
+ })
+ }
+
+ // -----------------------------------------------------------------------------
+
+ _updateSelectedPools = (newPools, newSrs, newNetworks) => {
+ const predicate = object => includes(resolveIds(newPools), object.$pool)
+
+ this.setState(
+ {
+ nPools: newPools.length,
+ pools: newPools,
+ srPredicate: predicate,
+ vmTemplatePredicate: predicate,
+ },
+ () => this._updateSelectedSrs(newSrs || this.state.srs, newNetworks)
+ )
+ }
+
+ _updateSelectedSrs = (newSrs, newNetworks) => {
+ const availableHosts = computeAvailableHosts(
+ this.state.pools,
+ newSrs,
+ this.props.hostsByPool
+ )
+ const networkPredicate = network => {
+ let kept = false
+ forEach(
+ availableHosts,
+ host => !(kept = intersection(network.PIFs, host.PIFs).length > 0)
+ )
+ return kept
+ }
+
+ this.setState(
+ {
+ availableHosts,
+ networkPredicate,
+ nSrs: newSrs.length,
+ srs: newSrs,
+ },
+ () => this._updateSelectedNetworks(newNetworks || this.state.networks)
+ )
+ }
+
+ _updateSelectedNetworks = newNetworks => {
+ const { availableHosts, srs } = this.state
+
+ const eligibleHosts = filter(availableHosts, host => {
+ let keptBySr = false
+ let keptByNetwork = false
+
+ forEach(
+ srs,
+ sr => !(keptBySr = intersection(sr.$PBDs, host.$PBDs).length > 0)
+ )
+
+ if (keptBySr) {
+ forEach(
+ newNetworks,
+ network =>
+ !(keptByNetwork = intersection(network.PIFs, host.PIFs).length > 0)
+ )
+ }
+
+ return keptBySr && keptByNetwork
+ })
+
+ this.setState({
+ eligibleHosts,
+ excludedHosts: differenceBy(
+ this.props.hosts,
+ eligibleHosts,
+ host => host.id
+ ),
+ networks: newNetworks,
+ })
+ }
+
+ // -----------------------------------------------------------------------------
+
+ _onChangeIpPool = newIpPool => {
+ const { ipPools, newIpPoolQuantity } = this.state
+
+ this.setState({
+ ipPools: [...ipPools, { id: newIpPool.id, quantity: newIpPoolQuantity }],
+ newIpPoolQuantity: '',
+ })
+ }
+
+ _removeIpPool = index => {
+ const ipPools = [...this.state.ipPools]
+ remove(ipPools, (_, i) => index === i)
+ this.setState({ ipPools })
+ }
+
+ _getIpPoolPredicate = createSelector(
+ () => map(this.state.ipPools, 'id'),
+ ipPoolsIds => ipPool => !includes(ipPoolsIds, ipPool.id)
+ )
+
+ // -----------------------------------------------------------------------------
+
+ render () {
+ const { state } = this
+ const { resourceSet } = this.props
+
+ return (
+
+
+
+
+
+
+
+ {_('saveResourceSet')}
+
+
+ {_('resetResourceSet')}
+
+ {resourceSet && (
+
+ {_('deleteResourceSet')}
+
+ )}
+
+
+
+ )
+ }
+}
+
+@addSubscriptions({
+ ipPools: subscribeIpPools,
+})
+@injectIntl
+class ResourceSet extends Component {
+ _renderDisplay = () => {
+ const { resourceSet } = this.props
+ const resolvedIpPools = mapKeys(this.props.ipPools, 'id')
+ const { limits, ipPools, subjects, objectsByType } = resourceSet
+
+ return [
+
+
+ ,
+ ...map(objectsByType, (objectsSet, type) => (
+
+ {map(objectsSet, object =>
+ renderXoItem(object, { className: 'mr-1' })
+ )}
+
+ )),
+ !isEmpty(ipPools) && (
+
+ {map(ipPools, pool => {
+ const resolvedIpPool = resolvedIpPools[pool]
+ const ipPoolLimits = limits && get(limits, `[ipPool:${pool}]`)
+ const available = ipPoolLimits && ipPoolLimits.available
+ const total = ipPoolLimits && ipPoolLimits.total
+ return (
+
+ {renderXoItem({
+ name: resolvedIpPool && resolvedIpPool.name,
+ type: 'ipPool',
+ })}
+ {ipPoolLimits && (
+
+ {' '}
+ ({available}/{total})
+
+ )}
+
+ )
+ })}
+
+ ),
+
+
+ ,
+
+
+
+ {_('editResourceSet')}
+
+
+ {_('deleteResourceSet')}
+
+
+ ,
+ ]
+ }
+
+ _autoExpand = ref => {
+ if (ref && ref.scrollIntoView) {
+ ref.scrollIntoView()
+ }
+ }
+
+ render () {
+ const { resourceSet, autoExpand } = this.props
+
+ return (
+
+
+
+ {this.state.editionMode ? (
+
+ ) : (
+ this._renderDisplay()
+ )}
+
+
+ {resourceSet.missingObjects.length > 0 && (
+
+ {_('resourceSetMissingObjects')} {' '}
+ {resourceSet.missingObjects.join(', ')}
+
+ )}
+
+ )
+ }
+}
+
+// ===================================================================
+
+export default class Self extends Component {
+ constructor (props) {
+ super(props)
+ this.state = {}
+ }
+
+ componentWillMount () {
+ this.componentWillUnmount = subscribeResourceSets(resourceSets => {
+ this.setState({
+ resourceSets: resolveResourceSets(resourceSets),
+ })
+ })
+ }
+
+ render () {
+ const { resourceSets, showNewResourceSetForm } = this.state
+ const { location } = this.props
+
+ return (
+
+ {process.env.XOA_PLAN > 3 ? (
+
+
+
+ {_('resourceSetNew')}
+
+
+ {_('recomputeResourceSets')}
+
+
+ {showNewResourceSetForm && [
+
,
+
,
+ ]}
+ {resourceSets
+ ? isEmpty(resourceSets)
+ ? _('noResourceSets')
+ : map(resourceSets, resourceSet => (
+
+ ))
+ : _('loadingResourceSets')}
+
+ ) : (
+
+
+
+ )}
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/settings/acls/index.js b/packages/xo-web/src/xo-app/settings/acls/index.js
new file mode 100644
index 000000000..f96a99f3a
--- /dev/null
+++ b/packages/xo-web/src/xo-app/settings/acls/index.js
@@ -0,0 +1,315 @@
+import _ from 'intl'
+import ActionButton from 'action-button'
+import ActionRowButton from 'action-row-button'
+import ButtonGroup from 'button-group'
+import Component from 'base-component'
+import filter from 'lodash/filter'
+import forEach from 'lodash/forEach'
+import isEmpty from 'lodash/isEmpty'
+import keyBy from 'lodash/keyBy'
+import map from 'lodash/map'
+import pickBy from 'lodash/pickBy'
+import React from 'react'
+import renderXoItem, { renderXoItemFromId } from 'render-xo-item'
+import some from 'lodash/some'
+import SortedTable from 'sorted-table'
+import toArray from 'lodash/toArray'
+import Upgrade from 'xoa-upgrade'
+import store from 'store'
+import { connectStore } from 'utils'
+import { Container } from 'grid'
+import { error } from 'notification'
+import {
+ SelectHighLevelObject,
+ SelectRole,
+ SelectSubject,
+} from 'select-objects'
+
+import { createGetObjectsOfType, createSelector } from 'selectors'
+
+import {
+ addAcl,
+ editAcl,
+ removeAcl,
+ subscribeAcls,
+ subscribeGroups,
+ subscribeRoles,
+ subscribeUsers,
+} from 'xo'
+
+const TYPES = ['VM', 'host', 'pool', 'SR', 'network']
+
+const ACL_COLUMNS = [
+ {
+ name: _('subjectName'),
+ itemRenderer: acl =>
+ acl.subject.id
+ ? renderXoItem(acl.subject)
+ : renderXoItemFromId(acl.subject),
+ sortCriteria: acl =>
+ (acl.subject.name || acl.subject.email || '').toLowerCase(),
+ },
+ {
+ name: _('objectName'),
+ itemRenderer: acl =>
+ acl.object.id ? renderXoItem(acl.object) : renderXoItemFromId(acl.object),
+ sortCriteria: acl =>
+ (acl.object.name || acl.object.name_label || '').toLowerCase(),
+ },
+ {
+ name: _('roleName'),
+ itemRenderer: acl => (
+ action && editAcl(acl, { action })}
+ value={acl.action}
+ />
+ ),
+ sortCriteria: acl => (acl.action.name || '').toLowerCase(),
+ },
+ {
+ name: '',
+ itemRenderer: acl => (
+
+ ),
+ },
+]
+
+@connectStore(() => {
+ const getHighLevelObjects = createSelector(
+ createGetObjectsOfType('host'),
+ createGetObjectsOfType('network'),
+ createGetObjectsOfType('pool'),
+ createGetObjectsOfType('SR'),
+ createGetObjectsOfType('VM'),
+ createGetObjectsOfType('VM-snapshot'),
+ (hosts, networks, pools, srs, vms, snapshots) => ({
+ ...keyBy(hosts, 'id'),
+ ...keyBy(networks, 'id'),
+ ...keyBy(pools, 'id'),
+ ...keyBy(snapshots, 'id'),
+ ...keyBy(srs, 'id'),
+ ...keyBy(vms, 'id'),
+ })
+ )
+ return { xoObjects: getHighLevelObjects }
+})
+class AclTable extends Component {
+ componentWillMount () {
+ let subjects = {}
+ const refresh = (newSubjects = undefined) => {
+ newSubjects && (subjects = newSubjects)
+ const { xoObjects } = this.props
+ const { acls, roles } = this.state
+ const resolvedAcls = filter(
+ map(acls, ({ subject, object, action }) => ({
+ subject: subjects[subject] || subject,
+ object: xoObjects[object] || object,
+ action: roles[action] || action,
+ })),
+ ({ subject, object, action }) =>
+ subject && object && action && object.type !== 'VM-snapshot'
+ )
+ this.setState({
+ resolvedAcls,
+ })
+ }
+
+ const unsubscribeAcls = subscribeAcls(acls =>
+ this.setState({ acls }, refresh)
+ )
+ const unsubscribeRoles = subscribeRoles(roles =>
+ this.setState({ roles: keyBy(roles, 'id') }, refresh)
+ )
+ const unsubscribeGroups = subscribeGroups(groups => {
+ groups = keyBy(groups, 'id')
+ refresh({
+ ...pickBy(subjects, subject => subject.type === 'user'),
+ ...groups,
+ })
+ })
+ const unsubscribeUsers = subscribeUsers(users => {
+ users = keyBy(users, 'id')
+ refresh({
+ ...pickBy(subjects, subject => subject.type === 'group'),
+ ...users,
+ })
+ })
+
+ this.componentWillUnmount = () => {
+ unsubscribeAcls()
+ unsubscribeGroups()
+ unsubscribeRoles()
+ unsubscribeUsers()
+ }
+ }
+
+ render () {
+ const { resolvedAcls = [] } = this.state
+
+ return isEmpty(resolvedAcls) ? (
+
+ {_('aclNoneFound')}
+
+ ) : (
+
+ )
+ }
+}
+
+export default class Acls extends Component {
+ constructor (props) {
+ super(props)
+ this.state = {
+ action: null,
+ objects: [],
+ subjects: [],
+ typeFilters: {},
+ }
+ }
+
+ _toggleTypeFilter = type => {
+ const { someTypeFilters, typeFilters, objects } = this.state
+
+ const newTypeFilters = { ...typeFilters, [type]: !typeFilters[type] }
+ const newSomeTypeFilters = some(newTypeFilters)
+
+ // If some objects need to be removed from the selected objects
+ if (!newTypeFilters[type] || (!someTypeFilters && newSomeTypeFilters)) {
+ this.setState({
+ objects: filter(
+ objects,
+ ({ type }) => !newSomeTypeFilters || newTypeFilters[type]
+ ),
+ })
+ }
+
+ this.setState(
+ {
+ typeFilters: { ...typeFilters, [type]: !typeFilters[type] },
+ someTypeFilters: some(newTypeFilters),
+ },
+ () => {
+ // If some objects need to be removed from the selected objects
+ if (
+ !this.state.typeFilters[type] ||
+ (!someTypeFilters && this.state.someTypeFilters)
+ ) {
+ this.setState({
+ objects: filter(objects, this._getObjectPredicate()),
+ })
+ }
+ }
+ )
+ }
+
+ _getObjectPredicate = createSelector(
+ () => this.state.typeFilters,
+ () => this.state.someTypeFilters,
+ (typeFilters, someTypeFilters) => ({ type }) =>
+ !someTypeFilters || typeFilters[type]
+ )
+
+ _selectAll = () => {
+ const { someTypeFilters, typeFilters } = this.state
+
+ const objects = []
+ forEach(TYPES, type => {
+ if (!someTypeFilters || typeFilters[type]) {
+ const typeObjects = createGetObjectsOfType(type)(store.getState())
+ objects.push(...toArray(typeObjects))
+ }
+ })
+ this.setState({ objects })
+ }
+
+ _addAcl = async () => {
+ const { subjects, objects, action } = this.state
+ try {
+ const promises = []
+ forEach(subjects, subject => {
+ forEach(objects, object => {
+ promises.push(addAcl({ subject, object, action }))
+ })
+ })
+ await Promise.all(promises)
+
+ this.setState({
+ subjects: [],
+ objects: [],
+ action: '',
+ })
+ } catch (err) {
+ error('Add ACL(s)', err.message || String(err))
+ }
+ }
+
+ render () {
+ const { typeFilters, objects, action, subjects } = this.state
+
+ return process.env.XOA_PLAN > 2 ? (
+
+
+
+
+
+ ) : (
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/settings/config/index.js b/packages/xo-web/src/xo-app/settings/config/index.js
new file mode 100644
index 000000000..45f9f106b
--- /dev/null
+++ b/packages/xo-web/src/xo-app/settings/config/index.js
@@ -0,0 +1,113 @@
+import _ from 'intl'
+import ActionButton from 'action-button'
+import Button from 'button'
+import Component from 'base-component'
+import Dropzone from 'dropzone'
+import Icon from 'icon'
+import React from 'react'
+import { formatSize } from 'utils'
+import { importConfig, exportConfig } from 'xo'
+
+// ===================================================================
+
+export default class Config extends Component {
+ componentWillMount () {
+ this.state = { importStatus: 'noFile' }
+ }
+
+ _importConfig = () => {
+ this.setState({ importStatus: 'start' }, () =>
+ importConfig(this.state.configFile).then(
+ () => this.setState({ configFile: undefined, importStatus: 'end' }),
+ () =>
+ this.setState({ configFile: undefined, importStatus: 'importError' })
+ )
+ )
+ }
+
+ _handleDrop = files =>
+ this.setState({
+ configFile: files && files[0],
+ importStatus: 'selectedFile',
+ })
+
+ _unselectFile = () =>
+ this.setState({ configFile: undefined, importStatus: 'noFile' })
+
+ _renderImportStatus = () => {
+ const { configFile, importStatus } = this.state
+
+ switch (importStatus) {
+ case 'noFile':
+ return _('noConfigFile')
+ case 'selectedFile':
+ return (
+ {`${configFile.name} (${formatSize(configFile.size)})`}
+ )
+ case 'start':
+ return
+ case 'end':
+ return {_('importConfigSuccess')}
+ case 'importError':
+ return {_('importConfigError')}
+ }
+ }
+
+ render () {
+ const { configFile } = this.state
+
+ return (
+
+ {process.env.XOA_PLAN < 5 ? (
+
+
+ {_('importConfig')}
+
+
+
+ ) : (
+
+ )}
+
+
+
+ {_('exportConfig')}
+
+
+ {_('downloadConfig')}
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/settings/groups/index.js b/packages/xo-web/src/xo-app/settings/groups/index.js
new file mode 100644
index 000000000..40d7c4b73
--- /dev/null
+++ b/packages/xo-web/src/xo-app/settings/groups/index.js
@@ -0,0 +1,187 @@
+import _, { messages } from 'intl'
+import ActionButton from 'action-button'
+import Component from 'base-component'
+import includes from 'lodash/includes'
+import isEmpty from 'lodash/isEmpty'
+import keyBy from 'lodash/keyBy'
+import map from 'lodash/map'
+import propTypes from 'prop-types-decorator'
+import React from 'react'
+import size from 'lodash/size'
+import SortedTable from 'sorted-table'
+import { addSubscriptions } from 'utils'
+import { injectIntl } from 'react-intl'
+import { SelectSubject } from 'select-objects'
+import { Text } from 'editable'
+
+import {
+ addUserToGroup,
+ createGroup,
+ deleteGroup,
+ removeUserFromGroup,
+ setGroupName,
+ subscribeGroups,
+ subscribeUsers,
+} from 'xo'
+
+@addSubscriptions({
+ users: cb => subscribeUsers(users => cb(keyBy(users, 'id'))),
+})
+@propTypes({
+ id: propTypes.string.isRequired, // user id
+ group: propTypes.object.isRequired, // group
+})
+class UserDisplay extends Component {
+ _removeUser = () => {
+ const { id, group } = this.props
+ return removeUserFromGroup(id, group)
+ }
+
+ render () {
+ const { id, users } = this.props
+
+ return (
+
+ {(id && users && users[id] && users[id].email) || (
+ <{_('unknownUser')}>
+ )}{' '}
+
+
+ )
+ }
+}
+
+class GroupMembersDisplay extends Component {
+ _toggle = () => this.setState({ open: !this.state.open })
+
+ render () {
+ const { group } = this.props
+ return (
+
+ {isEmpty(group.users) ? (
+
{_('noUserInGroup')}
+ ) : (
+
+
+ {_('countUsers', { users: size(group.users) })}
+
+
+ {this.state.open && (
+
+
+ {map(group.users, user => (
+
+
+
+ ))}
+
+
+ )}
+
+ )}
+
+ )
+ }
+}
+
+const getPredicate = users => entity =>
+ entity.email && !includes(users, entity.id) // Entity is a user and is not already in list
+
+const GROUP_COLUMNS = [
+ {
+ name: _('groupNameColumn'),
+ itemRenderer: group => (
+ setGroupName(group, value)} />
+ ),
+ sortCriteria: group => group.name,
+ },
+ {
+ name: _('groupUsersColumn'),
+ itemRenderer: group => ,
+ },
+ {
+ name: _('addUserToGroupColumn'),
+ itemRenderer: group => (
+ user && addUserToGroup(user, group)}
+ value={null}
+ />
+ ),
+ },
+ {
+ name: '',
+ itemRenderer: group => (
+
+ ),
+ },
+]
+
+@addSubscriptions({
+ groups: subscribeGroups,
+})
+@injectIntl
+export default class Groups extends Component {
+ _createGroup = () => {
+ const { name } = this.refs
+ if (name) {
+ return createGroup(name.value).then(() => {
+ name.value = ''
+ })
+ }
+ }
+
+ render () {
+ const { groups, intl } = this.props
+
+ return (
+
+
+
+ {isEmpty(groups) ? (
+
+ {_('noGroupFound')}
+
+ ) : (
+
+ )}
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/settings/index.js b/packages/xo-web/src/xo-app/settings/index.js
new file mode 100644
index 000000000..aee9da951
--- /dev/null
+++ b/packages/xo-web/src/xo-app/settings/index.js
@@ -0,0 +1,78 @@
+import _ from 'intl'
+import Icon from 'icon'
+import Page from '../page'
+import React from 'react'
+import { routes } from 'utils'
+import { Container, Row, Col } from 'grid'
+import { NavLink, NavTabs } from 'nav'
+
+import Acls from './acls'
+import Config from './config'
+import Groups from './groups'
+import Ips from './ips'
+import Logs from './logs'
+import Plugins from './plugins'
+import Remotes from './remotes'
+import Servers from './servers'
+import Users from './users'
+
+const HEADER = (
+
+
+
+
+ {_('settingsPage')}
+
+
+
+
+
+ {_('settingsServersPage')}
+
+
+ {_('settingsUsersPage')}
+
+
+ {_('settingsGroupsPage')}
+
+
+ {_('settingsAclsPage')}
+
+
+ {_('backupRemotesPage')}
+
+
+ {_('settingsPluginsPage')}
+
+
+ {_('settingsLogsPage')}
+
+
+ {_('settingsIpsPage')}
+
+
+ {_('settingsConfigPage')}
+
+
+
+
+
+)
+
+const Settings = routes('servers', {
+ acls: Acls,
+ config: Config,
+ groups: Groups,
+ ips: Ips,
+ logs: Logs,
+ plugins: Plugins,
+ remotes: Remotes,
+ servers: Servers,
+ users: Users,
+})(({ children }) => (
+
+ {children}
+
+))
+
+export default Settings
diff --git a/packages/xo-web/src/xo-app/settings/ips/index.js b/packages/xo-web/src/xo-app/settings/ips/index.js
new file mode 100644
index 000000000..9a0853a32
--- /dev/null
+++ b/packages/xo-web/src/xo-app/settings/ips/index.js
@@ -0,0 +1,426 @@
+import _, { messages } from 'intl'
+import ActionButton from 'action-button'
+import ActionRowButton from 'action-row-button'
+import BaseComponent from 'base-component'
+import Icon from 'icon'
+import React from 'react'
+import SingleLineRow from 'single-line-row'
+import SortedTable from 'sorted-table'
+import Upgrade from 'xoa-upgrade'
+import { addSubscriptions, connectStore } from 'utils'
+import { Container, Row, Col } from 'grid'
+import { createGetObjectsOfType, createSelector } from 'selectors'
+import { formatIps, getNextIpV4, parseIpPattern } from 'ip'
+import { injectIntl } from 'react-intl'
+import { Input as DebounceInput } from 'debounce-input-decorator'
+import { renderXoItemFromId } from 'render-xo-item'
+import { SelectNetwork } from 'select-objects'
+import { Text } from 'editable'
+import {
+ some,
+ findIndex,
+ forEach,
+ includes,
+ isEmpty,
+ isObject,
+ keys,
+ map,
+} from 'lodash'
+import { createIpPool, deleteIpPool, setIpPool, subscribeIpPools } from 'xo'
+
+const FULL_WIDTH = { width: '100%' }
+const NETWORK_FORM_STYLE = { maxWidth: '40em' }
+const IPS_PATTERN = (() => {
+ const ipRe = '\\d{1,3}(\\.\\d{1,3}){3}'
+ const ipOrRangeRe = `${ipRe}(-${ipRe})?`
+ return `${ipOrRangeRe}(;${ipOrRangeRe})*`
+})()
+
+@connectStore(() => ({
+ networks: createGetObjectsOfType('network').groupBy('id'),
+ vifs: createGetObjectsOfType('VIF').groupBy('id'),
+}))
+class IpsCell extends BaseComponent {
+ _addIps = () => {
+ const addresses = {}
+ forEach(parseIpPattern(this.state.newIps), ip => {
+ addresses[ip] = {}
+ })
+ setIpPool(this.props.ipPool.id, { addresses })
+ this.setState({ newIps: '' })
+ }
+
+ _deleteIp = ip => {
+ const toBeRemoved = {}
+ if (isObject(ip)) {
+ let currentIp = ip.first
+ while (currentIp !== ip.last) {
+ toBeRemoved[currentIp] = null
+ currentIp = getNextIpV4(currentIp)
+ }
+ toBeRemoved[currentIp] = null
+ } else {
+ toBeRemoved[ip] = null
+ }
+ setIpPool(this.props.ipPool.id, { addresses: toBeRemoved })
+ }
+
+ render () {
+ const { ipPool, networks, vifs } = this.props
+ const { newIps, showNewIpForm } = this.state
+
+ return (
+
+
+
+ {_('ipsVifs')}
+
+
+ {ipPool.addresses &&
+ map(formatIps(keys(ipPool.addresses)), (ip, key) => {
+ if (isObject(ip)) {
+ // Range of IPs
+ return (
+
+
+
+ {ip.first} {ip.last}
+
+
+
+
+
+
+ )
+ }
+ const addressVifs = ipPool.addresses[ip].vifs
+ return (
+
+
+ {ip}
+
+
+ {!isEmpty(addressVifs) ? (
+ map(addressVifs, (vifId, index) => {
+ const vif = vifs[vifId] && vifs[vifId][0]
+ const network =
+ vif &&
+ networks[vif.$network] &&
+ networks[vif.$network][0]
+ return (
+
+ {network && vif ? (
+ `${network.name_label} #${vif.device}`
+ ) : (
+ {_('ipPoolUnknownVif')}
+ )}
+
+ )
+ })
+ ) : (
+ {_('ipsNotUsed')}
+ )}
+
+
+
+
+
+ )
+ })}
+
+
+ {showNewIpForm ? (
+
+ ) : (
+
+ )}
+
+
+
+ )
+ }
+}
+
+class NetworksCell extends BaseComponent {
+ state = { newNetworks: [] }
+
+ _addNetworks = () => {
+ if (isEmpty(this.state.newNetworks)) {
+ return this._toggleNewNetworks()
+ }
+ const { ipPool } = this.props
+ setIpPool(ipPool.id, {
+ networks: [...ipPool.networks, ...this.state.newNetworks],
+ })
+ this._toggleNewNetworks()
+ this.setState({ newNetworks: [] })
+ }
+
+ _deleteNetwork = networkId => {
+ const _networks = [...this.props.ipPool.networks]
+ const index = findIndex(_networks, network => network === networkId)
+ if (index !== -1) {
+ _networks.splice(index, 1)
+ setIpPool(this.props.ipPool.id, { networks: _networks })
+ }
+ }
+
+ _toggleNewNetworks = () =>
+ this.setState({ showNewNetworkForm: !this.state.showNewNetworkForm })
+ _getNetworkPredicate = createSelector(
+ () => this.props.ipPool && this.props.ipPool.networks,
+ networks => network => !includes(networks, network.id)
+ )
+
+ render () {
+ const { ipPool } = this.props
+ const { newNetworks, showNewNetworkForm } = this.state
+
+ return (
+
+ {map(ipPool.networks, networkId => (
+
+ {renderXoItemFromId(networkId)}
+
+
+
+
+ ))}
+
+ {showNewNetworkForm ? (
+
+ ) : (
+
+
+
+ )}
+
+
+ )
+ }
+}
+
+@addSubscriptions({
+ ipPools: subscribeIpPools,
+})
+@injectIntl
+export default class Ips extends BaseComponent {
+ _create = () => {
+ const { name, ips, networks } = this.state
+
+ this.setState({ creatingIpPool: true })
+ return createIpPool({
+ ips: parseIpPattern(ips),
+ name,
+ networks: map(networks, 'id'),
+ }).then(() => {
+ this.setState({
+ creatingIpPool: false,
+ ips: undefined,
+ name: undefined,
+ networks: [],
+ })
+ })
+ }
+
+ _getNameAlreadyExists = createSelector(
+ () => this.props.ipPools,
+ ipPools => name => some(ipPools, { name })
+ )
+
+ _disableCreation = createSelector(
+ this._getNameAlreadyExists,
+ () => this.state,
+ (nameAlreadyExists, { name, ips, networks }) =>
+ !name || isEmpty(ips) || isEmpty(networks) || nameAlreadyExists(name)
+ )
+
+ _onChangeIpPoolName = (ipPool, name) => {
+ if (some(this.props.ipPools, { name })) {
+ throw new Error(
+ this.props.intl.formatMessage(messages.ipPoolNameAlreadyExists)
+ )
+ }
+
+ return setIpPool(ipPool, { name })
+ }
+
+ _ipColumns = [
+ {
+ default: true,
+ name: _('ipPoolName'),
+ itemRenderer: ipPool => (
+ this._onChangeIpPoolName(ipPool, name)}
+ value={ipPool.name}
+ />
+ ),
+ sortCriteria: ipPool => ipPool.name,
+ },
+ {
+ name: _('ipPoolIps'),
+ itemRenderer: ipPool => ,
+ },
+ {
+ name: _('ipPoolNetworks'),
+ itemRenderer: ipPool => ,
+ },
+ {
+ name: '',
+ itemRenderer: ipPool => (
+
+
+
+ ),
+ },
+ ]
+
+ render () {
+ if (process.env.XOA_PLAN < 4) {
+ return (
+
+
+
+ )
+ }
+
+ const { ipPools, intl } = this.props
+ const { creatingIpPool, ips, name, networks } = this.state
+
+ return (
+
+
+
+
+
+
+
+ {isEmpty(ipPools) ? (
+
+ {_('ipsNoIpPool')}
+
+ ) : (
+
+ )}
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/settings/logs/index.css b/packages/xo-web/src/xo-app/settings/logs/index.css
new file mode 100644
index 000000000..fe741bd6a
--- /dev/null
+++ b/packages/xo-web/src/xo-app/settings/logs/index.css
@@ -0,0 +1,5 @@
+.widthLimit {
+ overflow-wrap: break-word;
+ width: 100%;
+ max-width: 30em;
+}
diff --git a/packages/xo-web/src/xo-app/settings/logs/index.js b/packages/xo-web/src/xo-app/settings/logs/index.js
new file mode 100644
index 000000000..ee3682ed5
--- /dev/null
+++ b/packages/xo-web/src/xo-app/settings/logs/index.js
@@ -0,0 +1,184 @@
+import React from 'react'
+import { FormattedDate } from 'react-intl'
+import { find, map } from 'lodash'
+
+import _ from 'intl'
+import ActionRowButton from 'action-row-button'
+import BaseComponent from 'base-component'
+import ButtonGroup from 'button-group'
+import Copiable from 'copiable'
+import NoObjects from 'no-objects'
+import SortedTable from 'sorted-table'
+import styles from './index.css'
+import TabButton from 'tab-button'
+import { addSubscriptions } from 'utils'
+import { alert, confirm } from 'modal'
+import { createSelector } from 'selectors'
+import { subscribeApiLogs, subscribeUsers, deleteApiLog } from 'xo'
+
+const CAN_REPORT_BUG = process.env.XOA_PLAN > 1
+
+const reportBug = log => {
+ const title = encodeURIComponent(`Error on ${log.data.method}`)
+ const message = encodeURIComponent(
+ `\`\`\`\n${log.data.method}\n${JSON.stringify(
+ log.data.params,
+ null,
+ 2
+ )}\n${JSON.stringify(log.data.error, null, 2).replace(
+ /\\n/g,
+ '\n'
+ )}\n\`\`\``
+ )
+
+ window.open(
+ process.env.XOA_PLAN < 5
+ ? `https://xen-orchestra.com/#!/member/support?title=${title}&message=${message}`
+ : `https://github.com/vatesfr/xo-web/issues/new?title=${title}&body=${message}`
+ )
+}
+
+const COLUMNS = [
+ {
+ name: _('logUser'),
+ itemRenderer: (log, { users }) => {
+ if (log.data.userId == null) {
+ return _('noUser')
+ }
+ if (!users) {
+ return '...'
+ }
+ const user = find(users, user => user.id === log.data.userId)
+ return user ? user.email : _('unknownUser')
+ },
+ sortCriteria: log => log.data.userId,
+ },
+ {
+ name: _('logMessage'),
+ itemRenderer: log => (
+
+ {log.data.error && log.data.error.message}
+
+ ),
+ sortCriteria: log => log.data.error && log.data.error.message,
+ },
+ {
+ default: true,
+ name: _('logTime'),
+ itemRenderer: log => (
+
+ {log.time && (
+
+ )}
+
+ ),
+ sortCriteria: log => log.time,
+ sortOrder: 'desc',
+ },
+ {
+ name: '',
+ itemRenderer: (log, { showError }) => (
+
+
+
+
+ {CAN_REPORT_BUG && (
+ reportBug(log)}
+ icon='bug'
+ tooltip={_('reportBug')}
+ />
+ )}
+
+
+ ),
+ },
+]
+
+@addSubscriptions({
+ logs: subscribeApiLogs,
+ users: subscribeUsers,
+})
+export default class Logs extends BaseComponent {
+ _deleteAllLogs = () =>
+ confirm({
+ title: _('logDeleteAllTitle'),
+ body: _('logDeleteAllMessage'),
+ }).then(() =>
+ Promise.all(map(this.props.logs, (log, id) => deleteApiLog(id)))
+ )
+
+ _getLogs = createSelector(
+ () => this.props.logs,
+ logs => logs && map(logs, (log, id) => ({ ...log, id }))
+ )
+
+ _showError = log =>
+ alert(
+ _('logError'),
+
+ {`${log.data.method}\n${JSON.stringify(
+ log.data.params,
+ null,
+ 2
+ )}\n${JSON.stringify(log.data.error, null, 2).replace(/\\n/g, '\n')}`}
+
+ )
+
+ _getData = createSelector(
+ () => this.props.users,
+ () => this._showError,
+ (users, showError) => ({ users, showError })
+ )
+
+ _getPredicate = logs => logs != null
+
+ render () {
+ const logs = this._getLogs()
+
+ return (
+
+ {() => (
+
+
+
+ {' '}
+
+
+ )}
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/settings/plugins/index.js b/packages/xo-web/src/xo-app/settings/plugins/index.js
new file mode 100644
index 000000000..522db2051
--- /dev/null
+++ b/packages/xo-web/src/xo-app/settings/plugins/index.js
@@ -0,0 +1,280 @@
+import _ from 'intl'
+import ActionButton from 'action-button'
+import ActionToggle from 'action-toggle'
+import Button from 'button'
+import Component from 'base-component'
+import GenericInput from 'json-schema-input'
+import Icon from 'icon'
+import isEmpty from 'lodash/isEmpty'
+import map from 'lodash/map'
+import React from 'react'
+import size from 'lodash/size'
+import { addSubscriptions } from 'utils'
+import { alert } from 'modal'
+import { createSelector } from 'reselect'
+import { generateUiSchema } from 'xo-json-schema-input'
+import { lastly } from 'promise-toolbox'
+import { Row, Col } from 'grid'
+import {
+ configurePlugin,
+ disablePluginAutoload,
+ enablePluginAutoload,
+ loadPlugin,
+ purgePluginConfiguration,
+ subscribePlugins,
+ testPlugin,
+ unloadPlugin,
+} from 'xo'
+
+class Plugin extends Component {
+ constructor (props) {
+ super(props)
+
+ this.configFormId = `form-config-${props.id}`
+ this.testFormId = `form-test-${props.id}`
+ }
+
+ _getUiSchema = createSelector(
+ () => this.props.configurationSchema,
+ generateUiSchema
+ )
+
+ _updateExpanded = () => {
+ this.setState({
+ expanded: !this.state.expanded,
+ })
+ }
+
+ _setAutoload = event => {
+ if (this._updateAutoload) {
+ return
+ }
+
+ this._updateAutoload = true
+
+ const method = event.target.checked
+ ? enablePluginAutoload
+ : disablePluginAutoload
+ method(this.props.id)::lastly(() => {
+ this._updateAutoload = false
+ })
+ }
+
+ _updateLoad = () => {
+ const { props } = this
+
+ if (!props.loaded) {
+ return loadPlugin(props.id)
+ }
+ if (props.unloadable !== false) {
+ return unloadPlugin(props.id)
+ }
+ }
+
+ _saveConfiguration = async () => {
+ await configurePlugin(this.props.id, this.state.editedConfig)
+ this._stopEditing()
+ }
+
+ _deleteConfiguration = async () => {
+ await purgePluginConfiguration(this.props.id)
+ this._stopEditing()
+ }
+
+ _stopEditing = event => {
+ event && event.preventDefault()
+
+ this.setState({
+ editedConfig: undefined,
+ })
+ }
+
+ _applyPredefinedConfiguration = () => {
+ const configName = this.refs.selectPredefinedConfiguration.value
+ this.setState({
+ editedConfig: this.props.configurationPresets[configName],
+ })
+ }
+
+ _test = async () => {
+ try {
+ const { testInput } = this.refs
+ await testPlugin(this.props.id, testInput && testInput.value)
+ } catch (err) {
+ await alert(
+ 'You have an error!',
+
+
Code: {err.code}
+
Message: {err.message}
+ {err.data &&
{JSON.stringify(err.data, null, 2)} }
+
+ )
+ throw err
+ }
+ }
+
+ render () {
+ const { props, state } = this
+ const { editedConfig, expanded } = state
+ const { configurationPresets, configurationSchema, loaded } = props
+
+ return (
+
+
+
+
+
+ {` ${props.name} `}
+ {`(v${props.version}) `}
+
+
+ {_('autoloadPlugin')}{' '}
+
+
+
+
+
+ {configurationSchema !== undefined && (
+
+
+
+
+
+ )}
+
+ {expanded && (
+
+ )}
+ {expanded &&
+ props.testable && (
+
+ )}
+
+ )
+ }
+}
+
+@addSubscriptions({
+ plugins: subscribePlugins,
+})
+export default class Plugins extends Component {
+ render () {
+ if (isEmpty(this.props.plugins)) {
+ return (
+
+ {_('noPlugins')}
+
+ )
+ }
+
+ return (
+
+
+ {map(this.props.plugins, (plugin, key) => (
+
+
+
+ ))}
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/settings/remotes/index.js b/packages/xo-web/src/xo-app/settings/remotes/index.js
new file mode 100644
index 000000000..f956d3b26
--- /dev/null
+++ b/packages/xo-web/src/xo-app/settings/remotes/index.js
@@ -0,0 +1,517 @@
+import _, { messages } from 'intl'
+import ActionButton from 'action-button'
+import filter from 'lodash/filter'
+import Icon from 'icon'
+import isEmpty from 'lodash/isEmpty'
+import map from 'lodash/map'
+import React, { Component } from 'react'
+import some from 'lodash/some'
+import SortedTable from 'sorted-table'
+import StateButton from 'state-button'
+import Tooltip from 'tooltip'
+import { addSubscriptions } from 'utils'
+import { alert } from 'modal'
+import { error } from 'notification'
+import { format, parse } from 'xo-remote-parser'
+import { Password, Text } from 'editable'
+import { injectIntl } from 'react-intl'
+
+import {
+ createRemote,
+ deleteRemote,
+ deleteRemotes,
+ disableRemote,
+ editRemote,
+ enableRemote,
+ subscribeRemotes,
+ testRemote,
+} from 'xo'
+
+const remoteTypes = {
+ file: 'remoteTypeLocal',
+ nfs: 'remoteTypeNfs',
+ smb: 'remoteTypeSmb',
+}
+const _changeUrlElement = (remote, value, element) =>
+ editRemote(remote, { url: format({ ...remote, [element]: value }) })
+const _showError = remote => alert(_('remoteConnectionFailed'), remote.error)
+const COLUMN_NAME = {
+ component: @injectIntl
+ class RemoteName extends Component {
+ render () {
+ const { item: remote, intl } = this.props
+ return (
+ editRemote(remote, { name })}
+ placeholder={intl.formatMessage(messages.remoteMyNamePlaceHolder)}
+ value={remote.name}
+ />
+ )
+ }
+ },
+ name: _('remoteName'),
+ sortCriteria: 'name',
+}
+const COLUMN_STATE = {
+ itemRenderer: remote => (
+
+ ),
+ name: _('remoteState'),
+}
+
+const COLUMNS_LOCAL_REMOTE = [
+ COLUMN_NAME,
+ {
+ component: @injectIntl
+ class LocalRemotePath extends Component {
+ render () {
+ const { item: remote, intl } = this.props
+ return (
+ _changeUrlElement(remote, v, 'path')}
+ placeholder={intl.formatMessage(
+ messages.remoteLocalPlaceHolderPath
+ )}
+ value={remote.path}
+ />
+ )
+ }
+ },
+ name: _('remotePath'),
+ },
+ COLUMN_STATE,
+]
+const COLUMNS_NFS_REMOTE = [
+ COLUMN_NAME,
+ {
+ component: @injectIntl
+ class NfsRemoteInfo extends Component {
+ render () {
+ const { item: remote, intl } = this.props
+ return (
+
+ \\
+ _changeUrlElement(remote, v, 'host')}
+ placeholder={intl.formatMessage(
+ messages.remoteNfsPlaceHolderHost
+ )}
+ value={remote.host}
+ />
+ :
+ _changeUrlElement(remote, v, 'path')}
+ placeholder={intl.formatMessage(
+ messages.remoteNfsPlaceHolderPath
+ )}
+ value={remote.path}
+ />
+
+ )
+ }
+ },
+ name: _('remoteDevice'),
+ },
+ COLUMN_STATE,
+]
+const COLUMNS_SMB_REMOTE = [
+ COLUMN_NAME,
+ {
+ component: @injectIntl
+ class SmbRemoteInfo extends Component {
+ render () {
+ const { item: remote, intl } = this.props
+ return (
+
+ \\
+ _changeUrlElement(remote, v, 'host')}
+ />
+ \
+
+ _changeUrlElement(remote, v, 'path')}
+ placeholder={intl.formatMessage(
+ messages.remoteSmbPlaceHolderRemotePath
+ )}
+ value={remote.path}
+ />
+
+
+ )
+ }
+ },
+ name: _('remoteShare'),
+ },
+ COLUMN_STATE,
+ {
+ component: @injectIntl
+ class SmbRemoteAuthInfo extends Component {
+ render () {
+ const { item: remote, intl } = this.props
+ return (
+
+ _changeUrlElement(remote, v, 'username')}
+ />
+ :
+ _changeUrlElement(remote, v, 'password')}
+ placeholder={intl.formatMessage(
+ messages.remotePlaceHolderPassword
+ )}
+ />
+ @
+ _changeUrlElement(remote, v, 'domain')}
+ />
+
+ )
+ }
+ },
+ name: _('remoteAuth'),
+ },
+]
+
+const GROUPED_ACTIONS = [
+ {
+ handler: deleteRemotes,
+ icon: 'delete',
+ label: _('remoteDeleteSelected'),
+ level: 'danger',
+ },
+]
+
+const INDIVIDUAL_ACTIONS = [
+ {
+ disabled: remote => !remote.enabled,
+ handler: remote =>
+ testRemote(remote).then(
+ answer =>
+ answer.success
+ ? alert(
+
+ {' '}
+ {_('remoteTestSuccess', { name: remote.name })}
+ ,
+ _('remoteTestSuccessMessage')
+ )
+ : alert(
+
+ {' '}
+ {_('remoteTestFailure', { name: remote.name })}
+ ,
+
+
+ {_('remoteTestError')}
+ {answer.error}
+ {_('remoteTestStep')}
+ {answer.step}
+
+
+ )
+ ),
+ icon: 'diagnosis',
+ label: _('remoteTestTip'),
+ level: 'primary',
+ },
+ {
+ handler: deleteRemote,
+ icon: 'delete',
+ label: _('remoteDeleteTip'),
+ level: 'danger',
+ },
+]
+const FILTERS = {
+ filterRemotesOnlyConnected: 'enabled?',
+ filterRemotesOnlyDisconnected: '!enabled?',
+}
+
+@addSubscriptions({
+ remotes: cb =>
+ subscribeRemotes(rawRemotes => {
+ rawRemotes = map(rawRemotes, remote => ({
+ ...remote,
+ ...parse(remote.url),
+ }))
+ const remotes = {}
+ for (const remoteType in remoteTypes) {
+ remotes[remoteType] = filter(rawRemotes, r => r.type === remoteType)
+ }
+ cb(remotes)
+ }),
+})
+@injectIntl
+export default class Remotes extends Component {
+ constructor (props) {
+ super(props)
+ this.state = {
+ type: 'file',
+ }
+ }
+
+ _handleRemoteTypeSelection = type => this.setState({ type })
+
+ _checkNameExists = () =>
+ some(this.props.remotes, values =>
+ some(values, ['name', this.refs.name.value])
+ )
+ ? alert(
+
+ {_('remoteTestName')}
+ ,
+ {_('remoteTestNameFailure')}
+ )
+ : this._createRemote()
+
+ _createRemote = async () => {
+ const { name, host, path, username, password, domain } = this.refs
+ const { type } = this.state
+
+ const urlParams = {
+ type,
+ host: host && host.value,
+ path: path && path.value,
+ }
+ username && (urlParams.username = username.value)
+ password && (urlParams.password = password.value)
+ domain && (urlParams.domain = domain.value)
+
+ const url = format(urlParams)
+ return createRemote(name && name.value, url).then(
+ () => {
+ this.setState({ type: 'file' })
+ path && (path.value = '')
+ username && (username.value = '')
+ password && (password.value = '')
+ domain && (domain.value = '')
+ },
+ err => error('Create Remote', err.message || String(err))
+ )
+ }
+
+ render () {
+ const { remotes = {} } = this.props
+ const { type } = this.state
+
+ return (
+
+ {!isEmpty(remotes.file) && (
+
+
{_('remoteTypeLocal')}
+
+
+ )}
+
+ {!isEmpty(remotes.nfs) && (
+
+
{_('remoteTypeNfs')}
+
+
+ )}
+
+ {!isEmpty(remotes.smb) && (
+
+
{_('remoteTypeSmb')}
+
+
+ )}
+
+
{_('newRemote')}
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/settings/servers/index.js b/packages/xo-web/src/xo-app/settings/servers/index.js
new file mode 100644
index 000000000..6fd1769fb
--- /dev/null
+++ b/packages/xo-web/src/xo-app/settings/servers/index.js
@@ -0,0 +1,243 @@
+import _, { messages } from 'intl'
+import ActionButton from 'action-button'
+import Component from 'base-component'
+import Icon from 'icon'
+import React from 'react'
+import SortedTable from 'sorted-table'
+import StateButton from 'state-button'
+import Tooltip from 'tooltip'
+import { addSubscriptions } from 'utils'
+import { alert, confirm } from 'modal'
+import { Container } from 'grid'
+import { Password as EditablePassword, Text } from 'editable'
+import { Password, Toggle } from 'form'
+import { injectIntl } from 'react-intl'
+import { noop } from 'lodash'
+import {
+ addServer,
+ editServer,
+ connectServer,
+ disconnectServer,
+ removeServer,
+ subscribeServers,
+} from 'xo'
+
+const showInfo = () =>
+ alert(
+ _('serverAllowUnauthorizedCertificates'),
+ _('serverUnauthorizedCertificatesInfo')
+ )
+const showServerError = server => {
+ const { code, message } = server.error
+
+ if (code === 'DEPTH_ZERO_SELF_SIGNED_CERT') {
+ return confirm({
+ title: _('serverSelfSignedCertError'),
+ body: _('serverSelfSignedCertQuestion'),
+ }).then(
+ () =>
+ editServer(server, { allowUnauthorized: true }).then(() =>
+ connectServer(server)
+ ),
+ noop
+ )
+ }
+
+ if (code === 'SESSION_AUTHENTICATION_FAILED') {
+ return alert(_('serverAuthFailed'), message)
+ }
+
+ return alert(code || _('serverUnknownError'), message)
+}
+
+const COLUMNS = [
+ {
+ itemRenderer: (server, formatMessage) => (
+ editServer(server, { label })}
+ placeholder={formatMessage(messages.serverPlaceHolderLabel)}
+ />
+ ),
+ default: true,
+ name: _('serverLabel'),
+ sortCriteria: _ => _.label,
+ },
+ {
+ itemRenderer: (server, formatMessage) => (
+ editServer(server, { host })}
+ placeholder={formatMessage(messages.serverPlaceHolderAddress)}
+ />
+ ),
+ name: _('serverHost'),
+ sortCriteria: _ => _.host,
+ },
+ {
+ itemRenderer: (server, formatMessage) => (
+ editServer(server, { username })}
+ placeholder={formatMessage(messages.serverPlaceHolderUser)}
+ />
+ ),
+ name: _('serverUsername'),
+ sortCriteria: _ => _.username,
+ },
+ {
+ itemRenderer: (server, formatMessage) => (
+ editServer(server, { password })}
+ placeholder={formatMessage(messages.serverPlaceHolderPassword)}
+ />
+ ),
+ name: _('serverPassword'),
+ },
+ {
+ itemRenderer: server => (
+
+ ),
+ name: _('serverStatus'),
+ sortCriteria: _ => _.status,
+ },
+ {
+ itemRenderer: server => (
+ editServer(server, { readOnly })}
+ value={!!server.readOnly}
+ />
+ ),
+ name: _('serverReadOnly'),
+ sortCriteria: _ => !!_.readOnly,
+ },
+ {
+ itemRenderer: server => (
+
+ editServer(server, { allowUnauthorized })
+ }
+ />
+ ),
+ name: (
+
+ {_('serverUnauthorizedCertificates')}{' '}
+
+
+
+
+
+
+ ),
+ sortCriteria: _ => !!_.allowUnauthorized,
+ },
+]
+const INDIVIDUAL_ACTIONS = [
+ {
+ handler: removeServer,
+ icon: 'delete',
+ label: _('remove'),
+ level: 'danger',
+ },
+]
+
+@addSubscriptions({
+ servers: subscribeServers,
+})
+@injectIntl
+export default class Servers extends Component {
+ _addServer = async () => {
+ const { label, host, password, username } = this.state
+
+ await addServer(host, username, password, label)
+
+ this.setState({ label: '', host: '', password: '', username: '' })
+ }
+
+ render () {
+ const { props: { intl: { formatMessage }, servers }, state } = this
+
+ return (
+
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/settings/users/index.js b/packages/xo-web/src/xo-app/settings/users/index.js
new file mode 100644
index 000000000..fc44ac959
--- /dev/null
+++ b/packages/xo-web/src/xo-app/settings/users/index.js
@@ -0,0 +1,151 @@
+import * as Editable from 'editable'
+import _, { messages } from 'intl'
+import ActionButton from 'action-button'
+import ActionRowButton from 'action-row-button'
+import Component from 'base-component'
+import isEmpty from 'lodash/isEmpty'
+import keyBy from 'lodash/keyBy'
+import map from 'lodash/map'
+import React from 'react'
+import SortedTable from 'sorted-table'
+import { addSubscriptions } from 'utils'
+import { injectIntl } from 'react-intl'
+import { Password, Select } from 'form'
+
+import { createUser, deleteUser, editUser, subscribeUsers } from 'xo'
+
+const permissions = {
+ none: {
+ label: _('userLabel'),
+ value: 'none',
+ },
+ admin: {
+ label: _('adminLabel'),
+ value: 'admin',
+ },
+}
+
+const USER_COLUMNS = [
+ {
+ name: _('userNameColumn'),
+ itemRenderer: user => (
+ editUser(user, { email })}
+ value={user.email}
+ />
+ ),
+ sortCriteria: user => user.email,
+ },
+ {
+ name: _('userPermissionColumn'),
+ itemRenderer: user => (
+
+ editUser(user, { permission: permission.value })
+ }
+ options={map(permissions)}
+ />
+ ),
+ sortCriteria: user => user.permission,
+ },
+ {
+ name: _('userPasswordColumn'),
+ itemRenderer: user => (
+ editUser(user, { password })}
+ value=''
+ />
+ ),
+ },
+ {
+ name: '',
+ itemRenderer: user => (
+
+ ),
+ },
+]
+
+@addSubscriptions({
+ users: cb => subscribeUsers(users => cb(keyBy(users, 'id'))),
+})
+@injectIntl
+export default class Users extends Component {
+ state = {
+ email: '',
+ password: '',
+ permission: permissions.none,
+ }
+
+ _create = () => {
+ const { email, password, permission } = this.state
+ return createUser(email, password, permission.value).then(() => {
+ this.setState({ email: '', password: '', permission: permissions.none })
+ })
+ }
+
+ render () {
+ const { users, intl } = this.props
+ const { email, password, permission } = this.state
+
+ return (
+
+
+
+ {isEmpty(users) ? (
+
+ {_('noUserFound')}
+
+ ) : (
+
+ )}
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/sr/action-bar.js b/packages/xo-web/src/xo-app/sr/action-bar.js
new file mode 100644
index 000000000..d5f8e5f4b
--- /dev/null
+++ b/packages/xo-web/src/xo-app/sr/action-bar.js
@@ -0,0 +1,27 @@
+import _ from 'intl'
+import ActionBar, { Action } from 'action-bar'
+import React from 'react'
+import {
+ forgetSr,
+ rescanSr,
+ reconnectAllHostsSr,
+ disconnectAllHostsSr,
+} from 'xo'
+
+const SrActionBar = ({ sr }) => (
+
+
+
+
+
+
+)
+export default SrActionBar
diff --git a/packages/xo-web/src/xo-app/sr/add-subvolume-modal.js b/packages/xo-web/src/xo-app/sr/add-subvolume-modal.js
new file mode 100644
index 000000000..577c8248a
--- /dev/null
+++ b/packages/xo-web/src/xo-app/sr/add-subvolume-modal.js
@@ -0,0 +1,56 @@
+import _ from 'intl'
+import Component from 'base-component'
+import React from 'react'
+import { SelectSr } from 'select-objects'
+import { SizeInput } from 'form'
+import { Container, Row, Col } from 'grid'
+import { createSelector } from 'selectors'
+import { map, min } from 'lodash'
+
+export default class AddSubvolumeModalBody extends Component {
+ get value () {
+ return this.state
+ }
+
+ _getSrPredicate = createSelector(
+ () => this.props.sr.$pool,
+ poolId => sr => sr.SR_type === 'lvm' && sr.$pool === poolId
+ )
+
+ _selectSrs = srs => {
+ this.setState({
+ srs,
+ brickSize: min(map(srs, sr => sr.size - sr.physical_usage)),
+ })
+ }
+
+ render () {
+ return (
+
+
+
+ {_('xosanSelectNSrs', { nSrs: this.props.subvolumeSize })}
+
+
+
+
+
+
+ {_('xosanSize')}
+
+
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/sr/index.js b/packages/xo-web/src/xo-app/sr/index.js
new file mode 100644
index 000000000..dc468c1f3
--- /dev/null
+++ b/packages/xo-web/src/xo-app/sr/index.js
@@ -0,0 +1,191 @@
+import _ from 'intl'
+import Component from 'base-component'
+import Icon from 'icon'
+import Link from 'link'
+import Page from '../page'
+import PropTypes from 'prop-types'
+import React, { cloneElement } from 'react'
+import SrActionBar from './action-bar'
+import { Container, Row, Col } from 'grid'
+import { editSr } from 'xo'
+import { NavLink, NavTabs } from 'nav'
+import { Text } from 'editable'
+import { assign, map, pick } from 'lodash'
+import { connectStore, routes } from 'utils'
+import {
+ createGetObject,
+ createGetObjectMessages,
+ createGetObjectsOfType,
+ createSelector,
+} from 'selectors'
+
+import TabAdvanced from './tab-advanced'
+import TabGeneral from './tab-general'
+import TabLogs from './tab-logs'
+import TabHosts from './tab-host'
+import TabDisks from './tab-disks'
+import TabXosan from './tab-xosan'
+
+// ===================================================================
+
+@routes('general', {
+ advanced: TabAdvanced,
+ general: TabGeneral,
+ logs: TabLogs,
+ hosts: TabHosts,
+ disks: TabDisks,
+ xosan: TabXosan,
+})
+@connectStore(() => {
+ const getSr = createGetObject()
+
+ const getContainer = createGetObject(
+ (state, props) => getSr(state, props).$container
+ )
+
+ const getPbds = createGetObjectsOfType('PBD').pick(
+ createSelector(getSr, sr => sr.$PBDs)
+ )
+
+ const getSrHosts = createGetObjectsOfType('host').pick(
+ createSelector(getPbds, pbds => map(pbds, pbd => pbd.host))
+ )
+
+ // -----------------------------------------------------------------
+
+ const getLogs = createGetObjectMessages(getSr)
+
+ // -----------------------------------------------------------------
+
+ const getVdiIds = (state, props) => getSr(state, props).VDIs
+
+ const getVdis = createGetObjectsOfType('VDI')
+ .pick(getVdiIds)
+ .sort()
+ const getVdiSnapshots = createGetObjectsOfType('VDI-snapshot')
+ .pick(getVdiIds)
+ .sort()
+ const getUnmanagedVdis = createGetObjectsOfType('VDI-unmanaged')
+ .pick(createSelector(getSr, sr => sr.VDIs))
+ .sort()
+
+ // -----------------------------------------------------------------
+
+ return (state, props) => {
+ const sr = getSr(state, props)
+ if (!sr) {
+ return {}
+ }
+
+ return {
+ container: getContainer(state, props),
+ hosts: getSrHosts(state, props),
+ pbds: getPbds(state, props),
+ logs: getLogs(state, props),
+ vdis: getVdis(state, props),
+ unmanagedVdis: getUnmanagedVdis(state, props),
+ vdiSnapshots: getVdiSnapshots(state, props),
+ sr,
+ }
+ }
+})
+export default class Sr extends Component {
+ static contextTypes = {
+ router: PropTypes.object,
+ }
+
+ componentWillReceiveProps (props) {
+ if (this.props.sr && !props.sr) {
+ this.context.router.push('/')
+ }
+ }
+
+ header () {
+ const { sr, container } = this.props
+ if (!sr) {
+ return
+ }
+ return (
+
+
+
+
+ {' '}
+ editSr(sr, { nameLabel })}
+ />
+
+
+ editSr(sr, { nameDescription })}
+ />
+ {container && (
+
+ {' - '}
+
+ {container.name_label}
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+ {_('generalTabName')}
+
+
+ {_('disksTabName', { disks: sr.VDIs.length })}
+
+ {sr.SR_type === 'xosan' && (
+ XOSAN
+ )}
+ {_('hostsTabName')}
+ {_('logsTabName')}
+
+ {_('advancedTabName')}
+
+
+
+
+
+ )
+ }
+
+ render () {
+ const { container, sr } = this.props
+ if (!sr) {
+ return {_('statusLoading')}
+ }
+ const childProps = assign(
+ pick(this.props, [
+ 'hosts',
+ 'logs',
+ 'pbds',
+ 'sr',
+ 'vdis',
+ 'unmanagedVdis',
+ 'vdiSnapshots',
+ ])
+ )
+ return (
+
+ {cloneElement(this.props.children, childProps)}
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/sr/replace-brick-modal.js b/packages/xo-web/src/xo-app/sr/replace-brick-modal.js
new file mode 100644
index 000000000..31d140751
--- /dev/null
+++ b/packages/xo-web/src/xo-app/sr/replace-brick-modal.js
@@ -0,0 +1,83 @@
+import _ from 'intl'
+import Component from 'base-component'
+import React from 'react'
+import { SelectSr } from 'select-objects'
+import { Toggle, SizeInput } from 'form'
+import { Container, Row, Col } from 'grid'
+import { createSelector } from 'selectors'
+
+export default class ReplaceBrickModalBody extends Component {
+ get value () {
+ return this.state
+ }
+
+ _getSrPredicate = createSelector(
+ () => this.props.vm,
+ () => this.state.onSameVm,
+ (vm, onSameVm) => {
+ if (vm === undefined) {
+ return sr => sr.SR_type === 'lvm'
+ }
+
+ return onSameVm
+ ? sr => sr.$container === vm.$container && sr.SR_type === 'lvm'
+ : sr => sr.$pool === vm.$pool && sr.SR_type === 'lvm'
+ }
+ )
+
+ _toggleOnSameVm = () =>
+ this.setState({
+ onSameVm: !this.state.onSameVm,
+ sr: undefined,
+ })
+
+ _selectSr = sr => {
+ this.setState({
+ sr,
+ brickSize: sr.size - sr.physical_usage,
+ })
+ }
+
+ render () {
+ return (
+
+ {this.props.vm !== undefined && (
+
+
+ {_('xosanOnSameVm')}
+
+
+
+
+
+ )}
+
+
+ {_('xosanUnderlyingStorage')}
+
+
+
+
+
+
+
+ {_('xosanBrickSize')}
+
+
+
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/sr/tab-advanced.js b/packages/xo-web/src/xo-app/sr/tab-advanced.js
new file mode 100644
index 000000000..2cd08ff2e
--- /dev/null
+++ b/packages/xo-web/src/xo-app/sr/tab-advanced.js
@@ -0,0 +1,84 @@
+import _ from 'intl'
+import Copiable from 'copiable'
+import React from 'react'
+import SortedTable from 'sorted-table'
+import TabButton from 'tab-button'
+import { addSubscriptions, connectStore, formatSize } from 'utils'
+import { Container, Row, Col } from 'grid'
+import { createGetObjectsOfType } from 'selectors'
+import { createSelector } from 'reselect'
+import { createSrUnhealthyVdiChainsLengthSubscription, deleteSr } from 'xo'
+import { flowRight, isEmpty, keys, sum, values } from 'lodash'
+
+// ===================================================================
+
+const COLUMNS = [
+ {
+ name: _('srUnhealthyVdiNameLabel'),
+ itemRenderer: vdi => {vdi.name_label} ,
+ sortCriteria: vdi => vdi.name_label,
+ },
+ {
+ name: _('srUnhealthyVdiSize'),
+ itemRenderer: vdi => formatSize(vdi.size),
+ sortCriteria: vdi => vdi.size,
+ },
+ {
+ name: _('srUnhealthyVdiDepth'),
+ itemRenderer: (vdi, chains) => chains[vdi.uuid],
+ sortCriteria: (vdi, chains) => chains[vdi.uuid],
+ },
+]
+
+const UnhealthyVdiChains = flowRight(
+ addSubscriptions(props => ({
+ chains: createSrUnhealthyVdiChainsLengthSubscription(props.sr),
+ })),
+ connectStore(() => ({
+ vdis: createGetObjectsOfType('VDI').pick(
+ createSelector((_, props) => props.chains, keys)
+ ),
+ }))
+)(
+ ({ chains, vdis }) =>
+ isEmpty(vdis) ? null : (
+
+
{_('srUnhealthyVdiTitle', { total: sum(values(chains)) })}
+
+
+ )
+)
+
+export default ({ sr }) => (
+
+
+
+
+
+
+
+
+ {_('xenSettingsLabel')}
+
+
+
+ {_('uuid')}
+ {sr.uuid}
+
+
+
+
+
+
+
+
+
+
+
+)
diff --git a/packages/xo-web/src/xo-app/sr/tab-disks.js b/packages/xo-web/src/xo-app/sr/tab-disks.js
new file mode 100644
index 000000000..674d22eca
--- /dev/null
+++ b/packages/xo-web/src/xo-app/sr/tab-disks.js
@@ -0,0 +1,154 @@
+import _ from 'intl'
+import Component from 'base-component'
+import Icon from 'icon'
+import Link from 'link'
+import React from 'react'
+import renderXoItem, { renderXoUnknownItem } from 'render-xo-item'
+import SortedTable from 'sorted-table'
+import { concat, isEmpty } from 'lodash'
+import { connectStore, formatSize } from 'utils'
+import { Container, Row, Col } from 'grid'
+import { createGetObject, createSelector } from 'selectors'
+import { deleteVdi, deleteVdis, editVdi } from 'xo'
+import { Text } from 'editable'
+
+// ===================================================================
+
+const COLUMNS = [
+ {
+ name: _('vdiNameLabel'),
+ itemRenderer: vdi => (
+
+ editVdi(vdi, { name_label: value })}
+ />{' '}
+ {vdi.type === 'VDI-snapshot' && (
+
+
+
+ )}
+
+ ),
+ sortCriteria: vdi => vdi.name_label,
+ },
+ {
+ name: _('vdiNameDescription'),
+ itemRenderer: vdi => (
+ editVdi(vdi, { name_description: value })}
+ />
+ ),
+ },
+ {
+ name: _('vdiVm'),
+ component: connectStore(() => {
+ const getObject = createGetObject((_, id) => id)
+
+ return {
+ vm: (state, { item: { $VBDs: [vbdId] } }) => {
+ if (vbdId === undefined) {
+ return null
+ }
+
+ const vbd = getObject(state, vbdId)
+ if (vbd != null) {
+ return getObject(state, vbd.VM)
+ }
+ },
+ }
+ })(({ vm }) => {
+ if (vm === null) {
+ return null // no attached VM
+ }
+
+ if (vm === undefined) {
+ return renderXoUnknownItem()
+ }
+
+ let link
+ const { type } = vm
+ if (type === 'VM') {
+ link = `/vms/${vm.id}`
+ } else if (type === 'VM-snapshot') {
+ const id = vm.$snapshot_of
+ link = id !== undefined ? `/vms/${id}/snapshots` : '/dashboard/health'
+ }
+
+ const item = renderXoItem(vm)
+ return link === undefined ? item : {item}
+ }),
+ },
+ {
+ name: _('vdiTags'),
+ itemRenderer: vdi => vdi.tags,
+ },
+ {
+ name: _('vdiSize'),
+ itemRenderer: vdi => formatSize(vdi.size),
+ sortCriteria: vdi => vdi.size,
+ },
+]
+
+const GROUPED_ACTIONS = [
+ {
+ handler: deleteVdis,
+ icon: 'delete',
+ label: _('deleteSelectedVdis'),
+ },
+]
+
+const INDIVIDUAL_ACTIONS = [
+ {
+ handler: deleteVdi,
+ icon: 'delete',
+ label: _('deleteSelectedVdi'),
+ level: 'danger',
+ },
+]
+
+const FILTERS = {
+ filterOnlyManaged: 'type:!VDI-unmanaged',
+ filterOnlyRegular: '!type:|(VDI-snapshot VDI-unmanaged)',
+ filterOnlySnapshots: 'type:VDI-snapshot',
+ filterOnlyOrphaned: 'type:!VDI-unmanaged $VBDs:!""',
+ filterOnlyUnmanaged: 'type:VDI-unmanaged',
+}
+
+// ===================================================================
+
+export default class SrDisks extends Component {
+ _getAllVdis = createSelector(
+ () => this.props.vdis,
+ () => this.props.vdiSnapshots,
+ () => this.props.unmanagedVdis,
+ concat
+ )
+
+ render () {
+ const vdis = this._getAllVdis()
+ return (
+
+
+
+ {!isEmpty(vdis) ? (
+
+ ) : (
+ {_('srNoVdis')}
+ )}
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/sr/tab-general.js b/packages/xo-web/src/xo-app/sr/tab-general.js
new file mode 100644
index 000000000..99c8da2d1
--- /dev/null
+++ b/packages/xo-web/src/xo-app/sr/tab-general.js
@@ -0,0 +1,115 @@
+import _ from 'intl'
+import Component from 'base-component'
+import HomeTags from 'home-tags'
+import Icon from 'icon'
+import map from 'lodash/map'
+import React from 'react'
+import Usage, { UsageElement } from 'usage'
+import { addTag, removeTag, getLicense } from 'xo'
+import { connectStore, formatSize } from 'utils'
+import { Container, Row, Col } from 'grid'
+import { createGetObject } from 'selectors'
+import { renderXoItemFromId } from 'render-xo-item'
+
+const UsageTooltip = connectStore(() => ({
+ vbd: createGetObject((_, { vdi }) => vdi.$VBDs[0]),
+}))(({ vbd, vdi }) => (
+
+ {vdi.name_label} − {formatSize(vdi.usage)}
+ {vbd != null && }
+ {vbd != null && renderXoItemFromId(vbd.VM)}
+
+))
+
+export default class TabGeneral extends Component {
+ componentDidMount () {
+ const { sr } = this.props
+
+ if (sr.SR_type === 'xosan') {
+ getLicense('xosan.trial', sr.id).then(() =>
+ this.setState({ licenseRestriction: true })
+ )
+ }
+ }
+
+ render () {
+ const { sr, vdis, vdiSnapshots, unmanagedVdis } = this.props
+
+ return (
+
+
+
+
+ {sr.VDIs.length}x
+
+
+
+
+ {formatSize(sr.size)}
+
+ Type: {sr.SR_type}
+ {this.state.licenseRestriction && (
+ {_('xosanLicenseRestricted')}
+ )}
+
+
+
+ {sr.$PBDs.length}x
+
+
+
+
+
+
+ {formatSize(sr.physical_usage)} {_('srUsed')} ({formatSize(
+ sr.size - sr.physical_usage
+ )}{' '}
+ {_('srFree')})
+
+
+
+
+
+
+ {map(unmanagedVdis, vdi => (
+ }
+ value={vdi.usage}
+ />
+ ))}
+ {map(vdis, vdi => (
+ }
+ value={vdi.usage}
+ />
+ ))}
+ {map(vdiSnapshots, vdi => (
+ }
+ value={vdi.usage}
+ />
+ ))}
+
+
+
+
+
+
+ removeTag(sr.id, tag)}
+ onAdd={tag => addTag(sr.id, tag)}
+ />
+
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/sr/tab-host.js b/packages/xo-web/src/xo-app/sr/tab-host.js
new file mode 100644
index 000000000..f065d7ea4
--- /dev/null
+++ b/packages/xo-web/src/xo-app/sr/tab-host.js
@@ -0,0 +1,89 @@
+import _ from 'intl'
+import ActionRowButton from 'action-row-button'
+import isEmpty from 'lodash/isEmpty'
+import Link from 'link'
+import React from 'react'
+import SortedTable from 'sorted-table'
+import StateButton from 'state-button'
+import { Container, Row, Col } from 'grid'
+import { editHost, connectPbd, disconnectPbd, deletePbd } from 'xo'
+import { Text } from 'editable'
+
+const HOST_COLUMNS = [
+ {
+ name: _('hostNameLabel'),
+ itemRenderer: (pbd, hosts) => {
+ const host = hosts[pbd.host]
+ return (
+
+ editHost(host, { name_label: value })}
+ useLongClick
+ />
+
+ )
+ },
+ sortCriteria: (pbd, hosts) => hosts[pbd.host].name_label,
+ },
+ {
+ name: _('hostDescription'),
+ itemRenderer: (pbd, hosts) => {
+ const host = hosts[pbd.host]
+ return (
+ editHost(host, { name_description: value })}
+ />
+ )
+ },
+ sortCriteria: (pbd, hosts) => hosts[pbd.host].name_description,
+ },
+ {
+ name: _('pbdStatus'),
+ itemRenderer: pbd => (
+
+ ),
+ sortCriteria: 'attached',
+ },
+ {
+ name: _('pbdAction'),
+ itemRenderer: pbd =>
+ !pbd.attached && (
+
+ ),
+ textAlign: 'right',
+ },
+]
+
+export default ({ hosts, pbds }) => (
+
+
+
+ {!isEmpty(hosts) ? (
+
+ ) : (
+ {_('noHost')}
+ )}
+
+
+
+)
diff --git a/packages/xo-web/src/xo-app/sr/tab-logs.js b/packages/xo-web/src/xo-app/sr/tab-logs.js
new file mode 100644
index 000000000..608199328
--- /dev/null
+++ b/packages/xo-web/src/xo-app/sr/tab-logs.js
@@ -0,0 +1,104 @@
+import _ from 'intl'
+import ActionRow from 'action-row-button'
+import Button from 'button'
+import isEmpty from 'lodash/isEmpty'
+import map from 'lodash/map'
+import React, { Component } from 'react'
+import TabButton from 'tab-button'
+import { deleteMessage } from 'xo'
+import { createPager } from 'selectors'
+import { FormattedRelative, FormattedTime } from 'react-intl'
+import { Container, Row, Col } from 'grid'
+
+export default class TabLogs extends Component {
+ constructor () {
+ super()
+
+ this.getLogs = createPager(() => this.props.logs, () => this.state.page, 10)
+
+ this.state = {
+ page: 1,
+ }
+ }
+
+ _deleteAllLogs = () => map(this.props.logs, deleteMessage)
+ _nextPage = () => this.setState({ page: this.state.page + 1 })
+ _previousPage = () => this.setState({ page: this.state.page - 1 })
+
+ render () {
+ const logs = this.getLogs()
+
+ return (
+
+ {isEmpty(logs) ? (
+
+
+
+ {_('noLogs')}
+
+
+ ) : (
+
+
+
+
+ <
+
+
+ >
+
+
+
+
+
+
+
+
+
+ {_('logDate')}
+ {_('logName')}
+ {_('logContent')}
+ {_('logAction')}
+
+
+
+ {map(logs, log => (
+
+
+ {' '}
+ ( )
+
+ {log.name}
+ {log.body}
+
+
+
+
+ ))}
+
+
+
+
+
+ )}
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/sr/tab-xosan.js b/packages/xo-web/src/xo-app/sr/tab-xosan.js
new file mode 100644
index 000000000..6b50bc15f
--- /dev/null
+++ b/packages/xo-web/src/xo-app/sr/tab-xosan.js
@@ -0,0 +1,849 @@
+import _ from 'intl'
+import ActionButton from 'action-button'
+import Collapse from 'collapse'
+import Copiable from 'copiable'
+import Component from 'base-component'
+import Icon from 'icon'
+import Link from 'link'
+import React from 'react'
+import Tooltip from 'tooltip'
+import SingleLineRow from 'single-line-row'
+import { confirm } from 'modal'
+import { error } from 'notification'
+import { Toggle } from 'form'
+import { Container, Col, Row } from 'grid'
+import { find, forEach, isEmpty, map, reduce, sum } from 'lodash'
+import { createGetObjectsOfType, createSelector, isAdmin } from 'selectors'
+import { addSubscriptions, connectStore, formatSize } from 'utils'
+import {
+ addXosanBricks,
+ getLicense,
+ fixHostNotInXosanNetwork,
+ // TODO: uncomment when implementing subvolume deletion
+ // removeXosanBricks,
+ replaceXosanBrick,
+ startVm,
+ subscribePlugins,
+ subscribeVolumeInfo,
+} from 'xo'
+
+import { INFO_TYPES } from '../xosan'
+
+import ReplaceBrickModalBody from './replace-brick-modal'
+import AddSubvolumeModalBody from './add-subvolume-modal'
+
+const ISSUE_CODE_TO_MESSAGE = {
+ VMS_DOWN: 'xosanVmsNotRunning',
+ VMS_NOT_FOUND: 'xosanVmsNotFound',
+ FILES_NEED_HEALING: 'xosanFilesNeedHealing',
+ HOST_NOT_IN_NETWORK: 'xosanHostNotInNetwork',
+}
+
+const BORDERS = {
+ border: 'solid 2px #ccc',
+ borderRadius: '5px',
+ borderTop: 'none',
+}
+
+const Issues = ({ issues }) => (
+
+ {map(issues, issue => (
+
+
+ {' '}
+ {_(ISSUE_CODE_TO_MESSAGE[issue.code], issue.params)}
+ {issue.fix && (
+
+
+ {_('xosanFixIssue')}
+
+
+ )}
+
+
+ ))}
+
+)
+
+const Field = ({ title, children }) => (
+
+
+ {title}
+
+ {children}
+
+)
+
+@connectStore({
+ srs: createGetObjectsOfType('SR'),
+ vms: createGetObjectsOfType('VM'),
+})
+class Node extends Component {
+ _replaceBrick = async ({ brick, vm }) => {
+ const { sr, brickSize, onSameVm = false } = await confirm({
+ icon: 'refresh',
+ title: _('xosanReplace'),
+ body: ,
+ })
+
+ if (sr == null || brickSize == null) {
+ return error(
+ _('xosanReplaceBrickErrorTitle'),
+ _('xosanReplaceBrickErrorMessage')
+ )
+ }
+
+ await replaceXosanBrick(this.props.sr, brick, sr, brickSize, onSameVm)
+ }
+
+ _getSizeUsage = createSelector(
+ () => this.props.node.statusDetail,
+ statusDetail => ({
+ used: String(
+ Math.round(100 - +statusDetail.sizeFree / +statusDetail.sizeTotal * 100)
+ ),
+ free: formatSize(+statusDetail.sizeFree),
+ })
+ )
+
+ _getInodesUsage = createSelector(
+ () => this.props.node.statusDetail,
+ statusDetail => ({
+ used: String(
+ Math.round(
+ 100 - +statusDetail.inodesFree / +statusDetail.inodesTotal * 100
+ )
+ ),
+ free: formatSize(+statusDetail.inodesFree),
+ })
+ )
+
+ render () {
+ const { srs } = this.props
+ const { showAdvanced } = this.state
+
+ const {
+ config,
+ heal,
+ size,
+ status,
+ statusDetail,
+ uuid,
+ vm,
+ } = this.props.node
+
+ return (
+
+ {' '}
+ {srs[config.underlyingSr].name_label}
+
+ }
+ className='mb-1'
+ >
+
+
+
+ {vm !== undefined ? (
+
+
+
+ {' '}
+ {vm.name_label}
+ {vm.power_state !== 'Running' && (
+
+
+
+ )}
+
+ ) : (
+
+ {_('xosanCouldNotFindVm')}
+
+ )}
+
+
+
+ {srs[config.underlyingSr].name_label}
+
+ {' - '}
+ {size != null &&
+ _('xosanUnderlyingStorageUsage', { usage: formatSize(size) })}
+
+
+ {heal ? heal.status : 'unknown'}
+
+ {statusDetail && (
+
+
+
+
+
+
+
+ )}
+ {config.arbiter === 'True' && }
+
+
+
+ {_('xosanReplace')}
+
+
+
+
+
+
+ {' '}
+ {_('xosanAdvanced')}
+
+
+
+ {showAdvanced && [
+
+ {config.brickName}
+ ,
+
+ {uuid}
+ ,
+
+ {statusDetail && [
+
+
+
+
+
+
+ ,
+
+ {statusDetail.blockSize}
+ ,
+
+ {statusDetail.device}
+ ,
+
+ {statusDetail.fsName}
+ ,
+
+ {statusDetail.mntOptions}
+ ,
+
+ {statusDetail.path}
+ ,
+ ]}
+
,
+
+ {status &&
+ status.length !== 0 && (
+
+
+
+
+ {_('xosanJob')}
+ {_('xosanPath')}
+ {_('xosanStatus')}
+ {_('xosanPid')}
+ {_('xosanPort')}
+
+
+ {map(status, job => (
+
+ {job.hostname}
+ {job.path}
+ {job.status}
+ {job.pid}
+ {job.port}
+
+ ))}
+
+
+
+
+ )}
+
,
+
+ {heal &&
+ heal.file &&
+ heal.file.length !== 0 && (
+
+
{_('xosanFilesNeedingHealing')}
+ {map(heal.file, file => (
+
+ {file._}
+ {file.gfid}
+
+ ))}
+
+ )}
+
,
+ ]}
+
+
+
+ )
+ }
+}
+
+// -----------------------------------------------------------------------------
+
+@connectStore(() => ({
+ isAdmin,
+ vms: createGetObjectsOfType('VM'),
+ hosts: createGetObjectsOfType('host'),
+ vbds: createGetObjectsOfType('VBD'),
+ vdis: createGetObjectsOfType('VDI'),
+}))
+@addSubscriptions(({ sr }) => {
+ const subscriptions = {}
+ forEach(INFO_TYPES, infoType => {
+ subscriptions[`${infoType}_`] = cb =>
+ subscribeVolumeInfo({ sr, infoType }, cb)
+ })
+
+ subscriptions.plugins = subscribePlugins
+
+ return subscriptions
+})
+export default class TabXosan extends Component {
+ componentDidMount () {
+ const { id } = this.props.sr
+
+ getLicense('xosan', id)
+ .catch(() => getLicense('xosan.trial', id))
+ .then(
+ license => this.setState({ license }),
+ error => this.setState({ licenseError: error })
+ )
+ }
+
+ _addSubvolume = async () => {
+ const { srs, brickSize } = await confirm({
+ icon: 'add',
+ title: _('xosanAddSubvolume'),
+ body: (
+
+ ),
+ })
+
+ if (brickSize == null || (srs && srs.length) !== this._getSubvolumeSize()) {
+ return error(
+ _('xosanAddSubvolumeErrorTitle'),
+ _('xosanAddSubvolumeErrorMessage', { nSrs: this._getSubvolumeSize() })
+ )
+ }
+
+ return this._addBricks({ srs, brickSize })
+ }
+
+ // TODO: uncomment when implementing subvolume deletion
+ // async _removeSubVolume (bricks) {
+ // await removeXosanBricks(this.props.sr.id, bricks)
+ // }
+
+ async _addBricks ({ srs, brickSize }) {
+ await addXosanBricks(this.props.sr.id, srs.map(sr => sr.id), brickSize)
+ }
+
+ _getStrippedVolumeInfo = createSelector(
+ () => this.props.info_,
+ info => (info && info.commandStatus ? info.result : null)
+ )
+
+ _getSubvolumeSize = createSelector(
+ this._getStrippedVolumeInfo,
+ strippedVolumeInfo =>
+ strippedVolumeInfo
+ ? +strippedVolumeInfo.disperseCount || +strippedVolumeInfo.replicaCount
+ : null
+ )
+
+ // TODO: uncomment when implementing subvolume deletion
+ // _getSubvolumes = createSelector(
+ // this._getStrippedVolumeInfo,
+ // this._getSubvolumeSize,
+ // (strippedVolumeInfo, subvolumeSize) => {
+ // const subVolumes = []
+ // if (strippedVolumeInfo) {
+ // for (let i = 0; i < strippedVolumeInfo.bricks.length; i += subvolumeSize) {
+ // subVolumes.push(strippedVolumeInfo.bricks.slice(i, i + subvolumeSize))
+ // }
+ // }
+ //
+ // return subVolumes
+ // }
+ // )
+
+ _getMissingXoaPlugin = createSelector(
+ () => this.props.plugins,
+ plugins => {
+ if (plugins === undefined) {
+ return _('xosanInstallXoaPlugin')
+ }
+
+ const xoaPlugin = find(plugins, { id: 'xoa' })
+ if (xoaPlugin === undefined) {
+ return _('xosanInstallXoaPlugin')
+ }
+
+ if (!xoaPlugin.loaded) {
+ return _('xosanLoadXoaPlugin')
+ }
+ }
+ )
+
+ _getConfig = createSelector(
+ () => this.props.sr && this.props.sr.other_config['xo:xosan_config'],
+ otherConfig => (otherConfig ? JSON.parse(otherConfig) : null)
+ )
+
+ _getBrickByName = createSelector(
+ this._getConfig,
+ () => this.props.vms,
+ () => this.props.vdis,
+ () => this.props.vbds,
+ () => this.props.heal_,
+ () => this.props.status_,
+ () => this.props.statusDetail_,
+ this._getStrippedVolumeInfo,
+ (
+ xosanConfig,
+ vms,
+ vdis,
+ vbds,
+ heal,
+ status,
+ statusDetail,
+ strippedVolumeInfo
+ ) => {
+ const nodes = xosanConfig && xosanConfig.nodes
+
+ const brickByName = {}
+ forEach(nodes, node => {
+ const vm = vms[node.vm.id]
+
+ brickByName[node.brickName] = {
+ config: node,
+ uuid: '-',
+ size: isEmpty(vm && vm.$VBDs)
+ ? null
+ : sum(
+ map(vm.$VBDs, vbdId => {
+ const vdi = vdis[vbds[vbdId].VDI]
+ return vdi === undefined ? 0 : vdi.size
+ })
+ ),
+ vm,
+ }
+ })
+
+ const brickByUuid = {}
+ if (strippedVolumeInfo) {
+ forEach(strippedVolumeInfo.bricks, brick => {
+ brickByName[brick.name] = brickByName[brick.name] || {}
+ brickByName[brick.name].info = brick
+ brickByName[brick.name].uuid = brick.hostUuid
+ brickByUuid[brick.hostUuid] =
+ brickByUuid[brick.hostUuid] || brickByName[brick.name]
+ })
+ }
+
+ if (heal && heal.commandStatus) {
+ forEach(heal.result.bricks, brick => {
+ brickByName[brick.name] = brickByName[brick.name] || {}
+ brickByName[brick.name].heal = brick
+ brickByName[brick.name].uuid = brick.hostUuid
+ brickByUuid[brick.hostUuid] =
+ brickByUuid[brick.hostUuid] || brickByName[brick.name]
+ })
+ }
+
+ if (status && status.commandStatus) {
+ forEach(brickByUuid, (brick, uuid) => {
+ brick.status = status.result.nodes[uuid]
+ })
+ }
+
+ if (statusDetail && statusDetail.commandStatus) {
+ forEach(brickByUuid, (brick, uuid) => {
+ if (uuid in statusDetail.result.nodes) {
+ brick.statusDetail = statusDetail.result.nodes[uuid][0]
+ }
+ })
+ }
+
+ return brickByName
+ }
+ )
+
+ _getOrderedBrickList = createSelector(
+ this._getConfig,
+ this._getBrickByName,
+ (xosanConfig, brickByName) => {
+ if (!xosanConfig || !xosanConfig.nodes) {
+ return
+ }
+
+ return map(xosanConfig.nodes, node => brickByName[node.brickName])
+ }
+ )
+
+ _getIssues = createSelector(
+ this._getOrderedBrickList,
+ () => this.props.hosts_,
+ () => this.props.hosts,
+ () => this.props.sr,
+ (orderedBrickList, hosts_, hosts, sr) => {
+ if (orderedBrickList == null) {
+ return
+ }
+
+ const issues = []
+ if (
+ reduce(
+ orderedBrickList,
+ (hasStopped, node) =>
+ hasStopped || (node.vm && node.vm.power_state !== 'Running'),
+ false
+ )
+ ) {
+ issues.push({ code: 'VMS_DOWN' })
+ }
+
+ if (
+ reduce(
+ orderedBrickList,
+ (hasNotFound, node) => hasNotFound || node.vm === undefined,
+ false
+ )
+ ) {
+ issues.push({ code: 'VMS_NOT_FOUND' })
+ }
+
+ if (
+ reduce(
+ orderedBrickList,
+ (hasFileToHeal, node) =>
+ hasFileToHeal ||
+ (node.heal && node.heal.file && node.heal.file.length !== 0),
+ false
+ )
+ ) {
+ issues.push({ code: 'FILES_NEED_HEALING' })
+ }
+
+ forEach(hosts_, ({ host }) => {
+ issues.push({
+ code: 'HOST_NOT_IN_NETWORK',
+ key: 'HOST_NOT_IN_NETWORK' + host,
+ params: { hostName: hosts[host].name_label },
+ fix: {
+ action: () => fixHostNotInXosanNetwork(sr.id, host),
+ title: _('xosanIssueHostNotInNetwork'),
+ },
+ })
+ })
+
+ return issues
+ }
+ )
+
+ render () {
+ const { license, licenseError, showAdvanced } = this.state
+ const {
+ heal_,
+ info_,
+ sr,
+ status_,
+ statusDetail_,
+ vbds,
+ vdis,
+ isAdmin,
+ } = this.props
+
+ const missingXoaPlugin = this._getMissingXoaPlugin()
+ if (missingXoaPlugin !== undefined) {
+ return {missingXoaPlugin}
+ }
+
+ const xosanConfig = this._getConfig()
+ if (
+ (license === undefined && licenseError === undefined) ||
+ xosanConfig === undefined
+ ) {
+ return {_('statusLoading')}
+ }
+
+ if (
+ licenseError !== undefined &&
+ licenseError.message !== 'No license found'
+ ) {
+ return {_('xosanCheckLicenseError')}
+ }
+
+ if (
+ licenseError !== undefined ||
+ (license !== undefined &&
+ license.productId !== 'xosan' &&
+ license.productId !== 'xosan.trial')
+ ) {
+ return (
+
+ {_('xosanAdminNoLicenseDisclaimer')}{' '}
+ {isAdmin && {_('licensesManage')}}
+
+ )
+ }
+
+ if (license.expires < Date.now()) {
+ return (
+
+ {_('xosanAdminExpiredLicenseDisclaimer')}{' '}
+ {isAdmin && {_('licensesManage')}}
+
+ )
+ }
+
+ if (!xosanConfig.version) {
+ return {_('xosanWarning')}
+ }
+
+ const strippedVolumeInfo = this._getStrippedVolumeInfo()
+ // const subVolumes = this._getSubvolumes() // TODO: uncomment when implementing subvolume deletion
+ const orderedBrickList = this._getOrderedBrickList()
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {map(orderedBrickList, node => (
+
+
+
+
+
+ ))}
+
+
+
+ {_('xosanAddSubvolume')}
+
+
+
+
+ {/* We will implement this later */}
+ {/*
+
+ {_('xosanRemoveSubvolumes')}
+
+ {map(subVolumes, (subvolume, i) =>
+
+ {map(subvolume, (brick, j) => {brick.name} )}
+
+
+ brick.name)}
+ >
+ {_('xosanRemove')}
+
+
+ )}
+
+
+
+
*/}
+
+
+
+ {' '}
+ {_('xosanAdvanced')}
+
+ {strippedVolumeInfo &&
+ showAdvanced && (
+
+
{_('xosanVolume')}
+
+ {strippedVolumeInfo.name}
+ {strippedVolumeInfo.statusStr}
+ {strippedVolumeInfo.typeStr}
+
+ {strippedVolumeInfo.brickCount}
+
+
+ {strippedVolumeInfo.stripeCount}
+
+
+ {strippedVolumeInfo.replicaCount}
+
+
+ {strippedVolumeInfo.arbiterCount}
+
+
+ {strippedVolumeInfo.disperseCount}
+
+
+ {strippedVolumeInfo.redundancyCount}
+
+
+ {_('xosanVolumeOptions')}
+
+ {map(strippedVolumeInfo.options, option => (
+
+ {option.value}
+
+ ))}
+
+
+ )}
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/tasks/index.js b/packages/xo-web/src/xo-app/tasks/index.js
new file mode 100644
index 000000000..290d873b0
--- /dev/null
+++ b/packages/xo-web/src/xo-app/tasks/index.js
@@ -0,0 +1,213 @@
+import _, { messages } from 'intl'
+import CenterPanel from 'center-panel'
+import Component from 'base-component'
+import Icon from 'icon'
+import Link from 'link'
+import React from 'react'
+import SortedTable from 'sorted-table'
+import { injectIntl } from 'react-intl'
+import { SelectPool } from 'select-objects'
+import { connectStore, resolveIds } from 'utils'
+import { Card, CardBlock, CardHeader } from 'card'
+import { Col, Container, Row } from 'grid'
+import { flatMap, flatten, isEmpty, keys, toArray } from 'lodash'
+import {
+ createGetObject,
+ createGetObjectsOfType,
+ createSelector,
+} from 'selectors'
+import { cancelTask, cancelTasks, destroyTask, destroyTasks } from 'xo'
+
+import Page from '../page'
+
+const HEADER = (
+
+
+
+
+ {_('taskMenu')}
+
+
+
+
+)
+
+const TASK_ITEM_STYLE = {
+ // Remove all margin, otherwise it breaks vertical alignment.
+ margin: 0,
+}
+@connectStore(() => ({
+ host: createGetObject((_, props) => props.item.$host),
+}))
+export class TaskItem extends Component {
+ render () {
+ const { host, item: task } = this.props
+
+ return (
+
+ {task.name_label} ({task.name_description &&
+ `${task.name_description} `}on{' '}
+ {host ? (
+ {host.name_label}
+ ) : (
+ `unknown host − ${task.$host}`
+ )})
+ {' ' + Math.round(task.progress * 100)}%
+
+ )
+ }
+}
+
+const COLUMNS = [
+ {
+ default: true,
+ itemRenderer: (task, userData) => {
+ const pool = userData.pools[task.$poolId]
+ return (
+ pool !== undefined && (
+ {pool.name_label}
+ )
+ )
+ },
+ name: _('pool'),
+ sortCriteria: (task, userData) => {
+ const pool = userData.pools[task.$poolId]
+ return pool !== undefined && pool.name_label
+ },
+ },
+ {
+ component: TaskItem,
+ name: _('task'),
+ sortCriteria: 'name_label',
+ },
+ {
+ itemRenderer: task => (
+
+ ),
+ name: _('progress'),
+ sortCriteria: 'progress',
+ },
+]
+
+const INDIVIDUAL_ACTIONS = [
+ {
+ handler: cancelTask,
+ icon: 'task-cancel',
+ label: _('cancelTask'),
+ level: 'danger',
+ },
+ {
+ handler: destroyTask,
+ icon: 'task-destroy',
+ label: _('destroyTask'),
+ level: 'danger',
+ },
+]
+
+const GROUPED_ACTIONS = [
+ {
+ handler: cancelTasks,
+ icon: 'task-cancel',
+ label: _('cancelTasks'),
+ level: 'danger',
+ },
+ {
+ handler: destroyTasks,
+ icon: 'task-destroy',
+ label: _('destroyTasks'),
+ level: 'danger',
+ },
+]
+
+@connectStore(() => {
+ const getPendingTasks = createGetObjectsOfType('task').filter([
+ task => task.status === 'pending',
+ ])
+
+ const getNPendingTasks = getPendingTasks.count()
+
+ const getPendingTasksByPool = getPendingTasks.sort().groupBy('$pool')
+
+ const getPools = createGetObjectsOfType('pool').pick(
+ createSelector(getPendingTasksByPool, keys)
+ )
+
+ return {
+ nTasks: getNPendingTasks,
+ pendingTasksByPool: getPendingTasksByPool,
+ pools: getPools,
+ }
+})
+@injectIntl
+export default class Tasks extends Component {
+ _getTasks = createSelector(
+ createSelector(() => this.state.pools, resolveIds),
+ () => this.props.pendingTasksByPool,
+ (poolIds, pendingTasksByPool) =>
+ isEmpty(poolIds)
+ ? flatten(toArray(pendingTasksByPool))
+ : flatMap(poolIds, poolId => pendingTasksByPool[poolId] || [])
+ )
+
+ render () {
+ const { props } = this
+ const { intl, nTasks, pendingTasksByPool, pools } = props
+
+ if (isEmpty(pendingTasksByPool)) {
+ return (
+
+
+
+ {_('noTasks')}
+
+
+
+ {_('xsTasks')}
+
+
+
+
+
+
+ )
+ }
+
+ const { formatMessage } = intl
+
+ return (
+
+
+
+
+
+
+
+ this.setState({ container })} />
+
+
+
+
+ this.state.container}
+ groupedActions={GROUPED_ACTIONS}
+ individualActions={INDIVIDUAL_ACTIONS}
+ stateUrlParam='s'
+ userData={{ pools }}
+ />
+
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/user/index.js b/packages/xo-web/src/xo-app/user/index.js
new file mode 100644
index 000000000..56f8bd9ec
--- /dev/null
+++ b/packages/xo-web/src/xo-app/user/index.js
@@ -0,0 +1,430 @@
+import * as FormGrid from 'form-grid'
+import * as homeFilters from 'home-filters'
+import _, { messages } from 'intl'
+import ActionButton from 'action-button'
+import Component from 'base-component'
+import Icon from 'icon'
+import propTypes from 'prop-types-decorator'
+import React from 'react'
+import SortedTable from 'sorted-table'
+import { Text } from 'editable'
+import { alert } from 'modal'
+import { Container, Row, Col } from 'grid'
+import { getLang } from 'selectors'
+import { map } from 'lodash'
+import { injectIntl } from 'react-intl'
+import { Select } from 'form'
+import { Card, CardBlock, CardHeader } from 'card'
+import { addSubscriptions, connectStore, noop } from 'utils'
+import {
+ addSshKey,
+ changePassword,
+ deleteSshKey,
+ deleteSshKeys,
+ editCustomFilter,
+ removeCustomFilter,
+ setDefaultHomeFilter,
+ subscribeCurrentUser,
+} from 'xo'
+
+import Page from '../page'
+
+// ===================================================================
+
+const HEADER = (
+
+
+
+
+ {_('userPage')}
+
+
+
+
+)
+
+// ===================================================================
+
+const FILTER_TYPE_TO_LABEL_ID = {
+ host: 'homeTypeHost',
+ pool: 'homeTypePool',
+ VM: 'homeTypeVm',
+ vmTemplate: 'homeTypeVmTemplate',
+}
+
+const SSH_KEY_STYLE = { wordWrap: 'break-word' }
+
+const getDefaultFilter = (defaultFilters, type) => {
+ if (defaultFilters == null) {
+ return ''
+ }
+
+ return defaultFilters[type] || ''
+}
+
+const getUserPreferences = user => user.preferences || {}
+
+// ===================================================================
+
+@propTypes({
+ customFilters: propTypes.object,
+ defaultFilter: propTypes.string.isRequired,
+ filters: propTypes.object.isRequired,
+ type: propTypes.string.isRequired,
+})
+class DefaultFilterPicker extends Component {
+ _computeOptions (props) {
+ const { customFilters, filters } = props
+
+ // Custom filters.
+ const options = [
+ {
+ label: _('customFilters'),
+ disabled: true,
+ },
+ ]
+
+ options.push.apply(
+ options,
+ map(customFilters, (filter, name) => ({
+ label: name,
+ value: name,
+ }))
+ )
+
+ // Default filters
+ options.push({
+ label: _('defaultFilters'),
+ disabled: true,
+ })
+
+ options.push.apply(
+ options,
+ map(filters, (filter, labelId) => ({
+ label: _(labelId),
+ value: labelId,
+ }))
+ )
+
+ this.setState({ options })
+ }
+
+ _handleDefaultFilter = value =>
+ setDefaultHomeFilter(this.props.type, value && value.value).catch(noop)
+
+ componentWillMount () {
+ this._computeOptions(this.props)
+ }
+
+ componentWillReceiveProps (props) {
+ this._computeOptions(props)
+ }
+
+ render () {
+ return (
+
+
+
+
+ {_('defaultFilter')}
+
+
+
+
+
+
+
+ )
+ }
+}
+
+// ===================================================================
+
+@propTypes({
+ user: propTypes.object.isRequired,
+})
+class UserFilters extends Component {
+ _removeFilter = ({ name, type }) => removeCustomFilter(type, name)
+
+ render () {
+ const {
+ defaultHomeFilters,
+ filters: customFiltersByType,
+ } = getUserPreferences(this.props.user)
+
+ return (
+
+
+
+ {_('customizeFilters')}
+
+ {map(homeFilters, (filters, type) => {
+ const labelId = FILTER_TYPE_TO_LABEL_ID[type]
+ if (!labelId) {
+ return
+ }
+
+ const customFilters =
+ customFiltersByType && customFiltersByType[type]
+ const defaultFilter = getDefaultFilter(defaultHomeFilters, type)
+
+ return (
+
+
{_(labelId)}
+
+
+ {map(customFilters, (filter, name) => (
+
+
+
+
+ editCustomFilter(type, name, { newName })
+ }
+ value={name}
+ />
+
+
+
+
+
+ editCustomFilter(type, name, { newValue })
+ }
+ value={filter}
+ />
+
+
+
+
+
+
+ ))}
+
+ )
+ })}
+
+
+
+
+ )
+ }
+}
+
+// ===================================================================
+const COLUMNS = [
+ {
+ default: true,
+ itemRenderer: sshKey => sshKey.title,
+ name: _('title'),
+ sortCriteria: 'title',
+ },
+ {
+ itemRenderer: sshKey =>
{sshKey.key} ,
+ name: _('key'),
+ },
+]
+
+const INDIVIDUAL_ACTIONS = [
+ {
+ handler: deleteSshKey,
+ icon: 'delete',
+ label: _('deleteSshKey'),
+ level: 'danger',
+ },
+]
+
+const GROUPED_ACTIONS = [
+ {
+ handler: deleteSshKeys,
+ icon: 'delete',
+ label: _('deleteSshKeys'),
+ level: 'danger',
+ },
+]
+
+const SshKeys = addSubscriptions({
+ user: subscribeCurrentUser,
+})(({ user }) => {
+ const sshKeys = user && user.preferences && user.preferences.sshKeys
+
+ const sshKeysWithIds = map(sshKeys, sshKey => ({ ...sshKey, id: sshKey.key }))
+
+ return (
+
+
+
+ {_('sshKeys')}
+
+ {_('newSshKey')}
+
+
+
+
+
+
+
+ )
+})
+
+// ===================================================================
+
+@addSubscriptions({
+ user: subscribeCurrentUser,
+})
+@connectStore({
+ lang: getLang,
+})
+@injectIntl
+export default class User extends Component {
+ handleSelectLang = event => {
+ this.props.selectLang(event.target.value)
+ }
+
+ _handleSavePassword = () => {
+ const { oldPassword, newPassword, confirmPassword } = this.state
+ if (newPassword !== confirmPassword) {
+ return alert(
+ _('confirmationPasswordError'),
+ _('confirmationPasswordErrorBody')
+ )
+ }
+ return changePassword(oldPassword, newPassword).then(() =>
+ this.setState({
+ oldPassword: undefined,
+ newPassword: undefined,
+ confirmPassword: undefined,
+ })
+ )
+ }
+
+ _handleOldPasswordChange = event =>
+ this.setState({ oldPassword: event.target.value })
+ _handleNewPasswordChange = event =>
+ this.setState({ newPassword: event.target.value })
+ _handleConfirmPasswordChange = event =>
+ this.setState({ confirmPassword: event.target.value })
+
+ render () {
+ const { lang, user } = this.props
+
+ if (!user) {
+ return
Loading…
+ }
+
+ const { formatMessage } = this.props.intl
+ const { confirmPassword, newPassword, oldPassword } = this.state
+
+ return (
+
+
+
+
+ {_('username')}
+
+ {user.email}
+
+
+
+
+ {_('password')}
+
+
+
+
+
+
+
+
+ {_('language')}
+
+
+
+ English
+ Français
+ עברי
+ Polski
+ Português
+ Español
+ 简体中文
+ Magyar
+
+
+
+
+
+
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/vm-import/index.css b/packages/xo-web/src/xo-app/vm-import/index.css
new file mode 100644
index 000000000..702546f12
--- /dev/null
+++ b/packages/xo-web/src/xo-app/vm-import/index.css
@@ -0,0 +1,6 @@
+.vmContainer {
+ margin-bottom: 1em;
+ border-radius: 0.25rem;
+ border: Solid 0.05em dropzoneColor;
+ padding: 1em;
+}
diff --git a/packages/xo-web/src/xo-app/vm-import/index.js b/packages/xo-web/src/xo-app/vm-import/index.js
new file mode 100644
index 000000000..cedd47adf
--- /dev/null
+++ b/packages/xo-web/src/xo-app/vm-import/index.js
@@ -0,0 +1,420 @@
+import * as FormGrid from 'form-grid'
+import _ from 'intl'
+import ActionButton from 'action-button'
+import Button from 'button'
+import Component from 'base-component'
+import Dropzone from 'dropzone'
+import Icon from 'icon'
+import isEmpty from 'lodash/isEmpty'
+import map from 'lodash/map'
+import orderBy from 'lodash/orderBy'
+import propTypes from 'prop-types-decorator'
+import React from 'react'
+import Upgrade from 'xoa-upgrade'
+import { Container, Col, Row } from 'grid'
+import { importVms, isSrWritable } from 'xo'
+import { SizeInput } from 'form'
+import {
+ createFinder,
+ createGetObject,
+ createGetObjectsOfType,
+ createSelector,
+} from 'selectors'
+import { connectStore, formatSize, mapPlus, noop } from 'utils'
+import { SelectNetwork, SelectPool, SelectSr } from 'select-objects'
+
+import Page from '../page'
+import parseOvaFile from './ova'
+
+import styles from './index.css'
+
+// ===================================================================
+
+const FORMAT_TO_HANDLER = {
+ ova: parseOvaFile,
+ xva: noop,
+}
+
+const HEADER = (
+
+
+
+
+ {_('newImport')}
+
+
+
+
+)
+
+// ===================================================================
+
+@propTypes({
+ descriptionLabel: propTypes.string,
+ disks: propTypes.objectOf(
+ propTypes.shape({
+ capacity: propTypes.number.isRequired,
+ descriptionLabel: propTypes.string.isRequired,
+ nameLabel: propTypes.string.isRequired,
+ path: propTypes.string.isRequired,
+ })
+ ),
+ memory: propTypes.number,
+ nameLabel: propTypes.string,
+ nCpus: propTypes.number,
+ networks: propTypes.array,
+ pool: propTypes.object.isRequired,
+})
+@connectStore(
+ () => {
+ const getHostMaster = createGetObject((_, props) => props.pool.master)
+ const getPifs = createGetObjectsOfType('PIF').pick(
+ (state, props) => getHostMaster(state, props).$PIFs
+ )
+ const getDefaultNetworkId = createSelector(
+ createFinder(getPifs, [pif => pif.management]),
+ pif => pif.$network
+ )
+
+ return {
+ defaultNetwork: getDefaultNetworkId,
+ }
+ },
+ { withRef: true }
+)
+class VmData extends Component {
+ get value () {
+ const { props, refs } = this
+ return {
+ descriptionLabel: refs.descriptionLabel.value,
+ disks: map(props.disks, ({ capacity, path, position }, diskId) => ({
+ capacity,
+ descriptionLabel: refs[`disk-description-${diskId}`].value,
+ nameLabel: refs[`disk-name-${diskId}`].value,
+ path,
+ position,
+ })),
+ memory: +refs.memory.value,
+ nameLabel: refs.nameLabel.value,
+ networks: map(props.networks, (_, networkId) => {
+ const network = refs[`network-${networkId}`].value
+ return network.id ? network.id : network
+ }),
+ nCpus: +refs.nCpus.value,
+ }
+ }
+
+ _getNetworkPredicate = createSelector(
+ () => this.props.pool.id,
+ id => network => network.$pool === id
+ )
+
+ render () {
+ const {
+ descriptionLabel,
+ defaultNetwork,
+ disks,
+ memory,
+ nameLabel,
+ nCpus,
+ networks,
+ } = this.props
+
+ return (
+
+ )
+ }
+}
+
+// ===================================================================
+
+const parseFile = async (file, type, func) => {
+ try {
+ return {
+ data: await func(file),
+ file,
+ type,
+ }
+ } catch (error) {
+ return { error, file, type }
+ }
+}
+
+export default class Import extends Component {
+ constructor (props) {
+ super(props)
+ this.state.vms = []
+ }
+
+ _import = () => {
+ const { state } = this
+ return importVms(
+ mapPlus(state.vms, (vm, push, vmIndex) => {
+ if (!vm.error) {
+ const ref = this.refs[`vm-data-${vmIndex}`]
+ push({
+ ...vm,
+ data: ref && ref.value,
+ })
+ }
+ }),
+ state.sr
+ )
+ }
+
+ _handleDrop = async files => {
+ const vms = await Promise.all(
+ mapPlus(files, (file, push) => {
+ const { name } = file
+ const extIndex = name.lastIndexOf('.')
+
+ let func
+ let type
+
+ if (
+ extIndex >= 0 &&
+ (type = name.substring(extIndex + 1)) &&
+ (func = FORMAT_TO_HANDLER[type])
+ ) {
+ push(parseFile(file, type, func))
+ }
+ })
+ )
+
+ this.setState({
+ vms: orderBy(vms, vm => [vm.error != null, vm.type, vm.file.name]),
+ })
+ }
+
+ _handleCleanSelectedVms = () => {
+ this.setState({
+ vms: [],
+ })
+ }
+
+ _handleSelectedPool = pool => {
+ if (pool === '') {
+ this.setState({
+ pool: undefined,
+ sr: undefined,
+ srPredicate: undefined,
+ })
+ } else {
+ this.setState({
+ pool,
+ sr: pool.default_SR,
+ srPredicate: sr => sr.$pool === this.state.pool.id && isSrWritable(sr),
+ })
+ }
+ }
+
+ _handleSelectedSr = sr => {
+ this.setState({
+ sr: sr === '' ? undefined : sr,
+ })
+ }
+
+ render () {
+ const { pool, sr, srPredicate, vms } = this.state
+
+ return (
+
+ {process.env.XOA_PLAN > 1 ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/vm-import/ova/index.js b/packages/xo-web/src/xo-app/vm-import/ova/index.js
new file mode 100644
index 000000000..d7c7a1d32
--- /dev/null
+++ b/packages/xo-web/src/xo-app/vm-import/ova/index.js
@@ -0,0 +1,184 @@
+import find from 'lodash/find'
+import forEach from 'lodash/forEach'
+import tar from 'tar-stream'
+import xml2js from 'xml2js'
+import { ensureArray, htmlFileToStream, streamToString } from 'utils'
+
+// ===================================================================
+
+// See: http://opennodecloud.com/howto/2013/12/25/howto-ON-ovf-reference.html
+// See: http://www.dmtf.org/sites/default/files/standards/documents/DSP0243_1.0.0.pdf
+// See: http://www.dmtf.org/sites/default/files/standards/documents/DSP0243_2.1.0.pdf
+
+// ===================================================================
+
+const MEMORY_UNIT_TO_FACTOR = {
+ k: 1024,
+ m: 1048576,
+ g: 1073741824,
+ t: 1099511627776,
+}
+
+const RESOURCE_TYPE_TO_HANDLER = {
+ // CPU.
+ '3': (data, { 'rasd:VirtualQuantity': nCpus }) => {
+ data.nCpus = +nCpus
+ },
+ // RAM.
+ '4': (
+ data,
+ { 'rasd:AllocationUnits': unit, 'rasd:VirtualQuantity': quantity }
+ ) => {
+ data.memory = quantity * allocationUnitsToFactor(unit)
+ },
+ // Network.
+ '10': (
+ { networks },
+ { 'rasd:AutomaticAllocation': enabled, 'rasd:Connection': name }
+ ) => {
+ if (enabled) {
+ networks.push(name)
+ }
+ },
+ // Disk.
+ '17': (
+ { disks },
+ {
+ 'rasd:AddressOnParent': position,
+ 'rasd:Description': description = 'No description',
+ 'rasd:ElementName': name,
+ 'rasd:HostResource': resource,
+ }
+ ) => {
+ const diskId = resource.match(/^(?:ovf:)?\/disk\/(.+)$/)
+ const disk = diskId && disks[diskId[1]]
+
+ if (disk) {
+ disk.descriptionLabel = description
+ disk.nameLabel = name
+ disk.position = +position
+ } else {
+ // TODO: Log error in U.I.
+ console.error(`No disk found: '${diskId}'.`)
+ }
+ },
+}
+
+const allocationUnitsToFactor = unit => {
+ const intValue = unit.match(/\^([0-9]+)$/)
+ return intValue != null
+ ? Math.pow(2, intValue[1])
+ : MEMORY_UNIT_TO_FACTOR[unit.charAt(0).toLowerCase()]
+}
+
+const filterDisks = disks => {
+ for (const diskId in disks) {
+ if (disks[diskId].position == null) {
+ // TODO: Log error in U.I.
+ console.error(`No position specified for '${diskId}'.`)
+ delete disks[diskId]
+ }
+ }
+}
+
+// ===================================================================
+
+const parseOvaFile = file =>
+ new Promise((resolve, reject) => {
+ const stream = htmlFileToStream(file)
+ const extract = tar.extract()
+
+ stream.on('error', reject)
+
+ // tar module can work with bad tar files...
+ // So it's necessary to reject at end of stream.
+ extract.on('finish', () => {
+ reject(new Error('No ovf file found.'))
+ })
+ extract.on('error', reject)
+ extract.on('entry', ({ name }, stream, cb) => {
+ // Not a XML file.
+ const extIndex = name.lastIndexOf('.')
+ if (extIndex === -1 || name.substring(extIndex + 1) !== 'ovf') {
+ stream.on('end', cb)
+ stream.resume()
+ return
+ }
+
+ // XML file.
+ streamToString(stream).then(xmlString => {
+ xml2js.parseString(
+ xmlString,
+ {
+ mergeAttrs: true,
+ explicitArray: false,
+ },
+ (err, res) => {
+ if (err) {
+ reject(err)
+ return
+ }
+
+ const {
+ Envelope: {
+ DiskSection: { Disk: disks },
+ References: { File: files },
+ VirtualSystem: system,
+ },
+ } = res
+
+ const data = {
+ disks: {},
+ networks: [],
+ }
+ const hardware = system.VirtualHardwareSection
+
+ // Get VM name/description.
+ data.nameLabel = hardware.System['vssd:VirtualSystemIdentifier']
+ data.descriptionLabel =
+ (system.AnnotationSection &&
+ system.AnnotationSection.Annotation) ||
+ (system.OperatingSystemSection &&
+ system.OperatingSystemSection.Description)
+
+ // Get disks.
+ forEach(ensureArray(disks), disk => {
+ const file = find(
+ ensureArray(files),
+ file => file['ovf:id'] === disk['ovf:fileRef']
+ )
+ const unit = disk['ovf:capacityAllocationUnits']
+
+ data.disks[disk['ovf:diskId']] = {
+ capacity:
+ disk['ovf:capacity'] *
+ ((unit && allocationUnitsToFactor(unit)) || 1),
+ path: file && file['ovf:href'],
+ }
+ })
+
+ // Get hardware info: CPU, RAM, disks, networks...
+ forEach(ensureArray(hardware.Item), item => {
+ const handler =
+ RESOURCE_TYPE_TO_HANDLER[item['rasd:ResourceType']]
+ if (!handler) {
+ return
+ }
+ handler(data, item)
+ })
+
+ // Remove disks which not have a position.
+ // (i.e. no info in hardware.Item section.)
+ filterDisks(data.disks)
+
+ // Done!
+ resolve(data)
+ cb()
+ }
+ )
+ })
+ })
+
+ stream.pipe(extract)
+ })
+export { parseOvaFile as default }
diff --git a/packages/xo-web/src/xo-app/vm/action-bar.js b/packages/xo-web/src/xo-app/vm/action-bar.js
new file mode 100644
index 000000000..7df33395c
--- /dev/null
+++ b/packages/xo-web/src/xo-app/vm/action-bar.js
@@ -0,0 +1,206 @@
+import _ from 'intl'
+import ActionBar, { Action } from 'action-bar'
+import React from 'react'
+import { addSubscriptions, connectStore } from 'utils'
+import { find, includes } from 'lodash'
+import { createSelector, getCheckPermissions, getUser } from 'selectors'
+import {
+ cloneVm,
+ copyVm,
+ exportVm,
+ migrateVm,
+ restartVm,
+ resumeVm,
+ snapshotVm,
+ startVm,
+ stopVm,
+ subscribeResourceSets,
+} from 'xo'
+
+const vmActionBarByState = {
+ Running: ({ vm, isSelfUser, canAdministrate }) => (
+
+
+
+ {!isSelfUser && (
+
+ )}
+ {!isSelfUser && (
+
+ )}
+ {!isSelfUser &&
+ canAdministrate && (
+
+ )}
+ {!isSelfUser &&
+ canAdministrate && (
+
+ )}
+
+ ),
+ Halted: ({ vm, isSelfUser, canAdministrate }) => (
+
+
+ {!isSelfUser &&
+ canAdministrate && (
+
+ )}
+ {!isSelfUser && (
+
+ )}
+ {!isSelfUser && (
+
+ )}
+ {!isSelfUser &&
+ canAdministrate && (
+
+ )}
+ {!isSelfUser &&
+ canAdministrate && (
+
+ )}
+
+ ),
+ Suspended: ({ vm, isSelfUser, canAdministrate }) => (
+
+
+ {!isSelfUser && (
+
+ )}
+ {!isSelfUser &&
+ canAdministrate && (
+
+ )}
+ {!isSelfUser &&
+ canAdministrate && (
+
+ )}
+
+ ),
+}
+
+const VmActionBar = addSubscriptions(() => ({
+ resourceSets: subscribeResourceSets,
+}))(
+ connectStore(() => ({
+ checkPermissions: getCheckPermissions,
+ userId: createSelector(getUser, user => user.id),
+ }))(({ checkPermissions, vm, userId, resourceSets }) => {
+ // Is the user in the same resource set as the VM
+ const _getIsSelfUser = createSelector(
+ () => resourceSets,
+ resourceSets => {
+ const vmResourceSet =
+ vm.resourceSet && find(resourceSets, { id: vm.resourceSet })
+
+ return vmResourceSet && includes(vmResourceSet.subjects, userId)
+ }
+ )
+
+ const _getCanAdministrate = createSelector(
+ () => checkPermissions,
+ () => vm.id,
+ (check, vmId) => check(vmId, 'administrate')
+ )
+
+ const ActionBar = vmActionBarByState[vm.power_state]
+ if (!ActionBar) {
+ return
No action bar for state {vm.power_state}
+ }
+
+ return (
+
+ )
+ })
+)
+export default VmActionBar
diff --git a/packages/xo-web/src/xo-app/vm/index.js b/packages/xo-web/src/xo-app/vm/index.js
new file mode 100644
index 000000000..543f24cf6
--- /dev/null
+++ b/packages/xo-web/src/xo-app/vm/index.js
@@ -0,0 +1,300 @@
+import _ from 'intl'
+import BaseComponent from 'base-component'
+import Icon from 'icon'
+import Link from 'link'
+import { NavLink, NavTabs } from 'nav'
+import Page from '../page'
+import React, { cloneElement } from 'react'
+import VmActionBar from './action-bar'
+import { Select, Text } from 'editable'
+import { assign, isEmpty, map, pick } from 'lodash'
+import { editVm, fetchVmStats, isVmRunning, migrateVm } from 'xo'
+import { Container, Row, Col } from 'grid'
+import { connectStore, routes } from 'utils'
+import {
+ createGetObject,
+ createGetObjectsOfType,
+ createGetVmDisks,
+ createSelector,
+ createSumBy,
+ getCheckPermissions,
+ isAdmin,
+} from 'selectors'
+
+import TabGeneral from './tab-general'
+import TabStats from './tab-stats'
+import TabConsole from './tab-console'
+import TabContainers from './tab-containers'
+import TabDisks from './tab-disks'
+import TabNetwork from './tab-network'
+import TabSnapshots from './tab-snapshots'
+import TabLogs from './tab-logs'
+import TabAdvanced from './tab-advanced'
+
+// ===================================================================
+
+@routes('general', {
+ advanced: TabAdvanced,
+ console: TabConsole,
+ containers: TabContainers,
+ disks: TabDisks,
+ general: TabGeneral,
+ logs: TabLogs,
+ network: TabNetwork,
+ snapshots: TabSnapshots,
+ stats: TabStats,
+})
+@connectStore(() => {
+ const getVm = createGetObject()
+
+ const getContainer = createGetObject(
+ (state, props) => getVm(state, props).$container
+ )
+
+ const getPool = createGetObject((state, props) => getVm(state, props).$pool)
+
+ const getVbds = createGetObjectsOfType('VBD')
+ .pick((state, props) => getVm(state, props).$VBDs)
+ .sort()
+ const getVdis = createGetVmDisks(getVm)
+ const getSrs = createGetObjectsOfType('SR').pick(
+ createSelector(getVdis, vdis => map(vdis, '$SR'))
+ )
+
+ const getVmTotalDiskSpace = createSumBy(createGetVmDisks(getVm), 'size')
+
+ const getHosts = createGetObjectsOfType('host')
+
+ return (state, props) => {
+ const vm = getVm(state, props)
+ if (!vm) {
+ return {}
+ }
+
+ return {
+ checkPermissions: getCheckPermissions(state, props),
+ container: getContainer(state, props),
+ hosts: getHosts(state, props),
+ isAdmin: isAdmin(state, props),
+ pool: getPool(state, props),
+ srs: getSrs(state, props),
+ vbds: getVbds(state, props),
+ vdis: getVdis(state, props),
+ vm,
+ vmTotalDiskSpace: getVmTotalDiskSpace(state, props),
+ }
+ }
+})
+export default class Vm extends BaseComponent {
+ static contextTypes = {
+ router: React.PropTypes.object,
+ }
+
+ loop (vm = this.props.vm) {
+ if (this.cancel) {
+ this.cancel()
+ }
+
+ if (!isVmRunning(vm)) {
+ return
+ }
+
+ let cancelled = false
+ this.cancel = () => {
+ cancelled = true
+ }
+
+ fetchVmStats(vm).then(stats => {
+ if (cancelled) {
+ return
+ }
+ this.cancel = null
+
+ clearTimeout(this.timeout)
+ this.setState(
+ {
+ statsOverview: stats,
+ },
+ () => {
+ this.timeout = setTimeout(this.loop, stats.interval * 1000)
+ }
+ )
+ })
+ }
+ loop = ::this.loop
+
+ componentWillMount () {
+ this.loop()
+ }
+
+ componentWillUnmount () {
+ clearTimeout(this.timeout)
+ }
+
+ componentWillReceiveProps (props) {
+ const vmCur = this.props.vm
+ const vmNext = props.vm
+
+ if (vmCur && !vmNext) {
+ this.context.router.push('/')
+ }
+
+ if (!isVmRunning(vmCur) && isVmRunning(vmNext)) {
+ this.loop(vmNext)
+ } else if (isVmRunning(vmCur) && !isVmRunning(vmNext)) {
+ this.setState({
+ statsOverview: undefined,
+ })
+ }
+ }
+
+ _getCanSnapshot = createSelector(
+ () => this.props.checkPermissions,
+ () => this.props.vm,
+ () => this.props.srs,
+ (checkPermissions, vm, srs) =>
+ checkPermissions([
+ [vm.id, 'administrate'],
+ ...map(srs, sr => [sr.id, 'operate']),
+ ])
+ )
+
+ _setNameDescription = nameDescription =>
+ editVm(this.props.vm, { name_description: nameDescription })
+ _setNameLabel = nameLabel => editVm(this.props.vm, { name_label: nameLabel })
+ _migrateVm = host => migrateVm(this.props.vm, host)
+
+ _selectOptionRenderer = option => option.name_label
+
+ header () {
+ const { vm, container, pool, hosts } = this.props
+ if (!vm) {
+ return
+ }
+ return (
+
+
+
+
+ {isEmpty(vm.current_operations) ? (
+
+ ) : (
+
+ )}{' '}
+
+
+
+
+
+ {vm.power_state === 'Running' &&
+ container && (
+
+ -
+
+
+ {container.name_label}
+
+
+
+ )}{' '}
+ {pool && (
+ {pool.name_label}
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {_('generalTabName')}
+
+ {_('statsTabName')}
+
+ {_('consoleTabName')}
+
+
+ {_('networkTabName')}
+
+
+ {_('disksTabName', { disks: vm.$VBDs.length })}
+
+ {this._getCanSnapshot() && (
+
+ {_('snapshotsTabName')}{' '}
+ {vm.snapshots.length !== 0 && (
+
+ {vm.snapshots.length}
+
+ )}
+
+ )}
+ {_('logsTabName')}
+ {vm.docker && (
+
+ {_('containersTabName')}
+
+ )}
+
+ {_('advancedTabName')}
+
+
+
+
+
+ )
+ }
+
+ _toggleHeader = () =>
+ this.setState({ collapsedHeader: !this.state.collapsedHeader })
+
+ render () {
+ const { container, vm } = this.props
+ if (!vm) {
+ return
{_('statusLoading')}
+ }
+
+ const childProps = assign(
+ pick(this.props, [
+ 'container',
+ 'pool',
+ 'removeTag',
+ 'srs',
+ 'vbds',
+ 'vdis',
+ 'vm',
+ 'vmTotalDiskSpace',
+ ]),
+ pick(this.state, ['statsOverview'])
+ )
+ return (
+
+ {cloneElement(this.props.children, {
+ ...childProps,
+ toggleHeader: this._toggleHeader,
+ })}
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/vm/tab-advanced.js b/packages/xo-web/src/xo-app/vm/tab-advanced.js
new file mode 100644
index 000000000..c8f9ef4b0
--- /dev/null
+++ b/packages/xo-web/src/xo-app/vm/tab-advanced.js
@@ -0,0 +1,623 @@
+import _ from 'intl'
+import ActionButton from 'action-button'
+import Component from 'base-component'
+import Copiable from 'copiable'
+import defined from 'xo-defined'
+import getEventValue from 'get-event-value'
+import Icon from 'icon'
+import React from 'react'
+import renderXoItem from 'render-xo-item'
+import TabButton from 'tab-button'
+import Tooltip from 'tooltip'
+import { Toggle } from 'form'
+import { Number, Size, Text, XoSelect } from 'editable'
+import { Container, Row, Col } from 'grid'
+import { SelectResourceSet, SelectVgpuType } from 'select-objects'
+import { confirm } from 'modal'
+import { assign, every, find, includes, isEmpty, map, uniq } from 'lodash'
+import {
+ addSubscriptions,
+ connectStore,
+ formatSize,
+ getCoresPerSocketPossibilities,
+ normalizeXenToolsStatus,
+ osFamily,
+} from 'utils'
+import {
+ createVgpu,
+ cloneVm,
+ convertVmToTemplate,
+ deleteVgpu,
+ deleteVm,
+ editVm,
+ isVmRunning,
+ recoveryStartVm,
+ restartVm,
+ resumeVm,
+ stopVm,
+ subscribeResourceSets,
+ suspendVm,
+ XEN_DEFAULT_CPU_CAP,
+ XEN_DEFAULT_CPU_WEIGHT,
+ XEN_VIDEORAM_VALUES,
+} from 'xo'
+import { createGetObjectsOfType, createSelector, isAdmin } from 'selectors'
+
+const forceReboot = vm => restartVm(vm, true)
+const forceShutdown = vm => stopVm(vm, true)
+const fullCopy = vm => cloneVm(vm, true)
+
+@connectStore(() => {
+ const getAffinityHost = createGetObjectsOfType('host').find((_, { vm }) => ({
+ id: vm.affinityHost,
+ }))
+
+ const getVbds = createGetObjectsOfType('VBD').pick((_, { vm }) => vm.$VBDs)
+ const getVdis = createGetObjectsOfType('VDI').pick(
+ createSelector(getVbds, vbds => map(vbds, 'VDI'))
+ )
+ const getSrs = createGetObjectsOfType('SR').pick(
+ createSelector(getVdis, vdis => uniq(map(vdis, '$SR')))
+ )
+ const getSrsContainers = createSelector(getSrs, srs =>
+ uniq(map(srs, '$container'))
+ )
+
+ const getAffinityHostPredicate = createSelector(
+ getSrsContainers,
+ containers => host =>
+ every(
+ containers,
+ container => container === host.$pool || container === host.id
+ )
+ )
+
+ return {
+ affinityHost: getAffinityHost,
+ affinityHostPredicate: getAffinityHostPredicate,
+ }
+})
+class AffinityHost extends Component {
+ _editAffinityHost = host =>
+ editVm(this.props.vm, { affinityHost: host.id || null })
+
+ render () {
+ const { affinityHost, affinityHostPredicate } = this.props
+
+ return (
+
+
+ {affinityHost ? renderXoItem(affinityHost) : _('noAffinityHost')}
+ {' '}
+ {affinityHost && (
+
+
+
+ )}
+
+ )
+ }
+}
+
+@addSubscriptions({
+ resourceSets: subscribeResourceSets,
+})
+class ResourceSetItem extends Component {
+ _getResourceSet = createSelector(
+ () => this.props.resourceSets,
+ () => this.props.id,
+ (resourceSets, id) =>
+ assign(find(resourceSets, { id }), { type: 'resourceSet' })
+ )
+
+ render () {
+ return this.props.resourceSets === undefined
+ ? null
+ : renderXoItem(this._getResourceSet())
+ }
+}
+
+class NewVgpu extends Component {
+ get value () {
+ return this.state
+ }
+
+ _getPredicate = createSelector(
+ () => this.props.vm && this.props.vm.$pool,
+ poolId => vgpuType => poolId === vgpuType.$pool
+ )
+
+ render () {
+ return (
+
+
+ {_('vmSelectVgpuType')}
+
+
+
+
+
+ )
+ }
+}
+
+class Vgpus extends Component {
+ _createVgpu = vgpuType =>
+ confirm({
+ icon: 'gpu',
+ title: _('vmAddVgpu'),
+ body:
,
+ }).then(({ vgpuType }) =>
+ createVgpu(this.props.vm, { vgpuType, gpuGroup: vgpuType.gpuGroup })
+ )
+
+ render () {
+ const { vgpus, vm } = this.props
+
+ return (
+
+ {map(vgpus, vgpu => (
+
+ {!isVmRunning(vm) && (
+
+ )}{' '}
+ {renderXoItem(vgpu)}
+
+ ))}
+ {isEmpty(vgpus) && (
+
+ {!isVmRunning(vm) && (
+
+ )}{' '}
+ {_('vmVgpuNone')}
+
+ )}
+
+ )
+ }
+}
+
+class CoresPerSocket extends Component {
+ _getCoresPerSocketPossibilities = createSelector(
+ () => {
+ const { container } = this.props
+ if (container != null) {
+ return container.cpus.cores
+ }
+ },
+ () => this.props.vm.CPUs.number,
+ getCoresPerSocketPossibilities
+ )
+
+ _selectedValueIsNotInOptions = createSelector(
+ () => this.props.vm.coresPerSocket,
+ this._getCoresPerSocketPossibilities,
+ (selectedCoresPerSocket, options) =>
+ selectedCoresPerSocket !== undefined &&
+ !includes(options, selectedCoresPerSocket)
+ )
+
+ _onChange = event =>
+ editVm(this.props.vm, { coresPerSocket: getEventValue(event) || null })
+
+ render () {
+ const { container, vm } = this.props
+ const selectedCoresPerSocket = vm.coresPerSocket
+ const options = this._getCoresPerSocketPossibilities()
+
+ return (
+
+ )
+ }
+}
+
+export default connectStore(() => {
+ const getVgpus = createGetObjectsOfType('vgpu').pick((_, { vm }) => vm.$VGPUs)
+
+ const getVgpuTypes = createGetObjectsOfType('vgpuType').pick(
+ createSelector(getVgpus, vgpus => map(vgpus, 'vgpuType'))
+ )
+
+ const getGpuGroup = createGetObjectsOfType('gpuGroup').pick(
+ createSelector(getVgpus, vgpus => map(vgpus, 'gpuGroup'))
+ )
+
+ return {
+ gpuGroup: getGpuGroup,
+ isAdmin,
+ vgpus: getVgpus,
+ vgpuTypes: getVgpuTypes,
+ }
+})(({ container, gpuGroup, isAdmin, vgpus, vgpuTypes, vm }) => (
+
+
+
+ {vm.power_state === 'Running' && (
+
+
+
+
+
+ )}
+ {vm.power_state === 'Halted' && (
+
+
+
+
+
+ )}
+ {vm.power_state === 'Suspended' && (
+
+
+
+
+ )}
+
+
+
+
+
+ {_('xenSettingsLabel')}
+
+
+
+ {_('uuid')}
+ {vm.uuid}
+
+
+ {_('virtualizationMode')}
+
+ {vm.virtualizationMode === 'pv'
+ ? _('paraVirtualizedMode')
+ : _('hardwareVirtualizedMode')}
+
+
+ {vm.virtualizationMode === 'pv' && (
+
+ {_('pvArgsLabel')}
+
+ editVm(vm, { PV_args: value })}
+ />
+
+
+ )}
+
+ {_('cpuWeightLabel')}
+
+ editVm(vm, { cpuWeight: value })}
+ nullable
+ >
+ {vm.cpuWeight == null
+ ? _('defaultCpuWeight', { value: XEN_DEFAULT_CPU_WEIGHT })
+ : vm.cpuWeight}
+
+
+
+
+ {_('cpuCapLabel')}
+
+ editVm(vm, { cpuCap: value })}
+ nullable
+ >
+ {vm.cpuCap == null
+ ? _('defaultCpuCap', { value: XEN_DEFAULT_CPU_CAP })
+ : vm.cpuCap}
+
+
+
+
+ {_('autoPowerOn')}
+
+ editVm(vm, { auto_poweron: value })}
+ />
+
+
+
+ {_('ha')}
+
+ editVm(vm, { high_availability: value })}
+ />
+
+
+
+ {_('vmAffinityHost')}
+
+
+
+
+ {vm.virtualizationMode === 'hvm' && (
+
+ {_('vmVgpus')}
+
+
+
+
+ )}
+ {vm.virtualizationMode === 'hvm' && (
+
+ {_('vmVga')}
+
+
+ editVm(vm, { vga: value ? 'std' : 'cirrus' })
+ }
+ />
+
+
+ )}
+ {vm.vga === 'std' && (
+
+ {_('vmVideoram')}
+
+
+ editVm(vm, { videoram: +getEventValue(event) })
+ }
+ value={vm.videoram}
+ >
+ {map(XEN_VIDEORAM_VALUES, val => (
+
+ {formatSize(val * 1048576)}
+
+ ))}
+
+
+
+ )}
+
+
+
+ {_('vmLimitsLabel')}
+
+
+
+ {_('vmCpuLimitsLabel')}
+
+ editVm(vm, { cpus })}
+ />
+ /
+ {vm.power_state === 'Running' ? (
+ vm.CPUs.max
+ ) : (
+ editVm(vm, { cpusStaticMax })}
+ />
+ )}
+
+
+
+ {_('vmCpuTopology')}
+
+
+
+
+
+ {_('vmMemoryLimitsLabel')}
+
+
+ Static: {formatSize(vm.memory.static[0])}/
+ editVm(vm, { memoryStaticMax })
+ }
+ />
+
+
+ Dynamic:{' '}
+ editVm(vm, { memoryMin })}
+ />/ editVm(vm, { memoryMax })}
+ />
+
+
+
+
+
+
+ {_('guestOsLabel')}
+
+
+
+ {_('xenToolsStatus')}
+
+ {_('xenToolsStatusValue', {
+ status: normalizeXenToolsStatus(vm.xenTools),
+ })}
+
+
+
+ {_('osName')}
+
+ {isEmpty(vm.os_version) ? (
+ _('unknownOsName')
+ ) : (
+
+ {vm.os_version.name}
+
+ )}
+
+
+
+ {_('osKernel')}
+
+ {(vm.os_version && vm.os_version.uname) || _('unknownOsKernel')}
+
+
+
+
+
+ {_('miscLabel')}
+
+
+
+ {_('originalTemplate')}
+
+ {vm.other.base_template_name
+ ? vm.other.base_template_name
+ : _('unknownOriginalTemplate')}
+
+
+
+ {_('resourceSet')}
+
+ {isAdmin ? (
+
+ editVm(vm, {
+ resourceSet:
+ resourceSet != null ? resourceSet.id : resourceSet,
+ })
+ }
+ value={vm.resourceSet}
+ />
+ ) : vm.resourceSet !== undefined ? (
+
+ ) : (
+ _('resourceSetNone')
+ )}
+
+
+
+
+
+
+
+))
diff --git a/packages/xo-web/src/xo-app/vm/tab-console.js b/packages/xo-web/src/xo-app/vm/tab-console.js
new file mode 100644
index 000000000..8efc37a98
--- /dev/null
+++ b/packages/xo-web/src/xo-app/vm/tab-console.js
@@ -0,0 +1,161 @@
+import _ from 'intl'
+import Button from 'button'
+import Component from 'base-component'
+import CopyToClipboard from 'react-copy-to-clipboard'
+import debounce from 'lodash/debounce'
+import Icon from 'icon'
+import invoke from 'invoke'
+import IsoDevice from 'iso-device'
+import NoVnc from 'react-novnc'
+import React from 'react'
+import Tooltip from 'tooltip'
+import { resolveUrl, isVmRunning } from 'xo'
+import { Container, Row, Col } from 'grid'
+import {
+ CpuSparkLines,
+ MemorySparkLines,
+ NetworkSparkLines,
+ XvdSparkLines,
+} from 'xo-sparklines'
+
+export default class TabConsole extends Component {
+ state = { scale: 1 }
+
+ componentWillReceiveProps (props) {
+ if (
+ isVmRunning(this.props.vm) &&
+ !isVmRunning(props.vm) &&
+ this.state.minimalLayout
+ ) {
+ this._toggleMinimalLayout()
+ }
+ }
+ _sendCtrlAltDel = () => {
+ this.refs.noVnc.sendCtrlAltDel()
+ }
+
+ _getRemoteClipboard = clipboard => {
+ this.setState({ clipboard })
+ this.refs.clipboard.value = clipboard
+ }
+ _setRemoteClipboard = invoke(() => {
+ const setRemoteClipboard = debounce(value => {
+ this.setState({ clipboard: value })
+ this.refs.noVnc.setClipboard(value)
+ }, 200)
+ return event => setRemoteClipboard(event.target.value)
+ })
+
+ _getClipboardContent = () => this.refs.clipboard && this.refs.clipboard.value
+
+ _toggleMinimalLayout = () => {
+ this.props.toggleHeader()
+ this.setState({ minimalLayout: !this.state.minimalLayout })
+ }
+
+ render () {
+ const { statsOverview, vm } = this.props
+ const { minimalLayout, scale } = this.state
+
+ if (!isVmRunning(vm)) {
+ return (
+
+ Console is only available for running VMs.
+
+ )
+ }
+
+ return (
+
+ {!minimalLayout &&
+ statsOverview && (
+
+
+
+ {' '}
+
+
+
+
+
+ {' '}
+
+
+
+
+
+ {' '}
+
+
+
+
+
+ {' '}
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ {_('copyToClipboardLabel')}
+
+
+
+
+
+
+
+ {_('ctrlAltDelButtonLabel')}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/vm/tab-containers.js b/packages/xo-web/src/xo-app/vm/tab-containers.js
new file mode 100644
index 000000000..1c5f72511
--- /dev/null
+++ b/packages/xo-web/src/xo-app/vm/tab-containers.js
@@ -0,0 +1,130 @@
+import _ from 'intl'
+import ActionRowButton from 'action-row-button'
+import ButtonGroup from 'button-group'
+import isEmpty from 'lodash/isEmpty'
+import React, { Component } from 'react'
+import SortedTable from 'sorted-table'
+import Tooltip from 'tooltip'
+import { FormattedRelative, FormattedTime } from 'react-intl'
+import { Container, Row, Col } from 'grid'
+import {
+ startContainer,
+ stopContainer,
+ pauseContainer,
+ unpauseContainer,
+ restartContainer,
+} from 'xo'
+
+const CONTAINER_COLUMNS = [
+ {
+ name: _('containerName'),
+ itemRenderer: container => container.entry.names,
+ sortCriteria: container => container.entry.names,
+ sortOrder: 'asc',
+ },
+ {
+ name: _('containerCommand'),
+ itemRenderer: container => container.entry.command,
+ sortCriteria: container => container.entry.command,
+ },
+ {
+ name: _('containerCreated'),
+ itemRenderer: container => (
+
+ {' '}
+ ( )
+
+ ),
+ sortCriteria: container => container.entry.created,
+ sortOrder: 'desc',
+ },
+ {
+ name: _('containerStatus'),
+ itemRenderer: container => container.entry.status,
+ sortCriteria: container => container.entry.status,
+ },
+ {
+ action: _('containerAction'),
+ itemRenderer: (container, vm) => (
+
+ {container.entry.status === 'Up' && [
+
+ stopContainer(vm, container.entry.container)}
+ icon='vm-stop'
+ />
+ ,
+
+ restartContainer(vm, container.entry.container)}
+ icon='vm-reboot'
+ />
+ ,
+
+ pauseContainer(vm, container.entry.container)}
+ icon='vm-suspend'
+ />
+ ,
+ ]}
+ {container.entry.status === 'Exited (137)' && (
+
+ startContainer(vm, container.entry.container)}
+ icon='vm-start'
+ />
+
+ )}
+ {container.entry.status === 'Up (Paused)' && (
+
+ unpauseContainer(vm, container.entry.container)}
+ icon='vm-start'
+ />
+
+ )}
+
+ ),
+ },
+]
+
+export default class TabContainers extends Component {
+ render () {
+ const { vm } = this.props
+ if (isEmpty(vm.docker.containers)) {
+ return (
+
+
+ {_('noContainers')}
+
+
+ )
+ }
+
+ return (
+
+
+
+
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/vm/tab-disks.js b/packages/xo-web/src/xo-app/vm/tab-disks.js
new file mode 100644
index 000000000..787edbff2
--- /dev/null
+++ b/packages/xo-web/src/xo-app/vm/tab-disks.js
@@ -0,0 +1,713 @@
+import _, { messages } from 'intl'
+import ActionButton from 'action-button'
+import ActionRowButton from 'action-row-button'
+import Component from 'base-component'
+import HTML5Backend from 'react-dnd-html5-backend'
+import Icon from 'icon'
+import IsoDevice from 'iso-device'
+import Link from 'link'
+import propTypes from 'prop-types-decorator'
+import React from 'react'
+import SingleLineRow from 'single-line-row'
+import StateButton from 'state-button'
+import TabButton from 'tab-button'
+import Tooltip from 'tooltip'
+import { Container, Row, Col } from 'grid'
+import {
+ createSelector,
+ createFinder,
+ getCheckPermissions,
+ isAdmin,
+} from 'selectors'
+import { DragDropContext, DragSource, DropTarget } from 'react-dnd'
+import { injectIntl } from 'react-intl'
+import {
+ noop,
+ addSubscriptions,
+ formatSize,
+ connectStore,
+ resolveResourceSet,
+} from 'utils'
+import { SelectSr, SelectVdi, SelectResourceSetsSr } from 'select-objects'
+import { SizeInput, Toggle } from 'form'
+import { XoSelect, Size, Text } from 'editable'
+import { confirm } from 'modal'
+import { error } from 'notification'
+import { forEach, get, isEmpty, map, some } from 'lodash'
+import {
+ attachDiskToVm,
+ createDisk,
+ connectVbd,
+ deleteVbd,
+ deleteVdi,
+ disconnectVbd,
+ editVdi,
+ isSrWritable,
+ isVmRunning,
+ migrateVdi,
+ setBootableVbd,
+ setVmBootOrder,
+ subscribeResourceSets,
+} from 'xo'
+
+const parseBootOrder = bootOrder => {
+ // FIXME missing translation
+ const bootOptions = {
+ c: 'Hard-Drive',
+ d: 'DVD-Drive',
+ n: 'Network',
+ }
+ const order = []
+ if (bootOrder) {
+ for (const id of bootOrder) {
+ if (id in bootOptions) {
+ order.push({ id, text: bootOptions[id], active: true })
+ delete bootOptions[id]
+ }
+ }
+ }
+ forEach(bootOptions, (text, id) => {
+ order.push({ id, text, active: false })
+ })
+ return order
+}
+
+@injectIntl
+@propTypes({
+ onClose: propTypes.func,
+ vm: propTypes.object.isRequired,
+})
+@addSubscriptions({
+ resourceSets: subscribeResourceSets,
+})
+@connectStore({
+ isAdmin,
+})
+class NewDisk extends Component {
+ _createDisk = () => {
+ const { vm, onClose = noop } = this.props
+ const { bootable, name, readOnly, size, sr } = this.state
+
+ return createDisk(name, size, sr, {
+ vm,
+ bootable,
+ mode: readOnly ? 'RO' : 'RW',
+ }).then(onClose)
+ }
+
+ // FIXME: duplicate code
+ _getSrPredicate = createSelector(
+ () => {
+ const { vm } = this.props
+ return vm && vm.$pool
+ },
+ poolId => sr => sr.$pool === poolId && isSrWritable(sr)
+ )
+
+ _getResourceSet = createFinder(
+ () => this.props.resourceSets,
+ createSelector(
+ () => this.props.vm.resourceSet,
+ id => resourceSet => resourceSet.id === id
+ )
+ )
+
+ _getResolvedResourceSet = createSelector(
+ this._getResourceSet,
+ resolveResourceSet
+ )
+
+ _getResourceSetDiskLimit = createSelector(this._getResourceSet, resourceSet =>
+ get(resourceSet, 'limits.disk.available')
+ )
+
+ render () {
+ const { vm, isAdmin } = this.props
+ const { formatMessage } = this.props.intl
+ const { size, sr, name, bootable, readOnly } = this.state
+
+ const diskLimit = this._getResourceSetDiskLimit()
+ const resourceSet = this._getResolvedResourceSet()
+
+ const SelectSr_ =
+ isAdmin || resourceSet == null ? SelectSr : SelectResourceSetsSr
+
+ return (
+
+ )
+ }
+}
+
+@propTypes({
+ onClose: propTypes.func,
+ vbds: propTypes.array.isRequired,
+ vm: propTypes.object.isRequired,
+})
+class AttachDisk extends Component {
+ _getVdiPredicate = createSelector(
+ () => {
+ const { vm } = this.props
+ return vm && vm.$pool
+ },
+ poolId => vdi => vdi.$pool === poolId
+ )
+
+ // FIXME: duplicate code
+ _getSrPredicate = createSelector(
+ () => {
+ const { vm } = this.props
+ return vm && vm.$pool
+ },
+ poolId => sr => sr.$pool === poolId && isSrWritable(sr)
+ )
+
+ _selectVdi = vdi => this.setState({ vdi })
+
+ _addVdi = () => {
+ const { vm, vbds, onClose = noop } = this.props
+ const { bootable, readOnly, vdi } = this.state
+
+ const _isFreeForWriting = vdi =>
+ vdi.$VBDs.length === 0 ||
+ some(vdi.$VBDs, id => {
+ const vbd = vbds[id]
+ return !vbd || !vbd.attached || vbd.read_only
+ })
+ return attachDiskToVm(vdi, vm, {
+ bootable,
+ mode: readOnly || !_isFreeForWriting(vdi) ? 'RO' : 'RW',
+ }).then(onClose)
+ }
+
+ render () {
+ const { vm } = this.props
+ const { vdi } = this.state
+
+ return (
+
+ )
+ }
+}
+
+const orderItemSource = {
+ beginDrag: props => ({
+ id: props.id,
+ index: props.index,
+ }),
+}
+
+const orderItemTarget = {
+ hover: (props, monitor, component) => {
+ const dragIndex = monitor.getItem().index
+ const hoverIndex = props.index
+
+ if (dragIndex === hoverIndex) {
+ return
+ }
+
+ props.move(dragIndex, hoverIndex)
+ monitor.getItem().index = hoverIndex
+ },
+}
+
+@DropTarget('orderItem', orderItemTarget, connect => ({
+ connectDropTarget: connect.dropTarget(),
+}))
+@DragSource('orderItem', orderItemSource, (connect, monitor) => ({
+ connectDragSource: connect.dragSource(),
+ isDragging: monitor.isDragging(),
+}))
+@propTypes({
+ connectDragSource: propTypes.func.isRequired,
+ connectDropTarget: propTypes.func.isRequired,
+ index: propTypes.number.isRequired,
+ isDragging: propTypes.bool.isRequired,
+ id: propTypes.any.isRequired,
+ item: propTypes.object.isRequired,
+ move: propTypes.func.isRequired,
+})
+class OrderItem extends Component {
+ _toggle = checked => {
+ const { item } = this.props
+ item.active = checked
+ this.forceUpdate()
+ }
+
+ render () {
+ const { item, connectDragSource, connectDropTarget } = this.props
+ return connectDragSource(
+ connectDropTarget(
+
+ {item.text}
+
+
+
+
+ )
+ )
+ }
+}
+
+@propTypes({
+ onClose: propTypes.func,
+ vm: propTypes.object.isRequired,
+})
+@DragDropContext(HTML5Backend)
+class BootOrder extends Component {
+ constructor (props) {
+ super(props)
+ const { vm } = props
+ const order = parseBootOrder(vm.boot && vm.boot.order)
+ this.state = { order }
+ }
+
+ _moveOrderItem = (dragIndex, hoverIndex) => {
+ const order = this.state.order.slice()
+ const dragItem = order.splice(dragIndex, 1)
+ if (dragItem.length) {
+ order.splice(hoverIndex, 0, dragItem.pop())
+ this.setState({ order })
+ }
+ }
+
+ _reset = () => {
+ const { vm } = this.props
+ const order = parseBootOrder(vm.boot && vm.boot.order)
+ this.setState({ order })
+ }
+
+ _save = () => {
+ const { vm, onClose = noop } = this.props
+ const { order: newOrder } = this.state
+ let order = ''
+ forEach(newOrder, item => {
+ item.active && (order += item.id)
+ })
+ return setVmBootOrder(vm, order).then(onClose)
+ }
+
+ render () {
+ const { order } = this.state
+
+ return (
+
+ )
+ }
+}
+
+class MigrateVdiModalBody extends Component {
+ get value () {
+ return this.state
+ }
+
+ render () {
+ return (
+
+
+ {_('vdiMigrateSelectSr')}
+
+
+
+
+
+
+
+ {' '}
+ {_('vdiMigrateAll')}
+
+
+
+
+ )
+ }
+}
+
+@connectStore(() => ({
+ checkPermissions: getCheckPermissions,
+ isAdmin,
+}))
+export default class TabDisks extends Component {
+ constructor (props) {
+ super(props)
+ this.state = {
+ attachDisk: false,
+ bootOrder: false,
+ newDisk: false,
+ }
+ }
+
+ _toggleNewDisk = () =>
+ this.setState({
+ newDisk: !this.state.newDisk,
+ attachDisk: false,
+ bootOrder: false,
+ })
+
+ _toggleAttachDisk = () =>
+ this.setState({
+ attachDisk: !this.state.attachDisk,
+ bootOrder: false,
+ newDisk: false,
+ })
+
+ _toggleBootOrder = () =>
+ this.setState({
+ bootOrder: !this.state.bootOrder,
+ attachDisk: false,
+ newDisk: false,
+ })
+
+ _migrateVdi = vdi => {
+ return confirm({
+ title: _('vdiMigrate'),
+ body:
,
+ }).then(({ sr, migrateAll }) => {
+ if (!sr) {
+ return error(_('vdiMigrateNoSr'), _('vdiMigrateNoSrMessage'))
+ }
+ return migrateAll
+ ? Promise.all(map(this.props.vdis, vdi => migrateVdi(vdi, sr)))
+ : migrateVdi(vdi, sr)
+ })
+ }
+
+ _getIsVmAdmin = createSelector(
+ () => this.props.checkPermissions,
+ () => this.props.vm && this.props.vm.id,
+ (check, vmId) => check(vmId, 'administrate')
+ )
+
+ _getAttachDiskPredicate = createSelector(
+ () => this.props.isAdmin,
+ () => this.props.vm.resourceSet,
+ this._getIsVmAdmin,
+ (isAdmin, resourceSet, isVmAdmin) =>
+ isAdmin || (resourceSet == null && isVmAdmin)
+ )
+
+ render () {
+ const { srs, vbds, vdis, vm } = this.props
+
+ const { attachDisk, bootOrder, newDisk } = this.state
+
+ return (
+
+
+
+
+ {this._getAttachDiskPredicate() && (
+
+ )}
+ {vm.virtualizationMode !== 'pv' && (
+
+ )}
+
+
+
+
+ {newDisk && (
+
+
+
+
+ )}
+ {attachDisk && (
+
+ )}
+ {bootOrder && (
+
+
+
+
+ )}
+
+
+
+
+ {!isEmpty(vbds) ? (
+
+
+
+ {_('vdiNameLabel')}
+ {_('vdiNameDescription')}
+ {_('vdiSize')}
+ {_('vdiSr')}
+ {vm.virtualizationMode === 'pv' && (
+ {_('vbdBootableStatus')}
+ )}
+ {_('vbdStatus')}
+ {_('vbdAction')}
+
+
+
+ {map(vbds, vbd => {
+ const vdi = vdis[vbd.VDI]
+ if (vbd.is_cd_drive || !vdi) {
+ return
+ }
+
+ const sr = srs[vdi.$SR]
+
+ return (
+
+
+
+ editVdi(vdi, { name_label: value })
+ }
+ />
+
+
+
+ editVdi(vdi, { name_description: value })
+ }
+ />
+
+
+ editVdi(vdi, { size })}
+ />
+
+
+ {' '}
+ {sr && (
+ migrateVdi(vdi, sr)}
+ xoType='SR'
+ predicate={sr =>
+ sr.$pool === vm.$pool && isSrWritable(sr)
+ }
+ labelProp='name_label'
+ value={sr}
+ useLongClick
+ >
+ {sr.name_label}
+
+ )}
+
+ {vm.virtualizationMode === 'pv' && (
+
+
+ setBootableVbd(vbd, bootable)
+ }
+ />
+
+ )}
+
+
+
+
+
+
+
+ {!vbd.attached && (
+
+
+
+
+
+
+
+
+ )}
+
+
+ )
+ })}
+
+
+ ) : (
+ {_('vbdNoVbd')}
+ )}
+
+
+
+
+
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/vm/tab-general.js b/packages/xo-web/src/xo-app/vm/tab-general.js
new file mode 100644
index 000000000..d92271367
--- /dev/null
+++ b/packages/xo-web/src/xo-app/vm/tab-general.js
@@ -0,0 +1,199 @@
+import _ from 'intl'
+import Copiable from 'copiable'
+import defined from 'xo-defined'
+import Icon from 'icon'
+import isEmpty from 'lodash/isEmpty'
+import map from 'lodash/map'
+import React from 'react'
+import HomeTags from 'home-tags'
+import renderXoItem from 'render-xo-item'
+import Tooltip from 'tooltip'
+import { addTag, editVm, removeTag } from 'xo'
+import { BlockLink } from 'link'
+import { FormattedRelative } from 'react-intl'
+import { Container, Row, Col } from 'grid'
+import { Number, Size } from 'editable'
+import {
+ createFinder,
+ createGetObjectsOfType,
+ createGetVmLastShutdownTime,
+ createSelector,
+} from 'selectors'
+import { connectStore, formatSize, osFamily } from 'utils'
+import {
+ CpuSparkLines,
+ MemorySparkLines,
+ NetworkSparkLines,
+ XvdSparkLines,
+} from 'xo-sparklines'
+
+export default connectStore(() => {
+ const getVgpus = createGetObjectsOfType('vgpu')
+ .pick((_, { vm }) => vm.$VGPUs)
+ .sort()
+
+ const getAttachedVgpu = createFinder(getVgpus, vgpu => vgpu.currentlyAttached)
+
+ const getVgpuTypes = createGetObjectsOfType('vgpuType').pick(
+ createSelector(getVgpus, vgpus => map(vgpus, 'vgpuType'))
+ )
+
+ return {
+ lastShutdownTime: createGetVmLastShutdownTime(),
+ vgpu: getAttachedVgpu,
+ vgpuTypes: getVgpuTypes,
+ }
+})(
+ ({
+ lastShutdownTime,
+ statsOverview,
+ vgpu,
+ vgpuTypes,
+ vm,
+ vmTotalDiskSpace,
+ }) => (
+
+ {/* TODO: use CSS style */}
+
+
+
+
+ editVm(vm, { CPUs: vcpus })}
+ />x
+
+
+ {statsOverview && }
+
+
+
+
+ editVm(vm, { memory })}
+ />
+
+
+
+
+
+ {statsOverview && }
+
+
+
+
+
+ {vm.VIFs.length}x
+
+
+
+ {statsOverview && }
+
+
+
+
+
+ {formatSize(vmTotalDiskSpace)}
+
+
+
+ {statsOverview && }
+
+
+
+ {/* TODO: use CSS style */}
+
+
+
+ {vm.power_state === 'Running' ? (
+
+
+ {_('started', {
+ ago: ,
+ })}
+
+
+ ) : (
+
+ {lastShutdownTime
+ ? _('vmHaltedSince', {
+ ago: ,
+ })
+ : _('vmNotRunning')}
+
+ )}
+
+
+
+ {vm.virtualizationMode === 'pv'
+ ? _('paraVirtualizedMode')
+ : _('hardwareVirtualizedMode')}
+
+ {vgpu !== undefined && (
+ {renderXoItem(vgpuTypes[vgpu.vgpuType])}
+ )}
+
+
+
+ {vm.addresses && vm.addresses['0/ip'] ? (
+ {vm.addresses['0/ip']}
+ ) : (
+ {_('noIpv4Record')}
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+ {!vm.xenTools &&
+ vm.power_state === 'Running' && (
+
+
+
+ {_('noToolsDetected')}.
+
+
+ )}
+ {/* TODO: use CSS style */}
+
+
+
+
+ removeTag(vm.id, tag)}
+ onAdd={tag => addTag(vm.id, tag)}
+ />
+
+
+
+ {isEmpty(vm.current_operations) ? null : (
+
+
+
+ {_('vmCurrentStatus')} {map(vm.current_operations)[0]}
+
+
+
+ )}
+
+ )
+)
diff --git a/packages/xo-web/src/xo-app/vm/tab-logs.js b/packages/xo-web/src/xo-app/vm/tab-logs.js
new file mode 100644
index 000000000..8ef1086de
--- /dev/null
+++ b/packages/xo-web/src/xo-app/vm/tab-logs.js
@@ -0,0 +1,77 @@
+import _ from 'intl'
+import React, { Component } from 'react'
+import SortedTable from 'sorted-table'
+import { connectStore } from 'utils'
+import { createGetObjectMessages } from 'selectors'
+import { FormattedRelative, FormattedTime } from 'react-intl'
+import { deleteMessage, deleteMessages } from 'xo'
+
+const LOG_COLUMNS = [
+ {
+ itemRenderer: log => (
+
+ {' '}
+ ( )
+
+ ),
+ name: _('logDate'),
+ sortCriteria: 'time',
+ sortOrder: 'desc',
+ },
+ {
+ itemRenderer: log => log.name,
+ name: _('logName'),
+ sortCriteria: 'name',
+ },
+ {
+ itemRenderer: log => log.body,
+ name: _('logContent'),
+ sortCriteria: 'body',
+ },
+]
+
+const INDIVIDUAL_ACTIONS = [
+ {
+ handler: deleteMessage,
+ icon: 'delete',
+ label: _('logDelete'),
+ level: 'danger',
+ },
+]
+
+const GROUPED_ACTIONS = [
+ {
+ handler: deleteMessages,
+ icon: 'delete',
+ label: _('logsDelete'),
+ level: 'danger',
+ },
+]
+
+@connectStore(() => {
+ const logs = createGetObjectMessages((_, props) => props.vm)
+
+ return (state, props) => ({
+ logs: logs(state, props),
+ })
+})
+export default class TabLogs extends Component {
+ render () {
+ return (
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/vm/tab-network.js b/packages/xo-web/src/xo-app/vm/tab-network.js
new file mode 100644
index 000000000..d05d08e20
--- /dev/null
+++ b/packages/xo-web/src/xo-app/vm/tab-network.js
@@ -0,0 +1,562 @@
+import _, { messages } from 'intl'
+import ActionButton from 'action-button'
+import ActionRowButton from 'action-row-button'
+import BaseComponent from 'base-component'
+import Icon from 'icon'
+import propTypes from 'prop-types-decorator'
+import React from 'react'
+import SortedTable from 'sorted-table'
+import StateButton from 'state-button'
+import TabButton from 'tab-button'
+import Tooltip from 'tooltip'
+import { isIp, isIpV4 } from 'ip'
+import { Container, Row, Col } from 'grid'
+import { injectIntl } from 'react-intl'
+import { XoSelect, Text } from 'editable'
+import {
+ addSubscriptions,
+ connectStore,
+ EMPTY_ARRAY,
+ noop,
+ resolveResourceSet,
+} from 'utils'
+import {
+ SelectNetwork,
+ SelectIp,
+ SelectResourceSetIp,
+ SelectResourceSetsNetwork,
+} from 'select-objects'
+import {
+ concat,
+ every,
+ find,
+ includes,
+ isEmpty,
+ keys,
+ map,
+ remove,
+ some,
+} from 'lodash'
+
+import {
+ createFinder,
+ createGetObject,
+ createGetObjectsOfType,
+ createSelector,
+ isAdmin,
+} from 'selectors'
+
+import {
+ connectVif,
+ createVmInterface,
+ deleteVif,
+ deleteVifs,
+ disconnectVif,
+ isVmRunning,
+ setVif,
+ subscribeIpPools,
+ subscribeResourceSets,
+} from 'xo'
+
+class VifNetwork extends BaseComponent {
+ _getNetworkPredicate = createSelector(
+ () => this.props.vif.$pool,
+ vifPoolId => network => network.$pool === vifPoolId
+ )
+
+ render () {
+ const { network } = this.props
+
+ return (
+ network !== undefined && (
+
setVif(this.props.vif, { network })}
+ predicate={this._getNetworkPredicate()}
+ value={network}
+ xoType='network'
+ >
+ {network.name_label}
+
+ )
+ )
+ }
+}
+
+@addSubscriptions({
+ ipPools: subscribeIpPools,
+ resourceSets: subscribeResourceSets,
+})
+class VifAllowedIps extends BaseComponent {
+ _saveIp = (ipIndex, newIp) => {
+ if (!isIp(newIp.id)) {
+ throw new Error('Not a valid IP')
+ }
+ const vif = this.props.item
+ const { allowedIpv4Addresses, allowedIpv6Addresses } = vif
+ if (isIpV4(newIp.id)) {
+ allowedIpv4Addresses[ipIndex] = newIp.id
+ } else {
+ allowedIpv6Addresses[ipIndex - allowedIpv4Addresses.length] = newIp.id
+ }
+ setVif(vif, { allowedIpv4Addresses, allowedIpv6Addresses })
+ }
+ _addIp = ip => {
+ this._toggleNewIp()
+ if (!isIp(ip.id)) {
+ return
+ }
+ const vif = this.props.item
+ let { allowedIpv4Addresses, allowedIpv6Addresses } = vif
+ if (isIpV4(ip.id)) {
+ allowedIpv4Addresses = [...allowedIpv4Addresses, ip.id]
+ } else {
+ allowedIpv6Addresses = [...allowedIpv6Addresses, ip.id]
+ }
+ setVif(vif, { allowedIpv4Addresses, allowedIpv6Addresses })
+ }
+ _deleteIp = ipIndex => {
+ const vif = this.props.item
+ const { allowedIpv4Addresses, allowedIpv6Addresses } = vif
+ if (ipIndex < allowedIpv4Addresses.length) {
+ remove(allowedIpv4Addresses, (_, i) => i === ipIndex)
+ } else {
+ remove(
+ allowedIpv6Addresses,
+ (_, i) => i === ipIndex - allowedIpv4Addresses.length
+ )
+ }
+ setVif(vif, { allowedIpv4Addresses, allowedIpv6Addresses })
+ }
+ _getIps = createSelector(
+ () => this.props.item.allowedIpv4Addresses || EMPTY_ARRAY,
+ () => this.props.item.allowedIpv6Addresses || EMPTY_ARRAY,
+ concat
+ )
+ _getIpPredicate = createSelector(
+ this._getIps,
+ () => this.props.ipPools,
+ () => this.props.resourceSet,
+ () => this.props.resourceSets,
+ (ips, ipPools, resourceSetId, resourceSets) => {
+ return selectedIp => {
+ const isNotUsed = every(ips, vifIp => vifIp !== selectedIp.id)
+ let enoughResources
+ if (resourceSetId) {
+ const resourceSet = find(
+ resourceSets,
+ set => set.id === resourceSetId
+ )
+ const ipPool = find(ipPools, ipPool =>
+ includes(keys(ipPool.addresses), selectedIp.id)
+ )
+ const ipPoolLimits =
+ resourceSet && resourceSet.limits[`ipPool:${ipPool.id}`]
+ enoughResources =
+ resourceSet && ipPool && (!ipPoolLimits || ipPoolLimits.available)
+ }
+ return isNotUsed && (!resourceSetId || enoughResources)
+ }
+ }
+ )
+ _getIsNetworkAllowed = createSelector(
+ () => this.props.item.$network,
+ vifNetworkId => ipPool =>
+ find(ipPool.networks, ipPoolNetwork => ipPoolNetwork === vifNetworkId)
+ )
+
+ _toggleNewIp = () =>
+ this.setState({ showNewIpForm: !this.state.showNewIpForm })
+
+ render () {
+ const { showNewIpForm } = this.state
+ const { resourceSet, item: vif } = this.props
+
+ if (!vif) {
+ return null
+ }
+ return (
+
+ {isEmpty(this._getIps()) ? (
+
+
+ {_('vifNoIps')}
+
+
+ ) : (
+ map(this._getIps(), (ip, ipIndex) => (
+
+
+ this._saveIp(ipIndex, newIp)}
+ predicate={this._getIpPredicate()}
+ resourceSetId={resourceSet}
+ value={ip}
+ xoType={resourceSet ? 'resourceSetIp' : 'ip'}
+ >
+ {ip}
+
+
+
+
+
+
+ ))
+ )}
+
+
+ {showNewIpForm ? (
+
+ {resourceSet ? (
+
+ ) : (
+
+ )}
+
+ ) : (
+
+ )}
+
+
+
+ )
+ }
+}
+
+class VifStatus extends BaseComponent {
+ _getIps = createSelector(
+ () => this.props.vif.allowedIpv4Addresses || EMPTY_ARRAY,
+ () => this.props.vif.allowedIpv6Addresses || EMPTY_ARRAY,
+ concat
+ )
+
+ _getNetworkStatus = () => {
+ if (!isEmpty(this._getIps())) {
+ return (
+
+
+
+ )
+ }
+ const { network } = this.props
+ if (!network) {
+ return (
+
+
+
+ )
+ }
+ if (network.defaultIsLocked) {
+ return (
+
+
+
+ )
+ }
+ return (
+
+
+
+ )
+ }
+
+ render () {
+ const { vif } = this.props
+
+ return (
+
+ {' '}
+ {this._getNetworkStatus()}
+
+ )
+ }
+}
+
+const COLUMNS = [
+ {
+ itemRenderer: vif => `VIF #${vif.device}`,
+ name: _('vifDeviceLabel'),
+ sortCriteria: 'device',
+ },
+ {
+ itemRenderer: vif => (
+
+ setVif(vif, { mac })} />
+
+ ),
+ name: _('vifMacLabel'),
+ sortCriteria: 'MAC',
+ },
+ {
+ itemRenderer: vif => vif.MTU,
+ name: _('vifMtuLabel'),
+ sortCriteria: 'MTU',
+ },
+ {
+ itemRenderer: (vif, userData) => (
+
+ ),
+ name: _('vifNetworkLabel'),
+ sortCriteria: (vif, userData) => userData.networks[vif.$network].name_label,
+ },
+ {
+ component: VifAllowedIps,
+ name: _('vifAllowedIps'),
+ },
+ {
+ itemRenderer: (vif, userData) => (
+
+ ),
+ name: _('vifStatusLabel'),
+ },
+]
+const GROUPED_ACTIONS = [
+ {
+ disabled: selectedItems => some(selectedItems, 'attached'),
+ handler: deleteVifs,
+ icon: 'remove',
+ label: _('vifsRemove'),
+ level: 'danger',
+ },
+]
+const INDIVIDUAL_ACTIONS = [
+ {
+ disabled: vif => vif.attached,
+ handler: deleteVif,
+ icon: 'remove',
+ label: _('vifRemove'),
+ level: 'danger',
+ },
+]
+const FILTERS = {
+ filterVifsOnlyConnected: 'attached?',
+ filterVifsOnlyDisconnected: '!attached?',
+}
+
+@propTypes({
+ onClose: propTypes.func,
+ vm: propTypes.object.isRequired,
+})
+@addSubscriptions({
+ resourceSets: subscribeResourceSets,
+})
+@connectStore(() => {
+ const getHostMaster = createGetObject(
+ (_, props) => props.pool && props.pool.master
+ )
+ const getPifs = createGetObjectsOfType('PIF').pick((state, props) => {
+ const hostMaster = getHostMaster(state, props)
+ return hostMaster && hostMaster.$PIFs
+ })
+ const getDefaultNetwork = createGetObject(
+ createSelector(
+ createFinder(getPifs, [pif => pif.management]),
+ pif => pif && pif.$network
+ )
+ )
+ return {
+ defaultNetwork: getDefaultNetwork,
+ isAdmin,
+ }
+})
+@injectIntl
+class NewVif extends BaseComponent {
+ componentWillMount () {
+ this._autoFill(this.props)
+ }
+
+ componentWillReceiveProps (props) {
+ this._autoFill(props)
+ }
+
+ _autoFill = props => {
+ const { defaultNetwork } = props
+ if (defaultNetwork && !this.state.network) {
+ this.setState({
+ network: defaultNetwork,
+ })
+ }
+ }
+
+ _getNetworkPredicate = createSelector(
+ () => {
+ const { vm } = this.props
+ return vm && vm.$pool
+ },
+ poolId => network => network.$pool === poolId
+ )
+
+ _selectNetwork = network => {
+ this.setState({
+ network,
+ })
+ }
+
+ _createVif = () => {
+ const { vm, onClose = noop } = this.props
+ const { mac, network } = this.state
+ return createVmInterface(vm, network, mac).then(onClose)
+ }
+
+ _getResourceSet = createFinder(
+ () => this.props.resourceSets,
+ createSelector(
+ () => this.props.vm.resourceSet,
+ id => resourceSet => resourceSet.id === id
+ )
+ )
+
+ _getResolvedResourceSet = createSelector(
+ this._getResourceSet,
+ resolveResourceSet
+ )
+
+ render () {
+ const formatMessage = this.props.intl.formatMessage
+ const { isAdmin } = this.props
+ const { mac, network } = this.state
+ const resourceSet = this._getResolvedResourceSet()
+
+ const Select_ =
+ isAdmin || resourceSet == null ? SelectNetwork : SelectResourceSetsNetwork
+
+ return (
+
+ )
+ }
+}
+
+@connectStore(() => {
+ const getVifs = createGetObjectsOfType('VIF').pick(
+ (_, props) => props.vm.VIFs
+ )
+ const getNetworksId = createSelector(getVifs, vifs =>
+ map(vifs, vif => vif.$network)
+ )
+ const getNetworks = createGetObjectsOfType('network').pick(getNetworksId)
+
+ return (state, props) => ({
+ vifs: getVifs(state, props),
+ networks: getNetworks(state, props),
+ })
+})
+export default class TabNetwork extends BaseComponent {
+ _toggleNewVif = () =>
+ this.setState({
+ newVif: !this.state.newVif,
+ })
+
+ render () {
+ const { newVif } = this.state
+ const { pool, vm, vifs, networks } = this.props
+ return (
+
+
+
+
+
+
+ {newVif && (
+
+
+
+
+
+ )}
+
+
+
+ {!isEmpty(vm.addresses) ? (
+
+ {_('vifIpAddresses')}
+ {map(vm.addresses, (address, key) => (
+
+ {address}
+
+ ))}
+
+ ) : (
+ _('noIpRecord')
+ )}
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/vm/tab-snapshots.js b/packages/xo-web/src/xo-app/vm/tab-snapshots.js
new file mode 100644
index 000000000..da357fced
--- /dev/null
+++ b/packages/xo-web/src/xo-app/vm/tab-snapshots.js
@@ -0,0 +1,152 @@
+import _ from 'intl'
+import Icon from 'icon'
+import React, { Component } from 'react'
+import SortedTable from 'sorted-table'
+import TabButton from 'tab-button'
+import Tooltip from 'tooltip'
+import { connectStore } from 'utils'
+import { FormattedRelative, FormattedTime } from 'react-intl'
+import { Container, Row, Col } from 'grid'
+import { Text } from 'editable'
+import { includes, isEmpty } from 'lodash'
+import { createGetObjectsOfType } from 'selectors'
+import {
+ copyVm,
+ deleteSnapshot,
+ deleteSnapshots,
+ exportVm,
+ editVm,
+ revertSnapshot,
+ snapshotVm,
+} from 'xo'
+
+const COLUMNS = [
+ {
+ itemRenderer: snapshot => (
+
+ {' '}
+ ( ){' '}
+ {includes(snapshot.tags, 'quiesce') && (
+
+
+
+ )}
+
+ ),
+ default: true,
+ name: _('snapshotDate'),
+ sortCriteria: _ => _.snapshot_time,
+ sortOrder: 'desc',
+ },
+ {
+ itemRenderer: snapshot => (
+
editVm(snapshot, { name_label: value })}
+ value={snapshot.name_label}
+ />
+ ),
+ name: _('snapshotName'),
+ sortCriteria: _ => _.name_label,
+ },
+ {
+ itemRenderer: snapshot => (
+ editVm(snapshot, { name_description: value })}
+ value={snapshot.name_description}
+ />
+ ),
+ name: _('snapshotDescription'),
+ sortCriteria: _ => _.name_description,
+ },
+]
+
+const GROUPED_ACTIONS = [
+ {
+ handler: deleteSnapshots,
+ icon: 'delete',
+ label: _('deleteSnapshots'),
+ },
+]
+
+const INDIVIDUAL_ACTIONS = [
+ {
+ handler: copyVm,
+ icon: 'vm-copy',
+ label: _('copySnapshot'),
+ },
+ {
+ handler: exportVm,
+ icon: 'export',
+ label: _('exportSnapshot'),
+ },
+ {
+ handler: revertSnapshot,
+ icon: 'snapshot-revert',
+ label: _('revertSnapshot'),
+ level: 'warning',
+ },
+ {
+ handler: deleteSnapshot,
+ icon: 'delete',
+ label: _('deleteSnapshot'),
+ level: 'danger',
+ },
+]
+
+@connectStore(() => ({
+ snapshots: createGetObjectsOfType('VM-snapshot')
+ .pick((_, props) => props.vm.snapshots)
+ .sort(),
+}))
+export default class TabSnapshot extends Component {
+ render () {
+ const { snapshots, vm } = this.props
+ return (
+
+
+
+
+
+
+ {isEmpty(snapshots) ? (
+
+
+ {_('noSnapshots')}
+
+
+ {_('tipLabel')}{' '}
+ {_('tipCreateSnapshotLabel')}
+
+
+
+
+ ) : (
+
+
+
+
+
+ )}
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/vm/tab-stats.js b/packages/xo-web/src/xo-app/vm/tab-stats.js
new file mode 100644
index 000000000..de3913df4
--- /dev/null
+++ b/packages/xo-web/src/xo-app/vm/tab-stats.js
@@ -0,0 +1,187 @@
+import _, { messages } from 'intl'
+import Component from 'base-component'
+import Icon from 'icon'
+import React from 'react'
+import Tooltip from 'tooltip'
+import Upgrade from 'xoa-upgrade'
+import { fetchVmStats } from 'xo'
+import { Toggle } from 'form'
+import { injectIntl } from 'react-intl'
+import { Container, Row, Col } from 'grid'
+import {
+ CpuLineChart,
+ MemoryLineChart,
+ VifLineChart,
+ XvdLineChart,
+} from 'xo-line-chart'
+
+export default injectIntl(
+ class VmStats extends Component {
+ constructor (props) {
+ super(props)
+ this.state.useCombinedValues = false
+ }
+
+ loop (vm = this.props.vm) {
+ if (this.cancel) {
+ this.cancel()
+ }
+
+ if (vm.power_state !== 'Running') {
+ return
+ }
+
+ let cancelled = false
+ this.cancel = () => {
+ cancelled = true
+ }
+
+ fetchVmStats(vm, this.state.granularity).then(stats => {
+ if (cancelled) {
+ return
+ }
+ this.cancel = null
+
+ clearTimeout(this.timeout)
+ this.setState(
+ {
+ stats,
+ selectStatsLoading: false,
+ },
+ () => {
+ this.timeout = setTimeout(this.loop, stats.interval * 1000)
+ }
+ )
+ })
+ }
+ loop = ::this.loop
+
+ componentWillMount () {
+ this.loop()
+ }
+
+ componentWillUnmount () {
+ clearTimeout(this.timeout)
+ }
+
+ componentWillReceiveProps (props) {
+ const vmCur = this.props.vm
+ const vmNext = props.vm
+
+ if (vmCur.power_state !== 'Running' && vmNext.power_state === 'Running') {
+ this.loop(vmNext)
+ } else if (
+ vmCur.power_state === 'Running' &&
+ vmNext.power_state !== 'Running'
+ ) {
+ this.setState({
+ stats: undefined,
+ })
+ }
+ }
+
+ handleSelectStats (event) {
+ const granularity = event.target.value
+ clearTimeout(this.timeout)
+
+ this.setState(
+ {
+ granularity,
+ selectStatsLoading: true,
+ },
+ this.loop
+ )
+ }
+ handleSelectStats = ::this.handleSelectStats
+
+ render () {
+ const { intl } = this.props
+ const {
+ granularity,
+ selectStatsLoading,
+ stats,
+ useCombinedValues,
+ } = this.state
+
+ return !stats ? (
+ No stats.
+ ) : process.env.XOA_PLAN > 2 ? (
+
+
+
+
+
+
+
+
+ {selectStatsLoading && (
+
+
+
+ )}
+
+
+
+
+
+ {intl.formatMessage(messages.statLastTenMinutes)}
+
+
+ {intl.formatMessage(messages.statLastTwoHours)}
+
+
+ {intl.formatMessage(messages.statLastWeek)}
+
+
+ {intl.formatMessage(messages.statLastYear)}
+
+
+
+
+
+
+
+
+ {_('statsCpu')}
+
+
+
+
+
+ {_('statsMemory')}
+
+
+
+
+
+
+
+
+
+ {_('statsNetwork')}
+
+
+
+
+
+ {_('statDisk')}
+
+
+
+
+
+ ) : (
+
+
+
+ )
+ }
+ }
+)
diff --git a/packages/xo-web/src/xo-app/xoa/index.js b/packages/xo-web/src/xo-app/xoa/index.js
new file mode 100644
index 000000000..5c31adf2d
--- /dev/null
+++ b/packages/xo-web/src/xo-app/xoa/index.js
@@ -0,0 +1,58 @@
+import _ from 'intl'
+import Icon from 'icon'
+import Page from '../page'
+import React from 'react'
+import { routes } from 'utils'
+import { Container, Row, Col } from 'grid'
+import { NavLink, NavTabs } from 'nav'
+
+import Update from './update'
+import Licenses from './licenses'
+
+const HEADER = (
+
+
+
+
+ {_('xoaPage')}
+
+
+
+
+
+ {_('updatePage')}
+
+
+ {_('licensesPage')}
+
+
+
+
+
+)
+
+const Xoa = routes('xoa', {
+ update: Update,
+ licenses: Licenses,
+})(
+ ({ children }) =>
+ +process.env.XOA_PLAN === 5 ? (
+
+ {_('noUpdaterCommunity')}
+
+ {_('considerSubscribe', {
+ link: (
+ https://xen-orchestra.com
+ ),
+ })}
+
+ {_('noUpdaterWarning')}
+
+ ) : (
+
+ {children}
+
+ )
+)
+
+export default Xoa
diff --git a/packages/xo-web/src/xo-app/xoa/licenses/index.js b/packages/xo-web/src/xo-app/xoa/licenses/index.js
new file mode 100644
index 000000000..316bc761d
--- /dev/null
+++ b/packages/xo-web/src/xo-app/xoa/licenses/index.js
@@ -0,0 +1,234 @@
+import _ from 'intl'
+import ActionButton from 'action-button'
+import Component from 'base-component'
+import Link from 'link'
+import React from 'react'
+import renderXoItem from 'render-xo-item'
+import SortedTable from 'sorted-table'
+import { Container, Row, Col } from 'grid'
+import { createSelector, createGetObjectsOfType } from 'selectors'
+import { find, forEach } from 'lodash'
+import { addSubscriptions, connectStore, ShortDate } from 'utils'
+import { subscribePlugins, getLicenses } from 'xo'
+import { get } from 'xo-defined'
+
+import Xosan from './xosan'
+
+const openNewLicense = () => {
+ // FIXME: use link with target attribute
+ window.open('https://xen-orchestra.com/#!/member/purchaser')
+}
+
+const openSupport = () => {
+ // FIXME: use link with target attribute
+ window.open('https://xen-orchestra.com/#!/xosan-home/')
+}
+
+const PRODUCTS_COLUMNS = [
+ {
+ name: _('licenseProduct'),
+ itemRenderer: ({ product, id }) => (
+
+ {product} ({id.slice(-4)})
+
+ ),
+ sortCriteria: ({ product, id }) => product + id.slice(-4),
+ default: true,
+ },
+ {
+ name: _('licenseBoundObject'),
+ itemRenderer: ({ renderBoundObject }) =>
+ renderBoundObject !== undefined && renderBoundObject(),
+ },
+ {
+ name: _('licensePurchaser'),
+ itemRenderer: ({ buyer }, { registeredEmail }) =>
+ buyer !== undefined ? (
+ buyer.email === registeredEmail ? (
+ _('licensePurchaserYou')
+ ) : (
+ {buyer.email}
+ )
+ ) : (
+ '-'
+ ),
+ sortCriteria: 'buyer.email',
+ },
+ {
+ name: _('licenseExpires'),
+ itemRenderer: ({ expires }) =>
+ expires !== undefined ? : '-',
+ sortCriteria: 'expires',
+ sortOrder: 'desc',
+ },
+]
+
+const getBoundXosanRenderer = (boundObjectId, xosanSrs) => {
+ if (boundObjectId === undefined) {
+ return () => _('licenseNotBoundXosan')
+ }
+
+ const sr = xosanSrs[boundObjectId]
+ if (sr === undefined) {
+ return () => _('licenseBoundUnknownXosan')
+ }
+
+ return () => {renderXoItem(sr)}
+}
+
+@connectStore({
+ xosanSrs: createGetObjectsOfType('SR').filter([
+ ({ SR_type }) => SR_type === 'xosan', // eslint-disable-line camelcase
+ ]),
+ xoaRegistration: state => state.xoaRegisterState,
+})
+@addSubscriptions(() => ({
+ plugins: subscribePlugins,
+}))
+export default class Licenses extends Component {
+ constructor () {
+ super()
+
+ this.componentDidMount = this._updateLicenses
+ }
+
+ _updateLicenses = () =>
+ Promise.all([getLicenses('xosan'), getLicenses('xosan.trial')])
+ .then(([xosanLicenses, xosanTrialLicenses]) => {
+ this.setState({
+ xosanLicenses,
+ xosanTrialLicenses,
+ licenseError: undefined,
+ })
+ })
+ .catch(error => {
+ this.setState({ licenseError: error })
+ })
+
+ _getProducts = createSelector(
+ () => this.state.xosanLicenses,
+ () => this.props.xosanSrs,
+ (xosanLicenses, xosanSrs) => {
+ const products = []
+ if (get(() => xosanLicenses.state) === 'register-needed') {
+ // Should not happen
+ return
+ }
+
+ // XOSAN
+ const boundSrs = []
+ forEach(xosanLicenses, license => {
+ if (license.boundObjectId !== undefined) {
+ boundSrs.push(license.boundObjectId)
+ }
+ products.push({
+ product: 'XOSAN',
+ renderBoundObject: getBoundXosanRenderer(
+ license.boundObjectId,
+ xosanSrs
+ ),
+ buyer: license.buyer,
+ expires: license.expires,
+ id: license.id,
+ })
+ })
+
+ return products
+ }
+ )
+
+ _getMissingXoaPlugin = createSelector(
+ () => this.props.plugins,
+ plugins => {
+ if (plugins === undefined) {
+ return true
+ }
+
+ const xoaPlugin = find(plugins, { id: 'xoa' })
+ if (!xoaPlugin) {
+ return _('xosanInstallXoaPlugin')
+ }
+
+ if (!xoaPlugin.loaded) {
+ return _('xosanLoadXoaPlugin')
+ }
+ }
+ )
+
+ render () {
+ if (get(() => this.props.xoaRegistration.state) !== 'registered') {
+ return (
+
+ {_('licensesUnregisteredDisclaimer')} {' '}
+ {_('registerNow')}
+
+ )
+ }
+
+ const missingXoaPlugin = this._getMissingXoaPlugin()
+ if (missingXoaPlugin !== undefined) {
+ return {missingXoaPlugin}
+ }
+
+ if (this.state.licenseError !== undefined) {
+ return {_('xosanGetLicensesError')}
+ }
+
+ if (
+ this.state.xosanLicenses === undefined &&
+ this.state.xosanTrialLicenses === undefined
+ ) {
+ return {_('statusLoading')}
+ }
+
+ return (
+
+
+
+
+ {_('newLicense')}
+
+
+ {_('refreshLicenses')}
+
+
+
+
+
+
+
+
+
+
+
+ XOSAN
+
+ {_('productSupport')}
+
+
+
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/xoa/licenses/xosan.js b/packages/xo-web/src/xo-app/xoa/licenses/xosan.js
new file mode 100644
index 000000000..389cab6e0
--- /dev/null
+++ b/packages/xo-web/src/xo-app/xoa/licenses/xosan.js
@@ -0,0 +1,192 @@
+import _ from 'intl'
+import ActionButton from 'action-button'
+import Component from 'base-component'
+import Link from 'link'
+import React from 'react'
+import renderXoItem from 'render-xo-item'
+import SortedTable from 'sorted-table'
+import { connectStore } from 'utils'
+import { createSelector, createGetObjectsOfType, createFilter } from 'selectors'
+import { unlockXosan } from 'xo'
+import { get } from 'xo-defined'
+import { filter, forEach, includes, map } from 'lodash'
+import { injectIntl } from 'react-intl'
+
+@injectIntl
+class SelectLicense extends Component {
+ state = { license: 'none' }
+
+ render () {
+ return (
+
+ )
+ }
+}
+
+const XOSAN_COLUMNS = [
+ {
+ name: _('xosanName'),
+ itemRenderer: sr => {renderXoItem(sr)},
+ sortCriteria: 'name_label',
+ },
+ {
+ name: _('xosanPool'),
+ itemRenderer: (sr, { poolsBySr }) => {
+ const pool = poolsBySr[sr.id]
+ return {renderXoItem(pool)}
+ },
+ },
+ {
+ name: _('xosanLicense'),
+ itemRenderer: (
+ sr,
+ { availableLicenses, licensesByXosan, updateLicenses }
+ ) => {
+ const license = licensesByXosan[sr.id]
+
+ if (license === null) {
+ return (
+
+ {_('xosanMultipleLicenses')}{' '}
+ {_('contactUs')}
+
+ )
+ }
+
+ return license !== undefined ? (
+ license.id.slice(-4)
+ ) : (
+
+ unlockXosan(licenseId, sr.id).then(updateLicenses)
+ }
+ />
+ )
+ },
+ },
+]
+
+const XOSAN_INDIVIDUAL_ACTIONS = [
+ {
+ label: _('productSupport'),
+ icon: 'support',
+ handler: () => window.open('https://xen-orchestra.com'),
+ },
+]
+
+@connectStore(() => {
+ const getXosanSrs = createGetObjectsOfType('SR').filter([
+ ({ SR_type }) => SR_type === 'xosan', // eslint-disable-line camelcase
+ ])
+ const getPoolsBySr = createSelector(
+ getXosanSrs,
+ createGetObjectsOfType('pool'),
+ (srs, pools) => {
+ const poolsBySr = {}
+ forEach(srs, sr => {
+ poolsBySr[sr.id] = pools[sr.$pool]
+ })
+
+ return poolsBySr
+ }
+ )
+
+ return {
+ xosanSrs: getXosanSrs,
+ poolsBySr: getPoolsBySr,
+ }
+})
+export default class Xosan extends Component {
+ _getLicensesByXosan = createSelector(
+ () => this.props.xosanLicenses,
+ licenses => {
+ const licensesByXosan = {}
+ forEach(licenses, license => {
+ let xosanId
+ if ((xosanId = license.boundObjectId) === undefined) {
+ return
+ }
+ licensesByXosan[xosanId] =
+ licensesByXosan[xosanId] !== undefined
+ ? null // XOSAN bound to multiple licenses!
+ : license
+ })
+
+ return licensesByXosan
+ }
+ )
+
+ _getAvailableLicenses = createFilter(() => this.props.xosanLicenses, [
+ ({ boundObjectId, expires }) =>
+ boundObjectId === undefined &&
+ (expires === undefined || expires > Date.now()),
+ ])
+
+ _getKnownXosans = createSelector(
+ createSelector(
+ () => this.props.xosanLicenses,
+ () => this.props.xosanTrialLicenses,
+ (licenses = [], trialLicenses = []) =>
+ filter(map(licenses.concat(trialLicenses), 'boundObjectId'))
+ ),
+ () => this.props.xosanSrs,
+ (knownXosanIds, xosanSrs) =>
+ filter(xosanSrs, ({ id }) => includes(knownXosanIds, id))
+ )
+
+ render () {
+ return (
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/xoa/update/index.js b/packages/xo-web/src/xo-app/xoa/update/index.js
new file mode 100644
index 000000000..e14772464
--- /dev/null
+++ b/packages/xo-web/src/xo-app/xoa/update/index.js
@@ -0,0 +1,468 @@
+import _, { messages } from 'intl'
+import ActionButton from 'action-button'
+import AnsiUp from 'ansi_up'
+import Button from 'button'
+import Component from 'base-component'
+import Icon from 'icon'
+import React from 'react'
+import Tooltip from 'tooltip'
+import xoaUpdater, { exposeTrial, isTrialRunning } from 'xoa-updater'
+import { confirm } from 'modal'
+import { connectStore } from 'utils'
+import { Card, CardBlock, CardHeader } from 'card'
+import { Container, Row, Col } from 'grid'
+import { error } from 'notification'
+import { injectIntl } from 'react-intl'
+import { Password } from 'form'
+import { serverVersion } from 'xo'
+import { assign, includes, isEmpty, map } from 'lodash'
+
+import pkg from '../../../../package'
+
+const ansiUp = new AnsiUp()
+
+let updateSource
+const promptForReload = (source, force) => {
+ if (force || (updateSource && source !== updateSource)) {
+ confirm({
+ title: _('promptUpgradeReloadTitle'),
+ body: {_('promptUpgradeReloadMessage')}
,
+ }).then(() => window.location.reload())
+ }
+ updateSource = source
+}
+
+if (+process.env.XOA_PLAN < 5) {
+ xoaUpdater.start()
+ xoaUpdater.on('upgradeSuccessful', source => promptForReload(source, !source))
+ xoaUpdater.on('upToDate', promptForReload)
+}
+
+// FIXME: can't translate
+const states = {
+ disconnected: 'Disconnected',
+ updating: 'Updating',
+ upgrading: 'Upgrading',
+ upToDate: 'Up to Date',
+ upgradeNeeded: 'Upgrade required',
+ registerNeeded: 'Registration required',
+ error: 'An error occured',
+}
+
+const update = () => xoaUpdater.update()
+const upgrade = () => xoaUpdater.upgrade()
+
+@connectStore(state => {
+ return {
+ configuration: state.xoaConfiguration,
+ log: state.xoaUpdaterLog,
+ registration: state.xoaRegisterState,
+ state: state.xoaUpdaterState,
+ trial: state.xoaTrialState,
+ }
+})
+@injectIntl
+export default class XoaUpdates extends Component {
+ // These 3 inputs are "controlled" http://facebook.github.io/react/docs/forms.html#controlled-components
+ _handleProxyHostChange = event =>
+ this.setState({ proxyHost: event.target.value || '' })
+ _handleProxyPortChange = event =>
+ this.setState({ proxyPort: event.target.value || '' })
+ _handleProxyUserChange = event =>
+ this.setState({ proxyUser: event.target.value || '' })
+
+ _handleConfigReset = () => {
+ const { configuration } = this.props
+ const { proxyPassword } = this.refs
+ proxyPassword.value = ''
+ this.setState(configuration)
+ }
+
+ _register = async () => {
+ const { email, password } = this.state
+
+ const { registration } = this.props
+ const alreadyRegistered = registration.state === 'registered'
+
+ if (alreadyRegistered) {
+ try {
+ await confirm({
+ title: _('alreadyRegisteredModal'),
+ body: (
+
+ {_('alreadyRegisteredModalText', { email: registration.email })}
+
+ ),
+ })
+ } catch (error) {
+ return
+ }
+ }
+ this.setState({ askRegisterAgain: false })
+ return xoaUpdater
+ .register(email, password, alreadyRegistered)
+ .then(() => this.setState({ email: '', password: '' }))
+ }
+
+ _configure = async () => {
+ const { proxyHost, proxyPort, proxyUser } = this.state
+ const { proxyPassword } = this.refs
+ return xoaUpdater
+ .configure({
+ proxyHost,
+ proxyPort,
+ proxyUser,
+ proxyPassword: proxyPassword.value,
+ })
+ .then(config => {
+ this.setState({
+ proxyHost: undefined,
+ proxyPort: undefined,
+ proxyUser: undefined,
+ })
+ proxyPassword.value = ''
+ })
+ }
+
+ _trialAllowed = trial => trial.state === 'default' && exposeTrial(trial.trial)
+ _trialAvailable = trial =>
+ trial.state === 'default' && isTrialRunning(trial.trial)
+ _trialConsumed = trial =>
+ trial.state === 'default' &&
+ !isTrialRunning(trial.trial) &&
+ !exposeTrial(trial.trial)
+ _updaterDown = trial => isEmpty(trial) || trial.state === 'ERROR'
+ _toggleAskRegisterAgain = () =>
+ this.setState({ askRegisterAgain: !this.state.askRegisterAgain })
+
+ _startTrial = async () => {
+ try {
+ await confirm({
+ title: _('trialReadyModal'),
+ body: {_('trialReadyModalText')}
,
+ })
+ return xoaUpdater
+ .requestTrial()
+ .then(() => xoaUpdater.update())
+ .catch(err => error('Request Trial', err.message || String(err)))
+ } catch (_) {}
+ }
+
+ componentWillMount () {
+ this.setState({ askRegisterAgain: false })
+ serverVersion.then(serverVersion => {
+ this.setState({ serverVersion })
+ })
+ update()
+ }
+
+ render () {
+ const textClasses = {
+ info: 'text-info',
+ success: 'text-success',
+ warning: 'text-warning',
+ error: 'text-danger',
+ }
+
+ const { log, registration, state, trial } = this.props
+ let { configuration } = this.props // Configuration from the store
+
+ const alreadyRegistered = registration.state === 'registered'
+
+ configuration = assign({}, configuration)
+ const { proxyHost, proxyPort, proxyUser } = this.state // Edited non-saved configuration values override in view
+ let configEdited = false
+ proxyHost !== undefined &&
+ (configuration.proxyHost = proxyHost) &&
+ (configEdited = true)
+ proxyPort !== undefined &&
+ (configuration.proxyPort = proxyPort) &&
+ (configEdited = true)
+ proxyUser !== undefined &&
+ (configuration.proxyUser = proxyUser) &&
+ (configEdited = true)
+
+ const { formatMessage } = this.props.intl
+ return (
+
+
+
+
+
+ {states[state]}
+
+
+
+ {_('currentVersion')}{' '}
+ {`xo-server ${this.state.serverVersion}`} /{' '}
+ {`xo-web ${pkg.version}`}
+
+ {includes(['error', 'disconnected'], state) && (
+
+
+ {_('updaterTroubleshootingLink')}
+
+
+ )}
+
+ {_('refresh')}
+ {' '}
+
+ {_('upgrade')}
+
+
+
+ {map(log, (log, key) => (
+
+ {log.date} :{' '}
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+ {_('proxySettings')} {configEdited ? '*' : ''}
+
+
+
+
+
+
+
+
+ {_('registration')}
+
+ {registration.state}
+ {registration.email && to {registration.email} }
+ {registration.error}
+ {!alreadyRegistered || this.state.askRegisterAgain ? (
+
+ ) : (
+
+ {_('editRegistration')}
+
+ )}
+ {+process.env.XOA_PLAN === 1 && (
+
+
{_('trial')}
+ {this._trialAllowed(trial) && (
+
+ {registration.state !== 'registered' && (
+
{_('trialRegistration')}
+ )}
+ {registration.state === 'registered' && (
+
+ {_('trialStartButton')}
+
+ )}
+
+ )}
+ {this._trialAvailable(trial) && (
+
+ {_('trialAvailableUntil', {
+ date: new Date(trial.trial.end),
+ })}
+
+ )}
+ {this._trialConsumed(trial) &&
{_('trialConsumed')}
}
+
+ )}
+ {process.env.XOA_PLAN > 1 &&
+ process.env.XOA_PLAN < 5 && (
+
+ {trial.state === 'trustedTrial' &&
{trial.message}
}
+ {trial.state === 'untrustedTrial' && (
+
{trial.message}
+ )}
+
+ )}
+ {process.env.XOA_PLAN < 5 && (
+
+ {this._updaterDown(trial) && (
+
{_('trialLocked')}
+ )}
+
+ )}
+
+
+
+
+
+ )
+ }
+}
+
+const UpdateAlarm = () => (
+
+
+
+
+)
+
+const UpdateError = () => (
+
+
+
+
+)
+
+const UpdateWarning = () => (
+
+
+
+
+)
+
+const UpdateSuccess = () =>
+
+const UpdateAlert = () => (
+
+
+
+
+)
+
+const RegisterAlarm = () => (
+
+)
+
+export const UpdateTag = connectStore(state => {
+ return {
+ configuration: state.xoaConfiguration,
+ log: state.xoaUpdaterLog,
+ registration: state.xoaRegisterState,
+ state: state.xoaUpdaterState,
+ trial: state.xoaTrialState,
+ }
+})(props => {
+ const { state } = props
+ const components = {
+ disconnected: ,
+ connected: ,
+ upToDate: ,
+ upgradeNeeded: ,
+ registerNeeded: ,
+ error: ,
+ }
+ const tooltips = {
+ disconnected: _('noUpdateInfo'),
+ connected: _('waitingUpdateInfo'),
+ upToDate: _('upToDate'),
+ upgradeNeeded: _('mustUpgrade'),
+ registerNeeded: _('registerNeeded'),
+ error: _('updaterError'),
+ }
+ return {components[state]}
+})
diff --git a/packages/xo-web/src/xo-app/xosan/creation-progress.js b/packages/xo-web/src/xo-app/xosan/creation-progress.js
new file mode 100644
index 000000000..d0eba87d0
--- /dev/null
+++ b/packages/xo-web/src/xo-app/xosan/creation-progress.js
@@ -0,0 +1,128 @@
+import _ from 'intl'
+import Component from 'base-component'
+import React from 'react'
+import { Col, Row } from 'grid'
+import { createSelector } from 'selectors'
+import { addSubscriptions, createFakeProgress } from 'utils'
+import { subscribeCheckSrCurrentState } from 'xo'
+import { map, sum } from 'lodash'
+
+const ESTIMATED_DURATIONS = [
+ 10, // configuringNetwork
+ 50, // importingVm
+ 30, // copyingVms
+ 30, // configuringVms
+ 10, // configuringGluster
+ 5, // creatingSr
+ 5, // scanningSr
+]
+
+const TOTAL_ESTIMATED_DURATION = sum(ESTIMATED_DURATIONS)
+
+@addSubscriptions(props => ({
+ currentState: cb => subscribeCheckSrCurrentState(props.pool, cb),
+}))
+export default class CreationProgress extends Component {
+ constructor () {
+ super()
+
+ this.state = { intermediateProgress: 0 }
+
+ let sum = 0
+ let _sum = 0
+ this._milestones = map(ESTIMATED_DURATIONS, duration => {
+ _sum = sum
+ sum += duration
+
+ return _sum
+ })
+ }
+
+ _startNewFakeProgress = state => {
+ this._fakeProgress = createFakeProgress(ESTIMATED_DURATIONS[state])
+ this.setState({ intermediateProgress: 0 })
+ this._loopProgress()
+ }
+
+ _loopProgress = () => {
+ this.setState({ intermediateProgress: this._fakeProgress() })
+ this._loopTimeout = setTimeout(this._loopProgress, 50)
+ }
+
+ componentDidMount () {
+ const { currentState } = this.props
+
+ if (currentState && currentState.operation === 'createSr') {
+ this._startNewFakeProgress(currentState.state)
+ }
+ }
+
+ componentWillUnmount () {
+ clearTimeout(this._loopTimeout)
+ }
+
+ componentWillReceiveProps (props) {
+ const oldState = this.props.currentState
+ const newState = props.currentState
+
+ if (oldState === newState) {
+ return
+ }
+
+ clearTimeout(this._loopTimeout)
+
+ if (newState && newState.operation === 'createSr') {
+ if (oldState != null) {
+ // Step transition: set the end of the milestone to the current position so the
+ // progress bar doesn't unsmoothly jump to the actual end of the milestone
+ this._milestones[newState.state] = this._getMainProgress()
+ }
+ this._startNewFakeProgress(newState.state)
+ }
+ }
+
+ _getMainProgress = createSelector(
+ () => this.props.currentState && this.props.currentState.state,
+ () => this.state.intermediateProgress,
+ (state, intermediateProgress) => {
+ if (state == null) {
+ return null
+ }
+
+ const previousMilestone = this._milestones[state]
+ const stepLength =
+ (this._milestones[state + 1] || TOTAL_ESTIMATED_DURATION) -
+ previousMilestone
+
+ return previousMilestone + intermediateProgress * stepLength
+ }
+ )
+
+ render () {
+ const { currentState, pool } = this.props
+
+ if (currentState == null || currentState.operation !== 'createSr') {
+ return null
+ }
+
+ const { state, states } = currentState
+
+ return (
+
+
+ {_('xosanCreatingOn', { pool: pool.name_label })}
+
+
+ ({state + 1}/{states.length}) {_(`xosanState_${states[state]}`)}
+
+
+
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/xosan/graph/index.css b/packages/xo-web/src/xo-app/xosan/graph/index.css
new file mode 100644
index 000000000..35f6f3d30
--- /dev/null
+++ b/packages/xo-web/src/xo-app/xosan/graph/index.css
@@ -0,0 +1,26 @@
+.wrapper {
+ display: flex column;
+}
+
+.graphWrapper {
+ display: flex;
+}
+
+.graph {
+ margin: auto;
+ font-size: 15px;
+}
+
+.wrapper:hover .loss {
+ color: #990822;
+ font-weight: bold;
+}
+
+.legend {
+ color: #990822;
+ visibility: hidden;
+}
+
+.wrapper:hover .legend {
+ visibility: visible;
+}
diff --git a/packages/xo-web/src/xo-app/xosan/graph/index.js b/packages/xo-web/src/xo-app/xosan/graph/index.js
new file mode 100644
index 000000000..c526c66e5
--- /dev/null
+++ b/packages/xo-web/src/xo-app/xosan/graph/index.js
@@ -0,0 +1,125 @@
+import _ from 'intl'
+import Component from 'base-component'
+import Icon from 'icon'
+import propTypes from 'prop-types-decorator'
+import React from 'react'
+import { isInteger, map } from 'lodash'
+
+import styles from './index.css'
+
+const ICON_WIDTH = 38.56 // fa-2x (2em) ; font-size 15px
+
+const disk = (x, y, loss) => (
+
+
+
+)
+
+const xosan = (x, y, h) => (
+
+
+
+)
+
+const stroke = (x1, y1, x2, y2, xo = 0, yo = 0) => (
+
+)
+
+const fork = (n, x, y, w, h, nDisksLoss) => [
+ // horizontal line
+ stroke(w / (2 * n), 0, w - w / (2 * n), 0, x, y),
+ // vertical lines (and disks icons)
+ map(new Array(n), (_, i) => [
+ stroke(i * w / n + w / (2 * n), 0, i * w / n + w / (2 * n), h, x, y),
+ nDisksLoss !== undefined &&
+ disk(x + i * w / n + w / (2 * n), y + h, i >= n - nDisksLoss),
+ ]),
+]
+
+const graph = (nGroups, nPerGroup, w, h, disksLoss) => {
+ const hUnit = h / 5
+
+ return (
+
+ {xosan(w / 2, 0)}
+ {stroke(w / 2, hUnit, w / 2, 2 * hUnit)}
+ {nGroups === 1
+ ? fork(nPerGroup, 0, 2 * hUnit, w, hUnit, disksLoss)
+ : [
+ fork(nGroups, 0, 2 * hUnit, w, hUnit),
+ map(new Array(nGroups), (_, i) =>
+ fork(
+ nPerGroup,
+ i * w / nGroups,
+ 3 * hUnit,
+ w / nGroups,
+ hUnit,
+ disksLoss
+ )
+ ),
+ ]}
+
+ )
+}
+
+const disperseGraph = (nSrs, redundancy, w, h) => {
+ return graph(1, nSrs, w, h, redundancy)
+}
+
+const replicationGraph = (nSrs, redundancy, w, h) => {
+ const nGroups = nSrs / redundancy
+
+ if (!isInteger(nGroups)) {
+ return null
+ }
+
+ return graph(nGroups, redundancy, w, h, redundancy - 1)
+}
+
+@propTypes({
+ layout: propTypes.string.isRequired,
+ redundancy: propTypes.number.isRequired,
+ nSrs: propTypes.number,
+})
+export default class Graph extends Component {
+ render () {
+ const { layout, redundancy, nSrs, width, height } = this.props
+
+ return (
+
+
+
+ {layout === 'disperse'
+ ? disperseGraph(nSrs, redundancy, width, height)
+ : replicationGraph(
+ nSrs,
+ redundancy - (layout === 'replica_arbiter' ? 1 : 0),
+ width,
+ height
+ )}
+
+
+
+ {_('xosanDiskLossLegend')}
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/xosan/index.js b/packages/xo-web/src/xo-app/xosan/index.js
new file mode 100644
index 000000000..fe505c03f
--- /dev/null
+++ b/packages/xo-web/src/xo-app/xosan/index.js
@@ -0,0 +1,525 @@
+import _ from 'intl'
+import ActionButton from 'action-button'
+import Component from 'base-component'
+import Icon from 'icon'
+import Link from 'link'
+import Page from '../page'
+import React from 'react'
+import SortedTable from 'sorted-table'
+import Tooltip from 'tooltip'
+import { Container, Col, Row } from 'grid'
+import { get } from 'xo-defined'
+import {
+ every,
+ filter,
+ find,
+ flatten,
+ forEach,
+ isEmpty,
+ map,
+ mapValues,
+ some,
+} from 'lodash'
+import { createGetObjectsOfType, createSelector, isAdmin } from 'selectors'
+import {
+ addSubscriptions,
+ connectStore,
+ cowSet,
+ formatSize,
+ isXosanPack,
+ ShortDate,
+} from 'utils'
+import {
+ deleteSr,
+ getLicenses,
+ subscribePlugins,
+ subscribeResourceCatalog,
+ subscribeVolumeInfo,
+} from 'xo'
+
+import NewXosan from './new-xosan'
+import CreationProgress from './creation-progress'
+
+export const INFO_TYPES = ['heal', 'status', 'info', 'statusDetail', 'hosts']
+const EXPIRES_SOON_DELAY = 30 * 24 * 60 * 60 * 1000 // 1 month
+
+// ==================================================================
+
+const HEADER = (
+
+
+ {_('xosanTitle')}
+
+
+)
+
+// ==================================================================
+
+const XOSAN_COLUMNS = [
+ {
+ itemRenderer: (sr, { status }) => {
+ if (status === undefined || status[sr.id] === undefined) {
+ return null
+ }
+
+ const pbdsDetached = every(map(sr.pbds, 'attached'))
+ ? null
+ : _('xosanPbdsDetached')
+ const badStatus = every(status[sr.id])
+ ? null
+ : _('xosanBadStatus', {
+ badStatuses: (
+
+ {map(status, (_, status) => {status} )}
+
+ ),
+ })
+
+ if (pbdsDetached != null || badStatus != null) {
+ return (
+
+
+
+ )
+ }
+
+ return (
+
+
+
+ )
+ },
+ },
+ {
+ name: _('xosanPool'),
+ itemRenderer: sr =>
+ sr.pool == null ? null : (
+ {sr.pool.name_label}
+ ),
+ sortCriteria: sr => sr.pool && sr.pool.name_label,
+ },
+ {
+ name: _('xosanName'),
+ itemRenderer: sr => {sr.name_label},
+ sortCriteria: sr => sr.name_label,
+ },
+ {
+ name: _('xosanHosts'),
+ itemRenderer: sr => (
+
+ {map(sr.hosts, (host, i) => [
+ i ? ', ' : null,
+ {host.name_label},
+ ])}
+
+ ),
+ },
+ {
+ name: _('xosanSize'),
+ itemRenderer: sr => formatSize(sr.size),
+ sortCriteria: sr => sr.size,
+ },
+ {
+ name: _('xosanUsedSpace'),
+ itemRenderer: sr =>
+ sr.size > 0 ? (
+
+
+
+ ) : null,
+ sortCriteria: sr => sr.physical_usage * 100 / sr.size,
+ },
+ {
+ name: _('xosanLicense'),
+ itemRenderer: (sr, { isAdmin, licensesByXosan, licenseError }) => {
+ if (licenseError !== undefined) {
+ return
+ }
+
+ const license = licensesByXosan[sr.id]
+
+ // XOSAN not bound to any license, not even trial
+ if (license === undefined) {
+ return (
+
+ {_('xosanUnknownSr')}{' '}
+ {_('contactUs')}
+
+ )
+ }
+
+ // XOSAN bound to multiple licenses
+ if (license === null) {
+ return (
+
+ {_('xosanMultipleLicenses')}{' '}
+ {_('contactUs')}
+
+ )
+ }
+
+ const now = Date.now()
+ const expiresSoon = license.expires - now < EXPIRES_SOON_DELAY
+ const expired = license.expires < now
+ return license.productId === 'xosan' ? (
+
+ {license.expires === undefined ? (
+ '✔'
+ ) : expired ? (
+
+ {_('xosanLicenseHasExpired')}{' '}
+ {isAdmin && (
+ {_('xosanUpdateLicenseMessage')}
+ )}
+
+ ) : (
+
+ {_('xosanLicenseExpiresDate', {
+ date: ,
+ })}{' '}
+ {expiresSoon &&
+ isAdmin && (
+
+ {_('xosanUpdateLicenseMessage')}
+
+ )}
+
+ )}
+
+ ) : (
+
+ {_('xosanNoLicense')}{' '}
+ {_('xosanUnlockNow')}
+
+ )
+ },
+ },
+]
+
+const XOSAN_INDIVIDUAL_ACTIONS = [
+ {
+ handler: deleteSr,
+ icon: 'delete',
+ label: _('xosanDelete'),
+ level: 'danger',
+ },
+]
+
+@connectStore(() => {
+ const getHosts = createGetObjectsOfType('host')
+ const getHostsByPool = getHosts.groupBy('$pool')
+ const getPools = createGetObjectsOfType('pool')
+
+ const noPacksByPool = createSelector(getHostsByPool, hostsByPool =>
+ mapValues(
+ hostsByPool,
+ (poolHosts, poolId) =>
+ !every(poolHosts, host => some(host.supplementalPacks, isXosanPack))
+ )
+ )
+
+ const getPbdsBySr = createGetObjectsOfType('PBD').groupBy('SR')
+ const getXosanSrs = createSelector(
+ createGetObjectsOfType('SR').filter([
+ sr => sr.shared && sr.SR_type === 'xosan',
+ ]),
+ getPbdsBySr,
+ getPools,
+ getHosts,
+ (srs, pbdsBySr, pools, hosts) => {
+ return map(srs, sr => ({
+ ...sr,
+ pbds: pbdsBySr[sr.id],
+ pool: find(pools, { id: sr.$pool }),
+ hosts: map(pbdsBySr[sr.id], ({ host }) => find(hosts, ['id', host])),
+ config:
+ sr.other_config['xo:xosan_config'] &&
+ JSON.parse(sr.other_config['xo:xosan_config']),
+ }))
+ }
+ )
+
+ const getIsMasterOfflineByPool = createSelector(
+ getHostsByPool,
+ getPools,
+ (hostsByPool, pools) => {
+ const isMasterOfflineByPool = {}
+ forEach(pools, pool => {
+ const poolMaster = find(hostsByPool[pool.id], { id: pool.master })
+ isMasterOfflineByPool[pool.id] =
+ poolMaster && poolMaster.power_state !== 'Running'
+ })
+ }
+ )
+
+ // Hosts whose toolstack hasn't been restarted since XOSAN-pack installation
+ const getHostsNeedRestartByPool = createSelector(
+ getHostsByPool,
+ getPools,
+ (hostsByPool, pools) => {
+ const hostsNeedRestartByPool = {}
+ forEach(pools, pool => {
+ hostsNeedRestartByPool[pool.id] = filter(
+ hostsByPool[pool.id],
+ host =>
+ host.power_state === 'Running' &&
+ pool.xosanPackInstallationTime !== null &&
+ pool.xosanPackInstallationTime > host.agentStartTime
+ )
+ })
+
+ return hostsNeedRestartByPool
+ }
+ )
+
+ const getPoolPredicate = createSelector(getXosanSrs, srs => pool =>
+ every(srs, sr => sr.$pool !== pool.id)
+ )
+
+ return {
+ isAdmin,
+ isMasterOfflineByPool: getIsMasterOfflineByPool,
+ hostsNeedRestartByPool: getHostsNeedRestartByPool,
+ noPacksByPool,
+ poolPredicate: getPoolPredicate,
+ pools: getPools,
+ xoaRegistration: state => state.xoaRegisterState,
+ xosanSrs: getXosanSrs,
+ }
+})
+@addSubscriptions({
+ catalog: subscribeResourceCatalog,
+ plugins: subscribePlugins,
+})
+export default class Xosan extends Component {
+ componentDidMount () {
+ this._updateLicenses().then(() =>
+ this._subscribeVolumeInfo(this.props.xosanSrs)
+ )
+ }
+
+ componentWillReceiveProps ({ pools, xosanSrs }) {
+ if (xosanSrs !== this.props.xosanSrs) {
+ this.unsubscribeVolumeInfo && this.unsubscribeVolumeInfo()
+ this._subscribeVolumeInfo(xosanSrs)
+ }
+ }
+
+ componentWillUnmount () {
+ if (this.unsubscribeVolumeInfo != null) this.unsubscribeVolumeInfo()
+ }
+
+ _updateLicenses = () =>
+ Promise.all([getLicenses('xosan'), getLicenses('xosan.trial')])
+ .then(([xosanLicenses, xosanTrialLicenses]) => {
+ this.setState({
+ xosanLicenses,
+ xosanTrialLicenses,
+ })
+ })
+ .catch(error => {
+ this.setState({ licenseError: error })
+ })
+
+ _subscribeVolumeInfo = srs => {
+ const licensesByXosan = this._getLicensesByXosan()
+ const now = Date.now()
+ const canAdminXosan = sr => {
+ const license = licensesByXosan[sr.id]
+
+ return (
+ license !== undefined &&
+ (license.expires === undefined || license.expires > now)
+ )
+ }
+
+ const unsubscriptions = []
+ forEach(srs, sr => {
+ if (!canAdminXosan(sr)) {
+ return
+ }
+ forEach(INFO_TYPES, infoType =>
+ unsubscriptions.push(
+ subscribeVolumeInfo({ sr, infoType }, info =>
+ this.setState({
+ status: cowSet(this.state.status, [sr.id, infoType], info),
+ })
+ )
+ )
+ )
+ })
+ this.unsubscribeVolumeInfo = () =>
+ forEach(unsubscriptions, unsubscribe => unsubscribe())
+ }
+
+ _getLicensesByXosan = createSelector(
+ () => this.state.xosanLicenses,
+ () => this.state.xosanTrialLicenses,
+ (xosanLicenses = [], xosanTrialLicenses = []) => {
+ const licensesByXosan = {}
+ forEach(flatten([xosanLicenses, xosanTrialLicenses]), license => {
+ let xosanId
+ if ((xosanId = license.boundObjectId) === undefined) {
+ return
+ }
+ licensesByXosan[xosanId] =
+ licensesByXosan[xosanId] !== undefined
+ ? null // XOSAN bound to multiple licenses!
+ : license
+ })
+
+ return licensesByXosan
+ }
+ )
+
+ _getError = createSelector(
+ () => this.props.plugins,
+ plugins => {
+ const cloudPlugin = find(plugins, { id: 'cloud' })
+ if (!cloudPlugin) {
+ return _('xosanInstallCloudPlugin')
+ }
+
+ if (!cloudPlugin.loaded) {
+ return _('xosanLoadCloudPlugin')
+ }
+ }
+ )
+
+ _showBetaIsOver = createSelector(
+ () => this.props.catalog,
+ () => this.state.xosanLicenses,
+ () => this.state.xosanTrialLicenses,
+ () => this.state.licenseError,
+ (catalog, xosanLicenses, xosanTrialLicenses, licenseError) =>
+ licenseError === undefined &&
+ get(() => catalog._namespaces.xosan) !== undefined &&
+ isEmpty(xosanLicenses) &&
+ isEmpty(xosanTrialLicenses)
+ )
+
+ _onSrCreationStarted = () => this.setState({ showNewXosanForm: false })
+
+ render () {
+ const {
+ hostsNeedRestartByPool,
+ isAdmin,
+ noPacksByPool,
+ poolPredicate,
+ xoaRegistration,
+ xosanSrs,
+ } = this.props
+ const { licenseError } = this.state
+ const error = this._getError()
+
+ return (
+
+ {process.env.XOA_PLAN < 5 ? (
+
+ {error ? (
+
+
+ {error}
+
+
+ ) : (
+ [
+ this._showBetaIsOver() && (
+
+
+ {_('xosanBetaOverMessage')}
+
+
+ ),
+
+
+
+ {_('xosanNew')}
+
+
+
,
+
+
+ {this.state.showNewXosanForm && (
+ xoaRegistration.state) !== 'registered'
+ }
+ />
+ )}
+
+
,
+
+
+ {map(this.props.pools, pool => (
+
+ ))}
+
+
,
+ licenseError !== undefined && (
+
+
+
+ {_('xosanGetLicensesError')}
+
+
+
+ ),
+
+
+ {isEmpty(xosanSrs) ? (
+ {_('xosanNoSrs')}
+ ) : (
+
+ )}
+
+
,
+ ]
+ )}
+
+ ) : (
+
+ {_('xosanCommunity')}
+
+ {_('considerSubscribe', {
+ link: (
+
+ https://xen-orchestra.com
+
+ ),
+ })}
+
+
+ )}
+
+ )
+ }
+}
diff --git a/packages/xo-web/src/xo-app/xosan/new-xosan.js b/packages/xo-web/src/xo-app/xosan/new-xosan.js
new file mode 100644
index 000000000..0308bfceb
--- /dev/null
+++ b/packages/xo-web/src/xo-app/xosan/new-xosan.js
@@ -0,0 +1,569 @@
+import _ from 'intl'
+import ActionButton from 'action-button'
+import Component from 'base-component'
+import getEventValue from 'get-event-value'
+import Icon from 'icon'
+import Link from 'link'
+import React from 'react'
+import SingleLineRow from 'single-line-row'
+import Tooltip from 'tooltip'
+import { Container, Col, Row } from 'grid'
+import { Toggle, SizeInput } from 'form'
+import { SelectPif, SelectPool } from 'select-objects'
+import {
+ every,
+ filter,
+ find,
+ forEach,
+ groupBy,
+ isEmpty,
+ keys,
+ map,
+ pickBy,
+} from 'lodash'
+import {
+ createFilter,
+ createGetObjectsOfType,
+ createSelector,
+ createSort,
+} from 'selectors'
+import {
+ addSubscriptions,
+ compareVersions,
+ connectStore,
+ formatSize,
+ mapPlus,
+} from 'utils'
+import {
+ computeXosanPossibleOptions,
+ createXosanSR,
+ downloadAndInstallXosanPack,
+ restartHostsAgents,
+ subscribeResourceCatalog,
+} from 'xo'
+
+import Graph from './graph'
+
+const _findLatestTemplate = templates => {
+ let latestTemplate = templates[0]
+
+ forEach(templates, template => {
+ if (compareVersions(template.version, latestTemplate.version) > 0) {
+ latestTemplate = template
+ }
+ })
+
+ return latestTemplate
+}
+
+const DEFAULT_BRICKSIZE = 100 * 1024 * 1024 * 1024 // 100 GiB
+const DEFAULT_MEMORY = 2 * 1024 * 1024 * 1024 // 2 GiB
+
+@addSubscriptions({
+ catalog: subscribeResourceCatalog,
+})
+@connectStore({
+ pbds: createGetObjectsOfType('PBD'),
+ hosts: createGetObjectsOfType('host'),
+ srs: createGetObjectsOfType('SR'),
+})
+export default class NewXosan extends Component {
+ state = {
+ selectedSrs: {},
+ brickSize: DEFAULT_BRICKSIZE,
+ ipRange: '172.31.100.0',
+ memorySize: DEFAULT_MEMORY,
+ suggestion: 0,
+ }
+
+ _selectPool = pool => {
+ this.setState({
+ selectedSrs: {},
+ brickSize: DEFAULT_BRICKSIZE,
+ memorySize: DEFAULT_MEMORY,
+ pif: undefined,
+ pool,
+ })
+ }
+
+ componentDidUpdate () {
+ this._refreshSuggestions()
+ }
+
+ // Selector that doesn't return anything but updates the suggestions only if necessary
+ _refreshSuggestions = createSelector(
+ () => this.state.selectedSrs,
+ () => this.state.brickSize,
+ () => this.state.customBrickSize,
+ async (selectedSrs, brickSize, customBrickSize) => {
+ this.setState({
+ suggestion: 0,
+ suggestions: await computeXosanPossibleOptions(
+ keys(pickBy(selectedSrs)),
+ customBrickSize ? brickSize : undefined
+ ),
+ })
+ }
+ )
+
+ _getIsInPool = createSelector(
+ () => this.state.pool != null && this.state.pool.id,
+ poolId => obj => obj.$pool === poolId
+ )
+
+ _getPbdsBySr = createSelector(
+ () => this.props.pbds,
+ pbds => groupBy(filter(pbds, this._getIsInPool), 'SR')
+ )
+
+ _getHosts = createSelector(
+ () => this.props.hosts,
+ hosts => filter(hosts, this._getIsInPool)
+ )
+
+ // LVM SRs that are connected
+ _getLvmSrs = createSort(
+ createSelector(
+ createFilter(
+ () => this.props.srs,
+ createSelector(
+ this._getHosts,
+ this._getIsInPool,
+ (hosts, isInPool) => sr =>
+ isInPool(sr) &&
+ !sr.shared &&
+ sr.SR_type === 'lvm' &&
+ find(hosts, { id: sr.$container }).power_state === 'Running'
+ )
+ ),
+ this._getPbdsBySr,
+ (srs, pbdsBySr) =>
+ mapPlus(srs, (sr, push) => {
+ let pbds
+ if ((pbds = pbdsBySr[sr.id]).length) {
+ push({ ...sr, pbds })
+ }
+ })
+ ),
+ 'name_label'
+ )
+
+ _onCustomBrickSizeChange = async event => {
+ const customBrickSize = getEventValue(event)
+ this.setState({ customBrickSize })
+ }
+
+ _onBrickSizeChange = async event => {
+ const brickSize = getEventValue(event)
+ this.setState({ brickSize })
+ }
+
+ _selectSr = async (event, sr) => {
+ const selectedSrs = { ...this.state.selectedSrs }
+ selectedSrs[sr.id] = event.target.checked
+ this.setState({ selectedSrs })
+ }
+
+ _getPifPredicate = createSelector(
+ () => this.state.pool,
+ pool => pif => pif.vlan === -1 && pif.$host === (pool && pool.master)
+ )
+
+ _getNSelectedSrs = createSelector(
+ () => this.state.selectedSrs,
+ srs => filter(srs).length
+ )
+
+ _getLatestTemplate = createSelector(
+ createFilter(() => this.props.catalog && map(this.props.catalog.xosan), [
+ ({ type }) => type === 'xva',
+ ]),
+ _findLatestTemplate
+ )
+
+ _getDisableSrCheckbox = createSelector(
+ () => this.state.selectedSrs,
+ this._getLvmSrs,
+ (selectedSrs, lvmsrs) => sr =>
+ !every(
+ keys(pickBy(selectedSrs)),
+ selectedSrId =>
+ selectedSrId === sr.id ||
+ find(lvmsrs, { id: selectedSrId }).$container !== sr.$container
+ )
+ )
+
+ _getDisableCreation = createSelector(
+ () => this.state.suggestion,
+ () => this.state.suggestions,
+ () => this.state.pif,
+ this._getNSelectedSrs,
+ (suggestion, suggestions, pif, nSelectedSrs) =>
+ !suggestions ||
+ !suggestions[suggestion] ||
+ !pif ||
+ nSelectedSrs < 2 ||
+ suggestions[suggestion].availableSpace === 0
+ )
+
+ _createXosanVm = () => {
+ const params = this.state.suggestions[this.state.suggestion]
+
+ if (!params) {
+ return
+ }
+
+ createXosanSR({
+ template: this._getLatestTemplate(),
+ pif: this.state.pif,
+ vlan: this.state.vlan || 0,
+ srs: keys(pickBy(this.state.selectedSrs)),
+ glusterType: params.layout,
+ redundancy: params.redundancy,
+ brickSize: this.state.customBrickSize ? this.state.brickSize : undefined,
+ memorySize: this.state.memorySize,
+ ipRange: this.state.customIpRange ? this.state.ipRange : undefined,
+ }).then(this.props.onSrCreationFinished)
+
+ this.props.onSrCreationStarted()
+ }
+
+ render () {
+ if (process.env.XOA_PLAN === 5) {
+ return (
+
+ {_('xosanSourcesDisclaimer', {
+ link: (
+ https://xen-orchestra.com
+ ),
+ })}
+
+ )
+ }
+
+ const {
+ brickSize,
+ customBrickSize,
+ customIpRange,
+ ipRange,
+ memorySize,
+ pif,
+ pool,
+ selectedSrs,
+ suggestion,
+ suggestions,
+ useVlan,
+ vlan,
+ } = this.state
+
+ const {
+ hostsNeedRestartByPool,
+ noPacksByPool,
+ poolPredicate,
+ notRegistered,
+ } = this.props
+
+ if (notRegistered) {
+ return (
+
+ {_('xosanUnregisteredDisclaimer', {
+ link: {_('registerNow')},
+ })}
+
+ )
+ }
+
+ const lvmsrs = this._getLvmSrs()
+ const hosts = this._getHosts()
+
+ const disableSrCheckbox = this._getDisableSrCheckbox()
+ const hostsNeedRestart =
+ pool !== undefined &&
+ hostsNeedRestartByPool !== undefined &&
+ hostsNeedRestartByPool[pool.id]
+ const architecture = suggestions != null && suggestions[suggestion]
+
+ return (
+
+
+
+
+
+
+
+
+
+ {pool != null &&
+ noPacksByPool[pool.id] && (
+
+ {_('xosanNeedPack')}
+
+
+ {_('xosanInstallIt')}
+
+
+ )}
+ {!isEmpty(hostsNeedRestart) && (
+
+ {_('xosanNeedRestart')}
+
+
+ {_('xosanRestartAgents')}
+
+
+ )}
+ {pool != null &&
+ !noPacksByPool[pool.id] &&
+ isEmpty(hostsNeedRestart) && [
+
+ {_('xosanSelect2Srs')}
+
+
,
+
+ {!isEmpty(suggestions) && (
+
+
{_('xosanSuggestions')}
+
+ {architecture.layout === 'disperse' && (
+
+ )}
+
+
+
{' '}
+ {_('xosanAdvanced')}{' '}
+ {this.state.showAdvanced && (
+
+
+ {_('xosanVlan')}
+
+
+
+
+
+
+
+
+
+
+ {_('xosanCustomIpNetwork')}
+
+
+
+
+
+
+
+
+
+
+ {_('xosanBrickSize')}
+
+
+
+
+
+
+
+
+
+
+
+ {_('xosanMemorySize')}
+
+
+
+
+ )}
+
+
+ )}
+
,
+
+
+
+ {_('xosanCreate')}
+
+
+
,
+ ]}
+
+
+ )
+ }
+}
diff --git a/packages/xo-web/tools/run-benchmarks.js b/packages/xo-web/tools/run-benchmarks.js
new file mode 100755
index 000000000..dac32be72
--- /dev/null
+++ b/packages/xo-web/tools/run-benchmarks.js
@@ -0,0 +1,50 @@
+#!/usr/bin/env node
+
+require('babel-register')
+
+const Benchmark = require('benchmark')
+const globby = require('globby')
+const resolve = require('path').resolve
+
+// ===================================================================
+
+function bench (path) {
+ let fn = require(resolve(path))
+ if (typeof fn !== 'function') {
+ fn = fn.default
+ }
+
+ const benchmarks = []
+ function benchmark (name, fn) {
+ benchmarks.push(new Benchmark(name, fn))
+ }
+
+ fn({
+ benchmark: benchmark,
+ })
+
+ benchmarks.forEach(function (benchmark) {
+ console.log(String(benchmark.run()))
+ })
+}
+
+function main (args) {
+ if (!args.length) {
+ throw new Error('missing path patterns')
+ }
+
+ return globby(args).then(function (paths) {
+ if (!paths.length) {
+ throw new Error('no files to run')
+ }
+
+ for (let i = 0, n = paths.length; i < n; ++i) {
+ bench(paths[i])
+ }
+ })
+}
+new Promise(function (resolve) {
+ resolve(main(process.argv.slice(2)))
+}).catch(function (error) {
+ console.log((error != null && (error.stack || error.message)) || error)
+})
diff --git a/packages/xo-web/tools/update-locales b/packages/xo-web/tools/update-locales
new file mode 100755
index 000000000..1d2de35b7
--- /dev/null
+++ b/packages/xo-web/tools/update-locales
@@ -0,0 +1,110 @@
+#!/usr/bin/env node
+
+var filter = require('lodash/filter')
+var forEach = require('lodash/forEach')
+var join = require('path').join
+var map = require('lodash/map')
+var readDir = require('fs').readdirSync
+var writeFile = require('fs').writeFileSync
+
+// Necessary to load messages and locales files which are not in
+// ES5.
+//
+// Note: must be done after all other requires to avoid impacting too
+// much the perfs.
+require('babel-register')
+
+// ===================================================================
+
+function defaultRequire (module) {
+ var value = require(module)
+ return value && value.__esModule ? value.default : value
+}
+
+// See https://github.com/joliss/js-string-escape/blob/master/index.js
+function quote (string, q) {
+ string = string.split('')
+
+ if (q === undefined) {
+ q = string.indexOf("'") !== -1 && string.indexOf('"') === -1
+ ? '"'
+ : "'"
+ }
+
+ var replacements = {
+ '\\': '\\\\',
+ '\n': '\\n',
+ '\r': '\\r',
+ '\u2028': '\\u2028',
+ '\u2029': '\\u2029'
+ }
+ replacements[q] = '\\' + q
+
+ forEach(string, function (c, i) {
+ if (c in replacements) {
+ string[i] = replacements[c]
+ }
+ })
+
+ return q + string.join('') + q
+}
+
+// ===================================================================
+
+var LOCALES_DIR = join(__dirname, '../src/common/intl/locales/')
+var LOCALE_RE = /^(.+)\.js(?:on)?$/
+var messages = defaultRequire('../src/common/intl/messages')
+
+function updateLocale (locale) {
+ var path = join(LOCALES_DIR, locale + '.js')
+
+ try {
+ var translations = defaultRequire(path)
+ var flags = 'w'
+ } catch (error) {
+ if (error.code !== 'MODULE_NOT_FOUND') {
+ console.error('failed to load ' + locale, error)
+ return
+ }
+ flags = 'wx'
+ }
+
+ var content = [
+ '// See http://momentjs.com/docs/#/use-it/browserify/',
+ 'import \'moment/locale/' + locale + '\'',
+ '',
+ 'import reactIntlData from \'react-intl/locale-data/' + locale + '\'',
+ 'import { addLocaleData } from \'react-intl\'',
+ 'addLocaleData(reactIntlData)',
+ '',
+ '// ===================================================================',
+ '',
+ 'export default {'
+ ]
+
+ content.push(map(messages, function (message, id) {
+ var translation = translations && translations[id]
+ return [
+ ' // Original text: ', quote(message.defaultMessage, translation && '"'), '\n',
+ ' ', id, ': ', translation ? quote(translation) : 'undefined'
+ ].join('')
+ }).join(',\n\n'))
+
+ content.push('}', '')
+
+ writeFile(path, content.join('\n'), { flag: flags })
+}
+
+// ===================================================================
+
+(function main (args) {
+ var locales = args.length
+ ? args
+ : filter(map(readDir(LOCALES_DIR), function (entry) {
+ var matches = LOCALE_RE.exec(entry)
+ return matches && matches[1]
+ }))
+
+ forEach(locales, updateLocale)
+})(process.argv.slice(2))
+
diff --git a/packages/xo-web/yarn.lock b/packages/xo-web/yarn.lock
new file mode 100644
index 000000000..30d100a9a
--- /dev/null
+++ b/packages/xo-web/yarn.lock
@@ -0,0 +1,9094 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@babel/code-frame@7.0.0-beta.36":
+ version "7.0.0-beta.36"
+ resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0-beta.36.tgz#2349d7ec04b3a06945ae173280ef8579b63728e4"
+ dependencies:
+ chalk "^2.0.0"
+ esutils "^2.0.2"
+ js-tokens "^3.0.0"
+
+"@babel/code-frame@^7.0.0-beta.35":
+ version "7.0.0-beta.39"
+ resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0-beta.39.tgz#91c90bb65207fc5a55128cb54956ded39e850457"
+ dependencies:
+ chalk "^2.0.0"
+ esutils "^2.0.2"
+ js-tokens "^3.0.0"
+
+"@babel/helper-function-name@7.0.0-beta.36":
+ version "7.0.0-beta.36"
+ resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.0.0-beta.36.tgz#366e3bc35147721b69009f803907c4d53212e88d"
+ dependencies:
+ "@babel/helper-get-function-arity" "7.0.0-beta.36"
+ "@babel/template" "7.0.0-beta.36"
+ "@babel/types" "7.0.0-beta.36"
+
+"@babel/helper-get-function-arity@7.0.0-beta.36":
+ version "7.0.0-beta.36"
+ resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0-beta.36.tgz#f5383bac9a96b274828b10d98900e84ee43e32b8"
+ dependencies:
+ "@babel/types" "7.0.0-beta.36"
+
+"@babel/template@7.0.0-beta.36":
+ version "7.0.0-beta.36"
+ resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.0.0-beta.36.tgz#02e903de5d68bd7899bce3c5b5447e59529abb00"
+ dependencies:
+ "@babel/code-frame" "7.0.0-beta.36"
+ "@babel/types" "7.0.0-beta.36"
+ babylon "7.0.0-beta.36"
+ lodash "^4.2.0"
+
+"@babel/traverse@7.0.0-beta.36":
+ version "7.0.0-beta.36"
+ resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.0.0-beta.36.tgz#1dc6f8750e89b6b979de5fe44aa993b1a2192261"
+ dependencies:
+ "@babel/code-frame" "7.0.0-beta.36"
+ "@babel/helper-function-name" "7.0.0-beta.36"
+ "@babel/types" "7.0.0-beta.36"
+ babylon "7.0.0-beta.36"
+ debug "^3.0.1"
+ globals "^11.1.0"
+ invariant "^2.2.0"
+ lodash "^4.2.0"
+
+"@babel/types@7.0.0-beta.36":
+ version "7.0.0-beta.36"
+ resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.0.0-beta.36.tgz#64f2004353de42adb72f9ebb4665fc35b5499d23"
+ dependencies:
+ esutils "^2.0.2"
+ lodash "^4.2.0"
+ to-fast-properties "^2.0.0"
+
+"@browserify/acorn5-object-spread@^5.0.1":
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/@browserify/acorn5-object-spread/-/acorn5-object-spread-5.0.1.tgz#92e9b37f97beac9ec429a3cc479ded380297540c"
+ dependencies:
+ acorn "^5.2.1"
+
+"@gulp-sourcemaps/identity-map@1.X":
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/@gulp-sourcemaps/identity-map/-/identity-map-1.0.1.tgz#cfa23bc5840f9104ce32a65e74db7e7a974bbee1"
+ dependencies:
+ acorn "^5.0.3"
+ css "^2.2.1"
+ normalize-path "^2.1.1"
+ source-map "^0.5.6"
+ through2 "^2.0.3"
+
+"@gulp-sourcemaps/map-sources@1.X":
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/@gulp-sourcemaps/map-sources/-/map-sources-1.0.0.tgz#890ae7c5d8c877f6d384860215ace9d7ec945bda"
+ dependencies:
+ normalize-path "^2.0.1"
+ through2 "^2.0.3"
+
+"@nraynaud/novnc@0.6.1":
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/@nraynaud/novnc/-/novnc-0.6.1.tgz#995459bb6f7bd5dd9bd7899b021f68f7744cf1b3"
+ dependencies:
+ pako "^1.0.3"
+
+"@types/node@*":
+ version "9.4.0"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-9.4.0.tgz#b85a0bcf1e1cc84eb4901b7e96966aedc6f078d1"
+
+JSONStream@^1.0.3:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.2.tgz#c102371b6ec3a7cf3b847ca00c20bb0fce4c6dea"
+ dependencies:
+ jsonparse "^1.2.0"
+ through ">=2.2.7 <3"
+
+abab@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.4.tgz#5faad9c2c07f60dd76770f71cf025b62a63cfd4e"
+
+abbrev@1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
+
+acorn-globals@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-3.1.0.tgz#fd8270f71fbb4996b004fa880ee5d46573a731bf"
+ dependencies:
+ acorn "^4.0.4"
+
+acorn-globals@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.1.0.tgz#ab716025dbe17c54d3ef81d32ece2b2d99fe2538"
+ dependencies:
+ acorn "^5.0.0"
+
+acorn-jsx@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b"
+ dependencies:
+ acorn "^3.0.4"
+
+acorn@5.X, acorn@^5.0.0, acorn@^5.0.3, acorn@^5.2.1, acorn@^5.3.0:
+ version "5.3.0"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.3.0.tgz#7446d39459c54fb49a80e6ee6478149b940ec822"
+
+acorn@^3.0.4, acorn@^3.1.0, acorn@~3.3.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
+
+acorn@^4.0.3, acorn@^4.0.4, acorn@~4.0.2:
+ version "4.0.13"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787"
+
+ajv-keywords@^2.1.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.1.tgz#617997fc5f60576894c435f940d819e135b80762"
+
+ajv@^4.9.1:
+ version "4.11.8"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536"
+ dependencies:
+ co "^4.6.0"
+ json-stable-stringify "^1.0.1"
+
+ajv@^5.1.0, ajv@^5.2.3, ajv@^5.3.0:
+ version "5.5.2"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965"
+ dependencies:
+ co "^4.6.0"
+ fast-deep-equal "^1.0.0"
+ fast-json-stable-stringify "^2.0.0"
+ json-schema-traverse "^0.3.0"
+
+align-text@^0.1.1, align-text@^0.1.3:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117"
+ dependencies:
+ kind-of "^3.0.2"
+ longest "^1.0.1"
+ repeat-string "^1.5.2"
+
+amdefine@>=0.0.4:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5"
+
+ansi-colors@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-1.0.1.tgz#e94c6c306005af8b482240241e2f3dea4b855ff3"
+ dependencies:
+ ansi-wrap "^0.1.0"
+
+ansi-cyan@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/ansi-cyan/-/ansi-cyan-0.1.1.tgz#538ae528af8982f28ae30d86f2f17456d2609873"
+ dependencies:
+ ansi-wrap "0.1.0"
+
+ansi-escapes@^1.0.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e"
+
+ansi-escapes@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.0.0.tgz#ec3e8b4e9f8064fc02c3ac9b65f1c275bda8ef92"
+
+ansi-gray@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/ansi-gray/-/ansi-gray-0.1.1.tgz#2962cf54ec9792c48510a3deb524436861ef7251"
+ dependencies:
+ ansi-wrap "0.1.0"
+
+ansi-red@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/ansi-red/-/ansi-red-0.1.1.tgz#8c638f9d1080800a353c9c28c8a81ca4705d946c"
+ dependencies:
+ ansi-wrap "0.1.0"
+
+ansi-regex@^0.2.0, ansi-regex@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-0.2.1.tgz#0d8e946967a3d8143f93e24e298525fc1b2235f9"
+
+ansi-regex@^2.0.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
+
+ansi-regex@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
+
+ansi-styles@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.1.0.tgz#eaecbf66cd706882760b2f4691582b8f55d7a7de"
+
+ansi-styles@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe"
+
+ansi-styles@^3.1.0, ansi-styles@^3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88"
+ dependencies:
+ color-convert "^1.9.0"
+
+ansi-wrap@0.1.0, ansi-wrap@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf"
+
+ansi_up@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/ansi_up/-/ansi_up-2.0.2.tgz#9b54de508c5c579f5b6968e65c1b863e0680ab92"
+
+any-observable@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/any-observable/-/any-observable-0.2.0.tgz#c67870058003579009083f54ac0abafb5c33d242"
+
+anymatch@^1.3.0:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a"
+ dependencies:
+ micromatch "^2.1.5"
+ normalize-path "^2.0.0"
+
+anymatch@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb"
+ dependencies:
+ micromatch "^3.1.4"
+ normalize-path "^2.1.1"
+
+app-root-path@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-2.0.1.tgz#cd62dcf8e4fd5a417efc664d2e5b10653c651b46"
+
+append-buffer@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/append-buffer/-/append-buffer-1.0.2.tgz#d8220cf466081525efea50614f3de6514dfa58f1"
+ dependencies:
+ buffer-equal "^1.0.0"
+
+append-transform@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-0.4.0.tgz#d76ebf8ca94d276e247a36bad44a4b74ab611991"
+ dependencies:
+ default-require-extensions "^1.0.0"
+
+aproba@^1.0.3:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
+
+archy@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40"
+
+are-we-there-yet@~1.1.2:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz#bb5dca382bb94f05e15194373d16fd3ba1ca110d"
+ dependencies:
+ delegates "^1.0.0"
+ readable-stream "^2.0.6"
+
+argparse@^1.0.7:
+ version "1.0.9"
+ resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86"
+ dependencies:
+ sprintf-js "~1.0.2"
+
+arr-diff@^1.0.1:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-1.1.0.tgz#687c32758163588fef7de7b36fabe495eb1a399a"
+ dependencies:
+ arr-flatten "^1.0.1"
+ array-slice "^0.2.3"
+
+arr-diff@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf"
+ dependencies:
+ arr-flatten "^1.0.1"
+
+arr-diff@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
+
+arr-filter@^1.1.1:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/arr-filter/-/arr-filter-1.1.2.tgz#43fdddd091e8ef11aa4c45d9cdc18e2dff1711ee"
+ dependencies:
+ make-iterator "^1.0.0"
+
+arr-flatten@^1.0.1, arr-flatten@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1"
+
+arr-map@^2.0.0, arr-map@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/arr-map/-/arr-map-2.0.2.tgz#3a77345ffc1cf35e2a91825601f9e58f2e24cac4"
+ dependencies:
+ make-iterator "^1.0.0"
+
+arr-union@^2.0.1:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-2.1.0.tgz#20f9eab5ec70f5c7d215b1077b1c39161d292c7d"
+
+arr-union@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4"
+
+array-differ@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-1.0.0.tgz#eff52e3758249d33be402b8bb8e564bb2b5d4031"
+
+array-each@^1.0.0, array-each@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/array-each/-/array-each-1.0.1.tgz#a794af0c05ab1752846ee753a1f211a05ba0c44f"
+
+array-equal@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93"
+
+array-filter@~0.0.0:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-0.0.1.tgz#7da8cf2e26628ed732803581fd21f67cacd2eeec"
+
+array-find-index@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1"
+
+array-includes@^3.0.3:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.0.3.tgz#184b48f62d92d7452bb31b323165c7f8bd02266d"
+ dependencies:
+ define-properties "^1.1.2"
+ es-abstract "^1.7.0"
+
+array-initial@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/array-initial/-/array-initial-1.1.0.tgz#2fa74b26739371c3947bd7a7adc73be334b3d795"
+ dependencies:
+ array-slice "^1.0.0"
+ is-number "^4.0.0"
+
+array-last@^1.1.1:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/array-last/-/array-last-1.3.0.tgz#7aa77073fec565ddab2493f5f88185f404a9d336"
+ dependencies:
+ is-number "^4.0.0"
+
+array-map@~0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/array-map/-/array-map-0.0.0.tgz#88a2bab73d1cf7bcd5c1b118a003f66f665fa662"
+
+array-reduce@~0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/array-reduce/-/array-reduce-0.0.0.tgz#173899d3ffd1c7d9383e4479525dbe278cab5f2b"
+
+array-slice@^0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/array-slice/-/array-slice-0.2.3.tgz#dd3cfb80ed7973a75117cdac69b0b99ec86186f5"
+
+array-slice@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/array-slice/-/array-slice-1.1.0.tgz#e368ea15f89bc7069f7ffb89aec3a6c7d4ac22d4"
+
+array-sort@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/array-sort/-/array-sort-1.0.0.tgz#e4c05356453f56f53512a7d1d6123f2c54c0a88a"
+ dependencies:
+ default-compare "^1.0.0"
+ get-value "^2.0.6"
+ kind-of "^5.0.2"
+
+array-union@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39"
+ dependencies:
+ array-uniq "^1.0.1"
+
+array-uniq@^1.0.1, array-uniq@^1.0.2:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6"
+
+array-unique@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53"
+
+array-unique@^0.3.2:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
+
+arrify@^1.0.0, arrify@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
+
+asap@^2.0.6, asap@~2.0.3:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
+
+asn1.js@^4.0.0:
+ version "4.9.2"
+ resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.9.2.tgz#8117ef4f7ed87cd8f89044b5bff97ac243a16c9a"
+ dependencies:
+ bn.js "^4.0.0"
+ inherits "^2.0.1"
+ minimalistic-assert "^1.0.0"
+
+asn1@~0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86"
+
+assert-plus@1.0.0, assert-plus@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
+
+assert-plus@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234"
+
+assert@^1.4.0:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91"
+ dependencies:
+ util "0.10.3"
+
+assign-symbols@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
+
+astral-regex@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9"
+
+astw@^2.0.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/astw/-/astw-2.2.0.tgz#7bd41784d32493987aeb239b6b4e1c57a873b917"
+ dependencies:
+ acorn "^4.0.3"
+
+async-done@^1.2.0, async-done@^1.2.2:
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/async-done/-/async-done-1.2.4.tgz#17b0fcefb9a33cb9de63daa8904c0a65bd535fa0"
+ dependencies:
+ end-of-stream "^1.1.0"
+ once "^1.3.2"
+ process-nextick-args "^1.0.7"
+ stream-exhaust "^1.0.1"
+
+async-each@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d"
+
+async-foreach@^0.1.3:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542"
+
+async-limiter@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8"
+
+async-settle@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/async-settle/-/async-settle-1.0.0.tgz#1d0a914bb02575bec8a8f3a74e5080f72b2c0c6b"
+ dependencies:
+ async-done "^1.2.2"
+
+async@^1.4.0:
+ version "1.5.2"
+ resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
+
+async@^2.1.4:
+ version "2.6.0"
+ resolved "https://registry.yarnpkg.com/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4"
+ dependencies:
+ lodash "^4.14.0"
+
+asynckit@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
+
+atob@^2.0.0:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/atob/-/atob-2.0.3.tgz#19c7a760473774468f20b2d2d03372ad7d4cbf5d"
+
+atob@~1.1.0:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/atob/-/atob-1.1.3.tgz#95f13629b12c3a51a5d215abdce2aa9f32f80773"
+
+attr-accept@^1.0.3:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-1.1.0.tgz#b5cd35227f163935a8f1de10ed3eba16941f6be6"
+
+autoprefixer@^7.0.0:
+ version "7.2.5"
+ resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-7.2.5.tgz#04ccbd0c6a61131b6d13f53d371926092952d192"
+ dependencies:
+ browserslist "^2.11.1"
+ caniuse-lite "^1.0.30000791"
+ normalize-range "^0.1.2"
+ num2fraction "^1.2.2"
+ postcss "^6.0.16"
+ postcss-value-parser "^3.2.3"
+
+aws-sign2@~0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f"
+
+aws-sign2@~0.7.0:
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
+
+aws4@^1.2.1, aws4@^1.6.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e"
+
+babel-code-frame@^6.22.0, babel-code-frame@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
+ dependencies:
+ chalk "^1.1.3"
+ esutils "^2.0.2"
+ js-tokens "^3.0.2"
+
+babel-core@^6.0.0, babel-core@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.0.tgz#af32f78b31a6fcef119c87b0fd8d9753f03a0bb8"
+ dependencies:
+ babel-code-frame "^6.26.0"
+ babel-generator "^6.26.0"
+ babel-helpers "^6.24.1"
+ babel-messages "^6.23.0"
+ babel-register "^6.26.0"
+ babel-runtime "^6.26.0"
+ babel-template "^6.26.0"
+ babel-traverse "^6.26.0"
+ babel-types "^6.26.0"
+ babylon "^6.18.0"
+ convert-source-map "^1.5.0"
+ debug "^2.6.8"
+ json5 "^0.5.1"
+ lodash "^4.17.4"
+ minimatch "^3.0.4"
+ path-is-absolute "^1.0.1"
+ private "^0.1.7"
+ slash "^1.0.0"
+ source-map "^0.5.6"
+
+babel-eslint@^8.1.2:
+ version "8.2.1"
+ resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-8.2.1.tgz#136888f3c109edc65376c23ebf494f36a3e03951"
+ dependencies:
+ "@babel/code-frame" "7.0.0-beta.36"
+ "@babel/traverse" "7.0.0-beta.36"
+ "@babel/types" "7.0.0-beta.36"
+ babylon "7.0.0-beta.36"
+ eslint-scope "~3.7.1"
+ eslint-visitor-keys "^1.0.0"
+
+babel-generator@^6.18.0, babel-generator@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.0.tgz#ac1ae20070b79f6e3ca1d3269613053774f20dc5"
+ dependencies:
+ babel-messages "^6.23.0"
+ babel-runtime "^6.26.0"
+ babel-types "^6.26.0"
+ detect-indent "^4.0.0"
+ jsesc "^1.3.0"
+ lodash "^4.17.4"
+ source-map "^0.5.6"
+ trim-right "^1.0.1"
+
+babel-helper-bindify-decorators@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-bindify-decorators/-/babel-helper-bindify-decorators-6.24.1.tgz#14c19e5f142d7b47f19a52431e52b1ccbc40a330"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-helper-builder-binary-assignment-operator-visitor@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz#cce4517ada356f4220bcae8a02c2b346f9a56664"
+ dependencies:
+ babel-helper-explode-assignable-expression "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-helper-builder-react-jsx@^6.24.1:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-helper-builder-react-jsx/-/babel-helper-builder-react-jsx-6.26.0.tgz#39ff8313b75c8b65dceff1f31d383e0ff2a408a0"
+ dependencies:
+ babel-runtime "^6.26.0"
+ babel-types "^6.26.0"
+ esutils "^2.0.2"
+
+babel-helper-call-delegate@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz#ece6aacddc76e41c3461f88bfc575bd0daa2df8d"
+ dependencies:
+ babel-helper-hoist-variables "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-helper-define-map@^6.24.1:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz#a5f56dab41a25f97ecb498c7ebaca9819f95be5f"
+ dependencies:
+ babel-helper-function-name "^6.24.1"
+ babel-runtime "^6.26.0"
+ babel-types "^6.26.0"
+ lodash "^4.17.4"
+
+babel-helper-explode-assignable-expression@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz#f25b82cf7dc10433c55f70592d5746400ac22caa"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-helper-explode-class@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-explode-class/-/babel-helper-explode-class-6.24.1.tgz#7dc2a3910dee007056e1e31d640ced3d54eaa9eb"
+ dependencies:
+ babel-helper-bindify-decorators "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-helper-function-name@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz#d3475b8c03ed98242a25b48351ab18399d3580a9"
+ dependencies:
+ babel-helper-get-function-arity "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-helper-get-function-arity@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz#8f7782aa93407c41d3aa50908f89b031b1b6853d"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-helper-hoist-variables@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz#1ecb27689c9d25513eadbc9914a73f5408be7a76"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-helper-module-imports@^7.0.0-beta.3:
+ version "7.0.0-beta.3"
+ resolved "https://registry.yarnpkg.com/babel-helper-module-imports/-/babel-helper-module-imports-7.0.0-beta.3.tgz#e15764e3af9c8e11810c09f78f498a2bdc71585a"
+ dependencies:
+ babel-types "7.0.0-beta.3"
+ lodash "^4.2.0"
+
+babel-helper-optimise-call-expression@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz#f7a13427ba9f73f8f4fa993c54a97882d1244257"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-helper-regex@^6.24.1:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz#325c59f902f82f24b74faceed0363954f6495e72"
+ dependencies:
+ babel-runtime "^6.26.0"
+ babel-types "^6.26.0"
+ lodash "^4.17.4"
+
+babel-helper-remap-async-to-generator@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz#5ec581827ad723fecdd381f1c928390676e4551b"
+ dependencies:
+ babel-helper-function-name "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-helper-replace-supers@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz#bf6dbfe43938d17369a213ca8a8bf74b6a90ab1a"
+ dependencies:
+ babel-helper-optimise-call-expression "^6.24.1"
+ babel-messages "^6.23.0"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-helpers@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.24.1.tgz#3471de9caec388e5c850e597e58a26ddf37602b2"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
+babel-jest@^22.1.0:
+ version "22.1.0"
+ resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-22.1.0.tgz#7fae6f655fffe77e818a8c2868c754a42463fdfd"
+ dependencies:
+ babel-plugin-istanbul "^4.1.5"
+ babel-preset-jest "^22.1.0"
+
+babel-messages@^6.23.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-check-es2015-constants@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz#35157b101426fd2ffd3da3f75c7d1e91835bbf8a"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-dev@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-dev/-/babel-plugin-dev-1.0.0.tgz#6b1baeadc1740fc894082a450d297e49469dd0de"
+
+babel-plugin-istanbul@^4.1.5:
+ version "4.1.5"
+ resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.5.tgz#6760cdd977f411d3e175bb064f2bc327d99b2b6e"
+ dependencies:
+ find-up "^2.1.0"
+ istanbul-lib-instrument "^1.7.5"
+ test-exclude "^4.1.1"
+
+babel-plugin-jest-hoist@^22.1.0:
+ version "22.1.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-22.1.0.tgz#c1281dd7887d77a1711dc760468c3b8285dde9ee"
+
+babel-plugin-lodash@^3.2.11:
+ version "3.3.2"
+ resolved "https://registry.yarnpkg.com/babel-plugin-lodash/-/babel-plugin-lodash-3.3.2.tgz#da3a5b49ba27447f54463f6c4fa81396ccdd463f"
+ dependencies:
+ babel-helper-module-imports "^7.0.0-beta.3"
+ babel-types "^6.26.0"
+ glob "^7.1.1"
+ lodash "^4.17.4"
+ require-package-name "^2.0.1"
+
+babel-plugin-syntax-async-functions@^6.8.0:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz#cad9cad1191b5ad634bf30ae0872391e0647be95"
+
+babel-plugin-syntax-async-generators@^6.5.0:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-generators/-/babel-plugin-syntax-async-generators-6.13.0.tgz#6bc963ebb16eccbae6b92b596eb7f35c342a8b9a"
+
+babel-plugin-syntax-class-constructor-call@^6.18.0:
+ version "6.18.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-class-constructor-call/-/babel-plugin-syntax-class-constructor-call-6.18.0.tgz#9cb9d39fe43c8600bec8146456ddcbd4e1a76416"
+
+babel-plugin-syntax-class-properties@^6.8.0:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz#d7eb23b79a317f8543962c505b827c7d6cac27de"
+
+babel-plugin-syntax-decorators@^6.1.18, babel-plugin-syntax-decorators@^6.13.0:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-decorators/-/babel-plugin-syntax-decorators-6.13.0.tgz#312563b4dbde3cc806cee3e416cceeaddd11ac0b"
+
+babel-plugin-syntax-do-expressions@^6.8.0:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-do-expressions/-/babel-plugin-syntax-do-expressions-6.13.0.tgz#5747756139aa26d390d09410b03744ba07e4796d"
+
+babel-plugin-syntax-dynamic-import@^6.18.0:
+ version "6.18.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz#8d6a26229c83745a9982a441051572caa179b1da"
+
+babel-plugin-syntax-exponentiation-operator@^6.8.0:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz#9ee7e8337290da95288201a6a57f4170317830de"
+
+babel-plugin-syntax-export-extensions@^6.8.0:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-export-extensions/-/babel-plugin-syntax-export-extensions-6.13.0.tgz#70a1484f0f9089a4e84ad44bac353c95b9b12721"
+
+babel-plugin-syntax-flow@^6.18.0:
+ version "6.18.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz#4c3ab20a2af26aa20cd25995c398c4eb70310c8d"
+
+babel-plugin-syntax-function-bind@^6.8.0:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-function-bind/-/babel-plugin-syntax-function-bind-6.13.0.tgz#48c495f177bdf31a981e732f55adc0bdd2601f46"
+
+babel-plugin-syntax-jsx@^6.3.13, babel-plugin-syntax-jsx@^6.8.0:
+ version "6.18.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946"
+
+babel-plugin-syntax-object-rest-spread@^6.13.0, babel-plugin-syntax-object-rest-spread@^6.8.0:
+ version "6.13.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5"
+
+babel-plugin-syntax-trailing-function-commas@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz#ba0360937f8d06e40180a43fe0d5616fff532cf3"
+
+babel-plugin-transform-async-generator-functions@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-generator-functions/-/babel-plugin-transform-async-generator-functions-6.24.1.tgz#f058900145fd3e9907a6ddf28da59f215258a5db"
+ dependencies:
+ babel-helper-remap-async-to-generator "^6.24.1"
+ babel-plugin-syntax-async-generators "^6.5.0"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-async-to-generator@^6.22.0, babel-plugin-transform-async-to-generator@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz#6536e378aff6cb1d5517ac0e40eb3e9fc8d08761"
+ dependencies:
+ babel-helper-remap-async-to-generator "^6.24.1"
+ babel-plugin-syntax-async-functions "^6.8.0"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-class-constructor-call@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-class-constructor-call/-/babel-plugin-transform-class-constructor-call-6.24.1.tgz#80dc285505ac067dcb8d6c65e2f6f11ab7765ef9"
+ dependencies:
+ babel-plugin-syntax-class-constructor-call "^6.18.0"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
+babel-plugin-transform-class-properties@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz#6a79763ea61d33d36f37b611aa9def81a81b46ac"
+ dependencies:
+ babel-helper-function-name "^6.24.1"
+ babel-plugin-syntax-class-properties "^6.8.0"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
+babel-plugin-transform-decorators-legacy@^1.3.4:
+ version "1.3.4"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-decorators-legacy/-/babel-plugin-transform-decorators-legacy-1.3.4.tgz#741b58f6c5bce9e6027e0882d9c994f04f366925"
+ dependencies:
+ babel-plugin-syntax-decorators "^6.1.18"
+ babel-runtime "^6.2.0"
+ babel-template "^6.3.0"
+
+babel-plugin-transform-decorators@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-decorators/-/babel-plugin-transform-decorators-6.24.1.tgz#788013d8f8c6b5222bdf7b344390dfd77569e24d"
+ dependencies:
+ babel-helper-explode-class "^6.24.1"
+ babel-plugin-syntax-decorators "^6.13.0"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-do-expressions@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-do-expressions/-/babel-plugin-transform-do-expressions-6.22.0.tgz#28ccaf92812d949c2cd1281f690c8fdc468ae9bb"
+ dependencies:
+ babel-plugin-syntax-do-expressions "^6.8.0"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-arrow-functions@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz#452692cb711d5f79dc7f85e440ce41b9f244d221"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-block-scoped-functions@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz#bbc51b49f964d70cb8d8e0b94e820246ce3a6141"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-block-scoping@^6.23.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz#d70f5299c1308d05c12f463813b0a09e73b1895f"
+ dependencies:
+ babel-runtime "^6.26.0"
+ babel-template "^6.26.0"
+ babel-traverse "^6.26.0"
+ babel-types "^6.26.0"
+ lodash "^4.17.4"
+
+babel-plugin-transform-es2015-classes@^6.23.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz#5a4c58a50c9c9461e564b4b2a3bfabc97a2584db"
+ dependencies:
+ babel-helper-define-map "^6.24.1"
+ babel-helper-function-name "^6.24.1"
+ babel-helper-optimise-call-expression "^6.24.1"
+ babel-helper-replace-supers "^6.24.1"
+ babel-messages "^6.23.0"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-computed-properties@^6.22.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz#6fe2a8d16895d5634f4cd999b6d3480a308159b3"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
+babel-plugin-transform-es2015-destructuring@^6.23.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz#997bb1f1ab967f682d2b0876fe358d60e765c56d"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-duplicate-keys@^6.22.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz#73eb3d310ca969e3ef9ec91c53741a6f1576423e"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-for-of@^6.23.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz#f47c95b2b613df1d3ecc2fdb7573623c75248691"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-function-name@^6.22.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz#834c89853bc36b1af0f3a4c5dbaa94fd8eacaa8b"
+ dependencies:
+ babel-helper-function-name "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-literals@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz#4f54a02d6cd66cf915280019a31d31925377ca2e"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-modules-amd@^6.22.0, babel-plugin-transform-es2015-modules-amd@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz#3b3e54017239842d6d19c3011c4bd2f00a00d154"
+ dependencies:
+ babel-plugin-transform-es2015-modules-commonjs "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
+babel-plugin-transform-es2015-modules-commonjs@^6.23.0, babel-plugin-transform-es2015-modules-commonjs@^6.24.1:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.0.tgz#0d8394029b7dc6abe1a97ef181e00758dd2e5d8a"
+ dependencies:
+ babel-plugin-transform-strict-mode "^6.24.1"
+ babel-runtime "^6.26.0"
+ babel-template "^6.26.0"
+ babel-types "^6.26.0"
+
+babel-plugin-transform-es2015-modules-systemjs@^6.23.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz#ff89a142b9119a906195f5f106ecf305d9407d23"
+ dependencies:
+ babel-helper-hoist-variables "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
+babel-plugin-transform-es2015-modules-umd@^6.23.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz#ac997e6285cd18ed6176adb607d602344ad38468"
+ dependencies:
+ babel-plugin-transform-es2015-modules-amd "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+
+babel-plugin-transform-es2015-object-super@^6.22.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz#24cef69ae21cb83a7f8603dad021f572eb278f8d"
+ dependencies:
+ babel-helper-replace-supers "^6.24.1"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-parameters@^6.23.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz#57ac351ab49caf14a97cd13b09f66fdf0a625f2b"
+ dependencies:
+ babel-helper-call-delegate "^6.24.1"
+ babel-helper-get-function-arity "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-template "^6.24.1"
+ babel-traverse "^6.24.1"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-shorthand-properties@^6.22.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz#24f875d6721c87661bbd99a4622e51f14de38aa0"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-spread@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz#d6d68a99f89aedc4536c81a542e8dd9f1746f8d1"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-sticky-regex@^6.22.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz#00c1cdb1aca71112cdf0cf6126c2ed6b457ccdbc"
+ dependencies:
+ babel-helper-regex "^6.24.1"
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-plugin-transform-es2015-template-literals@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz#a84b3450f7e9f8f1f6839d6d687da84bb1236d8d"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-typeof-symbol@^6.23.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz#dec09f1cddff94b52ac73d505c84df59dcceb372"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-es2015-unicode-regex@^6.22.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz#d38b12f42ea7323f729387f18a7c5ae1faeb35e9"
+ dependencies:
+ babel-helper-regex "^6.24.1"
+ babel-runtime "^6.22.0"
+ regexpu-core "^2.0.0"
+
+babel-plugin-transform-exponentiation-operator@^6.22.0, babel-plugin-transform-exponentiation-operator@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz#2ab0c9c7f3098fa48907772bb813fe41e8de3a0e"
+ dependencies:
+ babel-helper-builder-binary-assignment-operator-visitor "^6.24.1"
+ babel-plugin-syntax-exponentiation-operator "^6.8.0"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-export-extensions@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-export-extensions/-/babel-plugin-transform-export-extensions-6.22.0.tgz#53738b47e75e8218589eea946cbbd39109bbe653"
+ dependencies:
+ babel-plugin-syntax-export-extensions "^6.8.0"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-flow-strip-types@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-flow-strip-types/-/babel-plugin-transform-flow-strip-types-6.22.0.tgz#84cb672935d43714fdc32bce84568d87441cf7cf"
+ dependencies:
+ babel-plugin-syntax-flow "^6.18.0"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-function-bind@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-function-bind/-/babel-plugin-transform-function-bind-6.22.0.tgz#c6fb8e96ac296a310b8cf8ea401462407ddf6a97"
+ dependencies:
+ babel-plugin-syntax-function-bind "^6.8.0"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-object-rest-spread@^6.22.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.26.0.tgz#0f36692d50fef6b7e2d4b3ac1478137a963b7b06"
+ dependencies:
+ babel-plugin-syntax-object-rest-spread "^6.8.0"
+ babel-runtime "^6.26.0"
+
+babel-plugin-transform-react-constant-elements@^6.5.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-constant-elements/-/babel-plugin-transform-react-constant-elements-6.23.0.tgz#2f119bf4d2cdd45eb9baaae574053c604f6147dd"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-react-display-name@^6.23.0:
+ version "6.25.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-display-name/-/babel-plugin-transform-react-display-name-6.25.0.tgz#67e2bf1f1e9c93ab08db96792e05392bf2cc28d1"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-react-inline-elements@^6.6.5:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-inline-elements/-/babel-plugin-transform-react-inline-elements-6.22.0.tgz#6687211a32b49a52f22c573a2b5504a25ef17c53"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-react-jsx-self@^6.11.0, babel-plugin-transform-react-jsx-self@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-jsx-self/-/babel-plugin-transform-react-jsx-self-6.22.0.tgz#df6d80a9da2612a121e6ddd7558bcbecf06e636e"
+ dependencies:
+ babel-plugin-syntax-jsx "^6.8.0"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-react-jsx-source@^6.22.0, babel-plugin-transform-react-jsx-source@^6.9.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-jsx-source/-/babel-plugin-transform-react-jsx-source-6.22.0.tgz#66ac12153f5cd2d17b3c19268f4bf0197f44ecd6"
+ dependencies:
+ babel-plugin-syntax-jsx "^6.8.0"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-react-jsx@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-jsx/-/babel-plugin-transform-react-jsx-6.24.1.tgz#840a028e7df460dfc3a2d29f0c0d91f6376e66a3"
+ dependencies:
+ babel-helper-builder-react-jsx "^6.24.1"
+ babel-plugin-syntax-jsx "^6.8.0"
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-regenerator@^6.22.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz#e0703696fbde27f0a3efcacf8b4dca2f7b3a8f2f"
+ dependencies:
+ regenerator-transform "^0.10.0"
+
+babel-plugin-transform-runtime@^6.6.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-runtime/-/babel-plugin-transform-runtime-6.23.0.tgz#88490d446502ea9b8e7efb0fe09ec4d99479b1ee"
+ dependencies:
+ babel-runtime "^6.22.0"
+
+babel-plugin-transform-strict-mode@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz#d5faf7aa578a65bbe591cf5edae04a0c67020758"
+ dependencies:
+ babel-runtime "^6.22.0"
+ babel-types "^6.24.1"
+
+babel-preset-env@^1.6.1:
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/babel-preset-env/-/babel-preset-env-1.6.1.tgz#a18b564cc9b9afdf4aae57ae3c1b0d99188e6f48"
+ dependencies:
+ babel-plugin-check-es2015-constants "^6.22.0"
+ babel-plugin-syntax-trailing-function-commas "^6.22.0"
+ babel-plugin-transform-async-to-generator "^6.22.0"
+ babel-plugin-transform-es2015-arrow-functions "^6.22.0"
+ babel-plugin-transform-es2015-block-scoped-functions "^6.22.0"
+ babel-plugin-transform-es2015-block-scoping "^6.23.0"
+ babel-plugin-transform-es2015-classes "^6.23.0"
+ babel-plugin-transform-es2015-computed-properties "^6.22.0"
+ babel-plugin-transform-es2015-destructuring "^6.23.0"
+ babel-plugin-transform-es2015-duplicate-keys "^6.22.0"
+ babel-plugin-transform-es2015-for-of "^6.23.0"
+ babel-plugin-transform-es2015-function-name "^6.22.0"
+ babel-plugin-transform-es2015-literals "^6.22.0"
+ babel-plugin-transform-es2015-modules-amd "^6.22.0"
+ babel-plugin-transform-es2015-modules-commonjs "^6.23.0"
+ babel-plugin-transform-es2015-modules-systemjs "^6.23.0"
+ babel-plugin-transform-es2015-modules-umd "^6.23.0"
+ babel-plugin-transform-es2015-object-super "^6.22.0"
+ babel-plugin-transform-es2015-parameters "^6.23.0"
+ babel-plugin-transform-es2015-shorthand-properties "^6.22.0"
+ babel-plugin-transform-es2015-spread "^6.22.0"
+ babel-plugin-transform-es2015-sticky-regex "^6.22.0"
+ babel-plugin-transform-es2015-template-literals "^6.22.0"
+ babel-plugin-transform-es2015-typeof-symbol "^6.23.0"
+ babel-plugin-transform-es2015-unicode-regex "^6.22.0"
+ babel-plugin-transform-exponentiation-operator "^6.22.0"
+ babel-plugin-transform-regenerator "^6.22.0"
+ browserslist "^2.1.2"
+ invariant "^2.2.2"
+ semver "^5.3.0"
+
+babel-preset-flow@^6.23.0:
+ version "6.23.0"
+ resolved "https://registry.yarnpkg.com/babel-preset-flow/-/babel-preset-flow-6.23.0.tgz#e71218887085ae9a24b5be4169affb599816c49d"
+ dependencies:
+ babel-plugin-transform-flow-strip-types "^6.22.0"
+
+babel-preset-jest@^22.1.0:
+ version "22.1.0"
+ resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-22.1.0.tgz#ff4e704102f9642765e2254226050561d8942ec9"
+ dependencies:
+ babel-plugin-jest-hoist "^22.1.0"
+ babel-plugin-syntax-object-rest-spread "^6.13.0"
+
+babel-preset-react@^6.5.0:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-preset-react/-/babel-preset-react-6.24.1.tgz#ba69dfaea45fc3ec639b6a4ecea6e17702c91380"
+ dependencies:
+ babel-plugin-syntax-jsx "^6.3.13"
+ babel-plugin-transform-react-display-name "^6.23.0"
+ babel-plugin-transform-react-jsx "^6.24.1"
+ babel-plugin-transform-react-jsx-self "^6.22.0"
+ babel-plugin-transform-react-jsx-source "^6.22.0"
+ babel-preset-flow "^6.23.0"
+
+babel-preset-stage-0@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-preset-stage-0/-/babel-preset-stage-0-6.24.1.tgz#5642d15042f91384d7e5af8bc88b1db95b039e6a"
+ dependencies:
+ babel-plugin-transform-do-expressions "^6.22.0"
+ babel-plugin-transform-function-bind "^6.22.0"
+ babel-preset-stage-1 "^6.24.1"
+
+babel-preset-stage-1@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-preset-stage-1/-/babel-preset-stage-1-6.24.1.tgz#7692cd7dcd6849907e6ae4a0a85589cfb9e2bfb0"
+ dependencies:
+ babel-plugin-transform-class-constructor-call "^6.24.1"
+ babel-plugin-transform-export-extensions "^6.22.0"
+ babel-preset-stage-2 "^6.24.1"
+
+babel-preset-stage-2@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-preset-stage-2/-/babel-preset-stage-2-6.24.1.tgz#d9e2960fb3d71187f0e64eec62bc07767219bdc1"
+ dependencies:
+ babel-plugin-syntax-dynamic-import "^6.18.0"
+ babel-plugin-transform-class-properties "^6.24.1"
+ babel-plugin-transform-decorators "^6.24.1"
+ babel-preset-stage-3 "^6.24.1"
+
+babel-preset-stage-3@^6.24.1:
+ version "6.24.1"
+ resolved "https://registry.yarnpkg.com/babel-preset-stage-3/-/babel-preset-stage-3-6.24.1.tgz#836ada0a9e7a7fa37cb138fb9326f87934a48395"
+ dependencies:
+ babel-plugin-syntax-trailing-function-commas "^6.22.0"
+ babel-plugin-transform-async-generator-functions "^6.24.1"
+ babel-plugin-transform-async-to-generator "^6.24.1"
+ babel-plugin-transform-exponentiation-operator "^6.24.1"
+ babel-plugin-transform-object-rest-spread "^6.22.0"
+
+babel-register@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.26.0.tgz#6ed021173e2fcb486d7acb45c6009a856f647071"
+ dependencies:
+ babel-core "^6.26.0"
+ babel-runtime "^6.26.0"
+ core-js "^2.5.0"
+ home-or-tmp "^2.0.0"
+ lodash "^4.17.4"
+ mkdirp "^0.5.1"
+ source-map-support "^0.4.15"
+
+babel-runtime@^5.8.25:
+ version "5.8.38"
+ resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-5.8.38.tgz#1c0b02eb63312f5f087ff20450827b425c9d4c19"
+ dependencies:
+ core-js "^1.0.0"
+
+babel-runtime@^6.18.0, babel-runtime@^6.2.0, babel-runtime@^6.22.0, babel-runtime@^6.23.0, babel-runtime@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
+ dependencies:
+ core-js "^2.4.0"
+ regenerator-runtime "^0.11.0"
+
+babel-template@^6.16.0, babel-template@^6.24.1, babel-template@^6.26.0, babel-template@^6.3.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02"
+ dependencies:
+ babel-runtime "^6.26.0"
+ babel-traverse "^6.26.0"
+ babel-types "^6.26.0"
+ babylon "^6.18.0"
+ lodash "^4.17.4"
+
+babel-traverse@^6.18.0, babel-traverse@^6.24.1, babel-traverse@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee"
+ dependencies:
+ babel-code-frame "^6.26.0"
+ babel-messages "^6.23.0"
+ babel-runtime "^6.26.0"
+ babel-types "^6.26.0"
+ babylon "^6.18.0"
+ debug "^2.6.8"
+ globals "^9.18.0"
+ invariant "^2.2.2"
+ lodash "^4.17.4"
+
+babel-types@7.0.0-beta.3:
+ version "7.0.0-beta.3"
+ resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-7.0.0-beta.3.tgz#cd927ca70e0ae8ab05f4aab83778cfb3e6eb20b4"
+ dependencies:
+ esutils "^2.0.2"
+ lodash "^4.2.0"
+ to-fast-properties "^2.0.0"
+
+babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.24.1, babel-types@^6.26.0:
+ version "6.26.0"
+ resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497"
+ dependencies:
+ babel-runtime "^6.26.0"
+ esutils "^2.0.2"
+ lodash "^4.17.4"
+ to-fast-properties "^1.0.3"
+
+babelify@^8.0.0:
+ version "8.0.0"
+ resolved "https://registry.yarnpkg.com/babelify/-/babelify-8.0.0.tgz#6f60f5f062bfe7695754ef2403b842014a580ed3"
+
+babylon@7.0.0-beta.36:
+ version "7.0.0-beta.36"
+ resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.36.tgz#3a3683ba6a9a1e02b0aa507c8e63435e39305b9e"
+
+babylon@^6.18.0:
+ version "6.18.0"
+ resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3"
+
+bach@^1.0.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/bach/-/bach-1.2.0.tgz#4b3ce96bf27134f79a1b414a51c14e34c3bd9880"
+ dependencies:
+ arr-filter "^1.1.1"
+ arr-flatten "^1.0.1"
+ arr-map "^2.0.0"
+ array-each "^1.0.0"
+ array-initial "^1.0.0"
+ array-last "^1.1.1"
+ async-done "^1.2.2"
+ async-settle "^1.0.0"
+ now-and-later "^2.0.0"
+
+balanced-match@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
+
+base64-js@^1.0.2:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.1.tgz#a91947da1f4a516ea38e5b4ec0ec3773675e0886"
+
+base@^0.11.1:
+ version "0.11.2"
+ resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f"
+ dependencies:
+ cache-base "^1.0.1"
+ class-utils "^0.3.5"
+ component-emitter "^1.2.1"
+ define-property "^1.0.0"
+ isobject "^3.0.1"
+ mixin-deep "^1.2.0"
+ pascalcase "^0.1.1"
+
+bcrypt-pbkdf@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d"
+ dependencies:
+ tweetnacl "^0.14.3"
+
+beeper@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/beeper/-/beeper-1.1.1.tgz#e6d5ea8c5dad001304a70b22638447f69cb2f809"
+
+benchmark@^2.1.0:
+ version "2.1.4"
+ resolved "https://registry.yarnpkg.com/benchmark/-/benchmark-2.1.4.tgz#09f3de31c916425d498cc2ee565a0ebf3c2a5629"
+ dependencies:
+ lodash "^4.17.4"
+ platform "^1.3.3"
+
+binary-extensions@^1.0.0:
+ version "1.11.0"
+ resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.11.0.tgz#46aa1751fb6a2f93ee5e689bb1087d4b14c6c205"
+
+bl@^1.0.0:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.1.tgz#cac328f7bee45730d404b692203fcb590e172d5e"
+ dependencies:
+ readable-stream "^2.0.5"
+
+block-stream@*:
+ version "0.0.9"
+ resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a"
+ dependencies:
+ inherits "~2.0.0"
+
+bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0:
+ version "4.11.8"
+ resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
+
+body-parser@~1.14.0:
+ version "1.14.2"
+ resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.14.2.tgz#1015cb1fe2c443858259581db53332f8d0cf50f9"
+ dependencies:
+ bytes "2.2.0"
+ content-type "~1.0.1"
+ debug "~2.2.0"
+ depd "~1.1.0"
+ http-errors "~1.3.1"
+ iconv-lite "0.4.13"
+ on-finished "~2.3.0"
+ qs "5.2.0"
+ raw-body "~2.1.5"
+ type-is "~1.6.10"
+
+boolbase@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
+
+boom@2.x.x:
+ version "2.10.1"
+ resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f"
+ dependencies:
+ hoek "2.x.x"
+
+boom@4.x.x:
+ version "4.3.1"
+ resolved "https://registry.yarnpkg.com/boom/-/boom-4.3.1.tgz#4f8a3005cb4a7e3889f749030fd25b96e01d2e31"
+ dependencies:
+ hoek "4.x.x"
+
+boom@5.x.x:
+ version "5.2.0"
+ resolved "https://registry.yarnpkg.com/boom/-/boom-5.2.0.tgz#5dd9da6ee3a5f302077436290cb717d3f4a54e02"
+ dependencies:
+ hoek "4.x.x"
+
+bootstrap@4.0.0-alpha.5:
+ version "4.0.0-alpha.5"
+ resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.0.0-alpha.5.tgz#a126b648c3bd2f52b8fad4bbc5e2d0ad2abf7064"
+ dependencies:
+ jquery "1.9.1 - 3"
+ tether "^1.3.7"
+
+brace-expansion@^1.1.7:
+ version "1.1.8"
+ resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292"
+ dependencies:
+ balanced-match "^1.0.0"
+ concat-map "0.0.1"
+
+braces@^1.8.2:
+ version "1.8.5"
+ resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7"
+ dependencies:
+ expand-range "^1.8.1"
+ preserve "^0.2.0"
+ repeat-element "^1.1.2"
+
+braces@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.0.tgz#a46941cb5fb492156b3d6a656e06c35364e3e66e"
+ dependencies:
+ arr-flatten "^1.1.0"
+ array-unique "^0.3.2"
+ define-property "^1.0.0"
+ extend-shallow "^2.0.1"
+ fill-range "^4.0.0"
+ isobject "^3.0.1"
+ repeat-element "^1.1.2"
+ snapdragon "^0.8.1"
+ snapdragon-node "^2.0.1"
+ split-string "^3.0.2"
+ to-regex "^3.0.1"
+
+brorand@^1.0.1:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
+
+browser-pack@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/browser-pack/-/browser-pack-5.0.1.tgz#4197719b20c6e0aaa09451c5111e53efb6fbc18d"
+ dependencies:
+ JSONStream "^1.0.3"
+ combine-source-map "~0.6.1"
+ defined "^1.0.0"
+ through2 "^1.0.0"
+ umd "^3.0.0"
+
+browser-pack@^6.0.1:
+ version "6.0.3"
+ resolved "https://registry.yarnpkg.com/browser-pack/-/browser-pack-6.0.3.tgz#91ca96518583ef580ab063a309de62e407767a39"
+ dependencies:
+ JSONStream "^1.0.3"
+ combine-source-map "~0.8.0"
+ defined "^1.0.0"
+ safe-buffer "^5.1.1"
+ through2 "^2.0.0"
+ umd "^3.0.0"
+
+browser-process-hrtime@^0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-0.1.2.tgz#425d68a58d3447f02a04aa894187fce8af8b7b8e"
+
+browser-resolve@^1.11.0, browser-resolve@^1.11.2, browser-resolve@^1.7.0:
+ version "1.11.2"
+ resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-1.11.2.tgz#8ff09b0a2c421718a1051c260b32e48f442938ce"
+ dependencies:
+ resolve "1.1.7"
+
+browser-unpack@^1.1.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/browser-unpack/-/browser-unpack-1.2.0.tgz#357aee31fc467831684d063e4355e070a782970d"
+ dependencies:
+ acorn "^4.0.3"
+ browser-pack "^5.0.1"
+ concat-stream "^1.5.0"
+ minimist "^1.1.1"
+
+browserify-aes@^1.0.0, browserify-aes@^1.0.4:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.1.1.tgz#38b7ab55edb806ff2dcda1a7f1620773a477c49f"
+ dependencies:
+ buffer-xor "^1.0.3"
+ cipher-base "^1.0.0"
+ create-hash "^1.1.0"
+ evp_bytestokey "^1.0.3"
+ inherits "^2.0.1"
+ safe-buffer "^5.0.1"
+
+browserify-cipher@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.0.tgz#9988244874bf5ed4e28da95666dcd66ac8fc363a"
+ dependencies:
+ browserify-aes "^1.0.4"
+ browserify-des "^1.0.0"
+ evp_bytestokey "^1.0.0"
+
+browserify-des@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.0.tgz#daa277717470922ed2fe18594118a175439721dd"
+ dependencies:
+ cipher-base "^1.0.1"
+ des.js "^1.0.0"
+ inherits "^2.0.1"
+
+browserify-rsa@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524"
+ dependencies:
+ bn.js "^4.1.0"
+ randombytes "^2.0.1"
+
+browserify-sign@^4.0.0:
+ version "4.0.4"
+ resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.0.4.tgz#aa4eb68e5d7b658baa6bf6a57e630cbd7a93d298"
+ dependencies:
+ bn.js "^4.1.1"
+ browserify-rsa "^4.0.0"
+ create-hash "^1.1.0"
+ create-hmac "^1.1.2"
+ elliptic "^6.0.0"
+ inherits "^2.0.1"
+ parse-asn1 "^5.0.0"
+
+browserify-zlib@~0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f"
+ dependencies:
+ pako "~1.0.5"
+
+browserify@^15.1.0, browserify@^15.2.0:
+ version "15.2.0"
+ resolved "https://registry.yarnpkg.com/browserify/-/browserify-15.2.0.tgz#1e121ba1fa72cf9fd2d8df002f8674b68b45df89"
+ dependencies:
+ JSONStream "^1.0.3"
+ assert "^1.4.0"
+ browser-pack "^6.0.1"
+ browser-resolve "^1.11.0"
+ browserify-zlib "~0.2.0"
+ buffer "^5.0.2"
+ cached-path-relative "^1.0.0"
+ concat-stream "~1.5.1"
+ console-browserify "^1.1.0"
+ constants-browserify "~1.0.0"
+ crypto-browserify "^3.0.0"
+ defined "^1.0.0"
+ deps-sort "^2.0.0"
+ domain-browser "~1.1.0"
+ duplexer2 "~0.1.2"
+ events "~1.1.0"
+ glob "^7.1.0"
+ has "^1.0.0"
+ htmlescape "^1.1.0"
+ https-browserify "^1.0.0"
+ inherits "~2.0.1"
+ insert-module-globals "^7.0.0"
+ labeled-stream-splicer "^2.0.0"
+ mkdirp "^0.5.0"
+ module-deps "^5.0.1"
+ os-browserify "~0.3.0"
+ parents "^1.0.1"
+ path-browserify "~0.0.0"
+ process "~0.11.0"
+ punycode "^1.3.2"
+ querystring-es3 "~0.2.0"
+ read-only-stream "^2.0.0"
+ readable-stream "^2.0.2"
+ resolve "^1.1.4"
+ shasum "^1.0.0"
+ shell-quote "^1.6.1"
+ stream-browserify "^2.0.0"
+ stream-http "^2.0.0"
+ string_decoder "~1.0.0"
+ subarg "^1.0.0"
+ syntax-error "^1.1.1"
+ through2 "^2.0.0"
+ timers-browserify "^1.0.1"
+ tty-browserify "~0.0.0"
+ url "~0.11.0"
+ util "~0.10.1"
+ vm-browserify "~0.0.1"
+ xtend "^4.0.0"
+
+browserslist@^2.1.2, browserslist@^2.11.1:
+ version "2.11.3"
+ resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-2.11.3.tgz#fe36167aed1bbcde4827ebfe71347a2cc70b99b2"
+ dependencies:
+ caniuse-lite "^1.0.30000792"
+ electron-to-chromium "^1.3.30"
+
+bser@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/bser/-/bser-2.0.0.tgz#9ac78d3ed5d915804fd87acb158bc797147a1719"
+ dependencies:
+ node-int64 "^0.4.0"
+
+buffer-equal@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-1.0.0.tgz#59616b498304d556abd466966b22eeda3eca5fbe"
+
+buffer-xor@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9"
+
+buffer@^5.0.2, buffer@^5.0.3:
+ version "5.0.8"
+ resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.0.8.tgz#84daa52e7cf2fa8ce4195bc5cf0f7809e0930b24"
+ dependencies:
+ base64-js "^1.0.2"
+ ieee754 "^1.1.4"
+
+builtin-modules@^1.0.0, builtin-modules@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
+
+builtin-status-codes@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
+
+builtins@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/builtins/-/builtins-2.0.0.tgz#018999641e11252188652dbb2db01ad386fcdc46"
+ dependencies:
+ semver "^5.4.1"
+
+bundle-collapser@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/bundle-collapser/-/bundle-collapser-1.3.0.tgz#f4b4ff58b2f22ee7701b20fa76306e23f53a3fb6"
+ dependencies:
+ browser-pack "^5.0.1"
+ browser-unpack "^1.1.0"
+ concat-stream "^1.5.0"
+ falafel "^2.1.0"
+ minimist "^1.1.1"
+ through2 "^2.0.0"
+
+bytes@2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.2.0.tgz#fd35464a403f6f9117c2de3609ecff9cae000588"
+
+bytes@2.4.0:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.4.0.tgz#7d97196f9d5baf7f6935e25985549edd2a6c2339"
+
+cache-base@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2"
+ dependencies:
+ collection-visit "^1.0.0"
+ component-emitter "^1.2.1"
+ get-value "^2.0.6"
+ has-value "^1.0.0"
+ isobject "^3.0.1"
+ set-value "^2.0.0"
+ to-object-path "^0.3.0"
+ union-value "^1.0.0"
+ unset-value "^1.0.0"
+
+cached-path-relative@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/cached-path-relative/-/cached-path-relative-1.0.1.tgz#d09c4b52800aa4c078e2dd81a869aac90d2e54e7"
+
+caller-path@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f"
+ dependencies:
+ callsites "^0.2.0"
+
+callsites@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca"
+
+callsites@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50"
+
+camelcase-keys@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7"
+ dependencies:
+ camelcase "^2.0.0"
+ map-obj "^1.0.0"
+
+camelcase@^1.0.2:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39"
+
+camelcase@^2.0.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f"
+
+camelcase@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a"
+
+camelcase@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd"
+
+caniuse-lite@^1.0.30000791, caniuse-lite@^1.0.30000792:
+ version "1.0.30000792"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000792.tgz#d0cea981f8118f3961471afbb43c9a1e5bbf0332"
+
+caseless@~0.11.0:
+ version "0.11.0"
+ resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.11.0.tgz#715b96ea9841593cc33067923f5ec60ebda4f7d7"
+
+caseless@~0.12.0:
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
+
+center-align@^0.1.1:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad"
+ dependencies:
+ align-text "^0.1.3"
+ lazy-cache "^1.0.3"
+
+chain-function@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/chain-function/-/chain-function-1.0.0.tgz#0d4ab37e7e18ead0bdc47b920764118ce58733dc"
+
+chalk@^0.5.1:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.5.1.tgz#663b3a648b68b55d04690d49167aa837858f2174"
+ dependencies:
+ ansi-styles "^1.1.0"
+ escape-string-regexp "^1.0.0"
+ has-ansi "^0.1.0"
+ strip-ansi "^0.3.0"
+ supports-color "^0.2.0"
+
+chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
+ dependencies:
+ ansi-styles "^2.2.1"
+ escape-string-regexp "^1.0.2"
+ has-ansi "^2.0.0"
+ strip-ansi "^3.0.0"
+ supports-color "^2.0.0"
+
+chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.0.tgz#b5ea48efc9c1793dccc9b4767c93914d3f2d52ba"
+ dependencies:
+ ansi-styles "^3.1.0"
+ escape-string-regexp "^1.0.5"
+ supports-color "^4.0.0"
+
+character-parser@^2.1.1:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/character-parser/-/character-parser-2.2.0.tgz#c7ce28f36d4bcd9744e5ffc2c5fcde1c73261fc0"
+ dependencies:
+ is-regex "^1.0.3"
+
+chardet@^0.4.0:
+ version "0.4.2"
+ resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2"
+
+chartist-plugin-legend@^0.6.1:
+ version "0.6.2"
+ resolved "https://registry.yarnpkg.com/chartist-plugin-legend/-/chartist-plugin-legend-0.6.2.tgz#53bf2771ddc1dc288c8abc16c151788f0b750244"
+
+chartist-plugin-tooltip@0.0.11:
+ version "0.0.11"
+ resolved "https://registry.yarnpkg.com/chartist-plugin-tooltip/-/chartist-plugin-tooltip-0.0.11.tgz#cec2195fa505f4d34e6d8e41a86bfbcb9bfff461"
+
+chartist@^0.10.1:
+ version "0.10.1"
+ resolved "https://registry.yarnpkg.com/chartist/-/chartist-0.10.1.tgz#3dd513d531dfca6b78e777fe0500d9c7e6406931"
+
+cheerio@^1.0.0-rc.2:
+ version "1.0.0-rc.2"
+ resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.2.tgz#4b9f53a81b27e4d5dac31c0ffd0cfa03cc6830db"
+ dependencies:
+ css-select "~1.2.0"
+ dom-serializer "~0.1.0"
+ entities "~1.1.1"
+ htmlparser2 "^3.9.1"
+ lodash "^4.15.0"
+ parse5 "^3.0.1"
+
+chokidar@^1.0.0:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468"
+ dependencies:
+ anymatch "^1.3.0"
+ async-each "^1.0.0"
+ glob-parent "^2.0.0"
+ inherits "^2.0.1"
+ is-binary-path "^1.0.0"
+ is-glob "^2.0.0"
+ path-is-absolute "^1.0.0"
+ readdirp "^2.0.0"
+ optionalDependencies:
+ fsevents "^1.0.0"
+
+chokidar@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.0.tgz#6686313c541d3274b2a5c01233342037948c911b"
+ dependencies:
+ anymatch "^2.0.0"
+ async-each "^1.0.0"
+ braces "^2.3.0"
+ glob-parent "^3.1.0"
+ inherits "^2.0.1"
+ is-binary-path "^1.0.0"
+ is-glob "^4.0.0"
+ normalize-path "^2.1.1"
+ path-is-absolute "^1.0.0"
+ readdirp "^2.0.0"
+ optionalDependencies:
+ fsevents "^1.0.0"
+
+ci-info@^1.0.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.1.2.tgz#03561259db48d0474c8bdc90f5b47b068b6bbfb4"
+
+cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de"
+ dependencies:
+ inherits "^2.0.1"
+ safe-buffer "^5.0.1"
+
+circular-json@^0.3.1:
+ version "0.3.3"
+ resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66"
+
+class-utils@^0.3.5:
+ version "0.3.6"
+ resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463"
+ dependencies:
+ arr-union "^3.1.0"
+ define-property "^0.2.5"
+ isobject "^3.0.0"
+ static-extend "^0.1.1"
+
+classnames@^2.1.5, classnames@^2.2.3, classnames@^2.2.4, classnames@^2.2.5:
+ version "2.2.5"
+ resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.5.tgz#fb3801d453467649ef3603c7d61a02bd129bde6d"
+
+clean-css@^3.3.0:
+ version "3.4.28"
+ resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-3.4.28.tgz#bf1945e82fc808f55695e6ddeaec01400efd03ff"
+ dependencies:
+ commander "2.8.x"
+ source-map "0.4.x"
+
+cli-cursor@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987"
+ dependencies:
+ restore-cursor "^1.0.1"
+
+cli-cursor@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5"
+ dependencies:
+ restore-cursor "^2.0.0"
+
+cli-spinners@^0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-0.1.2.tgz#bb764d88e185fb9e1e6a2a1f19772318f605e31c"
+
+cli-truncate@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-0.2.1.tgz#9f15cfbb0705005369216c626ac7d05ab90dd574"
+ dependencies:
+ slice-ansi "0.0.4"
+ string-width "^1.0.1"
+
+cli-width@^2.0.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639"
+
+cliui@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1"
+ dependencies:
+ center-align "^0.1.1"
+ right-align "^0.1.1"
+ wordwrap "0.0.2"
+
+cliui@^3.2.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d"
+ dependencies:
+ string-width "^1.0.1"
+ strip-ansi "^3.0.1"
+ wrap-ansi "^2.0.0"
+
+cliui@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.0.0.tgz#743d4650e05f36d1ed2575b59638d87322bfbbcc"
+ dependencies:
+ string-width "^2.1.1"
+ strip-ansi "^4.0.0"
+ wrap-ansi "^2.0.0"
+
+clone-buffer@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58"
+
+clone-stats@^0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-0.0.1.tgz#b88f94a82cf38b8791d58046ea4029ad88ca99d1"
+
+clone-stats@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-1.0.0.tgz#b3782dff8bb5474e18b9b6bf0fdfe782f8777680"
+
+clone@^1.0.0:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.3.tgz#298d7e2231660f40c003c2ed3140decf3f53085f"
+
+clone@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.1.tgz#d217d1e961118e3ac9a4b8bba3285553bf647cdb"
+
+cloneable-readable@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/cloneable-readable/-/cloneable-readable-1.0.0.tgz#a6290d413f217a61232f95e458ff38418cfb0117"
+ dependencies:
+ inherits "^2.0.1"
+ process-nextick-args "^1.0.6"
+ through2 "^2.0.1"
+
+co@^4.6.0:
+ version "4.6.0"
+ resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
+
+code-point-at@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
+
+collection-map@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/collection-map/-/collection-map-1.0.0.tgz#aea0f06f8d26c780c2b75494385544b2255af18c"
+ dependencies:
+ arr-map "^2.0.2"
+ for-own "^1.0.0"
+ make-iterator "^1.0.0"
+
+collection-visit@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0"
+ dependencies:
+ map-visit "^1.0.0"
+ object-visit "^1.0.0"
+
+color-convert@^1.9.0:
+ version "1.9.1"
+ resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.1.tgz#c1261107aeb2f294ebffec9ed9ecad529a6097ed"
+ dependencies:
+ color-name "^1.1.1"
+
+color-name@^1.1.1:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
+
+color-support@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2"
+
+colors@0.5.x:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/colors/-/colors-0.5.1.tgz#7d0023eaeb154e8ee9fce75dcb923d0ed1667774"
+
+combine-source-map@~0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/combine-source-map/-/combine-source-map-0.6.1.tgz#9b4a09c316033d768e0f11e029fa2730e079ad96"
+ dependencies:
+ convert-source-map "~1.1.0"
+ inline-source-map "~0.5.0"
+ lodash.memoize "~3.0.3"
+ source-map "~0.4.2"
+
+combine-source-map@~0.7.1:
+ version "0.7.2"
+ resolved "https://registry.yarnpkg.com/combine-source-map/-/combine-source-map-0.7.2.tgz#0870312856b307a87cc4ac486f3a9a62aeccc09e"
+ dependencies:
+ convert-source-map "~1.1.0"
+ inline-source-map "~0.6.0"
+ lodash.memoize "~3.0.3"
+ source-map "~0.5.3"
+
+combine-source-map@~0.8.0:
+ version "0.8.0"
+ resolved "https://registry.yarnpkg.com/combine-source-map/-/combine-source-map-0.8.0.tgz#a58d0df042c186fcf822a8e8015f5450d2d79a8b"
+ dependencies:
+ convert-source-map "~1.1.0"
+ inline-source-map "~0.6.0"
+ lodash.memoize "~3.0.3"
+ source-map "~0.5.3"
+
+combined-stream@^1.0.5, combined-stream@~1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009"
+ dependencies:
+ delayed-stream "~1.0.0"
+
+combokeys@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/combokeys/-/combokeys-3.0.0.tgz#955c59a3959af40d26846ab6fc3c682448e7572e"
+
+commander@2, commander@^2.11.0, commander@^2.9.0, commander@~2.13.0:
+ version "2.13.0"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c"
+
+commander@2.8.x:
+ version "2.8.1"
+ resolved "https://registry.yarnpkg.com/commander/-/commander-2.8.1.tgz#06be367febfda0c330aa1e2a072d3dc9762425d4"
+ dependencies:
+ graceful-readlink ">= 1.0.0"
+
+complex-matcher@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/complex-matcher/-/complex-matcher-0.2.1.tgz#1612279088bfe2bba76607bf3213de95a00a9947"
+ dependencies:
+ lodash "^4.17.4"
+
+component-emitter@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6"
+
+concat-map@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
+
+concat-stream@^1.4.8, concat-stream@^1.5.0, concat-stream@^1.6.0, concat-stream@~1.6.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7"
+ dependencies:
+ inherits "^2.0.3"
+ readable-stream "^2.2.2"
+ typedarray "^0.0.6"
+
+concat-stream@~1.5.1:
+ version "1.5.2"
+ resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.5.2.tgz#708978624d856af41a5a741defdd261da752c266"
+ dependencies:
+ inherits "~2.0.1"
+ readable-stream "~2.0.0"
+ typedarray "~0.0.5"
+
+console-browserify@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10"
+ dependencies:
+ date-now "^0.1.4"
+
+console-control-strings@^1.0.0, console-control-strings@~1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
+
+constantinople@^3.0.1:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/constantinople/-/constantinople-3.1.0.tgz#7569caa8aa3f8d5935d62e1fa96f9f702cd81c79"
+ dependencies:
+ acorn "^3.1.0"
+ is-expression "^2.0.1"
+
+constants-browserify@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75"
+
+contains-path@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a"
+
+content-type-parser@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/content-type-parser/-/content-type-parser-1.0.2.tgz#caabe80623e63638b2502fd4c7f12ff4ce2352e7"
+
+content-type@~1.0.1:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
+
+convert-source-map@1.X, convert-source-map@^1.4.0, convert-source-map@^1.5.0:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.1.tgz#b8278097b9bc229365de5c62cf5fcaed8b5599e5"
+
+convert-source-map@~1.1.0:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.1.3.tgz#4829c877e9fe49b3161f3bf3673888e204699860"
+
+cookies-js@^1.2.2:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/cookies-js/-/cookies-js-1.2.3.tgz#03315049e7c52bee3f73186a69167eab0ddb2d31"
+
+copy-descriptor@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
+
+copy-props@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/copy-props/-/copy-props-2.0.1.tgz#665fc32046ca84a898abaa3c5945e7f248ccba00"
+ dependencies:
+ each-props "^1.3.0"
+ is-plain-object "^2.0.1"
+
+copy-to-clipboard@^3:
+ version "3.0.8"
+ resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.0.8.tgz#f4e82f4a8830dce4666b7eb8ded0c9bcc313aba9"
+ dependencies:
+ toggle-selection "^1.0.3"
+
+core-js@^1.0.0:
+ version "1.2.7"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
+
+core-js@^2.4.0, core-js@^2.5.0:
+ version "2.5.3"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.3.tgz#8acc38345824f16d8365b7c9b4259168e8ed603e"
+
+core-util-is@1.0.2, core-util-is@~1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
+
+cosmiconfig@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-4.0.0.tgz#760391549580bbd2df1e562bc177b13c290972dc"
+ dependencies:
+ is-directory "^0.3.1"
+ js-yaml "^3.9.0"
+ parse-json "^4.0.0"
+ require-from-string "^2.0.1"
+
+create-ecdh@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.0.tgz#888c723596cdf7612f6498233eebd7a35301737d"
+ dependencies:
+ bn.js "^4.1.0"
+ elliptic "^6.0.0"
+
+create-hash@^1.1.0, create-hash@^1.1.2:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.1.3.tgz#606042ac8b9262750f483caddab0f5819172d8fd"
+ dependencies:
+ cipher-base "^1.0.1"
+ inherits "^2.0.1"
+ ripemd160 "^2.0.0"
+ sha.js "^2.4.0"
+
+create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.6.tgz#acb9e221a4e17bdb076e90657c42b93e3726cf06"
+ dependencies:
+ cipher-base "^1.0.3"
+ create-hash "^1.1.0"
+ inherits "^2.0.1"
+ ripemd160 "^2.0.0"
+ safe-buffer "^5.0.1"
+ sha.js "^2.4.8"
+
+create-react-class@^15.5.1, create-react-class@^15.6.0:
+ version "15.6.3"
+ resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.6.3.tgz#2d73237fb3f970ae6ebe011a9e66f46dbca80036"
+ dependencies:
+ fbjs "^0.8.9"
+ loose-envify "^1.3.1"
+ object-assign "^4.1.1"
+
+cross-spawn@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz#1256037ecb9f0c5f79e3d6ef135e30770184b982"
+ dependencies:
+ lru-cache "^4.0.1"
+ which "^1.2.9"
+
+cross-spawn@^5.0.1, cross-spawn@^5.1.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
+ dependencies:
+ lru-cache "^4.0.1"
+ shebang-command "^1.2.0"
+ which "^1.2.9"
+
+cryptiles@2.x.x:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8"
+ dependencies:
+ boom "2.x.x"
+
+cryptiles@3.x.x:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-3.1.2.tgz#a89fbb220f5ce25ec56e8c4aa8a4fd7b5b0d29fe"
+ dependencies:
+ boom "5.x.x"
+
+crypto-browserify@^3.0.0:
+ version "3.12.0"
+ resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec"
+ dependencies:
+ browserify-cipher "^1.0.0"
+ browserify-sign "^4.0.0"
+ create-ecdh "^4.0.0"
+ create-hash "^1.1.0"
+ create-hmac "^1.1.0"
+ diffie-hellman "^5.0.0"
+ inherits "^2.0.1"
+ pbkdf2 "^3.0.3"
+ public-encrypt "^4.0.0"
+ randombytes "^2.0.0"
+ randomfill "^1.0.3"
+
+css-color-keywords@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz#fea2616dc676b2962686b3af8dbdbe180b244e05"
+
+css-select@~1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858"
+ dependencies:
+ boolbase "~1.0.0"
+ css-what "2.1"
+ domutils "1.5.1"
+ nth-check "~1.0.1"
+
+css-to-react-native@^2.0.3:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/css-to-react-native/-/css-to-react-native-2.0.4.tgz#cf4cc407558b3474d4ba8be1a2cd3b6ce713101b"
+ dependencies:
+ css-color-keywords "^1.0.0"
+ fbjs "^0.8.5"
+ postcss-value-parser "^3.3.0"
+
+css-tree@1.0.0-alpha.27:
+ version "1.0.0-alpha.27"
+ resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.27.tgz#f211526909c7dc940843d83b9376ed98ddb8de47"
+ dependencies:
+ mdn-data "^1.0.0"
+ source-map "^0.5.3"
+
+css-what@2.1:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.0.tgz#9467d032c38cfaefb9f2d79501253062f87fa1bd"
+
+css@2.X, css@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/css/-/css-2.2.1.tgz#73a4c81de85db664d4ee674f7d47085e3b2d55dc"
+ dependencies:
+ inherits "^2.0.1"
+ source-map "^0.1.38"
+ source-map-resolve "^0.3.0"
+ urix "^0.1.0"
+
+csso@^3.0.0:
+ version "3.5.0"
+ resolved "https://registry.yarnpkg.com/csso/-/csso-3.5.0.tgz#acdbba5719e2c87bc801eadc032764b2e4b9d4e7"
+ dependencies:
+ css-tree "1.0.0-alpha.27"
+
+cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0":
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.2.tgz#b8036170c79f07a90ff2f16e22284027a243848b"
+
+"cssstyle@>= 0.2.37 < 0.3.0":
+ version "0.2.37"
+ resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-0.2.37.tgz#541097234cb2513c83ceed3acddc27ff27987d54"
+ dependencies:
+ cssom "0.3.x"
+
+cuint@^0.2.2:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/cuint/-/cuint-0.2.2.tgz#408086d409550c2631155619e9fa7bcadc3b991b"
+
+currently-unhandled@^0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea"
+ dependencies:
+ array-find-index "^1.0.1"
+
+d3-array@1, d3-array@1.2.1, d3-array@^1.2.0:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.1.tgz#d1ca33de2f6ac31efadb8e050a021d7e2396d5dc"
+
+d3-axis@1.0.8:
+ version "1.0.8"
+ resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-1.0.8.tgz#31a705a0b535e65759de14173a31933137f18efa"
+
+d3-brush@1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/d3-brush/-/d3-brush-1.0.4.tgz#00c2f238019f24f6c0a194a26d41a1530ffe7bc4"
+ dependencies:
+ d3-dispatch "1"
+ d3-drag "1"
+ d3-interpolate "1"
+ d3-selection "1"
+ d3-transition "1"
+
+d3-chord@1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/d3-chord/-/d3-chord-1.0.4.tgz#7dec4f0ba886f713fe111c45f763414f6f74ca2c"
+ dependencies:
+ d3-array "1"
+ d3-path "1"
+
+d3-collection@1, d3-collection@1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.4.tgz#342dfd12837c90974f33f1cc0a785aea570dcdc2"
+
+d3-color@1, d3-color@1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.0.3.tgz#bc7643fca8e53a8347e2fbdaffa236796b58509b"
+
+d3-dispatch@1, d3-dispatch@1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-1.0.3.tgz#46e1491eaa9b58c358fce5be4e8bed626e7871f8"
+
+d3-drag@1, d3-drag@1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-1.2.1.tgz#df8dd4c502fb490fc7462046a8ad98a5c479282d"
+ dependencies:
+ d3-dispatch "1"
+ d3-selection "1"
+
+d3-dsv@1, d3-dsv@1.0.8:
+ version "1.0.8"
+ resolved "https://registry.yarnpkg.com/d3-dsv/-/d3-dsv-1.0.8.tgz#907e240d57b386618dc56468bacfe76bf19764ae"
+ dependencies:
+ commander "2"
+ iconv-lite "0.4"
+ rw "1"
+
+d3-ease@1, d3-ease@1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-1.0.3.tgz#68bfbc349338a380c44d8acc4fbc3304aa2d8c0e"
+
+d3-force@1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/d3-force/-/d3-force-1.1.0.tgz#cebf3c694f1078fcc3d4daf8e567b2fbd70d4ea3"
+ dependencies:
+ d3-collection "1"
+ d3-dispatch "1"
+ d3-quadtree "1"
+ d3-timer "1"
+
+d3-format@1, d3-format@1.2.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.2.2.tgz#1a39c479c8a57fe5051b2e67a3bee27061a74e7a"
+
+d3-geo@1.9.1:
+ version "1.9.1"
+ resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-1.9.1.tgz#157e3b0f917379d0f73bebfff3be537f49fa7356"
+ dependencies:
+ d3-array "1"
+
+d3-hierarchy@1.1.5:
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-1.1.5.tgz#a1c845c42f84a206bcf1c01c01098ea4ddaa7a26"
+
+d3-interpolate@1, d3-interpolate@1.1.6:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.1.6.tgz#2cf395ae2381804df08aa1bf766b7f97b5f68fb6"
+ dependencies:
+ d3-color "1"
+
+d3-path@1, d3-path@1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.5.tgz#241eb1849bd9e9e8021c0d0a799f8a0e8e441764"
+
+d3-polygon@1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/d3-polygon/-/d3-polygon-1.0.3.tgz#16888e9026460933f2b179652ad378224d382c62"
+
+d3-quadtree@1, d3-quadtree@1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/d3-quadtree/-/d3-quadtree-1.0.3.tgz#ac7987e3e23fe805a990f28e1b50d38fcb822438"
+
+d3-queue@3.0.7:
+ version "3.0.7"
+ resolved "https://registry.yarnpkg.com/d3-queue/-/d3-queue-3.0.7.tgz#c93a2e54b417c0959129d7d73f6cf7d4292e7618"
+
+d3-random@1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/d3-random/-/d3-random-1.1.0.tgz#6642e506c6fa3a648595d2b2469788a8d12529d3"
+
+d3-request@1.0.6:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/d3-request/-/d3-request-1.0.6.tgz#a1044a9ef4ec28c824171c9379fae6d79474b19f"
+ dependencies:
+ d3-collection "1"
+ d3-dispatch "1"
+ d3-dsv "1"
+ xmlhttprequest "1"
+
+d3-scale@1.0.7:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-1.0.7.tgz#fa90324b3ea8a776422bd0472afab0b252a0945d"
+ dependencies:
+ d3-array "^1.2.0"
+ d3-collection "1"
+ d3-color "1"
+ d3-format "1"
+ d3-interpolate "1"
+ d3-time "1"
+ d3-time-format "2"
+
+d3-selection@1, d3-selection@1.3.0, d3-selection@^1.1.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.3.0.tgz#d53772382d3dc4f7507bfb28bcd2d6aed2a0ad6d"
+
+d3-shape@1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.2.0.tgz#45d01538f064bafd05ea3d6d2cb748fd8c41f777"
+ dependencies:
+ d3-path "1"
+
+d3-time-format@2, d3-time-format@2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-2.1.1.tgz#85b7cdfbc9ffca187f14d3c456ffda268081bb31"
+ dependencies:
+ d3-time "1"
+
+d3-time@1, d3-time@1.0.8:
+ version "1.0.8"
+ resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.0.8.tgz#dbd2d6007bf416fe67a76d17947b784bffea1e84"
+
+d3-timer@1, d3-timer@1.0.7:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-1.0.7.tgz#df9650ca587f6c96607ff4e60cc38229e8dd8531"
+
+d3-transition@1, d3-transition@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-1.1.1.tgz#d8ef89c3b848735b060e54a39b32aaebaa421039"
+ dependencies:
+ d3-color "1"
+ d3-dispatch "1"
+ d3-ease "1"
+ d3-interpolate "1"
+ d3-selection "^1.1.0"
+ d3-timer "1"
+
+d3-voronoi@1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/d3-voronoi/-/d3-voronoi-1.1.2.tgz#1687667e8f13a2d158c80c1480c5a29cb0d8973c"
+
+d3-zoom@1.7.1:
+ version "1.7.1"
+ resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-1.7.1.tgz#02f43b3c3e2db54f364582d7e4a236ccc5506b63"
+ dependencies:
+ d3-dispatch "1"
+ d3-drag "1"
+ d3-interpolate "1"
+ d3-selection "1"
+ d3-transition "1"
+
+d3@^4.12.2:
+ version "4.13.0"
+ resolved "https://registry.yarnpkg.com/d3/-/d3-4.13.0.tgz#ab236ff8cf0cfc27a81e69bf2fb7518bc9b4f33d"
+ dependencies:
+ d3-array "1.2.1"
+ d3-axis "1.0.8"
+ d3-brush "1.0.4"
+ d3-chord "1.0.4"
+ d3-collection "1.0.4"
+ d3-color "1.0.3"
+ d3-dispatch "1.0.3"
+ d3-drag "1.2.1"
+ d3-dsv "1.0.8"
+ d3-ease "1.0.3"
+ d3-force "1.1.0"
+ d3-format "1.2.2"
+ d3-geo "1.9.1"
+ d3-hierarchy "1.1.5"
+ d3-interpolate "1.1.6"
+ d3-path "1.0.5"
+ d3-polygon "1.0.3"
+ d3-quadtree "1.0.3"
+ d3-queue "3.0.7"
+ d3-random "1.1.0"
+ d3-request "1.0.6"
+ d3-scale "1.0.7"
+ d3-selection "1.3.0"
+ d3-shape "1.2.0"
+ d3-time "1.0.8"
+ d3-time-format "2.1.1"
+ d3-timer "1.0.7"
+ d3-transition "1.1.1"
+ d3-voronoi "1.1.2"
+ d3-zoom "1.7.1"
+
+d@1:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f"
+ dependencies:
+ es5-ext "^0.10.9"
+
+dashdash@^1.12.0:
+ version "1.14.1"
+ resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
+ dependencies:
+ assert-plus "^1.0.0"
+
+date-fns@^1.27.2:
+ version "1.29.0"
+ resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.29.0.tgz#12e609cdcb935127311d04d33334e2960a2a54e6"
+
+date-now@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
+
+dateformat@^2.0.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-2.2.0.tgz#4065e2013cf9fb916ddfd82efb506ad4c6769062"
+
+debounce-input-decorator@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/debounce-input-decorator/-/debounce-input-decorator-0.1.0.tgz#e1c4ca7f40fa2d63e7bc4e14ea5dc850fdabaeea"
+ dependencies:
+ lodash "^4.17.4"
+
+debug-fabulous@1.X:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/debug-fabulous/-/debug-fabulous-1.0.0.tgz#57f6648646097b1b0849dcda0017362c1ec00f8b"
+ dependencies:
+ debug "3.X"
+ memoizee "0.4.X"
+ object-assign "4.X"
+
+debug@3.X, debug@^3.0.1, debug@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
+ dependencies:
+ ms "2.0.0"
+
+debug@^2.1.0, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9:
+ version "2.6.9"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
+ dependencies:
+ ms "2.0.0"
+
+debug@~2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da"
+ dependencies:
+ ms "0.7.1"
+
+decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
+
+decode-uri-component@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
+
+dedent@^0.7.0:
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c"
+
+deep-extend@~0.4.0:
+ version "0.4.2"
+ resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f"
+
+deep-is@~0.1.3:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
+
+default-compare@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/default-compare/-/default-compare-1.0.0.tgz#cb61131844ad84d84788fb68fd01681ca7781a2f"
+ dependencies:
+ kind-of "^5.0.2"
+
+default-require-extensions@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-1.0.0.tgz#f37ea15d3e13ffd9b437d33e1a75b5fb97874cb8"
+ dependencies:
+ strip-bom "^2.0.0"
+
+default-resolution@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/default-resolution/-/default-resolution-2.0.0.tgz#bcb82baa72ad79b426a76732f1a81ad6df26d684"
+
+define-properties@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.2.tgz#83a73f2fea569898fb737193c8f873caf6d45c94"
+ dependencies:
+ foreach "^2.0.5"
+ object-keys "^1.0.8"
+
+define-property@^0.2.5:
+ version "0.2.5"
+ resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116"
+ dependencies:
+ is-descriptor "^0.1.0"
+
+define-property@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6"
+ dependencies:
+ is-descriptor "^1.0.0"
+
+defined@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693"
+
+del@^2.0.2:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/del/-/del-2.2.2.tgz#c12c981d067846c84bcaf862cff930d907ffd1a8"
+ dependencies:
+ globby "^5.0.0"
+ is-path-cwd "^1.0.0"
+ is-path-in-cwd "^1.0.0"
+ object-assign "^4.0.1"
+ pify "^2.0.0"
+ pinkie-promise "^2.0.0"
+ rimraf "^2.2.8"
+
+delayed-stream@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
+
+delegates@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
+
+depd@~1.1.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
+
+dependency-check@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/dependency-check/-/dependency-check-3.0.0.tgz#92e2a4141812d4e99cb91298c36a7435b848b1c6"
+ dependencies:
+ builtins "^2.0.0"
+ debug "^3.1.0"
+ detective "^5.0.2"
+ is-relative "^1.0.0"
+ minimist "^1.2.0"
+ read-package-json "^2.0.10"
+ resolve "^1.1.7"
+
+dependency-graph@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/dependency-graph/-/dependency-graph-0.6.0.tgz#f155fd58b5ca0f5559bfce0e8720cac1a1b8433d"
+
+deps-sort@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/deps-sort/-/deps-sort-2.0.0.tgz#091724902e84658260eb910748cccd1af6e21fb5"
+ dependencies:
+ JSONStream "^1.0.3"
+ shasum "^1.0.0"
+ subarg "^1.0.0"
+ through2 "^2.0.0"
+
+des.js@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc"
+ dependencies:
+ inherits "^2.0.1"
+ minimalistic-assert "^1.0.0"
+
+detect-file@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7"
+
+detect-indent@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208"
+ dependencies:
+ repeating "^2.0.0"
+
+detect-libc@^1.0.2:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
+
+detect-newline@2.X, detect-newline@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2"
+
+detective@^5.0.2:
+ version "5.0.2"
+ resolved "https://registry.yarnpkg.com/detective/-/detective-5.0.2.tgz#84ec2e1c581e74211e2ae4ffce1edf52c3263f84"
+ dependencies:
+ "@browserify/acorn5-object-spread" "^5.0.1"
+ acorn "^5.2.1"
+ defined "^1.0.0"
+
+diff@^3.2.0:
+ version "3.4.0"
+ resolved "https://registry.yarnpkg.com/diff/-/diff-3.4.0.tgz#b1d85507daf3964828de54b37d0d73ba67dda56c"
+
+diffie-hellman@^5.0.0:
+ version "5.0.2"
+ resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e"
+ dependencies:
+ bn.js "^4.1.0"
+ miller-rabin "^4.0.0"
+ randombytes "^2.0.0"
+
+dir-glob@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.0.0.tgz#0b205d2b6aef98238ca286598a8204d29d0a0034"
+ dependencies:
+ arrify "^1.0.1"
+ path-type "^3.0.0"
+
+discontinuous-range@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/discontinuous-range/-/discontinuous-range-1.0.0.tgz#e38331f0844bba49b9a9cb71c771585aab1bc65a"
+
+disposables@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/disposables/-/disposables-1.0.2.tgz#36c6a674475f55a2d6913567a601444e487b4b6e"
+
+dnd-core@^2.5.4:
+ version "2.5.4"
+ resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-2.5.4.tgz#0c70a8dcbb609c0b222e275fcae9fa83e5897397"
+ dependencies:
+ asap "^2.0.6"
+ invariant "^2.0.0"
+ lodash "^4.2.0"
+ redux "^3.7.1"
+
+doctrine@1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa"
+ dependencies:
+ esutils "^2.0.2"
+ isarray "^1.0.0"
+
+doctrine@^2.0.2, doctrine@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d"
+ dependencies:
+ esutils "^2.0.2"
+
+doctypes@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/doctypes/-/doctypes-1.1.0.tgz#ea80b106a87538774e8a3a4a5afe293de489e0a9"
+
+dom-helpers@^2.4.0:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-2.4.0.tgz#9bb4b245f637367b1fa670274272aa28fe06c367"
+
+"dom-helpers@^2.4.0 || ^3.0.0", dom-helpers@^3.2.0, dom-helpers@^3.2.1:
+ version "3.3.1"
+ resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.3.1.tgz#fc1a4e15ffdf60ddde03a480a9c0fece821dd4a6"
+
+dom-serializer@0, dom-serializer@~0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82"
+ dependencies:
+ domelementtype "~1.1.1"
+ entities "~1.1.1"
+
+domain-browser@~1.1.0:
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.1.7.tgz#867aa4b093faa05f1de08c06f4d7b21fdf8698bc"
+
+domelementtype@1, domelementtype@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.0.tgz#b17aed82e8ab59e52dd9c19b1756e0fc187204c2"
+
+domelementtype@~1.1.1:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.1.3.tgz#bd28773e2642881aec51544924299c5cd822185b"
+
+domexception@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90"
+ dependencies:
+ webidl-conversions "^4.0.2"
+
+domhandler@^2.3.0:
+ version "2.4.1"
+ resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.1.tgz#892e47000a99be55bbf3774ffea0561d8879c259"
+ dependencies:
+ domelementtype "1"
+
+domutils@1.5.1:
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
+ dependencies:
+ dom-serializer "0"
+ domelementtype "1"
+
+domutils@^1.5.1:
+ version "1.6.2"
+ resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.6.2.tgz#1958cc0b4c9426e9ed367fb1c8e854891b0fa3ff"
+ dependencies:
+ dom-serializer "0"
+ domelementtype "1"
+
+dot-prop@^4.1.1:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57"
+ dependencies:
+ is-obj "^1.0.0"
+
+duplexer2@0.0.2:
+ version "0.0.2"
+ resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.0.2.tgz#c614dcf67e2fb14995a91711e5a617e8a60a31db"
+ dependencies:
+ readable-stream "~1.1.9"
+
+duplexer2@^0.1.2, duplexer2@~0.1.0, duplexer2@~0.1.2:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1"
+ dependencies:
+ readable-stream "^2.0.2"
+
+duplexer@~0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1"
+
+duplexify@^3.5.3:
+ version "3.5.3"
+ resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.5.3.tgz#8b5818800df92fd0125b27ab896491912858243e"
+ dependencies:
+ end-of-stream "^1.0.0"
+ inherits "^2.0.1"
+ readable-stream "^2.0.0"
+ stream-shift "^1.0.0"
+
+each-props@^1.3.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/each-props/-/each-props-1.3.1.tgz#fc138f51e3a2774286d4858e02d6e7de462de158"
+ dependencies:
+ is-plain-object "^2.0.1"
+ object.defaults "^1.1.0"
+
+ecc-jsbn@~0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505"
+ dependencies:
+ jsbn "~0.1.0"
+
+ee-first@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
+
+electron-to-chromium@^1.3.30:
+ version "1.3.32"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.32.tgz#11d0684c0840e003c4be8928f8ac5f35dbc2b4e6"
+
+elegant-spinner@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e"
+
+elliptic@^6.0.0:
+ version "6.4.0"
+ resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df"
+ dependencies:
+ bn.js "^4.4.0"
+ brorand "^1.0.1"
+ hash.js "^1.0.0"
+ hmac-drbg "^1.0.0"
+ inherits "^2.0.1"
+ minimalistic-assert "^1.0.0"
+ minimalistic-crypto-utils "^1.0.0"
+
+encoding@^0.1.11:
+ version "0.1.12"
+ resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb"
+ dependencies:
+ iconv-lite "~0.4.13"
+
+end-of-stream@^1.0.0, end-of-stream@^1.1.0:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43"
+ dependencies:
+ once "^1.4.0"
+
+entities@^1.1.1, entities@~1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0"
+
+enzyme-adapter-react-15@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/enzyme-adapter-react-15/-/enzyme-adapter-react-15-1.0.5.tgz#99f9a03ff2c2303e517342935798a6bdfbb75fac"
+ dependencies:
+ enzyme-adapter-utils "^1.1.0"
+ lodash "^4.17.4"
+ object.assign "^4.0.4"
+ object.values "^1.0.4"
+ prop-types "^15.5.10"
+
+enzyme-adapter-utils@^1.1.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/enzyme-adapter-utils/-/enzyme-adapter-utils-1.3.0.tgz#d6c85756826c257a8544d362cc7a67e97ea698c7"
+ dependencies:
+ lodash "^4.17.4"
+ object.assign "^4.0.4"
+ prop-types "^15.6.0"
+
+enzyme-to-json@^3.3.0:
+ version "3.3.1"
+ resolved "https://registry.yarnpkg.com/enzyme-to-json/-/enzyme-to-json-3.3.1.tgz#64239dcd417e2fb552f4baa6632de4744b9b5b93"
+ dependencies:
+ lodash "^4.17.4"
+
+enzyme@^3.3.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/enzyme/-/enzyme-3.3.0.tgz#0971abd167f2d4bf3f5bd508229e1c4b6dc50479"
+ dependencies:
+ cheerio "^1.0.0-rc.2"
+ function.prototype.name "^1.0.3"
+ has "^1.0.1"
+ is-boolean-object "^1.0.0"
+ is-callable "^1.1.3"
+ is-number-object "^1.0.3"
+ is-string "^1.0.4"
+ is-subset "^0.1.1"
+ lodash "^4.17.4"
+ object-inspect "^1.5.0"
+ object-is "^1.0.1"
+ object.assign "^4.1.0"
+ object.entries "^1.0.4"
+ object.values "^1.0.4"
+ raf "^3.4.0"
+ rst-selector-parser "^2.2.3"
+
+error-ex@^1.2.0, error-ex@^1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc"
+ dependencies:
+ is-arrayish "^0.2.1"
+
+es-abstract@^1.5.1, es-abstract@^1.6.1, es-abstract@^1.7.0:
+ version "1.10.0"
+ resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.10.0.tgz#1ecb36c197842a00d8ee4c2dfd8646bb97d60864"
+ dependencies:
+ es-to-primitive "^1.1.1"
+ function-bind "^1.1.1"
+ has "^1.0.1"
+ is-callable "^1.1.3"
+ is-regex "^1.0.4"
+
+es-to-primitive@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.1.1.tgz#45355248a88979034b6792e19bb81f2b7975dd0d"
+ dependencies:
+ is-callable "^1.1.1"
+ is-date-object "^1.0.1"
+ is-symbol "^1.0.1"
+
+es5-ext@^0.10.14, es5-ext@^0.10.30, es5-ext@^0.10.35, es5-ext@^0.10.9, es5-ext@~0.10.14, es5-ext@~0.10.2:
+ version "0.10.38"
+ resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.38.tgz#fa7d40d65bbc9bb8a67e1d3f9cc656a00530eed3"
+ dependencies:
+ es6-iterator "~2.0.3"
+ es6-symbol "~3.1.1"
+
+es6-iterator@^2.0.1, es6-iterator@~2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7"
+ dependencies:
+ d "1"
+ es5-ext "^0.10.35"
+ es6-symbol "^3.1.1"
+
+es6-symbol@^3.1.1, es6-symbol@~3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77"
+ dependencies:
+ d "1"
+ es5-ext "~0.10.14"
+
+es6-weak-map@^2.0.1, es6-weak-map@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.2.tgz#5e3ab32251ffd1538a1f8e5ffa1357772f92d96f"
+ dependencies:
+ d "1"
+ es5-ext "^0.10.14"
+ es6-iterator "^2.0.1"
+ es6-symbol "^3.1.1"
+
+escape-string-regexp@^1.0.0, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
+
+escodegen@^1.9.0:
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.9.0.tgz#9811a2f265dc1cd3894420ee3717064b632b8852"
+ dependencies:
+ esprima "^3.1.3"
+ estraverse "^4.2.0"
+ esutils "^2.0.2"
+ optionator "^0.8.1"
+ optionalDependencies:
+ source-map "~0.5.6"
+
+eslint-config-standard-jsx@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/eslint-config-standard-jsx/-/eslint-config-standard-jsx-4.0.2.tgz#009e53c4ddb1e9ee70b4650ffe63a7f39f8836e1"
+
+eslint-config-standard@^10.2.1:
+ version "10.2.1"
+ resolved "https://registry.yarnpkg.com/eslint-config-standard/-/eslint-config-standard-10.2.1.tgz#c061e4d066f379dc17cd562c64e819b4dd454591"
+
+eslint-import-resolver-node@^0.3.1:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz#58f15fb839b8d0576ca980413476aab2472db66a"
+ dependencies:
+ debug "^2.6.9"
+ resolve "^1.5.0"
+
+eslint-module-utils@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.1.1.tgz#abaec824177613b8a95b299639e1b6facf473449"
+ dependencies:
+ debug "^2.6.8"
+ pkg-dir "^1.0.0"
+
+eslint-plugin-import@^2.8.0:
+ version "2.8.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.8.0.tgz#fa1b6ef31fcb3c501c09859c1b86f1fc5b986894"
+ dependencies:
+ builtin-modules "^1.1.1"
+ contains-path "^0.1.0"
+ debug "^2.6.8"
+ doctrine "1.5.0"
+ eslint-import-resolver-node "^0.3.1"
+ eslint-module-utils "^2.1.1"
+ has "^1.0.1"
+ lodash.cond "^4.3.0"
+ minimatch "^3.0.3"
+ read-pkg-up "^2.0.0"
+
+eslint-plugin-node@^5.2.1:
+ version "5.2.1"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-5.2.1.tgz#80df3253c4d7901045ec87fa660a284e32bdca29"
+ dependencies:
+ ignore "^3.3.6"
+ minimatch "^3.0.4"
+ resolve "^1.3.3"
+ semver "5.3.0"
+
+eslint-plugin-promise@^3.6.0:
+ version "3.6.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-3.6.0.tgz#54b7658c8f454813dc2a870aff8152ec4969ba75"
+
+eslint-plugin-react@^7.4.0:
+ version "7.6.1"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.6.1.tgz#5d0e908be599f0c02fbf4eef0c7ed6f29dff7633"
+ dependencies:
+ doctrine "^2.0.2"
+ has "^1.0.1"
+ jsx-ast-utils "^2.0.1"
+ prop-types "^15.6.0"
+
+eslint-plugin-standard@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-standard/-/eslint-plugin-standard-3.0.1.tgz#34d0c915b45edc6f010393c7eef3823b08565cf2"
+
+eslint-scope@^3.7.1, eslint-scope@~3.7.1:
+ version "3.7.1"
+ resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.1.tgz#3d63c3edfda02e06e01a452ad88caacc7cdcb6e8"
+ dependencies:
+ esrecurse "^4.1.0"
+ estraverse "^4.1.1"
+
+eslint-visitor-keys@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d"
+
+eslint@^4.14.0:
+ version "4.16.0"
+ resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.16.0.tgz#934ada9e98715e1d7bbfd6f6f0519ed2fab35cc1"
+ dependencies:
+ ajv "^5.3.0"
+ babel-code-frame "^6.22.0"
+ chalk "^2.1.0"
+ concat-stream "^1.6.0"
+ cross-spawn "^5.1.0"
+ debug "^3.1.0"
+ doctrine "^2.1.0"
+ eslint-scope "^3.7.1"
+ eslint-visitor-keys "^1.0.0"
+ espree "^3.5.2"
+ esquery "^1.0.0"
+ esutils "^2.0.2"
+ file-entry-cache "^2.0.0"
+ functional-red-black-tree "^1.0.1"
+ glob "^7.1.2"
+ globals "^11.0.1"
+ ignore "^3.3.3"
+ imurmurhash "^0.1.4"
+ inquirer "^3.0.6"
+ is-resolvable "^1.0.0"
+ js-yaml "^3.9.1"
+ json-stable-stringify-without-jsonify "^1.0.1"
+ levn "^0.3.0"
+ lodash "^4.17.4"
+ minimatch "^3.0.2"
+ mkdirp "^0.5.1"
+ natural-compare "^1.4.0"
+ optionator "^0.8.2"
+ path-is-inside "^1.0.2"
+ pluralize "^7.0.0"
+ progress "^2.0.0"
+ require-uncached "^1.0.3"
+ semver "^5.3.0"
+ strip-ansi "^4.0.0"
+ strip-json-comments "~2.0.1"
+ table "^4.0.1"
+ text-table "~0.2.0"
+
+espree@^3.5.2:
+ version "3.5.2"
+ resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.2.tgz#756ada8b979e9dcfcdb30aad8d1a9304a905e1ca"
+ dependencies:
+ acorn "^5.2.1"
+ acorn-jsx "^3.0.0"
+
+esprima@^3.1.3:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
+
+esprima@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804"
+
+esquery@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.0.tgz#cfba8b57d7fba93f17298a8a006a04cda13d80fa"
+ dependencies:
+ estraverse "^4.0.0"
+
+esrecurse@^4.1.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.0.tgz#fa9568d98d3823f9a41d91e902dcab9ea6e5b163"
+ dependencies:
+ estraverse "^4.1.0"
+ object-assign "^4.0.1"
+
+estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13"
+
+esutils@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b"
+
+event-emitter@^0.3.5:
+ version "0.3.5"
+ resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39"
+ dependencies:
+ d "1"
+ es5-ext "~0.10.14"
+
+event-stream@*, event-stream@^3.1.7:
+ version "3.3.4"
+ resolved "https://registry.yarnpkg.com/event-stream/-/event-stream-3.3.4.tgz#4ab4c9a0f5a54db9338b4c34d86bfce8f4b35571"
+ dependencies:
+ duplexer "~0.1.1"
+ from "~0"
+ map-stream "~0.1.0"
+ pause-stream "0.0.11"
+ split "0.3"
+ stream-combiner "~0.0.4"
+ through "~2.3.1"
+
+event-to-promise@^0.7.0:
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/event-to-promise/-/event-to-promise-0.7.0.tgz#cb07dfcd418da2221d90f77eab713bc235e2090f"
+
+event-to-promise@^0.8.0:
+ version "0.8.0"
+ resolved "https://registry.yarnpkg.com/event-to-promise/-/event-to-promise-0.8.0.tgz#4b84f11772b6f25f7752fc74d971531ac6f5b626"
+
+events@^1.0.2, events@~1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924"
+
+evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02"
+ dependencies:
+ md5.js "^1.3.4"
+ safe-buffer "^5.1.1"
+
+exec-sh@^0.2.0:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.2.1.tgz#163b98a6e89e6b65b47c2a28d215bc1f63989c38"
+ dependencies:
+ merge "^1.1.3"
+
+execa@^0.7.0:
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777"
+ dependencies:
+ cross-spawn "^5.0.1"
+ get-stream "^3.0.0"
+ is-stream "^1.1.0"
+ npm-run-path "^2.0.0"
+ p-finally "^1.0.0"
+ signal-exit "^3.0.0"
+ strip-eof "^1.0.0"
+
+execa@^0.8.0:
+ version "0.8.0"
+ resolved "https://registry.yarnpkg.com/execa/-/execa-0.8.0.tgz#d8d76bbc1b55217ed190fd6dd49d3c774ecfc8da"
+ dependencies:
+ cross-spawn "^5.0.1"
+ get-stream "^3.0.0"
+ is-stream "^1.1.0"
+ npm-run-path "^2.0.0"
+ p-finally "^1.0.0"
+ signal-exit "^3.0.0"
+ strip-eof "^1.0.0"
+
+exenv@^1.2.0, exenv@^1.2.1:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/exenv/-/exenv-1.2.2.tgz#2ae78e85d9894158670b03d47bec1f03bd91bb9d"
+
+exit-hook@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8"
+
+exit@^0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c"
+
+expand-brackets@^0.1.4:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b"
+ dependencies:
+ is-posix-bracket "^0.1.0"
+
+expand-brackets@^2.1.4:
+ version "2.1.4"
+ resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622"
+ dependencies:
+ debug "^2.3.3"
+ define-property "^0.2.5"
+ extend-shallow "^2.0.1"
+ posix-character-classes "^0.1.0"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.1"
+
+expand-range@^1.8.1:
+ version "1.8.2"
+ resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337"
+ dependencies:
+ fill-range "^2.1.0"
+
+expand-tilde@^2.0.0, expand-tilde@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502"
+ dependencies:
+ homedir-polyfill "^1.0.1"
+
+expect@^22.1.0:
+ version "22.1.0"
+ resolved "https://registry.yarnpkg.com/expect/-/expect-22.1.0.tgz#f8f9b019ab275d859cbefed531fbaefe8972431d"
+ dependencies:
+ ansi-styles "^3.2.0"
+ jest-diff "^22.1.0"
+ jest-get-type "^22.1.0"
+ jest-matcher-utils "^22.1.0"
+ jest-message-util "^22.1.0"
+ jest-regex-util "^22.1.0"
+
+extend-shallow@^1.1.2:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-1.1.4.tgz#19d6bf94dfc09d76ba711f39b872d21ff4dd9071"
+ dependencies:
+ kind-of "^1.1.0"
+
+extend-shallow@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"
+ dependencies:
+ is-extendable "^0.1.0"
+
+extend-shallow@^3.0.0:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8"
+ dependencies:
+ assign-symbols "^1.0.0"
+ is-extendable "^1.0.1"
+
+extend@^3.0.0, extend@~3.0.0, extend@~3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
+
+external-editor@^2.0.4:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.1.0.tgz#3d026a21b7f95b5726387d4200ac160d372c3b48"
+ dependencies:
+ chardet "^0.4.0"
+ iconv-lite "^0.4.17"
+ tmp "^0.0.33"
+
+extglob@^0.3.1:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1"
+ dependencies:
+ is-extglob "^1.0.0"
+
+extglob@^2.0.2:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543"
+ dependencies:
+ array-unique "^0.3.2"
+ define-property "^1.0.0"
+ expand-brackets "^2.1.4"
+ extend-shallow "^2.0.1"
+ fragment-cache "^0.2.1"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.1"
+
+extsprintf@1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
+
+extsprintf@^1.2.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
+
+falafel@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/falafel/-/falafel-2.1.0.tgz#96bb17761daba94f46d001738b3cedf3a67fe06c"
+ dependencies:
+ acorn "^5.0.0"
+ foreach "^2.0.5"
+ isarray "0.0.1"
+ object-keys "^1.0.6"
+
+fancy-log@^1.1.0, fancy-log@^1.3.2:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/fancy-log/-/fancy-log-1.3.2.tgz#f41125e3d84f2e7d89a43d06d958c8f78be16be1"
+ dependencies:
+ ansi-gray "^0.1.1"
+ color-support "^1.1.3"
+ time-stamp "^1.0.0"
+
+fast-deep-equal@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff"
+
+fast-json-stable-stringify@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2"
+
+fast-levenshtein@~2.0.4:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
+
+faye-websocket@~0.7.2:
+ version "0.7.3"
+ resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.7.3.tgz#cc4074c7f4a4dfd03af54dd65c354b135132ce11"
+ dependencies:
+ websocket-driver ">=0.3.6"
+
+fb-watchman@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.0.tgz#54e9abf7dfa2f26cd9b1636c588c1afc05de5d58"
+ dependencies:
+ bser "^2.0.0"
+
+fbjs@^0.8.16, fbjs@^0.8.4, fbjs@^0.8.5, fbjs@^0.8.9:
+ version "0.8.16"
+ resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.16.tgz#5e67432f550dc41b572bf55847b8aca64e5337db"
+ dependencies:
+ core-js "^1.0.0"
+ isomorphic-fetch "^2.1.1"
+ loose-envify "^1.0.0"
+ object-assign "^4.1.0"
+ promise "^7.1.1"
+ setimmediate "^1.0.5"
+ ua-parser-js "^0.7.9"
+
+figures@^1.7.0:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e"
+ dependencies:
+ escape-string-regexp "^1.0.5"
+ object-assign "^4.1.0"
+
+figures@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962"
+ dependencies:
+ escape-string-regexp "^1.0.5"
+
+file-entry-cache@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361"
+ dependencies:
+ flat-cache "^1.2.1"
+ object-assign "^4.0.1"
+
+filename-regex@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26"
+
+fileset@^2.0.2:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/fileset/-/fileset-2.0.3.tgz#8e7548a96d3cc2327ee5e674168723a333bba2a0"
+ dependencies:
+ glob "^7.0.3"
+ minimatch "^3.0.3"
+
+fill-range@^2.1.0:
+ version "2.2.3"
+ resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.3.tgz#50b77dfd7e469bc7492470963699fe7a8485a723"
+ dependencies:
+ is-number "^2.1.0"
+ isobject "^2.0.0"
+ randomatic "^1.1.3"
+ repeat-element "^1.1.2"
+ repeat-string "^1.5.2"
+
+fill-range@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7"
+ dependencies:
+ extend-shallow "^2.0.1"
+ is-number "^3.0.0"
+ repeat-string "^1.6.1"
+ to-regex-range "^2.1.0"
+
+find-parent-dir@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/find-parent-dir/-/find-parent-dir-0.3.0.tgz#33c44b429ab2b2f0646299c5f9f718f376ff8d54"
+
+find-up@^1.0.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f"
+ dependencies:
+ path-exists "^2.0.0"
+ pinkie-promise "^2.0.0"
+
+find-up@^2.0.0, find-up@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7"
+ dependencies:
+ locate-path "^2.0.0"
+
+findup-sync@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-2.0.0.tgz#9326b1488c22d1a6088650a86901b2d9a90a2cbc"
+ dependencies:
+ detect-file "^1.0.0"
+ is-glob "^3.1.0"
+ micromatch "^3.0.4"
+ resolve-dir "^1.0.1"
+
+fined@^1.0.1:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/fined/-/fined-1.1.0.tgz#b37dc844b76a2f5e7081e884f7c0ae344f153476"
+ dependencies:
+ expand-tilde "^2.0.2"
+ is-plain-object "^2.0.3"
+ object.defaults "^1.1.0"
+ object.pick "^1.2.0"
+ parse-filepath "^1.0.1"
+
+first-chunk-stream@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/first-chunk-stream/-/first-chunk-stream-2.0.0.tgz#1bdecdb8e083c0664b91945581577a43a9f31d70"
+ dependencies:
+ readable-stream "^2.0.2"
+
+flagged-respawn@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/flagged-respawn/-/flagged-respawn-1.0.0.tgz#4e79ae9b2eb38bf86b3bb56bf3e0a56aa5fcabd7"
+
+flat-cache@^1.2.1:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.3.0.tgz#d3030b32b38154f4e3b7e9c709f490f7ef97c481"
+ dependencies:
+ circular-json "^0.3.1"
+ del "^2.0.2"
+ graceful-fs "^4.1.2"
+ write "^0.2.1"
+
+flush-write-stream@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.0.2.tgz#c81b90d8746766f1a609a46809946c45dd8ae417"
+ dependencies:
+ inherits "^2.0.1"
+ readable-stream "^2.0.4"
+
+font-awesome@^4.7.0:
+ version "4.7.0"
+ resolved "https://registry.yarnpkg.com/font-awesome/-/font-awesome-4.7.0.tgz#8fa8cf0411a1a31afd07b06d2902bb9fc815a133"
+
+font-mfizz@^2.4.1:
+ version "2.4.1"
+ resolved "https://registry.yarnpkg.com/font-mfizz/-/font-mfizz-2.4.1.tgz#f2921705ab75c0f095df2a088e2d42811d98e49b"
+
+for-in@^1.0.1, for-in@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
+
+for-own@^0.1.4:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce"
+ dependencies:
+ for-in "^1.0.1"
+
+for-own@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/for-own/-/for-own-1.0.0.tgz#c63332f415cedc4b04dbfe70cf836494c53cb44b"
+ dependencies:
+ for-in "^1.0.1"
+
+foreach@^2.0.5:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99"
+
+forever-agent@~0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
+
+form-data@~2.1.1:
+ version "2.1.4"
+ resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1"
+ dependencies:
+ asynckit "^0.4.0"
+ combined-stream "^1.0.5"
+ mime-types "^2.1.12"
+
+form-data@~2.3.1:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.1.tgz#6fb94fbd71885306d73d15cc497fe4cc4ecd44bf"
+ dependencies:
+ asynckit "^0.4.0"
+ combined-stream "^1.0.5"
+ mime-types "^2.1.12"
+
+fragment-cache@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19"
+ dependencies:
+ map-cache "^0.2.2"
+
+from@~0:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe"
+
+fs-mkdirp-stream@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz#0b7815fc3201c6a69e14db98ce098c16935259eb"
+ dependencies:
+ graceful-fs "^4.1.11"
+ through2 "^2.0.3"
+
+fs.realpath@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
+
+fsevents@^1.0.0, fsevents@^1.1.1:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.3.tgz#11f82318f5fe7bb2cd22965a108e9306208216d8"
+ dependencies:
+ nan "^2.3.0"
+ node-pre-gyp "^0.6.39"
+
+fstream-ignore@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105"
+ dependencies:
+ fstream "^1.0.0"
+ inherits "2"
+ minimatch "^3.0.0"
+
+fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2:
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171"
+ dependencies:
+ graceful-fs "^4.1.2"
+ inherits "~2.0.0"
+ mkdirp ">=0.5 0"
+ rimraf "2"
+
+function-bind@^1.0.2, function-bind@^1.1.0, function-bind@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+
+function.prototype.name@^1.0.3:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.0.tgz#8bd763cc0af860a859cc5d49384d74b932cd2327"
+ dependencies:
+ define-properties "^1.1.2"
+ function-bind "^1.1.1"
+ is-callable "^1.1.3"
+
+functional-red-black-tree@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
+
+gauge@~2.7.3:
+ version "2.7.4"
+ resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
+ dependencies:
+ aproba "^1.0.3"
+ console-control-strings "^1.0.0"
+ has-unicode "^2.0.0"
+ object-assign "^4.1.0"
+ signal-exit "^3.0.0"
+ string-width "^1.0.1"
+ strip-ansi "^3.0.1"
+ wide-align "^1.1.0"
+
+gaze@^1.0.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/gaze/-/gaze-1.1.2.tgz#847224677adb8870d679257ed3388fdb61e40105"
+ dependencies:
+ globule "^1.0.0"
+
+generate-function@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.0.0.tgz#6858fe7c0969b7d4e9093337647ac79f60dfbe74"
+
+generate-object-property@^1.1.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/generate-object-property/-/generate-object-property-1.2.0.tgz#9c0e1c40308ce804f4783618b937fa88f99d50d0"
+ dependencies:
+ is-property "^1.0.0"
+
+get-caller-file@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5"
+
+get-own-enumerable-property-symbols@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-2.0.1.tgz#5c4ad87f2834c4b9b4e84549dc1e0650fb38c24b"
+
+get-stdin@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
+
+get-stream@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
+
+get-value@^2.0.3, get-value@^2.0.6:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
+
+getpass@^0.1.1:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
+ dependencies:
+ assert-plus "^1.0.0"
+
+glob-base@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4"
+ dependencies:
+ glob-parent "^2.0.0"
+ is-glob "^2.0.0"
+
+glob-parent@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28"
+ dependencies:
+ is-glob "^2.0.0"
+
+glob-parent@^3.0.1, glob-parent@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae"
+ dependencies:
+ is-glob "^3.1.0"
+ path-dirname "^1.0.0"
+
+glob-stream@^6.1.0:
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/glob-stream/-/glob-stream-6.1.0.tgz#7045c99413b3eb94888d83ab46d0b404cc7bdde4"
+ dependencies:
+ extend "^3.0.0"
+ glob "^7.1.1"
+ glob-parent "^3.1.0"
+ is-negated-glob "^1.0.0"
+ ordered-read-streams "^1.0.0"
+ pumpify "^1.3.5"
+ readable-stream "^2.1.5"
+ remove-trailing-separator "^1.0.1"
+ to-absolute-glob "^2.0.0"
+ unique-stream "^2.0.2"
+
+glob-watcher@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/glob-watcher/-/glob-watcher-5.0.0.tgz#5e147887f8733134c212bc19697dda19a029eb2e"
+ dependencies:
+ async-done "^1.2.0"
+ chokidar "^2.0.0"
+ just-debounce "^1.0.0"
+ object.defaults "^1.1.0"
+
+glob@^6.0.4:
+ version "6.0.4"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22"
+ dependencies:
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "2 || 3"
+ once "^1.3.0"
+ path-is-absolute "^1.0.0"
+
+glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.0, glob@^7.1.1, glob@^7.1.2, glob@~7.1.1:
+ version "7.1.2"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
+ dependencies:
+ fs.realpath "^1.0.0"
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "^3.0.4"
+ once "^1.3.0"
+ path-is-absolute "^1.0.0"
+
+global-modules@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea"
+ dependencies:
+ global-prefix "^1.0.1"
+ is-windows "^1.0.1"
+ resolve-dir "^1.0.0"
+
+global-prefix@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-1.0.2.tgz#dbf743c6c14992593c655568cb66ed32c0122ebe"
+ dependencies:
+ expand-tilde "^2.0.2"
+ homedir-polyfill "^1.0.1"
+ ini "^1.3.4"
+ is-windows "^1.0.1"
+ which "^1.2.14"
+
+globals@^11.0.1, globals@^11.1.0:
+ version "11.3.0"
+ resolved "https://registry.yarnpkg.com/globals/-/globals-11.3.0.tgz#e04fdb7b9796d8adac9c8f64c14837b2313378b0"
+
+globals@^9.18.0:
+ version "9.18.0"
+ resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a"
+
+globby@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/globby/-/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d"
+ dependencies:
+ array-union "^1.0.1"
+ arrify "^1.0.0"
+ glob "^7.0.3"
+ object-assign "^4.0.1"
+ pify "^2.0.0"
+ pinkie-promise "^2.0.0"
+
+globby@^7.1.1:
+ version "7.1.1"
+ resolved "https://registry.yarnpkg.com/globby/-/globby-7.1.1.tgz#fb2ccff9401f8600945dfada97440cca972b8680"
+ dependencies:
+ array-union "^1.0.1"
+ dir-glob "^2.0.0"
+ glob "^7.1.2"
+ ignore "^3.3.5"
+ pify "^3.0.0"
+ slash "^1.0.0"
+
+globule@^1.0.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/globule/-/globule-1.2.0.tgz#1dc49c6822dd9e8a2fa00ba2a295006e8664bd09"
+ dependencies:
+ glob "~7.1.1"
+ lodash "~4.17.4"
+ minimatch "~3.0.2"
+
+glogg@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/glogg/-/glogg-1.0.1.tgz#dcf758e44789cc3f3d32c1f3562a3676e6a34810"
+ dependencies:
+ sparkles "^1.0.0"
+
+graceful-fs@4.X, graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6:
+ version "4.1.11"
+ resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
+
+"graceful-readlink@>= 1.0.0":
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725"
+
+growly@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
+
+gulp-autoprefixer@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/gulp-autoprefixer/-/gulp-autoprefixer-4.1.0.tgz#064af73cc02cadac8ff34d0bf93ffdfb94ea12aa"
+ dependencies:
+ autoprefixer "^7.0.0"
+ fancy-log "^1.3.2"
+ plugin-error "^0.1.2"
+ postcss "^6.0.1"
+ through2 "^2.0.0"
+ vinyl-sourcemaps-apply "^0.2.0"
+
+gulp-cli@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/gulp-cli/-/gulp-cli-2.0.1.tgz#7847e220cb3662f2be8a6d572bf14e17be5a994b"
+ dependencies:
+ ansi-colors "^1.0.1"
+ archy "^1.0.0"
+ array-sort "^1.0.0"
+ color-support "^1.1.3"
+ concat-stream "^1.6.0"
+ copy-props "^2.0.1"
+ fancy-log "^1.3.2"
+ gulplog "^1.0.0"
+ interpret "^1.1.0"
+ isobject "^3.0.1"
+ liftoff "^2.5.0"
+ matchdep "^2.0.0"
+ mute-stdout "^1.0.0"
+ pretty-hrtime "^1.0.0"
+ replace-homedir "^1.0.0"
+ semver-greatest-satisfied-range "^1.1.0"
+ v8flags "^3.0.1"
+ yargs "^7.1.0"
+
+gulp-csso@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/gulp-csso/-/gulp-csso-3.0.1.tgz#3c160364491e32f2ecefd5d531cf7724f1afb7f0"
+ dependencies:
+ csso "^3.0.0"
+ plugin-error "^0.1.2"
+ vinyl-sourcemaps-apply "^0.2.1"
+
+gulp-embedlr@^0.5.2:
+ version "0.5.2"
+ resolved "https://registry.yarnpkg.com/gulp-embedlr/-/gulp-embedlr-0.5.2.tgz#7d6d21754ac43ca38e46d7edcf4ea6df83053e4a"
+ dependencies:
+ event-stream "*"
+ through2 "~0.4.0"
+
+gulp-plumber@^1.1.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/gulp-plumber/-/gulp-plumber-1.2.0.tgz#18ea03912c9ee483f8a5499973b5954cd90f6ad8"
+ dependencies:
+ chalk "^1.1.3"
+ fancy-log "^1.3.2"
+ plugin-error "^0.1.2"
+ through2 "^2.0.3"
+
+gulp-pug@^3.1.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/gulp-pug/-/gulp-pug-3.3.0.tgz#46982c1439c094c360542ed8ba5c882d3bb711cf"
+ dependencies:
+ gulp-util "^3.0.2"
+ object-assign "^4.1.0"
+ pug ">=2.0.0-alpha <3"
+ through2 "^2.0.0"
+
+gulp-refresh@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/gulp-refresh/-/gulp-refresh-1.1.0.tgz#f4becab6dcc62513162fb69a239cbb9d6db6cabe"
+ dependencies:
+ chalk "^0.5.1"
+ debug "^2.1.0"
+ event-stream "^3.1.7"
+ gulp-util "^3.0.2"
+ mini-lr "^0.1.8"
+
+gulp-sass@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/gulp-sass/-/gulp-sass-3.1.0.tgz#53dc4b68a1f5ddfe4424ab4c247655269a8b74b7"
+ dependencies:
+ gulp-util "^3.0"
+ lodash.clonedeep "^4.3.2"
+ node-sass "^4.2.0"
+ through2 "^2.0.0"
+ vinyl-sourcemaps-apply "^0.2.0"
+
+gulp-sourcemaps@^2.6.2:
+ version "2.6.4"
+ resolved "https://registry.yarnpkg.com/gulp-sourcemaps/-/gulp-sourcemaps-2.6.4.tgz#cbb2008450b1bcce6cd23bf98337be751bf6e30a"
+ dependencies:
+ "@gulp-sourcemaps/identity-map" "1.X"
+ "@gulp-sourcemaps/map-sources" "1.X"
+ acorn "5.X"
+ convert-source-map "1.X"
+ css "2.X"
+ debug-fabulous "1.X"
+ detect-newline "2.X"
+ graceful-fs "4.X"
+ source-map "~0.6.0"
+ strip-bom-string "1.X"
+ through2 "2.X"
+
+gulp-uglify@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/gulp-uglify/-/gulp-uglify-3.0.0.tgz#0df0331d72a0d302e3e37e109485dddf33c6d1ca"
+ dependencies:
+ gulplog "^1.0.0"
+ has-gulplog "^0.1.0"
+ lodash "^4.13.1"
+ make-error-cause "^1.1.1"
+ through2 "^2.0.0"
+ uglify-js "^3.0.5"
+ vinyl-sourcemaps-apply "^0.2.0"
+
+gulp-util@^3.0, gulp-util@^3.0.2, gulp-util@^3.0.7:
+ version "3.0.8"
+ resolved "https://registry.yarnpkg.com/gulp-util/-/gulp-util-3.0.8.tgz#0054e1e744502e27c04c187c3ecc505dd54bbb4f"
+ dependencies:
+ array-differ "^1.0.0"
+ array-uniq "^1.0.2"
+ beeper "^1.0.0"
+ chalk "^1.0.0"
+ dateformat "^2.0.0"
+ fancy-log "^1.1.0"
+ gulplog "^1.0.0"
+ has-gulplog "^0.1.0"
+ lodash._reescape "^3.0.0"
+ lodash._reevaluate "^3.0.0"
+ lodash._reinterpolate "^3.0.0"
+ lodash.template "^3.0.0"
+ minimist "^1.1.0"
+ multipipe "^0.1.2"
+ object-assign "^3.0.0"
+ replace-ext "0.0.1"
+ through2 "^2.0.0"
+ vinyl "^0.5.0"
+
+gulp-watch@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/gulp-watch/-/gulp-watch-5.0.0.tgz#6fb03ab1735972e0d2866475b568555836dfd0eb"
+ dependencies:
+ anymatch "^1.3.0"
+ chokidar "^2.0.0"
+ glob-parent "^3.0.1"
+ gulp-util "^3.0.7"
+ object-assign "^4.1.0"
+ path-is-absolute "^1.0.1"
+ readable-stream "^2.2.2"
+ slash "^1.0.0"
+ vinyl "^2.1.0"
+ vinyl-file "^2.0.0"
+
+gulp@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/gulp/-/gulp-4.0.0.tgz#95766c601dade4a77ed3e7b2b6dc03881b596366"
+ dependencies:
+ glob-watcher "^5.0.0"
+ gulp-cli "^2.0.0"
+ undertaker "^1.0.0"
+ vinyl-fs "^3.0.0"
+
+gulplog@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/gulplog/-/gulplog-1.0.0.tgz#e28c4d45d05ecbbed818363ce8f9c5926229ffe5"
+ dependencies:
+ glogg "^1.0.0"
+
+handlebars@^4.0.3:
+ version "4.0.11"
+ resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.11.tgz#630a35dfe0294bc281edae6ffc5d329fc7982dcc"
+ dependencies:
+ async "^1.4.0"
+ optimist "^0.6.1"
+ source-map "^0.4.4"
+ optionalDependencies:
+ uglify-js "^2.6"
+
+har-schema@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e"
+
+har-schema@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
+
+har-validator@~2.0.6:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-2.0.6.tgz#cdcbc08188265ad119b6a5a7c8ab70eecfb5d27d"
+ dependencies:
+ chalk "^1.1.1"
+ commander "^2.9.0"
+ is-my-json-valid "^2.12.4"
+ pinkie-promise "^2.0.0"
+
+har-validator@~4.2.1:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a"
+ dependencies:
+ ajv "^4.9.1"
+ har-schema "^1.0.5"
+
+har-validator@~5.0.3:
+ version "5.0.3"
+ resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.0.3.tgz#ba402c266194f15956ef15e0fcf242993f6a7dfd"
+ dependencies:
+ ajv "^5.1.0"
+ har-schema "^2.0.0"
+
+has-ansi@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-0.1.0.tgz#84f265aae8c0e6a88a12d7022894b7568894c62e"
+ dependencies:
+ ansi-regex "^0.2.0"
+
+has-ansi@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
+ dependencies:
+ ansi-regex "^2.0.0"
+
+has-flag@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa"
+
+has-flag@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51"
+
+has-gulplog@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/has-gulplog/-/has-gulplog-0.1.0.tgz#6414c82913697da51590397dafb12f22967811ce"
+ dependencies:
+ sparkles "^1.0.0"
+
+has-symbols@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44"
+
+has-unicode@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
+
+has-value@^0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f"
+ dependencies:
+ get-value "^2.0.3"
+ has-values "^0.1.4"
+ isobject "^2.0.0"
+
+has-value@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177"
+ dependencies:
+ get-value "^2.0.6"
+ has-values "^1.0.0"
+ isobject "^3.0.0"
+
+has-values@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771"
+
+has-values@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f"
+ dependencies:
+ is-number "^3.0.0"
+ kind-of "^4.0.0"
+
+has@^1.0.0, has@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/has/-/has-1.0.1.tgz#8461733f538b0837c9361e39a9ab9e9704dc2f28"
+ dependencies:
+ function-bind "^1.0.2"
+
+hash-base@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-2.0.2.tgz#66ea1d856db4e8a5470cadf6fce23ae5244ef2e1"
+ dependencies:
+ inherits "^2.0.1"
+
+hash-base@^3.0.0:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918"
+ dependencies:
+ inherits "^2.0.1"
+ safe-buffer "^5.0.1"
+
+hash.js@^1.0.0, hash.js@^1.0.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.3.tgz#340dedbe6290187151c1ea1d777a3448935df846"
+ dependencies:
+ inherits "^2.0.3"
+ minimalistic-assert "^1.0.0"
+
+hawk@3.1.3, hawk@~3.1.3:
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4"
+ dependencies:
+ boom "2.x.x"
+ cryptiles "2.x.x"
+ hoek "2.x.x"
+ sntp "1.x.x"
+
+hawk@~6.0.2:
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/hawk/-/hawk-6.0.2.tgz#af4d914eb065f9b5ce4d9d11c1cb2126eecc3038"
+ dependencies:
+ boom "4.x.x"
+ cryptiles "3.x.x"
+ hoek "4.x.x"
+ sntp "2.x.x"
+
+history@^3.0.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/history/-/history-3.3.0.tgz#fcedcce8f12975371545d735461033579a6dae9c"
+ dependencies:
+ invariant "^2.2.1"
+ loose-envify "^1.2.0"
+ query-string "^4.2.2"
+ warning "^3.0.0"
+
+hmac-drbg@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
+ dependencies:
+ hash.js "^1.0.3"
+ minimalistic-assert "^1.0.0"
+ minimalistic-crypto-utils "^1.0.1"
+
+hoek@2.x.x:
+ version "2.16.3"
+ resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed"
+
+hoek@4.x.x:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d"
+
+hoist-non-react-statics@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz#aa448cf0986d55cc40773b17174b7dd066cb7cfb"
+
+hoist-non-react-statics@^2.1.0, hoist-non-react-statics@^2.2.1:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.3.1.tgz#343db84c6018c650778898240135a1420ee22ce0"
+
+home-or-tmp@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8"
+ dependencies:
+ os-homedir "^1.0.0"
+ os-tmpdir "^1.0.1"
+
+homedir-polyfill@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz#4c2bbc8a758998feebf5ed68580f76d46768b4bc"
+ dependencies:
+ parse-passwd "^1.0.0"
+
+hosted-git-info@^2.1.4:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.5.0.tgz#6d60e34b3abbc8313062c3b798ef8d901a07af3c"
+
+html-encoding-sniffer@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz#e70d84b94da53aa375e11fe3a351be6642ca46f8"
+ dependencies:
+ whatwg-encoding "^1.0.1"
+
+htmlescape@^1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/htmlescape/-/htmlescape-1.1.1.tgz#3a03edc2214bca3b66424a3e7959349509cb0351"
+
+htmlparser2@^3.9.1:
+ version "3.9.2"
+ resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338"
+ dependencies:
+ domelementtype "^1.3.0"
+ domhandler "^2.3.0"
+ domutils "^1.5.1"
+ entities "^1.1.1"
+ inherits "^2.0.1"
+ readable-stream "^2.0.2"
+
+http-errors@~1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.3.1.tgz#197e22cdebd4198585e8694ef6786197b91ed942"
+ dependencies:
+ inherits "~2.0.1"
+ statuses "1"
+
+http-parser-js@>=0.4.0:
+ version "0.4.9"
+ resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.4.9.tgz#ea1a04fb64adff0242e9974f297dd4c3cad271e1"
+
+http-signature@~1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf"
+ dependencies:
+ assert-plus "^0.2.0"
+ jsprim "^1.2.2"
+ sshpk "^1.7.0"
+
+http-signature@~1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
+ dependencies:
+ assert-plus "^1.0.0"
+ jsprim "^1.2.2"
+ sshpk "^1.7.0"
+
+https-browserify@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
+
+human-format@^0.10.0:
+ version "0.10.0"
+ resolved "https://registry.yarnpkg.com/human-format/-/human-format-0.10.0.tgz#0583c91bfcef0e465a31097f1c627db32c7b502f"
+
+husky@^0.14.3:
+ version "0.14.3"
+ resolved "https://registry.yarnpkg.com/husky/-/husky-0.14.3.tgz#c69ed74e2d2779769a17ba8399b54ce0b63c12c3"
+ dependencies:
+ is-ci "^1.0.10"
+ normalize-path "^1.0.0"
+ strip-indent "^2.0.0"
+
+iconv-lite@0.4, iconv-lite@0.4.19, iconv-lite@^0.4.17, iconv-lite@~0.4.13:
+ version "0.4.19"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b"
+
+iconv-lite@0.4.13:
+ version "0.4.13"
+ resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.13.tgz#1f88aba4ab0b1508e8312acc39345f36e992e2f2"
+
+ieee754@^1.1.4:
+ version "1.1.8"
+ resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4"
+
+ignore@^3.3.3, ignore@^3.3.5, ignore@^3.3.6:
+ version "3.3.7"
+ resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.7.tgz#612289bfb3c220e186a58118618d5be8c1bab021"
+
+immutable@^3.8.2:
+ version "3.8.2"
+ resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3"
+
+import-local@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/import-local/-/import-local-1.0.0.tgz#5e4ffdc03f4fe6c009c6729beb29631c2f8227bc"
+ dependencies:
+ pkg-dir "^2.0.0"
+ resolve-cwd "^2.0.0"
+
+imurmurhash@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
+
+in-publish@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/in-publish/-/in-publish-2.0.0.tgz#e20ff5e3a2afc2690320b6dc552682a9c7fadf51"
+
+indent-string@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80"
+ dependencies:
+ repeating "^2.0.0"
+
+indent-string@^3.0.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289"
+
+index-modules@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/index-modules/-/index-modules-0.3.0.tgz#70a0d80be07ad3e1a6e4849c9aaf08348050a9ae"
+ dependencies:
+ lodash "^4.17.4"
+ promise-toolbox "^0.8.0"
+
+indexes-of@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"
+
+indexof@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d"
+
+inflight@^1.0.4:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
+ dependencies:
+ once "^1.3.0"
+ wrappy "1"
+
+inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
+
+inherits@2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1"
+
+ini@^1.3.4, ini@~1.3.0:
+ version "1.3.5"
+ resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
+
+inline-source-map@~0.5.0:
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/inline-source-map/-/inline-source-map-0.5.0.tgz#4a4c5dd8e4fb5e9b3cda60c822dfadcaee66e0af"
+ dependencies:
+ source-map "~0.4.0"
+
+inline-source-map@~0.6.0:
+ version "0.6.2"
+ resolved "https://registry.yarnpkg.com/inline-source-map/-/inline-source-map-0.6.2.tgz#f9393471c18a79d1724f863fa38b586370ade2a5"
+ dependencies:
+ source-map "~0.5.3"
+
+inquirer@^3.0.6:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9"
+ dependencies:
+ ansi-escapes "^3.0.0"
+ chalk "^2.0.0"
+ cli-cursor "^2.1.0"
+ cli-width "^2.0.0"
+ external-editor "^2.0.4"
+ figures "^2.0.0"
+ lodash "^4.3.0"
+ mute-stream "0.0.7"
+ run-async "^2.2.0"
+ rx-lite "^4.0.8"
+ rx-lite-aggregates "^4.0.8"
+ string-width "^2.1.0"
+ strip-ansi "^4.0.0"
+ through "^2.3.6"
+
+insert-module-globals@^7.0.0:
+ version "7.0.1"
+ resolved "https://registry.yarnpkg.com/insert-module-globals/-/insert-module-globals-7.0.1.tgz#c03bf4e01cb086d5b5e5ace8ad0afe7889d638c3"
+ dependencies:
+ JSONStream "^1.0.3"
+ combine-source-map "~0.7.1"
+ concat-stream "~1.5.1"
+ is-buffer "^1.1.0"
+ lexical-scope "^1.2.0"
+ process "~0.11.0"
+ through2 "^2.0.0"
+ xtend "^4.0.0"
+
+interpret@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614"
+
+intl-format-cache@^2.0.5:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/intl-format-cache/-/intl-format-cache-2.1.0.tgz#04a369fecbfad6da6005bae1f14333332dcf9316"
+
+intl-messageformat-parser@1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/intl-messageformat-parser/-/intl-messageformat-parser-1.4.0.tgz#b43d45a97468cadbe44331d74bb1e8dea44fc075"
+
+intl-messageformat@^2.0.0, intl-messageformat@^2.1.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-2.2.0.tgz#345bcd46de630b7683330c2e52177ff5eab484fc"
+ dependencies:
+ intl-messageformat-parser "1.4.0"
+
+intl-relativeformat@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/intl-relativeformat/-/intl-relativeformat-2.1.0.tgz#010f1105802251f40ac47d0e3e1a201348a255df"
+ dependencies:
+ intl-messageformat "^2.0.0"
+
+invariant@^2.0.0, invariant@^2.1.0, invariant@^2.1.1, invariant@^2.1.2, invariant@^2.2.0, invariant@^2.2.1, invariant@^2.2.2:
+ version "2.2.2"
+ resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360"
+ dependencies:
+ loose-envify "^1.0.0"
+
+invert-kv@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6"
+
+ip-regex@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9"
+
+is-absolute@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-absolute/-/is-absolute-1.0.0.tgz#395e1ae84b11f26ad1795e73c17378e48a301576"
+ dependencies:
+ is-relative "^1.0.0"
+ is-windows "^1.0.1"
+
+is-accessor-descriptor@^0.1.6:
+ version "0.1.6"
+ resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6"
+ dependencies:
+ kind-of "^3.0.2"
+
+is-accessor-descriptor@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656"
+ dependencies:
+ kind-of "^6.0.0"
+
+is-arrayish@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
+
+is-binary-path@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898"
+ dependencies:
+ binary-extensions "^1.0.0"
+
+is-boolean-object@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.0.tgz#98f8b28030684219a95f375cfbd88ce3405dff93"
+
+is-buffer@^1.1.0, is-buffer@^1.1.5:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
+
+is-builtin-module@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe"
+ dependencies:
+ builtin-modules "^1.0.0"
+
+is-callable@^1.1.1, is-callable@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.3.tgz#86eb75392805ddc33af71c92a0eedf74ee7604b2"
+
+is-ci@^1.0.10:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.1.0.tgz#247e4162e7860cebbdaf30b774d6b0ac7dcfe7a5"
+ dependencies:
+ ci-info "^1.0.0"
+
+is-data-descriptor@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56"
+ dependencies:
+ kind-of "^3.0.2"
+
+is-data-descriptor@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7"
+ dependencies:
+ kind-of "^6.0.0"
+
+is-date-object@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16"
+
+is-descriptor@^0.1.0:
+ version "0.1.6"
+ resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca"
+ dependencies:
+ is-accessor-descriptor "^0.1.6"
+ is-data-descriptor "^0.1.4"
+ kind-of "^5.0.0"
+
+is-descriptor@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec"
+ dependencies:
+ is-accessor-descriptor "^1.0.0"
+ is-data-descriptor "^1.0.0"
+ kind-of "^6.0.2"
+
+is-directory@^0.3.1:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1"
+
+is-dotfile@^1.0.0:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1"
+
+is-equal-shallow@^0.1.3:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534"
+ dependencies:
+ is-primitive "^2.0.0"
+
+is-expression@^2.0.1:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-expression/-/is-expression-2.1.0.tgz#91be9d47debcfef077977e9722be6dcfb4465ef0"
+ dependencies:
+ acorn "~3.3.0"
+ object-assign "^4.0.1"
+
+is-expression@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/is-expression/-/is-expression-3.0.0.tgz#39acaa6be7fd1f3471dc42c7416e61c24317ac9f"
+ dependencies:
+ acorn "~4.0.2"
+ object-assign "^4.0.1"
+
+is-extendable@^0.1.0, is-extendable@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89"
+
+is-extendable@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4"
+ dependencies:
+ is-plain-object "^2.0.4"
+
+is-extglob@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0"
+
+is-extglob@^2.1.0, is-extglob@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
+
+is-finite@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa"
+ dependencies:
+ number-is-nan "^1.0.0"
+
+is-fullwidth-code-point@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb"
+ dependencies:
+ number-is-nan "^1.0.0"
+
+is-fullwidth-code-point@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
+
+is-generator-fn@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-1.0.0.tgz#969d49e1bb3329f6bb7f09089be26578b2ddd46a"
+
+is-glob@^2.0.0, is-glob@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863"
+ dependencies:
+ is-extglob "^1.0.0"
+
+is-glob@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a"
+ dependencies:
+ is-extglob "^2.1.0"
+
+is-glob@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.0.tgz#9521c76845cc2610a85203ddf080a958c2ffabc0"
+ dependencies:
+ is-extglob "^2.1.1"
+
+is-ip@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/is-ip/-/is-ip-2.0.0.tgz#68eea07e8a0a0a94c2d080dd674c731ab2a461ab"
+ dependencies:
+ ip-regex "^2.0.0"
+
+is-my-json-valid@^2.12.4:
+ version "2.17.1"
+ resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.17.1.tgz#3da98914a70a22f0a8563ef1511a246c6fc55471"
+ dependencies:
+ generate-function "^2.0.0"
+ generate-object-property "^1.1.0"
+ jsonpointer "^4.0.0"
+ xtend "^4.0.0"
+
+is-negated-glob@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-negated-glob/-/is-negated-glob-1.0.0.tgz#6910bca5da8c95e784b5751b976cf5a10fee36d2"
+
+is-number-object@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.3.tgz#f265ab89a9f445034ef6aff15a8f00b00f551799"
+
+is-number@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f"
+ dependencies:
+ kind-of "^3.0.2"
+
+is-number@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195"
+ dependencies:
+ kind-of "^3.0.2"
+
+is-number@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/is-number/-/is-number-4.0.0.tgz#0026e37f5454d73e356dfe6564699867c6a7f0ff"
+
+is-obj@^1.0.0, is-obj@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f"
+
+is-observable@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/is-observable/-/is-observable-0.2.0.tgz#b361311d83c6e5d726cabf5e250b0237106f5ae2"
+ dependencies:
+ symbol-observable "^0.2.2"
+
+is-odd@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-odd/-/is-odd-1.0.0.tgz#3b8a932eb028b3775c39bb09e91767accdb69088"
+ dependencies:
+ is-number "^3.0.0"
+
+is-path-cwd@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d"
+
+is-path-in-cwd@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz#6477582b8214d602346094567003be8a9eac04dc"
+ dependencies:
+ is-path-inside "^1.0.0"
+
+is-path-inside@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036"
+ dependencies:
+ path-is-inside "^1.0.1"
+
+is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677"
+ dependencies:
+ isobject "^3.0.1"
+
+is-posix-bracket@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4"
+
+is-primitive@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575"
+
+is-promise@^2.0.0, is-promise@^2.1, is-promise@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa"
+
+is-property@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84"
+
+is-regex@^1.0.3, is-regex@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491"
+ dependencies:
+ has "^1.0.1"
+
+is-regexp@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069"
+
+is-relative@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-relative/-/is-relative-1.0.0.tgz#a1bb6935ce8c5dba1e8b9754b9b2dcc020e2260d"
+ dependencies:
+ is-unc-path "^1.0.0"
+
+is-resolvable@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88"
+
+is-stream@^1.0.1, is-stream@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
+
+is-string@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.4.tgz#cc3a9b69857d621e963725a24caeec873b826e64"
+
+is-subset@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/is-subset/-/is-subset-0.1.1.tgz#8a59117d932de1de00f245fcdd39ce43f1e939a6"
+
+is-symbol@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.1.tgz#3cc59f00025194b6ab2e38dbae6689256b660572"
+
+is-typedarray@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
+
+is-unc-path@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-unc-path/-/is-unc-path-1.0.0.tgz#d731e8898ed090a12c352ad2eaed5095ad322c9d"
+ dependencies:
+ unc-path-regex "^0.1.2"
+
+is-utf8@^0.2.0, is-utf8@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
+
+is-valid-glob@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/is-valid-glob/-/is-valid-glob-1.0.0.tgz#29bf3eff701be2d4d315dbacc39bc39fe8f601aa"
+
+is-windows@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.1.tgz#310db70f742d259a16a369202b51af84233310d9"
+
+isarray@0.0.1, isarray@~0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
+
+isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
+
+isexe@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+
+isobject@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"
+ dependencies:
+ isarray "1.0.0"
+
+isobject@^3.0.0, isobject@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
+
+isomorphic-fetch@^2.1.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9"
+ dependencies:
+ node-fetch "^1.0.1"
+ whatwg-fetch ">=0.10.0"
+
+isstream@~0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
+
+istanbul-api@^1.1.14:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/istanbul-api/-/istanbul-api-1.2.1.tgz#0c60a0515eb11c7d65c6b50bba2c6e999acd8620"
+ dependencies:
+ async "^2.1.4"
+ fileset "^2.0.2"
+ istanbul-lib-coverage "^1.1.1"
+ istanbul-lib-hook "^1.1.0"
+ istanbul-lib-instrument "^1.9.1"
+ istanbul-lib-report "^1.1.2"
+ istanbul-lib-source-maps "^1.2.2"
+ istanbul-reports "^1.1.3"
+ js-yaml "^3.7.0"
+ mkdirp "^0.5.1"
+ once "^1.4.0"
+
+istanbul-lib-coverage@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.1.1.tgz#73bfb998885299415c93d38a3e9adf784a77a9da"
+
+istanbul-lib-hook@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-1.1.0.tgz#8538d970372cb3716d53e55523dd54b557a8d89b"
+ dependencies:
+ append-transform "^0.4.0"
+
+istanbul-lib-instrument@^1.7.5, istanbul-lib-instrument@^1.8.0, istanbul-lib-instrument@^1.9.1:
+ version "1.9.1"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.9.1.tgz#250b30b3531e5d3251299fdd64b0b2c9db6b558e"
+ dependencies:
+ babel-generator "^6.18.0"
+ babel-template "^6.16.0"
+ babel-traverse "^6.18.0"
+ babel-types "^6.18.0"
+ babylon "^6.18.0"
+ istanbul-lib-coverage "^1.1.1"
+ semver "^5.3.0"
+
+istanbul-lib-report@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-1.1.2.tgz#922be27c13b9511b979bd1587359f69798c1d425"
+ dependencies:
+ istanbul-lib-coverage "^1.1.1"
+ mkdirp "^0.5.1"
+ path-parse "^1.0.5"
+ supports-color "^3.1.2"
+
+istanbul-lib-source-maps@^1.2.1, istanbul-lib-source-maps@^1.2.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.2.tgz#750578602435f28a0c04ee6d7d9e0f2960e62c1c"
+ dependencies:
+ debug "^3.1.0"
+ istanbul-lib-coverage "^1.1.1"
+ mkdirp "^0.5.1"
+ rimraf "^2.6.1"
+ source-map "^0.5.3"
+
+istanbul-reports@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-1.1.3.tgz#3b9e1e8defb6d18b1d425da8e8b32c5a163f2d10"
+ dependencies:
+ handlebars "^4.0.3"
+
+jest-changed-files@^22.1.4:
+ version "22.1.4"
+ resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-22.1.4.tgz#1f7844bcb739dec07e5899a633c0cb6d5069834e"
+ dependencies:
+ throat "^4.0.0"
+
+jest-cli@^22.1.4:
+ version "22.1.4"
+ resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-22.1.4.tgz#0fe9f3ac881b0cdc00227114c58583a2ebefcc04"
+ dependencies:
+ ansi-escapes "^3.0.0"
+ chalk "^2.0.1"
+ exit "^0.1.2"
+ glob "^7.1.2"
+ graceful-fs "^4.1.11"
+ import-local "^1.0.0"
+ is-ci "^1.0.10"
+ istanbul-api "^1.1.14"
+ istanbul-lib-coverage "^1.1.1"
+ istanbul-lib-instrument "^1.8.0"
+ istanbul-lib-source-maps "^1.2.1"
+ jest-changed-files "^22.1.4"
+ jest-config "^22.1.4"
+ jest-environment-jsdom "^22.1.4"
+ jest-get-type "^22.1.0"
+ jest-haste-map "^22.1.0"
+ jest-message-util "^22.1.0"
+ jest-regex-util "^22.1.0"
+ jest-resolve-dependencies "^22.1.0"
+ jest-runner "^22.1.4"
+ jest-runtime "^22.1.4"
+ jest-snapshot "^22.1.2"
+ jest-util "^22.1.4"
+ jest-worker "^22.1.0"
+ micromatch "^2.3.11"
+ node-notifier "^5.1.2"
+ realpath-native "^1.0.0"
+ rimraf "^2.5.4"
+ slash "^1.0.0"
+ string-length "^2.0.0"
+ strip-ansi "^4.0.0"
+ which "^1.2.12"
+ yargs "^10.0.3"
+
+jest-config@^22.1.4:
+ version "22.1.4"
+ resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-22.1.4.tgz#075ffacce83c3e38cf85b1b9ba0d21bd3ee27ad0"
+ dependencies:
+ chalk "^2.0.1"
+ glob "^7.1.1"
+ jest-environment-jsdom "^22.1.4"
+ jest-environment-node "^22.1.4"
+ jest-get-type "^22.1.0"
+ jest-jasmine2 "^22.1.4"
+ jest-regex-util "^22.1.0"
+ jest-resolve "^22.1.4"
+ jest-util "^22.1.4"
+ jest-validate "^22.1.2"
+ pretty-format "^22.1.0"
+
+jest-diff@^22.1.0:
+ version "22.1.0"
+ resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-22.1.0.tgz#0fad9d96c87b453896bf939df3dc8aac6919ac38"
+ dependencies:
+ chalk "^2.0.1"
+ diff "^3.2.0"
+ jest-get-type "^22.1.0"
+ pretty-format "^22.1.0"
+
+jest-docblock@^22.1.0:
+ version "22.1.0"
+ resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-22.1.0.tgz#3fe5986d5444cbcb149746eb4b07c57c5a464dfd"
+ dependencies:
+ detect-newline "^2.1.0"
+
+jest-environment-jsdom@^22.1.4:
+ version "22.1.4"
+ resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-22.1.4.tgz#704518ce8375f7ec5de048d1e9c4268b08a03e00"
+ dependencies:
+ jest-mock "^22.1.0"
+ jest-util "^22.1.4"
+ jsdom "^11.5.1"
+
+jest-environment-node@^22.1.4:
+ version "22.1.4"
+ resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-22.1.4.tgz#0f2946e8f8686ce6c5d8fa280ce1cd8d58e869eb"
+ dependencies:
+ jest-mock "^22.1.0"
+ jest-util "^22.1.4"
+
+jest-get-type@^21.2.0:
+ version "21.2.0"
+ resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-21.2.0.tgz#f6376ab9db4b60d81e39f30749c6c466f40d4a23"
+
+jest-get-type@^22.1.0:
+ version "22.1.0"
+ resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-22.1.0.tgz#4e90af298ed6181edc85d2da500dbd2753e0d5a9"
+
+jest-haste-map@^22.1.0:
+ version "22.1.0"
+ resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-22.1.0.tgz#1174c6ff393f9818ebf1163710d8868b5370da2a"
+ dependencies:
+ fb-watchman "^2.0.0"
+ graceful-fs "^4.1.11"
+ jest-docblock "^22.1.0"
+ jest-worker "^22.1.0"
+ micromatch "^2.3.11"
+ sane "^2.0.0"
+
+jest-jasmine2@^22.1.4:
+ version "22.1.4"
+ resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-22.1.4.tgz#cada0baf50a220c616a9575728b80d4ddedebe8b"
+ dependencies:
+ callsites "^2.0.0"
+ chalk "^2.0.1"
+ co "^4.6.0"
+ expect "^22.1.0"
+ graceful-fs "^4.1.11"
+ is-generator-fn "^1.0.0"
+ jest-diff "^22.1.0"
+ jest-matcher-utils "^22.1.0"
+ jest-message-util "^22.1.0"
+ jest-snapshot "^22.1.2"
+ source-map-support "^0.5.0"
+
+jest-leak-detector@^22.1.0:
+ version "22.1.0"
+ resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-22.1.0.tgz#08376644cee07103da069baac19adb0299b772c2"
+ dependencies:
+ pretty-format "^22.1.0"
+
+jest-matcher-utils@^22.1.0:
+ version "22.1.0"
+ resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-22.1.0.tgz#e164665b5d313636ac29f7f6fe9ef0a6ce04febc"
+ dependencies:
+ chalk "^2.0.1"
+ jest-get-type "^22.1.0"
+ pretty-format "^22.1.0"
+
+jest-message-util@^22.1.0:
+ version "22.1.0"
+ resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-22.1.0.tgz#51ba0794cb6e579bfc4e9adfac452f9f1a0293fc"
+ dependencies:
+ "@babel/code-frame" "^7.0.0-beta.35"
+ chalk "^2.0.1"
+ micromatch "^2.3.11"
+ slash "^1.0.0"
+ stack-utils "^1.0.1"
+
+jest-mock@^22.1.0:
+ version "22.1.0"
+ resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-22.1.0.tgz#87ec21c0599325671c9a23ad0e05c86fb5879b61"
+
+jest-regex-util@^22.1.0:
+ version "22.1.0"
+ resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-22.1.0.tgz#5daf2fe270074b6da63e5d85f1c9acc866768f53"
+
+jest-resolve-dependencies@^22.1.0:
+ version "22.1.0"
+ resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-22.1.0.tgz#340e4139fb13315cd43abc054e6c06136be51e31"
+ dependencies:
+ jest-regex-util "^22.1.0"
+
+jest-resolve@^22.1.4:
+ version "22.1.4"
+ resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-22.1.4.tgz#72b9b371eaac48f84aad4ad732222ffe37692602"
+ dependencies:
+ browser-resolve "^1.11.2"
+ chalk "^2.0.1"
+
+jest-runner@^22.1.4:
+ version "22.1.4"
+ resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-22.1.4.tgz#e039039110cb1b31febc0f99e349bf7c94304a2f"
+ dependencies:
+ exit "^0.1.2"
+ jest-config "^22.1.4"
+ jest-docblock "^22.1.0"
+ jest-haste-map "^22.1.0"
+ jest-jasmine2 "^22.1.4"
+ jest-leak-detector "^22.1.0"
+ jest-message-util "^22.1.0"
+ jest-runtime "^22.1.4"
+ jest-util "^22.1.4"
+ jest-worker "^22.1.0"
+ throat "^4.0.0"
+
+jest-runtime@^22.1.4:
+ version "22.1.4"
+ resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-22.1.4.tgz#1474d9f5cda518b702e0b25a17d4ef3fc563a20c"
+ dependencies:
+ babel-core "^6.0.0"
+ babel-jest "^22.1.0"
+ babel-plugin-istanbul "^4.1.5"
+ chalk "^2.0.1"
+ convert-source-map "^1.4.0"
+ exit "^0.1.2"
+ graceful-fs "^4.1.11"
+ jest-config "^22.1.4"
+ jest-haste-map "^22.1.0"
+ jest-regex-util "^22.1.0"
+ jest-resolve "^22.1.4"
+ jest-util "^22.1.4"
+ json-stable-stringify "^1.0.1"
+ micromatch "^2.3.11"
+ realpath-native "^1.0.0"
+ slash "^1.0.0"
+ strip-bom "3.0.0"
+ write-file-atomic "^2.1.0"
+ yargs "^10.0.3"
+
+jest-snapshot@^22.1.2:
+ version "22.1.2"
+ resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-22.1.2.tgz#b270cf6e3098f33aceeafda02b13eb0933dc6139"
+ dependencies:
+ chalk "^2.0.1"
+ jest-diff "^22.1.0"
+ jest-matcher-utils "^22.1.0"
+ mkdirp "^0.5.1"
+ natural-compare "^1.4.0"
+ pretty-format "^22.1.0"
+
+jest-util@^22.1.4:
+ version "22.1.4"
+ resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-22.1.4.tgz#ac8cbd43ee654102f1941f3f0e9d1d789a8b6a9b"
+ dependencies:
+ callsites "^2.0.0"
+ chalk "^2.0.1"
+ graceful-fs "^4.1.11"
+ is-ci "^1.0.10"
+ jest-message-util "^22.1.0"
+ jest-validate "^22.1.2"
+ mkdirp "^0.5.1"
+
+jest-validate@^21.1.0:
+ version "21.2.1"
+ resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-21.2.1.tgz#cc0cbca653cd54937ba4f2a111796774530dd3c7"
+ dependencies:
+ chalk "^2.0.1"
+ jest-get-type "^21.2.0"
+ leven "^2.1.0"
+ pretty-format "^21.2.1"
+
+jest-validate@^22.1.2:
+ version "22.1.2"
+ resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-22.1.2.tgz#c3b06bcba7bd9a850919fe336b5f2a8c3a239404"
+ dependencies:
+ chalk "^2.0.1"
+ jest-get-type "^22.1.0"
+ leven "^2.1.0"
+ pretty-format "^22.1.0"
+
+jest-worker@^22.1.0:
+ version "22.1.0"
+ resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-22.1.0.tgz#0987832fe58fbdc205357f4c19b992446368cafb"
+ dependencies:
+ merge-stream "^1.0.1"
+
+jest@^22.0.4:
+ version "22.1.4"
+ resolved "https://registry.yarnpkg.com/jest/-/jest-22.1.4.tgz#9ec71373a38f40ff92a3e5e96ae85687c181bb72"
+ dependencies:
+ jest-cli "^22.1.4"
+
+"jquery@1.9.1 - 3":
+ version "3.3.1"
+ resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.3.1.tgz#958ce29e81c9790f31be7792df5d4d95fc57fbca"
+
+js-base64@^2.1.8:
+ version "2.4.3"
+ resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.4.3.tgz#2e545ec2b0f2957f41356510205214e98fad6582"
+
+js-stringify@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/js-stringify/-/js-stringify-1.0.2.tgz#1736fddfd9724f28a3682adc6230ae7e4e9679db"
+
+js-tokens@^3.0.0, js-tokens@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
+
+js-yaml@^3.7.0, js-yaml@^3.9.0, js-yaml@^3.9.1:
+ version "3.10.0"
+ resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.10.0.tgz#2e78441646bd4682e963f22b6e92823c309c62dc"
+ dependencies:
+ argparse "^1.0.7"
+ esprima "^4.0.0"
+
+jsbn@~0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
+
+jsdom@^11.5.1:
+ version "11.6.2"
+ resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-11.6.2.tgz#25d1ef332d48adf77fc5221fe2619967923f16bb"
+ dependencies:
+ abab "^1.0.4"
+ acorn "^5.3.0"
+ acorn-globals "^4.1.0"
+ array-equal "^1.0.0"
+ browser-process-hrtime "^0.1.2"
+ content-type-parser "^1.0.2"
+ cssom ">= 0.3.2 < 0.4.0"
+ cssstyle ">= 0.2.37 < 0.3.0"
+ domexception "^1.0.0"
+ escodegen "^1.9.0"
+ html-encoding-sniffer "^1.0.2"
+ left-pad "^1.2.0"
+ nwmatcher "^1.4.3"
+ parse5 "4.0.0"
+ pn "^1.1.0"
+ request "^2.83.0"
+ request-promise-native "^1.0.5"
+ sax "^1.2.4"
+ symbol-tree "^3.2.2"
+ tough-cookie "^2.3.3"
+ w3c-hr-time "^1.0.1"
+ webidl-conversions "^4.0.2"
+ whatwg-encoding "^1.0.3"
+ whatwg-url "^6.4.0"
+ ws "^4.0.0"
+ xml-name-validator "^3.0.0"
+
+jsesc@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b"
+
+jsesc@~0.5.0:
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
+
+json-parse-better-errors@^1.0.0, json-parse-better-errors@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.1.tgz#50183cd1b2d25275de069e9e71b467ac9eab973a"
+
+json-rpc-peer@^0.13.1:
+ version "0.13.2"
+ resolved "https://registry.yarnpkg.com/json-rpc-peer/-/json-rpc-peer-0.13.2.tgz#76ef4374ab32d133017a0499b7211389e8c6d50a"
+ dependencies:
+ babel-runtime "^6.23.0"
+ json-rpc-protocol "^0.11.3"
+ lodash "^4.17.4"
+ readable-stream "^2.2.9"
+
+json-rpc-peer@^0.14.0:
+ version "0.14.0"
+ resolved "https://registry.yarnpkg.com/json-rpc-peer/-/json-rpc-peer-0.14.0.tgz#7bcccde3b22a781973930049e0c3de2e6ef2b798"
+ dependencies:
+ babel-runtime "^6.23.0"
+ json-rpc-protocol "^0.11.3"
+ lodash "^4.17.4"
+
+json-rpc-protocol@^0.11.3:
+ version "0.11.3"
+ resolved "https://registry.yarnpkg.com/json-rpc-protocol/-/json-rpc-protocol-0.11.3.tgz#9290f49efa7e57951aa56cc2dc4d3a00204d6ebb"
+ dependencies:
+ lodash "^4.17.4"
+ make-error "^1.3.0"
+
+json-schema-traverse@^0.3.0:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340"
+
+json-schema@0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
+
+json-stable-stringify-without-jsonify@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
+
+json-stable-stringify@^1.0.0, json-stable-stringify@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af"
+ dependencies:
+ jsonify "~0.0.0"
+
+json-stable-stringify@~0.0.0:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-0.0.1.tgz#611c23e814db375527df851193db59dd2af27f45"
+ dependencies:
+ jsonify "~0.0.0"
+
+json-stringify-safe@~5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
+
+json5@^0.5.1:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821"
+
+jsonify@~0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
+
+jsonparse@^1.2.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
+
+jsonpointer@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9"
+
+jsonrpc-websocket-client@^0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/jsonrpc-websocket-client/-/jsonrpc-websocket-client-0.1.2.tgz#93ff7c4bf81146b968de535e2c8738554fcc6242"
+ dependencies:
+ event-to-promise "^0.7.0"
+ json-rpc-peer "^0.13.1"
+ lodash "^4.15.0"
+ make-error "^1.1.1"
+ ws "^1.0.1"
+
+jsonrpc-websocket-client@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/jsonrpc-websocket-client/-/jsonrpc-websocket-client-0.2.0.tgz#553e35687e8d6cf81a9a6655734ba9774caa90c2"
+ dependencies:
+ event-to-promise "^0.8.0"
+ json-rpc-peer "^0.14.0"
+ lodash "^4.17.4"
+ make-error "^1.3.0"
+ promise-toolbox "^0.8.3"
+ ws "^3.0.0"
+
+jsprim@^1.2.2:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
+ dependencies:
+ assert-plus "1.0.0"
+ extsprintf "1.3.0"
+ json-schema "0.2.3"
+ verror "1.10.0"
+
+jstransformer@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/jstransformer/-/jstransformer-1.0.0.tgz#ed8bf0921e2f3f1ed4d5c1a44f68709ed24722c3"
+ dependencies:
+ is-promise "^2.0.0"
+ promise "^7.0.1"
+
+jsx-ast-utils@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.0.1.tgz#e801b1b39985e20fffc87b40e3748080e2dcac7f"
+ dependencies:
+ array-includes "^3.0.3"
+
+just-debounce@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/just-debounce/-/just-debounce-1.0.0.tgz#87fccfaeffc0b68cd19d55f6722943f929ea35ea"
+
+just-reduce-object@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/just-reduce-object/-/just-reduce-object-1.0.3.tgz#08b69499dba3504f7a46b73937b4cdf7a5a05a2e"
+
+keycode@^2.1.0:
+ version "2.1.9"
+ resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.1.9.tgz#964a23c54e4889405b4861a5c9f0480d45141dfa"
+
+kind-of@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-1.1.0.tgz#140a3d2d41a36d2efcfa9377b62c24f8495a5c44"
+
+kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.1.0, kind-of@^3.2.0:
+ version "3.2.2"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"
+ dependencies:
+ is-buffer "^1.1.5"
+
+kind-of@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57"
+ dependencies:
+ is-buffer "^1.1.5"
+
+kind-of@^5.0.0, kind-of@^5.0.2:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d"
+
+kind-of@^6.0.0, kind-of@^6.0.2:
+ version "6.0.2"
+ resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051"
+
+kindof@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/kindof/-/kindof-2.0.0.tgz#c335baf603a77cc37f8b406b73b6463fdbdf1abe"
+
+labeled-stream-splicer@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/labeled-stream-splicer/-/labeled-stream-splicer-2.0.0.tgz#a52e1d138024c00b86b1c0c91f677918b8ae0a59"
+ dependencies:
+ inherits "^2.0.1"
+ isarray "~0.0.1"
+ stream-splicer "^2.0.0"
+
+last-run@^1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/last-run/-/last-run-1.1.1.tgz#45b96942c17b1c79c772198259ba943bebf8ca5b"
+ dependencies:
+ default-resolution "^2.0.0"
+ es6-weak-map "^2.0.1"
+
+later@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/later/-/later-1.2.0.tgz#f2cf6c4dd7956dd2f520adf0329836e9876bad0f"
+
+lazy-cache@^1.0.3:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e"
+
+lazy-cache@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-2.0.2.tgz#b9190a4f913354694840859f8a8f7084d8822264"
+ dependencies:
+ set-getter "^0.1.0"
+
+lazystream@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.0.tgz#f6995fe0f820392f61396be89462407bb77168e4"
+ dependencies:
+ readable-stream "^2.0.5"
+
+lcid@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835"
+ dependencies:
+ invert-kv "^1.0.0"
+
+lead@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/lead/-/lead-1.0.0.tgz#6f14f99a37be3a9dd784f5495690e5903466ee42"
+ dependencies:
+ flush-write-stream "^1.0.2"
+
+left-pad@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/left-pad/-/left-pad-1.2.0.tgz#d30a73c6b8201d8f7d8e7956ba9616087a68e0ee"
+
+leven@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580"
+
+levn@^0.3.0, levn@~0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
+ dependencies:
+ prelude-ls "~1.1.2"
+ type-check "~0.3.2"
+
+lexical-scope@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/lexical-scope/-/lexical-scope-1.2.0.tgz#fcea5edc704a4b3a8796cdca419c3a0afaf22df4"
+ dependencies:
+ astw "^2.0.0"
+
+liftoff@^2.5.0:
+ version "2.5.0"
+ resolved "https://registry.yarnpkg.com/liftoff/-/liftoff-2.5.0.tgz#2009291bb31cea861bbf10a7c15a28caf75c31ec"
+ dependencies:
+ extend "^3.0.0"
+ findup-sync "^2.0.0"
+ fined "^1.0.1"
+ flagged-respawn "^1.0.0"
+ is-plain-object "^2.0.4"
+ object.map "^1.0.0"
+ rechoir "^0.6.2"
+ resolve "^1.1.7"
+
+lint-staged@^6.0.0:
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-6.1.0.tgz#28f600c10a6cbd249ceb003118a1552e53544a93"
+ dependencies:
+ app-root-path "^2.0.0"
+ chalk "^2.1.0"
+ commander "^2.11.0"
+ cosmiconfig "^4.0.0"
+ debug "^3.1.0"
+ dedent "^0.7.0"
+ execa "^0.8.0"
+ find-parent-dir "^0.3.0"
+ is-glob "^4.0.0"
+ jest-validate "^21.1.0"
+ listr "^0.13.0"
+ lodash "^4.17.4"
+ log-symbols "^2.0.0"
+ minimatch "^3.0.0"
+ npm-which "^3.0.1"
+ p-map "^1.1.1"
+ path-is-inside "^1.0.2"
+ pify "^3.0.0"
+ staged-git-files "0.0.4"
+ stringify-object "^3.2.0"
+
+listr-silent-renderer@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz#924b5a3757153770bf1a8e3fbf74b8bbf3f9242e"
+
+listr-update-renderer@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/listr-update-renderer/-/listr-update-renderer-0.4.0.tgz#344d980da2ca2e8b145ba305908f32ae3f4cc8a7"
+ dependencies:
+ chalk "^1.1.3"
+ cli-truncate "^0.2.1"
+ elegant-spinner "^1.0.1"
+ figures "^1.7.0"
+ indent-string "^3.0.0"
+ log-symbols "^1.0.2"
+ log-update "^1.0.2"
+ strip-ansi "^3.0.1"
+
+listr-verbose-renderer@^0.4.0:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/listr-verbose-renderer/-/listr-verbose-renderer-0.4.1.tgz#8206f4cf6d52ddc5827e5fd14989e0e965933a35"
+ dependencies:
+ chalk "^1.1.3"
+ cli-cursor "^1.0.2"
+ date-fns "^1.27.2"
+ figures "^1.7.0"
+
+listr@^0.13.0:
+ version "0.13.0"
+ resolved "https://registry.yarnpkg.com/listr/-/listr-0.13.0.tgz#20bb0ba30bae660ee84cc0503df4be3d5623887d"
+ dependencies:
+ chalk "^1.1.3"
+ cli-truncate "^0.2.1"
+ figures "^1.7.0"
+ indent-string "^2.1.0"
+ is-observable "^0.2.0"
+ is-promise "^2.1.0"
+ is-stream "^1.1.0"
+ listr-silent-renderer "^1.1.1"
+ listr-update-renderer "^0.4.0"
+ listr-verbose-renderer "^0.4.0"
+ log-symbols "^1.0.2"
+ log-update "^1.0.2"
+ ora "^0.2.3"
+ p-map "^1.1.1"
+ rxjs "^5.4.2"
+ stream-to-observable "^0.2.0"
+ strip-ansi "^3.0.1"
+
+livereload-js@^2.2.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/livereload-js/-/livereload-js-2.3.0.tgz#c3ab22e8aaf5bf3505d80d098cbad67726548c9a"
+
+load-json-file@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0"
+ dependencies:
+ graceful-fs "^4.1.2"
+ parse-json "^2.2.0"
+ pify "^2.0.0"
+ pinkie-promise "^2.0.0"
+ strip-bom "^2.0.0"
+
+load-json-file@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8"
+ dependencies:
+ graceful-fs "^4.1.2"
+ parse-json "^2.2.0"
+ pify "^2.0.0"
+ strip-bom "^3.0.0"
+
+locate-path@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
+ dependencies:
+ p-locate "^2.0.0"
+ path-exists "^3.0.0"
+
+lodash-compat@^3.10.1:
+ version "3.10.2"
+ resolved "https://registry.yarnpkg.com/lodash-compat/-/lodash-compat-3.10.2.tgz#c6940128a9d30f8e902cd2cf99fd0cba4ecfc183"
+
+lodash-es@^4.2.0, lodash-es@^4.2.1:
+ version "4.17.4"
+ resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.4.tgz#dcc1d7552e150a0640073ba9cb31d70f032950e7"
+
+lodash._basecopy@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36"
+
+lodash._basetostring@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/lodash._basetostring/-/lodash._basetostring-3.0.1.tgz#d1861d877f824a52f669832dcaf3ee15566a07d5"
+
+lodash._basevalues@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/lodash._basevalues/-/lodash._basevalues-3.0.0.tgz#5b775762802bde3d3297503e26300820fdf661b7"
+
+lodash._getnative@^3.0.0:
+ version "3.9.1"
+ resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5"
+
+lodash._isiterateecall@^3.0.0:
+ version "3.0.9"
+ resolved "https://registry.yarnpkg.com/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz#5203ad7ba425fae842460e696db9cf3e6aac057c"
+
+lodash._reescape@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/lodash._reescape/-/lodash._reescape-3.0.0.tgz#2b1d6f5dfe07c8a355753e5f27fac7f1cde1616a"
+
+lodash._reevaluate@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/lodash._reevaluate/-/lodash._reevaluate-3.0.0.tgz#58bc74c40664953ae0b124d806996daca431e2ed"
+
+lodash._reinterpolate@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
+
+lodash._root@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/lodash._root/-/lodash._root-3.0.1.tgz#fba1c4524c19ee9a5f8136b4609f017cf4ded692"
+
+lodash.assign@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7"
+
+lodash.clonedeep@^4.3.2:
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
+
+lodash.cond@^4.3.0:
+ version "4.5.2"
+ resolved "https://registry.yarnpkg.com/lodash.cond/-/lodash.cond-4.5.2.tgz#f471a1da486be60f6ab955d17115523dd1d255d5"
+
+lodash.escape@^3.0.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-3.2.0.tgz#995ee0dc18c1b48cc92effae71a10aab5b487698"
+ dependencies:
+ lodash._root "^3.0.0"
+
+lodash.findlast@^4.6.0:
+ version "4.6.0"
+ resolved "https://registry.yarnpkg.com/lodash.findlast/-/lodash.findlast-4.6.0.tgz#ea8bb78cf2e7e7804fc8aeb7d1953e07fe31fbc8"
+
+lodash.flattendeep@^4.4.0:
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2"
+
+lodash.foreach@^4.5.0:
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz#1a6a35eace401280c7f06dddec35165ab27e3e53"
+
+lodash.get@^4.4.2:
+ version "4.4.2"
+ resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
+
+lodash.invert@^4.3.0:
+ version "4.3.0"
+ resolved "https://registry.yarnpkg.com/lodash.invert/-/lodash.invert-4.3.0.tgz#8ffe20d4b616f56bea8f1aa0c6ebd80dcf742aee"
+
+lodash.isarguments@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a"
+
+lodash.isarray@^3.0.0:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55"
+
+lodash.keys@^3.0.0:
+ version "3.1.2"
+ resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a"
+ dependencies:
+ lodash._getnative "^3.0.0"
+ lodash.isarguments "^3.0.0"
+ lodash.isarray "^3.0.0"
+
+lodash.mapvalues@^4.6.0:
+ version "4.6.0"
+ resolved "https://registry.yarnpkg.com/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz#1bafa5005de9dd6f4f26668c30ca37230cc9689c"
+
+lodash.memoize@~3.0.3:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-3.0.4.tgz#2dcbd2c287cbc0a55cc42328bd0c736150d53e3f"
+
+lodash.mergewith@^4.6.0:
+ version "4.6.0"
+ resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.0.tgz#150cf0a16791f5903b8891eab154609274bdea55"
+
+lodash.restparam@^3.0.0:
+ version "3.6.1"
+ resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805"
+
+lodash.sortby@^4.7.0:
+ version "4.7.0"
+ resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
+
+lodash.template@^3.0.0:
+ version "3.6.2"
+ resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-3.6.2.tgz#f8cdecc6169a255be9098ae8b0c53d378931d14f"
+ dependencies:
+ lodash._basecopy "^3.0.0"
+ lodash._basetostring "^3.0.0"
+ lodash._basevalues "^3.0.0"
+ lodash._isiterateecall "^3.0.0"
+ lodash._reinterpolate "^3.0.0"
+ lodash.escape "^3.0.0"
+ lodash.keys "^3.0.0"
+ lodash.restparam "^3.0.0"
+ lodash.templatesettings "^3.0.0"
+
+lodash.templatesettings@^3.0.0:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz#fb307844753b66b9f1afa54e262c745307dba8e5"
+ dependencies:
+ lodash._reinterpolate "^3.0.0"
+ lodash.escape "^3.0.0"
+
+lodash.uniq@^4.5.0:
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
+
+lodash@^4.0.0, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.16.6, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.6.1, lodash@~4.17.4:
+ version "4.17.4"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
+
+log-symbols@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18"
+ dependencies:
+ chalk "^1.0.0"
+
+log-symbols@^2.0.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a"
+ dependencies:
+ chalk "^2.0.1"
+
+log-update@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/log-update/-/log-update-1.0.2.tgz#19929f64c4093d2d2e7075a1dad8af59c296b8d1"
+ dependencies:
+ ansi-escapes "^1.0.0"
+ cli-cursor "^1.0.2"
+
+longest@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097"
+
+loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.0, loose-envify@^1.3.1:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848"
+ dependencies:
+ js-tokens "^3.0.0"
+
+loud-rejection@^1.0.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f"
+ dependencies:
+ currently-unhandled "^0.4.1"
+ signal-exit "^3.0.0"
+
+lru-cache@^4.0.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55"
+ dependencies:
+ pseudomap "^1.0.2"
+ yallist "^2.1.2"
+
+lru-queue@0.1:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/lru-queue/-/lru-queue-0.1.0.tgz#2738bd9f0d3cf4f84490c5736c48699ac632cda3"
+ dependencies:
+ es5-ext "~0.10.2"
+
+make-error-cause@^1.1.1:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/make-error-cause/-/make-error-cause-1.2.2.tgz#df0388fcd0b37816dff0a5fb8108939777dcbc9d"
+ dependencies:
+ make-error "^1.2.0"
+
+make-error@^1.0.4, make-error@^1.1.1, make-error@^1.2.0, make-error@^1.2.1, make-error@^1.2.3, make-error@^1.3.0, make-error@^1.3.2:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.2.tgz#8762ffad2444dd8ff1f7c819629fa28e24fea1c4"
+
+make-iterator@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/make-iterator/-/make-iterator-1.0.0.tgz#57bef5dc85d23923ba23767324d8e8f8f3d9694b"
+ dependencies:
+ kind-of "^3.1.0"
+
+makeerror@1.0.x:
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c"
+ dependencies:
+ tmpl "1.0.x"
+
+map-cache@^0.2.0, map-cache@^0.2.2:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
+
+map-obj@^1.0.0, map-obj@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
+
+map-stream@~0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194"
+
+map-visit@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f"
+ dependencies:
+ object-visit "^1.0.0"
+
+marked@^0.3.9:
+ version "0.3.12"
+ resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.12.tgz#7cf25ff2252632f3fe2406bde258e94eee927519"
+
+matchdep@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/matchdep/-/matchdep-2.0.0.tgz#c6f34834a0d8dbc3b37c27ee8bbcb27c7775582e"
+ dependencies:
+ findup-sync "^2.0.0"
+ micromatch "^3.0.4"
+ resolve "^1.4.0"
+ stack-trace "0.0.10"
+
+md5.js@^1.3.4:
+ version "1.3.4"
+ resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.4.tgz#e9bdbde94a20a5ac18b04340fc5764d5b09d901d"
+ dependencies:
+ hash-base "^3.0.0"
+ inherits "^2.0.1"
+
+mdn-data@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-1.1.0.tgz#a7056319da95a2d0881267d7263075042eb061e2"
+
+media-typer@0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
+
+mem@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76"
+ dependencies:
+ mimic-fn "^1.0.0"
+
+memoizee@0.4.X:
+ version "0.4.11"
+ resolved "https://registry.yarnpkg.com/memoizee/-/memoizee-0.4.11.tgz#bde9817663c9e40fdb2a4ea1c367296087ae8c8f"
+ dependencies:
+ d "1"
+ es5-ext "^0.10.30"
+ es6-weak-map "^2.0.2"
+ event-emitter "^0.3.5"
+ is-promise "^2.1"
+ lru-queue "0.1"
+ next-tick "1"
+ timers-ext "^0.1.2"
+
+meow@^3.7.0:
+ version "3.7.0"
+ resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb"
+ dependencies:
+ camelcase-keys "^2.0.0"
+ decamelize "^1.1.2"
+ loud-rejection "^1.0.0"
+ map-obj "^1.0.1"
+ minimist "^1.1.3"
+ normalize-package-data "^2.3.4"
+ object-assign "^4.0.1"
+ read-pkg-up "^1.0.1"
+ redent "^1.0.0"
+ trim-newlines "^1.0.0"
+
+merge-stream@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-1.0.1.tgz#4041202d508a342ba00174008df0c251b8c135e1"
+ dependencies:
+ readable-stream "^2.0.1"
+
+merge@^1.1.3:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.0.tgz#7531e39d4949c281a66b8c5a6e0265e8b05894da"
+
+micromatch@^2.1.5, micromatch@^2.3.11:
+ version "2.3.11"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565"
+ dependencies:
+ arr-diff "^2.0.0"
+ array-unique "^0.2.1"
+ braces "^1.8.2"
+ expand-brackets "^0.1.4"
+ extglob "^0.3.1"
+ filename-regex "^2.0.0"
+ is-extglob "^1.0.0"
+ is-glob "^2.0.1"
+ kind-of "^3.0.2"
+ normalize-path "^2.0.1"
+ object.omit "^2.0.0"
+ parse-glob "^3.0.4"
+ regex-cache "^0.4.2"
+
+micromatch@^3.0.4, micromatch@^3.1.4:
+ version "3.1.5"
+ resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.5.tgz#d05e168c206472dfbca985bfef4f57797b4cd4ba"
+ dependencies:
+ arr-diff "^4.0.0"
+ array-unique "^0.3.2"
+ braces "^2.3.0"
+ define-property "^1.0.0"
+ extend-shallow "^2.0.1"
+ extglob "^2.0.2"
+ fragment-cache "^0.2.1"
+ kind-of "^6.0.0"
+ nanomatch "^1.2.5"
+ object.pick "^1.3.0"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.1"
+
+miller-rabin@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d"
+ dependencies:
+ bn.js "^4.0.0"
+ brorand "^1.0.1"
+
+mime-db@~1.30.0:
+ version "1.30.0"
+ resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01"
+
+mime-types@^2.1.12, mime-types@~2.1.15, mime-types@~2.1.17, mime-types@~2.1.7:
+ version "2.1.17"
+ resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a"
+ dependencies:
+ mime-db "~1.30.0"
+
+mime@^1.4.1:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
+
+mimic-fn@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18"
+
+mini-lr@^0.1.8:
+ version "0.1.9"
+ resolved "https://registry.yarnpkg.com/mini-lr/-/mini-lr-0.1.9.tgz#02199d27347953d1fd1d6dbded4261f187b2d0f6"
+ dependencies:
+ body-parser "~1.14.0"
+ debug "^2.2.0"
+ faye-websocket "~0.7.2"
+ livereload-js "^2.2.0"
+ parseurl "~1.3.0"
+ qs "~2.2.3"
+
+minimalistic-assert@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3"
+
+minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
+
+"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4, minimatch@~3.0.2:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
+ dependencies:
+ brace-expansion "^1.1.7"
+
+minimist@0.0.8:
+ version "0.0.8"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
+
+minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
+
+minimist@~0.0.1:
+ version "0.0.10"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
+
+mixin-deep@^1.2.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.0.tgz#47a8732ba97799457c8c1eca28f95132d7e8150a"
+ dependencies:
+ for-in "^1.0.2"
+ is-extendable "^1.0.1"
+
+"mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
+ dependencies:
+ minimist "0.0.8"
+
+modular-css-core@^7.2.0:
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/modular-css-core/-/modular-css-core-7.2.0.tgz#a7ca68bc7f04d641777a7d32ee6105965b499f5f"
+ dependencies:
+ dependency-graph "^0.6.0"
+ escape-string-regexp "^1.0.5"
+ lodash.findlast "^4.6.0"
+ lodash.foreach "^4.5.0"
+ lodash.get "^4.4.2"
+ lodash.invert "^4.3.0"
+ lodash.mapvalues "^4.6.0"
+ lodash.uniq "^4.5.0"
+ p-each-series "^1.0.0"
+ postcss "^6.0.11"
+ postcss-selector-parser "^3.0.0"
+ postcss-url "^7.3.0"
+ postcss-value-parser "^3.3.0"
+ resolve-from "^4.0.0"
+ "true-case-path" "^1.0.2"
+ unique-slug "^2.0.0"
+
+modular-cssify@^7.2.0:
+ version "7.2.0"
+ resolved "https://registry.yarnpkg.com/modular-cssify/-/modular-cssify-7.2.0.tgz#20accdc485e6e9d6960872dc9b6561fda49ff0af"
+ dependencies:
+ lodash.foreach "^4.5.0"
+ mkdirp "^0.5.1"
+ modular-css-core "^7.2.0"
+ sink-transform "^2.0.0"
+ through2 "^2.0.3"
+
+module-deps@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/module-deps/-/module-deps-5.0.1.tgz#3bc47c14b0a6d925aff2ec4a177b456a96ae0396"
+ dependencies:
+ JSONStream "^1.0.3"
+ browser-resolve "^1.7.0"
+ cached-path-relative "^1.0.0"
+ concat-stream "~1.6.0"
+ defined "^1.0.0"
+ detective "^5.0.2"
+ duplexer2 "^0.1.2"
+ inherits "^2.0.1"
+ parents "^1.0.0"
+ readable-stream "^2.0.2"
+ resolve "^1.1.3"
+ stream-combiner2 "^1.1.1"
+ subarg "^1.0.0"
+ through2 "^2.0.0"
+ xtend "^4.0.0"
+
+moment-timezone@^0.5.14:
+ version "0.5.14"
+ resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.14.tgz#4eb38ff9538b80108ba467a458f3ed4268ccfcb1"
+ dependencies:
+ moment ">= 2.9.0"
+
+"moment@>= 2.9.0", moment@^2.20.1:
+ version "2.20.1"
+ resolved "https://registry.yarnpkg.com/moment/-/moment-2.20.1.tgz#d6eb1a46cbcc14a2b2f9434112c1ff8907f313fd"
+
+ms@0.7.1:
+ version "0.7.1"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098"
+
+ms@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
+
+multipipe@^0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/multipipe/-/multipipe-0.1.2.tgz#2a8f2ddf70eed564dff2d57f1e1a137d9f05078b"
+ dependencies:
+ duplexer2 "0.0.2"
+
+mute-stdout@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/mute-stdout/-/mute-stdout-1.0.0.tgz#5b32ea07eb43c9ded6130434cf926f46b2a7fd4d"
+
+mute-stream@0.0.7:
+ version "0.0.7"
+ resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
+
+nan@^2.3.0, nan@^2.3.2:
+ version "2.8.0"
+ resolved "https://registry.yarnpkg.com/nan/-/nan-2.8.0.tgz#ed715f3fe9de02b57a5e6252d90a96675e1f085a"
+
+nanomatch@^1.2.5:
+ version "1.2.7"
+ resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.7.tgz#53cd4aa109ff68b7f869591fdc9d10daeeea3e79"
+ dependencies:
+ arr-diff "^4.0.0"
+ array-unique "^0.3.2"
+ define-property "^1.0.0"
+ extend-shallow "^2.0.1"
+ fragment-cache "^0.2.1"
+ is-odd "^1.0.0"
+ kind-of "^5.0.2"
+ object.pick "^1.3.0"
+ regex-not "^1.0.0"
+ snapdragon "^0.8.1"
+ to-regex "^3.0.1"
+
+natural-compare@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
+
+nearley@^2.7.10:
+ version "2.11.0"
+ resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.11.0.tgz#5e626c79a6cd2f6ab9e7e5d5805e7668967757ae"
+ dependencies:
+ nomnom "~1.6.2"
+ railroad-diagrams "^1.0.0"
+ randexp "^0.4.2"
+
+next-tick@1:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
+
+node-fetch@^1.0.1:
+ version "1.7.3"
+ resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"
+ dependencies:
+ encoding "^0.1.11"
+ is-stream "^1.0.1"
+
+node-gyp@^3.3.1:
+ version "3.6.2"
+ resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.6.2.tgz#9bfbe54562286284838e750eac05295853fa1c60"
+ dependencies:
+ fstream "^1.0.0"
+ glob "^7.0.3"
+ graceful-fs "^4.1.2"
+ minimatch "^3.0.2"
+ mkdirp "^0.5.0"
+ nopt "2 || 3"
+ npmlog "0 || 1 || 2 || 3 || 4"
+ osenv "0"
+ request "2"
+ rimraf "2"
+ semver "~5.3.0"
+ tar "^2.0.0"
+ which "1"
+
+node-int64@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
+
+node-notifier@^5.1.2:
+ version "5.2.1"
+ resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.2.1.tgz#fa313dd08f5517db0e2502e5758d664ac69f9dea"
+ dependencies:
+ growly "^1.3.0"
+ semver "^5.4.1"
+ shellwords "^0.1.1"
+ which "^1.3.0"
+
+node-pre-gyp@^0.6.39:
+ version "0.6.39"
+ resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz#c00e96860b23c0e1420ac7befc5044e1d78d8649"
+ dependencies:
+ detect-libc "^1.0.2"
+ hawk "3.1.3"
+ mkdirp "^0.5.1"
+ nopt "^4.0.1"
+ npmlog "^4.0.2"
+ rc "^1.1.7"
+ request "2.81.0"
+ rimraf "^2.6.1"
+ semver "^5.3.0"
+ tar "^2.2.1"
+ tar-pack "^3.4.0"
+
+node-sass@^4.2.0:
+ version "4.7.2"
+ resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.7.2.tgz#9366778ba1469eb01438a9e8592f4262bcb6794e"
+ dependencies:
+ async-foreach "^0.1.3"
+ chalk "^1.1.1"
+ cross-spawn "^3.0.0"
+ gaze "^1.0.0"
+ get-stdin "^4.0.1"
+ glob "^7.0.3"
+ in-publish "^2.0.0"
+ lodash.assign "^4.2.0"
+ lodash.clonedeep "^4.3.2"
+ lodash.mergewith "^4.6.0"
+ meow "^3.7.0"
+ mkdirp "^0.5.1"
+ nan "^2.3.2"
+ node-gyp "^3.3.1"
+ npmlog "^4.0.0"
+ request "~2.79.0"
+ sass-graph "^2.2.4"
+ stdout-stream "^1.4.0"
+ "true-case-path" "^1.0.2"
+
+nomnom@~1.6.2:
+ version "1.6.2"
+ resolved "https://registry.yarnpkg.com/nomnom/-/nomnom-1.6.2.tgz#84a66a260174408fc5b77a18f888eccc44fb6971"
+ dependencies:
+ colors "0.5.x"
+ underscore "~1.4.4"
+
+"nopt@2 || 3":
+ version "3.0.6"
+ resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9"
+ dependencies:
+ abbrev "1"
+
+nopt@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d"
+ dependencies:
+ abbrev "1"
+ osenv "^0.1.4"
+
+normalize-package-data@^2.0.0, normalize-package-data@^2.3.2, normalize-package-data@^2.3.4:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f"
+ dependencies:
+ hosted-git-info "^2.1.4"
+ is-builtin-module "^1.0.0"
+ semver "2 || 3 || 4 || 5"
+ validate-npm-package-license "^3.0.1"
+
+normalize-path@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-1.0.0.tgz#32d0e472f91ff345701c15a8311018d3b0a90379"
+
+normalize-path@^2.0.0, normalize-path@^2.0.1, normalize-path@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9"
+ dependencies:
+ remove-trailing-separator "^1.0.1"
+
+normalize-range@^0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942"
+
+notifyjs@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/notifyjs/-/notifyjs-3.0.0.tgz#7418c9d6c0533aebaa643414214af53b521d1b28"
+
+now-and-later@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/now-and-later/-/now-and-later-2.0.0.tgz#bc61cbb456d79cb32207ce47ca05136ff2e7d6ee"
+ dependencies:
+ once "^1.3.2"
+
+npm-path@^2.0.2:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/npm-path/-/npm-path-2.0.4.tgz#c641347a5ff9d6a09e4d9bce5580c4f505278e64"
+ dependencies:
+ which "^1.2.10"
+
+npm-run-path@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
+ dependencies:
+ path-key "^2.0.0"
+
+npm-which@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/npm-which/-/npm-which-3.0.1.tgz#9225f26ec3a285c209cae67c3b11a6b4ab7140aa"
+ dependencies:
+ commander "^2.9.0"
+ npm-path "^2.0.2"
+ which "^1.2.10"
+
+"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.0, npmlog@^4.0.2:
+ version "4.1.2"
+ resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
+ dependencies:
+ are-we-there-yet "~1.1.2"
+ console-control-strings "~1.1.0"
+ gauge "~2.7.3"
+ set-blocking "~2.0.0"
+
+nth-check@~1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.1.tgz#9929acdf628fc2c41098deab82ac580cf149aae4"
+ dependencies:
+ boolbase "~1.0.0"
+
+num2fraction@^1.2.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede"
+
+number-is-nan@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
+
+nwmatcher@^1.4.3:
+ version "1.4.3"
+ resolved "https://registry.yarnpkg.com/nwmatcher/-/nwmatcher-1.4.3.tgz#64348e3b3d80f035b40ac11563d278f8b72db89c"
+
+oauth-sign@~0.8.1, oauth-sign@~0.8.2:
+ version "0.8.2"
+ resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43"
+
+object-assign@4.X, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
+
+object-assign@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-3.0.0.tgz#9bedd5ca0897949bca47e7ff408062d549f587f2"
+
+object-copy@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c"
+ dependencies:
+ copy-descriptor "^0.1.0"
+ define-property "^0.2.5"
+ kind-of "^3.0.3"
+
+object-inspect@^1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.5.0.tgz#9d876c11e40f485c79215670281b767488f9bfe3"
+
+object-is@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.0.1.tgz#0aa60ec9989a0b3ed795cf4d06f62cf1ad6539b6"
+
+object-keys@^1.0.11, object-keys@^1.0.6, object-keys@^1.0.8:
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.11.tgz#c54601778ad560f1142ce0e01bcca8b56d13426d"
+
+object-keys@~0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-0.4.0.tgz#28a6aae7428dd2c3a92f3d95f21335dd204e0336"
+
+object-visit@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb"
+ dependencies:
+ isobject "^3.0.0"
+
+object.assign@^4.0.4, object.assign@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da"
+ dependencies:
+ define-properties "^1.1.2"
+ function-bind "^1.1.1"
+ has-symbols "^1.0.0"
+ object-keys "^1.0.11"
+
+object.defaults@^1.0.0, object.defaults@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/object.defaults/-/object.defaults-1.1.0.tgz#3a7f868334b407dea06da16d88d5cd29e435fecf"
+ dependencies:
+ array-each "^1.0.1"
+ array-slice "^1.0.0"
+ for-own "^1.0.0"
+ isobject "^3.0.0"
+
+object.entries@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.0.4.tgz#1bf9a4dd2288f5b33f3a993d257661f05d161a5f"
+ dependencies:
+ define-properties "^1.1.2"
+ es-abstract "^1.6.1"
+ function-bind "^1.1.0"
+ has "^1.0.1"
+
+object.getownpropertydescriptors@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16"
+ dependencies:
+ define-properties "^1.1.2"
+ es-abstract "^1.5.1"
+
+object.map@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/object.map/-/object.map-1.0.1.tgz#cf83e59dc8fcc0ad5f4250e1f78b3b81bd801d37"
+ dependencies:
+ for-own "^1.0.0"
+ make-iterator "^1.0.0"
+
+object.omit@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa"
+ dependencies:
+ for-own "^0.1.4"
+ is-extendable "^0.1.1"
+
+object.pick@^1.2.0, object.pick@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747"
+ dependencies:
+ isobject "^3.0.1"
+
+object.reduce@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/object.reduce/-/object.reduce-1.0.1.tgz#6fe348f2ac7fa0f95ca621226599096825bb03ad"
+ dependencies:
+ for-own "^1.0.0"
+ make-iterator "^1.0.0"
+
+object.values@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.0.4.tgz#e524da09b4f66ff05df457546ec72ac99f13069a"
+ dependencies:
+ define-properties "^1.1.2"
+ es-abstract "^1.6.1"
+ function-bind "^1.1.0"
+ has "^1.0.1"
+
+on-finished@~2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
+ dependencies:
+ ee-first "1.1.1"
+
+once@^1.3.0, once@^1.3.1, once@^1.3.2, once@^1.3.3, once@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
+ dependencies:
+ wrappy "1"
+
+onetime@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789"
+
+onetime@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4"
+ dependencies:
+ mimic-fn "^1.0.0"
+
+optimist@^0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686"
+ dependencies:
+ minimist "~0.0.1"
+ wordwrap "~0.0.2"
+
+optionator@^0.8.1, optionator@^0.8.2:
+ version "0.8.2"
+ resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64"
+ dependencies:
+ deep-is "~0.1.3"
+ fast-levenshtein "~2.0.4"
+ levn "~0.3.0"
+ prelude-ls "~1.1.2"
+ type-check "~0.3.2"
+ wordwrap "~1.0.0"
+
+options@>=0.0.5:
+ version "0.0.6"
+ resolved "https://registry.yarnpkg.com/options/-/options-0.0.6.tgz#ec22d312806bb53e731773e7cdaefcf1c643128f"
+
+ora@^0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/ora/-/ora-0.2.3.tgz#37527d220adcd53c39b73571d754156d5db657a4"
+ dependencies:
+ chalk "^1.1.1"
+ cli-cursor "^1.0.2"
+ cli-spinners "^0.1.2"
+ object-assign "^4.0.1"
+
+ordered-read-streams@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz#77c0cb37c41525d64166d990ffad7ec6a0e1363e"
+ dependencies:
+ readable-stream "^2.0.1"
+
+os-browserify@~0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27"
+
+os-homedir@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
+
+os-locale@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9"
+ dependencies:
+ lcid "^1.0.0"
+
+os-locale@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-2.1.0.tgz#42bc2900a6b5b8bd17376c8e882b65afccf24bf2"
+ dependencies:
+ execa "^0.7.0"
+ lcid "^1.0.0"
+ mem "^1.1.0"
+
+os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
+
+osenv@0, osenv@^0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.4.tgz#42fe6d5953df06c8064be6f176c3d05aaaa34644"
+ dependencies:
+ os-homedir "^1.0.0"
+ os-tmpdir "^1.0.0"
+
+outpipe@^1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/outpipe/-/outpipe-1.1.1.tgz#50cf8616365e87e031e29a5ec9339a3da4725fa2"
+ dependencies:
+ shell-quote "^1.4.2"
+
+p-each-series@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-1.0.0.tgz#930f3d12dd1f50e7434457a22cd6f04ac6ad7f71"
+ dependencies:
+ p-reduce "^1.0.0"
+
+p-finally@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
+
+p-limit@^1.1.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.2.0.tgz#0e92b6bedcb59f022c13d0f1949dc82d15909f1c"
+ dependencies:
+ p-try "^1.0.0"
+
+p-locate@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43"
+ dependencies:
+ p-limit "^1.1.0"
+
+p-map@^1.1.1:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b"
+
+p-reduce@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-reduce/-/p-reduce-1.0.0.tgz#18c2b0dd936a4690a529f8231f58a0fdb6a47dfa"
+
+p-try@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3"
+
+pako@^1.0.3, pako@~1.0.5:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258"
+
+parents@^1.0.0, parents@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/parents/-/parents-1.0.1.tgz#fedd4d2bf193a77745fe71e371d73c3307d9c751"
+ dependencies:
+ path-platform "~0.11.15"
+
+parse-asn1@^5.0.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.0.tgz#37c4f9b7ed3ab65c74817b5f2480937fbf97c712"
+ dependencies:
+ asn1.js "^4.0.0"
+ browserify-aes "^1.0.0"
+ create-hash "^1.1.0"
+ evp_bytestokey "^1.0.0"
+ pbkdf2 "^3.0.3"
+
+parse-filepath@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/parse-filepath/-/parse-filepath-1.0.2.tgz#a632127f53aaf3d15876f5872f3ffac763d6c891"
+ dependencies:
+ is-absolute "^1.0.0"
+ map-cache "^0.2.0"
+ path-root "^0.1.1"
+
+parse-glob@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c"
+ dependencies:
+ glob-base "^0.3.0"
+ is-dotfile "^1.0.0"
+ is-extglob "^1.0.0"
+ is-glob "^2.0.0"
+
+parse-json@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9"
+ dependencies:
+ error-ex "^1.2.0"
+
+parse-json@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0"
+ dependencies:
+ error-ex "^1.3.1"
+ json-parse-better-errors "^1.0.1"
+
+parse-passwd@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6"
+
+parse5@4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608"
+
+parse5@^3.0.1:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.3.tgz#042f792ffdd36851551cf4e9e066b3874ab45b5c"
+ dependencies:
+ "@types/node" "*"
+
+parseurl@~1.3.0:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3"
+
+pascalcase@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
+
+path-browserify@~0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a"
+
+path-dirname@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0"
+
+path-exists@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b"
+ dependencies:
+ pinkie-promise "^2.0.0"
+
+path-exists@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
+
+path-is-absolute@^1.0.0, path-is-absolute@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
+
+path-is-inside@^1.0.1, path-is-inside@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
+
+path-key@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
+
+path-parse@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1"
+
+path-platform@~0.11.15:
+ version "0.11.15"
+ resolved "https://registry.yarnpkg.com/path-platform/-/path-platform-0.11.15.tgz#e864217f74c36850f0852b78dc7bf7d4a5721bf2"
+
+path-root-regex@^0.1.0:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/path-root-regex/-/path-root-regex-0.1.2.tgz#bfccdc8df5b12dc52c8b43ec38d18d72c04ba96d"
+
+path-root@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/path-root/-/path-root-0.1.1.tgz#9a4a6814cac1c0cd73360a95f32083c8ea4745b7"
+ dependencies:
+ path-root-regex "^0.1.0"
+
+path-type@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441"
+ dependencies:
+ graceful-fs "^4.1.2"
+ pify "^2.0.0"
+ pinkie-promise "^2.0.0"
+
+path-type@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73"
+ dependencies:
+ pify "^2.0.0"
+
+path-type@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f"
+ dependencies:
+ pify "^3.0.0"
+
+pause-stream@0.0.11:
+ version "0.0.11"
+ resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445"
+ dependencies:
+ through "~2.3"
+
+pbkdf2@^3.0.3:
+ version "3.0.14"
+ resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.14.tgz#a35e13c64799b06ce15320f459c230e68e73bade"
+ dependencies:
+ create-hash "^1.1.2"
+ create-hmac "^1.1.4"
+ ripemd160 "^2.0.1"
+ safe-buffer "^5.0.1"
+ sha.js "^2.4.8"
+
+performance-now@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5"
+
+performance-now@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
+
+pify@^2.0.0, pify@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
+
+pify@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
+
+pinkie-promise@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa"
+ dependencies:
+ pinkie "^2.0.0"
+
+pinkie@^2.0.0:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870"
+
+pkg-dir@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-1.0.0.tgz#7a4b508a8d5bb2d629d447056ff4e9c9314cf3d4"
+ dependencies:
+ find-up "^1.0.0"
+
+pkg-dir@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b"
+ dependencies:
+ find-up "^2.1.0"
+
+platform@^1.3.0, platform@^1.3.3:
+ version "1.3.5"
+ resolved "https://registry.yarnpkg.com/platform/-/platform-1.3.5.tgz#fb6958c696e07e2918d2eeda0f0bc9448d733444"
+
+plugin-error@^0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/plugin-error/-/plugin-error-0.1.2.tgz#3b9bb3335ccf00f425e07437e19276967da47ace"
+ dependencies:
+ ansi-cyan "^0.1.1"
+ ansi-red "^0.1.1"
+ arr-diff "^1.0.1"
+ arr-union "^2.0.1"
+ extend-shallow "^1.1.2"
+
+pluralize@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777"
+
+pn@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb"
+
+posix-character-classes@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
+
+postcss-selector-parser@^3.0.0:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz#4f875f4afb0c96573d5cf4d74011aee250a7e865"
+ dependencies:
+ dot-prop "^4.1.1"
+ indexes-of "^1.0.1"
+ uniq "^1.0.1"
+
+postcss-url@^7.3.0:
+ version "7.3.0"
+ resolved "https://registry.yarnpkg.com/postcss-url/-/postcss-url-7.3.0.tgz#cf2f45e06743cf43cfea25309f81cbc003dc783f"
+ dependencies:
+ mime "^1.4.1"
+ minimatch "^3.0.4"
+ mkdirp "^0.5.0"
+ postcss "^6.0.1"
+ xxhashjs "^0.2.1"
+
+postcss-value-parser@^3.2.3, postcss-value-parser@^3.3.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz#87f38f9f18f774a4ab4c8a232f5c5ce8872a9d15"
+
+postcss@^6.0.1, postcss@^6.0.11, postcss@^6.0.16:
+ version "6.0.16"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.16.tgz#112e2fe2a6d2109be0957687243170ea5589e146"
+ dependencies:
+ chalk "^2.3.0"
+ source-map "^0.6.1"
+ supports-color "^5.1.0"
+
+prelude-ls@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
+
+preserve@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
+
+prettier@^1.9.2:
+ version "1.10.2"
+ resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.10.2.tgz#1af8356d1842276a99a5b5529c82dd9e9ad3cc93"
+
+pretty-format@^21.2.1:
+ version "21.2.1"
+ resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-21.2.1.tgz#ae5407f3cf21066cd011aa1ba5fce7b6a2eddb36"
+ dependencies:
+ ansi-regex "^3.0.0"
+ ansi-styles "^3.2.0"
+
+pretty-format@^22.1.0:
+ version "22.1.0"
+ resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-22.1.0.tgz#2277605b40ed4529ae4db51ff62f4be817647914"
+ dependencies:
+ ansi-regex "^3.0.0"
+ ansi-styles "^3.2.0"
+
+pretty-hrtime@^1.0.0:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1"
+
+private@^0.1.6, private@^0.1.7:
+ version "0.1.8"
+ resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
+
+process-nextick-args@^1.0.6, process-nextick-args@^1.0.7, process-nextick-args@~1.0.6:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3"
+
+process@~0.11.0:
+ version "0.11.10"
+ resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
+
+progress@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.0.tgz#8a1be366bf8fc23db2bd23f10c6fe920b4389d1f"
+
+promise-toolbox@^0.8.0, promise-toolbox@^0.8.3:
+ version "0.8.3"
+ resolved "https://registry.yarnpkg.com/promise-toolbox/-/promise-toolbox-0.8.3.tgz#b757232a21d246d8702df50da6784932dd0f5348"
+ dependencies:
+ make-error "^1.2.3"
+
+promise-toolbox@^0.9.5:
+ version "0.9.5"
+ resolved "https://registry.yarnpkg.com/promise-toolbox/-/promise-toolbox-0.9.5.tgz#ca33e53714cfde924a9bd3d2d23c53b21cb75acc"
+ dependencies:
+ make-error "^1.2.3"
+
+promise@^7.0.1, promise@^7.1.1:
+ version "7.3.1"
+ resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
+ dependencies:
+ asap "~2.0.3"
+
+prop-types-extra@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/prop-types-extra/-/prop-types-extra-1.0.1.tgz#a57bd4810e82d27a3ff4317ecc1b4ad005f79a82"
+ dependencies:
+ warning "^3.0.0"
+
+prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.6, prop-types@^15.5.7, prop-types@^15.5.8, prop-types@^15.6.0:
+ version "15.6.0"
+ resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.0.tgz#ceaf083022fc46b4a35f69e13ef75aed0d639856"
+ dependencies:
+ fbjs "^0.8.16"
+ loose-envify "^1.3.1"
+ object-assign "^4.1.1"
+
+pseudomap@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
+
+public-encrypt@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.0.tgz#39f699f3a46560dd5ebacbca693caf7c65c18cc6"
+ dependencies:
+ bn.js "^4.1.0"
+ browserify-rsa "^4.0.0"
+ create-hash "^1.1.0"
+ parse-asn1 "^5.0.0"
+ randombytes "^2.0.1"
+
+pug-attrs@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/pug-attrs/-/pug-attrs-2.0.2.tgz#8be2b2225568ffa75d1b866982bff9f4111affcb"
+ dependencies:
+ constantinople "^3.0.1"
+ js-stringify "^1.0.1"
+ pug-runtime "^2.0.3"
+
+pug-code-gen@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/pug-code-gen/-/pug-code-gen-2.0.0.tgz#96aea39a9e62f1ec5d2b6a5b42a29d528c70b43d"
+ dependencies:
+ constantinople "^3.0.1"
+ doctypes "^1.1.0"
+ js-stringify "^1.0.1"
+ pug-attrs "^2.0.2"
+ pug-error "^1.3.2"
+ pug-runtime "^2.0.3"
+ void-elements "^2.0.1"
+ with "^5.0.0"
+
+pug-error@^1.3.2:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/pug-error/-/pug-error-1.3.2.tgz#53ae7d9d29bb03cf564493a026109f54c47f5f26"
+
+pug-filters@^2.1.5:
+ version "2.1.5"
+ resolved "https://registry.yarnpkg.com/pug-filters/-/pug-filters-2.1.5.tgz#66bf6e80d97fbef829bab0aa35eddff33fc964f3"
+ dependencies:
+ clean-css "^3.3.0"
+ constantinople "^3.0.1"
+ jstransformer "1.0.0"
+ pug-error "^1.3.2"
+ pug-walk "^1.1.5"
+ resolve "^1.1.6"
+ uglify-js "^2.6.1"
+
+pug-lexer@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/pug-lexer/-/pug-lexer-3.1.0.tgz#fd087376d4a675b4f59f8fef422883434e9581a2"
+ dependencies:
+ character-parser "^2.1.1"
+ is-expression "^3.0.0"
+ pug-error "^1.3.2"
+
+pug-linker@^3.0.3:
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/pug-linker/-/pug-linker-3.0.3.tgz#25f59eb750237f0368e59c3379764229c0189c41"
+ dependencies:
+ pug-error "^1.3.2"
+ pug-walk "^1.1.5"
+
+pug-load@^2.0.9:
+ version "2.0.9"
+ resolved "https://registry.yarnpkg.com/pug-load/-/pug-load-2.0.9.tgz#ee217c914cc1d9324d44b86c32d1df241d36de7a"
+ dependencies:
+ object-assign "^4.1.0"
+ pug-walk "^1.1.5"
+
+pug-parser@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/pug-parser/-/pug-parser-4.0.0.tgz#c9f52322e4eabe4bf5beeba64ed18373bb627801"
+ dependencies:
+ pug-error "^1.3.2"
+ token-stream "0.0.1"
+
+pug-runtime@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/pug-runtime/-/pug-runtime-2.0.3.tgz#98162607b0fce9e254d427f33987a5aee7168bda"
+
+pug-strip-comments@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/pug-strip-comments/-/pug-strip-comments-1.0.2.tgz#d313afa01bcc374980e1399e23ebf2eb9bdc8513"
+ dependencies:
+ pug-error "^1.3.2"
+
+pug-walk@^1.1.5:
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/pug-walk/-/pug-walk-1.1.5.tgz#90e943acbcf7021e6454cf1b32245891cba6f851"
+
+"pug@>=2.0.0-alpha <3":
+ version "2.0.0-rc.4"
+ resolved "https://registry.yarnpkg.com/pug/-/pug-2.0.0-rc.4.tgz#b7b08f6599bd5302568042b7436984fb28c80a13"
+ dependencies:
+ pug-code-gen "^2.0.0"
+ pug-filters "^2.1.5"
+ pug-lexer "^3.1.0"
+ pug-linker "^3.0.3"
+ pug-load "^2.0.9"
+ pug-parser "^4.0.0"
+ pug-runtime "^2.0.3"
+ pug-strip-comments "^1.0.2"
+
+pump@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909"
+ dependencies:
+ end-of-stream "^1.1.0"
+ once "^1.3.1"
+
+pumpify@^1.3.5:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.4.0.tgz#80b7c5df7e24153d03f0e7ac8a05a5d068bd07fb"
+ dependencies:
+ duplexify "^3.5.3"
+ inherits "^2.0.3"
+ pump "^2.0.0"
+
+punycode@1.3.2:
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
+
+punycode@^1.3.2, punycode@^1.4.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
+
+punycode@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.0.tgz#5f863edc89b96db09074bad7947bf09056ca4e7d"
+
+qs@5.2.0:
+ version "5.2.0"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-5.2.0.tgz#a9f31142af468cb72b25b30136ba2456834916be"
+
+qs@~2.2.3:
+ version "2.2.5"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-2.2.5.tgz#1088abaf9dcc0ae5ae45b709e6c6b5888b23923c"
+
+qs@~6.3.0:
+ version "6.3.2"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.2.tgz#e75bd5f6e268122a2a0e0bda630b2550c166502c"
+
+qs@~6.4.0:
+ version "6.4.0"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
+
+qs@~6.5.1:
+ version "6.5.1"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
+
+query-string@^4.2.2:
+ version "4.3.4"
+ resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb"
+ dependencies:
+ object-assign "^4.1.0"
+ strict-uri-encode "^1.0.0"
+
+querystring-es3@~0.2.0:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
+
+querystring@0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
+
+querystringify@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-1.0.0.tgz#6286242112c5b712fa654e526652bf6a13ff05cb"
+
+raf@^3.4.0:
+ version "3.4.0"
+ resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.0.tgz#a28876881b4bc2ca9117d4138163ddb80f781575"
+ dependencies:
+ performance-now "^2.1.0"
+
+railroad-diagrams@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e"
+
+randexp@^0.4.2:
+ version "0.4.6"
+ resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.4.6.tgz#e986ad5e5e31dae13ddd6f7b3019aa7c87f60ca3"
+ dependencies:
+ discontinuous-range "1.0.0"
+ ret "~0.1.10"
+
+random-password@^0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/random-password/-/random-password-0.1.2.tgz#128f1302c65bfeef2ab5094c3e6da63b852e7be6"
+
+randomatic@^1.1.3:
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.7.tgz#c7abe9cc8b87c0baa876b19fde83fd464797e38c"
+ dependencies:
+ is-number "^3.0.0"
+ kind-of "^4.0.0"
+
+randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.6.tgz#d302c522948588848a8d300c932b44c24231da80"
+ dependencies:
+ safe-buffer "^5.1.0"
+
+randomfill@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.3.tgz#b96b7df587f01dd91726c418f30553b1418e3d62"
+ dependencies:
+ randombytes "^2.0.5"
+ safe-buffer "^5.1.0"
+
+raw-body@~2.1.5:
+ version "2.1.7"
+ resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.1.7.tgz#adfeace2e4fb3098058014d08c072dcc59758774"
+ dependencies:
+ bytes "2.4.0"
+ iconv-lite "0.4.13"
+ unpipe "1.0.0"
+
+rc@^1.1.7:
+ version "1.2.5"
+ resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.5.tgz#275cd687f6e3b36cc756baa26dfee80a790301fd"
+ dependencies:
+ deep-extend "~0.4.0"
+ ini "~1.3.0"
+ minimist "^1.2.0"
+ strip-json-comments "~2.0.1"
+
+react-addons-shallow-compare@^15.0.2, react-addons-shallow-compare@^15.6.2:
+ version "15.6.2"
+ resolved "https://registry.yarnpkg.com/react-addons-shallow-compare/-/react-addons-shallow-compare-15.6.2.tgz#198a00b91fc37623db64a28fd17b596ba362702f"
+ dependencies:
+ fbjs "^0.8.4"
+ object-assign "^4.1.0"
+
+react-addons-test-utils@^15.6.2:
+ version "15.6.2"
+ resolved "https://registry.yarnpkg.com/react-addons-test-utils/-/react-addons-test-utils-15.6.2.tgz#c12b6efdc2247c10da7b8770d185080a7b047156"
+
+react-bootstrap-4@^0.29.1:
+ version "0.29.1"
+ resolved "https://registry.yarnpkg.com/react-bootstrap-4/-/react-bootstrap-4-0.29.1.tgz#32270a0473f5bd84cf447f2eebec2435e666505d"
+ dependencies:
+ babel-runtime "^5.8.25"
+ classnames "^2.1.5"
+ dom-helpers "^2.4.0"
+ invariant "^2.1.2"
+ keycode "^2.1.0"
+ lodash-compat "^3.10.1"
+ react-overlays "^0.6.0"
+ react-prop-types "^0.3.0"
+ uncontrollable "^3.1.3"
+ warning "^2.1.0"
+
+react-chartist@^0.13.0:
+ version "0.13.1"
+ resolved "https://registry.yarnpkg.com/react-chartist/-/react-chartist-0.13.1.tgz#79cee44891dfd7008d555a7a5a623ab09c0b59d1"
+ dependencies:
+ prop-types "^15.5.8"
+
+react-copy-to-clipboard@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.yarnpkg.com/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.1.tgz#8eae107bb400be73132ed3b6a7b4fb156090208e"
+ dependencies:
+ copy-to-clipboard "^3"
+ prop-types "^15.5.8"
+
+react-dnd-html5-backend@^2.5.4:
+ version "2.5.4"
+ resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-2.5.4.tgz#974ad083f67b12d56977a5b171f5ffeb29d78352"
+ dependencies:
+ lodash "^4.2.0"
+
+react-dnd@^2.5.4:
+ version "2.5.4"
+ resolved "https://registry.yarnpkg.com/react-dnd/-/react-dnd-2.5.4.tgz#0b6dc5e9d0dfc2909f4f4fe736e5534f3afd1bd9"
+ dependencies:
+ disposables "^1.0.1"
+ dnd-core "^2.5.4"
+ hoist-non-react-statics "^2.1.0"
+ invariant "^2.1.0"
+ lodash "^4.2.0"
+ prop-types "^15.5.10"
+
+react-document-title@^2.0.2:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/react-document-title/-/react-document-title-2.0.3.tgz#bbf922a0d71412fc948245e4283b2412df70f2b9"
+ dependencies:
+ prop-types "^15.5.6"
+ react-side-effect "^1.0.2"
+
+react-dom@^15.4.1:
+ version "15.6.2"
+ resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.6.2.tgz#41cfadf693b757faf2708443a1d1fd5a02bef730"
+ dependencies:
+ fbjs "^0.8.9"
+ loose-envify "^1.1.0"
+ object-assign "^4.1.0"
+ prop-types "^15.5.10"
+
+react-dropzone@^4.2.3:
+ version "4.2.7"
+ resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-4.2.7.tgz#a4963b1f725d5a91e63cd1c2b55ddce537953d46"
+ dependencies:
+ attr-accept "^1.0.3"
+ prop-types "^15.5.7"
+
+react-input-autosize@^2.1.2:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/react-input-autosize/-/react-input-autosize-2.2.1.tgz#ec428fa15b1592994fb5f9aa15bb1eb6baf420f8"
+ dependencies:
+ prop-types "^15.5.8"
+
+react-intl@^2.4.0:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/react-intl/-/react-intl-2.4.0.tgz#66c14dc9df9a73b2fbbfbd6021726e80a613eb15"
+ dependencies:
+ intl-format-cache "^2.0.5"
+ intl-messageformat "^2.1.0"
+ intl-relativeformat "^2.0.0"
+ invariant "^2.1.1"
+
+react-key-handler@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/react-key-handler/-/react-key-handler-1.0.1.tgz#1fc0f4f4855f506a192c2cbe9fe8cb78fc553191"
+ dependencies:
+ exenv "^1.2.0"
+ prop-types "^15.5.7"
+
+react-notify@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/react-notify/-/react-notify-3.0.0.tgz#2924793a5e59e2a899bdc054961db31f2e57292e"
+
+react-overlays@^0.6.0:
+ version "0.6.12"
+ resolved "https://registry.yarnpkg.com/react-overlays/-/react-overlays-0.6.12.tgz#a079c750cc429d7db4c7474a95b4b54033e255c3"
+ dependencies:
+ classnames "^2.2.5"
+ dom-helpers "^3.2.0"
+ react-prop-types "^0.4.0"
+ warning "^3.0.0"
+
+react-overlays@^0.8.3:
+ version "0.8.3"
+ resolved "https://registry.yarnpkg.com/react-overlays/-/react-overlays-0.8.3.tgz#fad65eea5b24301cca192a169f5dddb0b20d3ac5"
+ dependencies:
+ classnames "^2.2.5"
+ dom-helpers "^3.2.1"
+ prop-types "^15.5.10"
+ prop-types-extra "^1.0.1"
+ react-transition-group "^2.2.0"
+ warning "^3.0.0"
+
+react-prop-types@^0.3.0:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/react-prop-types/-/react-prop-types-0.3.2.tgz#e2763ac6f3a80199d8981c3647c44b0554c97b7f"
+ dependencies:
+ warning "^2.0.0"
+
+react-prop-types@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/react-prop-types/-/react-prop-types-0.4.0.tgz#f99b0bfb4006929c9af2051e7c1414a5c75b93d0"
+ dependencies:
+ warning "^3.0.0"
+
+react-redux@^5.0.6:
+ version "5.0.6"
+ resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.0.6.tgz#23ed3a4f986359d68b5212eaaa681e60d6574946"
+ dependencies:
+ hoist-non-react-statics "^2.2.1"
+ invariant "^2.0.0"
+ lodash "^4.2.0"
+ lodash-es "^4.2.0"
+ loose-envify "^1.1.0"
+ prop-types "^15.5.10"
+
+react-router@^3.0.0:
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/react-router/-/react-router-3.2.0.tgz#62b6279d589b70b34e265113e4c0a9261a02ed36"
+ dependencies:
+ create-react-class "^15.5.1"
+ history "^3.0.0"
+ hoist-non-react-statics "^1.2.0"
+ invariant "^2.2.1"
+ loose-envify "^1.2.0"
+ prop-types "^15.5.6"
+ warning "^3.0.0"
+
+react-select@^1.1.0:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/react-select/-/react-select-1.2.1.tgz#a2fe58a569eb14dcaa6543816260b97e538120d1"
+ dependencies:
+ classnames "^2.2.4"
+ prop-types "^15.5.8"
+ react-input-autosize "^2.1.2"
+
+react-shortcuts@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/react-shortcuts/-/react-shortcuts-2.0.0.tgz#871b033a071a8537422b1529d691c38432823bae"
+ dependencies:
+ combokeys "^3.0.0"
+ events "^1.0.2"
+ invariant "^2.1.0"
+ just-reduce-object "^1.0.3"
+ platform "^1.3.0"
+ prop-types "^15.5.8"
+
+react-side-effect@^1.0.2:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/react-side-effect/-/react-side-effect-1.1.3.tgz#512c25abe0dec172834c4001ec5c51e04d41bc5c"
+ dependencies:
+ exenv "^1.2.1"
+ shallowequal "^1.0.1"
+
+react-sparklines@1.6.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/react-sparklines/-/react-sparklines-1.6.0.tgz#0fe5987b1895301ce724c40d78ab9a26e8a9c2bf"
+ dependencies:
+ react-addons-shallow-compare "^15.0.2"
+
+react-test-renderer@^15.6.2:
+ version "15.6.2"
+ resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-15.6.2.tgz#d0333434fc2c438092696ca770da5ed48037efa8"
+ dependencies:
+ fbjs "^0.8.9"
+ object-assign "^4.1.0"
+
+react-transition-group@^2.2.0:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.2.1.tgz#e9fb677b79e6455fd391b03823afe84849df4a10"
+ dependencies:
+ chain-function "^1.0.0"
+ classnames "^2.2.5"
+ dom-helpers "^3.2.0"
+ loose-envify "^1.3.1"
+ prop-types "^15.5.8"
+ warning "^3.0.0"
+
+react-virtualized@^9.15.0:
+ version "9.18.5"
+ resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.18.5.tgz#42dd390ebaa7ea809bfcaf775d39872641679b89"
+ dependencies:
+ babel-runtime "^6.26.0"
+ classnames "^2.2.3"
+ dom-helpers "^2.4.0 || ^3.0.0"
+ loose-envify "^1.3.0"
+ prop-types "^15.6.0"
+
+react@^15.4.1:
+ version "15.6.2"
+ resolved "https://registry.yarnpkg.com/react/-/react-15.6.2.tgz#dba0434ab439cfe82f108f0f511663908179aa72"
+ dependencies:
+ create-react-class "^15.6.0"
+ fbjs "^0.8.9"
+ loose-envify "^1.1.0"
+ object-assign "^4.1.0"
+ prop-types "^15.5.10"
+
+read-only-stream@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/read-only-stream/-/read-only-stream-2.0.0.tgz#2724fd6a8113d73764ac288d4386270c1dbf17f0"
+ dependencies:
+ readable-stream "^2.0.2"
+
+read-package-json@^2.0.10:
+ version "2.0.12"
+ resolved "https://registry.yarnpkg.com/read-package-json/-/read-package-json-2.0.12.tgz#68ea45f98b3741cb6e10ae3bbd42a605026a6951"
+ dependencies:
+ glob "^7.1.1"
+ json-parse-better-errors "^1.0.0"
+ normalize-package-data "^2.0.0"
+ slash "^1.0.0"
+ optionalDependencies:
+ graceful-fs "^4.1.2"
+
+read-pkg-up@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02"
+ dependencies:
+ find-up "^1.0.0"
+ read-pkg "^1.0.0"
+
+read-pkg-up@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be"
+ dependencies:
+ find-up "^2.0.0"
+ read-pkg "^2.0.0"
+
+read-pkg@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28"
+ dependencies:
+ load-json-file "^1.0.0"
+ normalize-package-data "^2.3.2"
+ path-type "^1.0.0"
+
+read-pkg@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8"
+ dependencies:
+ load-json-file "^2.0.0"
+ normalize-package-data "^2.3.2"
+ path-type "^2.0.0"
+
+"readable-stream@>=1.1.13-1 <1.2.0-0", readable-stream@~1.1.9:
+ version "1.1.14"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
+ dependencies:
+ core-util-is "~1.0.0"
+ inherits "~2.0.1"
+ isarray "0.0.1"
+ string_decoder "~0.10.x"
+
+readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.9, readable-stream@^2.3.3:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c"
+ dependencies:
+ core-util-is "~1.0.0"
+ inherits "~2.0.3"
+ isarray "~1.0.0"
+ process-nextick-args "~1.0.6"
+ safe-buffer "~5.1.1"
+ string_decoder "~1.0.3"
+ util-deprecate "~1.0.1"
+
+readable-stream@~1.0.17:
+ version "1.0.34"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c"
+ dependencies:
+ core-util-is "~1.0.0"
+ inherits "~2.0.1"
+ isarray "0.0.1"
+ string_decoder "~0.10.x"
+
+readable-stream@~2.0.0:
+ version "2.0.6"
+ resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e"
+ dependencies:
+ core-util-is "~1.0.0"
+ inherits "~2.0.1"
+ isarray "~1.0.0"
+ process-nextick-args "~1.0.6"
+ string_decoder "~0.10.x"
+ util-deprecate "~1.0.1"
+
+readdirp@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78"
+ dependencies:
+ graceful-fs "^4.1.2"
+ minimatch "^3.0.2"
+ readable-stream "^2.0.2"
+ set-immediate-shim "^1.0.1"
+
+realpath-native@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.0.0.tgz#7885721a83b43bd5327609f0ddecb2482305fdf0"
+ dependencies:
+ util.promisify "^1.0.0"
+
+rechoir@^0.6.2:
+ version "0.6.2"
+ resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384"
+ dependencies:
+ resolve "^1.1.6"
+
+redent@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde"
+ dependencies:
+ indent-string "^2.1.0"
+ strip-indent "^1.0.1"
+
+redux-thunk@^2.0.1:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.2.0.tgz#e615a16e16b47a19a515766133d1e3e99b7852e5"
+
+redux@^3.7.1, redux@^3.7.2:
+ version "3.7.2"
+ resolved "https://registry.yarnpkg.com/redux/-/redux-3.7.2.tgz#06b73123215901d25d065be342eb026bc1c8537b"
+ dependencies:
+ lodash "^4.2.1"
+ lodash-es "^4.2.1"
+ loose-envify "^1.1.0"
+ symbol-observable "^1.0.3"
+
+regenerate@^1.2.1:
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.3.tgz#0c336d3980553d755c39b586ae3b20aa49c82b7f"
+
+regenerator-runtime@^0.11.0:
+ version "0.11.1"
+ resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
+
+regenerator-transform@^0.10.0:
+ version "0.10.1"
+ resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.10.1.tgz#1e4996837231da8b7f3cf4114d71b5691a0680dd"
+ dependencies:
+ babel-runtime "^6.18.0"
+ babel-types "^6.19.0"
+ private "^0.1.6"
+
+regex-cache@^0.4.2:
+ version "0.4.4"
+ resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.4.tgz#75bdc58a2a1496cec48a12835bc54c8d562336dd"
+ dependencies:
+ is-equal-shallow "^0.1.3"
+
+regex-not@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.0.tgz#42f83e39771622df826b02af176525d6a5f157f9"
+ dependencies:
+ extend-shallow "^2.0.1"
+
+regexpu-core@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-2.0.0.tgz#49d038837b8dcf8bfa5b9a42139938e6ea2ae240"
+ dependencies:
+ regenerate "^1.2.1"
+ regjsgen "^0.2.0"
+ regjsparser "^0.1.4"
+
+regjsgen@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.2.0.tgz#6c016adeac554f75823fe37ac05b92d5a4edb1f7"
+
+regjsparser@^0.1.4:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.1.5.tgz#7ee8f84dc6fa792d3fd0ae228d24bd949ead205c"
+ dependencies:
+ jsesc "~0.5.0"
+
+remove-bom-buffer@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz#c2bf1e377520d324f623892e33c10cac2c252b53"
+ dependencies:
+ is-buffer "^1.1.5"
+ is-utf8 "^0.2.1"
+
+remove-bom-stream@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/remove-bom-stream/-/remove-bom-stream-1.2.0.tgz#05f1a593f16e42e1fb90ebf59de8e569525f9523"
+ dependencies:
+ remove-bom-buffer "^3.0.0"
+ safe-buffer "^5.1.0"
+ through2 "^2.0.3"
+
+remove-trailing-separator@^1.0.1, remove-trailing-separator@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
+
+repeat-element@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a"
+
+repeat-string@^1.5.2, repeat-string@^1.6.1:
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
+
+repeating@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda"
+ dependencies:
+ is-finite "^1.0.0"
+
+replace-ext@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-0.0.1.tgz#29bbd92078a739f0bcce2b4ee41e837953522924"
+
+replace-ext@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb"
+
+replace-homedir@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/replace-homedir/-/replace-homedir-1.0.0.tgz#e87f6d513b928dde808260c12be7fec6ff6e798c"
+ dependencies:
+ homedir-polyfill "^1.0.1"
+ is-absolute "^1.0.0"
+ remove-trailing-separator "^1.1.0"
+
+request-promise-core@1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.1.tgz#3eee00b2c5aa83239cfb04c5700da36f81cd08b6"
+ dependencies:
+ lodash "^4.13.1"
+
+request-promise-native@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.5.tgz#5281770f68e0c9719e5163fd3fab482215f4fda5"
+ dependencies:
+ request-promise-core "1.1.1"
+ stealthy-require "^1.1.0"
+ tough-cookie ">=2.3.3"
+
+request@2, request@^2.83.0:
+ version "2.83.0"
+ resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356"
+ dependencies:
+ aws-sign2 "~0.7.0"
+ aws4 "^1.6.0"
+ caseless "~0.12.0"
+ combined-stream "~1.0.5"
+ extend "~3.0.1"
+ forever-agent "~0.6.1"
+ form-data "~2.3.1"
+ har-validator "~5.0.3"
+ hawk "~6.0.2"
+ http-signature "~1.2.0"
+ is-typedarray "~1.0.0"
+ isstream "~0.1.2"
+ json-stringify-safe "~5.0.1"
+ mime-types "~2.1.17"
+ oauth-sign "~0.8.2"
+ performance-now "^2.1.0"
+ qs "~6.5.1"
+ safe-buffer "^5.1.1"
+ stringstream "~0.0.5"
+ tough-cookie "~2.3.3"
+ tunnel-agent "^0.6.0"
+ uuid "^3.1.0"
+
+request@2.81.0:
+ version "2.81.0"
+ resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0"
+ dependencies:
+ aws-sign2 "~0.6.0"
+ aws4 "^1.2.1"
+ caseless "~0.12.0"
+ combined-stream "~1.0.5"
+ extend "~3.0.0"
+ forever-agent "~0.6.1"
+ form-data "~2.1.1"
+ har-validator "~4.2.1"
+ hawk "~3.1.3"
+ http-signature "~1.1.0"
+ is-typedarray "~1.0.0"
+ isstream "~0.1.2"
+ json-stringify-safe "~5.0.1"
+ mime-types "~2.1.7"
+ oauth-sign "~0.8.1"
+ performance-now "^0.2.0"
+ qs "~6.4.0"
+ safe-buffer "^5.0.1"
+ stringstream "~0.0.4"
+ tough-cookie "~2.3.0"
+ tunnel-agent "^0.6.0"
+ uuid "^3.0.0"
+
+request@~2.79.0:
+ version "2.79.0"
+ resolved "https://registry.yarnpkg.com/request/-/request-2.79.0.tgz#4dfe5bf6be8b8cdc37fcf93e04b65577722710de"
+ dependencies:
+ aws-sign2 "~0.6.0"
+ aws4 "^1.2.1"
+ caseless "~0.11.0"
+ combined-stream "~1.0.5"
+ extend "~3.0.0"
+ forever-agent "~0.6.1"
+ form-data "~2.1.1"
+ har-validator "~2.0.6"
+ hawk "~3.1.3"
+ http-signature "~1.1.0"
+ is-typedarray "~1.0.0"
+ isstream "~0.1.2"
+ json-stringify-safe "~5.0.1"
+ mime-types "~2.1.7"
+ oauth-sign "~0.8.1"
+ qs "~6.3.0"
+ stringstream "~0.0.4"
+ tough-cookie "~2.3.0"
+ tunnel-agent "~0.4.1"
+ uuid "^3.0.0"
+
+require-directory@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
+
+require-from-string@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.1.tgz#c545233e9d7da6616e9d59adfb39fc9f588676ff"
+
+require-main-filename@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
+
+require-package-name@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/require-package-name/-/require-package-name-2.0.1.tgz#c11e97276b65b8e2923f75dabf5fb2ef0c3841b9"
+
+require-uncached@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3"
+ dependencies:
+ caller-path "^0.1.0"
+ resolve-from "^1.0.0"
+
+requires-port@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
+
+reselect@^2.5.4:
+ version "2.5.4"
+ resolved "https://registry.yarnpkg.com/reselect/-/reselect-2.5.4.tgz#b7d23fdf00b83fa7ad0279546f8dbbbd765c7047"
+
+resolve-cwd@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"
+ dependencies:
+ resolve-from "^3.0.0"
+
+resolve-dir@^1.0.0, resolve-dir@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-1.0.1.tgz#79a40644c362be82f26effe739c9bb5382046f43"
+ dependencies:
+ expand-tilde "^2.0.0"
+ global-modules "^1.0.0"
+
+resolve-from@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226"
+
+resolve-from@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748"
+
+resolve-from@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
+
+resolve-options@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/resolve-options/-/resolve-options-1.1.0.tgz#32bb9e39c06d67338dc9378c0d6d6074566ad131"
+ dependencies:
+ value-or-function "^3.0.0"
+
+resolve-url@^0.2.1, resolve-url@~0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
+
+resolve@1.1.7:
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
+
+resolve@^1.1.3, resolve@^1.1.4, resolve@^1.1.6, resolve@^1.1.7, resolve@^1.3.3, resolve@^1.4.0, resolve@^1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.5.0.tgz#1f09acce796c9a762579f31b2c1cc4c3cddf9f36"
+ dependencies:
+ path-parse "^1.0.5"
+
+restore-cursor@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541"
+ dependencies:
+ exit-hook "^1.0.0"
+ onetime "^1.0.0"
+
+restore-cursor@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf"
+ dependencies:
+ onetime "^2.0.0"
+ signal-exit "^3.0.2"
+
+ret@~0.1.10:
+ version "0.1.15"
+ resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
+
+right-align@^0.1.1:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef"
+ dependencies:
+ align-text "^0.1.1"
+
+rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.5.4, rimraf@^2.6.1:
+ version "2.6.2"
+ resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36"
+ dependencies:
+ glob "^7.0.5"
+
+ripemd160@^2.0.0, ripemd160@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.1.tgz#0f4584295c53a3628af7e6d79aca21ce57d1c6e7"
+ dependencies:
+ hash-base "^2.0.0"
+ inherits "^2.0.1"
+
+rst-selector-parser@^2.2.3:
+ version "2.2.3"
+ resolved "https://registry.yarnpkg.com/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz#81b230ea2fcc6066c89e3472de794285d9b03d91"
+ dependencies:
+ lodash.flattendeep "^4.4.0"
+ nearley "^2.7.10"
+
+run-async@^2.2.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0"
+ dependencies:
+ is-promise "^2.1.0"
+
+rw@1:
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4"
+
+rx-lite-aggregates@^4.0.8:
+ version "4.0.8"
+ resolved "https://registry.yarnpkg.com/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz#753b87a89a11c95467c4ac1626c4efc4e05c67be"
+ dependencies:
+ rx-lite "*"
+
+rx-lite@*, rx-lite@^4.0.8:
+ version "4.0.8"
+ resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444"
+
+rxjs@^5.4.2:
+ version "5.5.6"
+ resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.6.tgz#e31fb96d6fd2ff1fd84bcea8ae9c02d007179c02"
+ dependencies:
+ symbol-observable "1.0.1"
+
+safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
+
+sane@^2.0.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/sane/-/sane-2.3.0.tgz#3f3df584abf69e63d4bb74f0f8c42468e4d7d46b"
+ dependencies:
+ anymatch "^1.3.0"
+ exec-sh "^0.2.0"
+ fb-watchman "^2.0.0"
+ minimatch "^3.0.2"
+ minimist "^1.1.1"
+ walker "~1.0.5"
+ watch "~0.18.0"
+ optionalDependencies:
+ fsevents "^1.1.1"
+
+sass-graph@^2.2.4:
+ version "2.2.4"
+ resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.4.tgz#13fbd63cd1caf0908b9fd93476ad43a51d1e0b49"
+ dependencies:
+ glob "^7.0.0"
+ lodash "^4.0.0"
+ scss-tokenizer "^0.2.3"
+ yargs "^7.0.0"
+
+sax@>=0.6.0, sax@^1.2.4:
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
+
+scss-tokenizer@^0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz#8eb06db9a9723333824d3f5530641149847ce5d1"
+ dependencies:
+ js-base64 "^2.1.8"
+ source-map "^0.4.2"
+
+semver-greatest-satisfied-range@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-1.1.0.tgz#13e8c2658ab9691cb0cd71093240280d36f77a5b"
+ dependencies:
+ sver-compat "^1.5.0"
+
+"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1:
+ version "5.5.0"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab"
+
+semver@5.3.0, semver@~5.3.0:
+ version "5.3.0"
+ resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
+
+set-blocking@^2.0.0, set-blocking@~2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
+
+set-getter@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/set-getter/-/set-getter-0.1.0.tgz#d769c182c9d5a51f409145f2fba82e5e86e80376"
+ dependencies:
+ to-object-path "^0.3.0"
+
+set-immediate-shim@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61"
+
+set-value@^0.4.3:
+ version "0.4.3"
+ resolved "https://registry.yarnpkg.com/set-value/-/set-value-0.4.3.tgz#7db08f9d3d22dc7f78e53af3c3bf4666ecdfccf1"
+ dependencies:
+ extend-shallow "^2.0.1"
+ is-extendable "^0.1.1"
+ is-plain-object "^2.0.1"
+ to-object-path "^0.3.0"
+
+set-value@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.0.tgz#71ae4a88f0feefbbf52d1ea604f3fb315ebb6274"
+ dependencies:
+ extend-shallow "^2.0.1"
+ is-extendable "^0.1.1"
+ is-plain-object "^2.0.3"
+ split-string "^3.0.1"
+
+setimmediate@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
+
+sha.js@^2.4.0, sha.js@^2.4.8, sha.js@~2.4.4:
+ version "2.4.10"
+ resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.10.tgz#b1fde5cd7d11a5626638a07c604ab909cfa31f9b"
+ dependencies:
+ inherits "^2.0.1"
+ safe-buffer "^5.0.1"
+
+shallowequal@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.0.2.tgz#1561dbdefb8c01408100319085764da3fcf83f8f"
+
+shasum@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/shasum/-/shasum-1.0.2.tgz#e7012310d8f417f4deb5712150e5678b87ae565f"
+ dependencies:
+ json-stable-stringify "~0.0.0"
+ sha.js "~2.4.4"
+
+shebang-command@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
+ dependencies:
+ shebang-regex "^1.0.0"
+
+shebang-regex@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
+
+shell-quote@^1.4.2, shell-quote@^1.6.1:
+ version "1.6.1"
+ resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.6.1.tgz#f4781949cce402697127430ea3b3c5476f481767"
+ dependencies:
+ array-filter "~0.0.0"
+ array-map "~0.0.0"
+ array-reduce "~0.0.0"
+ jsonify "~0.0.0"
+
+shellwords@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
+
+signal-exit@^3.0.0, signal-exit@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
+
+sink-transform@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/sink-transform/-/sink-transform-2.0.0.tgz#fe1817cab33c366104827b943c91dc7672e3ac0e"
+ dependencies:
+ concat-stream "^1.4.8"
+ readable-stream "^2.0.0"
+
+slash@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55"
+
+slice-ansi@0.0.4:
+ version "0.0.4"
+ resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35"
+
+slice-ansi@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-1.0.0.tgz#044f1a49d8842ff307aad6b505ed178bd950134d"
+ dependencies:
+ is-fullwidth-code-point "^2.0.0"
+
+snapdragon-node@^2.0.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
+ dependencies:
+ define-property "^1.0.0"
+ isobject "^3.0.0"
+ snapdragon-util "^3.0.1"
+
+snapdragon-util@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2"
+ dependencies:
+ kind-of "^3.2.0"
+
+snapdragon@^0.8.1:
+ version "0.8.1"
+ resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.1.tgz#e12b5487faded3e3dea0ac91e9400bf75b401370"
+ dependencies:
+ base "^0.11.1"
+ debug "^2.2.0"
+ define-property "^0.2.5"
+ extend-shallow "^2.0.1"
+ map-cache "^0.2.2"
+ source-map "^0.5.6"
+ source-map-resolve "^0.5.0"
+ use "^2.0.0"
+
+sntp@1.x.x:
+ version "1.0.9"
+ resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198"
+ dependencies:
+ hoek "2.x.x"
+
+sntp@2.x.x:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.1.0.tgz#2c6cec14fedc2222739caf9b5c3d85d1cc5a2cc8"
+ dependencies:
+ hoek "4.x.x"
+
+source-map-resolve@^0.3.0:
+ version "0.3.1"
+ resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.3.1.tgz#610f6122a445b8dd51535a2a71b783dfc1248761"
+ dependencies:
+ atob "~1.1.0"
+ resolve-url "~0.2.1"
+ source-map-url "~0.3.0"
+ urix "~0.1.0"
+
+source-map-resolve@^0.5.0:
+ version "0.5.1"
+ resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.1.tgz#7ad0f593f2281598e854df80f19aae4b92d7a11a"
+ dependencies:
+ atob "^2.0.0"
+ decode-uri-component "^0.2.0"
+ resolve-url "^0.2.1"
+ source-map-url "^0.4.0"
+ urix "^0.1.0"
+
+source-map-support@^0.4.15:
+ version "0.4.18"
+ resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f"
+ dependencies:
+ source-map "^0.5.6"
+
+source-map-support@^0.5.0:
+ version "0.5.3"
+ resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.3.tgz#2b3d5fff298cfa4d1afd7d4352d569e9a0158e76"
+ dependencies:
+ source-map "^0.6.0"
+
+source-map-url@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3"
+
+source-map-url@~0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.3.0.tgz#7ecaf13b57bcd09da8a40c5d269db33799d4aaf9"
+
+source-map@0.4.x, source-map@^0.4.2, source-map@^0.4.4, source-map@~0.4.0, source-map@~0.4.2:
+ version "0.4.4"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b"
+ dependencies:
+ amdefine ">=0.0.4"
+
+source-map@^0.1.38:
+ version "0.1.43"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.1.43.tgz#c24bc146ca517c1471f5dacbe2571b2b7f9e3346"
+ dependencies:
+ amdefine ">=0.0.4"
+
+source-map@^0.5.1, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1, source-map@~0.5.3, source-map@~0.5.6:
+ version "0.5.7"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
+
+source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1:
+ version "0.6.1"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
+
+sparkles@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/sparkles/-/sparkles-1.0.0.tgz#1acbbfb592436d10bbe8f785b7cc6f82815012c3"
+
+spdx-correct@~1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40"
+ dependencies:
+ spdx-license-ids "^1.0.2"
+
+spdx-expression-parse@~1.0.0:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz#9bdf2f20e1f40ed447fbe273266191fced51626c"
+
+spdx-license-ids@^1.0.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57"
+
+split-string@^3.0.1, split-string@^3.0.2:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
+ dependencies:
+ extend-shallow "^3.0.0"
+
+split@0.3:
+ version "0.3.3"
+ resolved "https://registry.yarnpkg.com/split/-/split-0.3.3.tgz#cd0eea5e63a211dfff7eb0f091c4133e2d0dd28f"
+ dependencies:
+ through "2"
+
+sprintf-js@~1.0.2:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
+
+sshpk@^1.7.0:
+ version "1.13.1"
+ resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.13.1.tgz#512df6da6287144316dc4c18fe1cf1d940739be3"
+ dependencies:
+ asn1 "~0.2.3"
+ assert-plus "^1.0.0"
+ dashdash "^1.12.0"
+ getpass "^0.1.1"
+ optionalDependencies:
+ bcrypt-pbkdf "^1.0.0"
+ ecc-jsbn "~0.1.1"
+ jsbn "~0.1.0"
+ tweetnacl "~0.14.0"
+
+stack-trace@0.0.10:
+ version "0.0.10"
+ resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0"
+
+stack-utils@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.1.tgz#d4f33ab54e8e38778b0ca5cfd3b3afb12db68620"
+
+staged-git-files@0.0.4:
+ version "0.0.4"
+ resolved "https://registry.yarnpkg.com/staged-git-files/-/staged-git-files-0.0.4.tgz#d797e1b551ca7a639dec0237dc6eb4bb9be17d35"
+
+static-extend@^0.1.1:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"
+ dependencies:
+ define-property "^0.2.5"
+ object-copy "^0.1.0"
+
+statuses@1:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087"
+
+stdout-stream@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/stdout-stream/-/stdout-stream-1.4.0.tgz#a2c7c8587e54d9427ea9edb3ac3f2cd522df378b"
+ dependencies:
+ readable-stream "^2.0.1"
+
+stealthy-require@^1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
+
+stream-browserify@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db"
+ dependencies:
+ inherits "~2.0.1"
+ readable-stream "^2.0.2"
+
+stream-combiner2@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/stream-combiner2/-/stream-combiner2-1.1.1.tgz#fb4d8a1420ea362764e21ad4780397bebcb41cbe"
+ dependencies:
+ duplexer2 "~0.1.0"
+ readable-stream "^2.0.2"
+
+stream-combiner@~0.0.4:
+ version "0.0.4"
+ resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.0.4.tgz#4d5e433c185261dde623ca3f44c586bcf5c4ad14"
+ dependencies:
+ duplexer "~0.1.1"
+
+stream-exhaust@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/stream-exhaust/-/stream-exhaust-1.0.2.tgz#acdac8da59ef2bc1e17a2c0ccf6c320d120e555d"
+
+stream-http@^2.0.0:
+ version "2.8.0"
+ resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.0.tgz#fd86546dac9b1c91aff8fc5d287b98fafb41bc10"
+ dependencies:
+ builtin-status-codes "^3.0.0"
+ inherits "^2.0.1"
+ readable-stream "^2.3.3"
+ to-arraybuffer "^1.0.0"
+ xtend "^4.0.0"
+
+stream-shift@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952"
+
+stream-splicer@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/stream-splicer/-/stream-splicer-2.0.0.tgz#1b63be438a133e4b671cc1935197600175910d83"
+ dependencies:
+ inherits "^2.0.1"
+ readable-stream "^2.0.2"
+
+stream-to-observable@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/stream-to-observable/-/stream-to-observable-0.2.0.tgz#59d6ea393d87c2c0ddac10aa0d561bc6ba6f0e10"
+ dependencies:
+ any-observable "^0.2.0"
+
+strict-uri-encode@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
+
+string-length@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed"
+ dependencies:
+ astral-regex "^1.0.0"
+ strip-ansi "^4.0.0"
+
+string-width@^1.0.1, string-width@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
+ dependencies:
+ code-point-at "^1.0.0"
+ is-fullwidth-code-point "^1.0.0"
+ strip-ansi "^3.0.0"
+
+string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
+ dependencies:
+ is-fullwidth-code-point "^2.0.0"
+ strip-ansi "^4.0.0"
+
+string_decoder@~0.10.x:
+ version "0.10.31"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
+
+string_decoder@~1.0.0, string_decoder@~1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab"
+ dependencies:
+ safe-buffer "~5.1.0"
+
+stringify-object@^3.2.0:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.2.1.tgz#2720c2eff940854c819f6ee252aaeb581f30624d"
+ dependencies:
+ get-own-enumerable-property-symbols "^2.0.1"
+ is-obj "^1.0.1"
+ is-regexp "^1.0.0"
+
+stringstream@~0.0.4, stringstream@~0.0.5:
+ version "0.0.5"
+ resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878"
+
+strip-ansi@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-0.3.0.tgz#25f48ea22ca79187f3174a4db8759347bb126220"
+ dependencies:
+ ansi-regex "^0.2.1"
+
+strip-ansi@^3.0.0, strip-ansi@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
+ dependencies:
+ ansi-regex "^2.0.0"
+
+strip-ansi@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
+ dependencies:
+ ansi-regex "^3.0.0"
+
+strip-bom-stream@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/strip-bom-stream/-/strip-bom-stream-2.0.0.tgz#f87db5ef2613f6968aa545abfe1ec728b6a829ca"
+ dependencies:
+ first-chunk-stream "^2.0.0"
+ strip-bom "^2.0.0"
+
+strip-bom-string@1.X:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/strip-bom-string/-/strip-bom-string-1.0.0.tgz#e5211e9224369fbb81d633a2f00044dc8cedad92"
+
+strip-bom@3.0.0, strip-bom@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
+
+strip-bom@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e"
+ dependencies:
+ is-utf8 "^0.2.0"
+
+strip-eof@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
+
+strip-indent@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2"
+ dependencies:
+ get-stdin "^4.0.1"
+
+strip-indent@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68"
+
+strip-json-comments@~2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
+
+styled-components@^3.1.5:
+ version "3.1.5"
+ resolved "https://registry.yarnpkg.com/styled-components/-/styled-components-3.1.5.tgz#7aaf0a97f8c5cd49791e924887f9d8f428fb010c"
+ dependencies:
+ buffer "^5.0.3"
+ css-to-react-native "^2.0.3"
+ fbjs "^0.8.9"
+ hoist-non-react-statics "^1.2.0"
+ is-plain-object "^2.0.1"
+ prop-types "^15.5.4"
+ stylis "^3.4.0"
+ stylis-rule-sheet "^0.0.7"
+ supports-color "^3.2.3"
+
+stylis-rule-sheet@^0.0.7:
+ version "0.0.7"
+ resolved "https://registry.yarnpkg.com/stylis-rule-sheet/-/stylis-rule-sheet-0.0.7.tgz#5c51dc879141a61821c2094ba91d2cbcf2469c6c"
+
+stylis@^3.4.0:
+ version "3.4.8"
+ resolved "https://registry.yarnpkg.com/stylis/-/stylis-3.4.8.tgz#94380babbcd4c75726215794ca985b38ec96d1a3"
+
+subarg@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/subarg/-/subarg-1.0.0.tgz#f62cf17581e996b48fc965699f54c06ae268b8d2"
+ dependencies:
+ minimist "^1.1.0"
+
+supports-color@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-0.2.0.tgz#d92de2694eb3f67323973d7ae3d8b55b4c22190a"
+
+supports-color@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
+
+supports-color@^3.1.2, supports-color@^3.2.3:
+ version "3.2.3"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6"
+ dependencies:
+ has-flag "^1.0.0"
+
+supports-color@^4.0.0:
+ version "4.5.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.5.0.tgz#be7a0de484dec5c5cddf8b3d59125044912f635b"
+ dependencies:
+ has-flag "^2.0.0"
+
+supports-color@^5.1.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.1.0.tgz#058a021d1b619f7ddf3980d712ea3590ce7de3d5"
+ dependencies:
+ has-flag "^2.0.0"
+
+sver-compat@^1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/sver-compat/-/sver-compat-1.5.0.tgz#3cf87dfeb4d07b4a3f14827bc186b3fd0c645cd8"
+ dependencies:
+ es6-iterator "^2.0.1"
+ es6-symbol "^3.1.1"
+
+symbol-observable@1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.1.tgz#8340fc4702c3122df5d22288f88283f513d3fdd4"
+
+symbol-observable@^0.2.2:
+ version "0.2.4"
+ resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-0.2.4.tgz#95a83db26186d6af7e7a18dbd9760a2f86d08f40"
+
+symbol-observable@^1.0.3:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
+
+symbol-tree@^3.2.2:
+ version "3.2.2"
+ resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6"
+
+syntax-error@^1.1.1:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/syntax-error/-/syntax-error-1.3.0.tgz#1ed9266c4d40be75dc55bf9bb1cb77062bb96ca1"
+ dependencies:
+ acorn "^4.0.3"
+
+table@^4.0.1:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/table/-/table-4.0.2.tgz#a33447375391e766ad34d3486e6e2aedc84d2e36"
+ dependencies:
+ ajv "^5.2.3"
+ ajv-keywords "^2.1.0"
+ chalk "^2.1.0"
+ lodash "^4.17.4"
+ slice-ansi "1.0.0"
+ string-width "^2.1.1"
+
+tar-pack@^3.4.0:
+ version "3.4.1"
+ resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.1.tgz#e1dbc03a9b9d3ba07e896ad027317eb679a10a1f"
+ dependencies:
+ debug "^2.2.0"
+ fstream "^1.0.10"
+ fstream-ignore "^1.0.5"
+ once "^1.3.3"
+ readable-stream "^2.1.4"
+ rimraf "^2.5.1"
+ tar "^2.2.1"
+ uid-number "^0.0.6"
+
+tar-stream@^1.5.5:
+ version "1.5.5"
+ resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.5.5.tgz#5cad84779f45c83b1f2508d96b09d88c7218af55"
+ dependencies:
+ bl "^1.0.0"
+ end-of-stream "^1.0.0"
+ readable-stream "^2.0.0"
+ xtend "^4.0.0"
+
+tar@^2.0.0, tar@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1"
+ dependencies:
+ block-stream "*"
+ fstream "^1.0.2"
+ inherits "2"
+
+test-exclude@^4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-4.1.1.tgz#4d84964b0966b0087ecc334a2ce002d3d9341e26"
+ dependencies:
+ arrify "^1.0.1"
+ micromatch "^2.3.11"
+ object-assign "^4.1.0"
+ read-pkg-up "^1.0.1"
+ require-main-filename "^1.0.1"
+
+tether@^1.3.7:
+ version "1.4.3"
+ resolved "https://registry.yarnpkg.com/tether/-/tether-1.4.3.tgz#fd547024c47b6e5c9b87e1880f997991a9a6ad54"
+
+text-table@~0.2.0:
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
+
+throat@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a"
+
+through2-filter@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/through2-filter/-/through2-filter-2.0.0.tgz#60bc55a0dacb76085db1f9dae99ab43f83d622ec"
+ dependencies:
+ through2 "~2.0.0"
+ xtend "~4.0.0"
+
+through2@2.X, through2@^2.0.0, through2@^2.0.1, through2@^2.0.3, through2@~2.0.0:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be"
+ dependencies:
+ readable-stream "^2.1.5"
+ xtend "~4.0.1"
+
+through2@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/through2/-/through2-1.1.1.tgz#0847cbc4449f3405574dbdccd9bb841b83ac3545"
+ dependencies:
+ readable-stream ">=1.1.13-1 <1.2.0-0"
+ xtend ">=4.0.0 <4.1.0-0"
+
+through2@~0.4.0:
+ version "0.4.2"
+ resolved "https://registry.yarnpkg.com/through2/-/through2-0.4.2.tgz#dbf5866031151ec8352bb6c4db64a2292a840b9b"
+ dependencies:
+ readable-stream "~1.0.17"
+ xtend "~2.1.1"
+
+through@2, "through@>=2.2.7 <3", through@^2.3.6, through@~2.3, through@~2.3.1:
+ version "2.3.8"
+ resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
+
+time-stamp@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-1.1.0.tgz#764a5a11af50561921b133f3b44e618687e0f5c3"
+
+timers-browserify@^1.0.1:
+ version "1.4.2"
+ resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-1.4.2.tgz#c9c58b575be8407375cb5e2462dacee74359f41d"
+ dependencies:
+ process "~0.11.0"
+
+timers-ext@^0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/timers-ext/-/timers-ext-0.1.2.tgz#61cc47a76c1abd3195f14527f978d58ae94c5204"
+ dependencies:
+ es5-ext "~0.10.14"
+ next-tick "1"
+
+tmp@^0.0.33:
+ version "0.0.33"
+ resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
+ dependencies:
+ os-tmpdir "~1.0.2"
+
+tmpl@1.0.x:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1"
+
+to-absolute-glob@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz#1865f43d9e74b0822db9f145b78cff7d0f7c849b"
+ dependencies:
+ is-absolute "^1.0.0"
+ is-negated-glob "^1.0.0"
+
+to-arraybuffer@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43"
+
+to-fast-properties@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47"
+
+to-fast-properties@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
+
+to-object-path@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af"
+ dependencies:
+ kind-of "^3.0.2"
+
+to-regex-range@^2.1.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38"
+ dependencies:
+ is-number "^3.0.0"
+ repeat-string "^1.6.1"
+
+to-regex@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.1.tgz#15358bee4a2c83bd76377ba1dc049d0f18837aae"
+ dependencies:
+ define-property "^0.2.5"
+ extend-shallow "^2.0.1"
+ regex-not "^1.0.0"
+
+to-through@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/to-through/-/to-through-2.0.0.tgz#fc92adaba072647bc0b67d6b03664aa195093af6"
+ dependencies:
+ through2 "^2.0.3"
+
+toggle-selection@^1.0.3:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32"
+
+token-stream@0.0.1:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/token-stream/-/token-stream-0.0.1.tgz#ceeefc717a76c4316f126d0b9dbaa55d7e7df01a"
+
+tough-cookie@>=2.3.3, tough-cookie@^2.3.3, tough-cookie@~2.3.0, tough-cookie@~2.3.3:
+ version "2.3.3"
+ resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.3.tgz#0b618a5565b6dea90bf3425d04d55edc475a7561"
+ dependencies:
+ punycode "^1.4.1"
+
+tr46@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09"
+ dependencies:
+ punycode "^2.1.0"
+
+trim-newlines@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613"
+
+trim-right@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
+
+"true-case-path@^1.0.2":
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/true-case-path/-/true-case-path-1.0.2.tgz#7ec91130924766c7f573be3020c34f8fdfd00d62"
+ dependencies:
+ glob "^6.0.4"
+
+tty-browserify@~0.0.0:
+ version "0.0.1"
+ resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.1.tgz#3f05251ee17904dfd0677546670db9651682b811"
+
+tunnel-agent@^0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
+ dependencies:
+ safe-buffer "^5.0.1"
+
+tunnel-agent@~0.4.1:
+ version "0.4.3"
+ resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.4.3.tgz#6373db76909fe570e08d73583365ed828a74eeeb"
+
+tweetnacl@^0.14.3, tweetnacl@~0.14.0:
+ version "0.14.5"
+ resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
+
+type-check@~0.3.2:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
+ dependencies:
+ prelude-ls "~1.1.2"
+
+type-is@~1.6.10:
+ version "1.6.15"
+ resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410"
+ dependencies:
+ media-typer "0.3.0"
+ mime-types "~2.1.15"
+
+typedarray@^0.0.6, typedarray@~0.0.5:
+ version "0.0.6"
+ resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
+
+ua-parser-js@^0.7.9:
+ version "0.7.17"
+ resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.17.tgz#e9ec5f9498b9ec910e7ae3ac626a805c4d09ecac"
+
+uglify-es@^3.3.4:
+ version "3.3.9"
+ resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677"
+ dependencies:
+ commander "~2.13.0"
+ source-map "~0.6.1"
+
+uglify-js@^2.6, uglify-js@^2.6.1:
+ version "2.8.29"
+ resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd"
+ dependencies:
+ source-map "~0.5.1"
+ yargs "~3.10.0"
+ optionalDependencies:
+ uglify-to-browserify "~1.0.0"
+
+uglify-js@^3.0.5:
+ version "3.3.9"
+ resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.3.9.tgz#33869666c8ab7f7658ce3d22f0f1ced40097d33a"
+ dependencies:
+ commander "~2.13.0"
+ source-map "~0.6.1"
+
+uglify-to-browserify@~1.0.0:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7"
+
+uid-number@^0.0.6:
+ version "0.0.6"
+ resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81"
+
+ultron@1.0.x:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.0.2.tgz#ace116ab557cd197386a4e88f4685378c8b2e4fa"
+
+ultron@~1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c"
+
+umd@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/umd/-/umd-3.0.1.tgz#8ae556e11011f63c2596708a8837259f01b3d60e"
+
+unc-path-regex@^0.1.2:
+ version "0.1.2"
+ resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa"
+
+uncontrollable-input@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/uncontrollable-input/-/uncontrollable-input-0.1.1.tgz#412f47b9816018820228bbfdad9599010eb85bbf"
+
+uncontrollable@^3.1.3:
+ version "3.3.1"
+ resolved "https://registry.yarnpkg.com/uncontrollable/-/uncontrollable-3.3.1.tgz#e23b402e7a4c69b1853fb4b43ce34b6480c65b6f"
+ dependencies:
+ invariant "^2.1.0"
+
+underscore@~1.4.4:
+ version "1.4.4"
+ resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.4.4.tgz#61a6a32010622afa07963bf325203cf12239d604"
+
+undertaker-registry@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/undertaker-registry/-/undertaker-registry-1.0.1.tgz#5e4bda308e4a8a2ae584f9b9a4359a499825cc50"
+
+undertaker@^1.0.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/undertaker/-/undertaker-1.2.0.tgz#339da4646252d082dc378e708067299750e11b49"
+ dependencies:
+ arr-flatten "^1.0.1"
+ arr-map "^2.0.0"
+ bach "^1.0.0"
+ collection-map "^1.0.0"
+ es6-weak-map "^2.0.1"
+ last-run "^1.1.0"
+ object.defaults "^1.0.0"
+ object.reduce "^1.0.0"
+ undertaker-registry "^1.0.0"
+
+union-value@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4"
+ dependencies:
+ arr-union "^3.1.0"
+ get-value "^2.0.6"
+ is-extendable "^0.1.1"
+ set-value "^0.4.3"
+
+uniq@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff"
+
+unique-slug@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.0.tgz#db6676e7c7cc0629878ff196097c78855ae9f4ab"
+ dependencies:
+ imurmurhash "^0.1.4"
+
+unique-stream@^2.0.2:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/unique-stream/-/unique-stream-2.2.1.tgz#5aa003cfbe94c5ff866c4e7d668bb1c4dbadb369"
+ dependencies:
+ json-stable-stringify "^1.0.0"
+ through2-filter "^2.0.0"
+
+unpipe@1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
+
+unset-value@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559"
+ dependencies:
+ has-value "^0.3.1"
+ isobject "^3.0.0"
+
+urix@^0.1.0, urix@~0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72"
+
+url-parse@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.2.0.tgz#3a19e8aaa6d023ddd27dcc44cb4fc8f7fec23986"
+ dependencies:
+ querystringify "~1.0.0"
+ requires-port "~1.0.0"
+
+url@~0.11.0:
+ version "0.11.0"
+ resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"
+ dependencies:
+ punycode "1.3.2"
+ querystring "0.2.0"
+
+use@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/use/-/use-2.0.2.tgz#ae28a0d72f93bf22422a18a2e379993112dec8e8"
+ dependencies:
+ define-property "^0.2.5"
+ isobject "^3.0.0"
+ lazy-cache "^2.0.2"
+
+util-deprecate@~1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
+
+util.promisify@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.0.tgz#440f7165a459c9a16dc145eb8e72f35687097030"
+ dependencies:
+ define-properties "^1.1.2"
+ object.getownpropertydescriptors "^2.0.3"
+
+util@0.10.3, util@~0.10.1:
+ version "0.10.3"
+ resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9"
+ dependencies:
+ inherits "2.0.1"
+
+uuid@^3.0.0, uuid@^3.1.0:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14"
+
+v8flags@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-3.0.1.tgz#dce8fc379c17d9f2c9e9ed78d89ce00052b1b76b"
+ dependencies:
+ homedir-polyfill "^1.0.1"
+
+validate-npm-package-license@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc"
+ dependencies:
+ spdx-correct "~1.0.0"
+ spdx-expression-parse "~1.0.0"
+
+value-matcher@^0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/value-matcher/-/value-matcher-0.0.0.tgz#c0caf87dc3998a68ea56b31fd1916adefe39f7be"
+ dependencies:
+ "@babel/polyfill" "^7.0.0-beta.36"
+
+value-or-function@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/value-or-function/-/value-or-function-3.0.0.tgz#1c243a50b595c1be54a754bfece8563b9ff8d813"
+
+verror@1.10.0:
+ version "1.10.0"
+ resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
+ dependencies:
+ assert-plus "^1.0.0"
+ core-util-is "1.0.2"
+ extsprintf "^1.2.0"
+
+vinyl-file@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/vinyl-file/-/vinyl-file-2.0.0.tgz#a7ebf5ffbefda1b7d18d140fcb07b223efb6751a"
+ dependencies:
+ graceful-fs "^4.1.2"
+ pify "^2.3.0"
+ pinkie-promise "^2.0.0"
+ strip-bom "^2.0.0"
+ strip-bom-stream "^2.0.0"
+ vinyl "^1.1.0"
+
+vinyl-fs@^3.0.0:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/vinyl-fs/-/vinyl-fs-3.0.2.tgz#1b86258844383f57581fcaac081fe09ef6d6d752"
+ dependencies:
+ fs-mkdirp-stream "^1.0.0"
+ glob-stream "^6.1.0"
+ graceful-fs "^4.0.0"
+ is-valid-glob "^1.0.0"
+ lazystream "^1.0.0"
+ lead "^1.0.0"
+ object.assign "^4.0.4"
+ pumpify "^1.3.5"
+ readable-stream "^2.3.3"
+ remove-bom-buffer "^3.0.0"
+ remove-bom-stream "^1.2.0"
+ resolve-options "^1.1.0"
+ through2 "^2.0.0"
+ to-through "^2.0.0"
+ value-or-function "^3.0.0"
+ vinyl "^2.0.0"
+ vinyl-sourcemap "^1.1.0"
+
+vinyl-sourcemap@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/vinyl-sourcemap/-/vinyl-sourcemap-1.1.0.tgz#92a800593a38703a8cdb11d8b300ad4be63b3e16"
+ dependencies:
+ append-buffer "^1.0.2"
+ convert-source-map "^1.5.0"
+ graceful-fs "^4.1.6"
+ normalize-path "^2.1.1"
+ now-and-later "^2.0.0"
+ remove-bom-buffer "^3.0.0"
+ vinyl "^2.0.0"
+
+vinyl-sourcemaps-apply@^0.2.0, vinyl-sourcemaps-apply@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz#ab6549d61d172c2b1b87be5c508d239c8ef87705"
+ dependencies:
+ source-map "^0.5.1"
+
+vinyl@^0.5.0:
+ version "0.5.3"
+ resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-0.5.3.tgz#b0455b38fc5e0cf30d4325132e461970c2091cde"
+ dependencies:
+ clone "^1.0.0"
+ clone-stats "^0.0.1"
+ replace-ext "0.0.1"
+
+vinyl@^1.1.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-1.2.0.tgz#5c88036cf565e5df05558bfc911f8656df218884"
+ dependencies:
+ clone "^1.0.0"
+ clone-stats "^0.0.1"
+ replace-ext "0.0.1"
+
+vinyl@^2.0.0, vinyl@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-2.1.0.tgz#021f9c2cf951d6b939943c89eb5ee5add4fd924c"
+ dependencies:
+ clone "^2.1.1"
+ clone-buffer "^1.0.0"
+ clone-stats "^1.0.0"
+ cloneable-readable "^1.0.0"
+ remove-trailing-separator "^1.0.1"
+ replace-ext "^1.0.0"
+
+vm-browserify@~0.0.1:
+ version "0.0.4"
+ resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73"
+ dependencies:
+ indexof "0.0.1"
+
+void-elements@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec"
+
+w3c-hr-time@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz#82ac2bff63d950ea9e3189a58a65625fedf19045"
+ dependencies:
+ browser-process-hrtime "^0.1.2"
+
+walker@~1.0.5:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb"
+ dependencies:
+ makeerror "1.0.x"
+
+warning@^2.0.0, warning@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/warning/-/warning-2.1.0.tgz#21220d9c63afc77a8c92111e011af705ce0c6901"
+ dependencies:
+ loose-envify "^1.0.0"
+
+warning@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/warning/-/warning-3.0.0.tgz#32e5377cb572de4ab04753bdf8821c01ed605b7c"
+ dependencies:
+ loose-envify "^1.0.0"
+
+watch@~0.18.0:
+ version "0.18.0"
+ resolved "https://registry.yarnpkg.com/watch/-/watch-0.18.0.tgz#28095476c6df7c90c963138990c0a5423eb4b986"
+ dependencies:
+ exec-sh "^0.2.0"
+ minimist "^1.2.0"
+
+watchify@^3.7.0:
+ version "3.10.0"
+ resolved "https://registry.yarnpkg.com/watchify/-/watchify-3.10.0.tgz#f436605553c432d87f62f718cc78077418ffbccb"
+ dependencies:
+ anymatch "^1.3.0"
+ browserify "^15.2.0"
+ chokidar "^1.0.0"
+ defined "^1.0.0"
+ outpipe "^1.1.0"
+ through2 "^2.0.0"
+ xtend "^4.0.0"
+
+webidl-conversions@^4.0.1, webidl-conversions@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
+
+websocket-driver@>=0.3.6:
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.0.tgz#0caf9d2d755d93aee049d4bdd0d3fe2cca2a24eb"
+ dependencies:
+ http-parser-js ">=0.4.0"
+ websocket-extensions ">=0.1.1"
+
+websocket-extensions@>=0.1.1:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29"
+
+whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.3.tgz#57c235bc8657e914d24e1a397d3c82daee0a6ba3"
+ dependencies:
+ iconv-lite "0.4.19"
+
+whatwg-fetch@>=0.10.0, whatwg-fetch@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz#9c84ec2dcf68187ff00bc64e1274b442176e1c84"
+
+whatwg-url@^6.4.0:
+ version "6.4.0"
+ resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.4.0.tgz#08fdf2b9e872783a7a1f6216260a1d66cc722e08"
+ dependencies:
+ lodash.sortby "^4.7.0"
+ tr46 "^1.0.0"
+ webidl-conversions "^4.0.1"
+
+which-module@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f"
+
+which-module@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
+
+which@1, which@^1.2.10, which@^1.2.12, which@^1.2.14, which@^1.2.9, which@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a"
+ dependencies:
+ isexe "^2.0.0"
+
+wide-align@^1.1.0:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.2.tgz#571e0f1b0604636ebc0dfc21b0339bbe31341710"
+ dependencies:
+ string-width "^1.0.2"
+
+window-size@0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d"
+
+with@^5.0.0:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/with/-/with-5.1.1.tgz#fa4daa92daf32c4ea94ed453c81f04686b575dfe"
+ dependencies:
+ acorn "^3.1.0"
+ acorn-globals "^3.0.0"
+
+wordwrap@0.0.2:
+ version "0.0.2"
+ resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f"
+
+wordwrap@~0.0.2:
+ version "0.0.3"
+ resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
+
+wordwrap@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
+
+wrap-ansi@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"
+ dependencies:
+ string-width "^1.0.1"
+ strip-ansi "^3.0.1"
+
+wrappy@1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
+
+write-file-atomic@^2.1.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.3.0.tgz#1ff61575c2e2a4e8e510d6fa4e243cce183999ab"
+ dependencies:
+ graceful-fs "^4.1.11"
+ imurmurhash "^0.1.4"
+ signal-exit "^3.0.2"
+
+write@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757"
+ dependencies:
+ mkdirp "^0.5.1"
+
+ws@^1.0.1:
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-1.1.5.tgz#cbd9e6e75e09fc5d2c90015f21f0c40875e0dd51"
+ dependencies:
+ options ">=0.0.5"
+ ultron "1.0.x"
+
+ws@^3.0.0:
+ version "3.3.3"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz#f1cf84fe2d5e901ebce94efaece785f187a228f2"
+ dependencies:
+ async-limiter "~1.0.0"
+ safe-buffer "~5.1.0"
+ ultron "~1.1.0"
+
+ws@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-4.0.0.tgz#bfe1da4c08eeb9780b986e0e4d10eccd7345999f"
+ dependencies:
+ async-limiter "~1.0.0"
+ safe-buffer "~5.1.0"
+ ultron "~1.1.0"
+
+xml-name-validator@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
+
+xml2js@^0.4.19:
+ version "0.4.19"
+ resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7"
+ dependencies:
+ sax ">=0.6.0"
+ xmlbuilder "~9.0.1"
+
+xmlbuilder@~9.0.1:
+ version "9.0.4"
+ resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.4.tgz#519cb4ca686d005a8420d3496f3f0caeecca580f"
+
+xmlhttprequest@1:
+ version "1.8.0"
+ resolved "https://registry.yarnpkg.com/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz#67fe075c5c24fef39f9d65f5f7b7fe75171968fc"
+
+xo-acl-resolver@^0.2.3:
+ version "0.2.3"
+ resolved "https://registry.yarnpkg.com/xo-acl-resolver/-/xo-acl-resolver-0.2.3.tgz#693f4181727379be0d969f7c22d660f3bddf935a"
+
+xo-common@^0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/xo-common/-/xo-common-0.1.1.tgz#bdad9ea7926c1f27d8fdaecc92d672854c911815"
+ dependencies:
+ babel-runtime "^6.18.0"
+ lodash "^4.16.6"
+ make-error "^1.2.1"
+
+xo-lib@^0.8.0:
+ version "0.8.2"
+ resolved "https://registry.yarnpkg.com/xo-lib/-/xo-lib-0.8.2.tgz#0fab6836d07278444d15ae028382a9448271119d"
+ dependencies:
+ jsonrpc-websocket-client "^0.1.2"
+ lodash "^4.17.2"
+ make-error "^1.0.4"
+
+xo-remote-parser@^0.3:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/xo-remote-parser/-/xo-remote-parser-0.3.0.tgz#4cbf8391151eb9820a7df8ece64ba79ea44fd070"
+ dependencies:
+ lodash "^4.13.1"
+
+"xtend@>=4.0.0 <4.1.0-0", xtend@^4.0.0, xtend@~4.0.0, xtend@~4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
+
+xtend@~2.1.1:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/xtend/-/xtend-2.1.2.tgz#6efecc2a4dad8e6962c4901b337ce7ba87b5d28b"
+ dependencies:
+ object-keys "~0.4.0"
+
+xxhashjs@^0.2.1:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/xxhashjs/-/xxhashjs-0.2.2.tgz#8a6251567621a1c46a5ae204da0249c7f8caa9d8"
+ dependencies:
+ cuint "^0.2.2"
+
+y18n@^3.2.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"
+
+yallist@^2.1.2:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
+
+yargs-parser@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.0.tgz#275ecf0d7ffe05c77e64e7c86e4cd94bf0e1228a"
+ dependencies:
+ camelcase "^3.0.0"
+
+yargs-parser@^8.1.0:
+ version "8.1.0"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-8.1.0.tgz#f1376a33b6629a5d063782944da732631e966950"
+ dependencies:
+ camelcase "^4.1.0"
+
+yargs@^10.0.3:
+ version "10.1.2"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-10.1.2.tgz#454d074c2b16a51a43e2fb7807e4f9de69ccb5c5"
+ dependencies:
+ cliui "^4.0.0"
+ decamelize "^1.1.1"
+ find-up "^2.1.0"
+ get-caller-file "^1.0.1"
+ os-locale "^2.0.0"
+ require-directory "^2.1.1"
+ require-main-filename "^1.0.1"
+ set-blocking "^2.0.0"
+ string-width "^2.0.0"
+ which-module "^2.0.0"
+ y18n "^3.2.1"
+ yargs-parser "^8.1.0"
+
+yargs@^7.0.0, yargs@^7.1.0:
+ version "7.1.0"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.0.tgz#6ba318eb16961727f5d284f8ea003e8d6154d0c8"
+ dependencies:
+ camelcase "^3.0.0"
+ cliui "^3.2.0"
+ decamelize "^1.1.1"
+ get-caller-file "^1.0.1"
+ os-locale "^1.4.0"
+ read-pkg-up "^1.0.1"
+ require-directory "^2.1.1"
+ require-main-filename "^1.0.1"
+ set-blocking "^2.0.0"
+ string-width "^1.0.2"
+ which-module "^1.0.0"
+ y18n "^3.2.1"
+ yargs-parser "^5.0.0"
+
+yargs@~3.10.0:
+ version "3.10.0"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1"
+ dependencies:
+ camelcase "^1.0.2"
+ cliui "^2.1.0"
+ decamelize "^1.0.0"
+ window-size "0.1.0"