Compare commits

..

22 Commits

Author SHA1 Message Date
Julien Fontanet
683d510aa6 5.0.3 2016-06-30 15:12:26 +02:00
Pierre Donias
ebd7e58f61 feat(home): bulk VM migration (#1187)
Fixes #1146
2016-06-30 15:04:21 +02:00
Fabrice Marsaud
9a498b54ac fix(menu): only display one icon for updates when collapsed (#1190)
Fixes #1188
2016-06-30 15:03:26 +02:00
ABHAMON Ronan
2687f45e6e fix(settings/plugins): set config value to undefined if value is null (#1189) 2016-06-30 14:31:43 +02:00
ABHAMON Ronan
f79a17fcec feat(json-schema-input): generate uiSchema JSON schema (#1182) 2016-06-30 13:52:20 +02:00
ABHAMON Ronan
8fd377d1e2 feat(dashboard/dataviz): parallel coordinates graph (#1174)
Fixes #1157
2016-06-30 11:36:29 +02:00
Olivier Lambert
fda06fbd29 feat(VM/network): VIFs management (#1186)
Fixes #1176
2016-06-30 11:28:25 +02:00
Fabrice Marsaud
cee4378e6d feat(xoa-updates): reload after upgrading (#1183)
Fixes #1131.
2016-06-30 11:26:03 +02:00
Fabrice Marsaud
ab6d342886 fix(VM/network): fix broken propTypes import (#1184) 2016-06-29 17:59:39 +02:00
Fabrice Marsaud
9954c08993 fix(xoa-updates): fix env test (#1181) 2016-06-29 12:16:25 +02:00
Julien Fontanet
3ae80aeab3 feat(link): expose Link and BlockLink components 2016-06-29 11:57:42 +02:00
Julien Fontanet
2a3534f659 chore(utils): do not re-export propTypes 2016-06-29 11:57:42 +02:00
Julien Fontanet
fc39de0d5a chore(sign-in): remove because unused 2016-06-29 11:57:41 +02:00
Julien Fontanet
64e4b79d41 chore(utils/createSimpleMatcher): remove because not used 2016-06-29 11:57:41 +02:00
Fabrice Marsaud
53887da3da feat(VM/network): VIF creation (#1173)
Fixes #1138.
2016-06-28 17:47:44 +02:00
ABHAMON Ronan
7c60d68f56 fix(xo-line-chart): set precision on LoadLineChart (#1175)
Fixes #1167
2016-06-28 17:17:46 +02:00
Julien Fontanet
2ac1b991b1 feat(BaseComponent#_linkedState): only allocate when necessary 2016-06-28 15:56:53 +02:00
Julien Fontanet
8257714cdb feat(get-event-value): works with checkbox/radio/select 2016-06-28 15:56:53 +02:00
Julien Fontanet
1b8bacbf5a chore(utils/autobind): remove in favor of ES7 class properties syntax 2016-06-28 15:56:52 +02:00
Julien Fontanet
1d5b84389d chore(utils): do not re-export invoke 2016-06-28 15:56:51 +02:00
Julien Fontanet
f7dcf52977 chore(utils/If): remove because does not work 2016-06-28 15:03:34 +02:00
Julien Fontanet
e26dd5147a feat(BaseComponent#linkState): creates a callback associated to a state entry 2016-06-28 14:56:51 +02:00
92 changed files with 1344 additions and 556 deletions

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "xo-web",
"version": "5.0.2",
"version": "5.0.3",
"license": "AGPL-3.0",
"description": "Web interface client for Xen-Orchestra",
"keywords": [

View File

@@ -5,7 +5,7 @@ import { Button } from 'react-bootstrap-4/lib'
import Component from './base-component'
import logError from './log-error'
import { autobind, propTypes } from './utils'
import propTypes from './prop-types'
@propTypes({
btnStyle: propTypes.string,
@@ -28,7 +28,6 @@ export default class ActionButton extends Component {
router: React.PropTypes.object
}
@autobind
async _execute () {
if (this.state.working) {
return
@@ -66,6 +65,7 @@ export default class ActionButton extends Component {
logError(error)
}
}
_execute = ::this._execute
_eventListener = event => {
event.preventDefault()

View File

@@ -1,6 +1,7 @@
import ActionButton from 'action-button'
import React from 'react'
import { propTypes } from 'utils'
import ActionButton from './action-button'
import propTypes from './prop-types'
const ActionToggle = ({ className, value, ...props }) =>
<ActionButton

View File

@@ -1,6 +1,7 @@
import forEach from 'lodash/forEach'
import { Component } from 'react'
import getEventValue from './get-event-value'
import invoke from './invoke'
import shallowEqual from './shallow-equal'
@@ -11,6 +12,8 @@ export default class BaseComponent extends Component {
// It really should have been done in React.Component!
this.state = {}
this._linkedState = null
if (process.env.NODE_ENV !== 'production') {
this.render = invoke(this.render, render => () => {
console.log('render', this.constructor.name)
@@ -20,6 +23,23 @@ export default class BaseComponent extends Component {
}
}
// See https://preactjs.com/guide/linked-state
linkState (name) {
let linkedState = this._linkedState
let cb
if (!linkedState) {
linkedState = this._linkedState = {}
} else if ((cb = linkedState[name])) {
return cb
}
return (linkedState[name] = event => {
this.setState({
[name]: getEventValue(event)
})
})
}
shouldComponentUpdate (newProps, newState) {
return !(
shallowEqual(this.props, newProps) &&

View File

@@ -1,5 +1,6 @@
import React from 'react'
import { propTypes } from 'utils'
import propTypes from './prop-types'
const CARD_STYLE = {
minHeight: '100%'

View File

@@ -1,7 +1,8 @@
import Component from 'base-component'
import Icon from 'icon'
import React from 'react'
import { propTypes } from 'utils'
import Component from './base-component'
import Icon from './icon'
import propTypes from './prop-types'
@propTypes({
children: propTypes.any.isRequired,

View File

@@ -3,7 +3,7 @@ import classNames from 'classnames'
import React, { createElement } from 'react'
import Icon from '../icon'
import { propTypes } from '../utils'
import propTypes from '../prop-types'
import styles from './index.css'

9
src/common/d3-utils.js vendored Normal file
View File

@@ -0,0 +1,9 @@
import forEach from 'lodash/forEach'
export function setStyles (style) {
forEach(style, (value, key) => {
this.style(key, value)
})
return this
}

View File

@@ -7,10 +7,11 @@ import React from 'react'
import _ from './intl'
import Component from './base-component'
import logError from './log-error'
import Icon from './icon'
import logError from './log-error'
import propTypes from './prop-types'
import Tooltip from './tooltip'
import { formatSize, propTypes } from './utils'
import { formatSize } from './utils'
import { SizeInput } from './form'
import {
SelectHost,

View File

@@ -1,6 +1,7 @@
import React from 'react'
import * as Grid from 'grid'
import { propTypes } from 'utils'
import * as Grid from './grid'
import propTypes from './prop-types'
export const LabelCol = propTypes({
children: propTypes.any.isRequired

View File

@@ -9,11 +9,10 @@ import {
} from 'react-bootstrap-4/lib'
import Component from '../base-component'
import propTypes from '../prop-types'
import {
autobind,
formatSizeRaw,
parseSize,
propTypes
parseSize
} from '../utils'
export Select from './select'
@@ -33,16 +32,14 @@ export class Password extends Component {
this.refs.field.value = value
}
@autobind
_generate () {
_generate = () => {
this.refs.field.value = randomPassword(8)
this.setState({
visible: true
})
}
@autobind
_toggleVisibility () {
_toggleVisibility = () => {
this.setState({
visible: !this.state.visible
})
@@ -107,8 +104,7 @@ export class Range extends Component {
}
}
@autobind
_handleChange (event) {
_handleChange = event => {
const { onChange } = this.props
const { value } = event.target

View File

@@ -1,8 +1,10 @@
import find from 'lodash/find'
import map from 'lodash/map'
import React, { Component } from 'react'
import { Select } from 'form'
import { propTypes } from 'utils'
import propTypes from '../prop-types'
import Select from './select'
@propTypes({
autoFocus: propTypes.bool,

View File

@@ -1,11 +1,12 @@
import React, { Component } from 'react'
import ReactSelect from 'react-select'
import { propTypes } from 'utils'
import {
AutoSizer,
VirtualScroll
} from 'react-virtualized'
import propTypes from '../prop-types'
const SELECT_MENU_STYLE = {
overflow: 'hidden'
}

View File

@@ -0,0 +1,18 @@
// If the param is an event, returns the value of it's target,
// otherwise returns the param.
const getEventValue = event => {
let target
if (!event || !(target = event.target)) {
return event
}
let type
return target.nodeName.toLowerCase() === 'input' && (
(type = target.type.toLowerCase()) === 'checkbox' ||
type === 'radio'
)
? target.checked
: target.value
}
export { getEventValue as default }

View File

@@ -1,6 +1,7 @@
import classNames from 'classnames'
import React from 'react'
import { propTypes } from 'utils'
import propTypes from './prop-types'
export const Col = propTypes({
className: propTypes.string,

View File

@@ -57,10 +57,7 @@ var messages = {
customJob: 'Custom Job',
userPage: 'User',
// ----- Sign in/out -----
usernameLabel: 'Username:',
passwordLabel: 'Password:',
signInButton: 'Sign in',
// ----- Sign out -----
signOut: 'Sign out',
// ----- Home view ------
@@ -453,6 +450,7 @@ var messages = {
vifStatusConnected: 'Connected',
vifStatusDisconnected: 'Disconnected',
vifIpAddresses: 'IP addresses',
vifMacAutoGenerate: 'Auto-generated if empty',
// ----- VM snapshot tab -----
noSnapshots: 'No snapshots',
@@ -678,14 +676,16 @@ var messages = {
deleteVmModalMessage: 'Are you sure you want to delete this VM? ALL VM DISKS WILL BE REMOVED',
deleteVmsModalMessage: 'Are you sure you want to delete {vms} VM{vms, plural, one {} other {s}}? ALL VM DISKS WILL BE REMOVED',
migrateVmModalTitle: 'Migrate VM',
migrateVmAdvancedModalSelectHost: 'Select a destination host:',
migrateVmAdvancedModalSelectNetwork: 'Select a migration network:',
migrateVmAdvancedModalSelectSrs: 'For each VDI, select an SR:',
migrateVmAdvancedModalSelectNetworks: 'For each VIF, select a network:',
migrateVmAdvancedModalName: 'Name',
migrateVmAdvancedModalSr: 'SR',
migrateVmAdvancedModalVif: 'VIF',
migrateVmAdvancedModalNetwork: 'Network',
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:',
migrateVmsSelectNetwork: 'Select a network on which to connect each VIF:',
migrateVmName: 'Name',
migrateVmSr: 'SR',
migrateVmVif: 'VIF',
migrateVmNetwork: 'Network',
importBackupModalTitle: 'Import a {name} Backup',
importBackupModalStart: 'Start VM after restore',
importBackupModalSelectBackup: 'Select your backup…',
@@ -780,6 +780,8 @@ var messages = {
mustUpgrade: 'You need to update your XOA (new version is available)',
registerNeeded: 'Your XOA is not registered for updates',
updaterError: 'Can\'t fetch update information',
promptUpgradeReloadTitle: 'Upgrade successful',
promptUpgradeReloadMessage: 'Your XOA has successfully upgraded, and your browser must reload the application. Do you want to reload now ?',
// ----- OS Disclaimer -----
disclaimerTitle: 'Xen Orchestra from the sources',

View File

@@ -2,6 +2,8 @@ import React from 'react'
import ActionButton from './action-button'
import Component from './base-component'
import propTypes from './prop-types'
import { connectStore } from './utils'
import { SelectVdi } from './select-objects'
import {
createGetObjectsOfType,
@@ -9,10 +11,6 @@ import {
createGetObject,
createSelector
} from './selectors'
import {
connectStore,
propTypes
} from './utils'
import {
ejectCd,
insertCd

View File

@@ -1,7 +1,6 @@
import { Component } from 'react'
import {
propTypes
} from 'utils'
import propTypes from '../prop-types'
// ===================================================================

View File

@@ -1,16 +1,12 @@
import _ from 'intl'
import React, { Component, cloneElement } from 'react'
import map from 'lodash/map'
import filter from 'lodash/filter'
import {
autobind,
propsEqual,
propTypes
} from 'utils'
import _ from '../intl'
import propTypes from '../prop-types'
import { propsEqual } from '../utils'
import GenericInput from './generic-input'
import {
descriptionRender,
forceDisplayOptionalAttr
@@ -76,15 +72,13 @@ export default class ArrayInput extends Component {
})
}
@autobind
_handleOptionalChange (event) {
_handleOptionalChange = event => {
this.setState({
use: event.target.checked
})
}
@autobind
_handleAdd () {
_handleAdd = () => {
const { children } = this.state
this.setState({
children: children.concat(this._makeChild(this.props))

View File

@@ -1,10 +1,7 @@
import React, { Component } from 'react'
import includes from 'lodash/includes'
import {
EMPTY_OBJECT,
propTypes
} from 'utils'
import propTypes from '../prop-types'
import { EMPTY_OBJECT } from '../utils'
import ArrayInput from './array-input'
import BooleanInput from './boolean-input'
@@ -13,55 +10,18 @@ import IntegerInput from './integer-input'
import NumberInput from './number-input'
import ObjectInput from './object-input'
import StringInput from './string-input'
import XoHighLevelObjectInput from './xo-highlevel-object-input'
import XoHostInput from './xo-host-input'
import XoPoolInput from './xo-pool-input'
import XoRemoteInput from './xo-remote-input'
import XoRoleInput from './xo-role-input'
import XoSrInput from './xo-sr-input'
import XoSubjectInput from './xo-subject-input'
import XoVmInput from './xo-vm-input'
import { getType } from './helpers'
// ===================================================================
const getType = (schema, attr = 'type') => {
if (!schema) {
return
}
const type = schema[attr]
if (Array.isArray(type)) {
if (includes(type, 'integer')) {
return 'integer'
}
if (includes(type, 'number')) {
return 'number'
}
return 'string'
}
return type
}
const getXoType = schema => getType(schema, 'xo:type')
const InputByType = {
array: ArrayInput,
boolean: BooleanInput,
host: XoHostInput,
integer: IntegerInput,
number: NumberInput,
object: ObjectInput,
pool: XoPoolInput,
remote: XoRemoteInput,
sr: XoSrInput,
string: StringInput,
vm: XoVmInput,
xoobject: XoHighLevelObjectInput,
role: XoRoleInput,
subject: XoSubjectInput
string: StringInput
}
// ===================================================================
@@ -104,14 +64,13 @@ export default class GenericInput extends Component {
return <EnumInput {...props} />
}
// $type = Job Creation Schemas && Old XO plugins.
const type = getXoType(schema) || getType(schema, '$type') || getType(schema)
const type = getType(schema)
const Input = uiSchema.widget || InputByType[type.toLowerCase()]
if (!Input) {
throw new Error(`Unsupported type: ${type}.`)
}
return <Input {...props} />
return <Input {...props} {...uiSchema.config} />
}
}

View File

@@ -1,10 +1,43 @@
import React from 'react'
import includes from 'lodash/includes'
import isArray from 'lodash/isArray'
import marked from 'marked'
import { Col, Row } from 'grid'
// ===================================================================
export const getType = schema => {
if (!schema) {
return
}
const type = schema.type
if (isArray(type)) {
if (includes(type, 'integer')) {
return 'integer'
}
if (includes(type, 'number')) {
return 'number'
}
return 'string'
}
return type
}
export const getXoType = schema => {
const type = schema && (schema['xo:type'] || schema.$type)
if (type) {
return type.toLowerCase()
}
}
// ===================================================================
export const descriptionRender = description =>
<span className='text-muted' dangerouslySetInnerHTML={{__html: marked(description || '')}} />

View File

@@ -1,2 +1 @@
import GenericInput from './generic-input'
export default GenericInput
export default from './generic-input'

View File

@@ -4,11 +4,8 @@ import forEach from 'lodash/forEach'
import includes from 'lodash/includes'
import map from 'lodash/map'
import {
autobind,
propsEqual,
propTypes
} from 'utils'
import propTypes from '../prop-types'
import { propsEqual } from '../utils'
import GenericInput from './generic-input'
@@ -81,8 +78,7 @@ export default class ObjectInput extends Component {
})
}
@autobind
_handleOptionalChange (event) {
_handleOptionalChange = event => {
const { checked } = event.target
this.setState({
@@ -98,6 +94,7 @@ export default class ObjectInput extends Component {
defaultValue = {}
} = props
const obj = {}
const { properties } = uiSchema
forEach(schema.properties, (childSchema, key) => {
obj[key] = (
@@ -108,7 +105,7 @@ export default class ObjectInput extends Component {
label={childSchema.title || key}
required={includes(schema.required, key)}
schema={childSchema}
uiSchema={uiSchema.properties}
uiSchema={properties && properties[key]}
defaultValue={defaultValue[key]}
/>
</ObjectItem>

View File

@@ -1,10 +1,14 @@
import React from 'react'
import AbstractInput from './abstract-input'
import propTypes from '../prop-types'
import { PrimitiveInputWrapper } from './helpers'
// ===================================================================
@propTypes({
password: propTypes.bool
})
export default class StringInput extends AbstractInput {
render () {
const { props } = this
@@ -20,7 +24,7 @@ export default class StringInput extends AbstractInput {
placeholder={props.placeholder}
ref='input'
required={props.required}
type={props.schema['xo:type'] === 'password' ? 'password' : 'text'}
type={props.password ? 'password' : 'text'}
/>
</PrimitiveInputWrapper>
)

59
src/common/link.js Normal file
View File

@@ -0,0 +1,59 @@
import Link from 'react-router/lib/Link'
import React from 'react'
import { routerShape } from 'react-router/lib/PropTypes'
import Component from './base-component'
import propTypes from './prop-types'
// ===================================================================
export { Link as default }
// -------------------------------------------------------------------
const _IGNORED_TAGNAMES = {
A: true,
BUTTON: true,
INPUT: true,
SELECT: true
}
@propTypes({
tagName: propTypes.string
})
export class BlockLink extends Component {
static contextTypes = {
router: routerShape
}
_style = { cursor: 'pointer' }
_onClickCapture = event => {
const { currentTarget } = event
let element = event.target
while (element !== currentTarget) {
if (_IGNORED_TAGNAMES[element.tagName]) {
return
}
element = element.parentNode
}
event.stopPropagation()
if (event.ctrlKey || event.button === 1) {
window.open(this.context.router.createHref(this.props.to))
} else {
this.context.router.push(this.props.to)
}
}
render () {
const { children, tagName = 'div' } = this.props
const Component = tagName
return (
<Component
style={this._style}
onClickCapture={this._onClickCapture}
>
{children}
</Component>
)
}
}

View File

@@ -4,7 +4,8 @@ import isArray from 'lodash/isArray'
import isString from 'lodash/isString'
import React, { Component, cloneElement } from 'react'
import { Button, Modal as ReactModal } from 'react-bootstrap-4/lib'
import { propTypes } from './utils'
import propTypes from './prop-types'
let instance

View File

@@ -1,7 +1,8 @@
import classNames from 'classnames'
import Link from 'react-router/lib/Link'
import React from 'react'
import Link from './link'
export const NavLink = ({ children, to }) => (
<li className='nav-item' role='tab'>
<Link className='nav-link' activeClassName='active' to={to}>

View File

@@ -1,6 +1,5 @@
import React, { Component } from 'react'
import { createBackoff } from 'jsonrpc-websocket-client'
import { propTypes } from 'utils'
import { RFB } from 'novnc-node'
import {
format as formatUrl,
@@ -8,6 +7,8 @@ import {
resolve as resolveUrl
} from 'url'
import propTypes from './prop-types'
const parseRelativeUrl = url => parseUrl(resolveUrl(String(window.location), url))
const PROTOCOL_ALIASES = {

View File

@@ -1,12 +1,13 @@
import Icon from 'icon'
import React, { Component } from 'react'
import { createGetObject } from 'selectors'
import { isSrWritable } from 'xo'
import Icon from './icon'
import propTypes from './prop-types'
import { createGetObject } from './selectors'
import { isSrWritable } from './xo'
import {
connectStore,
formatSize,
propTypes
} from 'utils'
formatSize
} from './utils'
// ===================================================================

View File

@@ -1,24 +1,22 @@
import Component from 'base-component'
import React from 'react'
import _ from 'intl'
import forEach from 'lodash/forEach'
import includes from 'lodash/includes'
import join from 'lodash/join'
import later from 'later'
import map from 'lodash/map'
import React from 'react'
import sortedIndex from 'lodash/sortedIndex'
import { Range } from 'form'
import { FormattedTime } from 'react-intl'
import { Col, Row } from 'grid'
import {
Panel,
Tab,
Tabs
} from 'react-bootstrap-4/lib'
import {
propTypes
} from 'utils'
import _ from './intl'
import Component from './base-component'
import propTypes from './prop-types'
import { Col, Row } from './grid'
import { Range } from './form'
// ===================================================================

View File

@@ -1,6 +1,4 @@
import Component from 'base-component'
import React from 'react'
import _ from 'intl'
import assign from 'lodash/assign'
import classNames from 'classnames'
import filter from 'lodash/filter'
@@ -9,31 +7,31 @@ import groupBy from 'lodash/groupBy'
import keyBy from 'lodash/keyBy'
import keys from 'lodash/keys'
import map from 'lodash/map'
import renderXoItem from 'render-xo-item'
import sortBy from 'lodash/sortBy'
import { parse as parseRemote } from 'xo-remote-parser'
import { Select } from 'form'
import _ from './intl'
import Component from './base-component'
import propTypes from './prop-types'
import renderXoItem from './render-xo-item'
import { Select } from './form'
import {
createFilter,
createGetObjectsOfType,
createGetTags,
createSelector
} from 'selectors'
} from './selectors'
import {
connectStore,
mapPlus,
propTypes
} from 'utils'
mapPlus
} from './utils'
import {
isSrWritable,
subscribeGroups,
subscribeRemotes,
subscribeRoles,
subscribeUsers
} from 'xo'
} from './xo'
// ===================================================================

View File

@@ -13,8 +13,9 @@ import size from 'lodash/size'
import slice from 'lodash/slice'
import { createSelector as create } from 'reselect'
import invoke from './invoke'
import shallowEqual from './shallow-equal'
import { EMPTY_ARRAY, EMPTY_OBJECT, invoke } from './utils'
import { EMPTY_ARRAY, EMPTY_OBJECT } from './utils'
// ===================================================================

View File

@@ -1,5 +1,6 @@
import React, { cloneElement } from 'react'
import { propTypes } from 'utils'
import propTypes from './prop-types'
const SINGLE_LINE_STYLE = { display: 'flex' }
const COL_STYLE = { margin: 'auto' }

View File

@@ -1,16 +1,16 @@
import Component from 'base-component'
import Icon from 'icon'
import React from 'react'
import SingleLineRow from 'single-line-row'
import ceil from 'lodash/ceil'
import debounce from 'lodash/debounce'
import map from 'lodash/map'
import { Pagination } from 'react-bootstrap-4/lib'
import { Portal } from 'react-overlays'
import { Container, Col } from 'grid'
import { create as createMatcher } from 'complex-matcher'
import { propTypes } from 'utils'
import Component from '../base-component'
import Icon from '../icon'
import propTypes from '../prop-types'
import SingleLineRow from '../single-line-row'
import { Container, Col } from '../grid'
import { create as createMatcher } from '../complex-matcher'
import {
createFilter,
createPager,

View File

@@ -1,8 +1,9 @@
import _ from 'intl'
import ActionButton from 'action-button'
import Icon from 'icon'
import React from 'react'
import { Link } from 'react-router'
import _ from './intl'
import ActionButton from './action-button'
import Icon from './icon'
import Link from './link'
const STYLE = {
marginBottom: '1em',

View File

@@ -1,9 +1,9 @@
import React from 'react'
import Icon from 'icon'
import map from 'lodash/map'
import Component from './base-component'
import { propTypes } from './utils'
import Icon from './icon'
import propTypes from './prop-types'
@propTypes({
labels: propTypes.arrayOf(React.PropTypes.string).isRequired,

View File

@@ -2,7 +2,6 @@ import * as actions from 'store/actions'
import every from 'lodash/every'
import forEach from 'lodash/forEach'
import humanFormat from 'human-format'
import includes from 'lodash/includes'
import isArray from 'lodash/isArray'
import isEmpty from 'lodash/isEmpty'
import isFunction from 'lodash/isFunction'
@@ -10,8 +9,7 @@ import isPlainObject from 'lodash/isPlainObject'
import isString from 'lodash/isString'
import map from 'lodash/map'
import mapValues from 'lodash/mapValues'
import propTypes from 'prop-types'
import React, { cloneElement } from 'react'
import React from 'react'
import { connect } from 'react-redux'
import BaseComponent from './base-component'
@@ -20,8 +18,6 @@ import invoke from './invoke'
export const EMPTY_ARRAY = Object.freeze([ ])
export const EMPTY_OBJECT = Object.freeze({ })
export { propTypes }
// ===================================================================
export const ensureArray = (value) => {
@@ -78,92 +74,6 @@ export const addSubscriptions = subscriptions => Component => {
// -------------------------------------------------------------------
const _bind = (fn, thisArg) => function bound () {
return fn.apply(thisArg, arguments)
}
const _defineProperty = Object.defineProperty
export const autobind = (target, key, {
configurable,
enumerable,
value: fn,
writable
}) => ({
configurable,
enumerable,
get () {
if (this === target) {
return fn
}
const bound = _bind(fn, this)
_defineProperty(this, key, {
configurable: true,
enumerable: false,
value: bound,
writable: true
})
return bound
},
set (newValue) {
// Cannot use assignment because it will call the setter on
// the prototype.
_defineProperty(this, key, {
configurable: true,
enumerable: true,
value: newValue,
writable: true
})
}
})
// -------------------------------------------------------------------
@propTypes({
tagName: propTypes.string
})
export class BlockLink extends React.Component {
static contextTypes = {
router: React.PropTypes.object
}
_style = { cursor: 'pointer' }
_onClickCapture = event => {
const { currentTarget } = event
let element = event.target
while (element !== currentTarget) {
if (includes(['A', 'INPUT', 'BUTTON', 'SELECT'], element.tagName)) {
return
}
element = element.parentNode
}
event.stopPropagation()
if (event.ctrlKey || event.button === 1) {
window.open(this.context.router.createHref(this.props.to))
} else {
this.context.router.push(this.props.to)
}
}
render () {
const { children, tagName = 'div' } = this.props
const Component = tagName
return (
<Component
style={this._style}
onClickCapture={this._onClickCapture}
>
{children}
</Component>
)
}
}
// -------------------------------------------------------------------
export const checkPropsState = (propsNames, stateNames) => Component => {
const nProps = propsNames && propsNames.length
const nState = stateNames && stateNames.length
@@ -255,18 +165,6 @@ export const connectStore = (mapStateToProps, opts = {}) => {
// -------------------------------------------------------------------
// Simple matcher to use in object filtering.
export const createSimpleMatcher = (pattern, valueGetter) => {
if (!pattern) {
return
}
pattern = pattern.toLowerCase()
return item => valueGetter(item).toLowerCase().indexOf(pattern) !== -1
}
// -------------------------------------------------------------------
export { default as Debug } from './debug'
// -------------------------------------------------------------------
@@ -344,24 +242,6 @@ export const osFamily = invoke({
// -------------------------------------------------------------------
// Experimental!
//
// ```js
// <If cond={user}>
// <p>user.name</p>
// <p>user.email</p>
// </If>
// ```
export const If = ({ cond, children }) => cond && children
? map(children, (child, key) => cloneElement(child, { key }))
: null
// -------------------------------------------------------------------
export { invoke }
// -------------------------------------------------------------------
export const formatSize = bytes => humanFormat(bytes, { scale: 'binary', unit: 'B' })
export const formatSizeRaw = bytes => humanFormat.raw(bytes, { scale: 'binary', unit: 'B' })
@@ -470,11 +350,3 @@ export function rethrow (cb) {
Promise.resolve(cb(error)).then(() => { throw error })
)
}
// -------------------------------------------------------------------
// If param is an event: returns the value associated to it
// Otherwise: returns param
export function getEventValue (param) {
return param && param.target ? param.target.value : param
}

View File

@@ -1,11 +1,12 @@
import _ from 'intl'
import classNames from 'classnames'
import every from 'lodash/every'
import Icon from 'icon'
import map from 'lodash/map'
import { propTypes } from 'utils'
import React, { Component, cloneElement } from 'react'
import _ from '../intl'
import Icon from '../icon'
import propTypes from '../prop-types'
import styles from './index.css'
const Wizard = ({ children }) => {

View File

@@ -0,0 +1,67 @@
import forEach from 'lodash/forEach'
import XoHighLevelObjectInput from './xo-highlevel-object-input'
import XoHostInput from './xo-host-input'
import XoPoolInput from './xo-pool-input'
import XoRemoteInput from './xo-remote-input'
import XoRoleInput from './xo-role-input'
import XoSrInput from './xo-sr-input'
import XoSubjectInput from './xo-subject-input'
import XoVmInput from './xo-vm-input'
import { getType, getXoType } from '../json-schema-input/helpers'
// ===================================================================
const XO_TYPE_TO_COMPONENT = {
host: XoHostInput,
xoobject: XoHighLevelObjectInput,
pool: XoPoolInput,
remote: XoRemoteInput,
role: XoRoleInput,
sr: XoSrInput,
subject: XoSubjectInput,
vm: XoVmInput
}
// ===================================================================
const buildStringInput = (uiSchema, key, xoType) => {
if (key === 'password') {
uiSchema.config = { password: true }
}
uiSchema.widget = XO_TYPE_TO_COMPONENT[xoType]
}
// ===================================================================
const _generateUiSchema = (schema, uiSchema, key) => {
const type = getType(schema)
if (type === 'object') {
const properties = uiSchema.properties = {}
forEach(schema.properties, (schema, key) => {
const subUiSchema = properties[key] = {}
_generateUiSchema(schema, subUiSchema, key)
})
} else if (type === 'array') {
const widget = XO_TYPE_TO_COMPONENT[getXoType(schema.items)]
if (widget) {
uiSchema.widget = widget
uiSchema.config = { multi: true }
} else {
const subUiSchema = uiSchema.items = {}
_generateUiSchema(schema.items, subUiSchema, key)
}
} else if (type === 'string') {
buildStringInput(uiSchema, key, getXoType(schema))
}
}
export const generateUiSchema = schema => {
const uiSchema = {}
_generateUiSchema(schema, uiSchema, '')
return uiSchema
}

View File

@@ -1,5 +1,5 @@
import map from 'lodash/map'
import AbstractInput from './abstract-input'
import AbstractInput from '../json-schema-input/abstract-input'
// ===================================================================

View File

@@ -2,7 +2,7 @@ import React from 'react'
import { SelectHighLevelObject } from 'select-objects'
import XoAbstractInput from './xo-abstract-input'
import { PrimitiveInputWrapper } from './helpers'
import { PrimitiveInputWrapper } from '../json-schema-input/helpers'
// ===================================================================
@@ -14,7 +14,7 @@ export default class HighLevelObjectInput extends XoAbstractInput {
<PrimitiveInputWrapper {...props}>
<SelectHighLevelObject
disabled={props.disabled}
multi={props.schema.type === 'array'}
multi={props.multi}
onChange={props.onChange}
ref='input'
required={props.required}

View File

@@ -2,7 +2,7 @@ import React from 'react'
import { SelectHost } from 'select-objects'
import XoAbstractInput from './xo-abstract-input'
import { PrimitiveInputWrapper } from './helpers'
import { PrimitiveInputWrapper } from '../json-schema-input/helpers'
// ===================================================================
@@ -14,7 +14,7 @@ export default class HostInput extends XoAbstractInput {
<PrimitiveInputWrapper {...props}>
<SelectHost
disabled={props.disabled}
multi={props.schema.type === 'array'}
multi={props.multi}
onChange={props.onChange}
ref='input'
required={props.required}

View File

@@ -2,7 +2,7 @@ import React from 'react'
import { SelectPool } from 'select-objects'
import XoAbstractInput from './xo-abstract-input'
import { PrimitiveInputWrapper } from './helpers'
import { PrimitiveInputWrapper } from '../json-schema-input/helpers'
// ===================================================================
@@ -14,7 +14,7 @@ export default class PoolInput extends XoAbstractInput {
<PrimitiveInputWrapper {...props}>
<SelectPool
disabled={props.disabled}
multi={props.schema.type === 'array'}
multi={props.multi}
onChange={props.onChange}
ref='input'
required={props.required}

View File

@@ -2,7 +2,7 @@ import React from 'react'
import { SelectRemote } from 'select-objects'
import XoAbstractInput from './xo-abstract-input'
import { PrimitiveInputWrapper } from './helpers'
import { PrimitiveInputWrapper } from '../json-schema-input/helpers'
// ===================================================================
@@ -14,7 +14,7 @@ export default class RemoteInput extends XoAbstractInput {
<PrimitiveInputWrapper {...props}>
<SelectRemote
disabled={props.disabled}
multi={props.schema.type === 'array'}
multi={props.multi}
onChange={props.onChange}
ref='input'
required={props.required}

View File

@@ -2,7 +2,7 @@ import React from 'react'
import { SelectRole } from 'select-objects'
import XoAbstractInput from './xo-abstract-input'
import { PrimitiveInputWrapper } from './helpers'
import { PrimitiveInputWrapper } from '../json-schema-input/helpers'
// ===================================================================
@@ -14,7 +14,7 @@ export default class RoleInput extends XoAbstractInput {
<PrimitiveInputWrapper {...props}>
<SelectRole
disabled={props.disabled}
multi={props.schema.type === 'array'}
multi={props.multi}
onChange={props.onChange}
ref='input'
required={props.required}

View File

@@ -2,7 +2,7 @@ import React from 'react'
import { SelectSr } from 'select-objects'
import XoAbstractInput from './xo-abstract-input'
import { PrimitiveInputWrapper } from './helpers'
import { PrimitiveInputWrapper } from '../json-schema-input/helpers'
// ===================================================================
@@ -14,7 +14,7 @@ export default class SrInput extends XoAbstractInput {
<PrimitiveInputWrapper {...props}>
<SelectSr
disabled={props.disabled}
multi={props.schema.type === 'array'}
multi={props.multi}
onChange={props.onChange}
ref='input'
required={props.required}

View File

@@ -2,7 +2,7 @@ import React from 'react'
import { SelectSubject } from 'select-objects'
import XoAbstractInput from './xo-abstract-input'
import { PrimitiveInputWrapper } from './helpers'
import { PrimitiveInputWrapper } from '../json-schema-input/helpers'
// ===================================================================
@@ -14,7 +14,7 @@ export default class SubjectInput extends XoAbstractInput {
<PrimitiveInputWrapper {...props}>
<SelectSubject
disabled={props.disabled}
multi={props.schema.type === 'array'}
multi={props.multi}
onChange={props.onChange}
ref='input'
required={props.required}

View File

@@ -2,7 +2,7 @@ import React from 'react'
import { SelectVm } from 'select-objects'
import XoAbstractInput from './xo-abstract-input'
import { PrimitiveInputWrapper } from './helpers'
import { PrimitiveInputWrapper } from '../json-schema-input/helpers'
// ===================================================================
@@ -14,7 +14,7 @@ export default class VmInput extends XoAbstractInput {
<PrimitiveInputWrapper {...props}>
<SelectVm
disabled={props.disabled}
multi={props.schema.type === 'array'}
multi={props.multi}
onChange={props.onChange}
ref='input'
required={props.required}

View File

@@ -3,10 +3,9 @@ import ChartistLegend from 'chartist-plugin-legend'
import ChartistTooltip from 'chartist-plugin-tooltip'
import React from 'react'
import { injectIntl } from 'react-intl'
import {
formatSize,
propTypes
} from 'utils'
import propTypes from './prop-types'
import { formatSize } from './utils'
// Number of labels on axis X.
const N_LABELS_X = 5
@@ -282,7 +281,7 @@ export const LoadLineChart = injectIntl(propTypes({
nValues: length,
endTimestamp: data.endTimestamp,
interval: data.interval,
valueTransform: value => `${value}`
valueTransform: value => `${value.toPrecision(3)}`
}),
...options
}}

View File

@@ -0,0 +1,294 @@
import * as d3 from 'd3'
import React from 'react'
import forEach from 'lodash/forEach'
import keys from 'lodash/keys'
import map from 'lodash/map'
import times from 'lodash/times'
import Component from './base-component'
import propTypes from './prop-types'
import { setStyles } from './d3-utils'
// ===================================================================
const CHART_WIDTH = 2000
const CHART_HEIGHT = 800
const TICK_SIZE = CHART_WIDTH / 100
const N_TICKS = 4
const TOOLTIP_PADDING = 10
const DEFAULT_STROKE_WIDTH_FACTOR = 500
const HIGHLIGHT_STROKE_WIDTH_FACTOR = 200
const BRUSH_SELECTION_WIDTH = 2 * CHART_WIDTH / 100
// ===================================================================
const SVG_STYLE = {
display: 'block',
height: '100%',
left: 0,
position: 'absolute',
top: 0,
width: '100%'
}
const SVG_CONTAINER_STYLE = {
'padding-bottom': '50%',
'vertical-align': 'middle',
overflow: 'hidden',
position: 'relative',
width: '100%'
}
const SVG_CONTENT = {
'font-size': `${CHART_WIDTH / 100}px`
}
const COLUMN_TITLE_STYLE = {
'font-size': '100%',
'font-weight': 'bold',
'text-anchor': 'middle'
}
const COLUMN_VALUES_STYLE = {
'font-size': '100%'
}
const LINES_CONTAINER_STYLE = {
'stroke-opacity': 0.5,
'stroke-width': CHART_WIDTH / DEFAULT_STROKE_WIDTH_FACTOR,
fill: 'none',
stroke: 'red'
}
const TOOLTIP_STYLE = {
'fill': 'white',
'font-size': '125%',
'font-weight': 'bold'
}
// ===================================================================
@propTypes({
dataSet: propTypes.arrayOf(
propTypes.shape({
data: propTypes.object.isRequired,
label: propTypes.string.isRequired,
objectId: propTypes.string.isRequired
})
).isRequired,
labels: propTypes.object.isRequired,
renderers: propTypes.object
})
export default class XoParallelChart extends Component {
_line = d3.line()
_color = d3.scaleOrdinal(d3.schemeCategory10)
_handleBrush = () => {
// 1. Get selected brushes.
const brushes = []
this._svg.selectAll('.chartColumn')
.selectAll('.brush')
.each((_1, _2, [ brush ]) => {
if (d3.brushSelection(brush) != null) {
brushes.push(brush)
}
})
// 2. Change stroke of selected lines.
const lines = this._svg.select('.linesContainer')
.selectAll('path')
lines.each((elem, lineId, lines) => {
const { data } = elem
const res = brushes.every(brush => {
const selection = d3.brushSelection(brush)
const columnId = brush.__data__
const { invert } = this._y[columnId] // Range to domain.
return invert(selection[1]) <= data[columnId] && data[columnId] <= invert(selection[0])
})
const line = d3.select(lines[lineId])
if (!res) {
line.attr('stroke-opacity', 1.0).attr('stroke', '#e6e6e6')
} else {
line.attr('stroke-opacity', 0.5).attr('stroke', this._color(elem.label))
}
})
}
_brush = d3.brushY()
// Brush area: (x0, y0), (x1, y1)
.extent([[-BRUSH_SELECTION_WIDTH / 2, 0], [BRUSH_SELECTION_WIDTH / 2, CHART_HEIGHT]])
.on('brush', this._handleBrush)
.on('end', this._handleBrush)
_highlight (elem, position) {
const svg = this._svg
// Reset tooltip.
svg
.selectAll('.objectTooltip')
.remove()
// Reset all lines.
svg
.selectAll('.chartLine')
.attr('stroke-width', CHART_WIDTH / DEFAULT_STROKE_WIDTH_FACTOR)
if (!position) {
return
}
// Set stroke on selected line.
svg
.select('#chartLine-' + elem.objectId)
.attr('stroke-width', CHART_WIDTH / HIGHLIGHT_STROKE_WIDTH_FACTOR)
const { label } = elem
const tooltip = svg.append('g')
.attr('class', 'objectTooltip')
const bbox = tooltip.append('text')
.text(label)
.attr('x', position[0])
.attr('y', position[1] - 30)
::setStyles(TOOLTIP_STYLE)
.node().getBBox()
tooltip.insert('rect', '*')
.attr('x', bbox.x - TOOLTIP_PADDING)
.attr('y', bbox.y - TOOLTIP_PADDING)
.attr('width', bbox.width + TOOLTIP_PADDING * 2)
.attr('height', bbox.height + TOOLTIP_PADDING * 2)
.style('fill', this._color(label))
}
_handleMouseOver = (elem, pathId, paths) => {
this._highlight(elem, d3.mouse(paths[pathId]))
}
_handleMouseOut = (elem) => {
this._highlight()
}
_draw (props = this.props) {
const svg = this._svg
const { labels, dataSet } = props
const columnsIds = keys(labels)
const spacing = (CHART_WIDTH - 200) / (columnsIds.length - 1)
const x = d3.scaleOrdinal()
.domain(columnsIds).range(
times(columnsIds.length, n => n * spacing)
)
// 1. Remove old nodes.
svg
.selectAll('.chartColumn')
.remove()
svg
.selectAll('.linesContainer')
.remove()
// 2. Build Ys.
const y = this._y = {}
forEach(columnsIds, (columnId, index) => {
const max = d3.max(dataSet, elem => elem.data[columnId])
y[columnId] = d3.scaleLinear()
.domain([0, max])
.range([CHART_HEIGHT, 0])
})
// 3. Build columns.
const columns = svg.selectAll('.chartColumn')
.data(columnsIds)
.enter().append('g')
.attr('class', 'chartColumn')
.attr('transform', d => `translate(${x(d)})`)
// 4. Draw titles.
columns.append('text')
.text(columnId => labels[columnId])
.attr('y', -50)
::setStyles(COLUMN_TITLE_STYLE)
// 5. Draw axis.
columns.append('g')
.each((columnId, axisId, axes) => {
const axis = d3.axisLeft()
.ticks(N_TICKS, ',f')
.tickSize(TICK_SIZE)
.scale(y[columnId])
const renderer = props.renderers[columnId]
// Add optional renderer like formatSize.
if (renderer) {
axis.tickFormat(renderer)
}
d3.select(axes[axisId]).call(axis)
})
::setStyles(COLUMN_VALUES_STYLE)
// 6. Draw lines.
const path = elem => this._line(map(columnsIds.map(
columnId => [x(columnId), y[columnId](elem.data[columnId])]
)))
svg.append('g')
.attr('class', 'linesContainer')
::setStyles(LINES_CONTAINER_STYLE)
.selectAll('path')
.data(dataSet)
.enter().append('path')
.attr('d', path)
.attr('class', 'chartLine')
.attr('id', elem => 'chartLine-' + elem.objectId)
.attr('stroke', elem => this._color(elem.label))
.attr('shape-rendering', 'optimizeQuality')
.attr('stroke-linecap', 'round')
.attr('stroke-linejoin', 'round')
.on('mouseover', this._handleMouseOver)
.on('mouseout', this._handleMouseOut)
// 7. Brushes.
columns.append('g')
.attr('class', 'brush')
.each((_, brushId, brushes) => { d3.select(brushes[brushId]).call(this._brush) })
}
componentDidMount () {
this._svg = d3.select(this.refs.chart)
.append('div')
::setStyles(SVG_CONTAINER_STYLE)
.append('svg')
::setStyles(SVG_STYLE)
.attr('preserveAspectRatio', 'xMinYMin meet')
.attr('viewBox', `0 0 ${CHART_WIDTH} ${CHART_HEIGHT}`)
.append('g')
.attr('transform', `translate(${100}, ${100})`)
::setStyles(SVG_CONTENT)
this._draw()
}
componentWillReceiveProps (nextProps) {
this._draw(nextProps)
}
render () {
return <div ref='chart' />
}
}

View File

@@ -5,7 +5,8 @@ import {
SparklinesLine,
SparklinesSpots
} from 'react-sparklines'
import { propTypes } from 'utils'
import propTypes from './prop-types'
const STYLE = {}

View File

@@ -1,19 +1,20 @@
import React from 'react'
import _ from 'intl'
import * as d3 from 'd3'
import forEach from 'lodash/forEach'
import map from 'lodash/map'
import { Toggle } from 'form'
import Component from '../base-component'
import _ from '../intl'
import propTypes from '../prop-types'
import { Toggle } from '../form'
import { setStyles } from '../d3-utils'
import {
createGetObject,
createSelector
} from '../selectors'
import {
connectStore,
propsEqual,
propTypes
propsEqual
} from '../utils'
import styles from './index.css'
@@ -63,16 +64,6 @@ const HORIZON_AREA_PATH_STYLE = {
// ===================================================================
function setStyles (style) {
forEach(style, (value, key) => {
this.style(key, value)
})
return this
}
// ===================================================================
@propTypes({
chartHeight: propTypes.number,
chartWidth: propTypes.number,

View File

@@ -1,5 +1,4 @@
import React from 'react'
import _ from 'intl'
import forEach from 'lodash/forEach'
import map from 'lodash/map'
import moment from 'moment'
@@ -12,9 +11,10 @@ import {
} from 'd3'
import { FormattedTime } from 'react-intl'
import _ from '../intl'
import Component from '../base-component'
import propTypes from '../prop-types'
import Tooltip from '../tooltip'
import { propTypes } from '../utils'
import styles from './index.css'

View File

@@ -15,10 +15,11 @@ import { createBackoff } from 'jsonrpc-websocket-client'
import { resolve } from 'url'
import _ from '../intl'
import invoke from '../invoke'
import logError from '../log-error'
import { confirm } from '../modal'
import { error, info, success } from '../notification'
import { invoke, noop, rethrow, tap } from '../utils'
import { noop, rethrow, tap } from '../utils'
import {
connected,
disconnected,
@@ -453,22 +454,45 @@ export const migrateVm = (vm, host) => (
body: <MigrateVmModalBody vm={vm} host={host} />
}).then(
params => {
if (!params && !host) {
throw new Error('A target host is required to migrate a VM')
}
if (params) {
_call('vm.migrate', { vm: vm.id, ...params })
} else {
_call('vm.migrate', { vm: vm.id, targetHost: host.id })
if (!params) {
throw new Error('a target host is required to migrate a VM')
}
_call('vm.migrate', { vm: vm.id, ...params })
},
noop
)
)
export const migrateVms = vms => {
throw new Error('Not implemented.')
}
import MigrateVmsModalBody from './migrate-vms-modal'
export const migrateVms = vms => (
confirm({
title: _('migrateVmModalTitle'),
body: <MigrateVmsModalBody vms={vms} />
}).then(
params => {
if (!params) {
throw new Error('a target host is required to migrate a VM')
}
const vmsIds = resolveIds(vms)
const {
mapVmsMapVdisSrs,
mapVmsMapVifsNetworks,
migrationNetwork,
targetHost
} = params
Promise.all(map(vmsIds, vm =>
_call('vm.migrate', {
mapVdisSrs: mapVmsMapVdisSrs[vm],
mapVifsNetworks: mapVmsMapVifsNetworks[vm],
migrationNetwork,
targetHost,
vm
})
))
},
noop
)
)
export const createVm = args => (
_call('vm.create', args)
@@ -636,6 +660,20 @@ export const setBootableVbd = ({ id }, bootable) => (
_call('vbd.setBootable', { vbd: id, bootable })
)
// VIF ---------------------------------------------------------------
export const connectVif = vif => (
_call('vif.connect', { id: resolveId(vif) })
)
export const disconnectVif = vif => (
_call('vif.disconnect', { id: resolveId(vif) })
)
export const deleteVif = vif => (
_call('vif.delete', { id: resolveId(vif) })
)
// Network -----------------------------------------------------------
export const editNetwork = ({ id }, props) => (
@@ -664,6 +702,12 @@ export const deleteNetwork = network => (
)
)
// VIF ---------------------------------------------------------------
export const createVmInterface = (vm, network, mac, mtu) => (
_call('vm.createInterface', resolveIds({vm, network, mtu, mac}))
)
// PIF ---------------------------------------------------------------
export const connectPif = pif => (

View File

@@ -7,11 +7,11 @@
margin-top: 2px;
}
.firstBlock {
.block {
padding-bottom: 1em;
}
.block {
.groupBlock {
padding-bottom: 1em;
padding-top: 1em;
border-top: 1px solid #e5e5e5;

View File

@@ -1,11 +1,15 @@
import BaseComponent from 'base-component'
import forEach from 'lodash/forEach'
import find from 'lodash/find'
import map from 'lodash/map'
import React, { Component } from 'react'
import mapValues from 'lodash/mapValues'
import React from 'react'
import _ from '../../intl'
import invoke from '../../invoke'
import SingleLineRow from '../../single-line-row'
import { Col } from '../../grid'
import { getDefaultNetworkForVif } from '../utils'
import {
SelectHost,
SelectNetwork,
@@ -49,19 +53,17 @@ import styles from './index.css'
const getPifs = createGetObjectsOfType('PIF')
const getNetworks = createGetObjectsOfType('network')
const getSrs = createGetObjectsOfType('SR')
const getPools = createGetObjectsOfType('pool')
return {
networks: getNetworks,
pifs: getPifs,
pools: getPools,
srs: getSrs,
vdis: getVdis,
vifs: getVifs
}
}, { withRef: true })
export default class MigrateVmModalBody extends Component {
export default class MigrateVmModalBody extends BaseComponent {
constructor (props) {
super(props)
@@ -79,7 +81,7 @@ export default class MigrateVmModalBody extends Component {
() => this.state.host,
host => (host
? sr => isSrWritable(sr) && (sr.$container === host.id || sr.$container === host.$pool)
: () => false
: false
)
)
@@ -90,7 +92,7 @@ export default class MigrateVmModalBody extends Component {
),
pifs => {
if (!pifs) {
return () => false
return false
}
const networks = {}
@@ -112,125 +114,122 @@ export default class MigrateVmModalBody extends Component {
targetHost: this.state.host && this.state.host.id,
mapVdisSrs: this.state.mapVdisSrs,
mapVifsNetworks: this.state.mapVifsNetworks,
migrationNetwork: this.state.network && this.state.network.id
migrationNetwork: this.state.migrationNetworkId
}
}
_selectHost = host => {
if (!host) {
this.setState({ intraPool: undefined, targetHost: undefined })
this.setState({ intraPool: undefined, host: undefined })
return
}
const { networks, pools, pifs, srs, vdis, vifs } = this.props
const defaultMigrationNetwork = networks[find(pifs, pif => pif.$host === host.id && pif.management).$network]
const defaultSr = srs[pools[host.$pool].default_SR]
const defaultNetworks = {}
// Default network...
forEach(vifs, vif => {
// ...is the one which has the same name_label as the VIF's previous network (if it has an IP)...
const defaultPif = find(host.$PIFs, pifId => {
const pif = pifs[pifId]
return pif.ip && networks[vif.$network].name_label === networks[pif.$network].name_label
})
defaultNetworks[vif.id] = defaultPif && networks[defaultPif.$network]
// ...or the first network in the target host networks list that has an IP.
if (!defaultNetworks[vif.id]) {
defaultNetworks[vif.id] = networks[pifs[find(host.$PIFs, pif => pifs[pif].ip)].$network]
}
const { networks, pools, pifs, vdis, vifs } = this.props
const defaultMigrationNetworkId = find(pifs, pif => pif.$host === host.id && pif.management).$network
const defaultSr = pools[host.$pool].default_SR
const defaultNetwork = invoke(() => {
// First PIF with an IP.
const pifId = find(host.$PIFs, pif => pifs[pif].ip)
const pif = pifId && pifs[pifId]
return pif && pif.$network
})
const defaultNetworksForVif = {}
forEach(vifs, vif => {
defaultNetworksForVif[vif.id] = (
getDefaultNetworkForVif(vif, host, pifs, networks) ||
defaultNetwork
)
})
this.setState({
network: defaultMigrationNetwork,
defaultNetworks,
defaultSr,
host,
intraPool: this.props.vm.$pool === host.$pool
}, () => {
if (!this.state.intraPool) {
this.refs.network.value = defaultMigrationNetwork
forEach(vdis, vdi => {
this.refs['sr_' + vdi.id].value = defaultSr
})
forEach(vifs, vif => {
this.refs['network_' + vif.id].value = defaultNetworks[vif.id]
})
}
intraPool: this.props.vm.$pool === host.$pool,
mapVdisSrs: mapValues(vdis, vdi => defaultSr),
mapVifsNetworks: defaultNetworksForVif,
migrationNetworkId: defaultMigrationNetworkId
})
}
_selectMigrationNetwork = network => this.setState({ network })
_selectMigrationNetwork = migrationNetwork => this.setState({ migrationNetworkId: migrationNetwork.id })
render () {
const { host, vdis, vifs, networks } = this.props
const { vdis, vifs, networks } = this.props
const {
host,
intraPool,
mapVdisSrs,
mapVifsNetworks,
migrationNetworkId
} = this.state
return <div>
<div className={styles.firstBlock}>
<div className={styles.block}>
<SingleLineRow>
<Col size={6}>{_('migrateVmAdvancedModalSelectHost')}</Col>
<Col size={6}>{_('migrateVmSelectHost')}</Col>
<Col size={6}>
<SelectHost
defaultValue={host}
onChange={this._selectHost}
predicate={this._getHostPredicate()}
value={host}
/>
</Col>
</SingleLineRow>
</div>
{this.state.intraPool !== undefined &&
(!this.state.intraPool &&
{intraPool !== undefined &&
(!intraPool &&
<div>
<div className={styles.block}>
<div className={styles.groupBlock}>
<SingleLineRow>
<Col size={6}>{_('migrateVmAdvancedModalSelectNetwork')}</Col>
<Col size={6}>{_('migrateVmSelectMigrationNetwork')}</Col>
<Col size={6}>
<SelectNetwork
ref='network'
defaultValue={this.state.network}
onChange={this._selectMigrationNetwork}
predicate={this._getNetworkPredicate()}
value={migrationNetworkId}
/>
</Col>
</SingleLineRow>
</div>
<div className={styles.block}>
<div className={styles.groupBlock}>
<SingleLineRow>
<Col>{_('migrateVmAdvancedModalSelectSrs')}</Col>
<Col>{_('migrateVmSelectSrs')}</Col>
</SingleLineRow>
<br />
<SingleLineRow>
<Col size={6}><span className={styles.listTitle}>{_('migrateVmAdvancedModalName')}</span></Col>
<Col size={6}><span className={styles.listTitle}>{_('migrateVmAdvancedModalSr')}</span></Col>
<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
ref={'sr_' + vdi.id}
defaultValue={this.state.defaultSr}
onChange={sr => this.setState({ mapVdisSrs: { ...this.state.mapVdisSrs, [vdi.id]: sr.id } })}
onChange={sr => this.setState({ mapVdisSrs: { ...mapVdisSrs, [vdi.id]: sr.id } })}
predicate={this._getSrPredicate()}
value={mapVdisSrs[vdi.id]}
/>
</Col>
</SingleLineRow>
</div>)}
</div>
<div className={styles.block}>
<div className={styles.groupBlock}>
<SingleLineRow>
<Col>{_('migrateVmAdvancedModalSelectNetworks')}</Col>
<Col>{_('migrateVmSelectNetworks')}</Col>
</SingleLineRow>
<br />
<SingleLineRow>
<Col size={6}><span className={styles.listTitle}>{_('migrateVmAdvancedModalVif')}</span></Col>
<Col size={6}><span className={styles.listTitle}>{_('migrateVmAdvancedModalNetwork')}</span></Col>
<Col size={6}><span className={styles.listTitle}>{_('migrateVmVif')}</span></Col>
<Col size={6}><span className={styles.listTitle}>{_('migrateVmNetwork')}</span></Col>
</SingleLineRow>
{map(vifs, vif => <div className={styles.listItem} key={vif.id}>
<SingleLineRow>
<Col size={6}>{vif.MAC} ({networks[vif.$network].name_label})</Col>
<Col size={6}>
<SelectNetwork
ref={'network_' + vif.id}
defaultValue={this.state.defaultNetworks[vif.id]}
onChange={network => this.setState({ mapVifsNetworks: { ...this.state.mapVifsNetworks, [vif.id]: network.id } })}
onChange={network => this.setState({ mapVifsNetworks: { ...mapVifsNetworks, [vif.id]: network.id } })}
predicate={this._getNetworkPredicate()}
value={mapVifsNetworks[vif.id]}
/>
</Col>
</SingleLineRow>

View File

@@ -0,0 +1,200 @@
import BaseComponent from 'base-component'
import concat from 'lodash/concat'
import every from 'lodash/every'
import forEach from 'lodash/forEach'
import find from 'lodash/find'
import map from 'lodash/map'
import React from 'react'
import some from 'lodash/some'
import _ from '../../intl'
import SingleLineRow from '../../single-line-row'
import { Col } from '../../grid'
import {
SelectHost,
SelectNetwork,
SelectSr
} from '../../select-objects'
import {
connectStore
} from '../../utils'
import {
createGetObjectsOfType,
createPicker,
createSelector
} from '../../selectors'
import { isSrWritable } from '../'
const LINE_STYLE = { paddingBottom: '1em' }
@connectStore(() => {
const getPifs = createGetObjectsOfType('PIF')
const getPools = createGetObjectsOfType('pool')
const getVms = createGetObjectsOfType('VM').pick(
(_, props) => props.vms
)
const getVbdsByVm = createGetObjectsOfType('VBD').pick(
createSelector(
getVms,
vms => concat(...map(vms, vm => vm.$VBDs))
)
).groupBy('VM')
return {
pifs: getPifs,
pools: getPools,
vbdsByVm: getVbdsByVm,
vms: getVms
}
}, { withRef: true })
export default class MigrateVmsModalBody extends BaseComponent {
constructor (props) {
super(props)
this._getHostPredicate = createSelector(
() => this.props.vms,
vms => host => some(vms, vm => host.id !== vm.$container)
)
this._getSrPredicate = createSelector(
() => this.state.host,
host => (host
? sr => isSrWritable(sr) && (sr.$container === host.id || sr.$container === host.$pool)
: false
)
)
this._getNetworkPredicate = createSelector(
createPicker(
() => this.props.pifs,
() => this.state.host.$PIFs
),
pifs => {
if (!pifs) {
return false
}
const networks = {}
forEach(pifs, pif => {
pif.ip && (networks[pif.$network] = true)
})
return network => networks[network.id]
}
)
}
componentDidMount () {
this._selectHost(this.props.host)
}
get value () {
// Map VM --> ( Map VDI --> SR )
const mapVmsMapVdisSrs = {}
forEach(this.props.vbdsByVm, (vbds, vm) => {
const mapVdisSrs = {}
forEach(vbds, vbd => {
if (!vbd.is_cd_drive && vbd.VDI) {
mapVdisSrs[vbd.VDI] = this.state.srId
}
})
mapVmsMapVdisSrs[vm] = mapVdisSrs
})
// Map VM --> ( Map VIF --> network )
const mapVmsMapVifsNetworks = {}
forEach(this.props.vms, vm => {
const mapVifsNetworks = {}
forEach(vm.VIFs, vif => {
mapVifsNetworks[vif] = this.state.networkId
})
mapVmsMapVifsNetworks[vm.id] = mapVifsNetworks
})
return {
mapVmsMapVdisSrs,
mapVmsMapVifsNetworks,
migrationNetwork: this.state.migrationNetworkId,
targetHost: this.state.host && this.state.host.id
}
}
_selectHost = host => {
if (!host) {
this.setState({ targetHost: undefined })
return
}
const { pools, pifs } = this.props
const defaultMigrationNetworkId = find(pifs, pif => pif.$host === host.id && pif.management).$network
const defaultSrId = pools[host.$pool].default_SR
this.setState({
host,
intraPool: every(this.props.vms, vm => vm.$pool === host.$pool),
migrationNetworkId: defaultMigrationNetworkId,
networkId: defaultMigrationNetworkId,
srId: defaultSrId
})
}
_selectMigrationNetwork = migrationNetwork => this.setState({ migrationNetworkId: migrationNetwork.id })
_selectNetwork = network => this.setState({ networkId: network.id })
_selectSr = sr => this.setState({ srId: sr.id })
render () {
return <div>
<div style={LINE_STYLE}>
<SingleLineRow>
<Col size={6}>{_('migrateVmSelectHost')}</Col>
<Col size={6}>
<SelectHost
onChange={this._selectHost}
predicate={this._getHostPredicate()}
value={this.state.host}
/>
</Col>
</SingleLineRow>
</div>
{this.state.intraPool === false &&
<div style={LINE_STYLE}>
<SingleLineRow>
<Col size={6}>{_('migrateVmSelectMigrationNetwork')}</Col>
<Col size={6}>
<SelectNetwork
onChange={this._selectMigrationNetwork}
predicate={this._getNetworkPredicate()}
value={this.state.migrationNetworkId}
/>
</Col>
</SingleLineRow>
</div>
}
{this.state.host && [
<div key='sr' style={LINE_STYLE}>
<SingleLineRow>
<Col size={6}>{_('migrateVmsSelectSr')}</Col>
<Col size={6}>
<SelectSr
onChange={this._selectSr}
predicate={this._getSrPredicate()}
value={this.state.srId}
/>
</Col>
</SingleLineRow>
</div>,
<div key='network' style={LINE_STYLE}>
<SingleLineRow>
<Col size={6}>{_('migrateVmsSelectNetwork')}</Col>
<Col size={6}>
<SelectNetwork
onChange={this._selectNetwork}
predicate={this._getNetworkPredicate()}
value={this.state.networkId}
/>
</Col>
</SingleLineRow>
</div>
]}
</div>
}
}

14
src/common/xo/utils.js Normal file
View File

@@ -0,0 +1,14 @@
import forEach from 'lodash/forEach'
export const getDefaultNetworkForVif = (vif, host, pifs, networks) => {
const nameLabel = networks[vif.$network].name_label
let defaultNetwork
forEach(host.$PIFs, pifId => {
const pif = pifs[pifId]
if (pif.ip && networks[pif.$network].name_label === nameLabel) {
defaultNetwork = pif.$network
return false
}
})
return defaultNetwork
}

View File

@@ -98,8 +98,8 @@ class XoaUpdater extends EventEmitter {
await this._update(true)
}
_promptForReload () {
this.emit('promptForReload')
_upgradeSuccessful () {
this.emit('upgradeSuccessful')
}
async _open () {
@@ -158,7 +158,7 @@ class XoaUpdater extends EventEmitter {
if (this._lowState.state === 'updater-upgraded' || this._lowState.state === 'installer-upgraded') {
this.update()
} else if (this._lowState.state === 'xoa-upgraded') {
this._promptForReload()
this._upgradeSuccessful()
}
this.xoaState()
})

View File

@@ -1,9 +1,11 @@
import _ from 'intl'
import Icon from 'icon'
import Link from 'react-router/lib/Link'
import React from 'react'
import { Card, CardHeader, CardBlock } from 'card'
import { getXoaPlan, propTypes } from 'utils'
import _ from './intl'
import Icon from './icon'
import Link from './link'
import propTypes from './prop-types'
import { Card, CardHeader, CardBlock } from './card'
import { getXoaPlan } from './utils'
const Upgrade = propTypes({
available: propTypes.number.isRequired,

View File

@@ -839,6 +839,10 @@
@extend .fa;
@extend .fa-server;
}
&-connect {
@extend .fa;
@extend .fa-link;
}
&-disconnect {
@extend .fa;
@extend .fa-unlink;

View File

@@ -2,7 +2,7 @@ import _ from 'intl'
import Component from 'base-component'
import Copiable from 'copiable'
import Icon from 'icon'
import Link from 'react-router/lib/Link'
import Link from 'link'
import Page from '../page'
import React from 'react'
import { getUser } from 'selectors'

View File

@@ -11,6 +11,7 @@ import Upgrade from 'xoa-upgrade'
import Wizard, { Section } from 'wizard'
import { Container } from 'grid'
import { error } from 'notification'
import { generateUiSchema } from 'xo-json-schema-input'
import { injectIntl } from 'react-intl'
import {
@@ -34,7 +35,10 @@ const COMMON_SCHEMA = {
},
vms: {
type: 'array',
'xo:type': 'vm',
items: {
type: 'string',
'xo:type': 'vm'
},
title: 'VMs',
description: 'Choose VMs to backup.'
},
@@ -83,7 +87,7 @@ const BACKUP_SCHEMA = {
required: COMMON_SCHEMA.required.concat([ 'depth', 'remoteId' ])
}
const ROLLING_SNAPHOT_SCHEMA = {
const ROLLING_SNAPSHOT_SCHEMA = {
type: 'object',
properties: {
...COMMON_SCHEMA.properties,
@@ -143,13 +147,15 @@ if (process.env.XOA_PLAN < 4) {
const BACKUP_METHOD_TO_INFO = {
'vm.rollingBackup': {
schema: BACKUP_SCHEMA,
uiSchema: generateUiSchema(BACKUP_SCHEMA),
label: 'backup',
icon: 'backup',
jobKey: 'rollingBackup',
method: 'vm.rollingBackup'
},
'vm.rollingSnapshot': {
schema: ROLLING_SNAPHOT_SCHEMA,
schema: ROLLING_SNAPSHOT_SCHEMA,
uiSchema: generateUiSchema(ROLLING_SNAPSHOT_SCHEMA),
label: 'rollingSnapshot',
icon: 'rolling-snapshot',
jobKey: 'rollingSnapshot',
@@ -157,6 +163,7 @@ const BACKUP_METHOD_TO_INFO = {
},
'vm.rollingDeltaBackup': {
schema: DELTA_BACKUP_SCHEMA,
uiSchema: generateUiSchema(DELTA_BACKUP_SCHEMA),
label: 'deltaBackup',
icon: 'delta-backup',
jobKey: 'deltaBackup',
@@ -164,6 +171,7 @@ const BACKUP_METHOD_TO_INFO = {
},
'vm.rollingDrCopy': {
schema: DISASTER_RECOVERY_SCHEMA,
uiSchema: generateUiSchema(DISASTER_RECOVERY_SCHEMA),
label: 'disasterRecovery',
icon: 'disaster-recovery',
jobKey: 'disasterRecovery',
@@ -171,6 +179,7 @@ const BACKUP_METHOD_TO_INFO = {
},
'vm.deltaCopy': {
schema: CONTINUOUS_REPLICATION_SCHEMA,
uiSchema: generateUiSchema(CONTINUOUS_REPLICATION_SCHEMA),
label: 'continuousReplication',
icon: 'continuous-replication',
jobKey: 'continuousReplication',
@@ -318,6 +327,7 @@ export default class New extends Component {
label={<span><Icon icon={backupInfo.icon} /> {formatMessage(messages[backupInfo.label])}</span>}
required
schema={backupInfo.schema}
uiSchema={backupInfo.uiSchema}
ref='backupInput'
/>
}

View File

@@ -4,19 +4,17 @@ import ActionToggle from 'action-toggle'
import filter from 'lodash/filter'
import forEach from 'lodash/forEach'
import Icon from 'icon'
import Link from 'link'
import LogList from '../../logs'
import map from 'lodash/map'
import orderBy from 'lodash/orderBy'
import React, { Component } from 'react'
import { ButtonGroup } from 'react-bootstrap-4/lib'
import { Link } from 'react-router'
import {
Card,
CardHeader,
CardBlock
} from 'card'
import {
deleteBackupSchedule,
disableSchedule,

View File

@@ -3,7 +3,7 @@ import ActionButton from 'action-button'
import ActionRowButton from 'action-row-button'
import find from 'lodash/find'
import forEach from 'lodash/forEach'
import Link from 'react-router/lib/Link'
import Link from 'link'
import map from 'lodash/map'
import moment from 'moment'
import orderBy from 'lodash/orderBy'

View File

@@ -1,15 +1,16 @@
import ActionButton from 'action-button'
import Component from 'base-component'
import Icon from 'icon'
import React from 'react'
import XoWeekCharts from 'xo-week-charts'
import XoWeekHeatmap from 'xo-week-heatmap'
import _ from 'intl'
import ActionButton from 'action-button'
import cloneDeep from 'lodash/cloneDeep'
import Component from 'base-component'
import forEach from 'lodash/forEach'
import Icon from 'icon'
import map from 'lodash/map'
import propTypes from 'prop-types'
import React from 'react'
import renderXoItem from 'render-xo-item'
import sortBy from 'lodash/sortBy'
import XoWeekCharts from 'xo-week-charts'
import XoWeekHeatmap from 'xo-week-heatmap'
import { Container, Row, Col } from 'grid'
import { error } from 'notification'
import { SelectHostVm } from 'select-objects'
@@ -17,8 +18,7 @@ import { createGetObjectsOfType } from 'selectors'
import {
connectStore,
formatSize,
mapPlus,
propTypes
mapPlus
} from 'utils'
import {
fetchHostStats,

View File

@@ -1,11 +1,129 @@
import _ from 'intl'
import Component from 'base-component'
import React from 'react'
import XoParallelChart from 'xo-parallel-chart'
import forEach from 'lodash/forEach'
import invoke from 'invoke'
import map from 'lodash/map'
import mapValues from 'lodash/mapValues'
import { Container, Row, Col } from 'grid'
import {
createFilter,
createGetObjectsOfType,
createPicker,
createSelector
} from 'selectors'
import {
connectStore,
formatSize
} from 'utils'
export default () => <Container>
<Row>
<Col>
<h3>{_('comingSoon')}</h3>
</Col>
</Row>
</Container>
// ===================================================================
// Columns order is defined by the attributes declaration order.
const DATA_LABELS = {
nVCpus: 'vCPUs number',
ram: 'RAM quantity',
nVifs: 'VIF number',
nVdis: 'VDI number',
vdisSize: 'Total space'
}
const DATA_RENDERERS = {
ram: formatSize,
vdisSize: formatSize
}
// ===================================================================
@connectStore(() => {
const getVms = createGetObjectsOfType('VM')
const getVdisByVm = invoke(() => {
let current = {}
const getVdisByVmSelectors = createSelector(
vms => vms,
vms => {
let previous = current
current = {}
forEach(vms, vm => {
const { id } = vm
current[id] = previous[id] || createPicker(
(vm, vbds, vdis) => vdis,
createSelector(
createFilter(
createPicker(
(vm, vbds) => vbds,
vm => vm.$VBDs
),
[ vbd => !vbd.is_cd_drive && vbd.attached ]
),
vbds => map(vbds, vbd => vbd.VDI)
)
)
})
return current
}
)
return createSelector(
getVms,
createGetObjectsOfType('VBD'),
createGetObjectsOfType('VDI'),
(vms, vbds, vdis) => mapValues(
getVdisByVmSelectors(vms),
(getVdis, vmId) => getVdis(vms[vmId], vbds, vdis)
)
)
})
return {
vms: getVms,
vdisByVm: getVdisByVm
}
})
export default class Visualizations extends Component {
_getData = createSelector(
() => this.props.vms,
() => this.props.vdisByVm,
(vms, vdisByVm) => (
map(vms, (vm, vmId) => {
let vdisSize = 0
let nVdis = 0
forEach(vdisByVm[vmId], vdi => {
vdisSize += vdi.size
nVdis++
})
return {
objectId: vmId,
label: vm.name_label,
data: {
nVCpus: vm.CPUs.number,
nVdis,
nVifs: vm.VIFs.length,
ram: vm.memory.size,
vdisSize
}
}
})
)
)
render () {
return (
<Container>
<Row>
<Col>
<XoParallelChart
dataSet={this._getData()}
labels={DATA_LABELS}
renderers={DATA_RENDERERS}
/>
</Col>
</Row>
</Container>
)
}
}

View File

@@ -3,12 +3,12 @@ import Component from 'base-component'
import Ellipsis, { EllipsisContainer } from 'ellipsis'
import Icon from 'icon'
import isEmpty from 'lodash/isEmpty'
import Link, { BlockLink } from 'link'
import map from 'lodash/map'
import React from 'react'
import SingleLineRow from 'single-line-row'
import Tags from 'tags'
import Tooltip from 'tooltip'
import { Link } from 'react-router'
import { Row, Col } from 'grid'
import { Text } from 'editable'
import {
@@ -19,7 +19,6 @@ import {
stopHost
} from 'xo'
import {
BlockLink,
connectStore,
formatSize,
osFamily

View File

@@ -7,9 +7,11 @@ import Component from 'base-component'
import debounce from 'lodash/debounce'
import forEach from 'lodash/forEach'
import Icon from 'icon'
import invoke from 'invoke'
import isEmpty from 'lodash/isEmpty'
import isString from 'lodash/isString'
import keys from 'lodash/keys'
import Link from 'link'
import map from 'lodash/map'
import Page from '../page'
import React from 'react'
@@ -24,7 +26,6 @@ import {
startVms,
stopVms
} from 'xo'
import { Link } from 'react-router'
import { Container, Row, Col } from 'grid'
import {
SelectHost,
@@ -32,8 +33,7 @@ import {
SelectTag
} from 'select-objects'
import {
connectStore,
invoke
connectStore
} from 'utils'
import {
areObjectsFetched,

View File

@@ -3,12 +3,12 @@ import Component from 'base-component'
import Ellipsis, { EllipsisContainer } from 'ellipsis'
import Icon from 'icon'
import isEmpty from 'lodash/isEmpty'
import Link, { BlockLink } from 'link'
import map from 'lodash/map'
import React from 'react'
import SingleLineRow from 'single-line-row'
import Tags from 'tags'
import Tooltip from 'tooltip'
import { Link } from 'react-router'
import { Row, Col } from 'grid'
import { Text, XoSelect } from 'editable'
import {
@@ -20,7 +20,6 @@ import {
stopVm
} from 'xo'
import {
BlockLink,
connectStore,
formatSize,
osFamily

View File

@@ -4,7 +4,7 @@ import HostActionBar from './action-bar'
import Icon from 'icon'
import isEmpty from 'lodash/isEmpty'
import map from 'lodash/map'
import { Link } from 'react-router'
import Link from 'link'
import { NavLink, NavTabs } from 'nav'
import Page from '../page'
import pick from 'lodash/pick'
@@ -14,7 +14,6 @@ import { Text } from 'editable'
import { editHost, fetchHostStats, getHostMissingPatches, installAllHostPatches, installHostPatch } from 'xo'
import { Container, Row, Col } from 'grid'
import {
autobind,
connectStore,
routes
} from 'utils'
@@ -129,7 +128,6 @@ const isRunning = host => host && host.power_state === 'Running'
}
})
export default class Host extends Component {
@autobind
loop (host = this.props.host) {
if (this.cancel) {
this.cancel()
@@ -156,6 +154,7 @@ export default class Host extends Component {
})
})
}
loop = ::this.loop
_getMissingPatches (host) {
getHostMissingPatches(host).then(missingPatches => {

View File

@@ -6,12 +6,10 @@ import React from 'react'
import Tags from 'tags'
import Tooltip from 'tooltip'
import { addTag, removeTag } from 'xo'
import { FormattedRelative } from 'react-intl'
import { BlockLink } from 'link'
import { Container, Row, Col } from 'grid'
import {
BlockLink,
formatSize
} from 'utils'
import { FormattedRelative } from 'react-intl'
import { formatSize } from 'utils'
import {
CpuSparkLines,
MemorySparkLines,

View File

@@ -3,7 +3,6 @@ import Component from 'base-component'
import Icon from 'icon'
import React from 'react'
import Upgrade from 'xoa-upgrade'
import { autobind } from 'utils'
import { fetchHostStats } from 'xo'
import { Container, Row, Col } from 'grid'
import {
@@ -14,7 +13,6 @@ import {
} from 'xo-line-chart'
export default class HostStats extends Component {
@autobind
loop (host = this.props.host) {
if (this.cancel) {
this.cancel()
@@ -42,6 +40,7 @@ export default class HostStats extends Component {
})
})
}
loop = ::this.loop
componentWillMount () {
this.loop()
@@ -64,7 +63,6 @@ export default class HostStats extends Component {
}
}
@autobind
handleSelectStats (event) {
const granularity = event.target.value
clearTimeout(this.timeout)
@@ -74,6 +72,7 @@ export default class HostStats extends Component {
selectStatsLoading: true
}, this.loop)
}
handleSelectStats = ::this.handleSelectStats
render () {
const {

View File

@@ -3,8 +3,9 @@ import React from 'react'
import _ from 'intl'
import isEmpty from 'lodash/isEmpty'
import map from 'lodash/map'
import { BlockLink } from 'link'
import { TabButtonLink } from 'tab-button'
import { BlockLink, formatSize } from 'utils'
import { formatSize } from 'utils'
import { ButtonGroup } from 'react-bootstrap-4/lib'
import { Container, Row, Col } from 'grid'
import { Text } from 'editable'

View File

@@ -4,7 +4,7 @@ import ActionRowButton from 'action-row-button'
import delay from 'lodash/delay'
import find from 'lodash/find'
import forEach from 'lodash/forEach'
import GenericInput from 'json-schema-input/generic-input'
import GenericInput from 'json-schema-input'
import includes from 'lodash/includes'
import isEmpty from 'lodash/isEmpty'
import map from 'lodash/map'
@@ -13,6 +13,7 @@ import size from 'lodash/size'
import Upgrade from 'xoa-upgrade'
import React, { Component } from 'react'
import { error } from 'notification'
import { generateUiSchema } from 'xo-json-schema-input'
import { SelectPlainObject } from 'form'
import {
@@ -53,7 +54,7 @@ const reduceObject = (value, propertyName = 'id') => value && value[propertyName
const dataToParamVectorItems = function (params, data) {
const items = []
forEach(params, (param, name) => {
if (Array.isArray(data[name]) && param.$type) { // We have an array for building cross product, the "real" type was $type
if (Array.isArray(data[name]) && param.items) { // We have an array for building cross product, the "real" type was $type
const values = []
if (data[name].length === 1) { // One value, no need to engage cross-product
data[name] = data[name].pop()
@@ -173,8 +174,11 @@ export default class Jobs extends Component {
Vm: 'VM(s)',
XoObject: 'Object(s)'
}
prop.$type = type
prop.type = 'array'
prop.items = {
type: 'string',
$type: type
}
prop.title = titles[type]
}
@@ -215,7 +219,8 @@ export default class Jobs extends Component {
method,
group,
command,
info
info,
uiSchema: generateUiSchema(info)
})
}
}
@@ -314,7 +319,7 @@ export default class Jobs extends Component {
</div>
<SelectPlainObject ref='method' options={actions} optionKey='method' onChange={this._handleSelectMethod} placeholder={_('jobActionPlaceHolder')} />
{action && <fieldset>
<GenericInput ref='params' schema={action.info} label={action.method} required />
<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 })}</p>}
{process.env.XOA_PLAN > 3
? <span><ActionButton form='newJobForm' handler={this._handleSubmit} icon='save' btnStyle='primary'>{_('saveResourceSet')}</ActionButton>

View File

@@ -4,21 +4,19 @@ import ActionToggle from 'action-toggle'
import filter from 'lodash/filter'
import forEach from 'lodash/forEach'
import Icon from 'icon'
import Link from 'link'
import LogList from '../../logs'
import map from 'lodash/map'
import orderBy from 'lodash/orderBy'
import Upgrade from 'xoa-upgrade'
import React, { Component } from 'react'
import Upgrade from 'xoa-upgrade'
import { ButtonGroup } from 'react-bootstrap-4/lib'
import { Container } from 'grid'
import { Link } from 'react-router'
import {
Card,
CardHeader,
CardBlock
} from 'card'
import {
deleteSchedule,
disableSchedule,

View File

@@ -7,11 +7,12 @@ import Icon from 'icon'
import includes from 'lodash/includes'
import map from 'lodash/map'
import orderBy from 'lodash/orderBy'
import propTypes from 'prop-types'
import React, { Component } from 'react'
import renderXoItem from 'render-xo-item'
import SortedTable from 'sorted-table'
import { alert, confirm } from 'modal'
import { connectStore, propTypes } from 'utils'
import { connectStore } from 'utils'
import { createGetObject } from 'selectors'
import { FormattedDate } from 'react-intl'

View File

@@ -2,7 +2,7 @@ import _ from 'intl'
import Component from 'base-component'
import classNames from 'classnames'
import Icon from 'icon'
import Link from 'react-router/lib/Link'
import Link from 'link'
import map from 'lodash/map'
import React from 'react'
import Tooltip from 'tooltip'
@@ -182,7 +182,7 @@ const MenuLinkItem = props => {
return <li className='nav-item xo-menu-item'>
<Link activeClassName='active' className={classNames('nav-link', styles.centerCollapsed)} to={to}>
<Icon className={classNames(pill && styles.hiddenCollapsed)} icon={`${icon}`} size='lg' fixedWidth />
<Icon className={classNames((pill || extra) && styles.hiddenCollapsed)} icon={`${icon}`} size='lg' fixedWidth />
<span className={styles.hiddenCollapsed}>{' '}{_(label)}&nbsp;</span>
{pill > 0 && <span className='tag tag-pill tag-primary'>{pill}</span>}
{extra}

View File

@@ -8,6 +8,7 @@ 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 Icon from 'icon'
import isArray from 'lodash/isArray'
import map from 'lodash/map'
@@ -38,7 +39,6 @@ import {
import {
connectStore,
formatSize,
getEventValue,
noop
} from 'utils'
import {

View File

@@ -1,15 +1,16 @@
import _, { messages } from 'intl'
import ActionButton from 'action-button'
import Component from 'base-component'
import Icon from 'icon'
import React from 'react'
import _, { messages } from 'intl'
import differenceBy from 'lodash/differenceBy'
import filter from 'lodash/filter'
import forEach from 'lodash/forEach'
import Icon from 'icon'
import intersection from 'lodash/intersection'
import isEmpty from 'lodash/isEmpty'
import keyBy from 'lodash/keyBy'
import map from 'lodash/map'
import propTypes from 'prop-types'
import React from 'react'
import reduce from 'lodash/reduce'
import renderXoItem from 'render-xo-item'
import Upgrade from 'xoa-upgrade'
@@ -17,13 +18,11 @@ import { Container, Col, Row } from 'grid'
import { createGetObjectsOfType } from 'selectors'
import { injectIntl } from 'react-intl'
import { SizeInput } from 'form'
import {
Card,
CardBlock,
CardHeader
} from 'card'
import {
SelectSubject,
SelectNetwork,
@@ -31,13 +30,10 @@ import {
SelectSr,
SelectVmTemplate
} from 'select-objects'
import {
connectStore,
formatSize,
propTypes
formatSize
} from 'utils'
import {
createResourceSet,
deleteResourceSet,

View File

@@ -1,12 +1,12 @@
import React, { Component } from 'react'
import _ from 'intl'
import forEach from 'lodash/forEach'
import keyBy from 'lodash/keyBy'
import map from 'lodash/map'
import propTypes from 'prop-types'
import React, { Component } from 'react'
import renderXoItem from 'render-xo-item'
import store from 'store'
import { getObject } from 'selectors'
import { propTypes } from 'utils'
import {
subscribeGroups,

View File

@@ -5,10 +5,11 @@ import includes from 'lodash/includes'
import isEmpty from 'lodash/isEmpty'
import keyBy from 'lodash/keyBy'
import map from 'lodash/map'
import propTypes from 'prop-types'
import React from 'react'
import size from 'lodash/size'
import SortedTable from 'sorted-table'
import { addSubscriptions, propTypes } from 'utils'
import { addSubscriptions } from 'utils'
import { injectIntl } from 'react-intl'
import { SelectSubject } from 'select-objects'
import { Text } from 'editable'

View File

@@ -6,6 +6,7 @@ import React, { Component } from 'react'
import _ from 'intl'
import map from 'lodash/map'
import { addSubscriptions } from 'utils'
import { generateUiSchema } from 'xo-json-schema-input'
import { lastly } from 'promise-toolbox'
import {
configurePlugin,
@@ -20,11 +21,13 @@ import {
class Plugin extends Component {
constructor (props) {
super(props)
const { configurationSchema } = props
// Don't update input with schema in edit mode!
// It's always the same!
this.state = {
configurationSchema: props.configurationSchema
configurationSchema,
uiSchema: generateUiSchema(configurationSchema)
}
this.formId = `form-${props.id}`
}
@@ -32,13 +35,17 @@ class Plugin extends Component {
componentWillReceiveProps (nextProps) {
// Don't update input with schema in edit mode!
if (!this.state.edit) {
const { configurationSchema } = nextProps
this.setState({
configurationSchema: nextProps.configurationSchema
configurationSchema,
uiSchema: generateUiSchema(configurationSchema)
})
if (this.refs.pluginInput) {
// TODO: Compare values!!!
this.refs.pluginInput.value = nextProps.configuration
// `|| undefined` because old configs can be null.
this.refs.pluginInput.value = nextProps.configuration || undefined
}
}
}
@@ -139,9 +146,10 @@ class Plugin extends Component {
disabled={!edit}
label='Configuration'
schema={state.configurationSchema}
uiSchema={state.uiSchema}
required
ref='pluginInput'
defaultValue={props.configuration}
defaultValue={props.configuration || undefined}
/>
<div className='form-group pull-xs-right'>
<div className='btn-toolbar'>

View File

@@ -1,40 +0,0 @@
import _ from 'intl'
import React, {
Component
} from 'react'
import { propTypes } from 'utils'
@propTypes({
onSubmit: propTypes.func.isRequired
})
export default class SignIn extends Component {
render () {
return <form onSubmit={event => {
event.preventDefault()
const { refs } = this
this.props.onSubmit({
password: refs.password.value,
username: refs.username.value
})
}}>
<p>
<label>
{_('usernameLabel')}
</label>
<input type='text' ref='username' />
</p>
<p>
<label>
{_('passwordLabel')}
</label>
<input type='password' ref='password' />
</p>
<p>
<button type='submit'>
{_('signInButton')}
</button>
</p>
</form>
}
}

View File

@@ -4,7 +4,7 @@ import CenterPanel from 'center-panel'
import Icon from 'icon'
import isEmpty from 'lodash/isEmpty'
import keys from 'lodash/keys'
import Link from 'react-router/lib/Link'
import Link from 'link'
import map from 'lodash/map'
import React from 'react'
import SingleLineRow from 'single-line-row'

View File

@@ -4,7 +4,7 @@ import BaseComponent from 'base-component'
import Icon from 'icon'
import React from 'react'
import { alert } from 'modal'
import { autobind, connectStore } from 'utils'
import { connectStore } from 'utils'
import { changePassword } from 'xo'
import { Container, Row, Col } from 'grid'
import { getLang, getUser } from 'selectors'
@@ -26,10 +26,10 @@ const HEADER = <Container>
})
@injectIntl
export default class User extends BaseComponent {
@autobind
handleSelectLang (event) {
handleSelectLang = event => {
this.props.selectLang(event.target.value)
}
_handleSavePassword = () => {
const { oldPassword, newPassword, confirmPassword } = this.state
if (newPassword !== confirmPassword) {

View File

@@ -3,8 +3,8 @@ import assign from 'lodash/assign'
import forEach from 'lodash/forEach'
import Icon from 'icon'
import isEmpty from 'lodash/isEmpty'
import Link from 'link'
import map from 'lodash/map'
import { Link } from 'react-router'
import { NavLink, NavTabs } from 'nav'
import Page from '../page'
import pick from 'lodash/pick'
@@ -18,7 +18,6 @@ import {
} from 'xo'
import { Container, Row, Col } from 'grid'
import {
autobind,
connectStore,
mapPlus,
routes
@@ -119,7 +118,6 @@ export default class Vm extends Component {
router: React.PropTypes.object
}
@autobind
loop (vm = this.props.vm) {
if (this.cancel) {
this.cancel()
@@ -146,6 +144,7 @@ export default class Vm extends Component {
})
})
}
loop = ::this.loop
componentWillMount () {
this.loop()

View File

@@ -7,20 +7,20 @@ import HTML5Backend from 'react-dnd-html5-backend'
import Icon from 'icon'
import isEmpty from 'lodash/isEmpty'
import IsoDevice from 'iso-device'
import Link from 'link'
import map from 'lodash/map'
import propTypes from 'prop-types'
import React from 'react'
import some from 'lodash/some'
import TabButton from 'tab-button'
import { ButtonGroup } from 'react-bootstrap-4/lib'
import { Container, Row, Col } from 'grid'
import { createSelector } from 'selectors'
import { DragDropContext, DragSource, DropTarget } from 'react-dnd'
import { Link } from 'react-router'
import { noop, propTypes } from 'utils'
import { noop } from 'utils'
import { SelectSr, SelectVdi } from 'select-objects'
import { SizeInput, Toggle } from 'form'
import { XoSelect, Size, Text } from 'editable'
import some from 'lodash/some'
import { createSelector } from 'selectors'
import {
attachDiskToVm,
createDisk,

View File

@@ -6,11 +6,11 @@ import map from 'lodash/map'
import React from 'react'
import Tags from 'tags'
import { addTag, editVm, removeTag } from 'xo'
import { BlockLink } from 'link'
import { FormattedRelative } from 'react-intl'
import { Container, Row, Col } from 'grid'
import { Number, Size } from 'editable'
import {
BlockLink,
formatSize,
osFamily
} from 'utils'

View File

@@ -1,14 +1,79 @@
import _ from 'intl'
import _, { messages } from 'intl'
import ActionButton from 'action-button'
import ActionRowButton from 'action-row-button'
import isEmpty from 'lodash/isEmpty'
import map from 'lodash/map'
import propTypes from 'prop-types'
import React, { Component } from 'react'
import TabButton from 'tab-button'
import { connectStore } from 'utils'
import { ButtonGroup } from 'react-bootstrap-4/lib'
import { connectStore, noop } from 'utils'
import { Container, Row, Col } from 'grid'
import {
createGetObjectsOfType,
createSelector
} from 'selectors'
import { injectIntl } from 'react-intl'
import { SelectNetwork } from 'select-objects'
import {
connectVif,
createVmInterface,
deleteVif,
disconnectVif
} from 'xo'
@propTypes({
onClose: propTypes.func,
vm: propTypes.object.isRequired
})
@injectIntl
class NewVif extends Component {
constructor (props) {
super(props)
this.state = {
network: undefined
}
}
_getNetworkPredicate = createSelector(
() => {
const { vm } = this.props
return vm && vm.$pool
},
poolId => network => network.$pool === poolId
)
_selectNetwork = network => this.setState({network})
_createVif = () => {
const { vm, onClose = noop } = this.props
const { mac, mtu } = this.refs
const { network } = this.state
return createVmInterface(vm, network, mac.value || undefined, mtu.value || String(network.MTU))
.then(onClose)
}
render () {
const formatMessage = this.props.intl.formatMessage
return <form id='newVifForm'>
<div className='form-group'>
<SelectNetwork predicate={this._getNetworkPredicate()} onChange={this._selectNetwork} required />
</div>
<fieldset className='form-inline'>
<div className='form-group'>
<input type='text' ref='mac' placeholder={formatMessage(messages.vifMacLabel)} className='form-control' /> ({_('vifMacAutoGenerate')})
</div>
{' '}
<div className='form-group'>
<input type='text' ref='mtu' placeholder={formatMessage(messages.vifMtuLabel)} className='form-control' />
</div>
<span className='pull-right'>
<ActionButton form='newVifForm' icon='add' btnStyle='primary' handler={this._createVif}>Create</ActionButton>
</span>
</fieldset>
</form>
}
}
@connectStore(() => {
const vifs = createGetObjectsOfType('VIF').pick(
@@ -27,7 +92,19 @@ import {
})
})
export default class TabNetwork extends Component {
constructor (props) {
super(props)
this.state = {
newVif: false
}
}
_toggleNewVif = () => this.setState({
newVif: !this.state.newVif
})
render () {
const { newVif } = this.state
const {
networks,
vifs,
@@ -39,12 +116,17 @@ export default class TabNetwork extends Component {
<Col className='text-xs-right'>
<TabButton
btnStyle='primary'
handler={() => null()} // TODO: add vif
handler={this._toggleNewVif}
icon='add'
labelId='vifCreateDeviceButton'
/>
</Col>
</Row>
<Row>
<Col>
{newVif && <div><NewVif vm={vm} onClose={this._toggleNewVif} /><hr /></div>}
</Col>
</Row>
<Row>
<Col>
{!isEmpty(vifs)
@@ -68,11 +150,34 @@ export default class TabNetwork extends Component {
<td>{networks[vif.$network] && networks[vif.$network].name_label}</td>
<td>
{vif.attached
? <span className='tag tag-success'>
? <span>
<span className='tag tag-success'>
{_('vifStatusConnected')}
</span>
<ButtonGroup className='pull-xs-right'>
<ActionRowButton
icon='disconnect'
handler={disconnectVif}
handlerParam={vif}
/>
</ButtonGroup>
</span>
: <span className='tag tag-default'>
: <span>
<span className='tag tag-default'>
{_('vifStatusDisconnected')}
</span>
<ButtonGroup className='pull-xs-right'>
<ActionRowButton
icon='connect'
handler={connectVif}
handlerParam={vif}
/>
<ActionRowButton
icon='remove'
handler={deleteVif}
handlerParam={vif}
/>
</ButtonGroup>
</span>
}
</td>

View File

@@ -3,7 +3,6 @@ import Component from 'base-component'
import Icon from 'icon'
import React from 'react'
import Upgrade from 'xoa-upgrade'
import { autobind } from 'utils'
import { fetchVmStats } from 'xo'
import { injectIntl } from 'react-intl'
import { Container, Row, Col } from 'grid'
@@ -16,7 +15,6 @@ import {
export default injectIntl(
class VmStats extends Component {
@autobind
loop (vm = this.props.vm) {
if (this.cancel) {
this.cancel()
@@ -44,6 +42,7 @@ export default injectIntl(
})
})
}
loop = ::this.loop
componentWillMount () {
this.loop()
@@ -66,7 +65,6 @@ export default injectIntl(
}
}
@autobind
handleSelectStats (event) {
const granularity = event.target.value
clearTimeout(this.timeout)
@@ -76,6 +74,7 @@ export default injectIntl(
selectStatsLoading: true
}, this.loop)
}
handleSelectStats = ::this.handleSelectStats
render () {
const {

View File

@@ -19,8 +19,14 @@ import { serverVersion } from 'xo'
import pkg from '../../../package'
const promptForReload = () => confirm({
title: _('promptUpgradeReloadTitle'),
body: <p>{_('promptUpgradeReloadMessage')}</p>
}).then(() => window.location.reload())
if (+process.env.XOA_PLAN < 5) {
xoaUpdater.start()
xoaUpdater.on('upgradeSuccessful', promptForReload)
}
const HEADER = <Container>
@@ -78,9 +84,9 @@ export default class XoaUpdates extends Component {
} catch (error) {
return
}
return xoaUpdater.register(email.value, password.value, alreadyRegistered)
.then(() => { email.value = password.value = '' })
}
return xoaUpdater.register(email.value, password.value, alreadyRegistered)
.then(() => { email.value = password.value = '' })
}
_configure = async () => {
@@ -129,7 +135,9 @@ export default class XoaUpdates extends Component {
serverVersion.then(serverVersion => {
this.setState({ serverVersion })
})
update()
}
render () {
const textClasses = {
info: 'text-info',
@@ -264,7 +272,7 @@ export default class XoaUpdates extends Component {
{' '}
<ActionButton form='registrationForm' icon='success' btnStyle='primary' handler={this._register}>{_('register')}</ActionButton>
</form>
{process.env.XOA_PLAN === 1 &&
{+process.env.XOA_PLAN === 1 &&
<div>
<h2>{_('trial')}</h2>
{this._trialAllowed(trial) &&
@@ -320,17 +328,17 @@ export const UpdateTag = connectStore((state) => {
const icons = {
'disconnected': 'update-unknown',
'connected': 'update-unknown',
'upToDate': 'check',
'upToDate': 'success',
'upgradeNeeded': 'update-ready',
'registerNeeded': 'not-registered',
'error': 'alarm'
}
const classNames = {
'disconnected': 'text-warning',
'connected': 'text-info',
'disconnected': 'text-danger',
'connected': 'text-success',
'upToDate': 'text-success',
'upgradeNeeded': 'text-primary',
'registerNeeded': 'text-warning',
'upgradeNeeded': 'text-warning',
'registerNeeded': 'text-danger',
'error': 'text-danger'
}
const tooltips = {
@@ -341,5 +349,5 @@ export const UpdateTag = connectStore((state) => {
'registerNeeded': _('registerNeeded'),
'error': _('updaterError')
}
return <Tooltip content={tooltips[state]}><Icon className={classNames[state]} icon={icons[state]} /></Tooltip>
return <Tooltip content={tooltips[state]}><Icon className={classNames[state]} icon={icons[state]} size='lg' /></Tooltip>
})