Compare commits

..

30 Commits

Author SHA1 Message Date
BCedric
1083aba33c add message for current operation 2017-06-23 16:51:46 +02:00
BCedric
d90156ff15 tabManagement, remove displaying vm list if the group is empty 2017-06-23 16:17:25 +02:00
BCedric
45b6e010df correct getMemoryTotal function 2017-06-23 16:14:43 +02:00
BCedric
1fbdb7799e display current operations 2017-06-23 16:12:45 +02:00
BCedric
3050fd7ce1 display an halted icon if the groupe is empty 2017-06-23 16:09:04 +02:00
BCedric
1988934f8a use proptypes decorator 2017-06-23 16:04:31 +02:00
BCedric
5331ced4fe add dragNDropOrder generic component 2017-06-22 15:57:56 +02:00
BCedric
74066f7d44 move NewVmGroup in new folder 2017-06-22 15:57:56 +02:00
BCedric
98e3aa89bb use intl in vm list 2017-06-22 15:57:56 +02:00
BCedric
070bb65740 use connectStore in VmGroup TabGeneral 2017-06-22 15:57:56 +02:00
BCedric
4e51959e3b add function removeAppliance 2017-06-22 15:57:56 +02:00
BCedric
b5ff695c74 add functions to calculate stats 2017-06-22 15:57:56 +02:00
BCedric
f879e2ace0 add VmGroup handlers in Home mainActions 2017-06-22 15:55:58 +02:00
BCedric
9aeabce4d8 add createVmGroup function 2017-06-22 15:55:58 +02:00
BCedric
2f17cd4ba9 allow to delete a vmGroup 2017-06-22 15:55:58 +02:00
BCedric
60e70a08c1 allow vms management 2017-06-22 15:55:58 +02:00
BCedric
6e0ba2bae3 add editVmGroup function 2017-06-22 15:55:58 +02:00
BCedric
feb996890e management of icon color 2017-06-22 15:55:58 +02:00
BCedric
a06bc85142 allow edition of a vmGroup 2017-06-22 15:55:58 +02:00
BCedric
f530aef92b remove vmGroup tag 2017-06-22 15:55:58 +02:00
BCedric
2eb7330335 add functions on action-bar 2017-06-22 15:55:58 +02:00
BCedric
89157e7b7e rename vm-groupe to vmGroup 2017-06-22 15:55:58 +02:00
BCedric
3834e2ef91 expose vmGroup, remove subscription 2017-06-22 15:55:58 +02:00
BCedric
a711231955 add creation of a vmGroup 2017-06-22 15:55:58 +02:00
BCedric
0a5e301b3e add stats tab 2017-06-22 15:55:58 +02:00
BCedric
c82b9893c5 add advanced tab 2017-06-22 15:55:58 +02:00
BCedric
f4dfabc34c add management tab 2017-06-22 15:55:58 +02:00
BCedric
059521aeda add general tab 2017-06-22 15:55:58 +02:00
BCedric
debca09e2c remove _getItems, rename VMGroupItem => VmGroupItem 2017-06-22 15:55:58 +02:00
BCedric
0699cfc449 display VM-Groups list 2017-06-22 15:55:58 +02:00
106 changed files with 4759 additions and 8510 deletions

View File

@@ -1,131 +1,5 @@
# ChangeLog
## **5.14.0** (2017-10-31)
### Enhancements
* VM snapshot description display [#2458](https://github.com/vatesfr/xo-web/issues/2458)
* [Home] Ability to sort VM by number of snapshots [#2450](https://github.com/vatesfr/xo-web/issues/2450)
* Display XS version in host view [#2439](https://github.com/vatesfr/xo-web/issues/2439)
* [File restore]: Clarify the possibility to select multiple files [#2438](https://github.com/vatesfr/xo-web/issues/2438)
* [Continuous Replication] Time in replicated VMs [#2431](https://github.com/vatesfr/xo-web/issues/2431)
* [SortedTable] Active page in URL param [#2405](https://github.com/vatesfr/xo-web/issues/2405)
* replace all '...' with the UTF-8 equivalent [#2391](https://github.com/vatesfr/xo-web/issues/2391)
* [SortedTable] Explicit when no items [#2388](https://github.com/vatesfr/xo-web/issues/2388)
* Handle patching licenses [#2382](https://github.com/vatesfr/xo-web/issues/2382)
* Credential leaking in logs for messages regarding invalid credentials and "too fast authentication" [#2363](https://github.com/vatesfr/xo-web/issues/2363)
* [SortedTable] Keyboard support [#2330](https://github.com/vatesfr/xo-web/issues/2330)
* token.create should accept an expiration [#1769](https://github.com/vatesfr/xo-web/issues/1769)
* On updater error, display link to documentation [#1610](https://github.com/vatesfr/xo-web/issues/1610)
### Bugs
* Config drive - Custom config not working properly [#2449](https://github.com/vatesfr/xo-web/issues/2449)
* Snapshot sorted table breaks copyVm [#2446](https://github.com/vatesfr/xo-web/issues/2446)
* [vm/snapshots] Incorrect default sort order [#2442](https://github.com/vatesfr/xo-web/issues/2442)
* [Backups/Jobs] Incorrect months mapping [#2427](https://github.com/vatesfr/xo-web/issues/2427)
* [Xapi#barrier()] Not compatible with XenServer < 6.1 [#2418](https://github.com/vatesfr/xo-web/issues/2418)
* [SortedTable] Change page when no more items on the page [#2401](https://github.com/vatesfr/xo-web/issues/2401)
* Review and fix creating a VM from a snapshot [#2343](https://github.com/vatesfr/xo-web/issues/2343)
* Unable to edit / save restored backup job [#1922](https://github.com/vatesfr/xo-web/issues/1922)
## **5.13.0** (2017-09-29)
### Enhancements
* replace all '...' with the UTF-8 equivalent [#2391](https://github.com/vatesfr/xo-web/issues/2391)
* [SortedTable] Explicit when no items [#2388](https://github.com/vatesfr/xo-web/issues/2388)
* Auto select iqn or lun if there is only one [#2379](https://github.com/vatesfr/xo-web/issues/2379)
* [Sparklines] Hide points [#2370](https://github.com/vatesfr/xo-web/issues/2370)
* Allow xo-server-recover-account to generate a random password [#2360](https://github.com/vatesfr/xo-web/issues/2360)
* Add disk in existing VM as self user [#2348](https://github.com/vatesfr/xo-web/issues/2348)
* Sorted table for Settings/server [#2340](https://github.com/vatesfr/xo-web/issues/2340)
* Sign in should be case insensitive [#2337](https://github.com/vatesfr/xo-web/issues/2337)
* [SortedTable] Extend checkbox click to whole column [#2329](https://github.com/vatesfr/xo-web/issues/2329)
* [SortedTable] Ability to select all items (across pages) [#2324](https://github.com/vatesfr/xo-web/issues/2324)
* [SortedTable] Range selection [#2323](https://github.com/vatesfr/xo-web/issues/2323)
* Warning on SMB remote creation [#2316](https://github.com/vatesfr/xo-web/issues/2316)
* [Home | SortedTable] Add link to syntax doc in the filter input [#2305](https://github.com/vatesfr/xo-web/issues/2305)
* [SortedTable] Add optional binding of filter to an URL query [#2301](https://github.com/vatesfr/xo-web/issues/2301)
* [Home][Keyboard navigation] Allow selecting the objects [#2214](https://github.com/vatesfr/xo-web/issues/2214)
* SR view / Disks: option to display non managed VDIs [#1724](https://github.com/vatesfr/xo-web/issues/1724)
* Continuous Replication Retention [#1692](https://github.com/vatesfr/xo-web/issues/1692)
### Bugs
* iSCSI issue on LUN selector [#2374](https://github.com/vatesfr/xo-web/issues/2374)
* Errors in VM copy are not properly reported [#2347](https://github.com/vatesfr/xo-web/issues/2347)
* Removing a PIF IP fails [#2346](https://github.com/vatesfr/xo-web/issues/2346)
* Review and fix creating a VM from a snapshot [#2343](https://github.com/vatesfr/xo-web/issues/2343)
* iSCSI LUN Detection fails with authentification [#2339](https://github.com/vatesfr/xo-web/issues/2339)
* Fix PoolActionBar to add a new SR [#2307](https://github.com/vatesfr/xo-web/issues/2307)
* [VM migration] Error if default SR not accessible to target host [#2180](https://github.com/vatesfr/xo-web/issues/2180)
* A job shouldn't executable more than once at the same time [#2053](https://github.com/vatesfr/xo-web/issues/2053)
## **5.12.0** (2017-08-31)
### Enhancements
* PIF selector with physical status [#2326](https://github.com/vatesfr/xo-web/issues/2326)
* [SortedTable] Range selection [#2323](https://github.com/vatesfr/xo-web/issues/2323)
* Self service filter for home/VM view [#2303](https://github.com/vatesfr/xo-web/issues/2303)
* SR/Disks Display total of VDIs to coalesce [#2300](https://github.com/vatesfr/xo-web/issues/2300)
* Pool filter in the task view [#2293](https://github.com/vatesfr/xo-web/issues/2293)
* "Loading" while fetching objects [#2285](https://github.com/vatesfr/xo-web/issues/2285)
* [SortedTable] Add grouped actions feature [#2276](https://github.com/vatesfr/xo-web/issues/2276)
* Add a filter to the backups' log [#2246](https://github.com/vatesfr/xo-web/issues/2246)
* It should not be possible to migrate a halted VM. [#2233](https://github.com/vatesfr/xo-web/issues/2233)
* [Home][Keyboard navigation] Allow selecting the objects [#2214](https://github.com/vatesfr/xo-web/issues/2214)
* Allow to set pool master [#2213](https://github.com/vatesfr/xo-web/issues/2213)
* Continuous Replication Retention [#1692](https://github.com/vatesfr/xo-web/issues/1692)
### Bugs
* Home pagination bug [#2310](https://github.com/vatesfr/xo-web/issues/2310)
* Fix PoolActionBar to add a new SR [#2307](https://github.com/vatesfr/xo-web/issues/2307)
* VM snapshots are not correctly deleted [#2304](https://github.com/vatesfr/xo-web/issues/2304)
* Parallel deletion of VMs fails [#2297](https://github.com/vatesfr/xo-web/issues/2297)
* Continous replication create multiple zombie disks [#2292](https://github.com/vatesfr/xo-web/issues/2292)
* Add user to Group issue [#2196](https://github.com/vatesfr/xo-web/issues/2196)
* [VM migration] Error if default SR not accessible to target host [#2180](https://github.com/vatesfr/xo-web/issues/2180)
## **5.11.0** (2017-07-31)
### Enhancements
- Storage VHD chain health [\#2178](https://github.com/vatesfr/xo-web/issues/2178)
### Bug fixes
- No web VNC console [\#2258](https://github.com/vatesfr/xo-web/issues/2258)
- Patching issues [\#2254](https://github.com/vatesfr/xo-web/issues/2254)
- Advanced button in VM creation for self service user [\#2202](https://github.com/vatesfr/xo-web/issues/2202)
- Hide "new VM" menu entry if not admin or not self service user [\#2191](https://github.com/vatesfr/xo-web/issues/2191)
## **5.10.0** (2017-06-30)
### Enhancements
- 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

View File

@@ -183,11 +183,7 @@ function browserify (path, opts) {
// 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
})
bundler = require('watchify')(bundler)
}
// Append the extension if necessary.

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "xo-web",
"version": "5.14.0",
"version": "5.9.1",
"license": "AGPL-3.0",
"description": "Web interface client for Xen-Orchestra",
"keywords": [
@@ -31,10 +31,9 @@
"npm": ">=3"
},
"devDependencies": {
"@nraynaud/novnc": "^0.6.1-1",
"ansi_up": "^1.3.0",
"asap": "^2.0.6",
"babel-eslint": "^8.0.1",
"asap": "^2.0.4",
"babel-eslint": "^7.0.0",
"babel-plugin-dev": "^1.0.0",
"babel-plugin-lodash": "^3.2.11",
"babel-plugin-transform-decorators-legacy": "^1.3.4",
@@ -46,23 +45,22 @@
"babel-preset-es2015": "^6.6.0",
"babel-preset-react": "^6.5.0",
"babel-preset-stage-0": "^6.5.0",
"babel-register": "^6.26.0",
"babel-runtime": "^6.26.0",
"babel-register": "^6.16.3",
"babel-runtime": "^6.6.1",
"babelify": "^7.2.0",
"benchmark": "^2.1.0",
"bootstrap": "4.0.0-alpha.5",
"browserify": "^14.1.0",
"bundle-collapser": "^1.3.0",
"bundle-collapser": "^1.2.1",
"chartist": "^0.10.1",
"chartist-plugin-legend": "^0.6.1",
"chartist-plugin-tooltip": "0.0.11",
"classnames": "^2.2.3",
"cookies-js": "^1.2.2",
"d3": "^4.11.0",
"d3": "^4.2.8",
"dependency-check": "^2.5.1",
"enzyme": "^3.1.0",
"enzyme-adapter-react-15": "^1.0.1",
"enzyme-to-json": "^3.1.1",
"enzyme": "^2.6.0",
"enzyme-to-json": "^1.4.4",
"event-to-promise": "^0.8.0",
"font-awesome": "^4.7.0",
"font-mfizz": "github:fizzed/font-mfizz",
@@ -80,63 +78,63 @@
"gulp-uglify": "^3.0.0",
"gulp-watch": "^4.3.5",
"human-format": "^0.8.0",
"husky": "^0.14.3",
"immutable": "^3.8.2",
"husky": "^0.13.1",
"index-modules": "^0.3.0",
"is-ip": "^1.0.0",
"jest": "^21.2.1",
"jsonrpc-websocket-client": "^0.2.0",
"jest": "^20.0.4",
"jsonrpc-websocket-client": "^0.1.1",
"kindof": "^2.0.0",
"later": "^1.2.0",
"lodash": "^4.6.1",
"loose-envify": "^1.1.0",
"make-error": "^1.2.1",
"marked": "^0.3.5",
"modular-css": "^6.0.2",
"modular-css": "^5.1.6",
"moment": "^2.13.0",
"moment-timezone": "^0.5.4",
"notifyjs": "^3.0.0",
"promise-toolbox": "^0.9.5",
"novnc-node": "^0.5.3",
"promise-toolbox": "^0.9.4",
"random-password": "^0.1.2",
"react": "^15.4.1",
"react-addons-shallow-compare": "^15.6.2",
"react-addons-test-utils": "^15.6.2",
"react-addons-shallow-compare": "^15.1.0",
"react-addons-test-utils": "^15.4.1",
"react-bootstrap-4": "^0.29.1",
"react-chartist": "^0.13.0",
"react-copy-to-clipboard": "^5.0.1",
"react-dnd": "^2.5.3",
"react-dnd-html5-backend": "^2.5.3",
"react-chartist": "^0.12.0",
"react-copy-to-clipboard": "^5.0.0",
"react-debounce-input": "^3.0.0",
"react-dnd": "^2.1.4",
"react-dnd-html5-backend": "^2.1.2",
"react-document-title": "^2.0.2",
"react-dom": "^15.4.1",
"react-dropzone": "^4.1.3",
"react-intl": "^2.4.0",
"react-key-handler": "^1.0.0",
"react-notify": "^3.0.0",
"react-overlays": "^0.8.1",
"react-redux": "^5.0.6",
"react-dropzone": "^3.5.0",
"react-intl": "^2.0.1",
"react-key-handler": "^0.3.0",
"react-notify": "^2.0.1",
"react-overlays": "^0.6.0",
"react-redux": "^5.0.0",
"react-router": "^3.0.0",
"react-select": "^1.0.0-rc.10",
"react-shortcuts": "^1.6.1",
"react-sparklines": "1.6.0",
"react-test-renderer": "^15.6.2",
"react-select": "^1.0.0-rc.4",
"react-shortcuts": "^1.3.1",
"react-sparklines": "^1.5.0",
"react-virtualized": "^8.0.8",
"readable-stream": "^2.3.3",
"redux": "^3.7.2",
"readable-stream": "^2.0.6",
"redux": "^3.3.1",
"redux-devtools": "^3.1.1",
"redux-devtools-dock-monitor": "^1.1.0",
"redux-devtools-log-monitor": "^1.0.5",
"redux-thunk": "^2.0.1",
"reselect": "^2.5.4",
"semver": "^5.4.1",
"standard": "^10.0.3",
"styled-components": "^2.2.1",
"superagent": "^3.6.3",
"semver": "^5.3.0",
"standard": "^10.0.0",
"styled-components": "^2.1.0",
"superagent": "^3.5.0",
"tar-stream": "^1.5.2",
"uglify-es": "^3.1.3",
"uglify-es": "^3.0.18",
"uncontrollable-input": "^0.0.1",
"vinyl": "^2.1.0",
"vinyl": "^2.0.0",
"watchify": "^3.7.0",
"xml2js": "^0.4.19",
"xml2js": "^0.4.17",
"xo-acl-resolver": "^0.2.3",
"xo-common": "^0.1.1",
"xo-lib": "^0.8.0",
@@ -188,7 +186,6 @@
]
},
"jest": {
"setupTestFrameworkScriptFile": "./setup-tests.js",
"snapshotSerializers": [
"enzyme-to-json/serializer"
]

View File

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

View File

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

View File

@@ -1,11 +1,9 @@
import clone from 'lodash/clone'
import includes from 'lodash/includes'
import isArray from 'lodash/isArray'
import forEach from 'lodash/forEach'
import map from 'lodash/map'
import { PureComponent } from 'react'
import { cowSet } from 'utils'
import {
includes,
isArray,
forEach,
map
} from 'lodash'
import getEventValue from './get-event-value'
@@ -14,6 +12,17 @@ import getEventValue from './get-event-value'
// Usually set to process.env.NODE_ENV !== 'production'.
const VERBOSE = false
const cowSet = (object, path, value, depth) => {
if (depth >= path.length) {
return value
}
object = object != null ? clone(object) : {}
const prop = path[depth]
object[prop] = cowSet(object[prop], path, value, depth + 1)
return object
}
const get = (object, path, depth) => {
if (depth >= path.length) {
return object

View File

@@ -17,16 +17,16 @@ const CARD_HEADER_STYLE = {
}
export const Card = propTypes({
disableMaxHeight: propTypes.bool,
shadow: propTypes.bool
})(({
shadow,
...props
}) => {
props.className = 'card'
props.style = shadow ? CARD_STYLE_WITH_SHADOW : CARD_STYLE
return <div {...props} />
})
children,
shadow
}) => (
<div className='card' style={shadow ? CARD_STYLE_WITH_SHADOW : CARD_STYLE}>
{children}
</div>
))
export const CardHeader = propTypes({
className: propTypes.string

View File

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

View File

@@ -0,0 +1,128 @@
import _ from 'intl'
import ActionButton from 'action-button'
import Component from 'base-component'
import HTML5Backend from 'react-dnd-html5-backend'
import Icon from 'icon'
import map from 'lodash/map'
import propTypes from 'prop-types-decorator'
import React from 'react'
import { DragDropContext, DragSource, DropTarget } from 'react-dnd'
import { Toggle } from 'form'
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, toggle } = this.props
return connectDragSource(connectDropTarget(
<li className='list-group-item'>
<Icon icon='grab' />
{' '}
<Icon icon='grab' />
{' '}
{item.text}
{toggle && <span className='pull-right'>
<Toggle value={item.active} onChange={this._toggle} />
</span>}
</li>
))
}
}
@propTypes({
onClose: propTypes.func
})
@DragDropContext(HTML5Backend)
export default class DragNDropOrder extends Component {
constructor (props) {
super(props)
const { parseOrderParam, parseOrder } = props
this.state = parseOrder(parseOrderParam)
}
_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 { parseOrderParam, parseOrder } = this.props
this.state = this.setState(parseOrder(parseOrderParam))
}
_save = () => {
const { order, toggleActive } = this.state
this.props.setOrder(this.props.parseOrderParam, order, toggleActive)
}
_toggleOnChange = event => this.setState({toggleActive: event})
render () {
const { order, toggleActive } = this.state
const { toggleItems } = this.props
return <form>
{!toggleItems && <Toggle value={toggleActive} onChange={this._toggleOnChange} />}
<ul>
{map(order, (item, index) => <OrderItem
toggle={toggleItems}
key={index}
index={index}
id={item.id}
// FIXME missing translation
item={item}
move={this._moveOrderItem}
/>)}
</ul>
<fieldset className='form-inline'>
<span className='pull-right'>
<ActionButton icon='save' btnStyle='primary' handler={this._save}>{_('saveBootOption')}</ActionButton>
{' '}
<ActionButton icon='reset' handler={this._reset}>{_('resetBootOption')}</ActionButton>
</span>
</fieldset>
</form>
}
}

View File

@@ -25,7 +25,6 @@ import {
SelectSr,
SelectSubject,
SelectTag,
SelectVgpuType,
SelectVm,
SelectVmTemplate
} from '../select-objects'
@@ -386,12 +385,12 @@ const MAP_TYPE_SELECT = {
SR: SelectSr,
subject: SelectSubject,
tag: SelectTag,
vgpuType: SelectVgpuType,
VM: SelectVm,
'VM-template': SelectVmTemplate
}
@propTypes({
labelProp: propTypes.string.isRequired,
value: propTypes.oneOfType([
propTypes.string,
propTypes.object

View File

@@ -14,10 +14,10 @@ import {
import Button from '../button'
import Component from '../base-component'
import defined from '../xo-defined'
import getEventValue from '../get-event-value'
import propTypes from '../prop-types-decorator'
import {
firstDefined,
formatSizeRaw,
parseSize
} from '../utils'
@@ -158,7 +158,7 @@ export class SizeInput extends BaseComponent {
constructor (props) {
super(props)
this.state = this._createStateFromBytes(defined(props.value, props.defaultValue, null))
this.state = this._createStateFromBytes(firstDefined(props.value, props.defaultValue, null))
}
componentWillReceiveProps (props) {

View File

@@ -15,7 +15,7 @@ import Select from './select'
multi: propTypes.bool,
onChange: propTypes.func,
options: propTypes.array,
placeholder: propTypes.node,
placeholder: propTypes.string,
predicate: propTypes.func,
required: propTypes.bool,
value: propTypes.any

View File

@@ -31,3 +31,8 @@ export const SR = {
...common,
homeFilterTags: 'tags:'
}
export const vmGroup = {
...common,
homeFilterTags: 'tags:'
}

View File

@@ -1,16 +1,13 @@
import isEmpty from 'lodash/isEmpty'
import keys from 'lodash/keys'
import map from 'lodash/map'
import React from 'react'
import { Portal } from 'react-overlays'
import {
forEach,
isEmpty,
keys,
map,
noop
} from 'lodash'
import _ from './intl'
import ActionButton from './action-button'
import Component from './base-component'
import forEach from 'lodash/forEach'
import Link from './link'
import propTypes from './prop-types-decorator'
import SortedTable from './sorted-table'
@@ -78,9 +75,6 @@ const ActionButton_ = ({ children, labelId, ...props }) =>
// ===================================================================
@connectStore({
hostsById: createGetObjectsOfType('host').groupBy('id')
})
class HostsPatchesTable extends Component {
constructor (props) {
super(props)
@@ -96,11 +90,9 @@ class HostsPatchesTable extends Component {
)
_subscribeMissingPatches = (hosts = this.props.hosts) => {
const { hostsById } = this.props
const unsubs = map(hosts, host => hostsById
? subscribeHostMissingPatches(
hostsById[host.id][0],
const unsubs = map(hosts, host =>
subscribeHostMissingPatches(
host,
patches => this.setState({
missingPatches: {
...this.state.missingPatches,
@@ -108,7 +100,6 @@ class HostsPatchesTable extends Component {
}
})
)
: noop
)
if (this.unsubscribeMissingPatches !== undefined) {

View File

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

View File

@@ -48,10 +48,6 @@ const getMessage = (props, messageId, values, render) => {
{render}
</FormattedMessage>
}
getMessage.keyValue = (key, value) => getMessage('keyValue', {
key: <strong>{key}</strong>,
value
})
export { getMessage as default }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1427,8 +1427,8 @@ export default {
// Original text: "Installation started"
supplementalPackInstallStartedTitle: 'Installation Started',
// Original text: "Installing new supplemental pack"
supplementalPackInstallStartedMessage: 'Installing new supplemental pack',
// Original text: "Installing new supplemental pack..."
supplementalPackInstallStartedMessage: 'Installing new supplemental pack...',
// Original text: "Installation error"
supplementalPackInstallErrorTitle: 'Installation error',
@@ -2894,8 +2894,8 @@ export default {
// Original text: "Connection failed"
serverConnectionFailed: 'Csatlakozás Sikertelen',
// Original text: "Connecting"
serverConnecting: 'Csatlakozás',
// Original text: "Connecting..."
serverConnecting: 'Csatlakozás...',
// Original text: "Connected"
serverConnected: 'Kapcsolódva',
@@ -3557,7 +3557,7 @@ export default {
// Original text: 'Create'
xosanCreate: undefined,
// Original text: 'Installing XOSAN. Please wait'
// Original text: 'Installing XOSAN. Please wait...'
xosanInstalling: undefined,
// Original text: 'You need XenServer 7.0 to install XOSAN'
@@ -3572,7 +3572,7 @@ export default {
// Original text: 'Load cloud plugin first'
xosanLoadCloudPlugin: undefined,
// Original text: 'Loading'
// Original text: 'Loading...'
xosanLoading: undefined,
// Original text: 'XOSAN is not available at the moment'

View File

@@ -204,7 +204,7 @@ export default {
editUserProfile: undefined,
// Original text: "Fetching data…"
homeFetchingData: 'Obtendo dados',
homeFetchingData: 'Obtendo dados...',
// Original text: "Welcome on Xen Orchestra!"
homeWelcome: 'Bem-vindo ao Xen Orchestra',
@@ -228,7 +228,7 @@ export default {
homeNoVms: 'Não foram encontradas VMs!',
// Original text: "Or…"
homeNoVmsOr: 'Ou',
homeNoVmsOr: 'Ou...',
// Original text: "Import VM"
homeImportVm: 'Importar VM',
@@ -330,7 +330,7 @@ export default {
homeMore: 'Mais',
// Original text: "Migrate to…"
homeMigrateTo: 'Migrar para',
homeMigrateTo: 'Migrar para...',
// Original text: 'Missing patches'
homeMissingPaths: undefined,
@@ -360,28 +360,28 @@ export default {
selectSubjects: 'Escolha um usuário(s) e/ou grupo(s)',
// Original text: "Select Object(s)…"
selectObjects: 'Selecionar Objeto(s)',
selectObjects: 'Selecionar Objeto(s)...',
// Original text: "Choose a role"
selectRole: 'Escolha uma função',
// Original text: "Select Host(s)…"
selectHosts: 'Selecionar Host(s)',
selectHosts: 'Selecionar Host(s)...',
// Original text: "Select object(s)…"
selectHostsVms: 'Selecionar Objeto(s)',
selectHostsVms: 'Selecionar Objeto(s)...',
// Original text: "Select Network(s)…"
selectNetworks: 'Selecionar Rede(s)',
selectNetworks: 'Selecionar Rede(s)...',
// Original text: "Select PIF(s)…"
selectPifs: 'Selecionar PIF(s)',
selectPifs: 'Selecionar PIF(s)...',
// Original text: "Select Pool(s)…"
selectPools: 'Selecionar Pool(s)',
selectPools: 'Selecionar Pool(s)...',
// Original text: "Select Remote(s)…"
selectRemotes: 'Selecionar Remote(s)',
selectRemotes: 'Selecionar Remote(s)...',
// Original text: 'Select resource set(s)…'
selectResourceSets: undefined,
@@ -402,19 +402,19 @@ export default {
selectSshKey: undefined,
// Original text: "Select SR(s)…"
selectSrs: 'Selecionar SR(s)',
selectSrs: 'Selecionar SR(s)...',
// Original text: "Select VM(s)…"
selectVms: 'Selecionar VM(s)',
selectVms: 'Selecionar VM(s)...',
// Original text: "Select VM template(s)…"
selectVmTemplates: 'Selecionar VM(s) modelo(s)',
selectVmTemplates: 'Selecionar VM(s) modelo(s)...',
// Original text: "Select tag(s)…"
selectTags: 'Selecionar etiqueta(s)',
selectTags: 'Selecionar etiqueta(s)...',
// Original text: "Select disk(s)…"
selectVdis: 'Selecionar disco(s)',
selectVdis: 'Selecionar disco(s)...',
// Original text: 'Select timezone…'
selectTimezone: undefined,
@@ -1968,7 +1968,7 @@ export default {
statsDashboardSelectObjects: 'Selecionar',
// Original text: "Loading…"
metricsLoading: 'Carregando',
metricsLoading: 'Carregando...',
// Original text: "Coming soon!"
comingSoon: 'Em breve!',
@@ -2292,10 +2292,10 @@ export default {
vmImportFailed: 'Falha na importação',
// Original text: "Import starting…"
startVmImport: 'Iniciando importação',
startVmImport: 'Iniciando importação...',
// Original text: "Export starting…"
startVmExport: 'Iniciando exportação',
startVmExport: 'Iniciando exportação...',
// Original text: 'N CPUs'
nCpus: undefined,
@@ -2559,7 +2559,7 @@ export default {
importBackupModalStart: 'Iniciar VM após restauração',
// Original text: "Select your backup…"
importBackupModalSelectBackup: 'Selecionar backup',
importBackupModalSelectBackup: 'Selecionar backup...',
// Original text: "Are you sure you want to remove all orphaned snapshot VDIs?"
removeAllOrphanedModalWarning: 'Você tem certeza que deseja remover todos as VDIs orfãs?',

View File

@@ -285,7 +285,7 @@ export default {
homeMore: '更多',
// Original text: "Migrate to…"
homeMigrateTo: '迁移至',
homeMigrateTo: '迁移至...',
// Original text: "Missing patches"
homeMissingPaths: '缺少补丁',
@@ -1467,7 +1467,7 @@ export default {
statsDashboardSelectObjects: '选择',
// Original text: "Loading…"
metricsLoading: '加载中.',
metricsLoading: '加载中....',
// Original text: "Coming soon!"
comingSoon: '即将呈现',
@@ -1947,7 +1947,7 @@ export default {
importBackupModalStart: '恢复后启动虚拟机',
// Original text: "Select your backup…"
importBackupModalSelectBackup: '选择你的备份',
importBackupModalSelectBackup: '选择你的备份...',
// Original text: "Are you sure you want to remove all orphaned VDIs?"
removeAllOrphanedModalWarning: '你确定要删除所有孤立的虚拟磁盘?',

View File

@@ -5,8 +5,6 @@ var forEach = require('lodash/forEach')
var isString = require('lodash/isString')
var messages = {
keyValue: '{key}: {value}',
statusConnecting: 'Connecting',
statusDisconnected: 'Disconnected',
statusLoading: 'Loading…',
@@ -30,8 +28,6 @@ var messages = {
filterOnlyRegular: 'Normal disks',
filterOnlySnapshots: 'Snapshot disks',
filterOnlyUnmanaged: 'Unmanaged disks',
filterSaveAs: 'Save…',
filterSyntaxLinkTooltip: 'Explore the search syntax in the documentation',
// ----- Copiable component -----
copyToClipboard: 'Copy to clipboard',
@@ -42,6 +38,7 @@ var messages = {
// ----- Titles -----
homePage: 'Home',
homeVmPage: 'VMs',
homeVmGroupPage: 'VM-Groups',
homeHostPage: 'Hosts',
homePoolPage: 'Pools',
homeTemplatePage: 'Templates',
@@ -70,6 +67,7 @@ var messages = {
taskMenu: 'Tasks',
taskPage: 'Tasks',
newVmPage: 'VM',
newVmGroupPage: 'VM-Group',
newSrPage: 'Storage',
newServerPage: 'Server',
newImport: 'Import',
@@ -105,7 +103,7 @@ var messages = {
// ----- Home view ------
homeFetchingData: 'Fetching data…',
homeWelcome: 'Welcome to Xen Orchestra!',
homeWelcome: 'Welcome on Xen Orchestra!',
homeWelcomeText: 'Add your XenServer hosts or pools',
homeConnectServerText: 'Some XenServers have been registered but are not connected',
homeHelp: 'Want some help?',
@@ -125,13 +123,13 @@ var messages = {
homeTypePool: 'Pool',
homeTypeHost: 'Host',
homeTypeVm: 'VM',
homeTypeVmGroup: 'VM group',
homeTypeSr: 'SR',
homeTypeVmTemplate: 'Template',
homeSort: 'Sort',
homeAllPools: 'Pools',
homeAllHosts: 'Hosts',
homeAllTags: 'Tags',
homeAllResourceSets: 'Resource sets',
homeNewVm: 'New VM',
homeFilterNone: 'None',
homeFilterRunningHosts: 'Running hosts',
@@ -142,16 +140,15 @@ var messages = {
homeFilterHvmGuests: 'HVM guests',
homeFilterTags: 'Tags',
homeSortBy: 'Sort by',
homeSortByCpus: 'CPUs',
homeSortByName: 'Name',
homeSortByPowerstate: 'Power state',
homeSortByRAM: 'RAM',
homeSortByvCPUs: 'vCPUs',
homeSortByCpus: 'CPUs',
homeSortByShared: 'Shared/Not shared',
homeSortBySize: 'Size',
homeSortByType: 'Type',
homeSortByUsage: 'Usage',
homeSortByvCPUs: 'vCPUs',
homeSortVmsBySnapshots: 'Snapshots',
homeSortByType: 'Type',
homeDisplayedItems: '{displayed, number}x {icon} (on {total, number})',
homeSelectedItems: '{selected, number}x {icon} selected (on {total, number})',
homeMore: 'More',
@@ -163,14 +160,6 @@ var messages = {
srSharedType: 'Shared {type}',
srNotSharedType: 'Not shared {type}',
// ----- Common components -----
sortedTableAllItemsSelected: 'All of them are selected',
sortedTableNoItems: 'No items found',
sortedTableNumberOfFilteredItems: '{nFiltered, number} of {nTotal, number} items',
sortedTableNumberOfItems: '{nTotal, number} items',
sortedTableNumberOfSelectedItems: '{nSelected, number} selected',
sortedTableSelectAllItems: 'Click here to select all items',
// ----- Forms -----
add: 'Add',
selectAll: 'Select all',
@@ -201,7 +190,6 @@ var messages = {
selectTimezone: 'Select timezone…',
selectIp: 'Select IP(s)…',
selectIpPool: 'Select IP pool(s)…',
selectVgpuType: 'Select VGPU type(s)…',
fillRequiredInformations: 'Fill required informations.',
fillOptionalInformations: 'Fill informations (optional)',
selectTableReset: 'Reset',
@@ -233,12 +221,6 @@ var messages = {
cronPattern: 'Cron Pattern:',
backupEditNotFoundTitle: 'Cannot edit backup',
backupEditNotFoundMessage: 'Missing required info for edition',
successfulJobCall: 'Successful',
failedJobCall: 'Failed',
jobCallInProgess: 'In progress',
jobTransferredDataSize: 'size:',
jobTransferredDataSpeed: 'speed:',
allJobCalls: 'All',
job: 'Job',
jobModalTitle: 'Job {job}',
jobId: 'ID',
@@ -271,7 +253,7 @@ var messages = {
noJobs: 'No jobs found.',
noSchedules: 'No schedules found',
jobActionPlaceHolder: 'Select a xo-server API command',
jobTimeoutPlaceHolder: 'Timeout (number of seconds after which a VM is considered failed)',
jobTimeoutPlaceHolder: ' Timeout (number of seconds after which a VM is considered failed)',
jobSchedules: 'Schedules',
jobScheduleNamePlaceHolder: 'Name of your schedule',
jobScheduleJobPlaceHolder: 'Select a Job',
@@ -298,7 +280,7 @@ var messages = {
editBackupTagTitle: 'Tag',
editBackupReportTitle: 'Report',
editBackupScheduleEnabled: 'Automatically run as scheduled',
editBackupRetentionTitle: 'Retention',
editBackupDepthTitle: 'Depth',
editBackupRemoteTitle: 'Remote',
deleteOldBackupsFirst: 'Delete the old backups first',
@@ -309,7 +291,6 @@ var messages = {
remoteTypeNfs: 'NFS',
remoteTypeSmb: 'SMB',
remoteType: 'Type',
remoteSmbWarningMessage: 'SMB remotes are meant to work on Windows Server. For other systems (Linux Samba, which means almost all NAS), please use NFS.',
remoteTestTip: 'Test your remote',
testRemote: 'Test Remote',
remoteTestFailure: 'Test failed for {name}',
@@ -465,13 +446,7 @@ var messages = {
convertVmToTemplateLabel: 'Convert to template',
vmConsoleLabel: 'Console',
// ----- SR advanced tab -----
srUnhealthyVdiNameLabel: 'Name',
srUnhealthyVdiSize: 'Size',
srUnhealthyVdiDepth: 'Depth',
srUnhealthyVdiTitle: 'VDI to coalesce ({total, number})',
// ----- SR tabs -----
// ----- SR actions -----
srRescan: 'Rescan all disks',
srReconnectAll: 'Connect to all hosts',
@@ -495,8 +470,6 @@ var messages = {
poolHaStatus: 'High Availability',
poolHaEnabled: 'Enabled',
poolHaDisabled: 'Disabled',
setpoolMaster: 'Master',
poolGpuGroups: 'GPU groups',
// ----- Pool host tab -----
hostNameLabel: 'Name',
hostDescription: 'Description',
@@ -562,11 +535,10 @@ var messages = {
hostStartedSince: 'Host uptime',
hostStackStartedSince: 'Toolstack uptime',
hostCpusModel: 'CPU model',
hostGpus: 'GPUs',
hostCpusNumber: 'Core (socket)',
hostManufacturerinfo: 'Manufacturer info',
hostBiosinfo: 'BIOS info',
licenseHostSettingsLabel: 'License',
licenseHostSettingsLabel: 'Licence',
hostLicenseType: 'Type',
hostLicenseSocket: 'Socket',
hostLicenseExpiry: 'Expiry',
@@ -575,7 +547,7 @@ var messages = {
supplementalPackPoolNew: 'Install supplemental pack on every host',
supplementalPackTitle: '{name} (by {author})',
supplementalPackInstallStartedTitle: 'Installation started',
supplementalPackInstallStartedMessage: 'Installing new supplemental pack',
supplementalPackInstallStartedMessage: 'Installing new supplemental pack...',
supplementalPackInstallErrorTitle: 'Installation error',
supplementalPackInstallErrorMessage: 'The installation of the supplemental pack failed.',
supplementalPackInstallSuccessTitle: 'Installation success',
@@ -627,7 +599,7 @@ var messages = {
patchStatus: 'Status',
patchStatusApplied: 'Applied',
patchStatusNotApplied: 'Missing patches',
patchNothing: 'No patches detected',
patchNothing: 'No patch detected',
patchReleaseDate: 'Release date',
patchGuidance: 'Guidance',
patchAction: 'Action',
@@ -655,6 +627,7 @@ var messages = {
advancedTabName: 'Advanced',
networkTabName: 'Network',
disksTabName: 'Disk{disks, plural, one {} other {s}}',
managementTabName: 'Management',
powerStateHalted: 'halted',
powerStateRunning: 'running',
@@ -692,6 +665,7 @@ var messages = {
copyToClipboardLabel: 'Copy',
ctrlAltDelButtonLabel: 'Ctrl+Alt+Del',
tipLabel: 'Tip:',
tipConsoleLabel: 'Due to a XenServer issue, non-US keyboard layouts aren\'t well supported. Switch your own layout to US to workaround it.',
hideHeaderTooltip: 'Hide infos',
showHeaderTooltip: 'Show infos',
@@ -746,10 +720,6 @@ var messages = {
cdDriveInstallation: 'Stop and start the VM to install the CD drive',
saveBootOption: 'Save',
resetBootOption: 'Reset',
deleteSelectedVdis: 'Delete selected VDIs',
deleteSelectedVdi: 'Delete selected VDI',
useQuotaWarning: 'Creating this disk will use the disk space quota from the resource set {resourceSet} ({spaceLeft} left)',
notEnoughSpaceInResourceSet: 'Not enough space in resource set {resourceSet} ({spaceLeft} left)',
// ----- VM network tab -----
vifCreateDeviceButton: 'New device',
@@ -785,7 +755,6 @@ var messages = {
exportSnapshot: 'Export this snapshot',
snapshotDate: 'Creation date',
snapshotName: 'Name',
snapshotDescription: 'Description',
snapshotAction: 'Action',
snapshotQuiesce: 'Quiesced snapshot',
@@ -837,11 +806,6 @@ var messages = {
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 -----
@@ -920,6 +884,7 @@ var messages = {
// ----- New VM -----
newVmCreateNewVmOn: 'Create a new VM on {select}',
newVmCreateNewVmOn2: 'Create a new VM on {select1} or {select2}',
newVmCreateNewVmNoPermission: 'You have no permission to create a VM',
newVmInfoPanel: 'Infos',
newVmNameLabel: 'Name',
@@ -973,6 +938,32 @@ var messages = {
newVmHideAdvanced: 'Hide advanced settings',
newVmShare: 'Share this VM',
// ----- VM-Group-----
newVmGroupTitle: 'Create a new Vm-Group on ',
newVmGroupNameLabel: 'Label',
newVmGroupDescriptionLabel: 'Description',
newVmGroupReset: 'Reset',
newVmGroupCreate: 'Create',
newVmGroupInfoPanel: 'Infos',
// ----- VM-Group item -----
powerStateVmGroupRunning: 'All vms are running',
powerStateVmGroupHalted: 'All vms are halted',
powerStateVmGroupBusy: 'Contains vms halted and vms running',
// ----- VM-Group management Tab -----
'attachVmButton': 'new VM',
'vmsBootOrder': 'Boot order',
'vmGroupLabel': 'Label',
'vmGroupDescription': 'Description',
'vmGroupActions': 'Actions',
// ----- VM-Group general Tab -----
'vmGroupCurrentStatus': 'Current status',
// ----- VM-Group stats Tab -----
'vmGroupAllVm': 'All VMs',
// ----- Self -----
resourceSets: 'Resource sets',
noResourceSets: 'No resource sets.',
@@ -1050,10 +1041,6 @@ var messages = {
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',
@@ -1117,26 +1104,22 @@ var messages = {
migrateVmModalTitle: 'Migrate VM',
migrateVmSelectHost: 'Select a destination host:',
migrateVmSelectMigrationNetwork: 'Select a migration network:',
migrateVmSelectSrs: 'For each VDI, select an SR:',
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',
migrateVmName: 'Name',
migrateVmSr: 'SR',
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',
deleteVmGroupModalTitle: 'Delete VM-Group',
deleteVmGroupModalMessage: 'Are you sure you want to delete this VMGroup ?',
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',
revertVmModalTitle: 'Revert your VM',
deleteSnapshotModalTitle: 'Delete snapshot',
deleteSnapshotModalMessage: 'Are you sure you want to delete this snapshot?',
@@ -1178,7 +1161,7 @@ var messages = {
serverAddFailed: 'Adding server failed',
serverStatus: 'Status',
serverConnectionFailed: 'Connection failed. Click for more information.',
serverConnecting: 'Connecting',
serverConnecting: 'Connecting...',
serverConnected: 'Connected',
serverDisconnected: 'Disconnected',
serverAuthFailed: 'Authentication error',
@@ -1203,16 +1186,6 @@ var messages = {
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',
@@ -1278,12 +1251,11 @@ var messages = {
proxySettingsPasswordPlaceHolder: 'Password',
updateRegistrationEmailPlaceHolder: 'Your email account',
updateRegistrationPasswordPlaceHolder: 'Your password',
updaterTroubleshootingLink: 'Troubleshooting documentation',
update: 'Update',
refresh: 'Refresh',
upgrade: 'Upgrade',
noUpdaterCommunity: 'No updater available for Community Edition',
considerSubscribe: 'Please consider subscribing and trying it with all the 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}.',
noUpdaterWarning: 'Manual update could break your current installation due to dependencies issues, do it with caution',
currentVersion: 'Current version:',
register: 'Register',
@@ -1385,25 +1357,19 @@ var messages = {
// ----- 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_GO_TO_HOSTS: 'Go to hosts list',
shortcut_GO_TO_POOLS: 'Go to pools list',
shortcut_GO_TO_VMS: 'Go to VMs list',
shortcut_GO_TO_SRS: 'Go to SRs list',
shortcut_CREATE_VM: 'Create a new VM',
shortcut_UNFOCUS: 'Unfocus field',
shortcut_HELP: 'Show shortcuts key bindings',
shortcut_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',
shortcut_SEARCH: 'Focus search bar',
shortcut_NAV_DOWN: 'Next item',
shortcut_NAV_UP: 'Previous item',
shortcut_SELECT: 'Select item',
shortcut_JUMP_INTO: 'Open',
// ----- Settings/ACLs -----
settingsAclsButtonTooltipVM: 'VM',
@@ -1445,8 +1411,7 @@ var messages = {
xosanSuggestions: 'Suggestions',
xosanName: 'Name',
xosanHost: 'Host',
xosanHosts: 'Connected Hosts',
xosanPool: 'Pool',
xosanHosts: 'Hosts',
xosanVolumeId: 'Volume ID',
xosanSize: 'Size',
xosanUsedSpace: 'Used space',
@@ -1463,78 +1428,20 @@ var messages = {
xosanAvailableSpace: 'Available space',
xosanDiskLossLegend: '* Can fail without data loss',
xosanCreate: 'Create',
xosanAdd: 'Add',
xosanInstalling: 'Installing XOSAN. Please wait…',
xosanInstalling: 'Installing XOSAN. Please wait...',
xosanCommunity: 'No XOSAN available for Community Edition',
xosanNew: 'New',
xosanAdvanced: 'Advanced',
xosanRemoveSubvolumes: 'Remove subvolumes',
xosanAddSubvolume: 'Add subvolume…',
xosanWarning: 'This version of XOSAN SR is from the first beta phase. You can keep using it, but to modify it you\'ll have to save your disks and re-create it.',
xosanVlan: 'VLAN',
xosanNoSrs: 'No XOSAN found',
xosanPbdsDetached: 'Some SRs are detached from the XOSAN',
xosanBadStatus: 'Something is wrong with: {badStatuses}',
xosanRunning: 'Running',
xosanDelete: 'Delete XOSAN',
xosanFixIssue: 'Fix',
xosanCreatingOn: 'Creating XOSAN on {pool}',
xosanState_configuringNetwork: 'Configuring network…',
xosanState_importingVm: 'Importing VM…',
xosanState_copyingVms: 'Copying VMs…',
xosanState_configuringVms: 'Configuring VMs…',
xosanState_configuringGluster: 'Configuring gluster…',
xosanState_creatingSr: 'Creating SR…',
xosanState_scanningSr: 'Scanning SR…',
// Pack download modal
xosanInstallCloudPlugin: 'Install cloud plugin first',
xosanLoadCloudPlugin: 'Load cloud plugin first',
xosanLoading: 'Loading',
xosanLoading: 'Loading...',
xosanNotAvailable: 'XOSAN is not available at the moment',
xosanRegisterBeta: 'Register for the XOSAN beta',
xosanSuccessfullyRegistered: 'You have successfully registered for the XOSAN beta. Please wait until your request has been approved.',
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.'
xosanPackRequirements: 'At least one of these version requirements must be satisfied by all the hosts in this pool:'
}
forEach(messages, function (message, id) {
if (isString(message)) {

View File

@@ -19,7 +19,6 @@ const _IGNORED_TAGNAMES = {
}
@propTypes({
className: propTypes.string,
tagName: propTypes.string
})
export class BlockLink extends Component {
@@ -45,22 +44,11 @@ export class BlockLink extends Component {
}
}
_addAuxClickListener = ref => {
// FIXME: when https://github.com/facebook/react/issues/8529 is fixed,
// remove and use onAuxClickCapture.
// In Chrome ^55, middle-clicking triggers auxclick event instead of click
if (ref !== null) {
ref.addEventListener('auxclick', this._onClickCapture)
}
}
render () {
const { children, tagName = 'div', className } = this.props
const { children, tagName = 'div' } = this.props
const Component = tagName
return (
<Component
className={className}
ref={this._addAuxClickListener}
style={this._style}
onClickCapture={this._onClickCapture}
>

View File

@@ -29,7 +29,7 @@ const modal = (content, onClose) => {
buttons: propTypes.arrayOf(propTypes.shape({
btnStyle: propTypes.string,
icon: propTypes.string,
label: propTypes.node.isRequired,
label: propTypes.string.isRequired,
tooltip: propTypes.node,
value: propTypes.any
})).isRequired,
@@ -58,6 +58,8 @@ class GenericModal extends Component {
}
render () {
const { Body, Footer, Header, Title } = ReactModal
const {
buttons,
icon,
@@ -67,33 +69,34 @@ class GenericModal extends Component {
const body = _addRef(this.props.children, 'body')
return <div>
<ReactModal.Header closeButton>
<ReactModal.Title>
<Header closeButton>
<Title>
{icon
? <span><Icon icon={icon} /> {title}</span>
: title
}
</ReactModal.Title>
</ReactModal.Header>
<ReactModal.Body>
</Title>
</Header>
<Body>
{body}
</ReactModal.Body>
<ReactModal.Footer>
</Body>
<Footer>
{map(buttons, ({
label,
tooltip,
value,
icon,
...props
}, key) => {
}) => {
const button = <Button
onClick={() => this._resolve(value)}
key={value}
{...props}
>
{icon !== undefined && <Icon icon={icon} fixedWidth />}
{label}
</Button>
return <span key={key}>
return <span>
{tooltip !== undefined
? <Tooltip content={tooltip}>{button}</Tooltip>
: button
@@ -106,7 +109,7 @@ class GenericModal extends Component {
{_('genericCancel')}
</Button>
}
</ReactModal.Footer>
</Footer>
</div>
}
}
@@ -206,8 +209,14 @@ export default class Modal extends Component {
}
render () {
const { showModal } = this.state
/* TODO: remove this work-around and use
* ReactModal.Body, ReactModal.Header, ...
* after this issue has been fixed:
* https://phabricator.babeljs.io/T6976
*/
return (
<ReactModal show={this.state.showModal} onHide={this._onHide}>
<ReactModal show={showModal} onHide={this._onHide}>
{this.state.content}
</ReactModal>
)

View File

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

View File

@@ -1,7 +1,8 @@
import React, { Component } from 'react'
import RFB from '@nraynaud/novnc/lib/rfb'
import { createBackoff } from 'jsonrpc-websocket-client'
import { RFB } from 'novnc-node'
import {
format as formatUrl,
parse as parseUrl,
resolve as resolveUrl
} from 'url'
@@ -41,7 +42,7 @@ export default class NoVnc extends Component {
}
}
if (state !== 'disconnected' || this.refs.canvas == null) {
if (state !== 'disconnected') {
return
}
@@ -90,23 +91,14 @@ export default class NoVnc extends Component {
const rfb = this._rfb = new RFB({
encrypt: isSecure,
target: this.refs.canvas,
wsProtocols: [ 'chat' ],
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.path.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)
rfb.connect(formatUrl(url))
disableShortcuts()
}

View File

@@ -1,8 +1,5 @@
import _ from 'intl'
import React from 'react'
import {
startsWith
} from 'lodash'
import Icon from './icon'
import propTypes from './prop-types-decorator'
@@ -22,7 +19,7 @@ const OBJECT_TYPE_TO_ICON = {
}
// Host, Network, VM-template.
const PoolObjectItem = propTypes({
export const PoolObjectItem = propTypes({
object: propTypes.object.isRequired
})(connectStore(() => {
const getPool = createGetObject(
@@ -45,7 +42,7 @@ const PoolObjectItem = propTypes({
}))
// SR.
const SrItem = propTypes({
export const SrItem = propTypes({
sr: propTypes.object.isRequired
})(connectStore(() => {
const getContainer = createGetObject(
@@ -70,7 +67,7 @@ const SrItem = propTypes({
}))
// VM.
const VmItem = propTypes({
export const VmItem = propTypes({
vm: propTypes.object.isRequired
})(connectStore(() => {
const getContainer = createGetObject(
@@ -87,16 +84,6 @@ const VmItem = propTypes({
</span>
)))
const VgpuItem = connectStore(() => ({
vgpuType: createGetObject(
(_, props) => props.vgpu.vgpuType
)
}))(({ vgpu, vgpuType }) => (
<span>
<Icon icon='vgpu' /> {vgpuType.modelName}
</span>
))
// ===================================================================
const xoItemToRender = {
@@ -178,7 +165,7 @@ const xoItemToRender = {
// PIF.
PIF: pif => (
<span>
<Icon icon='network' color={pif.carrier ? 'text-success' : 'text-danger'} /> {pif.device} ({pif.deviceName})
<Icon icon='network' /> {pif.device} ({pif.deviceName})
</span>
),
@@ -187,25 +174,6 @@ const xoItemToRender = {
<span>
<Icon icon='tag' /> {tag.value}
</span>
),
// GPUs
vgpu: vgpu => <VgpuItem vgpu={vgpu} />,
vgpuType: type => (
<span>
<Icon icon='gpu' /> {type.modelName} ({type.vendorName}) {type.maxResolutionX}x{type.maxResolutionY}
</span>
),
gpuGroup: group => (
<span>
{startsWith(group.name_label, 'Group of ')
? group.name_label.slice(9)
: group.name_label
}
</span>
)
}

View File

@@ -75,11 +75,11 @@ const WEEK_DAYS = [
const HOURS = (() => {
const hours = []
for (let i = 0; i < 4; i++) {
for (let i = 0; i < 3; i++) {
hours[i] = []
for (let j = 0; j < 6; j++) {
hours[i].push(6 * i + j)
for (let j = 0; j < 8; j++) {
hours[i].push(8 * i + j)
}
}
@@ -147,13 +147,6 @@ export class SchedulePreview extends Component {
const { value } = this.state
const cronSched = later.parse.cron(cronPattern)
// Due to implementation, the range used for months is 0-11
// instead of 1-12
forEach(cronSched.schedules[0].M, (v, i, a) => {
a[i] = v + 1
})
const dates = later.schedule(cronSched).next(value)
return (
@@ -194,14 +187,7 @@ class ToggleTd extends Component {
render () {
const { props } = this
return (
<td
className={classNames(
'text-xs-center',
props.value && 'table-success'
)}
onClick={this._onClick}
style={CLICKABLE}
>
<td style={CLICKABLE} className={props.value ? 'table-success' : ''} onClick={this._onClick}>
{props.children}
</td>
)
@@ -518,7 +504,7 @@ export default class Scheduler extends Component {
return (
<div className='card-block'>
<Row>
<Col largeSize={6}>
<Col mediumSize={6}>
<TimePicker
labelId='Month'
optionRenderer={getMonthName}
@@ -527,17 +513,13 @@ export default class Scheduler extends Component {
range={MONTHS_RANGE}
value={cronPatternArr[PICKTIME_TO_ID['month']]}
/>
</Col>
<Col largeSize={6}>
<DayPicker
onChange={this._dayChange}
monthDayPattern={cronPatternArr[PICKTIME_TO_ID['monthDay']]}
weekDayPattern={cronPatternArr[PICKTIME_TO_ID['weekDay']]}
/>
</Col>
</Row>
<Row>
<Col largeSize={6}>
<Col mediumSize={6}>
<TimePicker
labelId='Hour'
options={HOURS}
@@ -545,8 +527,6 @@ export default class Scheduler extends Component {
onChange={this._hourChange}
value={cronPatternArr[PICKTIME_TO_ID['hour']]}
/>
</Col>
<Col largeSize={6}>
<TimePicker
labelId='Minute'
options={MINS}

View File

@@ -549,41 +549,6 @@ export const SelectVdi = propTypes({
}
}, { placeholder: _('selectVdis') }))
// ===================================================================
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.
// ===================================================================

View File

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

View File

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

View File

@@ -1,14 +1,10 @@
import React from 'react'
import styled from 'styled-components'
import {
omit
} from 'lodash'
import ActionButton from './action-button'
import propTypes from './prop-types-decorator'
// do not forward `state` to ActionButton
const Button = styled(p => <ActionButton {...omit(p, 'state')} />)`
const Button = styled(ActionButton)`
background-color: ${p => p.theme[`${p.state ? 'enabled' : 'disabled'}StateBg`]};
border: 2px solid ${p => p.theme[`${p.state ? 'enabled' : 'disabled'}StateColor`]};
color: ${p => p.theme[`${p.state ? 'enabled' : 'disabled'}StateColor`]};

View File

@@ -18,9 +18,7 @@ const TabButton = ({
{...props}
size='large'
style={STYLE}
>
{labelId !== undefined && <span className='hidden-md-down'>{_(labelId)}</span>}
</ActionButton>
><span className='hidden-md-down'>{_(labelId)}</span></ActionButton>
)
export { TabButton as default }

View File

@@ -1,26 +1,22 @@
import escapeRegExp from 'lodash/escapeRegExp'
import every from 'lodash/every'
import forEach from 'lodash/forEach'
import getStream from 'get-stream'
import humanFormat from 'human-format'
import isArray from 'lodash/isArray'
import isEmpty from 'lodash/isEmpty'
import isFunction from 'lodash/isFunction'
import isPlainObject from 'lodash/isPlainObject'
import isString from 'lodash/isString'
import join from 'lodash/join'
import keys from 'lodash/keys'
import map from 'lodash/map'
import mapValues from 'lodash/mapValues'
import React from 'react'
import ReadableStream from 'readable-stream'
import replace from 'lodash/replace'
import startsWith from 'lodash/startsWith'
import { connect } from 'react-redux'
import {
clone,
escapeRegExp,
every,
forEach,
isArray,
isEmpty,
isFunction,
isPlainObject,
isString,
join,
keys,
map,
mapValues,
replace,
sample,
startsWith
} from 'lodash'
import _ from './intl'
import * as actions from './store/actions'
@@ -67,12 +63,8 @@ export const addSubscriptions = subscriptions => Component => {
}
componentWillMount () {
this._unsubscribes = map(
isFunction(subscriptions)
? subscriptions(this.props)
: subscriptions,
(subscribe, prop) =>
subscribe(value => this._setState({ [prop]: value }))
this._unsubscribes = map(isFunction(subscriptions) ? subscriptions() : subscriptions, (subscribe, prop) =>
subscribe(value => this._setState({ [prop]: value }))
)
}
@@ -198,6 +190,19 @@ export { default as Debug } from './debug'
// -------------------------------------------------------------------
// Returns the first defined (non-undefined) value.
export const firstDefined = function () {
const n = arguments.length
for (let i = 0; i < n; ++i) {
const arg = arguments[i]
if (arg !== undefined) {
return arg
}
}
}
// -------------------------------------------------------------------
// Returns the current XOA Plan or the Plan name if number given
export const getXoaPlan = plan => {
switch (plan || +process.env.XOA_PLAN) {
@@ -261,15 +266,8 @@ export const osFamily = invoke({
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') {
@@ -543,43 +541,3 @@ export const getCoresPerSocketPossibilities = (maxCoresPerSocket, vCPUs) => {
return options
}
// Generates a random human-readable string of length `length`
// Useful to generate random default names intended for the UI user
export const generateReadableRandomString = (() => {
const CONSONANTS = 'bdfgklmnprtvz'.split('')
const VOWELS = 'aeiou'.split('')
return (length = 8) => {
const result = new Array(length)
for (let i = 0; i < length; ++i) {
result[i] = sample((i & 1) === 0 ? VOWELS : CONSONANTS)
}
return result.join('')
}
})()
export const cowSet = (object, path, value, depth = 0) => {
if (depth >= path.length) {
return value
}
object = object != null ? clone(object) : {}
const prop = path[depth]
object[prop] = cowSet(object[prop], path, value, depth + 1)
return object
}
// Generates a function that returns a value between 0 and 1
// This function returns an estimated progress value between 0 and 1
// based on the elapsed time since the createFakeProgress call and
// the given estimated duration d
export const createFakeProgress = (() => {
const S = 0.95 // Progress value after d seconds
return d => {
const startTime = Date.now() / 1e3
return () => {
const x = Date.now() / 1e3 - startTime
return -Math.exp((x * Math.log(1 - S)) / d) + 1
}
}
})()

View File

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

View File

@@ -8,6 +8,7 @@ import {
find,
flatten,
floor,
forEach,
map,
max,
size,
@@ -16,7 +17,7 @@ import {
} from 'lodash'
import propTypes from '../prop-types-decorator'
import { computeArraysSum } from '../xo-stats'
import { computeArraysSum, computeArraysAvg } from '../xo-stats'
import { formatSize } from '../utils'
import styles from './index.css'
@@ -214,6 +215,54 @@ export const PoolCpuLineChart = injectIntl(propTypes({
)
}))
export const VmGroupCpuLineChart = injectIntl(propTypes({
addSumSeries: propTypes.bool,
data: propTypes.object.isRequired,
options: propTypes.object
})(({ addSumSeries, data, options = {}, intl }) => {
const firstVmData = data[0]
const length = getStatsLength(firstVmData.stats.cpus)
if (!length) {
return templateError
}
const series = map(data, ({ vm, stats }) => ({
name: vm,
data: computeArraysSum(stats.cpus)
}))
if (addSumSeries) {
series.push({
name: intl.formatMessage(messages.vmGroupAllVm),
data: computeArraysSum(map(series, 'data')),
className: styles.dashedLine
})
}
const nbCpusByVm = map(data, ({ stats }) => stats.cpus.length)
return (
<ChartistGraph
type='Line'
data={{
series
}}
options={{
...makeOptions({
intl,
nValues: length,
endTimestamp: firstVmData.endTimestamp,
interval: firstVmData.interval,
valueTransform: value => `${floor(value)}%`
}),
high: 100 * (addSumSeries ? sum(nbCpusByVm) : max(nbCpusByVm)),
...options
}}
/>
)
}))
export const MemoryLineChart = injectIntl(propTypes({
data: propTypes.object.isRequired,
options: propTypes.object
@@ -302,6 +351,57 @@ export const PoolMemoryLineChart = injectIntl(propTypes({
)
}))
export const VmGroupMemoryLineChart = injectIntl(propTypes({
addSumSeries: propTypes.bool,
data: propTypes.object.isRequired,
options: propTypes.object
})(({ addSumSeries, data, options = {}, intl }) => {
const firstVmData = data[0]
const {
memory,
memoryUsed
} = firstVmData.stats
if (!memory || !memoryUsed) {
return templateError
}
const series = map(data, ({ vm, stats }) => ({
name: vm,
data: stats.memoryUsed
}))
if (addSumSeries) {
series.push({
name: intl.formatMessage(messages.vmGroupAllVm),
data: computeArraysSum(map(data, 'stats.memoryUsed')),
className: styles.dashedLine
})
}
const currentMemoryByHost = map(data, ({ stats }) => stats.memory[stats.memory.length - 1])
return (
<ChartistGraph
type='Line'
data={{
series
}}
options={{
...makeOptions({
intl,
nValues: firstVmData.stats.memoryUsed.length,
endTimestamp: firstVmData.endTimestamp,
interval: firstVmData.interval,
valueTransform: formatSize
}),
high: addSumSeries ? sum(currentMemoryByHost) : max(currentMemoryByHost),
...options
}}
/>
)
}))
export const XvdLineChart = injectIntl(propTypes({
addSumSeries: propTypes.bool,
data: propTypes.object.isRequired,
@@ -334,6 +434,60 @@ export const XvdLineChart = injectIntl(propTypes({
)
}))
export const VmGroupXvdLineChart = injectIntl(propTypes({
addSumSeries: propTypes.bool,
data: propTypes.object.isRequired,
options: propTypes.object
})(({ addSumSeries, data, options = {}, intl }) => {
const firstVmData = data[0]
const {
memory,
memoryUsed
} = firstVmData.stats
if (!memory || !memoryUsed) {
return templateError
}
const series = flatten(map(data, ({ stats, vm }) =>
map(stats.xvds, (xvd, key) => {
return {
name: `${vm} (${key})`,
data: computeArraysAvg(stats.xvds[key])
}
})
))
const datas = []
forEach(series, ({ data }) => datas.push(data))
if (addSumSeries) {
series.push({
name: intl.formatMessage(messages.vmGroupAllVm),
data: computeArraysSum(datas),
className: styles.dashedLine
})
}
return (
<ChartistGraph
type='Line'
data={{
series
}}
options={{
...makeOptions({
intl,
nValues: firstVmData.stats.xvds.r.length,
endTimestamp: data.endTimestamp,
interval: data.interval,
valueTransform: formatSize
}),
...options
}}
/>
)
}))
export const VifLineChart = injectIntl(propTypes({
addSumSeries: propTypes.bool,
data: propTypes.object.isRequired,
@@ -443,6 +597,48 @@ export const PoolPifLineChart = injectIntl(propTypes({
)
}))
export const VmGroupVifLineChart = injectIntl(propTypes({
addSumSeries: propTypes.bool,
data: propTypes.object.isRequired,
options: propTypes.object
})(({ addSumSeries, data, options = {}, intl }) => {
const firstVmData = data[0]
const length = firstVmData.stats && getStatsLength(firstVmData.stats.vifs.rx)
if (!length) {
return templateError
}
const series = addSumSeries
? map(ios, io => ({
name: `${intl.formatMessage(messages.vmGroupAllVm)} (${io})`,
data: computeArraysSum(map(data, ({ stats }) => computeArraysSum(stats.vifs[io])))
}))
: flatten(map(data, ({ stats, vm }) =>
map(ios, io => ({
name: `${vm} (${io})`,
data: computeArraysSum(stats.vifs[io])
}))
))
return (
<ChartistGraph
type='Line'
data={{
series
}}
options={{
...makeOptions({
intl,
nValues: length,
endTimestamp: firstVmData.endTimestamp,
interval: firstVmData.interval,
valueTransform: formatSize
}),
...options
}}
/>
)
}))
export const LoadLineChart = injectIntl(propTypes({
data: propTypes.object.isRequired,
options: propTypes.object

View File

@@ -1,139 +0,0 @@
import Collapse from 'collapse'
import Component from 'base-component'
import React from 'react'
import { every, forEach, map } from 'lodash'
import _ from '../../intl'
import propTypes from '../../prop-types-decorator'
import SingleLineRow from '../../single-line-row'
import { createSelector } from '../../selectors'
import { SelectSr } from '../../select-objects'
import { isSrWritable } from 'xo'
import {
Container,
Col
} from 'grid'
// 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 Collapsible = ({collapsible, children, ...props}) => collapsible
? <Collapse {...props}>{children}</Collapse>
: <div>
<span>{props.buttonText}</span>
<br />
{children}
</div>
Collapsible.propTypes = {
collapsible: propTypes.bool.isRequired,
children: propTypes.node.isRequired
}
@propTypes({
vdis: propTypes.array.isRequired,
predicate: propTypes.func
})
export default class ChooseSrForEachVdisModal extends Component {
state = {
mapVdisSrs: {}
}
componentWillReceiveProps (newProps) {
if (
this.props.predicate !== undefined &&
newProps.predicate !== this.props.predicate
) {
this.state = {
mainSr: undefined,
mapVdisSrs: {}
}
}
}
_onChange = props => {
this.setState(props)
this.props.onChange(props)
}
_onChangeMainSr = newSr => {
const oldSr = this.state.mainSr
if (oldSr == null || newSr == null || oldSr.$pool !== newSr.$pool) {
this.setState({
mapVdisSrs: {}
})
} else if (!newSr.shared) {
const mapVdisSrs = {...this.state.mapVdisSrs}
forEach(mapVdisSrs, (sr, vdi) => {
if (sr != null && newSr !== sr && sr.$container !== newSr.$container && !sr.shared) {
delete mapVdisSrs[vdi]
}
})
this._onChange({mapVdisSrs})
}
this._onChange({
mainSr: newSr
})
}
_getSrPredicate = createSelector(
() => this.state.mainSr,
() => this.state.mapVdisSrs,
(mainSr, mapVdisSrs) => sr =>
isSrWritable(sr) &&
mainSr.$pool === sr.$pool &&
areSrsCompatible(mainSr, sr) &&
every(mapVdisSrs, selectedSr => selectedSr == null || areSrsCompatible(selectedSr, sr))
)
render () {
const { props, state } = this
const { vdis } = props
const {
mainSr,
mapVdisSrs
} = state
const srPredicate = props.predicate || this._getSrPredicate()
return <div>
<SelectSr
onChange={mainSr => props.predicate !== undefined
? this._onChange({mainSr})
: this._onChangeMainSr(mainSr)
}
predicate={props.predicate || isSrWritable}
placeholder={_('chooseSrForEachVdisModalMainSr')}
value={mainSr}
/>
<br />
{vdis != null && mainSr != null &&
<Collapsible collapsible={vdis.length >= 3} buttonText={_('chooseSrForEachVdisModalSelectSr')}>
<br />
<Container>
<SingleLineRow>
<Col size={6}><strong>{_('chooseSrForEachVdisModalVdiLabel')}</strong></Col>
<Col size={6}><strong>{_('chooseSrForEachVdisModalSrLabel')}</strong></Col>
</SingleLineRow>
{map(vdis, vdi =>
<SingleLineRow key={vdi.uuid}>
<Col size={6}>{ vdi.name_label || vdi.name }</Col>
<Col size={6}>
<SelectSr
onChange={sr => this._onChange({ mapVdisSrs: { ...mapVdisSrs, [vdi.uuid]: sr } })}
value={mapVdisSrs[vdi.uuid]}
predicate={srPredicate}
/>
</Col>
</SingleLineRow>
)}
<i>{_('chooseSrForEachVdisModalOptionalEntry')}</i>
</Container>
</Collapsible>
}
</div>
}
}

View File

@@ -48,7 +48,7 @@ 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 isSrShared = sr => sr && sr.$PBDs.length > 1
export const isVmRunning = vm => vm && vm.power_state === 'Running'
// ===================================================================
@@ -163,11 +163,6 @@ const createSubscription = cb => {
let running = false
const uninstall = () => {
clearTimeout(timeout)
cache = undefined
}
const loop = () => {
if (running) {
return
@@ -176,11 +171,6 @@ const createSubscription = cb => {
running = true
_signIn.then(() => cb()).then(result => {
running = false
if (n === 0) {
return uninstall()
}
timeout = setTimeout(loop, delay)
if (!isEqual(result, cache)) {
@@ -197,11 +187,6 @@ const createSubscription = cb => {
}
}, error => {
running = false
if (n === 0) {
return uninstall()
}
console.error(error)
})
}
@@ -210,10 +195,8 @@ const createSubscription = cb => {
const id = nextId++
subscribers[id] = cb
if (n++ !== 0) {
if (cache !== undefined) {
asap(() => cb(cache))
}
if (n++) {
cache !== undefined && asap(() => cb(cache))
} else {
loop()
}
@@ -221,8 +204,9 @@ const createSubscription = cb => {
return once(() => {
delete subscribers[id]
if (--n === 0) {
uninstall()
if (!--n) {
clearTimeout(timeout)
cache = undefined
}
})
}
@@ -291,15 +275,17 @@ export const subscribeIpPools = createSubscription(() => _call('ipPool.getAll'))
export const subscribeResourceCatalog = createSubscription(() => _call('cloud.getResourceCatalog'))
const checkSrCurrentStateSubscriptions = {}
export const subscribeCheckSrCurrentState = (pool, cb) => {
export const subscribeVmGroups = createSubscription(() => _call('vmGroup.get'))
const xosanSubscriptions = {}
export const subscribeIsInstallingXosan = (pool, cb) => {
const poolId = resolveId(pool)
if (!checkSrCurrentStateSubscriptions[poolId]) {
checkSrCurrentStateSubscriptions[poolId] = createSubscription(() => _call('xosan.checkSrCurrentState', { poolId }))
if (!xosanSubscriptions[poolId]) {
xosanSubscriptions[poolId] = createSubscription(() => _call('xosan.checkSrIsBusy', { poolId }))
}
return checkSrCurrentStateSubscriptions[poolId](cb)
return xosanSubscriptions[poolId](cb)
}
const missingPatchesByHost = {}
@@ -307,7 +293,7 @@ export const subscribeHostMissingPatches = (host, cb) => {
const hostId = resolveId(host)
if (missingPatchesByHost[hostId] == null) {
missingPatchesByHost[hostId] = createSubscription(() => getHostMissingPatches(host))
missingPatchesByHost[hostId] = createSubscription(() => _call('host.listMissingPatches', { host: hostId }))
}
return missingPatchesByHost[hostId](cb)
@@ -324,46 +310,6 @@ subscribeHostMissingPatches.forceRefresh = host => {
}
}
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 = unhealthyVdiChainsLengthSubscriptionsBySr[sr] = createSubscription(
() => _call('sr.getUnhealthyVdiChainsLength', { sr })
)
}
return subscription
}
// System ============================================================
export const apiMethods = _call('system.getMethodsInfo')
@@ -471,30 +417,10 @@ export const detachHost = host => (
)
)
export const forgetHost = host => (
confirm({
icon: 'host-forget',
title: _('forgetHostModalTitle'),
body: _('forgetHostModalMessage', { host: <strong>{host.name_label}</strong> })
}).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: <strong>{host.name_label}</strong> })
}).then(
() => _call('pool.setPoolMaster', { host: resolveId(host) }),
noop
)
)
// Host --------------------------------------------------------------
export const editHost = (host, props) => (
@@ -588,12 +514,7 @@ export const disableHost = 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 ])
)
_call('host.listMissingPatches', { host: resolveId(host) })
)
export const emergencyShutdownHost = host => (
@@ -844,13 +765,13 @@ export const cloneVm = ({ id, name_label: nameLabel }, fullCopy = false) => (
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({
if (sr) {
return 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({
}).then(() => _call('vm.copy', { vm: vm.id, sr: sr.id, name: name || vm.name_label + '_COPY', compress }))
} else {
return confirm({
title: _('copyVm'),
body: <CopyVmModalBody vm={vm} />
}).then(
@@ -859,10 +780,11 @@ export const copyVm = (vm, sr, name, compress) => {
error('copyVmsNoTargetSr', 'copyVmsNoTargetSrMessage')
return
}
return _call('vm.copy', { vm: vmId, ...params })
_call('vm.copy', { vm: vm.id, ...params })
},
noop
)
}
}
import CopyVmsModalBody from './copy-vms-modal' // eslint-disable-line import/first
@@ -872,17 +794,19 @@ export const copyVms = vms => {
title: _('copyVm'),
body: <CopyVmsModalBody vms={_vms} />
}).then(
({
compress,
names,
sr
}) => {
if (sr !== undefined) {
return Promise.all(map(_vms, (vm, index) =>
_call('vm.copy', { vm, sr, compress, name: names[index] })
))
params => {
if (!params.sr) {
error(_('copyVmsNoTargetSr'), _('copyVmsNoTargetSrMessage'))
return
}
error(_('copyVmsNoTargetSr'), _('copyVmsNoTargetSrMessage'))
const {
compress,
names,
sr
} = params
Promise.all(map(_vms, (vm, index) =>
_call('vm.copy', { vm, sr, compress, name: names[index] })
))
},
noop
)
@@ -1030,13 +954,8 @@ 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)
}))
export const importDeltaBackup = ({ remote, file, sr }) => (
_call('vm.importDeltaBackup', resolveIds({ remote, filePath: file, sr }))
)
import RevertSnapshotModalBody from './revert-snapshot-modal' // eslint-disable-line import/first
@@ -1118,23 +1037,17 @@ export const attachDiskToVm = (vdi, vm, { bootable, mode, position }) => (
})
)
export const createVgpu = (vm, { gpuGroup, vgpuType }) =>
_call('vm.createVgpu', resolveIds({ vm, gpuGroup, vgpuType }))
export const deleteVgpu = vgpu =>
_call('vm.deleteVgpu', resolveIds({ vgpu }))
export const removeAppliance = vm => {
_call('vm.removeAppliance', { id: resolveId(vm) })
}
// DISK ---------------------------------------------------------------
export const createDisk = (name, size, sr, { vm, bootable, mode, position }) => (
export const createDisk = (name, size, sr) => (
_call('disk.create', {
bootable,
mode,
name,
position,
size,
sr: resolveId(sr),
vm: resolveId(vm)
sr: resolveId(sr)
})
)
@@ -1154,16 +1067,6 @@ export const deleteVdi = vdi => (
)
)
export const deleteVdis = vdis => (
confirm({
title: _('deleteVdisModalTitle', { nVdis: vdis.length }),
body: _('deleteVdisModalMessage', { nVdis: vdis.length })
}).then(
() => Promise.all(map(vdis, id => _call('vdi.delete', { id }))),
noop
)
)
export const deleteOrphanedVdis = vdis => (
confirm({
title: _('removeAllOrphanedObject'),
@@ -1430,8 +1333,8 @@ export const deletePbd = pbd => (
// Messages ----------------------------------------------------------
export const deleteMessage = message => (
_call('message.delete', { id: resolveId(message) })
export const deleteMessage = pbd => (
_call('message.delete', { id: resolveId(pbd) })
)
// Tags --------------------------------------------------------------
@@ -2030,28 +1933,18 @@ export const setIpPool = (ipPool, { name, addresses, networks }) => (
// XO SAN ----------------------------------------------------------------------
export const getVolumeInfo = (xosanSr, infoType) => _call('xosan.getVolumeInfo', { sr: xosanSr, infoType })
export const getVolumeInfo = (xosanSr) => _call('xosan.getVolumeInfo', { sr: xosanSr })
export const createXosanSR = ({ template, pif, vlan, srs, glusterType, redundancy, brickSize, memorySize, ipRange }) => _call('xosan.createSR', {
export const createXosanSR = ({ template, pif, vlan, srs, glusterType, redundancy }) => _call('xosan.createSR', {
template,
pif: pif.id,
vlan: String(vlan),
srs: resolveIds(srs),
glusterType,
redundancy: Number.parseInt(redundancy),
brickSize,
memorySize,
ipRange
redundancy: Number.parseInt(redundancy)
})
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 })
export const computeXosanPossibleOptions = lvmSrs => _call('xosan.computeXosanPossibleOptions', { lvmSrs })
import InstallXosanPackModal from './install-xosan-pack-modal' // eslint-disable-line import/first
export const downloadAndInstallXosanPack = pool =>
@@ -2065,4 +1958,32 @@ export const downloadAndInstallXosanPack = pool =>
export const registerXosan = namespace => _call('cloud.registerResource', { namespace: 'xosan' })
export const fixHostNotInXosanNetwork = (xosanSr, host) => _call('xosan.fixHostNotInNetwork', {xosanSr, host})
// VM-Group ----------------------------------------------------------------------
export const startVmGroup = vmGroup => {
_call('vmGroup.start', { id: resolveId(vmGroup) })
}
export const shutdownVmGroup = vmGroup => _call('vmGroup.shutdown', { id: resolveId(vmGroup) })
export const rebootVmGroup = async vmGroup => {
await shutdownVmGroup(vmGroup)
await startVmGroup(vmGroup)
}
export const editVmGroup = (vmGroup, props) => _call('vmGroup.set', { id: resolveId(vmGroup), ...props })
export const deleteVmGroup = (vmGroup, vms) =>
confirm({
title: _('deleteVmGroupModalTitle'),
body: _('deleteVmGroupModalMessage')
}).then(() => {
forEach(vms, vm => removeAppliance(vm))
_call('vmGroup.destroy', { id: resolveId(vmGroup) })
}, Promise.reject())
export const createVmGroup = ({ pool, name_label, name_description }) => _call('vmGroup.create', { id: resolveId(pool), name_label, name_description })
export const startVmGroups = (vmGroupIds) => {
forEach(vmGroupIds, id => startVmGroup({id: id}))
}
export const shutdownVmGroups = (vmGroupIds) => {
forEach(vmGroupIds, id => shutdownVmGroup({id: id}))
}
export const rebootVmGroups = (vmGroupIds) => {
forEach(vmGroupIds, id => rebootVmGroup({id: id}))
}

View File

@@ -93,14 +93,15 @@ export default class InstallXosanPackModal extends Component {
</div>
</div>
: <div>
{_('xosanNoPackFound')}
<br />
{_('xosanPackRequirements')}
<ul>
{map(this._getXosanPacks(), ({ name, requirements }, key) => <li key={key}>
{_.keyValue(name, requirements && requirements.xenserver ? requirements.xenserver : '/')}
</li>)}
</ul>
<p>{_('xosanNoPackFound')}</p>
<p>
{_('xosanPackRequirements')}
<ul>
{map(this._getXosanPacks(), ({ name, requirements }) => <li>
{name}: <strong>{requirements && requirements.xenserver ? requirements.xenserver : '/'}</strong>
</li>)}
</ul>
</p>
</div>
}
</div>

View File

@@ -3,23 +3,23 @@ import every from 'lodash/every'
import forEach from 'lodash/forEach'
import find from 'lodash/find'
import map from 'lodash/map'
import mapValues from 'lodash/mapValues'
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 { getDefaultNetworkForVif } from '../utils'
import {
SelectHost,
SelectNetwork
SelectNetwork,
SelectSr
} from '../../select-objects'
import {
connectStore,
mapPlus,
resolveIds
mapPlus
} from '../../utils'
import {
createGetObjectsOfType,
@@ -138,8 +138,7 @@ export default class MigrateVmModalBody extends BaseComponent {
get value () {
return {
targetHost: this.state.host && this.state.host.id,
sr: this.state.mainSr && this.state.mainSr.id,
mapVdisSrs: resolveIds(this.state.mapVdisSrs),
mapVdisSrs: this.state.mapVdisSrs,
mapVifsNetworks: this.state.mapVifsNetworks,
migrationNetwork: this.state.migrationNetworkId
}
@@ -159,10 +158,11 @@ export default class MigrateVmModalBody extends BaseComponent {
return
}
const { vbds, vm } = this.props
const { pools, vbds, vdis, vm } = this.props
const intraPool = vm.$pool === host.$pool
// Intra-pool
const defaultSr = pools[host.$pool].default_SR
if (intraPool) {
let doNotMigrateVdis
if (vm.$container === host.id) {
@@ -181,6 +181,7 @@ export default class MigrateVmModalBody extends BaseComponent {
doNotMigrateVdis,
host,
intraPool,
mapVdisSrs: doNotMigrateVdis ? undefined : mapValues(vdis, vdi => defaultSr),
mapVifsNetworks: undefined,
migrationNetwork: undefined
})
@@ -211,6 +212,7 @@ export default class MigrateVmModalBody extends BaseComponent {
doNotMigrateVdis: false,
host,
intraPool,
mapVdisSrs: mapValues(vdis, vdi => defaultSr),
mapVifsNetworks: defaultNetworksForVif,
migrationNetworkId: defaultMigrationNetworkId
})
@@ -224,6 +226,7 @@ export default class MigrateVmModalBody extends BaseComponent {
doNotMigrateVdis,
host,
intraPool,
mapVdisSrs,
mapVifsNetworks,
migrationNetworkId
} = this.state
@@ -242,14 +245,25 @@ export default class MigrateVmModalBody extends BaseComponent {
</div>
{host && !doNotMigrateVdis && <div className={styles.groupBlock}>
<SingleLineRow>
<Col size={12}>
<ChooseSrForEachVdisModal
onChange={props => this.setState(props)}
predicate={this._getSrPredicate()}
vdis={vdis}
/>
</Col>
<Col>{_('migrateVmSelectSrs')}</Col>
</SingleLineRow>
<br />
<SingleLineRow>
<Col size={6}><span className={styles.listTitle}>{_('migrateVmName')}</span></Col>
<Col size={6}><span className={styles.listTitle}>{_('migrateVmSr')}</span></Col>
</SingleLineRow>
{map(vdis, vdi => <div className={styles.listItem} key={vdi.id}>
<SingleLineRow>
<Col size={6}>{vdi.name_label}</Col>
<Col size={6}>
<SelectSr
onChange={sr => this.setState({ mapVdisSrs: { ...mapVdisSrs, [vdi.id]: sr.id } })}
predicate={this._getSrPredicate()}
value={mapVdisSrs[vdi.id]}
/>
</Col>
</SingleLineRow>
</div>)}
</div>}
{intraPool !== undefined &&
(!intraPool &&

View File

@@ -12,10 +12,8 @@ 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 {
@@ -219,7 +217,6 @@ export default class MigrateVmsModalBody extends BaseComponent {
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) => {
@@ -237,8 +234,6 @@ export default class MigrateVmsModalBody extends BaseComponent {
})
const noVdisMigration = every(doNotMigrateVmVdis)
this.setState({
defaultSrConnectedToHost,
defaultSrId,
host,
intraPool: every(this.props.vms, vm => vm.$pool === host.$pool),
doNotMigrateVdi,
@@ -247,7 +242,7 @@ export default class MigrateVmsModalBody extends BaseComponent {
networkId: defaultMigrationNetworkId,
noVdisMigration,
smartVifMapping: true,
srId: defaultSrConnectedToHost ? defaultSrId : undefined
srId: defaultSrId
})
}
_selectMigrationNetwork = migrationNetwork => this.setState({ migrationNetworkId: migrationNetwork.id })
@@ -257,8 +252,6 @@ export default class MigrateVmsModalBody extends BaseComponent {
render () {
const {
defaultSrConnectedToHost,
defaultSrId,
host,
intraPool,
migrationNetworkId,
@@ -297,24 +290,7 @@ export default class MigrateVmsModalBody extends BaseComponent {
{host && (!intraPool || !noVdisMigration) &&
<div key='sr' style={LINE_STYLE}>
<SingleLineRow>
<Col size={6}>
{!intraPool ? _('migrateVmsSelectSr') : _('migrateVmsSelectSrIntraPool')}
{' '}
{(defaultSrId === undefined || !defaultSrConnectedToHost) &&
<Tooltip
content={defaultSrId !== undefined
? _('migrateVmNotConnectedDefaultSrError')
: _('migrateVmNoDefaultSrError')
}
>
<Icon
icon={defaultSrId !== undefined ? 'alarm' : 'info'}
className={defaultSrId !== undefined ? 'text-warning' : 'text-info'}
size='lg'
/>
</Tooltip>
}
</Col>
<Col size={6}>{!intraPool ? _('migrateVmsSelectSr') : _('migrateVmsSelectSrIntraPool')}</Col>
<Col size={6}>
<SelectSr
onChange={this._selectSr}

View File

@@ -79,14 +79,6 @@
@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;
@@ -209,14 +201,6 @@
@extend .fa;
@extend .fa-microchip;
}
&-gpu {
@extend .fa;
@extend .fa-microchip;
}
&-vgpu {
@extend .fa;
@extend .fa-microchip;
}
&-memory {
@extend .fa;
@extend .fa-sliders;
@@ -415,16 +399,6 @@
@extend .xo-status-halted;
}
&-connected {
@extend .fa;
@extend .fa-link;
}
&-disconnected {
@extend .fa;
@extend .fa-unlink;
}
// Task
&-task {
&-cancel {
@@ -478,10 +452,6 @@
@extend .fa-server;
@extend .text-warning;
}
&-forget {
@extend .fa;
@extend .fa-ban;
}
&-working {
@extend .fa;
@extend .fa-circle;
@@ -981,15 +951,4 @@
@extend .fa;
@extend .fa-star;
}
// XOSAN related
&-health {
@extend .fa;
@extend .fa-heartbeat;
}
&-fix {
@extend .fa;
@extend .fa-wrench;
}
}

View File

@@ -17,13 +17,6 @@ const keymap = {
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 }
@@ -32,6 +25,6 @@ export const help = mapValues(keymap, (shortcuts, contextLabel) => ({
name: _(`shortcut_${contextLabel}`),
shortcuts: mapValues(shortcuts, (shortcut, label) => ({
keys: shortcuts[label],
message: _(`shortcut_${contextLabel}_${label}`)
message: _(`shortcut_${label}`)
}))
}))

View File

@@ -7,15 +7,14 @@ import React from 'react'
import replace from 'lodash/replace'
import Tooltip from 'tooltip'
import { Container, Col, Row } from 'grid'
import { createSelector } from 'reselect'
import { formatSize } from 'utils'
import { FormattedDate } from 'react-intl'
import { SelectPlainObject } from 'form'
import {
filter,
includes,
find,
isEmpty,
map
map,
filter
} from 'lodash'
import {
scanDisk,
@@ -99,14 +98,6 @@ export default class RestoreFileModalBody extends Component {
)
}
_getSelectableFiles = createSelector(
() => this.state.files,
() => this.state.selectedFiles,
(available, selected) => filter(available, file =>
!includes(selected, file)
)
)
_onBackupChange = backup => {
this.setState({
backup,
@@ -168,10 +159,23 @@ export default class RestoreFileModalBody extends Component {
}
_onFileChange = file => {
if (file == null) {
const { path, selectedFiles } = this.state
const isFile = file && file.id !== '..' && !endsWith(file.path, '/')
if (isFile) {
this.setState({
file,
selectedFiles: find(selectedFiles, { id: file.id })
? selectedFiles
: (selectedFiles || []).concat(file)
})
return
}
this.setState({
file: undefined
})
// 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
@@ -180,17 +184,9 @@ export default class RestoreFileModalBody extends Component {
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 {
if (file) {
this.setState({
path: file.id === '..' ? getParentPath(this.state.path) : file.path
path: file.id === '..' ? getParentPath(path) : file.path
}, this._scanFiles)
}
}
@@ -208,10 +204,12 @@ export default class RestoreFileModalBody extends Component {
}
_selectAllFolderFiles = () => {
const { files, selectedFiles } = this.state
this.setState({
selectedFiles: (this.state.selectedFiles || []).concat(
filter(this._getSelectableFiles(), ({path}) =>
!endsWith(path, '/')
selectedFiles: (selectedFiles || []).concat(
filter(files, ({ path }) =>
!endsWith(path, '/') && !find(selectedFiles, file => file.path === path)
)
)
})
@@ -224,6 +222,8 @@ export default class RestoreFileModalBody extends Component {
const {
backup,
disk,
file,
files,
format,
partition,
partitions,
@@ -293,9 +293,9 @@ export default class RestoreFileModalBody extends Component {
onChange={this._onFileChange}
optionKey='id'
optionRenderer={fileOptionRenderer}
options={this._getSelectableFiles()}
options={files}
placeholder={_('restoreFilesSelectFiles')}
value={null}
value={file}
/>,
<br />,
<div>

View File

@@ -138,9 +138,9 @@ const COMMON_SCHEMA = {
required: [ 'tag', 'vms', '_reportWhen' ]
}
const RETENTION_PROPERTY = {
const DEPTH_PROPERTY = {
type: 'integer',
title: _('editBackupRetentionTitle'),
title: _('editBackupDepthTitle'),
description: 'How many backups to rollover.', // FIXME: can't translate
min: 1
}
@@ -155,7 +155,7 @@ const BACKUP_SCHEMA = {
type: 'object',
properties: {
...COMMON_SCHEMA.properties,
retention: RETENTION_PROPERTY,
depth: DEPTH_PROPERTY,
remoteId: REMOTE_PROPERTY,
compress: {
type: 'boolean',
@@ -163,33 +163,33 @@ const BACKUP_SCHEMA = {
default: true
}
},
required: COMMON_SCHEMA.required.concat([ 'retention', 'remoteId' ])
required: COMMON_SCHEMA.required.concat([ 'depth', 'remoteId' ])
}
const ROLLING_SNAPSHOT_SCHEMA = {
type: 'object',
properties: {
...COMMON_SCHEMA.properties,
retention: RETENTION_PROPERTY
depth: DEPTH_PROPERTY
},
required: COMMON_SCHEMA.required.concat('retention')
required: COMMON_SCHEMA.required.concat('depth')
}
const DELTA_BACKUP_SCHEMA = {
type: 'object',
properties: {
...COMMON_SCHEMA.properties,
retention: RETENTION_PROPERTY,
depth: DEPTH_PROPERTY,
remote: REMOTE_PROPERTY
},
required: COMMON_SCHEMA.required.concat([ 'retention', 'remote' ])
required: COMMON_SCHEMA.required.concat([ 'depth', 'remote' ])
}
const DISASTER_RECOVERY_SCHEMA = {
type: 'object',
properties: {
...COMMON_SCHEMA.properties,
retention: RETENTION_PROPERTY,
depth: DEPTH_PROPERTY,
deleteOldBackupsFirst: {
type: 'boolean',
title: _('deleteOldBackupsFirst'),
@@ -205,14 +205,13 @@ const DISASTER_RECOVERY_SCHEMA = {
title: 'To SR'
}
},
required: COMMON_SCHEMA.required.concat([ 'retention', 'sr' ])
required: COMMON_SCHEMA.required.concat([ 'depth', 'sr' ])
}
const CONTINUOUS_REPLICATION_SCHEMA = {
type: 'object',
properties: {
...COMMON_SCHEMA.properties,
retention: RETENTION_PROPERTY,
sr: {
type: 'string',
'xo:type': 'sr',
@@ -331,17 +330,6 @@ const constructPattern = ({ not, values } = EMPTY_OBJECT, valueTransform = ident
: pattern
}
const normalizeMainParams = params => {
if (!('retention' in params)) {
const { depth, ...rest } = params
if (depth != null) {
params = rest
params.retention = depth
}
}
return params
}
@connectStore({
currentUser: getUser
})
@@ -360,10 +348,10 @@ export default class New extends Component {
// legacy backup jobs
if (items.length === 1) {
return {
main: normalizeMainParams({
main: {
enabled,
...items[0].values[0]
}),
},
vms: { vms: map(items[0].values.slice(1), extractId) }
}
}
@@ -374,10 +362,10 @@ export default class New extends Component {
const { $pool, tags } = pattern
return {
main: normalizeMainParams({
main: {
enabled,
...items[0].values[0]
}),
},
vms: {
$pool: destructPattern($pool),
power_state: pattern.power_state,
@@ -388,10 +376,10 @@ export default class New extends Component {
// normal backup
return {
main: normalizeMainParams({
main: {
enabled,
...items[1].values[0]
}),
},
vms: { vms: map(items[0].values, extractId) }
}
}
@@ -575,133 +563,131 @@ export default class New extends Component {
return (
<Upgrade place='newBackup' required={2}>
<form id='form-new-vm-backup'>
<Wizard>
<Section icon='backup' title={this.props.job ? 'editVmBackup' : 'newVmBackup'}>
<Container>
<Row>
<Col>
<Wizard><form id='form-new-vm-backup'>
<Section icon='backup' title={this.props.job ? 'editVmBackup' : 'newVmBackup'}>
<Container>
<Row>
<Col>
<fieldset className='form-group'>
<label>{_('backupOwner')}</label>
<SelectSubject
onChange={this.linkState('job.userId', 'id')}
predicate={this._subjectPredicate}
required
value={this._getValue('job', 'userId', this.props.currentUser.id)}
/>
</fieldset>
<fieldset className='form-group'>
<label>{_('jobTimeoutPlaceHolder')}</label>
<TimeoutInput
className='form-control'
onChange={this.linkState('job.timeout')}
value={this._getValue('job', 'timeout')}
/>
</fieldset>
<fieldset className='form-group'>
<label htmlFor='selectBackup'>{_('newBackupSelection')}</label>
<select
className='form-control'
id='selectBackup'
onChange={this.linkState('job.method')}
required
value={method}
>
{_('noSelectedValue', message => <option value=''>{message}</option>)}
{map(BACKUP_METHOD_TO_INFO, (info, key) =>
_(info.label, message => <option key={key} value={key}>{message}</option>)
)}
</select>
</fieldset>
{(method === 'vm.rollingDeltaBackup' || method === 'vm.deltaCopy') && <div className='alert alert-warning' role='alert'>
<Icon icon='error' /> {_('backupVersionWarning')}
</div>}
{backupInfo && <div>
<GenericInput
label={<span><Icon icon={backupInfo.icon} /> {_(backupInfo.label)}</span>}
required
schema={backupInfo.schema}
uiSchema={backupInfo.uiSchema}
onChange={this.linkState('mainParams')}
value={this._getMainParams()}
/>
<fieldset className='form-group'>
<label>{_('backupOwner')}</label>
<SelectSubject
onChange={this.linkState('job.userId', 'id')}
predicate={this._subjectPredicate}
required
value={this._getValue('job', 'userId', this.props.currentUser.id)}
/>
</fieldset>
<fieldset className='form-group'>
<label>{_('jobTimeoutPlaceHolder')}</label>
<TimeoutInput
className='form-control'
onChange={this.linkState('job.timeout')}
value={this._getValue('job', 'timeout')}
/>
</fieldset>
<fieldset className='form-group'>
<label htmlFor='selectBackup'>{_('newBackupSelection')}</label>
<label htmlFor='smartMode'>{_('smartBackupModeSelection')}</label>
<select
className='form-control'
id='selectBackup'
onChange={this.linkState('job.method')}
id='smartMode'
onChange={this._handleSmartBackupMode}
required
value={method}
value={smartBackupMode ? 'smart' : 'normal'}
>
{_('noSelectedValue', message => <option value=''>{message}</option>)}
{map(BACKUP_METHOD_TO_INFO, (info, key) =>
_({ key }, info.label, message => <option value={key}>{message}</option>)
)}
{_('normalBackup', message => <option value='normal'>{message}</option>)}
{_('smartBackup', message => <option value='smart'>{message}</option>)}
</select>
</fieldset>
{(method === 'vm.rollingDeltaBackup' || method === 'vm.deltaCopy') && <div className='alert alert-warning' role='alert'>
<Icon icon='error' /> {_('backupVersionWarning')}
</div>}
{backupInfo && <div>
<GenericInput
label={<span><Icon icon={backupInfo.icon} /> {_(backupInfo.label)}</span>}
required
schema={backupInfo.schema}
uiSchema={backupInfo.uiSchema}
onChange={this.linkState('mainParams')}
value={this._getMainParams()}
/>
<fieldset className='form-group'>
<label htmlFor='smartMode'>{_('smartBackupModeSelection')}</label>
<select
className='form-control'
id='smartMode'
onChange={this._handleSmartBackupMode}
required
value={smartBackupMode ? 'smart' : 'normal'}
>
{_('normalBackup', message => <option value='normal'>{message}</option>)}
{_('smartBackup', message => <option value='smart'>{message}</option>)}
</select>
</fieldset>
{smartBackupMode
? <Upgrade place='newBackup' required={3}>
<GenericInput
label={<span><Icon icon='vm' /> {_('vmsToBackup')}</span>}
onChange={this.linkState('vmsParam')}
required
schema={SMART_SCHEMA}
uiSchema={SMART_UI_SCHEMA}
value={vms}
/>
</Upgrade>
: <GenericInput
{smartBackupMode
? <Upgrade place='newBackup' required={3}>
<GenericInput
label={<span><Icon icon='vm' /> {_('vmsToBackup')}</span>}
onChange={this.linkState('vmsParam')}
required
schema={NO_SMART_SCHEMA}
uiSchema={NO_SMART_UI_SCHEMA}
schema={SMART_SCHEMA}
uiSchema={SMART_UI_SCHEMA}
value={vms}
/>
}
</div>}
</Col>
</Row>
</Container>
</Section>
<Section icon='schedule' title='schedule'>
<Scheduler
onChange={this.linkState('scheduling')}
value={scheduling}
/>
</Section>
<Section icon='preview' title='preview' summary>
<Container>
<Row>
<Col>
<SchedulePreview cronPattern={scheduling.cronPattern} />
{process.env.XOA_PLAN < 4 && backupInfo && process.env.XOA_PLAN < REQUIRED_XOA_PLAN[backupInfo.jobKey]
? <Upgrade place='newBackup' available={REQUIRED_XOA_PLAN[backupInfo.jobKey]} />
: (smartBackupMode && process.env.XOA_PLAN < 3
? <Upgrade place='newBackup' available={3} />
: <fieldset className='pull-right pt-1'>
<ActionButton
btnStyle='primary'
className='mr-1'
disabled={!backupInfo}
form='form-new-vm-backup'
handler={this._handleSubmit}
icon='save'
redirectOnSuccess='/backup/overview'
size='large'
>
{_('saveBackupJob')}
</ActionButton>
<Button onClick={this._handleReset} size='large'>
{_('selectTableReset')}
</Button>
</fieldset>)
}
</Col>
</Row>
</Container>
</Section>
</Wizard>
</form>
</Upgrade>
: <GenericInput
label={<span><Icon icon='vm' /> {_('vmsToBackup')}</span>}
onChange={this.linkState('vmsParam')}
required
schema={NO_SMART_SCHEMA}
uiSchema={NO_SMART_UI_SCHEMA}
value={vms}
/>
}
</div>}
</Col>
</Row>
</Container>
</Section>
<Section icon='schedule' title='schedule'>
<Scheduler
onChange={this.linkState('scheduling')}
value={scheduling}
/>
</Section>
<Section icon='preview' title='preview' summary>
<Container>
<Row>
<Col>
<SchedulePreview cronPattern={scheduling.cronPattern} />
{process.env.XOA_PLAN < 4 && backupInfo && process.env.XOA_PLAN < REQUIRED_XOA_PLAN[backupInfo.jobKey]
? <Upgrade place='newBackup' available={REQUIRED_XOA_PLAN[backupInfo.jobKey]} />
: (smartBackupMode && process.env.XOA_PLAN < 3
? <Upgrade place='newBackup' available={3} />
: <fieldset className='pull-right pt-1'>
<ActionButton
btnStyle='primary'
className='mr-1'
disabled={!backupInfo}
form='form-new-vm-backup'
handler={this._handleSubmit}
icon='save'
redirectOnSuccess='/backup/overview'
size='large'
>
{_('saveBackupJob')}
</ActionButton>
<Button onClick={this._handleReset} size='large'>
{_('selectTableReset')}
</Button>
</fieldset>)
}
</Col>
</Row>
</Container>
</Section>
</form></Wizard>
</Upgrade>
)
}

View File

@@ -2,10 +2,15 @@ import _ from 'intl'
import ActionRowButton from 'action-row-button'
import ButtonGroup from 'button-group'
import Component from 'base-component'
import filter from 'lodash/filter'
import find from 'lodash/find'
import forEach from 'lodash/forEach'
import get from 'lodash/get'
import Icon from 'icon'
import Link from 'link'
import LogList from '../../logs'
import NoObjects from 'no-objects'
import map from 'lodash/map'
import orderBy from 'lodash/orderBy'
import React from 'react'
import SortedTable from 'sorted-table'
import StateButton from 'state-button'
@@ -17,14 +22,6 @@ import {
CardHeader,
CardBlock
} from 'card'
import {
filter,
find,
forEach,
get,
map,
orderBy
} from 'lodash'
import {
deleteBackupSchedule,
disableSchedule,
@@ -125,6 +122,7 @@ export default class Overview extends Component {
constructor (props) {
super(props)
this.state = {
schedules: [],
scheduleTable: {}
}
}
@@ -214,12 +212,12 @@ export default class Overview extends Component {
<div>
<Card>
<CardHeader>
<Icon icon='schedule' /> {_('backupSchedules')}
<h5><Icon icon='schedule' /> {_('backupSchedules')}</h5>
</CardHeader>
<CardBlock>
<NoObjects collection={schedules} emptyMessage={_('noScheduledJobs')}>
{schedules.length ? (
<SortedTable columns={JOB_COLUMNS} collection={this._getScheduleCollection()} userData={isScheduleUserMissing} />
</NoObjects>
) : <p>{_('noScheduledJobs')}</p>}
</CardBlock>
</Card>
<LogList jobKeys={Object.keys(jobKeyToLabel)} />

View File

@@ -1,11 +1,8 @@
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 getEventValue from 'get-event-value'
import groupBy from 'lodash/groupBy'
import Icon from 'icon'
import isEmpty from 'lodash/isEmpty'
@@ -18,27 +15,22 @@ 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 { SelectPlainObject, Toggle } from 'form'
import { SelectSr } from 'select-objects'
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 => <span>
@@ -57,7 +49,7 @@ const VM_COLUMNS = [
{
name: _('backupTags'),
itemRenderer: ({ tagsByRemote }) => <Container>
{map(tagsByRemote, ({ tags, remoteName }, key) => <Row key={key}>
{map(tagsByRemote, ({ tags, remoteName }) => <Row>
<Col mediumSize={3}><strong>{remoteName}</strong></Col>
<Col mediumSize={9}>{tags.join(', ')}</Col>
</Row>)}
@@ -84,8 +76,8 @@ const openImportModal = ({ backups }) => confirm({
body: <ImportModalBody vmName={backups[0].name} backups={backups} />
}).then(doImport)
const doImport = ({ backup, mainSr, start, mapVdisSrs }) => {
if (!mainSr || !backup) {
const doImport = ({ backup, sr, start }) => {
if (!sr || !backup) {
error(_('backupRestoreErrorTitle'), _('backupRestoreErrorMessage'))
return
}
@@ -95,7 +87,7 @@ const doImport = ({ backup, mainSr, start, mapVdisSrs }) => {
}
info(_('importBackupTitle'), _('importBackupMessage'))
try {
const importPromise = importMethods[backup.type]({remote: backup.remoteId, sr: mainSr, file: backup.path, mapVdisSrs}).then(id => {
const importPromise = importMethods[backup.type]({remote: backup.remoteId, sr, file: backup.path}).then(id => {
return id
})
if (start) {
@@ -107,59 +99,16 @@ const doImport = ({ backup, mainSr, start, mapVdisSrs }) => {
}
class _ModalBody extends Component {
constructor () {
super()
this.state = {
mapVdisSrs: {}
}
}
get value () {
return this.state
}
_getSrPredicate = createSelector(
() => this.state.sr,
() => this.state.mapVdisSrs,
(defaultSr, mapVdisSrs) => sr =>
sr !== defaultSr &&
isSrWritable(sr) &&
defaultSr.$pool === sr.$pool &&
areSrsCompatible(defaultSr, sr) &&
every(mapVdisSrs, selectedSr => selectedSr == null || areSrsCompatible(selectedSr, sr))
)
_onChangeDefaultSr = event => {
const oldSr = this.state.sr
const newSr = getEventValue(event)
if (oldSr == null || newSr == null || oldSr.$pool !== newSr.$pool) {
this.setState({
mapVdisSrs: {}
})
} else if (!newSr.shared) {
const mapVdisSrs = {...this.state.mapVdisSrs}
forEach(mapVdisSrs, (sr, vdi) => {
if (sr != null && newSr !== sr && sr.$container !== newSr.$container && !sr.shared) {
delete mapVdisSrs[vdi]
}
})
this.setState({
mapVdisSrs
})
}
this.setState({
sr: newSr
})
}
render () {
const { backups, intl } = this.props
const vdis = this.state.backup && this.state.backup.vdis
return <div>
<SelectSr onChange={this.linkState('sr')} predicate={isSrWritable} />
<br />
<SelectPlainObject
onChange={this.linkState('backup')}
optionKey='path'
@@ -168,11 +117,6 @@ class _ModalBody extends Component {
placeholder={intl.formatMessage(messages.importBackupModalSelectBackup)}
/>
<br />
<ChooseSrForEachVdisModal
vdis={vdis}
onChange={props => this.setState(props)}
/>
<br />
<Toggle onChange={this.linkState('start')} /> {_('importBackupModalStart')}
</div>
}
@@ -192,26 +136,16 @@ export default class Restore extends Component {
}
_listAll = async remotes => {
const remotesInfo = await Promise.all(map(remotes, async remote => ({
files: await listRemote(remote.id),
backupsInfo: await listRemoteBackups(remote.id)
})))
const remotesFiles = await Promise.all(map(remotes, remote => listRemote(remote.id)))
const backupInfoByVm = {}
forEach(remotesInfo, (remoteInfo, index) => {
forEach(remotesFiles, (remoteFiles, index) => {
const remote = remotes[index]
forEach(remoteInfo.files, file => {
forEach(remoteFiles, 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),
@@ -220,8 +154,7 @@ export default class Restore extends Component {
path: file,
tag,
remoteId: remote.id,
remoteName: remote.name,
vdis
remoteName: remote.name
}
} else {
const backupInfo = /^([^_]+)_([^_]+)_(.*)\.xva$/.exec(file)

View File

@@ -3,18 +3,17 @@ 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 React from 'react'
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 { Container, Row, Col } from 'grid'
import {
deleteMessage,
deleteOrphanedVdis,
@@ -24,18 +23,18 @@ import {
isSrWritable
} from 'xo'
import {
areObjectsFetched,
flatten,
get,
isEmpty,
map,
mapValues
} from 'lodash'
import {
createCollectionWrapper,
createGetObject,
createGetObjectsOfType,
createSelector
} from 'selectors'
import {
flatten,
get,
map,
mapValues
} from 'lodash'
import {
connectStore,
formatSize,
@@ -353,7 +352,6 @@ const ALARM_COLUMNS = [
.filter([ message => message.name === 'ALARM' ])
return {
areObjectsFetched,
alertMessages: getAlertMessages,
controlDomainVdis: getControlDomainVdis,
userSrs: getUserSrs,
@@ -401,7 +399,7 @@ export default class Health extends Component {
).then(
formattedMessages => {
this.setState({
messages: map(formattedMessages, ({ id, ...formattedMessage }) => ({
messages: map(formattedMessages, ({ ...formattedMessage, id }) => ({
formatted: formattedMessage,
...props.alertMessages[id]
}))
@@ -430,8 +428,6 @@ export default class Health extends Component {
_getSrUrl = sr => `srs/${sr.id}`
render () {
const { props } = this
return process.env.XOA_PLAN > 3
? <Container>
<Row>
@@ -441,21 +437,18 @@ export default class Health extends Component {
<Icon icon='disk' /> {_('srStatePanel')}
</CardHeader>
<CardBlock>
<NoObjects
collection={props.areObjectsFetched ? props.userSrs : null}
emptyMessage={_('noSrs')}
>
<Row>
{isEmpty(this.props.userSrs)
? <p className='text-xs-center'>{_('noSrs')}</p>
: <Row>
<Col>
<SortedTable
collection={props.userSrs}
collection={this.props.userSrs}
columns={SR_COLUMNS}
rowLink={this._getSrUrl}
shortcutsTarget='body'
/>
</Col>
</Row>
</NoObjects>
}
</CardBlock>
</Card>
</Col>
@@ -467,11 +460,9 @@ export default class Health extends Component {
<Icon icon='disk' /> {_('orphanedVdis')}
</CardHeader>
<CardBlock>
<NoObjects
collection={props.areObjectsFetched ? props.vdiOrphaned : null}
emptyMessage={_('noOrphanedObject')}
>
<div>
{isEmpty(this.props.vdiOrphaned)
? <p className='text-xs-center'>{_('noOrphanedObject')}</p>
: <div>
<Row>
<Col className='text-xs-right'>
<TabButton
@@ -488,7 +479,7 @@ export default class Health extends Component {
</Col>
</Row>
</div>
</NoObjects>
}
</CardBlock>
</Card>
</Col>
@@ -500,29 +491,25 @@ export default class Health extends Component {
<Icon icon='disk' /> {_('vdisOnControlDomain')}
</CardHeader>
<CardBlock>
<NoObjects
collection={props.areObjectsFetched ? props.controlDomainVdis : null}
emptyMessage={_('noControlDomainVdis')}
>
<SortedTable collection={props.controlDomainVdis} columns={CONTROL_DOMAIN_VDI_COLUMNS} />
</NoObjects>
{isEmpty(this.props.controlDomainVdis)
? <p className='text-xs-center'>{_('noControlDomainVdis')}</p>
: <SortedTable collection={this.props.controlDomainVdis} columns={CONTROL_DOMAIN_VDI_COLUMNS} />
}
</CardBlock>
</Card>
</Col>
</Row>
<Row className='orphaned-vms'>
<Row>
<Col>
<Card>
<CardHeader>
<Icon icon='vm' /> {_('orphanedVms')}
</CardHeader>
<CardBlock>
<NoObjects
collection={props.areObjectsFetched ? props.vmOrphaned : null}
emptyMessage={_('noOrphanedObject')}
>
<SortedTable collection={props.vmOrphaned} columns={VM_COLUMNS} shortcutsTarget='.orphaned-vms' />
</NoObjects>
{isEmpty(this.props.vmOrphaned)
? <p className='text-xs-center'>{_('noOrphanedObject')}</p>
: <SortedTable collection={this.props.vmOrphaned} columns={VM_COLUMNS} />
}
</CardBlock>
</Card>
</Col>
@@ -534,11 +521,9 @@ export default class Health extends Component {
<Icon icon='alarm' /> {_('alarmMessage')}
</CardHeader>
<CardBlock>
<NoObjects
collection={props.areObjectsFetched ? props.alertMessages : null}
emptyMessage={_('noAlarms')}
>
<div>
{isEmpty(this.props.alertMessages)
? <p className='text-xs-center'>{_('noAlarms')}</p>
: <div>
<Row>
<Col className='text-xs-right'>
<TabButton
@@ -555,7 +540,7 @@ export default class Health extends Component {
</Col>
</Row>
</div>
</NoObjects>
}
</CardBlock>
</Card>
</Col>

View File

@@ -22,7 +22,6 @@ import {
import {
connectStore,
formatSize,
formatSizeShort,
osFamily
} from 'utils'
import {
@@ -204,9 +203,7 @@ export default class HostItem extends Component {
<span>
{host.cpus.cores}x <Icon icon='cpu' />
{' '}&nbsp;{' '}
{formatSizeShort(host.memory.size)} <Icon icon='memory' />
{' '}&nbsp;{' '}
v{host.version.substring(0, 3)}
{formatSize(host.memory.size)} <Icon icon='memory' />
</span>
</Col>
<Col mediumSize={4}>

View File

@@ -5,12 +5,10 @@ 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 propTypes from 'prop-types-decorator'
import React from 'react'
import Shortcuts from 'shortcuts'
import SingleLineRow from 'single-line-row'
@@ -22,6 +20,7 @@ import {
filter,
find,
forEach,
get,
identity,
includes,
isEmpty,
@@ -43,28 +42,30 @@ import {
forgetSrs,
isSrShared,
migrateVms,
rebootVmGroups,
reconnectAllHostsSrs,
rescanSrs,
restartHosts,
restartHostsAgents,
restartVms,
shutdownVmGroups,
snapshotVms,
startVmGroups,
startVms,
stopHosts,
stopVms,
subscribeResourceSets,
subscribeServers
} from 'xo'
import { Container, Row, Col } from 'grid'
import {
SelectHost,
SelectPool,
SelectResourceSet,
SelectTag
} from 'select-objects'
import {
addSubscriptions,
connectStore,
firstDefined,
noop
} from 'utils'
import {
@@ -75,8 +76,7 @@ import {
createPager,
createSelector,
createSort,
getUser,
isAdmin
getUser
} from 'selectors'
import {
DropdownButton,
@@ -89,6 +89,7 @@ import {
import styles from './index.css'
import HostItem from './host-item'
import PoolItem from './pool-item'
import VmGroupItem from './vm-group-item'
import VmItem from './vm-item'
import TemplateItem from './template-item'
import SrItem from './sr-item'
@@ -146,13 +147,25 @@ const OPTIONS = {
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' }
{ labelId: 'homeSortByCpus', sortBy: 'CPUs.number', sortOrder: 'desc' }
]
},
'VmGroup': {
defaultFilter: '',
filters: homeFilters.vmGroup,
mainActions: [
{ handler: shutdownVmGroups, icon: 'vm-stop', tooltip: _('stopVmLabel') },
{ handler: startVmGroups, icon: 'vm-start', tooltip: _('startVmLabel') },
{ handler: rebootVmGroups, icon: 'vm-reboot', tooltip: _('rebootVmLabel') }
],
Item: VmGroupItem,
sortOptions: [
{ labelId: 'homeSortByName', sortBy: 'name_label', sortOrder: 'asc' },
{ labelId: 'homeSortByPowerstate', sortBy: 'power_state', sortOrder: 'desc' }
]
},
pool: {
@@ -201,6 +214,7 @@ const OPTIONS = {
const TYPES = {
VM: _('homeTypeVm'),
VmGroup: _('homeTypeVmGroup'),
'VM-template': _('homeTypeVmTemplate'),
host: _('homeTypeHost'),
pool: _('homeTypePool'),
@@ -210,119 +224,25 @@ const TYPES = {
const DEFAULT_TYPE = 'VM'
@addSubscriptions({
noRegisteredServers: cb => subscribeServers(data => cb(isEmpty(data)))
servers: subscribeServers
})
@connectStore(() => {
const noServersConnected = invoke(
createGetObjectsOfType('host'),
hosts => state => isEmpty(hosts(state))
)
const getType = (_, props) => props.location.query.t || DEFAULT_TYPE
const getObjectsByType = createGetObjectsOfType(getType)
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 <CenterPanel>
<h2><img src='assets/loading.svg' /></h2>
</CenterPanel>
return (state, props) => {
const type = getType(state, props)
return {
areObjectsFetched: areObjectsFetched(state, props),
items: getObjectsByType(state, props),
noServersConnected: noServersConnected(state, props),
type,
user: getUser(state, props)
}
if (noServersConnected && isAdmin) {
return <CenterPanel>
<Card shadow>
<CardHeader>{_('homeWelcome')}</CardHeader>
<CardBlock>
<Link to='/settings/servers'>
<Icon icon='pool' size={4} />
<h4>{noRegisteredServers ? _('homeAddServer') : _('homeConnectServer')}</h4>
</Link>
<p className='text-muted'>{noRegisteredServers ? _('homeWelcomeText') : _('homeConnectServerText')}</p>
<br /><br />
<h3>{_('homeHelp')}</h3>
<Row>
<Col mediumSize={6}>
<a href='https://xen-orchestra.com/docs/' target='_blank' className='btn btn-link'>
<Icon icon='menu-about' size={4} />
<h4>{_('homeOnlineDoc')}</h4>
</a>
</Col>
<Col mediumSize={6}>
<a href='https://xen-orchestra.com/#!/member/support' target='_blank' className='btn btn-link'>
<Icon icon='menu-settings-users' size={4} />
<h4>{_('homeProSupport')}</h4>
</a>
</Col>
</Row>
</CardBlock>
</Card>
</CenterPanel>
}
return <CenterPanel>
<Card shadow>
<CardHeader>{_('homeNoVms')}</CardHeader>
{(isAdmin || !noResourceSets) && <CardBlock>
<Row>
<Col>
<Link to='/vms/new'>
<Icon icon='vm' size={4} />
<h4>{_('homeNewVm')}</h4>
</Link>
<p className='text-muted'>{_('homeNewVmMessage')}</p>
</Col>
</Row>
{isAdmin && <div>
<h2>{_('homeNoVmsOr')}</h2>
<Row>
<Col mediumSize={6}>
<Link to='/import'>
<Icon icon='menu-new-import' size={4} />
<h4>{_('homeImportVm')}</h4>
</Link>
<p className='text-muted'>{_('homeImportVmMessage')}</p>
</Col>
<Col mediumSize={6}>
<Link to='/backup/restore'>
<Icon icon='backup' size={4} />
<h4>{_('homeRestoreBackup')}</h4>
</Link>
<p className='text-muted'>{_('homeRestoreBackupMessage')}</p>
</Col>
</Row>
</div>}
</CardBlock>}
</Card>
</CenterPanel>
}
}
@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 {
@@ -388,12 +308,12 @@ export default class Home extends Component {
_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]
const preferences = get(props, 'user.preferences')
const defaultFilterName = get(preferences, [ 'defaultHomeFilters', type ])
return firstDefined(
defaultFilterName && firstDefined(
get(homeFilters, [ type, defaultFilterName ]),
get(preferences, [ 'filters', type, defaultFilterName ])
),
OPTIONS[type].defaultFilter
)
@@ -403,8 +323,8 @@ export default class Home extends Component {
const sortOption = find(OPTIONS[props.type].sortOptions, 'default')
return {
sortBy: defined(() => sortOption.sortBy, 'name_label'),
sortOrder: defined(() => sortOption.sortOrder, 'asc')
sortBy: firstDefined(sortOption && sortOption.sortBy, 'name_label'),
sortOrder: firstDefined(sortOption && sortOption.sortOrder, 'asc')
}
}
@@ -435,7 +355,6 @@ export default class Home extends Component {
selectedHosts: properties.$container,
selectedPools: properties.$pool,
selectedTags: properties.tags,
selectedResourceSets: properties.resourceSet,
...sort
})
@@ -473,8 +392,6 @@ export default class Home extends Component {
pathname,
query: { ...query, s: filter }
})
this.page = 1
}
_clearFilter = () => this._setFilter('')
@@ -552,19 +469,6 @@ export default class Home extends Component {
: filter::ComplexMatcher.removePropertyClause('tags')
)
}
_updateSelectedResourceSets = resourceSets => {
const filter = this._getParsedFilter()
this._setFilter(resourceSets.length
? filter::ComplexMatcher.setPropertyClause(
'resourceSet',
ComplexMatcher.createOr(map(resourceSets, set =>
ComplexMatcher.createString(set.id)
))
)
: filter::ComplexMatcher.removePropertyClause('resourceSet')
)
}
_addCustomFilter = () => {
return addCustomFilter(
this._getType(),
@@ -619,10 +523,10 @@ export default class Home extends Component {
this.refs.filterInput.focus()
break
case 'NAV_DOWN':
this.setState({ highlighted: (this.state.highlighted + 1) % items.length || 0 })
this.setState({ highlighted: (this.state.highlighted + items.length + 1) % items.length || 0 })
break
case 'NAV_UP':
this.setState({ highlighted: (this.state.highlighted - 1) % items.length || 0 })
this.setState({ highlighted: (this.state.highlighted + items.length - 1) % items.length || 0 })
break
case 'SELECT':
const itemId = items[this.state.highlighted].id
@@ -647,11 +551,7 @@ export default class Home extends Component {
// Header --------------------------------------------------------------------
_renderHeader () {
const {
isAdmin,
noResourceSets,
type
} = this.props
const { type } = this.props
const { filters } = OPTIONS[type]
const customFilters = this._getCustomFilters()
@@ -664,27 +564,25 @@ export default class Home extends Component {
</Col>
<Col mediumSize={6}>
<div className='input-group'>
<span className='input-group-btn'>
<DropdownButton id='filter' bsStyle='info' title={_('homeFilters')}>
<MenuItem onClick={this._addCustomFilter}>
{_('filterSaveAs')}
</MenuItem>
<MenuItem divider />
{!isEmpty(customFilters) && [
map(customFilters, (filter, name) =>
<MenuItem key={`custom-${name}`} onClick={() => this._setFilter(filter)}>
{name}
{!isEmpty(filters) && (
<div className='input-group-btn'>
<DropdownButton id='filter' bsStyle='info' title={_('homeFilters')}>
{!isEmpty(customFilters) && [
map(customFilters, (filter, name) =>
<MenuItem key={`custom-${name}`} onClick={() => this._setFilter(filter)}>
{name}
</MenuItem>
),
<MenuItem divider />
]}
{map(filters, (filter, label) =>
<MenuItem key={label} onClick={() => this._setFilter(filter)}>
{_(label)}
</MenuItem>
),
<MenuItem key='divider' divider />
]}
{map(filters, (filter, label) =>
<MenuItem key={label} onClick={() => this._setFilter(filter)}>
{_(label)}
</MenuItem>
)}
</DropdownButton>
</span>
)}
</DropdownButton>
</div>
)}
<input
className='form-control'
defaultValue={this._getFilter()}
@@ -692,30 +590,27 @@ export default class Home extends Component {
ref='filterInput'
type='text'
/>
<Tooltip content={_('filterSyntaxLinkTooltip')}>
<a
className='input-group-addon'
href='https://xen-orchestra.com/docs/search.html#filter-syntax'
target='_blank'
>
<Icon icon='info' />
</a>
</Tooltip>
<span className='input-group-btn'>
<div className='input-group-btn'>
<Button onClick={this._clearFilter}>
<Icon icon='clear-search' />
</Button>
</span>
</div>
<div className='input-group-btn'>
<ActionButton
btnStyle='primary'
handler={this._addCustomFilter}
icon='save'
/>
</div>
</div>
</Col>
{(isAdmin || !noResourceSets) && <Col mediumSize={3} className='text-xs-right'>
<Col mediumSize={3} className='text-xs-right'>
<Link
className='btn btn-success'
to='/vms/new'
>
to='/vms/new'>
<Icon icon='vm-new' /> {_('homeNewVm')}
</Link>
</Col>}
</Col>
</Row>
</Container>
}
@@ -724,17 +619,89 @@ export default class Home extends Component {
render () {
const {
isAdmin,
noResourceSets
areObjectsFetched,
noServersConnected,
servers,
user
} = this.props
const nItems = this._getNumberOfItems()
const isAdmin = user && user.permission === 'admin'
const noRegisteredServers = !servers || !servers.length
if (nItems < 1) {
return <NoObjects_
isAdmin={isAdmin}
noResourceSets={noResourceSets}
/>
if (!areObjectsFetched) {
return <CenterPanel>
<h2><img src='assets/loading.svg' /></h2>
</CenterPanel>
}
if (noServersConnected && isAdmin) {
return <CenterPanel>
<Card shadow>
<CardHeader>{_('homeWelcome')}</CardHeader>
<CardBlock>
<Link to='/settings/servers'>
<Icon icon='pool' size={4} />
<h4>{noRegisteredServers ? _('homeAddServer') : _('homeConnectServer')}</h4>
</Link>
<p className='text-muted'>{noRegisteredServers ? _('homeWelcomeText') : _('homeConnectServerText')}</p>
<br /><br />
<h3>{_('homeHelp')}</h3>
<Row>
<Col mediumSize={6}>
<a href='https://xen-orchestra.com/docs/' target='_blank' className='btn btn-link'>
<Icon icon='menu-about' size={4} />
<h4>{_('homeOnlineDoc')}</h4>
</a>
</Col>
<Col mediumSize={6}>
<a href='https://xen-orchestra.com/#!/member/support' target='_blank' className='btn btn-link'>
<Icon icon='menu-settings-users' size={4} />
<h4>{_('homeProSupport')}</h4>
</a>
</Col>
</Row>
</CardBlock>
</Card>
</CenterPanel>
}
const nItems = this._getNumberOfItems()
if (!nItems) {
return <CenterPanel>
<Card shadow>
<CardHeader>{_('homeNoVms')}</CardHeader>
<CardBlock>
<Row>
<Col>
<Link to='/vms/new'>
<Icon icon='vm' size={4} />
<h4>{_('homeNewVm')}</h4>
</Link>
<p className='text-muted'>{_('homeNewVmMessage')}</p>
</Col>
</Row>
{isAdmin && <div>
<h2>{_('homeNoVmsOr')}</h2>
<Row>
<Col mediumSize={6}>
<Link to='/import'>
<Icon icon='menu-new-import' size={4} />
<h4>{_('homeImportVm')}</h4>
</Link>
<p className='text-muted'>{_('homeImportVmMessage')}</p>
</Col>
<Col mediumSize={6}>
<Link to='/backup/restore'>
<Icon icon='backup' size={4} />
<h4>{_('homeRestoreBackup')}</h4>
</Link>
<p className='text-muted'>{_('homeRestoreBackupMessage')}</p>
</Col>
</Row>
</div>}
</CardBlock>
</Card>
</CenterPanel>
}
const filteredItems = this._getFilteredItems()
@@ -747,7 +714,6 @@ export default class Home extends Component {
selectedHosts,
selectedItems,
selectedPools,
selectedResourceSets,
selectedTags,
sortBy
} = this.state
@@ -762,8 +728,7 @@ export default class Home extends Component {
mainActions,
otherActions,
showHostsSelector,
showPoolsSelector,
showResourceSetsSelector
showPoolsSelector
} = options
// Necessary because indeterminate cannot be used as an attribute
@@ -843,6 +808,7 @@ export default class Home extends Component {
<Button btnStyle='link'><Icon icon='pool' /> {_('homeAllPools')}</Button>
</OverlayTrigger>
)}
{' '}
{showHostsSelector && (
<OverlayTrigger
trigger='click'
@@ -862,7 +828,8 @@ export default class Home extends Component {
<Button btnStyle='link'><Icon icon='host' /> {_('homeAllHosts')}</Button>
</OverlayTrigger>
)}
<OverlayTrigger
{' '}
{type !== 'VmGroup' && <OverlayTrigger
autoFocus
trigger='click'
rootClose
@@ -880,24 +847,8 @@ export default class Home extends Component {
}
>
<Button btnStyle='link'><Icon icon='tags' /> {_('homeAllTags')}</Button>
</OverlayTrigger>
{showResourceSetsSelector && isAdmin && !noResourceSets && <OverlayTrigger
trigger='click'
rootClose
placement='bottom'
overlay={
<Popover className={styles.selectObject} id='resourceSetPopover'>
<SelectResourceSet
autoFocus
multi
onChange={this._updateSelectedResourceSets}
value={selectedResourceSets}
/>
</Popover>
}
>
<Button btnStyle='link'><Icon icon='resource-set' /> {_('homeAllResourceSets')}</Button>
</OverlayTrigger>}
</OverlayTrigger> }
{' '}
<DropdownButton bsStyle='link' id='sort' title={_('homeSortBy')}>
{map(options.sortOptions, ({ labelId, sortBy: _sortBy, sortOrder }, key) => (
<MenuItem key={key} onClick={() => this.setState({ sortBy: _sortBy, sortOrder })}>
@@ -931,7 +882,7 @@ export default class Home extends Component {
item={item}
key={item.id}
onSelect={this.toggleState(`selectedItems.${item.id}`)}
selected={Boolean(selectedItems[item.id])}
selected={selectedItems[item.id]}
/>
</div>
))

View File

@@ -1,6 +1,5 @@
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'
@@ -18,6 +17,7 @@ import {
} from 'xo'
import {
connectStore,
firstDefined,
osFamily
} from 'utils'
import {
@@ -74,7 +74,7 @@ export default class TemplateItem extends Component {
<Col mediumSize={4} className={styles.itemExpanded}>
<span>
<Number value={vm.CPUs.number} onChange={this._setCpus} />x <Icon icon='cpu' className='mr-1' />
<Size value={defined(vm.memory.size, null)} onChange={this._setMemory} /> <Icon icon='memory' />
<Size value={firstDefined(vm.memory.size, null)} onChange={this._setMemory} /> <Icon icon='memory' />
</span>
</Col>
<Col largeSize={4} className={styles.itemExpanded}>

View File

@@ -0,0 +1,77 @@
import _ from 'intl'
import Component from 'base-component'
import Ellipsis, { EllipsisContainer } from 'ellipsis'
import forEach from 'lodash/forEach'
import Icon from 'icon'
import isEmpty from 'lodash/isEmpty'
import map from 'lodash/map'
import React from 'react'
import SingleLineRow from 'single-line-row'
import Tooltip from 'tooltip'
import { BlockLink } from 'link'
import { Col } from 'grid'
import { connectStore } from 'utils'
import { createGetObject } from 'selectors'
import { editVmGroup } from 'xo'
import { Text } from 'editable'
import styles from './index.css'
@connectStore(() => {
return (state, props) => {
const vms = {}
forEach(props.item.$VMs, vmId => {
const getVM = createGetObject(() => vmId)
vms[vmId] = getVM(state, props)
})
return {
vms
}
}
})
export default class VmGroupItem extends Component {
toggleState = stateField => () => this.setState({ [stateField]: !this.state[stateField] })
_onSelect = () => this.props.onSelect(this.props.item.id)
_setNameDescription = description => editVmGroup(this.props.item, {name_description: description})
_setNameLabel = label => editVmGroup(this.props.item, {name_label: label})
_getVmGroupState = (vmGroup) => {
const states = map(this.props.vms, vm => vm.power_state)
return isEmpty(states)
? 'Busy'
: states.indexOf('Halted') === -1
? states[0]
: states.indexOf('Running') === -1
? states[0]
: 'Busy'
}
render () {
const { item: vmGroup, selected, vms } = this.props
return <div className={styles.item}>
<BlockLink to={`/vm-group/${vmGroup.id}`}>
<SingleLineRow>
<Col smallSize={10} mediumSize={9} largeSize={3}>
<EllipsisContainer>
<input type='checkbox' checked={selected} onChange={this._onSelect} value={vmGroup.id} />
&nbsp;&nbsp;
<Ellipsis>
<Tooltip content={_(`powerStateVmGroup${this._getVmGroupState(vmGroup)}`)}>
<Icon icon={isEmpty(vms) ? 'halted' : `${this._getVmGroupState(vmGroup).toLowerCase()}`} />
</Tooltip>
<Text value={vmGroup.name_label} onChange={this._setNameLabel} useLongClick />
</Ellipsis>
&nbsp;&nbsp;
</EllipsisContainer>
</Col>
<Col mediumSize={4} className='hidden-md-down'>
<EllipsisContainer>
<Ellipsis>
<Text value={vmGroup.name_description} onChange={this._setNameDescription} useLongClick />
</Ellipsis>
</EllipsisContainer>
</Col>
</SingleLineRow>
</BlockLink>
</div>
}
}

View File

@@ -54,6 +54,11 @@ export default class VmItem extends Component {
return vm && vm.power_state === 'Running'
}
_getMigrationPredicate = createSelector(
() => this.props.container,
container => host => host.id !== container.id
)
_getResourceSet = createFinder(
() => this.props.resourceSets,
createSelector(
@@ -139,6 +144,7 @@ export default class VmItem extends Component {
labelProp='name_label'
onChange={this._migrateVm}
placeholder={_('homeMigrateTo')}
predicate={this._getMigrationPredicate()}
useLongClick
value={container}
xoType='host'

View File

@@ -1,5 +1,4 @@
import _ from 'intl'
import ActionBar, { Action } from 'action-bar'
import ActionBar from 'action-bar'
import React from 'react'
import {
// disableHost,
@@ -13,42 +12,44 @@ import {
const hostActionBarByState = {
Running: ({ host }) => (
<ActionBar
actions={[
{
icon: 'host-stop',
label: 'stopHostLabel',
handler: stopHost
},
{
icon: 'host-restart-agent',
label: 'restartHostAgent',
handler: restartHostAgent
},
{
icon: 'host-emergency-shutdown',
label: 'emergencyModeLabel',
handler: emergencyShutdownHost
},
{
icon: 'host-reboot',
label: 'rebootHostLabel',
handler: restartHost
}
]}
display='icon'
handlerParam={host}
>
<Action
handler={stopHost}
icon='host-stop'
label={_('stopHostLabel')}
/>
<Action
handler={restartHostAgent}
icon='host-restart-agent'
label={_('restartHostAgent')}
/>
<Action
handler={emergencyShutdownHost}
icon='host-emergency-shutdown'
label={_('emergencyModeLabel')}
/>
<Action
handler={restartHost}
icon='host-reboot'
label={_('rebootHostLabel')}
/>
</ActionBar>
param={host}
/>
),
Halted: ({ host }) => (
<ActionBar
actions={[
{
icon: 'host-start',
label: 'startHostLabel',
handler: startHost
}
]}
display='icon'
handlerParam={host}
>
<Action
handler={startHost}
icon='host-start'
label={_('startHostLabel')}
/>
</ActionBar>
param={host}
/>
)
}

View File

@@ -178,7 +178,12 @@ export default class Host extends Component {
componentDidMount () {
this.loop()
this._subscribePatches(this.props.host)
this.unsubscribeHostMissingPatches = subscribeHostMissingPatches(
this.props.routeParams.id,
missingPatches => this.setState({
missingPatches: sortBy(missingPatches, patch => -patch.time)
})
)
}
componentWillUnmount () {
@@ -188,17 +193,14 @@ export default class Host extends Component {
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)
const hostCur = this.props.host
if (hostCur && !hostNext) {
this.context.router.push('/')
}
if (!isRunning(hostCur) && isRunning(hostNext)) {
this.loop(hostNext)
@@ -209,19 +211,6 @@ export default class Host extends Component {
}
}
_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)
@@ -266,6 +255,7 @@ export default class Host extends Component {
</div>
</Col>
</Row>
<br />
<Row>
<Col>
<NavTabs>

View File

@@ -4,15 +4,10 @@ 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 { enableHost, detachHost, disableHost, restartHost, installSupplementalPack } from 'xo'
import { FormattedRelative, FormattedTime } from 'react-intl'
import { Container, Row, Col } from 'grid'
import {
createGetObjectsOfType,
createSelector
} from 'selectors'
import {
map
} from 'lodash'
@@ -27,26 +22,8 @@ const formatPack = ({ name, author, description, version }) => <tr>
<td>{version}</td>
</tr>
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
export default ({
host
}) => <Container>
<Row>
<Col className='text-xs-right'>
@@ -82,15 +59,6 @@ export default connectStore(() => {
icon='host-eject'
labelId='detachHost'
/>
{host.power_state !== 'Running' &&
<TabButton
btnStyle='danger'
handler={forgetHost}
handlerParam={host}
icon='host-forget'
labelId='forgetHost'
/>
}
</Col>
</Row>
<Row>
@@ -135,7 +103,7 @@ export default connectStore(() => {
</tr>
<tr>
<th>{_('hostXenServerVersion')}</th>
<Copiable tagName='td' data={host.version}>
<Copiable tagName='td'>
{host.license_params.sku_marketing_name} {host.version} ({host.license_params.sku_type})
</Copiable>
</tr>
@@ -163,10 +131,6 @@ export default connectStore(() => {
{host.CPUs.modelname}
</Copiable>
</tr>
<tr>
<th>{_('hostGpus')}</th>
<td>{map(pgpus, pgpu => pcis[pgpu.pci].device_name).join(', ')}</td>
</tr>
<tr>
<th>{_('hostCpusNumber')}</th>
<td>{host.cpus.cores} ({host.cpus.sockets})</td>
@@ -224,4 +188,4 @@ export default connectStore(() => {
]}
</Col>
</Row>
</Container>)
</Container>

View File

@@ -92,6 +92,7 @@ export default class extends Component {
<Row className='console'>
<Col>
<NoVnc ref='noVnc' url={resolveUrl(`consoles/${vmController.id}`)} onClipboardChange={this._getRemoteClipboard} />
<p><em><Icon icon='info' /> {_('tipLabel')} {_('tipConsoleLabel')}</em></p>
</Col>
</Row>
</Container>

View File

@@ -90,8 +90,6 @@ class ConfigureIpModal extends Component {
vifsByNetwork: createGetObjectsOfType('VIF').groupBy('$network')
}))
class PifItem extends Component {
state = { configModes: [] }
componentWillMount () {
getIpv4ConfigModes().then(configModes =>
this.setState({ configModes })
@@ -128,7 +126,7 @@ class PifItem extends Component {
const pifInUse = some(vifsByNetwork[pif.$network], vif => vif.attached)
return <tr>
return <tr key={pif.id}>
<td>{pif.device}</td>
<td>{networks[pif.$network].name_label}</td>
<td>
@@ -240,7 +238,7 @@ export default ({
</tr>
</thead>
<tbody>
{map(pifs, pif => <PifItem key={pif.id} pif={pif} networks={networks} />)}
{map(pifs, pif => <PifItem pif={pif} networks={networks} />)}
</tbody>
</table>
</span>

View File

@@ -36,6 +36,7 @@ import Sr from './sr'
import Tasks from './tasks'
import User from './user'
import Vm from './vm'
import VmGroup from './vm-group'
import VmImport from './vm-import'
import XoaUpdates from './xoa-updates'
import Xosan from './xosan'
@@ -86,6 +87,7 @@ const BODY_STYLE = {
'vms/import': VmImport,
'vms/new': NewVm,
'vms/:id': Vm,
'vm-group/:id': VmGroup,
'xoa-update': XoaUpdates,
'xosan': Xosan
})

View File

@@ -381,7 +381,7 @@ export default class Jobs extends Component {
/>
<input type='text' ref='name' className='form-control mb-1 mt-1' placeholder={formatMessage(messages.jobNamePlaceholder)} pattern='[^_]+' required />
<SelectPlainObject ref='method' options={actions} optionKey='method' onChange={this._handleSelectMethod} placeholder={_('jobActionPlaceHolder')} />
<input type='number' onChange={this.linkState('timeout')} value={state.timeout || ''} className='form-control mb-1 mt-1' placeholder={formatMessage(messages.jobTimeoutPlaceHolder)} />
<input type='number' onChange={this.linkState('timeout')} value={state.timeout} className='form-control mb-1 mt-1' placeholder='Job timeout (seconds)' />
{action && <fieldset>
<GenericInput ref='params' schema={action.info} uiSchema={action.uiSchema} label={action.method} required />
{job && <p className='text-warning'>{_('jobEditMessage', { name: job.name, id: job.id.slice(4, 8) })}</p>}

View File

@@ -1,37 +1,30 @@
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 forEach from 'lodash/forEach'
import get from 'lodash/get'
import Icon from 'icon'
import NoObjects from 'no-objects'
import includes from 'lodash/includes'
import map from 'lodash/map'
import orderBy from 'lodash/orderBy'
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 { connectStore } from 'utils'
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
@@ -59,9 +52,9 @@ class JobParam extends Component {
id
} = this.props
return object != null
? _.keyValue(object.type || paramKey, renderXoItem(object))
: _.keyValue(paramKey, String(id))
return object
? <span><strong>{object.type || paramKey}</strong>: {renderXoItem(object)} </span>
: <span><strong>{paramKey}:</strong> {String(id)} </span>
}
}
@@ -77,98 +70,33 @@ class JobReturn extends Component {
}
}
const JobCallStateInfos = ({ end, error }) => {
const [ icon, tooltip ] = error !== undefined
? ['halted', 'failedJobCall']
: end !== undefined
? ['running', 'successfulJobCall']
: ['busy', 'jobCallInProgess']
const Log = props => <ul className='list-group'>
{map(props.log.calls, call => {
const { returnedValue } = call
let id
if (returnedValue != null) {
id = returnedValue.id
if (id === undefined && typeof returnedValue === 'string') {
id = returnedValue
}
}
return <Tooltip content={_(tooltip)}>
<Icon icon={icon} />
</Tooltip>
}
const JobTransferredDataInfos = ({ start, end, size }) => <div>
<span><strong>{_('jobTransferredDataSize')}</strong> {formatSize(size)}</span>
<br />
<span><strong>{_('jobTransferredDataSpeed')}</strong> {formatSpeed(size, end - start)}</span>
</div>
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
}
class Log extends BaseComponent {
state = {
filter: 'all'
}
render () {
const { props, state } = this
const predicate = PREDICATES[state.filter]
return <div>
<select
className='form-control'
onChange={this.linkState('filter')}
value={state.filter}
>
{map(CALL_FILTER_OPTIONS, ({ label, value }) => _(
{ key: value },
label,
message => <option value={value}>{message}</option>
))}
</select>
<br />
<ul className='list-group'>
{map(props.log.calls, call => {
const {
end,
error,
returnedValue,
start
} = call
let id
if (returnedValue != null) {
id = returnedValue.id
if (id === undefined && typeof returnedValue === 'string') {
id = returnedValue
}
return <li key={call.callKey} className='list-group-item'>
<strong className='text-info'>{call.method}: </strong><br />
{map(call.params, (value, key) => [ <JobParam id={value} paramKey={key} key={key} />, <br /> ])}
{id !== undefined && <span>{' '}<JobReturn id={id} /></span>}
{call.error &&
<span className='text-danger'>
<Icon icon='error' />
{' '}
{call.error.message
? <strong>{call.error.message}</strong>
: JSON.stringify(call.error)
}
return predicate(call) && <li key={call.callKey} className='list-group-item'>
<strong className='text-info'>{call.method}: </strong><JobCallStateInfos end={end} error={error} /><br />
{map(call.params, (value, key) => [ <JobParam id={value} paramKey={key} key={key} />, <br /> ])}
{end !== undefined && _.keyValue(_('jobDuration'), <FormattedDuration duration={end - start} />)}
{returnedValue != null && returnedValue.size !== undefined && <JobTransferredDataInfos start={start} end={end} size={returnedValue.size} />}
{id !== undefined && <span>{' '}<JobReturn id={id} /></span>}
{call.error &&
<span className='text-danger'>
<Icon icon='error' />
{' '}
{call.error.message
? <strong>{call.error.message}</strong>
: JSON.stringify(call.error)
}
</span>}
</li>
})}
</ul>
</div>
}
}
</span>}
</li>
})}
</ul>
const showCalls = log => alert(_('jobModalTitle', { job: log.jobId }), <Log log={log} />)
@@ -237,6 +165,7 @@ export default class LogList extends Component {
constructor (props) {
super(props)
this.state = {
logs: [],
logsToClear: []
}
this.filters = {
@@ -279,7 +208,6 @@ export default class LogList extends Component {
callKey: logKey,
params: data.params,
method: data.method,
start: time,
time
}
} else if (data.event === 'jobCall.end') {
@@ -291,7 +219,6 @@ export default class LogList extends Component {
entry.meta = 'error'
} else {
call.returnedValue = data.returnedValue
call.end = time
}
}
}
@@ -326,12 +253,12 @@ export default class LogList extends Component {
return (
<Card>
<CardHeader>
<Icon icon='log' /> Logs<span className='pull-right'><ActionButton disabled={isEmpty(logs)} btnStyle='danger' handler={this._deleteAllLogs} icon='delete' /></span>
<Icon icon='log' /> Logs<span className='pull-right'><ActionButton disabled={!logs.length} btnStyle='danger' handler={this._deleteAllLogs} icon='delete' /></span>
</CardHeader>
<CardBlock>
<NoObjects collection={logs} emptyMessage={_('noLogs')}>
<SortedTable collection={logs} columns={LOG_COLUMNS} filters={this.filters} />
</NoObjects>
{logs.length
? <SortedTable collection={logs} columns={LOG_COLUMNS} filters={this.filters} />
: <p>{_('noLogs')}</p>}
</CardBlock>
</Card>
)

View File

@@ -127,6 +127,7 @@ export default class Menu extends Component {
const items = [
{ to: '/home', icon: 'menu-home', label: 'homePage', subMenu: [
{ to: '/home?t=VM', icon: 'vm', label: 'homeVmPage' },
{ to: '/home?t=VmGroup', icon: 'vm', label: 'homeVmGroupPage' },
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' },
@@ -166,7 +167,8 @@ export default class Menu extends Component {
{ 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' },
{ to: '/vms/new', icon: 'menu-new-vm', label: 'newVmPage' },
{ to: '/new/vm-group', icon: 'menu-new-vm', label: 'newVmGroupPage' },
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' }

View File

@@ -3,7 +3,7 @@ import ActionButton from 'action-button'
import BaseComponent from 'base-component'
import Button from 'button'
import classNames from 'classnames'
import defined from 'xo-defined'
import DebounceInput from 'react-debounce-input'
import Icon from 'icon'
import isIp from 'is-ip'
import Page from '../page'
@@ -14,10 +14,6 @@ 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-component-decorator'
import { Limits } from 'usage'
import {
clamp,
@@ -60,7 +56,6 @@ import {
SelectSr,
SelectSshKey,
SelectVdi,
SelectVgpuType,
SelectVmTemplate
} from 'select-objects'
import {
@@ -71,13 +66,14 @@ import {
addSubscriptions,
buildTemplate,
connectStore,
firstDefined,
formatSize,
getCoresPerSocketPossibilities,
generateReadableRandomString,
noop,
resolveResourceSet
} from 'utils'
import {
createFilter,
createSelector,
createGetObject,
createGetObjectsOfType,
@@ -86,6 +82,7 @@ import {
import styles from './index.css'
const DEBOUNCE_TIMEOUT = 300
const NB_VMS_MIN = 2
const NB_VMS_MAX = 100
@@ -93,6 +90,8 @@ const NB_VMS_MAX = 100
const getObject = createGetObject((_, id) => id)
const returnTrue = () => true
// Sub-components
const SectionContent = ({ column, children }) => (
@@ -138,6 +137,7 @@ class Vif extends BaseComponent {
<Item label={_('newVmMacLabel')}>
<DebounceInput
className='form-control'
debounceTimeout={DEBOUNCE_TIMEOUT}
onChange={onChangeMac}
placeholder={formatMessage(messages.newVmMacPlaceholder)}
rows={7}
@@ -240,6 +240,9 @@ export default class NewVm extends BaseComponent {
// Utils -----------------------------------------------------------------------
getUniqueId () {
return this._uniqueId++
}
get _isDiskTemplate () {
const { template } = this.state.state
return template &&
@@ -373,9 +376,7 @@ export default class NewVm extends BaseComponent {
share: state.share,
cloudConfig,
coreOs: state.template.name_label === 'CoreOS',
tags: state.tags,
vgpuType: state.vgpuType.id,
gpuGroup: state.vgpuType.gpuGroup
tags: state.tags
}
return state.multipleVms ? createVms(data, state.nameLabels) : createVm(data)
@@ -402,7 +403,7 @@ export default class NewVm extends BaseComponent {
}
const vdi = getObject(storeState, vbd.VDI, resourceSet)
if (vdi) {
existingDisks[vbd.position] = {
existingDisks[this.getUniqueId()] = {
name_label: vdi.name_label,
name_description: vdi.name_description,
size: vdi.size,
@@ -417,6 +418,7 @@ export default class NewVm extends BaseComponent {
forEach(template.VIFs, vifId => {
const vif = getObject(storeState, vifId, resourceSet)
VIFs.push({
id: this.getUniqueId(),
network: pool || isInResourceSet(vif.$network)
? vif.$network
: resourceSet.objectsByType['network'][0].id
@@ -425,6 +427,7 @@ export default class NewVm extends BaseComponent {
if (VIFs.length === 0) {
const networkId = this._getDefaultNetworkId()
VIFs.push({
id: this.getUniqueId(),
network: networkId
})
}
@@ -451,10 +454,12 @@ export default class NewVm extends BaseComponent {
// disks
existingDisks,
VDIs: map(template.template_info.disks, disk => {
const device = String(this.getUniqueId())
return {
...disk,
device,
name_description: disk.name_description || 'Created by XO',
name_label: (name_label || 'disk') + '_' + generateReadableRandomString(5),
name_label: (name_label || 'disk') + '_' + device,
SR: pool
? pool.default_SR
: resourceSet.objectsByType['SR'][0].id
@@ -488,6 +493,13 @@ export default class NewVm extends BaseComponent {
objectsIds => id => includes(objectsIds, id)
)
_getCanOperate = createSelector(
() => this.props.isAdmin,
() => this.props.permissions,
(isAdmin, permissions) => isAdmin
? returnTrue
: ({ id }) => permissions && permissions[id] && permissions[id].operate
)
_getVmPredicate = createSelector(
this._getIsInPool,
this._getIsInResourceSet,
@@ -536,7 +548,11 @@ export default class NewVm extends BaseComponent {
},
(networks, poolId) => filter(networks, network => network.$pool === poolId)
)
_getOperatablePools = createFilter(
() => this.props.pools,
this._getCanOperate,
[ (pool, canOperate) => canOperate(pool) ]
)
_getAffinityHostPredicate = createSelector(
() => this.props.pool,
() => this.state.state.existingDisks,
@@ -577,11 +593,6 @@ export default class NewVm extends BaseComponent {
})
)
_getVgpuTypePredicate = createSelector(
() => this.props.pool,
pool => vgpuType => pool !== undefined && pool.id === vgpuType.$pool
)
_getCoresPerSocketPossibilities = createSelector(
() => {
const { pool } = this.props
@@ -644,10 +655,12 @@ export default class NewVm extends BaseComponent {
_addVdi = () => {
const { state } = this.state
const { pool } = this.props
const device = String(this.getUniqueId())
this._setState({ VDIs: [ ...state.VDIs, {
device,
name_description: 'Created by XO',
name_label: (state.name_label || 'disk') + '_' + generateReadableRandomString(5),
name_label: (state.name_label || 'disk') + '_' + device,
SR: pool && pool.default_SR,
type: 'system'
}] })
@@ -661,6 +674,7 @@ export default class NewVm extends BaseComponent {
const networkId = this._getDefaultNetworkId()
this._setState({ VIFs: [ ...this.state.state.VIFs, {
id: this.getUniqueId(),
network: networkId
}] })
}
@@ -695,10 +709,13 @@ export default class NewVm extends BaseComponent {
// MAIN ------------------------------------------------------------------------
_renderHeader = () => {
const {isAdmin, pool, resourceSets} = this.props
const { pool } = this.props
const showSelectPool = !isEmpty(this._getOperatablePools())
const showSelectResourceSet = !this.props.isAdmin && !isEmpty(this.props.resourceSets)
const selectPool = <span className={styles.inlineSelect}>
<SelectPool
onChange={this._selectPool}
predicate={this._getCanOperate()}
value={pool}
/>
</span>
@@ -712,9 +729,14 @@ export default class NewVm extends BaseComponent {
<Row>
<Col mediumSize={12}>
<h2>
{isAdmin || !isEmpty(resourceSets)
{showSelectPool && showSelectResourceSet
? _('newVmCreateNewVmOn2', {
select1: selectPool,
select2: selectResourceSet
})
: showSelectPool || showSelectResourceSet
? _('newVmCreateNewVmOn', {
select: isAdmin ? selectPool : selectResourceSet
select: showSelectPool ? selectPool : selectResourceSet
})
: _('newVmCreateNewVmNoPermission')
}
@@ -797,6 +819,7 @@ export default class NewVm extends BaseComponent {
<Item label={_('newVmNameLabel')}>
<DebounceInput
className='form-control'
debounceTimeout={DEBOUNCE_TIMEOUT}
onChange={this._linkState('name_label')}
value={name_label}
/>
@@ -804,6 +827,7 @@ export default class NewVm extends BaseComponent {
<Item label={_('newVmDescriptionLabel')}>
<DebounceInput
className='form-control'
debounceTimeout={DEBOUNCE_TIMEOUT}
onChange={this._linkState('name_description')}
value={name_description}
/>
@@ -824,6 +848,7 @@ export default class NewVm extends BaseComponent {
<Item label={_('newVmVcpusLabel')}>
<DebounceInput
className='form-control'
debounceTimeout={DEBOUNCE_TIMEOUT}
min={0}
onChange={this._linkState('CPUs')}
type='number'
@@ -834,7 +859,7 @@ export default class NewVm extends BaseComponent {
<SizeInput
className={styles.sizeInput}
onChange={this._linkState('memoryDynamicMax')}
value={defined(memoryDynamicMax, null)}
value={firstDefined(memoryDynamicMax, null)}
/>
</Item>
<Item label={_('vmCpuTopology')}>
@@ -917,6 +942,7 @@ export default class NewVm extends BaseComponent {
<DebounceInput
className='form-control'
disabled={!configDrive || installMethod !== 'SSH'}
debounceTimeout={DEBOUNCE_TIMEOUT}
onChange={this._linkState('newSshKey')}
value={newSshKey}
/>
@@ -947,9 +973,11 @@ export default class NewVm extends BaseComponent {
&nbsp;
<span>{_('newVmCustomConfig')}</span>
&nbsp;
<DebounceTextarea
<DebounceInput
className={classNames('form-control', styles.customConfig)}
debounceTimeout={DEBOUNCE_TIMEOUT}
disabled={!configDrive || installMethod !== 'customConfig'}
element='textarea'
onChange={this._linkState('customConfig')}
value={customConfig}
/>
@@ -1000,6 +1028,7 @@ export default class NewVm extends BaseComponent {
{' '}
<DebounceInput
className='form-control'
debounceTimeout={DEBOUNCE_TIMEOUT}
disabled={installMethod !== 'network'}
key='networkInput'
onChange={this._linkState('installNetwork')}
@@ -1010,6 +1039,7 @@ export default class NewVm extends BaseComponent {
<Item label={_('newVmPvArgsLabel')} key='pv'>
<DebounceInput
className='form-control'
debounceTimeout={DEBOUNCE_TIMEOUT}
onChange={this._linkState('pv_args')}
value={pv_args}
/>
@@ -1030,8 +1060,10 @@ export default class NewVm extends BaseComponent {
</SectionContent>}
{template.name_label === 'CoreOS' && <div>
<label>{_('newVmCloudConfig')}</label>
<DebounceTextarea
<DebounceInput
className='form-control'
debounceTimeout={DEBOUNCE_TIMEOUT}
element='textarea'
onChange={this._linkState('cloudConfig')}
rows={7}
value={cloudConfig}
@@ -1132,6 +1164,7 @@ export default class NewVm extends BaseComponent {
<Item label={_('newVmNameLabel')}>
<DebounceInput
className='form-control'
debounceTimeout={DEBOUNCE_TIMEOUT}
onChange={this._linkState(`existingDisks.${index}.name_label`)}
value={disk.name_label}
/>
@@ -1139,6 +1172,7 @@ export default class NewVm extends BaseComponent {
<Item label={_('newVmDescriptionLabel')}>
<DebounceInput
className='form-control'
debounceTimeout={DEBOUNCE_TIMEOUT}
onChange={this._linkState(`existingDisks.${index}.name_description`)}
value={disk.name_description}
/>
@@ -1148,7 +1182,7 @@ export default class NewVm extends BaseComponent {
className={styles.sizeInput}
onChange={this._linkState(`existingDisks.${index}.size`)}
readOnly={!configDrive}
value={defined(disk.size, null)}
value={firstDefined(disk.size, null)}
/>
</Item>
</LineItem>
@@ -1156,7 +1190,7 @@ export default class NewVm extends BaseComponent {
</div>)}
{/* VDIs */}
{map(VDIs, (vdi, index) => <div key={index}>
{map(VDIs, (vdi, index) => <div key={vdi.device}>
<LineItem>
<Item label={_('newVmSrLabel')}>
<span className={styles.inlineSelect}>
@@ -1176,6 +1210,7 @@ export default class NewVm extends BaseComponent {
<Item label={_('newVmNameLabel')}>
<DebounceInput
className='form-control'
debounceTimeout={DEBOUNCE_TIMEOUT}
onChange={this._linkState(`VDIs.${index}.name_label`)}
value={vdi.name_label}
/>
@@ -1183,6 +1218,7 @@ export default class NewVm extends BaseComponent {
<Item label={_('newVmDescriptionLabel')}>
<DebounceInput
className='form-control'
debounceTimeout={DEBOUNCE_TIMEOUT}
onChange={this._linkState(`VDIs.${index}.name_description`)}
value={vdi.name_description}
/>
@@ -1191,7 +1227,7 @@ export default class NewVm extends BaseComponent {
<SizeInput
className={styles.sizeInput}
onChange={this._linkState(`VDIs.${index}.size`)}
value={defined(vdi.size, null)}
value={firstDefined(vdi.size, null)}
/>
</Item>
<Item>
@@ -1238,10 +1274,8 @@ export default class NewVm extends BaseComponent {
seqStart,
share,
showAdvanced,
tags,
template
tags
} = this.state.state
const { isAdmin } = this.props
const { formatMessage } = this.props.intl
return <Section icon='new-vm-advanced' title='newVmAdvancedPanel' done={this._isAdvancedDone()}>
<SectionContent column>
@@ -1289,6 +1323,7 @@ export default class NewVm extends BaseComponent {
<Item label={_('newVmCpuWeightLabel')}>
<DebounceInput
className='form-control'
debounceTimeout={DEBOUNCE_TIMEOUT}
min={0}
max={65535}
onChange={this._linkState('cpuWeight')}
@@ -1300,6 +1335,7 @@ export default class NewVm extends BaseComponent {
<Item label={_('newVmCpuCapLabel')}>
<DebounceInput
className='form-control'
debounceTimeout={DEBOUNCE_TIMEOUT}
min={0}
onChange={this._linkState('cpuCap')}
placeholder={formatMessage(messages.newVmDefaultCpuCap, { value: XEN_DEFAULT_CPU_CAP })}
@@ -1310,13 +1346,13 @@ export default class NewVm extends BaseComponent {
</SectionContent>,
<SectionContent>
<Item label={_('newVmDynamicMinLabel')}>
<SizeInput value={defined(memoryDynamicMin, null)} onChange={this._linkState('memoryDynamicMin')} className={styles.sizeInput} />
<SizeInput value={firstDefined(memoryDynamicMin, null)} onChange={this._linkState('memoryDynamicMin')} className={styles.sizeInput} />
</Item>
<Item label={_('newVmDynamicMaxLabel')}>
<SizeInput value={defined(memoryDynamicMax, null)} onChange={this._linkState('memoryDynamicMax')} className={styles.sizeInput} />
<SizeInput value={firstDefined(memoryDynamicMax, null)} onChange={this._linkState('memoryDynamicMax')} className={styles.sizeInput} />
</Item>
<Item label={_('newVmStaticMaxLabel')}>
<SizeInput value={defined(memoryStaticMax, null)} onChange={this._linkState('memoryStaticMax')} className={styles.sizeInput} />
<SizeInput value={firstDefined(memoryStaticMax, null)} onChange={this._linkState('memoryStaticMax')} className={styles.sizeInput} />
</Item>
</SectionContent>,
<SectionContent>
@@ -1326,6 +1362,7 @@ export default class NewVm extends BaseComponent {
<Item label={_('newVmMultipleVmsPattern')}>
<DebounceInput
className='form-control'
debounceTimeout={DEBOUNCE_TIMEOUT}
disabled={!multipleVms}
onChange={this._linkState('namePattern')}
placeholder={formatMessage(messages.newVmMultipleVmsPatternPlaceholder)}
@@ -1335,6 +1372,7 @@ export default class NewVm extends BaseComponent {
<Item label={_('newVmFirstIndex')}>
<DebounceInput
className={'form-control'}
debounceTimeout={DEBOUNCE_TIMEOUT}
disabled={!multipleVms}
onChange={this._linkState('seqStart')}
type='number'
@@ -1344,6 +1382,7 @@ export default class NewVm extends BaseComponent {
<Item className='input-group'>
<DebounceInput
className='form-control'
debounceTimeout={DEBOUNCE_TIMEOUT}
disabled={!multipleVms}
max={NB_VMS_MAX}
min={NB_VMS_MIN}
@@ -1372,7 +1411,7 @@ export default class NewVm extends BaseComponent {
)}
</LineItem>}
</SectionContent>,
isAdmin && <SectionContent>
<SectionContent>
<Item label={_('newVmAffinityHost')}>
<SelectHost
onChange={this._linkState('affinityHost')}
@@ -1380,14 +1419,6 @@ export default class NewVm extends BaseComponent {
value={affinityHost}
/>
</Item>
</SectionContent>,
template && template.virtualizationMode === 'hvm' && <SectionContent>
<Item label={_('vmVgpu')}>
<SelectVgpuType
onChange={this._linkState('vgpuType')}
predicate={this._getVgpuTypePredicate()}
/>
</Item>
</SectionContent>
]}
</Section>

View File

@@ -1,9 +1,11 @@
import { routes } from 'utils'
import Sr from './sr'
import VmGroup from './vm-group'
const New = routes('vm', {
sr: Sr
sr: Sr,
'vm-group': VmGroup
})(
({ children }) => children
)

View File

@@ -46,46 +46,45 @@ import {
options: propTypes.array.isRequired
})
class SelectIqn extends Component {
_getOptions = createSelector(
() => this.props.options,
options => map(options, ({ ip, iqn }, index) => ({
label: `${iqn} (${ip})`,
value: index
_computeOptions (props = this.props) {
this.setState({
options: map(props.options, (iqn, id) => ({
value: `${iqn.ip}$${iqn.iqn}`,
label: `${iqn.iqn} (${iqn.ip})`
}))
})
}
_handleChange = value => {
const { onChange } = this.props
value = value.value
const index = value.indexOf('$')
this.setState({
value
}, () => onChange({
ip: value.slice(0, index),
iqn: value.slice(index + 1)
}))
)
_handleChange = ({ value }) => {
const { props } = this
this.setState(
{ value },
() => props.onChange(props.options[value])
)
}
componentDidMount () {
return this.componentDidUpdate()
componentWillMount () {
this._computeOptions()
}
componentDidUpdate () {
let options
if (
this.state.value === null &&
(options = this._getOptions()).length === 1
) {
this._handleChange(options[0])
}
componentWillReceiveProps (props) {
this._computeOptions(props)
}
state = { value: null }
render () {
const { state } = this
return (
<Select
clearable={false}
onChange={this._handleChange}
options={this._getOptions()}
value={this.state.value}
options={state.options}
value={state.value}
/>
)
}
@@ -96,45 +95,37 @@ class SelectIqn extends Component {
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])
)
_computeOptions (props = this.props) {
this.setState({
options: map(props.options, lun => ({
value: lun.id,
label: `LUN ${lun.id}: ${lun.serial} - ${formatSize(+lun.size)} - (${lun.vendor})`
}))
})
}
componentDidMount () {
return this.componentDidUpdate()
_handleChange = value => {
const { onChange, options } = this.props
value = value.value
this.setState({ value }, () => onChange(options[value]))
}
componentDidUpdate () {
let options
if (
this.state.value === null &&
(options = this._getOptions()).length === 1
) {
this._handleChange(options[0])
}
componentWillMount () {
this._computeOptions()
}
state = { value: null }
componentWillReceiveProps (props) {
this._computeOptions(props)
}
render () {
const { state } = this
return (
<Select
clearable={false}
onChange={this._handleChange}
options={this._getOptions()}
value={this.state.value}
options={state.options}
value={state.value}
/>
)
}
@@ -181,7 +172,6 @@ export default class New extends Component {
host: hostId && getObject(store.getState(), hostId),
iqn: undefined,
iqns: undefined,
loading: 0,
lockCreation: undefined,
lun: undefined,
luns: undefined,
@@ -289,7 +279,7 @@ export default class New extends Component {
} = this.state
try {
this.setState(({ loading }) => ({ loading: loading + 1 }))
this.setState({loading: true})
const luns = await probeSrIscsiLuns(host.id, iqn.ip, iqn.iqn, username && username.value, password && password.value)
this.setState({
iqn,
@@ -298,7 +288,7 @@ export default class New extends Component {
} catch (err) {
error('LUNs Detection', err.message || String(err))
} finally {
this.setState(({ loading }) => ({ loading: loading - 1 }))
this.setState({loading: undefined})
}
}
@@ -314,7 +304,7 @@ export default class New extends Component {
} = this.state
try {
this.setState(({ loading }) => ({ loading: loading + 1 }))
this.setState({loading: true})
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))
@@ -329,7 +319,7 @@ export default class New extends Component {
} catch (err) {
error('iSCSI Error', err.message || String(err))
} finally {
this.setState(({ loading }) => ({ loading: loading - 1 }))
this.setState({loading: undefined})
}
}
@@ -385,7 +375,7 @@ export default class New extends Component {
} = this.state
try {
this.setState(({ loading }) => ({ loading: loading + 1 }))
this.setState({loading: true})
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))
@@ -400,7 +390,7 @@ export default class New extends Component {
} catch (err) {
error('NFS Error', err.message || String(err))
} finally {
this.setState(({ loading }) => ({ loading: loading - 1 }))
this.setState({loading: undefined})
}
}
@@ -501,7 +491,7 @@ export default class New extends Component {
>
<option value={null}>{formatMessage(messages.noSelectedValue)}</option>
{map(typeGroups, (types, group) =>
<optgroup key={group} label={SR_GROUP_TO_LABEL[group]}>
<optgroup label={SR_GROUP_TO_LABEL[group]}>
{map(types, type =>
<option key={type} value={type}>{SR_TYPE_TO_LABEL[type]}</option>
)}
@@ -667,7 +657,7 @@ export default class New extends Component {
}
</fieldset>
}
{loading !== 0 &&
{loading &&
<Icon icon='loading' />
}
</Section>

View File

@@ -0,0 +1,153 @@
import _ from 'intl'
import ActionButton from 'action-button'
import BaseComponent from 'base-component'
import classNames from 'classnames'
import DebounceInput from 'react-debounce-input'
import Icon from 'icon'
import Page from '../../page'
import React from 'react'
import Wizard, { Section } from 'wizard'
import { Container, Row, Col } from 'grid'
import {
createVmGroup,
subscribeCurrentUser,
subscribePermissions,
subscribeResourceSets
} from 'xo'
import {
createSelector,
createGetObjectsOfType,
getUser
} from 'selectors'
import { SelectPool } from 'select-objects'
import { addSubscriptions, connectStore } from 'utils'
import styles from '../../new-vm/index.css'
const SectionContent = ({ column, children }) => (
<div className={classNames(
'form-inline',
styles.sectionContent,
column && styles.sectionContentColumn
)}>
{children}
</div>
)
const Item = ({ label, children, className }) => (
<span className={styles.item}>
{label && <span>{label}&nbsp;</span>}
<span className={classNames(styles.input, className)}>{children}</span>
</span>
)
@addSubscriptions({
resourceSets: subscribeResourceSets,
permissions: subscribePermissions,
user: subscribeCurrentUser
})
@connectStore(() => ({
isAdmin: createSelector(
getUser,
user => user && user.permission === 'admin'
),
pools: createGetObjectsOfType('pool')
}))
export default class VmGroup extends BaseComponent {
static contextTypes = {
router: React.PropTypes.object
}
constructor () {
super()
this.state = {name_label: '', name_description: ''}
}
_selectPool = pool => {
this.setState({ pool })
this._reset()
}
_getCanOperate = createSelector(
() => this.props.isAdmin,
() => this.props.permissions,
(isAdmin, permissions) => isAdmin
? () => true
: ({ id }) => permissions && permissions[id] && permissions[id].operate
)
_renderHeader = () => {
const { pool } = this.state
return <Container>
<Row>
<Col mediumSize={12}>
<h2><Icon icon='sr' /> {_('newVmGroupTitle')}
<SelectPool
onChange={this._selectPool}
predicate={this._getCanOperate()}
value={pool}
/>
</h2>
</Col>
</Row>
</Container>
}
_reset = () => this.setState({poolId: '', name_label: '', name_description: ''})
_create = () => {
createVmGroup(this.state)
this.context.router.push('home?s=&t=VmGroup')
}
_getOnChange = item => ({target}) => this.setState({[item]: target.value})
render () {
const {pool, name_label: nameLabel, name_description: nameDescription} = this.state
return (
<Page header={this._renderHeader()}>
<form id='vmGroupCreation'>
<Wizard>
<Section icon='new-vm-infos' title='newVmGroupInfoPanel'>
<SectionContent>
<Item label={_('newVmGroupNameLabel')}>
<DebounceInput
className='form-control'
// debounceTimeout={DEBOUNCE_TIMEOUT}
onChange={this._getOnChange('name_label')}
value={nameLabel}
/>
</Item>
<Item label={_('newVmGroupDescriptionLabel')}>
<DebounceInput
className='form-control'
// debounceTimeout={DEBOUNCE_TIMEOUT}
onChange={this._getOnChange('name_description')}
value={nameDescription}
/>
</Item>
</SectionContent>
</Section>
</Wizard>
<div className={styles.submitSection}>
<ActionButton
className={styles.button}
handler={this._reset}
icon='new-vm-reset'
>
{_('newVmGroupReset')}
</ActionButton>
<ActionButton
btnStyle='primary'
className={styles.button}
disabled={nameLabel === '' || pool === undefined}
form='vmGroupCreation'
handler={this._create}
icon='new-vm-create'
redirectOnSuccess={this._getRedirectionUrl}
>
{_('newVmGroupCreate')}
</ActionButton>
</div>
</form>
</Page>
)
}
}

View File

@@ -1,78 +1,39 @@
import _ from 'intl'
import ActionBar, { Action } from 'action-bar'
import Component from 'base-component'
import ActionBar from 'action-bar'
import React from 'react'
import { createGetObjectsOfType, createSelector } from 'selectors'
import {
find
} from 'lodash'
import {
addSubscriptions,
connectStore,
noop
} from 'utils'
import {
addHostToPool,
disconnectServer,
subscribeServers
addHostToPool
} 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 <ActionBar
display='icon'
handlerParam={pool}
>
<Action
handler={noop}
icon='add-sr'
label={_('addSrLabel')}
redirectOnSuccess={`new/sr?host=${pool.master}`}
/>
<Action
handler={noop}
icon='add-vm'
label={_('addVmLabel')}
redirectOnSuccess={`vms/new?pool=${pool.id}`}
/>
<Action
handler={addHostToPool}
icon='add-host'
label={_('addHostLabel')}
/>
<Action
handler={this._disconnectServer}
icon='disconnect'
label={_('disconnectServer')}
redirectOnSuccess='/home'
/>
</ActionBar>
}
const NOT_IMPLEMENTED = () => {
throw new Error('not implemented')
}
const PoolActionBar = ({ pool }) => (
<ActionBar
actions={[
{
icon: 'add-sr',
label: 'addSrLabel',
redirectOnSuccess: `new/sr?host=${pool.master}`
},
{
icon: 'add-vm',
label: 'addVmLabel',
redirectOnSuccess: `vms/new?pool=${pool.id}`
},
{
icon: 'add-host',
label: 'addHostLabel',
handler: addHostToPool
},
{
icon: 'disconnect',
label: 'disconnectServer',
handler: NOT_IMPLEMENTED // TODO disconnect server
}
]}
display='icon'
param={pool}
/>
)
export default PoolActionBar

View File

@@ -124,6 +124,7 @@ export default class Pool extends Component {
</div>
</Col>
</Row>
<br />
<Row>
<Col>
<NavTabs>

View File

@@ -1,52 +1,12 @@
import React from 'react'
import _ from 'intl'
import Component from 'base-component'
import Copiable from 'copiable'
import renderXoItem from 'render-xo-item'
import React from 'react'
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'
import { Container, Row, Col } from 'grid'
import { installSupplementalPackOnAllHosts } from 'xo'
@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 <XoSelect
onChange={this._onChange}
predicate={this._getPoolMasterPredicate}
value={pool.master}
xoType='host'
>
{master.name_label}
</XoSelect>
}
}
export default connectStore({
gpuGroups: createGetObjectsOfType('gpuGroup')
})(({
gpuGroups,
export default ({
pool
}) => <div>
<h3 className='mb-1'>{_('xenSettingsLabel')}</h3>
@@ -72,29 +32,9 @@ export default connectStore({
}
</Col>
</Row>
<Row>
<Col size={3}>
<strong>{_('setpoolMaster')}</strong>
</Col>
<Col size={9}>
<PoolMaster pool={pool} />
</Col>
</Row>
</Container>
<h3 className='mt-1 mb-1'>{_('poolGpuGroups')}</h3>
<Container>
<Row>
<Col size={9}>
<ul className='list-group'>
{map(gpuGroups, gpuGroup => <li className='list-group-item'>
{renderXoItem(gpuGroup)}
</li>)}
</ul>
</Col>
</Row>
</Container>
<h3 className='mt-1 mb-1'>{_('supplementalPackPoolNew')}</h3>
<Upgrade place='poolSupplementalPacks' required={2}>
<SelectFiles onChange={file => installSupplementalPackOnAllHosts(pool, file)} />
</Upgrade>
</div>)
</div>

View File

@@ -1,18 +1,14 @@
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, createSelector } from 'selectors'
import { createPager } from 'selectors'
import { FormattedRelative, FormattedTime } from 'react-intl'
import { Container, Row, Col } from 'grid'
import {
ceil,
isEmpty,
map
} from 'lodash'
const LOGS_PER_PAGE = 10
export default class TabLogs extends Component {
constructor () {
@@ -21,12 +17,7 @@ export default class TabLogs extends Component {
this.getLogs = createPager(
() => this.props.logs,
() => this.state.page,
LOGS_PER_PAGE
)
this.getNPages = createSelector(
() => this.props.logs ? this.props.logs.length : 0,
nLogs => ceil(nLogs / LOGS_PER_PAGE)
10
)
this.state = {
@@ -35,12 +26,11 @@ export default class TabLogs extends Component {
}
_deleteAllLogs = () => map(this.props.logs, deleteMessage)
_nextPage = () => this.setState({ page: Math.min(this.state.page + 1, this.getNPages()) })
_previousPage = () => this.setState({ page: Math.max(this.state.page - 1, 1) })
_nextPage = () => this.setState({ page: this.state.page + 1 })
_previousPage = () => this.setState({ page: this.state.page - 1 })
render () {
const logs = this.getLogs()
const { page } = this.state
return <Container>
{isEmpty(logs)
@@ -53,21 +43,15 @@ export default class TabLogs extends Component {
: <div>
<Row>
<Col className='text-xs-right'>
<TabButton
btnStyle='secondary'
disabled={page === 1}
handler={this._previousPage}
icon='previous'
/>
<TabButton
btnStyle='secondary'
disabled={page === this.getNPages()}
handler={this._nextPage}
icon='next'
/>
<Button size='large' onClick={this._previousPage}>
&lt;
</Button>
<Button size='large' onClick={this._nextPage}>
&gt;
</Button>
<TabButton
btnStyle='danger'
handler={this._removeAllLogs} // FIXME: define this method
handler={this._removeAllLogs}
icon='delete'
labelId='logRemoveAll'
/>

View File

@@ -3,7 +3,6 @@ import ActionButton from 'action-button'
import ChartistGraph from 'react-chartist'
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'
@@ -37,6 +36,7 @@ import {
import {
addSubscriptions,
connectStore,
firstDefined,
formatSize,
resolveIds,
resolveResourceSets
@@ -312,15 +312,15 @@ export class Edit extends Component {
// -----------------------------------------------------------------------------
_onChangeIpPool = newIpPool => {
const { ipPools, newIpPoolQuantity } = this.state
_addIpPool = () => {
const { ipPools, newIpPool, newIpPoolQuantity } = this.state
this.setState({
ipPools: [ ...ipPools, { id: newIpPool.id, quantity: newIpPoolQuantity } ],
newIpPool: undefined,
newIpPoolQuantity: ''
})
}
_removeIpPool = index => {
const ipPools = [ ...this.state.ipPools ]
remove(ipPools, (_, i) => index === i)
@@ -444,30 +444,33 @@ export class Edit extends Component {
<Row>
<Col mediumSize={4}>
<Row>
<Col mediumSize={3}>
<strong>{_('quantity')}</strong>
</Col>
<Col mediumSize={7}>
<strong>{_('ipPool')}</strong>
</Col>
<Col mediumSize={3}>
<strong>{_('quantity')}</strong>
</Col>
</Row>
{map(state.ipPools, (ipPool, index) => <Row className='mb-1' key={index}>
<Col mediumSize={3}>
<input className='form-control' type='number' min={0} onChange={this.linkState(`ipPools.${index}.quantity`)} value={defined(ipPool.quantity, '')} placeholder='∞' />
</Col>
<Col mediumSize={7}>
<SelectIpPool onChange={this.linkState(`ipPools.${index}.id`, 'id')} value={ipPool.id} />
</Col>
<Col mediumSize={3}>
<input className='form-control' type='number' min={0} onChange={this.linkState(`ipPools.${index}.quantity`)} value={firstDefined(ipPool.quantity, '')} placeholder='∞' />
</Col>
<Col mediumSize={2}>
<ActionButton icon='delete' handler={this._removeIpPool} handlerParam={index} />
</Col>
</Row>)}
<Row>
<Col mediumSize={7}>
<SelectIpPool onChange={this.linkState('newIpPool')} value={state.newIpPool} predicate={this._getIpPoolPredicate()} />
</Col>
<Col mediumSize={3}>
<input className='form-control' type='number' min={0} onChange={this.linkState('newIpPoolQuantity')} value={state.newIpPoolQuantity || ''} placeholder='∞' />
</Col>
<Col mediumSize={7}>
<SelectIpPool onChange={this._onChangeIpPool} value='' predicate={this._getIpPoolPredicate()} />
<Col mediumSize={2}>
<ActionButton icon='add' handler={this._addIpPool} />
</Col>
</Row>
</Col>
@@ -507,7 +510,7 @@ class ResourceSet extends Component {
} = resourceSet
return [
<li key='subjects' className='list-group-item'>
<li className='list-group-item'>
<Subjects subjects={subjects} />
</li>,
...map(objectsByType, (objectsSet, type) => (
@@ -515,7 +518,7 @@ class ResourceSet extends Component {
{map(objectsSet, object => renderXoItem(object, { className: 'mr-1' }))}
</li>
)),
!isEmpty(ipPools) && <li key='ipPools' className='list-group-item'>
!isEmpty(ipPools) && <li className='list-group-item'>
{map(ipPools, pool => {
const resolvedIpPool = resolvedIpPools[pool]
const limits = get(resourceSet, `limits[ipPool:${pool}]`)
@@ -531,7 +534,7 @@ class ResourceSet extends Component {
}
)}
</li>,
<li key='graphs' className='list-group-item'>
<li className='list-group-item'>
<Row>
<Col mediumSize={4}>
<Card>
@@ -613,7 +616,7 @@ class ResourceSet extends Component {
</Col>
</Row>
</li>,
<li key='actions' className='list-group-item text-xs-center'>
<li className='list-group-item text-xs-center'>
<div className='btn-toolbar'>
<ActionButton btnStyle='primary' icon='edit' handler={this.toggleState('editionMode')}>{_('editResourceSet')}</ActionButton>
<ActionButton btnStyle='danger' icon='delete' handler={deleteResourceSet} handlerParam={resourceSet}>{_('deleteResourceSet')}</ActionButton>

View File

@@ -86,7 +86,7 @@ const GROUP_COLUMNS = [
},
{
name: _('addUserToGroupColumn'),
itemRenderer: group => <SelectSubject predicate={getPredicate(group.users)} onChange={user => user && addUserToGroup(user, group)} value={null} />
itemRenderer: group => <SelectSubject predicate={getPredicate(group.users)} onChange={user => user && addUserToGroup(user, group)} defaultValue={null} />
},
{
name: '',

View File

@@ -2,6 +2,7 @@ import _, { messages } from 'intl'
import ActionButton from 'action-button'
import ActionRowButton from 'action-row-button'
import BaseComponent from 'base-component'
import DebounceInput from 'react-debounce-input'
import Icon from 'icon'
import React from 'react'
import SingleLineRow from 'single-line-row'
@@ -9,10 +10,9 @@ 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 { createGetObjectsOfType, createSelector } from 'selectors'
import { injectIntl } from 'react-intl'
import { Input as DebounceInput } from 'debounce-component-decorator'
import { renderXoItemFromId } from 'render-xo-item'
import { SelectNetwork } from 'select-objects'
import { Text } from 'editable'
@@ -85,9 +85,9 @@ class IpsCell extends BaseComponent {
<Row>
<Col mediumSize={6} offset={5}><strong>{_('ipsVifs')}</strong></Col>
</Row>
{ipPool.addresses && map(formatIps(keys(ipPool.addresses)), (ip, key) => {
{ipPool.addresses && map(formatIps(keys(ipPool.addresses)), ip => {
if (isObject(ip)) { // Range of IPs
return <Row key={key}>
return <Row>
<Col mediumSize={5}>
<strong>{ip.first} <Icon icon='arrow-right' /> {ip.last}</strong>
</Col>
@@ -109,7 +109,7 @@ class IpsCell extends BaseComponent {
? map(addressVifs, (vifId, index) => {
const vif = vifs[vifId] && vifs[vifId][0]
const network = vif && networks[vif.$network] && networks[vif.$network][0]
return <span key={index} className='mr-1'>
return <span className='mr-1'>
{network && vif
? `${network.name_label} #${vif.device}`
: <em>{_('ipPoolUnknownVif')}</em>
@@ -188,7 +188,7 @@ class NetworksCell extends BaseComponent {
const { newNetworks, showNewNetworkForm } = this.state
return <Container>
{map(ipPool.networks, networkId => <Row key={networkId}>
{map(ipPool.networks, networkId => <Row>
<Col mediumSize={11}>
{renderXoItemFromId(networkId)}
</Col>

View File

@@ -1,19 +1,19 @@
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 find from 'lodash/find'
import isEmpty from 'lodash/isEmpty'
import map from 'lodash/map'
import React from 'react'
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 { FormattedDate } from 'react-intl'
import { subscribeApiLogs, subscribeUsers, deleteApiLog } from 'xo'
const CAN_REPORT_BUG = process.env.XOA_PLAN > 1
@@ -99,7 +99,7 @@ export default class Logs extends BaseComponent {
_getLogs = createSelector(
() => this.props.logs,
logs => logs && map(logs, (log, id) => ({ ...log, id }))
logs => map(logs, (log, id) => ({ ...log, id }))
)
_showError = log => alert(
@@ -115,28 +115,29 @@ export default class Logs extends BaseComponent {
(users, showError) => ({ users, showError })
)
_getPredicate = logs => logs != null
render () {
const logs = this._getLogs()
return <NoObjects collection={logs} message={_('noLogs')} predicate={this._getPredicate}>
<div>
<span className='pull-right'>
<TabButton
btnStyle='danger'
handler={this._deleteAllLogs}
icon='delete'
labelId='logDeleteAll'
return <div>
{isEmpty(logs)
? <p>{_('noLogs')}</p>
: <div>
<span className='pull-right'>
<TabButton
btnStyle='danger'
handler={this._deleteAllLogs}
icon='delete'
labelId='logDeleteAll'
/>
</span>
{' '}
<SortedTable
collection={logs}
columns={COLUMNS}
userData={this._getData()}
/>
</span>
{' '}
<SortedTable
collection={logs}
columns={COLUMNS}
userData={this._getData()}
/>
</div>
</NoObjects>
</div>
}
</div>
}
}

View File

@@ -347,7 +347,6 @@ export default class Remotes extends Component {
>
{map(remoteTypes, (label, key) => _({key}, label, message => <option value={key}>{message}</option>))}
</select>
{type === 'smb' && <em className='text-warning'>{_('remoteSmbWarningMessage')}</em>}
</div>
<div className='form-group'>
<input type='text' ref='name' className='form-control' placeholder={this.props.intl.formatMessage(messages.remoteMyNamePlaceHolder)} required />

View File

@@ -1,9 +1,9 @@
import _, { messages } from 'intl'
import ActionButton from 'action-button'
import ActionRowButton from 'action-row-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'
@@ -12,7 +12,7 @@ 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 { map, noop } from 'lodash'
import {
addServer,
editServer,
@@ -22,152 +22,10 @@ import {
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) =>
<Text
value={server.label || ''}
onChange={label => editServer(server, { label })}
placeholder={formatMessage(messages.serverPlaceHolderLabel)}
/>,
default: true,
name: _('serverLabel'),
sortCriteria: _ => _.name_label
},
{
itemRenderer: (server, formatMessage) =>
<Text
value={server.host}
onChange={host => editServer(server, { host })}
placeholder={formatMessage(messages.serverPlaceHolderAddress)}
/>,
name: _('serverHost'),
sortCriteria: _ => _.host
},
{
itemRenderer: (server, formatMessage) =>
<Text
value={server.username}
onChange={username => editServer(server, { username })}
placeholder={formatMessage(messages.serverPlaceHolderUser)}
/>,
name: _('serverUsername'),
sortCriteria: _ => _.username
},
{
itemRenderer: (server, formatMessage) =>
<EditablePassword
value=''
onChange={password => editServer(server, { password })}
placeholder={formatMessage(messages.serverPlaceHolderPassword)}
/>,
name: _('serverPassword')
},
{
itemRenderer: server =>
<div>
<StateButton
disabledLabel={_('serverDisconnected')}
disabledHandler={connectServer}
disabledTooltip={_('serverConnect')}
enabledLabel={_('serverConnected')}
enabledHandler={disconnectServer}
enabledTooltip={_('serverDisconnect')}
handlerParam={server}
pending={server.status === 'connecting'}
state={server.status === 'connected'}
/>
{' '}
{server.error &&
<Tooltip content={_('serverConnectionFailed')}>
<a
className='text-danger btn btn-link btn-sm'
onClick={() => showServerError(server)}
>
<Icon
icon='alarm'
size='lg'
/>
</a>
</Tooltip>
}
</div>,
name: _('serverStatus'),
sortCriteria: _ => _.status
},
{
itemRenderer: server =>
<Toggle
onChange={readOnly => editServer(server, { readOnly })}
value={!!server.readOnly}
/>,
name: _('serverReadOnly'),
sortCriteria: _ => !!_.readOnly
},
{
itemRenderer: server =>
<Toggle
value={server.allowUnauthorized}
onChange={allowUnauthorized => editServer(server, { allowUnauthorized })}
/>,
name: <span>
{_('serverUnauthorizedCertificates')}
{' '}
<Tooltip content={_('serverAllowUnauthorizedCertificates')}>
<a
className='text-info'
onClick={showInfo}
>
<Icon
icon='info'
size='lg'
/>
</a>
</Tooltip>
</span>,
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 () => {
@@ -178,6 +36,33 @@ export default class Servers extends Component {
this.setState({ label: '', host: '', password: '', username: '' })
}
_showError = 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)
}
_showInfo = () => alert(
_('serverAllowUnauthorizedCertificates'),
_('serverUnauthorizedCertificatesInfo')
)
render () {
const {
props: {
@@ -188,12 +73,112 @@ export default class Servers extends Component {
} = this
return <Container>
<SortedTable
collection={servers}
columns={COLUMNS}
individualActions={INDIVIDUAL_ACTIONS}
userData={formatMessage}
/>
<table className='table table-striped'>
<thead>
<tr>
<td>{_('serverLabel')}</td>
<td>{_('serverHost')}</td>
<td>{_('serverUsername')}</td>
<td>{_('serverPassword')}</td>
<td>{_('serverStatus')}</td>
<td>{_('serverReadOnly')}</td>
<td>
{_('serverUnauthorizedCertificates')}
{' '}
<Tooltip content={_('serverAllowUnauthorizedCertificates')}>
<a
className='text-info'
onClick={this._showInfo}
>
<Icon
icon='info'
size='lg'
/>
</a>
</Tooltip>
</td>
<td className='text-xs-right'>{_('serverAction')}</td>
</tr>
</thead>
<tbody>
{map(servers, server => (
<tr key={server.id}>
<td>
<Text
value={server.label || ''}
onChange={label => editServer(server, { label })}
placeholder={formatMessage(messages.serverPlaceHolderLabel)}
/>
</td>
<td>
<Text
value={server.host}
onChange={host => editServer(server, { host })}
placeholder={formatMessage(messages.serverPlaceHolderAddress)}
/>
</td>
<td>
<Text
value={server.username}
onChange={username => editServer(server, { username })}
placeholder={formatMessage(messages.serverPlaceHolderUser)}
/>
</td>
<td>
<EditablePassword
value=''
onChange={password => editServer(server, { password })}
placeholder={formatMessage(messages.serverPlaceHolderPassword)}
/>
</td>
<td>
<StateButton
disabledLabel={_('serverDisconnected')}
disabledHandler={connectServer}
disabledTooltip={_('serverConnect')}
enabledLabel={_('serverConnected')}
enabledHandler={disconnectServer}
enabledTooltip={_('serverDisconnect')}
handlerParam={server}
pending={server.status === 'connecting'}
state={server.status === 'connected'}
/>
{' '}
{server.error &&
<Tooltip content={_('serverConnectionFailed')}>
<a
className='text-danger btn btn-link btn-sm'
onClick={() => this._showError(server)}
>
<Icon
icon='alarm'
size='lg'
/>
</a>
</Tooltip>
}
</td>
<td><Toggle value={!!server.readOnly} onChange={readOnly => editServer(server, { readOnly })} /></td>
<td>
<Toggle
value={server.allowUnauthorized}
onChange={allowUnauthorized => editServer(server, { allowUnauthorized })}
/>
</td>
<td className='text-xs-right'>
<ActionRowButton
btnStyle='danger'
handler={removeServer}
handlerParam={server}
icon='delete'
/>
</td>
</tr>
))}
</tbody>
</table>
<form
className='form-inline'
id='form-add-server'

View File

@@ -1,33 +1,33 @@
import _ from 'intl'
import ActionBar, { Action } from 'action-bar'
import ActionBar from 'action-bar'
import React from 'react'
import { forgetSr, rescanSr, reconnectAllHostsSr, disconnectAllHostsSr } from 'xo'
const SrActionBar = ({ sr }) => (
<ActionBar
actions={[
{
icon: 'refresh',
label: 'srRescan',
handler: rescanSr
},
{
icon: 'sr-reconnect-all',
label: 'srReconnectAll',
handler: reconnectAllHostsSr
},
{
icon: 'sr-disconnect-all',
label: 'srDisconnectAll',
handler: disconnectAllHostsSr
},
{
icon: 'sr-forget',
label: 'srForget',
handler: forgetSr
}
]}
display='icon'
handlerParam={sr}
>
<Action
handler={rescanSr}
label={_('srRescan')}
icon='refresh'
/>
<Action
handler={reconnectAllHostsSr}
label={_('srReconnectAll')}
icon='sr-reconnect-all'
/>
<Action
handler={disconnectAllHostsSr}
label={_('srDisconnectAll')}
icon='sr-disconnect-all'
/>
<Action
handler={forgetSr}
label={_('srForget')}
icon='sr-forget'
/>
</ActionBar>
param={sr}
/>
)
export default SrActionBar

View File

@@ -1,55 +0,0 @@
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 <Container>
<Row className='mb-1'>
<Col size={6}>{_('xosanSelectNSrs', { nSrs: this.props.subvolumeSize })}</Col>
<Col size={6}>
<SelectSr
multi
onChange={this._selectSrs}
predicate={this._getSrPredicate()}
value={this.state.srs}
/>
</Col>
</Row>
<Row className='mb-1'>
<Col size={6}>{_('xosanSize')}</Col>
<Col size={6}>
<SizeInput
onChange={this.linkState('brickSize')}
required
value={this.state.brickSize}
/>
</Col>
</Row>
</Container>
}
}

View File

@@ -143,6 +143,7 @@ export default class Sr extends Component {
</div>
</Col>
</Row>
<br />
<Row>
<Col>
<NavTabs>

View File

@@ -1,63 +0,0 @@
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) => 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 <Container>
<Row className='mb-1'>
<Col size={6}><strong>{_('xosanOnSameVm')}</strong></Col>
<Col size={6}>
<Toggle onChange={this._toggleOnSameVm} value={this.state.onSameVm} />
</Col>
</Row>
<Row className='mb-1'>
<Col size={6}><strong>{_('xosanUnderlyingStorage')}</strong></Col>
<Col size={6}>
<SelectSr
onChange={this._selectSr}
predicate={this._getSrPredicate()}
value={this.state.sr}
/>
</Col>
</Row>
<Row className='mb-1'>
<Col size={6}><strong>{_('xosanBrickSize')}</strong></Col>
<Col size={6}>
<SizeInput
onChange={this.linkState('brickSize')}
value={this.state.brickSize}
/>
</Col>
</Row>
</Container>
}
}

View File

@@ -1,58 +1,9 @@
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 { deleteSr } from 'xo'
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 => <span>{vdi.name_label}</span>,
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
: <div>
<h3>{_('srUnhealthyVdiTitle', { total: sum(values(chains)) })}</h3>
<SortedTable
collection={vdis}
columns={COLUMNS}
userData={chains}
/>
</div>
)
export default ({
sr
@@ -83,9 +34,4 @@ export default ({
</table>
</Col>
</Row>
<Row>
<Col>
<UnhealthyVdiChains sr={sr} />
</Col>
</Row>
</Container>

View File

@@ -1,4 +1,5 @@
import _ from 'intl'
import ActionRow from 'action-row-button'
import Component from 'base-component'
import Icon from 'icon'
import Link from 'link'
@@ -9,7 +10,7 @@ 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 { deleteVdi, editVdi } from 'xo'
import { Text } from 'editable'
// ===================================================================
@@ -62,21 +63,13 @@ const COLUMNS = [
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
: <Link to={link}>{item}</Link>
return <Link to={`/vms/${
vm.type === 'VM-snapshot'
? `${vm.$snapshot_of}/snapshots`
: vm.id
}`}>
{renderXoItem(vm)}
</Link>
})
},
{
@@ -87,23 +80,17 @@ const COLUMNS = [
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'
name: _('vdiAction'),
itemRenderer: vdi => (
<ActionRow
btnStyle='danger'
handler={deleteVdi}
handlerParam={vdi}
icon='delete'
/>
)
}
]
@@ -136,10 +123,6 @@ export default class SrDisks extends Component {
columns={COLUMNS}
defaultFilter='filterOnlyManaged'
filters={FILTERS}
groupedActions={GROUPED_ACTIONS}
individualActions={INDIVIDUAL_ACTIONS}
shortcutsTarget='body'
stateUrlParam='s'
/>
: <h4 className='text-xs-center'>{_('srNoVdis')}</h4>
}

View File

@@ -1,585 +1,30 @@
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 { Container, Row, Col } from 'grid'
import {
forEach,
isEmpty,
map,
reduce,
sum
keys,
map
} from 'lodash'
import {
createGetObjectsOfType,
createSelector
} from 'selectors'
import {
addSubscriptions,
connectStore,
formatSize
} from 'utils'
import {
addXosanBricks,
fixHostNotInXosanNetwork,
// TODO: uncomment when implementing subvolume deletion
// removeXosanBricks,
replaceXosanBrick,
startVm,
subscribeVolumeInfo
getVolumeInfo
} 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 }) => <Container>
{map(issues, issue => <Row key={issue.key || issue.code} className='alert alert-danger mb-1' role='alert'>
<Col>
<Icon icon='error' /> <strong>{_(ISSUE_CODE_TO_MESSAGE[issue.code], issue.params)}</strong>
{issue.fix && <Tooltip content={issue.fix.title}>
<ActionButton
btnStyle='danger'
className='ml-1'
handler={issue.fix.action}
icon='fix'
size='small'
>
{_('xosanFixIssue')}
</ActionButton>
</Tooltip>}
</Col>
</Row>)}
</Container>
const Field = ({ title, children }) => <SingleLineRow>
<Col size={3}><strong>{title}</strong></Col>
<Col size={9}>{children}</Col>
</SingleLineRow>
@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: <ReplaceBrickModalBody vm={vm} />
})
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 <Collapse
buttonText={<span>
<Icon
color={heal
? heal.status === 'Connected'
? 'text-success'
: 'text-warning'
: 'text-danger'
}
icon='disk'
/> {srs[config.underlyingSr].name_label}
</span>}
className='mb-1'
>
<div style={BORDERS}>
<Container className='p-1'>
<Field title={_('xosanVm')}>
{vm !== undefined
? <span>
<Tooltip content={_(`powerState${vm.power_state}`)}>
<Icon icon={vm.power_state.toLowerCase()} />
</Tooltip> <Link to={`/vms/${config.vm.id}`}>{vm.name_label}</Link>
{(vm.power_state !== 'Running') &&
<Tooltip content={_('xosanRun')}>
<ActionButton
handler={startVm}
handlerParam={vm}
icon='vm-start'
size='small'
/>
</Tooltip>
}
</span>
: <span style={{color: 'red'}}>
<Icon icon='alarm' /> {_('xosanCouldNotFindVm')}
</span>
}
</Field>
<Field title={_('xosanUnderlyingStorage')}>
<Link to={`/srs/${config.underlyingSr}`}>{srs[config.underlyingSr].name_label}</Link>
{' - '}
{size != null && _('xosanUnderlyingStorageUsage', { usage: formatSize(size) })}
</Field>
<Field title={_('xosanStatus')}>
{heal ? heal.status : 'unknown'}
</Field>
{statusDetail && <Field title={_('xosanUsedSpace')}>
<span style={{ display: 'inline-block', width: '20em', height: '1em' }}>
<Tooltip content={_('spaceLeftTooltip', this._getSizeUsage())}>
<progress
className='progress'
max='100'
value={100 - (+statusDetail.sizeFree / +statusDetail.sizeTotal) * 100}
/>
</Tooltip>
</span>
</Field>}
{config.arbiter === 'True' && <Field title={_('xosanArbiter')} />}
<Row className='mt-1'>
<Col>
<ActionButton
btnStyle='success'
icon='refresh'
handler={this._replaceBrick}
handlerParam={{ brick: config.brickName, vm }}
>
{_('xosanReplace')}
</ActionButton>
</Col>
</Row>
<Row className='mt-1'>
<Col><h3><Toggle iconSize={1} onChange={this.toggleState('showAdvanced')} value={showAdvanced} /> {_('xosanAdvanced')}</h3></Col>
</Row>
{showAdvanced && [
<Field title={_('xosanBrickName')}>
<Copiable tagName='div'>{config.brickName}</Copiable>
</Field>,
<Field title={_('xosanBrickUuid')}>
<Copiable tagName='div'>{uuid}</Copiable>
</Field>,
<div>
{statusDetail && [
<Field key='usedInodes' title={_('xosanUsedInodes')}>
<span style={{ display: 'inline-block', width: '20em', height: '1em' }}>
<Tooltip content={_('spaceLeftTooltip', this._getInodesUsage())}>
<progress className='progress' max='100' value={100 - (+statusDetail.inodesFree / +statusDetail.inodesTotal) * 100}
/>
</Tooltip>
</span>
</Field>,
<Field key='blockSize' title={_('xosanBlockSize')}>{statusDetail.blockSize}</Field>,
<Field key='device' title={_('xosanDevice')}>{statusDetail.device}</Field>,
<Field key='fsName' title={_('xosanFsName')}>{statusDetail.fsName}</Field>,
<Field key='mountOptions' title={_('xosanMountOptions')}>{statusDetail.mntOptions}</Field>,
<Field key='path' title={_('xosanPath')}>{statusDetail.path}</Field>
]}
</div>,
<div>
{status && status.length !== 0 && <Row className='mt-1'>
<Col>
<table className='table' style={{ maxWidth: '50em' }}>
<thead>
<th>{_('xosanJob')}</th>
<th>{_('xosanPath')}</th>
<th>{_('xosanStatus')}</th>
<th>{_('xosanPid')}</th>
<th>{_('xosanPort')}</th>
</thead>
<tbody>
{map(status, job => <tr key={job.pid}>
<td>{job.hostname}</td>
<td>{job.path}</td>
<td>{job.status}</td>
<td>{job.pid}</td>
<td>{job.port}</td>
</tr>)}
</tbody>
</table>
</Col>
</Row>}
</div>,
<div>
{heal && heal.file && heal.file.length !== 0 && <div>
<h4>{_('xosanFilesNeedingHealing')}</h4>
{map(heal.file, file => <Row key={file.gfid}>
<Col size={5}>{file._}</Col >
<Col size={4}>{file.gfid}</Col>
</Row>)}
</div>}
</div>
]}
</Container>
</div>
</Collapse>
}
}
// -----------------------------------------------------------------------------
@connectStore(() => ({
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)
})
return subscriptions
})
export default class TabXosan extends Component {
_addSubvolume = async () => {
const { srs, brickSize } = await confirm({
icon: 'add',
title: _('xosanAddSubvolume'),
body: <AddSubvolumeModalBody sr={this.props.sr} subvolumeSize={this._getSubvolumeSize()} />
componentDidMount () {
getVolumeInfo(this.props.sr.id).then(info => {
this.setState({ volumeInfo: info })
})
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
// }
// )
_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 { showAdvanced } = this.state
const {
heal_,
info_,
sr,
status_,
statusDetail_,
vbds,
vdis
} = this.props
const xosanConfig = this._getConfig()
if (!xosanConfig) {
return null
}
if (!xosanConfig.version) {
return <div>
{_('xosanWarning')}
</div>
}
const strippedVolumeInfo = this._getStrippedVolumeInfo()
// const subVolumes = this._getSubvolumes() // TODO: uncomment when implementing subvolume deletion
const orderedBrickList = this._getOrderedBrickList()
return <Container>
<Row className='text-xs-center mb-1 mt-1'>
<Col size={3}>
<h2><Icon icon='sr' size='lg' color={status_ ? (status_.commandStatus ? 'text-success' : status_.error) : 'text-info'} /></h2>
</Col>
<Col size={3}>
<h2><Icon icon='health' size='lg' color={heal_ ? (heal_.commandStatus ? 'text-success' : heal_.error) : 'text-info'} /></h2>
</Col>
<Col size={3}>
<h2><Icon icon='settings' size='lg' color={statusDetail_ ? (statusDetail_.commandStatus ? 'text-success' : statusDetail_.error) : 'text-info'} /></h2>
</Col>
<Col size={3}>
<h2><Icon icon='info' size='lg' color={info_ ? (info_.commandStatus ? 'text-success' : info_.error) : 'text-info'} /></h2>
</Col>
</Row>
<Row className='mb-1'>
<Col><Issues issues={this._getIssues()} /></Col>
</Row>
{map(orderedBrickList, node => <Row key={node.config.brickName}>
<Col>
<Node
heal_={heal_}
info_={info_}
node={node}
sr={sr}
status_={status_}
statusDetail_={statusDetail_}
vbds={vbds}
vdis={vdis}
/>
</Col>
</Row>)}
<Row>
<Col>
<ActionButton
btnStyle='success'
handler={this._addSubvolume}
icon='add'
>
{_('xosanAddSubvolume')}
</ActionButton>
<hr />
</Col>
</Row>
{/* We will implement this later */}
{/* <Row>
<Col>
<h2>{_('xosanRemoveSubvolumes')}</h2>
<table className='table'>
{map(subVolumes, (subvolume, i) => <tr key={i}>
<td>
<ul>{map(subvolume, (brick, j) => <li key={j}>{brick.name}</li>)}</ul>
</td>
<td>
<ActionButton
btnStyle='danger'
icon='remove'
handler={::this._removeSubVolume}
handlerParam={map(subvolume, brick => brick.name)}
>
{_('xosanRemove')}
</ActionButton>
</td>
</tr>)}
</table>
<hr />
</Col>
</Row> */}
<Row>
<Col>
<h2><Toggle iconSize={1} onChange={this.toggleState('showAdvanced')} value={showAdvanced} /> {_('xosanAdvanced')}</h2>
{strippedVolumeInfo && showAdvanced && <div>
<h3>{_('xosanVolume')}</h3>
<Container>
<Field title={'Name'}>{strippedVolumeInfo.name}</Field>
<Field title={'Status'}>{strippedVolumeInfo.statusStr}</Field>
<Field title={'Type'}>{strippedVolumeInfo.typeStr}</Field>
<Field title={'Brick Count'}>{strippedVolumeInfo.brickCount}</Field>
<Field title={'Stripe Count'}>{strippedVolumeInfo.stripeCount}</Field>
<Field title={'Replica Count'}>{strippedVolumeInfo.replicaCount}</Field>
<Field title={'Arbiter Count'}>{strippedVolumeInfo.arbiterCount}</Field>
<Field title={'Disperse Count'}>{strippedVolumeInfo.disperseCount}</Field>
<Field title={'Redundancy Count'}>{strippedVolumeInfo.redundancyCount}</Field>
</Container>
<h3 className='mt-1'>{_('xosanVolumeOptions')}</h3>
<Container>
{map(strippedVolumeInfo.options, option =>
<Field key={option.name} title={option.name}>{option.value}</Field>
)}
</Container>
</div>}
</Col>
</Row>
</Container>
{this.state.volumeInfo && map(keys(this.state.volumeInfo).sort(), key => key !== 'Bricks'
? <Row key={key}>
<Col size={3}><strong>{key}</strong></Col>
<Col size={4}>{this.state.volumeInfo[key]}</Col>
</Row>
: null
)}
</Container >
}
}

View File

@@ -2,34 +2,17 @@ import _, { messages } from 'intl'
import ActionRowButton from 'action-row-button'
import ButtonGroup from 'button-group'
import CenterPanel from 'center-panel'
import Component from 'base-component'
import Icon from 'icon'
import isEmpty from 'lodash/isEmpty'
import keys from 'lodash/keys'
import Link from 'link'
import map from 'lodash/map'
import React from 'react'
import SingleLineRow from 'single-line-row'
import { injectIntl } from 'react-intl'
import { SelectPool } from 'select-objects'
import {
Card,
CardBlock,
CardHeader
} from 'card'
import {
connectStore,
resolveId,
resolveIds
} from 'utils'
import {
Col,
Container,
Row
} from 'grid'
import {
includes,
isEmpty,
keys,
map
} from 'lodash'
import { Card, CardBlock, CardHeader } from 'card'
import { connectStore } from 'utils'
import { Container, Row, Col } from 'grid'
import {
createGetObject,
createGetObjectsOfType,
@@ -84,37 +67,31 @@ export const TaskItem = connectStore(() => ({
</Col>
</SingleLineRow>)
@connectStore(() => {
const getPendingTasks = createGetObjectsOfType('task').filter(
[ task => task.status === 'pending' ]
)
export default injectIntl(
connectStore(() => {
const getTasks = createGetObjectsOfType('task')
const getNPendingTasks = getPendingTasks.count()
const getNPendingTasks = getTasks.count(
[ task => task.status === 'pending' ]
)
const getPendingTasksByPool = getPendingTasks.sort().groupBy('$pool')
const getPendingTasksByPool = getTasks.filter(
[ task => task.status === 'pending' ]
).sort().groupBy('$pool')
const getPools = createGetObjectsOfType('pool').pick(
createSelector(getPendingTasksByPool, keys)
).sort()
return {
nTasks: getNPendingTasks,
pendingTasksByPool: getPendingTasksByPool,
pools: getPools
}
})
@injectIntl
export default class Tasks extends Component {
_showPoolTasks = pool => isEmpty(this.state.pools) || includes(resolveIds(this.state.pools), resolveId(pool))
render () {
const { props, state } = this
const {
intl,
nTasks,
pendingTasksByPool
} = props
const getPools = createGetObjectsOfType('pool').pick(
createSelector(
getPendingTasksByPool,
pendingTasksByPool => keys(pendingTasksByPool)
)
).sort()
return (state, props) => ({
nTasks: getNPendingTasks(state, props),
pendingTasksByPool: getPendingTasksByPool(state, props),
pools: getPools(state, props)
})
})(({ intl, nTasks, pendingTasksByPool, pools }) => {
if (isEmpty(pendingTasksByPool)) {
return <Page header={HEADER} title='taskPage' formatTitle>
<CenterPanel>
@@ -135,24 +112,19 @@ export default class Tasks extends Component {
const { formatMessage } = intl
return <Page header={HEADER} title={`(${nTasks}) ${formatMessage(messages.taskPage)}`}>
<Container>
<Row className='mb-1'>
<SelectPool
multi
value={state.pools}
onChange={this.linkState('pools')}
/>
</Row>
{map(props.pools, pool => this._showPoolTasks(pool) && <Row>
<Card>
<CardHeader key={pool.id}><Link to={`/pools/${pool.id}`}>{pool.name_label}</Link></CardHeader>
<CardBlock>
{map(pendingTasksByPool[pool.id], task =>
<TaskItem key={task.id} task={task} />
)}
</CardBlock>
</Card>
</Row>)}
{map(pools, pool =>
<Row>
<Card>
<CardHeader key={pool.id}><Link to={`/pools/${pool.id}`}>{pool.name_label}</Link></CardHeader>
<CardBlock>
{map(pendingTasksByPool[pool.id], task =>
<TaskItem key={task.id} task={task} />
)}
</CardBlock>
</Card>
</Row>
)}
</Container>
</Page>
}
}
})
)

View File

@@ -329,7 +329,7 @@ export default class User extends Component {
<Col smallSize={10}>
<form className='form-inline' id='changePassword'>
<input
autoComplete='off'
autocomplete='off'
className='form-control'
onChange={this._handleOldPasswordChange}
placeholder={formatMessage(messages.oldPasswordPlaceholder)}
@@ -339,7 +339,7 @@ export default class User extends Component {
/>
{' '}
<input type='password'
autoComplete='off'
autocomplete='off'
className='form-control'
onChange={this._handleNewPasswordChange}
placeholder={formatMessage(messages.newPasswordPlaceholder)}
@@ -348,7 +348,7 @@ export default class User extends Component {
/>
{' '}
<input
autoComplete='off'
autocomplete='off'
className='form-control'
onChange={this._handleConfirmPasswordChange}
placeholder={formatMessage(messages.confirmPasswordPlaceholder)}

View File

@@ -0,0 +1,53 @@
import ActionBar from 'action-bar'
import React from 'react'
import { connectStore } from 'utils'
import { includes } from 'lodash'
import { isAdmin } from 'selectors'
import {
rebootVmGroup,
startVmGroup,
shutdownVmGroup
} from 'xo'
const vmGroupActionBarByState = ({ isAdmin, vmGroup }) => {
const actions = []
if (vmGroup.allowed_operations.includes('start') || vmGroup.allowed_operations.length === 0) {
actions.push({
icon: 'vm-start',
label: 'startVmLabel',
handler: startVmGroup,
pending: includes(vmGroup.current_operations, 'start')
})
}
if (vmGroup.allowed_operations.includes('shutdown') || vmGroup.allowed_operations.length === 0) {
actions.push({
icon: 'vm-stop',
label: 'stopVmLabel',
handler: shutdownVmGroup,
pending: includes(vmGroup.current_operations, 'shutdown')
})
}
actions.push({
icon: 'vm-reboot',
label: 'rebootVmLabel',
handler: rebootVmGroup,
pending: includes(vmGroup.current_operations, 'shutdown') || includes(vmGroup.current_operations, 'start')
})
return <ActionBar
actions={actions}
display='icon'
param={vmGroup}
/>
}
const VmGroupActionBar = connectStore({
isAdmin
})(({ isAdmin, vmGroup }) => {
const ActionBar = vmGroupActionBarByState
if (!ActionBar) {
return <p>No action bar for state</p>
}
return <ActionBar isAdmin={isAdmin} vmGroup={vmGroup} />
})
export default VmGroupActionBar

View File

@@ -0,0 +1,131 @@
import _ from 'intl'
import BaseComponent from 'base-component'
import Icon from 'icon'
import isEmpty from 'lodash/isEmpty'
import React, { cloneElement } from 'react'
import {
connectStore,
routes
} from 'utils'
import {
assign,
forEach,
map,
pick
} from 'lodash'
import { Container, Row, Col } from 'grid'
import { createGetObject } from 'selectors'
import { editVmGroup } from 'xo'
import { NavLink, NavTabs } from 'nav'
import { Text } from 'editable'
import Page from '../page'
import TabAdvanced from './tab-advanced'
import TabGeneral from './tab-general'
import TabManagement from './tab-management'
import TabStats from './tab-stats'
import VmGroupActionBar from './action-bar'
// ===================================================================
@routes('general', {
advanced: TabAdvanced,
general: TabGeneral,
management: TabManagement,
stats: TabStats
})
@connectStore(() => {
const getVmGroup = createGetObject()
return (state, props) => {
const vmGroup = getVmGroup(state, props)
if (!vmGroup) {
return {}
}
const vms = {}
forEach(vmGroup.$VMs, vmId => {
const getVM = createGetObject(() => vmId)
vms[vmId] = getVM(state, props)
})
return {
vmGroup,
vms
}
}
})
export default class VmGroup extends BaseComponent {
static contextTypes = {
router: React.PropTypes.object
}
_setNameDescription = description => editVmGroup(this.props.vmGroup, {name_description: description})
_setNameLabel = label => editVmGroup(this.props.vmGroup, {name_label: label})
_getVmGroupState = () => {
const states = map(this.props.vms, vm => vm.power_state)
return (isEmpty(states)
? 'busy'
: states.indexOf('Halted') === -1
? states[0]
: states.indexOf('Running') === -1
? states[0]
: 'busy').toLowerCase()
}
header () {
const { vmGroup, vms } = this.props
if (!vmGroup) {
return <Icon icon='loading' />
}
return <Container>
<Row>
<Col mediumSize={6} className='header-title'>
<h2>
<Icon icon={isEmpty(vms) ? `vm-halted` : `vm-${this._getVmGroupState()}`} />
{' '}
<Text value={vmGroup.name_label} onChange={this._setNameLabel} />
</h2>
<span>
<Text
value={vmGroup.name_description}
onChange={this._setNameDescription}
/>
<span className='text-muted'>
{' '}
</span>
</span>
</Col>
<Col mediumSize={6} className='text-xs-center'>
<div>
<VmGroupActionBar vmGroup={vmGroup} />
</div>
</Col>
</Row>
<br />
<Row>
<Col>
<NavTabs>
<NavLink to={`/vm-group/${vmGroup.id}/general`}>{_('generalTabName')}</NavLink>
<NavLink to={`/vm-group/${vmGroup.id}/stats`}>{_('statsTabName')}</NavLink>
<NavLink to={`/vm-group/${vmGroup.id}/management`}>{_('managementTabName')}</NavLink>
<NavLink to={`/vm-group/${vmGroup.id}/advanced`}>{_('advancedTabName')}</NavLink>
</NavTabs>
</Col>
</Row>
</Container>
}
render () {
const { container, vmGroup } = this.props
if (!vmGroup) {
return <h1>{_('statusLoading')}</h1>
}
const childProps = assign(pick(this.props, [
'vmGroup',
'vms'
]))
return <Page header={this.header()} title={`${vmGroup.name_label}${container ? ` (${container.name_label})` : ''}`}>
{cloneElement(this.props.children, { ...childProps })}
</Page>
}
}

View File

@@ -0,0 +1,55 @@
import _ from 'intl'
import Component from 'base-component'
import Copiable from 'copiable'
import React from 'react'
import TabButton from 'tab-button'
import { Container, Row, Col } from 'grid'
import { deleteVmGroup } from 'xo'
import { noop } from 'utils'
export default class TabAdvanced extends Component {
static contextTypes = {
router: React.PropTypes.object
}
_deleteVmGroup = (vmGroup, vms) => {
deleteVmGroup(vmGroup, vms).then(
() => this.context.router.push('home?s=&t=VmGroup'),
noop
)
}
render () {
const { vmGroup, vms } = this.props
return (
<Container>
<Row>
<Col className='text-xs-right'>
<TabButton
btnStyle='danger'
handler={() => this._deleteVmGroup(vmGroup, vms)}
icon='vm-delete'
labelId='vmRemoveButton'
/>
</Col>
<div>
<h3>{_('xenSettingsLabel')}</h3>
{ vmGroup &&
<table className='table'>
<tbody>
<tr>
<th>{_('uuid')}</th>
<Copiable tagName='td'>
{vmGroup.id}
</Copiable>
</tr>
</tbody>
</table>
}
</div>
</Row>
</Container>
)
}
}

View File

@@ -0,0 +1,67 @@
import _ from 'intl'
import forEach from 'lodash/forEach'
import Icon from 'icon'
import isEmpty from 'lodash/isEmpty'
import map from 'lodash/map'
import reduce from 'lodash/reduce'
import React from 'react'
import size from 'lodash/size'
import { connectStore, formatSize } from 'utils'
import { Container, Row, Col } from 'grid'
import { getObject } from 'selectors'
export default connectStore(() => {
const getMemoryTotal = (state, props) => {
const vdiIds = new Set()
forEach(props.vms, vm => forEach(vm.$VBDs, vbdId => vdiIds.add(getObject(state, vbdId).VDI)))
return reduce(Array.from(vdiIds), (sum, vdiId) => {
const vdi = getObject(state, vdiId)
return vdi !== undefined
? sum + vdi.size
: sum
}, 0)
}
const getMemoryDynamicTotal = props => reduce(props.vms, (sum, vm) => vm.memory.dynamic[1] + sum, 0)
const getNbCPU = props => reduce(props.vms, (sum, vm) => vm.CPUs.number + sum, 0)
return (state, props) => ({
memoryTotal: getMemoryTotal(state, props),
memoryDynamical: getMemoryDynamicTotal(props),
nbCPU: getNbCPU(props)
})
})(({ vms, memoryTotal, memoryDynamical, nbCPU, vmGroup }) => {
return (
<Container>
<br />
<div>
<Row className='text-xs-center'>
<Col mediumSize={3}>
<h2>{size(vms)}x <Icon icon='vm' size='lg' /></h2>
</Col>
<Col mediumSize={3}>
<h2>{nbCPU}x <Icon icon='cpu' size='lg' /></h2>
</Col>
<Col mediumSize={3}>
<h2 className='form-inline'>
{formatSize(memoryDynamical)}
&nbsp;<span><Icon icon='memory' size='lg' /></span>
</h2>
</Col>
<Col mediumSize={3}>
<h2>{formatSize(memoryTotal)} <Icon icon='disk' size='lg' /></h2>
</Col>
</Row>
{isEmpty(vmGroup.current_operations)
? null
: <Row className='text-xs-center'>
<Col>
<h4>{_('vmGroupCurrentStatus')}{' '}{map(vmGroup.current_operations)[0]}</h4>
</Col>
</Row>
}
</div>
</Container>
)
})

View File

@@ -0,0 +1,145 @@
import _ from 'intl'
import ActionButton from 'action-button'
import ActionRowButton from 'action-row-button'
import Component from 'base-component'
import DragNDropOrder from 'drag-n-drop-order'
import forEach from 'lodash/forEach'
import Icon from 'icon'
import isEmpty from 'lodash/isEmpty'
import Link from 'link'
import map from 'lodash/map'
import React from 'react'
import sortBy from 'lodash/sortBy'
import SortedTable from 'sorted-table'
import TabButton from 'tab-button'
import Tooltip from 'tooltip'
import { editVm, removeAppliance } from 'xo'
import { Row, Col } from 'grid'
import { SelectVm } from 'select-objects'
const VM_COLUMNS = [
{
name: _('vmGroupLabel'),
itemRenderer: vm => (<span><Tooltip
content={isEmpty(vm.current_operations)
? _(`powerState${vm.power_state}`)
: <div>{_(`powerState${vm.power_state}`)}{' ('}{map(vm.current_operations)[0]}{')'}</div>
}
>
{isEmpty(vm.current_operations)
? <Icon icon={`${vm.power_state.toLowerCase()}`} />
: <Icon icon='busy' />
}
</Tooltip>
&nbsp;
<Link to={`/vms/${vm.id}`}>
{vm.name_label}
</Link>
</span>),
sortCriteria: vm => vm.name_label
},
{
name: _('vmGroupDescription'),
itemRenderer: vm => vm.name_description,
sortCriteria: vm => vm.name_description
},
{
name: _('vmGroupActions'),
itemRenderer: vm => (
<ActionRowButton
btnStyle='danger'
handler={(vm) => removeAppliance(vm)}
handlerParam={vm}
icon='delete'
/>
)
}
]
export default class TabManagement extends Component {
constructor (props) {
super(props)
this.state = {
attachVm: false,
bootOrder: false
}
}
parseBootOrder = vms => {
// FIXME missing translation
var previousOrder = vms[Object.keys(vms)[0]].order
var toggleActive = false
const orderVms = sortBy(vms, vm => {
if (vm.order !== previousOrder) toggleActive = true
return vm.order
})
const order = []
forEach(orderVms, vm => {
order.push({id: vm.id, text: vm.name_label})
})
return {order, toggleActive}
}
setVmBootOrder = (vms, order, toggleActive) => {
var orderValue = 0
forEach(order, (vm, key) => {
editVm(vms[vm.id], { order: toggleActive ? orderValue : 0 })
orderValue += 1
})
}
_addVm = () => {
forEach(this.state.vmsToAdd, vm => editVm(vm, { appliance: this.props.vmGroup.id }))
}
_selectVm = vmsToAdd => this.setState({vmsToAdd})
_toggleBootOrder = () => this.setState({
bootOrder: !this.state.bootOrder,
attachVm: false
})
_toggleNewVm = () => this.setState({
attachVm: !this.state.attachVm,
bootOrder: false
})
_vmPredicate = vm => vm.appliance === null
render () {
const { vms } = this.props
const { attachVm, bootOrder } = this.state
return isEmpty(vms)
? <form id='attachVm'>
<SelectVm multi onChange={this._selectVm} predicate={this._vmPredicate} required />
<span className='pull-right'>
<ActionButton form='attachVm' icon='add' btnStyle='primary' handler={this._addVm}>{_('add')}</ActionButton>
</span>
</form>
: (<div>
<Row>
<Col className='text-xs-right'>
<TabButton
btnStyle={attachVm ? 'info' : 'primary'}
handler={this._toggleNewVm}
icon='add'
labelId='attachVmButton'
/>
<TabButton
btnStyle={bootOrder ? 'info' : 'primary'}
handler={this._toggleBootOrder}
icon='sort'
labelId='vmsBootOrder'
/>
</Col>
</Row>
{bootOrder && <div><DragNDropOrder parseOrderParam={vms} parseOrder={this.parseBootOrder} setOrder={this.setVmBootOrder} toggleItems={false} onClose={this._toggleBootOrder} /><hr /></div>}
{attachVm && <div>
<form id='attachVm'>
<SelectVm multi onChange={this._selectVm} predicate={this._vmPredicate} required />
<span className='pull-right'>
<ActionButton form='attachVm' icon='add' btnStyle='primary' handler={this._addVm}>{_('add')}</ActionButton>
</span>
</form>
</div>}
<SortedTable collection={vms} columns={VM_COLUMNS} />
</div>
)
}
}

View File

@@ -0,0 +1,138 @@
import _ from 'intl'
import Component from 'base-component'
import filter from 'lodash/filter'
import Icon from 'icon'
import map from 'lodash/map'
import React from 'react'
import Tooltip from 'tooltip'
import Upgrade from 'xoa-upgrade'
import { Container, Row, Col } from 'grid'
import { Toggle } from 'form'
import { fetchVmStats } from 'xo'
import {
VmGroupCpuLineChart,
VmGroupMemoryLineChart,
VmGroupVifLineChart,
VmGroupXvdLineChart
} from 'xo-line-chart'
export default class TabStats extends Component {
constructor (props) {
super(props)
this.state.useCombinedValues = false
}
loop (vmGroup = this.props.vmGroup) {
if (this.cancel) {
this.cancel()
}
let cancelled = false
this.cancel = () => { cancelled = true }
Promise.all(map(filter(this.props.vms, vm => vm.power_state === 'Running'), vm =>
fetchVmStats(vm, this.state.granularity).then(
stats => ({
vm: vm.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)
})
})
}
loop = ::this.loop
handleSelectStats (event) {
const granularity = event.target.value
clearTimeout(this.timeout)
this.setState({
granularity,
selectStatsLoading: true
}, this.loop)
}
handleSelectStats = ::this.handleSelectStats
componentWillMount () {
this.loop()
}
componentWillUnmount () {
clearTimeout(this.timeout)
}
render () {
const {
granularity,
selectStatsLoading,
stats,
useCombinedValues
} = this.state
return !stats
? <p>No stats.</p>
: process.env.XOA_PLAN > 2
? <Container>
<Row>
<Col mediumSize={5}>
<div className='form-group'>
<Tooltip content={_('useStackedValuesOnStats')}>
<Toggle value={useCombinedValues} onChange={this.linkState('useCombinedValues')} />
</Tooltip>
</div>
</Col>
<Col mediumSize={1}>
{selectStatsLoading && (
<div className='text-xs-right'>
<Icon icon='loading' size={2} />
</div>
)}
</Col>
<Col mediumSize={6}>
<div className='btn-tab'>
<select className='form-control' onChange={this.handleSelectStats} defaultValue={granularity} >
{_('statLastTenMinutes', message => <option value='seconds'>{message}</option>)}
{_('statLastTwoHours', message => <option value='minutes'>{message}</option>)}
{_('statLastWeek', message => <option value='hours'>{message}</option>)}
{_('statLastYear', message => <option value='days'>{message}</option>)}
</select>
</div>
</Col>
</Row>
<Row>
<Col mediumSize={6}>
<h5 className='text-xs-center'><Icon icon='cpu' size={1} /> {_('statsCpu')}</h5>
<VmGroupCpuLineChart addSumSeries={useCombinedValues} data={stats} />
</Col>
<Col mediumSize={6}>
<h5 className='text-xs-center'><Icon icon='memory' size={1} /> {_('statsMemory')}</h5>
<VmGroupMemoryLineChart addSumSeries={useCombinedValues} data={stats} />
</Col>
</Row>
<br />
<hr />
<Row>
<Col mediumSize={6}>
<h5 className='text-xs-center'><Icon icon='network' size={1} /> {_('statsNetwork')}</h5>
<VmGroupVifLineChart key={useCombinedValues ? 'stacked' : 'unstacked'} addSumSeries={useCombinedValues} data={stats} />
</Col>
<Col mediumSize={6}>
<h5 className='text-xs-center'><Icon icon='disk' size={1} /> {_('statDisk')}</h5>
<VmGroupXvdLineChart addSumSeries={useCombinedValues} data={stats} />
</Col>
</Row>
</Container>
: <Container><Upgrade place='hostStats' available={3} /></Container>
}
}

View File

@@ -1,9 +1,8 @@
import _ from 'intl'
import ActionBar, { Action } from 'action-bar'
import ActionBar from 'action-bar'
import React from 'react'
import { addSubscriptions, connectStore } from 'utils'
import { find, includes } from 'lodash'
import { createSelector, getCheckPermissions, getUser } from 'selectors'
import { connectStore } from 'utils'
import { includes } from 'lodash'
import { isAdmin } from 'selectors'
import {
cloneVm,
copyVm,
@@ -13,163 +12,142 @@ import {
resumeVm,
snapshotVm,
startVm,
stopVm,
subscribeResourceSets
stopVm
} from 'xo'
const vmActionBarByState = {
Running: ({ vm, isSelfUser, canAdministrate }) => (
Running: ({ isAdmin, vm }) => (
<ActionBar
display='icon'
handlerParam={vm}
>
<Action
handler={stopVm}
icon='vm-stop'
label={_('stopVmLabel')}
pending={includes(vm.current_operations, 'clean_shutdown')}
/>
<Action
handler={restartVm}
icon='vm-reboot'
label={_('rebootVmLabel')}
pending={includes(vm.current_operations, 'clean_reboot')}
/>
{!isSelfUser && <Action
handler={migrateVm}
icon='vm-migrate'
label={_('migrateVmLabel')}
pending={
includes(vm.current_operations, 'migrate_send') ||
includes(vm.current_operations, 'pool_migrate')
actions={[
{
icon: 'vm-stop',
label: 'stopVmLabel',
handler: stopVm,
pending: includes(vm.current_operations, 'clean_shutdown')
},
{
icon: 'vm-reboot',
label: 'rebootVmLabel',
handler: restartVm,
pending: includes(vm.current_operations, 'clean_reboot')
},
(isAdmin || !vm.resourceSet) && {
icon: 'vm-migrate',
label: 'migrateVmLabel',
handler: migrateVm,
pending:
includes(vm.current_operations, 'migrate_send') ||
includes(vm.current_operations, 'pool_migrate')
},
(isAdmin || !vm.resourceSet) && {
icon: 'vm-snapshot',
label: 'snapshotVmLabel',
handler: snapshotVm,
pending: includes(vm.current_operations, 'snapshot')
},
(isAdmin || !vm.resourceSet) && {
icon: 'export',
label: 'exportVmLabel',
handler: exportVm,
pending: includes(vm.current_operations, 'export')
},
(isAdmin || !vm.resourceSet) && {
icon: 'vm-copy',
label: 'copyVmLabel',
handler: copyVm,
pending: includes(vm.current_operations, 'copy')
}
/>}
{!isSelfUser && <Action
handler={snapshotVm}
icon='vm-snapshot'
label={_('snapshotVmLabel')}
pending={includes(vm.current_operations, 'snapshot')}
/>}
{!isSelfUser && canAdministrate && <Action
handler={exportVm}
icon='export'
label={_('exportVmLabel')}
pending={includes(vm.current_operations, 'export')}
/>}
{!isSelfUser && canAdministrate && <Action
handler={copyVm}
icon='vm-copy'
label={_('copyVmLabel')}
pending={includes(vm.current_operations, 'copy')}
/>}
</ActionBar>
),
Halted: ({ vm, isSelfUser, canAdministrate }) => (
<ActionBar
]}
display='icon'
handlerParam={vm}
>
<Action
handler={startVm}
icon='vm-start'
label={_('startVmLabel')}
pending={includes(vm.current_operations, 'start')}
/>
{!isSelfUser && canAdministrate && <Action
handler={cloneVm}
icon='vm-fast-clone'
label={_('fastCloneVmLabel')}
pending={includes(vm.current_operations, 'clone')}
/>}
{!isSelfUser && <Action
handler={migrateVm}
icon='vm-migrate'
label={_('migrateVmLabel')}
pending={
includes(vm.current_operations, 'migrate_send') ||
includes(vm.current_operations, 'pool_migrate')
param={vm}
/>
),
Halted: ({ isAdmin, vm }) => (
<ActionBar
actions={[
{
icon: 'vm-start',
label: 'startVmLabel',
handler: startVm,
pending: includes(vm.current_operations, 'start')
},
(isAdmin || !vm.resourceSet) && {
icon: 'vm-fast-clone',
label: 'fastCloneVmLabel',
handler: cloneVm,
pending: includes(vm.current_operations, 'clone')
},
(isAdmin || !vm.resourceSet) && {
icon: 'vm-migrate',
label: 'migrateVmLabel',
handler: migrateVm,
pending: includes(vm.current_operations, 'pool_migrate')
},
(isAdmin || !vm.resourceSet) && {
icon: 'vm-snapshot',
label: 'snapshotVmLabel',
handler: snapshotVm,
pending: includes(vm.current_operations, 'snapshot')
},
(isAdmin || !vm.resourceSet) && {
icon: 'export',
label: 'exportVmLabel',
handler: exportVm,
pending: includes(vm.current_operations, 'export')
},
(isAdmin || !vm.resourceSet) && {
icon: 'vm-copy',
label: 'copyVmLabel',
handler: copyVm,
pending: includes(vm.current_operations, 'copy')
}
/>}
{!isSelfUser && <Action
handler={snapshotVm}
icon='vm-snapshot'
label={_('snapshotVmLabel')}
pending={includes(vm.current_operations, 'snapshot')}
/>}
{!isSelfUser && canAdministrate && <Action
handler={exportVm}
icon='export'
label={_('exportVmLabel')}
pending={includes(vm.current_operations, 'export')}
/>}
{!isSelfUser && canAdministrate && <Action
handler={copyVm}
icon='vm-copy'
label={_('copyVmLabel')}
pending={includes(vm.current_operations, 'copy')}
/>}
</ActionBar>
),
Suspended: ({ vm, isSelfUser, canAdministrate }) => (
<ActionBar
]}
display='icon'
handlerParam={vm}
>
<Action
handler={resumeVm}
icon='vm-start'
label={_('resumeVmLabel')}
pending={includes(vm.current_operations, 'start')}
/>
{!isSelfUser && <Action
handler={snapshotVm}
icon='vm-snapshot'
label={_('snapshotVmLabel')}
pending={includes(vm.current_operations, 'snapshot')}
/>}
{!isSelfUser && canAdministrate && <Action
handler={exportVm}
icon='export'
label={_('exportVmLabel')}
pending={includes(vm.current_operations, 'export')}
/>}
{!isSelfUser && canAdministrate && <Action
handler={copyVm}
icon='vm-copy'
label={_('copyVmLabel')}
pending={includes(vm.current_operations, 'copy')}
/>}
</ActionBar>
param={vm}
/>
),
Suspended: ({ isAdmin, vm }) => (
<ActionBar
actions={[
{
icon: 'vm-start',
label: 'resumeVmLabel',
handler: resumeVm,
pending: includes(vm.current_operations, 'start')
},
(isAdmin || !vm.resourceSet) && {
icon: 'vm-snapshot',
label: 'snapshotVmLabel',
handler: snapshotVm,
pending: includes(vm.current_operations, 'snapshot')
},
(isAdmin || !vm.resourceSet) && {
icon: 'export',
label: 'exportVmLabel',
handler: exportVm,
pending: includes(vm.current_operations, 'export')
},
(isAdmin || !vm.resourceSet) && {
icon: 'vm-copy',
label: 'copyVmLabel',
handler: copyVm,
pending: includes(vm.current_operations, 'copy')
}
]}
display='icon'
param={vm}
/>
)
}
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 VmActionBar = connectStore({
isAdmin
})(({ isAdmin, vm }) => {
const ActionBar = vmActionBarByState[vm.power_state]
if (!ActionBar) {
return <p>No action bar for state {vm.power_state}</p>
}
return <ActionBar vm={vm} isSelfUser={_getIsSelfUser()} canAdministrate={_getCanAdministrate()} />
}))
return <ActionBar isAdmin={isAdmin} vm={vm} />
})
export default VmActionBar

View File

@@ -75,7 +75,7 @@ import TabAdvanced from './tab-advanced'
const getSrs = createGetObjectsOfType('SR').pick(
createSelector(
getVdis,
vdis => map(vdis, '$SR')
vdis => map(vdis, vdi => vdi.$SR)
)
)
@@ -230,6 +230,7 @@ export default class Vm extends BaseComponent {
</div>
</Col>
</Row>
<br />
<Row>
<Col>
<NavTabs>
@@ -252,6 +253,7 @@ export default class Vm extends BaseComponent {
render () {
const { container, vm } = this.props
if (!vm) {
return <h1>{_('statusLoading')}</h1>
}

View File

@@ -1,10 +1,9 @@
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 isEmpty from 'lodash/isEmpty'
import React from 'react'
import renderXoItem from 'render-xo-item'
import TabButton from 'tab-button'
@@ -12,30 +11,25 @@ import Tooltip from 'tooltip'
import { Toggle } from 'form'
import { Number, Size, Text, XoSelect } from 'editable'
import { Container, Row, Col } from 'grid'
import { SelectVgpuType } from 'select-objects'
import { confirm } from 'modal'
import {
every,
includes,
isEmpty,
map,
uniq
} from 'lodash'
import {
connectStore,
firstDefined,
formatSize,
getCoresPerSocketPossibilities,
normalizeXenToolsStatus,
osFamily
} from 'utils'
import {
createVgpu,
cloneVm,
convertVmToTemplate,
deleteVgpu,
deleteVm,
editVm,
isVmRunning,
recoveryStartVm,
restartVm,
resumeVm,
@@ -120,69 +114,6 @@ class AffinityHost extends Component {
}
}
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 <Container>
<Row>
<Col size={6}>
{_('vmSelectVgpuType')}
</Col>
<Col size={6}>
<SelectVgpuType
onChange={this.linkState('vgpuType')}
predicate={this._getPredicate()}
/>
</Col>
</Row>
</Container>
}
}
class Vgpus extends Component {
_createVgpu = vgpuType => confirm({
icon: 'gpu',
title: _('vmAddVgpu'),
body: <NewVgpu vm={this.props.vm} />
}).then(({ vgpuType }) =>
createVgpu(this.props.vm, { vgpuType, gpuGroup: vgpuType.gpuGroup })
)
render () {
const { vgpus, vm } = this.props
return <div>
{map(vgpus, vgpu => <span key={vgpu.id} className='mb-1'>
{!isVmRunning(vm) && <ActionButton
handler={deleteVgpu}
handlerParam={vgpu}
icon='delete'
size='small'
/>}
{' '}
{renderXoItem(vgpu)}
</span>)}
{isEmpty(vgpus) && <span>
{!isVmRunning(vm) && <ActionButton
handler={this._createVgpu}
icon='add'
size='small'
/>}
{' '}
{_('vmVgpuNone')}
</span>}
</div>
}
}
class CoresPerSocket extends Component {
_getCoresPerSocketPossibilities = createSelector(
() => {
@@ -214,21 +145,19 @@ class CoresPerSocket extends Component {
onChange={this._onChange}
value={selectedCoresPerSocket || ''}
>
{_('vmChooseCoresPerSocket', message => <option key='none' value=''>{message}</option>)}
{_('vmChooseCoresPerSocket', message => <option value=''>{message}</option>)}
{this._selectedValueIsNotInOptions() &&
_('vmCoresPerSocketIncorrectValue', message => <option key='incorrect' value={selectedCoresPerSocket}> {message}</option>)
_('vmCoresPerSocketIncorrectValue', message => <option value={selectedCoresPerSocket}> {message}</option>)
}
{map(
options,
coresPerSocket => <option
key={coresPerSocket}
value={coresPerSocket}
>
{_('vmCoresPerSocket', {
coresPerSocket => _(
'vmCoresPerSocket', {
nSockets: vm.CPUs.number / coresPerSocket,
nCores: coresPerSocket
})}
</option>
},
message => <option key={coresPerSocket} value={coresPerSocket}>{message}</option>
)
)}
</select>
{' '}
@@ -244,35 +173,8 @@ class CoresPerSocket extends Component {
}
}
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,
vgpus: getVgpus,
vgpuTypes: getVgpuTypes
}
})(({
export default ({
container,
gpuGroup,
vgpus,
vgpuTypes,
vm
}) => <Container>
<Row>
@@ -413,12 +315,6 @@ export default connectStore(() => {
<AffinityHost vm={vm} />
</td>
</tr>
{vm.virtualizationMode === 'hvm' && <tr>
<th>{_('vmVgpus')}</th>
<td>
<Vgpus vgpus={vgpus} vm={vm} />
</td>
</tr>}
{vm.virtualizationMode === 'hvm' &&
<tr>
<th>{_('vmVga')}</th>
@@ -471,8 +367,8 @@ export default connectStore(() => {
<tr>
<th>{_('vmMemoryLimitsLabel')}</th>
<td>
<p>Static: {formatSize(vm.memory.static[0])}/<Size value={defined(vm.memory.static[1], null)} onChange={memoryStaticMax => editVm(vm, { memoryStaticMax })} /></p>
<p>Dynamic: <Size value={defined(vm.memory.dynamic[0], null)} onChange={memoryMin => editVm(vm, { memoryMin })} />/<Size value={defined(vm.memory.dynamic[1], null)} onChange={memoryMax => editVm(vm, { memoryMax })} /></p>
<p>Static: {formatSize(vm.memory.static[0])}/<Size value={firstDefined(vm.memory.static[1], null)} onChange={memoryStaticMax => editVm(vm, { memoryStaticMax })} /></p>
<p>Dynamic: <Size value={firstDefined(vm.memory.dynamic[0], null)} onChange={memoryMin => editVm(vm, { memoryMin })} />/<Size value={firstDefined(vm.memory.dynamic[1], null)} onChange={memoryMax => editVm(vm, { memoryMax })} /></p>
</td>
</tr>
</tbody>
@@ -496,7 +392,7 @@ export default connectStore(() => {
</tr>
<tr>
<th>{_('osKernel')}</th>
<td>{(vm.os_version && vm.os_version.uname) || _('unknownOsKernel')}</td>
<td>{vm.os_version ? vm.os_version.uname ? vm.os_version.uname : _('unknownOsKernel') : _('unknownOsKernel')}</td>
</tr>
</tbody>
</table>
@@ -512,4 +408,4 @@ export default connectStore(() => {
</table>
</Col>
</Row>
</Container>)
</Container>

View File

@@ -150,6 +150,7 @@ export default class TabConsole extends Component {
scale={scale}
url={resolveUrl(`consoles/${vm.id}`)}
/>
{!minimalLayout && <p><em><Icon icon='info' /> <a href='https://bugs.xenserver.org/browse/XSO-650' target='_blank'>{_('tipLabel')} {_('tipConsoleLabel')}</a></em></p>}
</Col>
</Row>
</Container>

View File

@@ -2,33 +2,28 @@ 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 DragNDropOrder from 'drag-n-drop-order'
import forEach from 'lodash/forEach'
import isEmpty from 'lodash/isEmpty'
import IsoDevice from 'iso-device'
import Link from 'link'
import map from 'lodash/map'
import propTypes from 'prop-types-decorator'
import React from 'react'
import SingleLineRow from 'single-line-row'
import some from 'lodash/some'
import StateButton from 'state-button'
import TabButton from 'tab-button'
import Tooltip from 'tooltip'
import { Container, Row, Col } from 'grid'
import { createSelector, createFinder, getCheckPermissions, isAdmin } from 'selectors'
import { DragDropContext, DragSource, DropTarget } from 'react-dnd'
import { createSelector } from 'selectors'
import { injectIntl } from 'react-intl'
import { noop, addSubscriptions, formatSize, connectStore, resolveResourceSet } from 'utils'
import { SelectSr, SelectVdi, SelectResourceSetsSr } from 'select-objects'
import { noop } from 'utils'
import { SelectSr, SelectVdi } 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,
@@ -41,53 +36,39 @@ import {
isVmRunning,
migrateVdi,
setBootableVbd,
setVmBootOrder,
subscribeResourceSets
setVmBootOrder
} 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 {
constructor (props) {
super(props)
this.state = {
sr: undefined
}
}
_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)
const {name, size, bootable, readOnly} = this.refs
const { sr } = this.state
return createDisk(name.value, size.value, sr)
.then(diskId => {
const mode = readOnly.value ? 'RO' : 'RW'
return attachDiskToVm(diskId, vm, {
bootable: bootable && bootable.value,
mode
})
.then(onClose)
})
}
_selectSr = sr => this.setState({sr})
// FIXME: duplicate code
_getSrPredicate = createSelector(
() => {
@@ -97,66 +78,31 @@ class NewDisk extends Component {
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 { vm } = 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 <form id='newDiskForm'>
<div className='form-group'>
<SelectSr_
onChange={this.linkState('sr')}
predicate={this._getSrPredicate()}
required
resourceSet={isAdmin ? undefined : resourceSet}
value={sr}
/>
<SelectSr predicate={this._getSrPredicate()} onChange={this._selectSr} required />
</div>
<fieldset className='form-inline'>
<div className='form-group'>
<input type='text' onChange={this.linkState('name')} value={name} placeholder={formatMessage(messages.vbdNamePlaceHolder)} className='form-control' required />
<input type='text' ref='name' placeholder={formatMessage(messages.vbdNamePlaceHolder)} className='form-control' required />
</div>
{' '}
<div className='form-group'>
<SizeInput onChange={this.linkState('size')} value={size} placeholder={formatMessage(messages.vbdSizePlaceHolder)} required />
<SizeInput ref='size' placeholder={formatMessage(messages.vbdSizePlaceHolder)} required />
</div>
{' '}
<div className='form-group'>
{vm.virtualizationMode === 'pv' && <span>{_('vbdBootable')} <Toggle onChange={this.toggleState('bootable')} value={bootable} /> </span>}
<span>{_('vbdReadonly')} <Toggle onChange={this.toggleState('readOnly')} value={readOnly} /></span>
{vm.virtualizationMode === 'pv' && <span>{_('vbdBootable')} <Toggle ref='bootable' /> </span>}
<span>{_('vbdReadonly')} <Toggle ref='readOnly' /></span>
</div>
<span className='pull-right'>
<ActionButton form='newDiskForm' icon='add' btnStyle='primary' handler={this._createDisk} disabled={diskLimit < size}>{_('vbdCreate')}</ActionButton>
<ActionButton form='newDiskForm' icon='add' btnStyle='primary' handler={this._createDisk}>{_('vbdCreate')}</ActionButton>
</span>
</fieldset>
{resourceSet != null && diskLimit != null && (
diskLimit < size
? <em className='text-danger'>{_('notEnoughSpaceInResourceSet', { resourceSet: <strong>{resourceSet.name}</strong>, spaceLeft: formatSize(diskLimit) })}</em>
: <em>{_('useQuotaWarning', { resourceSet: <strong>{resourceSet.name}</strong>, spaceLeft: formatSize(diskLimit) })}</em>
)}
</form>
}
}
@@ -188,16 +134,18 @@ class AttachDisk extends Component {
_addVdi = () => {
const { vm, vbds, onClose = noop } = this.props
const { bootable, readOnly, vdi } = this.state
const { vdi } = this.state
const { bootable, readOnly } = this.refs
const _isFreeForWriting = vdi => vdi.$VBDs.length === 0 || some(vdi.$VBDs, id => {
const vbd = vbds[id]
return !vbd || !vbd.attached || vbd.read_only
})
const mode = readOnly.value || !_isFreeForWriting(vdi) ? 'RO' : 'RW'
return attachDiskToVm(vdi, vm, {
bootable,
mode: readOnly || !_isFreeForWriting(vdi) ? 'RO' : 'RW'
}).then(onClose)
bootable: bootable && bootable.value,
mode
})
.then(onClose)
}
render () {
@@ -226,129 +174,6 @@ class AttachDisk extends Component {
}
}
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(
<li className='list-group-item'>
<Icon icon='grab' />
{' '}
<Icon icon='grab' />
{' '}
{item.text}
<span className='pull-right'>
<Toggle value={item.active} onChange={this._toggle} />
</span>
</li>
))
}
}
@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 <form>
<ul>
{map(order, (item, index) => <OrderItem
key={index}
index={index}
id={item.id}
// FIXME missing translation
item={item}
move={this._moveOrderItem}
/>)}
</ul>
<fieldset className='form-inline'>
<span className='pull-right'>
<ActionButton icon='save' btnStyle='primary' handler={this._save}>{_('saveBootOption')}</ActionButton>
{' '}
<ActionButton icon='reset' handler={this._reset}>{_('resetBootOption')}</ActionButton>
</span>
</fieldset>
</form>
}
}
class MigrateVdiModalBody extends Component {
get value () {
return this.state
@@ -380,10 +205,6 @@ class MigrateVdiModalBody extends Component {
}
}
@connectStore(() => ({
checkPermissions: getCheckPermissions,
isAdmin
}))
export default class TabDisks extends Component {
constructor (props) {
super(props)
@@ -394,6 +215,34 @@ export default class TabDisks extends Component {
}
}
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}
}
setVmBootOrder = (_, newOrder) => {
const { vm } = this.props
let order = ''
forEach(newOrder, item => { item.active && (order += item.id) })
return setVmBootOrder(vm, order)
.then(noop)
}
_toggleNewDisk = () => this.setState({
newDisk: !this.state.newDisk,
attachDisk: false,
@@ -426,19 +275,6 @@ export default class TabDisks extends Component {
})
}
_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,
@@ -462,12 +298,12 @@ export default class TabDisks extends Component {
icon='add'
labelId='vbdCreateDeviceButton'
/>
{this._getAttachDiskPredicate() && <TabButton
<TabButton
btnStyle={attachDisk ? 'info' : 'primary'}
handler={this._toggleAttachDisk}
icon='disk'
labelId='vdiAttachDeviceButton'
/>}
/>
{vm.virtualizationMode !== 'pv' && <TabButton
btnStyle={bootOrder ? 'info' : 'primary'}
handler={this._toggleBootOrder}
@@ -479,8 +315,8 @@ export default class TabDisks extends Component {
<Row>
<Col>
{newDisk && <div><NewDisk vm={vm} onClose={this._toggleNewDisk} /><hr /></div>}
{attachDisk && <div><AttachDisk vm={vm} vbds={vbds} onClose={this._toggleAttachDisk} /><hr /></div>}
{bootOrder && <div><BootOrder vm={vm} onClose={this._toggleBootOrder} /><hr /></div>}
{attachDisk && <div><AttachDisk vm={vm} vbds={vbds} /><hr /></div>}
{bootOrder && <div><DragNDropOrder parseOrderParam={vm.boot && vm.boot.order} parseOrder={this.parseBootOrder} setOrder={this.setVmBootOrder} toggleItems onClose={this._toggleBootOrder} /><hr /></div>}
</Col>
</Row>
<Row>

View File

@@ -1,26 +1,20 @@
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 { createGetVmLastShutdownTime } from 'selectors'
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,
firstDefined,
formatSize,
osFamily
} from 'utils'
@@ -32,33 +26,11 @@ import {
} 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
}
return { lastShutdownTime: createGetVmLastShutdownTime() }
})(
({
lastShutdownTime,
statsOverview,
vgpu,
vgpuTypes,
vm,
vmTotalDiskSpace
}) => <Container>
@@ -71,7 +43,7 @@ export default connectStore(() => {
</Col>
<Col mediumSize={3}>
<h2 className='form-inline'>
<Size value={defined(vm.memory.dynamic[1], null)} onChange={memory => editVm(vm, { memory })} />
<Size value={firstDefined(vm.memory.dynamic[1], null)} onChange={memory => editVm(vm, { memory })} />
&nbsp;<span><Icon icon='memory' size='lg' /></span>
</h2>
<BlockLink to={`/vms/${vm.id}/stats`}>{statsOverview && <MemorySparkLines data={statsOverview} />}</BlockLink>
@@ -108,31 +80,19 @@ export default connectStore(() => {
: _('hardwareVirtualizedMode')
}
</p>
{vgpu !== undefined && <p>
{renderXoItem(vgpuTypes[vgpu.vgpuType])}
</p>}
</Col>
<Col mediumSize={3}>
<BlockLink to={`/vms/${vm.id}/network`}>
{vm.addresses && vm.addresses['0/ip']
? <Copiable tagName='p'>
{vm.addresses['0/ip']}
</Copiable>
: <p>{_('noIpv4Record')}</p>
}
<Copiable tagName='p'>
{vm.addresses && vm.addresses['0/ip']
? vm.addresses['0/ip']
: _('noIpv4Record')
}
</Copiable>
</BlockLink>
</Col>
<Col mediumSize={3}>
<BlockLink to={`/vms/${vm.id}/advanced`}>
<Tooltip content={vm.os_version ? vm.os_version.name : _('unknownOsName')}>
<h1>
<Icon
className='text-info'
icon={vm.os_version && vm.os_version.distro && osFamily(vm.os_version.distro)}
/>
</h1>
</Tooltip>
</BlockLink>
<BlockLink to={`/vms/${vm.id}/advanced`}><Tooltip content={vm.os_version ? vm.os_version.name : _('unknownOsName')}><h1><Icon className='text-info' icon={vm.os_version && vm.os_version.distro && osFamily(vm.os_version.distro)} /></h1></Tooltip></BlockLink>
</Col>
</Row>
{!vm.xenTools && vm.power_state === 'Running' &&

View File

@@ -403,7 +403,7 @@ export default class TabNetwork extends BaseComponent {
</tr>
</thead>
<tbody>
{map(vm.VIFs, vif => <VifItem key={vif} vifId={vif} isVmRunning={isVmRunning(vm)} resourceSet={vm.resourceSet} />)}
{map(vm.VIFs, vif => <VifItem vifId={vif} isVmRunning={isVmRunning(vm)} resourceSet={vm.resourceSet} />)}
</tbody>
</table>
{vm.addresses && !isEmpty(vm.addresses)

View File

@@ -1,7 +1,9 @@
import _ from 'intl'
import ActionRowButton from 'action-row-button'
import ButtonGroup from 'button-group'
import Icon from 'icon'
import isEmpty from 'lodash/isEmpty'
import React, { Component } from 'react'
import SortedTable from 'sorted-table'
import TabButton from 'tab-button'
import Tooltip from 'tooltip'
import { connectStore } from 'utils'
@@ -10,7 +12,7 @@ import { Container, Row, Col } from 'grid'
import { Text } from 'editable'
import {
includes,
isEmpty
map
} from 'lodash'
import {
createGetObjectsOfType
@@ -24,77 +26,6 @@ import {
snapshotVm
} from 'xo'
const COLUMNS = [
{
itemRenderer: snapshot =>
<div>
<FormattedTime
value={snapshot.snapshot_time * 1000}
day='numeric'
hour='numeric'
minute='numeric'
month='long'
year='numeric'
/>
{' '}
(<FormattedRelative value={snapshot.snapshot_time * 1000} />)
{' '}
{includes(snapshot.tags, 'quiesce') &&
<Tooltip content={_('snapshotQuiesce')}>
<Icon icon='info' />
</Tooltip>
}
</div>,
default: true,
name: _('snapshotDate'),
sortCriteria: _ => _.snapshot_time,
sortOrder: 'desc'
},
{
itemRenderer: snapshot =>
<Text
onChange={value => editVm(snapshot, {name_label: value})}
value={snapshot.name_label}
/>,
name: _('snapshotName'),
sortCriteria: _ => _.name_label
},
{
itemRenderer: snapshot =>
<Text
onChange={value => editVm(snapshot, {name_description: value})}
value={snapshot.name_description}
/>,
name: _('snapshotDescription'),
sortCriteria: _ => _.name_description
}
]
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
@@ -125,11 +56,65 @@ export default class TabSnapshot extends Component {
</Row>
: <Row>
<Col>
<SortedTable
collection={snapshots}
columns={COLUMNS}
individualActions={INDIVIDUAL_ACTIONS}
/>
<table className='table'>
<thead className='thead-default'>
<tr>
<th>{_('snapshotDate')}</th>
<th>{_('snapshotName')}</th>
<th>{_('snapshotAction')}</th>
</tr>
</thead>
<tbody>
{map(snapshots, snapshot =>
<tr key={snapshot.id}>
<td>
<FormattedTime value={snapshot.snapshot_time * 1000} minute='numeric' hour='numeric' day='numeric' month='long' year='numeric' /> (<FormattedRelative value={snapshot.snapshot_time * 1000} />)
{' '}
{includes(snapshot.tags, 'quiesce') && <Tooltip content={_('snapshotQuiesce')}><Icon icon='info' /></Tooltip>}
</td>
<td>
<Text value={snapshot.name_label} onChange={value => editVm(snapshot, {name_label: value})} />
</td>
<td>
<ButtonGroup>
<Tooltip content={_('copySnapshot')}>
<ActionRowButton
btnStyle='primary'
handler={copyVm}
handlerParam={snapshot}
icon='vm-copy'
/>
</Tooltip>
<Tooltip content={_('exportSnapshot')}>
<ActionRowButton
btnStyle='primary'
handler={exportVm}
handlerParam={snapshot}
icon='export'
/>
</Tooltip>
<Tooltip content={_('revertSnapshot')}>
<ActionRowButton
btnStyle='warning'
handler={revertSnapshot}
handlerParam={snapshot}
icon='snapshot-revert'
/>
</Tooltip>
<Tooltip content={_('deleteSnapshot')}>
<ActionRowButton
btnStyle='danger'
handler={deleteSnapshot}
handlerParam={snapshot}
icon='delete'
/>
</Tooltip>
</ButtonGroup>
</td>
</tr>
)}
</tbody>
</table>
</Col>
</Row>
}

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