Compare commits

..

55 Commits

Author SHA1 Message Date
Olivier Lambert
8882df7939 5.1.8 2016-08-17 11:07:30 +02:00
Olivier Lambert
185a554cd9 fix(newVm): fix wrong ISO SR predicate. Fixes #1415 2016-08-17 11:06:35 +02:00
Olivier Lambert
230e0dc2a5 5.1.7 2016-08-16 15:38:59 +02:00
Pierre Donias
f5b69fdfdc feat(vm/console): hide header and resize console (#1410)
Fix #1268
2016-08-16 14:49:44 +02:00
Greenkeeper
01dc0d8f1e chore(package): update modular-css to version 0.26.0 (#1385)
https://greenkeeper.io/
2016-08-16 12:45:29 +02:00
Greenkeeper
8035886a3c chore(package): update promise-toolbox to version 0.5.0 (#1409)
https://greenkeeper.io/
2016-08-16 12:22:45 +02:00
Olivier Lambert
0ab5f4b13f fix(host): wrong storage link. Fixes #1408 2016-08-16 11:16:56 +02:00
Pierre Donias
a1bc98def8 feat(host): redirect to home when host disappears (#1406) 2016-08-16 09:50:29 +02:00
Olivier Lambert
868cf6140b feat(settings): more tooltips for server connect/disconnect 2016-08-15 18:04:01 +02:00
Olivier Lambert
4b3473f480 feat(logstackmodal): use pre tag for stack trace 2016-08-15 17:52:04 +02:00
Olivier Lambert
7bc782cc62 feat(copiable): add tooltip on copiable component 2016-08-15 17:33:59 +02:00
Olivier Lambert
e625a53e4a fix(vm migration): allow target network without IPs. Fixes #1403 2016-08-15 15:20:59 +02:00
Olivier Lambert
b31185d96d fix(newVm): typo spotted by @Danp2 2016-08-15 14:07:12 +02:00
Olivier Lambert
09d75e972f feat(newVm): add missing tooltips. Fixes #1402 2016-08-15 11:44:36 +02:00
Olivier Lambert
f33568951b 5.1.6 2016-08-12 17:28:49 +02:00
Pierre Donias
8d8c442be5 feat(settings/logs): new view to display API logs (#1401)
Fix #1344
2016-08-12 17:27:50 +02:00
Olivier Lambert
f890b8ea7a feat(modal text): warns users about consequences of host eject 2016-08-11 21:13:32 +02:00
Pierre Donias
1b80b3929c feat(host): detach host from its pool (#1399)
Fixes #1395
2016-08-11 17:49:25 +02:00
Pierre Donias
4f946293f6 feat(pool): add host (#1398)
Fixes #1374
2016-08-11 17:05:41 +02:00
Olivier Lambert
36788cde2b feat(vm disk): add VBD connect for a running VM. Fixes #1397 2016-08-11 16:52:52 +02:00
Pierre Donias
1547c99e5a feat(new-vm): use saved SSH key in cloud config(#1394)
* feat(new-vm): use saved SSH key in cloud config. Fixes #1319
2016-08-11 13:32:54 +02:00
Olivier Lambert
5c9606dad8 feat(pool): improve pool view. Fixes #1393 2016-08-11 10:34:03 +02:00
Olivier Lambert
fdcb1dccf5 feat(pool): start to work on adding an existing host to a pool 2016-08-11 09:47:52 +02:00
Olivier Lambert
12812b8c23 5.1.5 2016-08-10 18:06:19 +02:00
Olivier Lambert
0098497255 fix(select): select color modified due to an update. Fixes #1391 2016-08-10 16:02:48 +02:00
Olivier Lambert
6562d2de7f feat(sr select): display space left on SR. Fixes #1358 2016-08-10 15:58:36 +02:00
Olivier Lambert
1f0e88cdb0 feat(backup): better tooltips. Fixes #1363 2016-08-10 14:17:27 +02:00
Olivier Lambert
197da91ef3 feat(vdi remove): add modal when removing a VDI. Fixes #1388 2016-08-10 13:39:13 +02:00
Olivier Lambert
cbd59789e2 fix(vm disks): _isFreeForWriting missing case. Fixes #1386 2016-08-10 13:13:17 +02:00
Olivier Lambert
190ecf3d74 fix(pool): pool name and description edition. Fixes #1390 2016-08-10 12:42:46 +02:00
Olivier Lambert
15b8f6bca2 feat(meter tooltips): add tooltips for meter object. Fixes #1387 2016-08-10 12:31:28 +02:00
Pierre Donias
5b406d731b fix(vm): select destination SR when at least one VDI is local (#1382)
* Fixes #1357 
* fix(vm): select destination SR when at least one VDI is local
* fix(vm): do not send map when not necessary
2016-08-09 17:03:08 +02:00
Olivier Lambert
4be9e67ac4 fix(metercss): remove useless and conflicting CSS styles 2016-08-09 10:31:03 +02:00
Olivier Lambert
d047421685 feat(updates): enhance update view. Also fixes #1341 2016-08-08 16:46:47 +02:00
Olivier Lambert
f6f415a421 fix(network): name instead of description 2016-08-08 14:55:15 +02:00
Pierre Donias
edfaaebac0 feat(dashboard/health): Storage table: BlockLink (SR) and Link (SR's pool)
Fixes #1381
2016-08-08 14:15:49 +02:00
Olivier Lambert
67df22a1bf feat(vmsnapshot): add snapshot export and copy. Fixes #1353 and #1336 2016-08-08 14:05:27 +02:00
Pierre Donias
7dc59a00f6 feat(pool): action button to create an SR (#1380)
Fixes #1372
2016-08-08 12:45:12 +02:00
Pierre Donias
6214fe4c2e feat(pool): action button to create a VM (#1379)
Fix #1373
2016-08-08 11:35:24 +02:00
Greenkeeper
21610c3e0a chore(package): update ava to version 0.16.0 (#1377)
https://greenkeeper.io/
2016-08-08 09:57:36 +02:00
Olivier Lambert
87550b0189 5.1.4 2016-08-07 19:35:37 +02:00
Ronan Abhamon
b7c42d0a08 fix(scheduling): range not working
Fixes #1376
2016-08-07 19:35:05 +02:00
Olivier Lambert
c15ad299ac fix(sparklines): smaller sparklines and removing useless dots 2016-08-05 14:39:07 +02:00
Olivier Lambert
48c56cd602 5.1.3 2016-08-05 12:42:01 +02:00
Ronan Abhamon
7957f621ef fix(backups-edit): display correctly old backup jobs
Fixes #1366
2016-08-05 12:12:20 +02:00
Olivier Lambert
38ddbfdc9c fix(dashboard): inverted value for SR total/used. Fixes #1370 2016-08-04 14:42:50 +02:00
Olivier Lambert
3d2aae81da 5.1.2 2016-08-03 17:22:49 +02:00
Olivier Lambert
2227b9d061 feat(new vm): hide URL install for HVM templates. Fixes #1362 2016-08-03 16:55:43 +02:00
Olivier Lambert
12aab5fa8c feat(snapshots): add tooltips and confirm modal for snapshot oprations. Fixes #1349 2016-08-03 16:34:31 +02:00
Olivier Lambert
7323e6e117 fix(tooltip): remove tooltip if button changes. Fixes #1360 2016-08-03 15:53:38 +02:00
Greenkeeper
6f36869609 chore(package): update gulp-uglify to version 2.0.0 (#1355)
https://greenkeeper.io/
2016-08-02 14:47:18 +02:00
Ronan Abhamon
4a12419162 feat(backups): supports smart backup (close #837) (#1335)
Fixes #837
2016-07-31 19:03:41 +02:00
ABHAMON Ronan
bf91938aa6 fix(form/Toggle): refresh when set is used (#1347)
Fixes #1339
2016-07-29 11:35:58 +02:00
ABHAMON Ronan
bd70bd2b45 fix(scheduling): fix month selection highlighting (#1345)
Fixes #1338
2016-07-29 10:31:42 +02:00
Greenkeeper
bb26c8e449 chore(package): update modular-css to version 0.25.0 (#1331)
https://greenkeeper.io/
2016-07-28 00:16:06 +02:00
52 changed files with 1320 additions and 477 deletions

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "xo-web",
"version": "5.1.1",
"version": "5.1.8",
"license": "AGPL-3.0",
"description": "Web interface client for Xen-Orchestra",
"keywords": [
@@ -33,7 +33,7 @@
"devDependencies": {
"ansi_up": "^1.3.0",
"asap": "^2.0.4",
"ava": "^0.15.0",
"ava": "^0.16.0",
"babel-eslint": "^6.0.0",
"babel-plugin-transform-decorators-legacy": "^1.3.4",
"babel-plugin-transform-react-constant-elements": "^6.5.0",
@@ -67,7 +67,7 @@
"gulp-plumber": "^1.1.0",
"gulp-refresh": "^1.1.0",
"gulp-sass": "^2.2.0",
"gulp-uglify": "^1.5.3",
"gulp-uglify": "^2.0.0",
"gulp-watch": "^4.3.5",
"human-format": "^0.6.0",
"jsonrpc-websocket-client": "0.0.1-5",
@@ -75,12 +75,12 @@
"lodash": "^4.6.1",
"loose-envify": "^1.1.0",
"marked": "^0.3.5",
"modular-css": "^0.23.2",
"modular-css": "^0.26.0",
"moment": "^2.13.0",
"moment-timezone": "^0.5.4",
"notifyjs": "^2.0.1",
"novnc-node": "^0.5.3",
"promise-toolbox": "^0.4.0",
"promise-toolbox": "^0.5.0",
"random-password": "^0.1.2",
"react": "^15.0.0",
"react-addons-shallow-compare": "^15.1.0",

View File

@@ -6,17 +6,21 @@ import Tooltip from 'tooltip'
import {
ButtonGroup
} from 'react-bootstrap-4/lib'
import {
noop
} from 'utils'
const ActionBar = ({ actions, param }) => (
<ButtonGroup>
{map(actions, ({ handler, handlerParam = param, label, icon }, index) => (
{map(actions, ({ handler, handlerParam = param, label, icon, redirectOnSuccess }, index) => (
<Tooltip key={index} content={_(label)}>
<ActionButton
key={index}
btnStyle='secondary'
handler={handler}
handler={handler || noop}
handlerParam={handlerParam}
icon={icon}
redirectOnSuccess={redirectOnSuccess}
size='large'
/>
</Tooltip>
@@ -28,7 +32,8 @@ ActionBar.propTypes = {
React.PropTypes.shape({
label: React.PropTypes.string.isRequired,
icon: React.PropTypes.string.isRequired,
handler: React.PropTypes.func
handler: React.PropTypes.func,
redirectOnSuccess: React.PropTypes.string
})
).isRequired,
display: React.PropTypes.oneOf(['icon', 'text', 'both'])

View File

@@ -1,5 +1,7 @@
import _ from 'intl'
import CopyToClipboard from 'react-copy-to-clipboard'
import classNames from 'classnames'
import Tooltip from 'tooltip'
import React, { createElement } from 'react'
import Icon from '../icon'
@@ -18,10 +20,12 @@ const Copiable = propTypes({
},
props.children,
' ',
<CopyToClipboard text={props.data || props.children}>
<button className={classNames('btn btn-sm btn-secondary', styles.button)}>
<Icon icon='clipboard' />
</button>
</CopyToClipboard>
<Tooltip content={_('copyToClipboard')}>
<CopyToClipboard text={props.data || props.children}>
<button className={classNames('btn btn-sm btn-secondary', styles.button)}>
<Icon icon='clipboard' />
</button>
</CopyToClipboard>
</Tooltip>
))
export { Copiable as default }

View File

@@ -99,12 +99,9 @@ export class Range extends Component {
}
set value (value) {
const { onChange } = this.props
this.state.value = +value
if (onChange) {
onChange(value)
}
this.setState({
value: +value
})
}
_handleChange = event => {

View File

@@ -49,6 +49,7 @@ export default class Toggle extends Component {
}
this.refs.input.checked = Boolean(value)
this.forceUpdate()
}
_onChange = event => {

View File

@@ -17,6 +17,9 @@ var messages = {
onError: 'On error',
successful: 'Successful',
// ----- Copiable component -----
copyToClipboard: 'Copy to clipboard',
// ----- Titles -----
homePage: 'Home',
homeVmPage: 'VMs',
@@ -39,6 +42,7 @@ var messages = {
settingsGroupsPage: 'Groups',
settingsAclsPage: 'ACLs',
settingsPluginsPage: 'Plugins',
settingsLogsPage: 'Logs',
aboutPage: 'About',
newMenu: 'New',
taskMenu: 'Tasks',
@@ -134,6 +138,7 @@ var messages = {
selectResourceSetsSr: 'Select SR(s)…',
selectResourceSetsNetwork: 'Select network(s)…',
selectResourceSetsVdi: 'Select disk(s)…',
selectSshKey: 'Select SSH key(s)…',
selectSrs: 'Select SR(s)…',
selectVms: 'Select VM(s)…',
selectVmTemplates: 'Select VM template(s)…',
@@ -197,6 +202,9 @@ var messages = {
// ------ New backup -----
newBackupSelection: 'Select your backup type:',
smartBackupModeSelection: 'Select backup mode:',
normalBackup: 'Normal backup',
smartBackup: 'Smart backup',
// ------ New Remote -----
remoteList: 'Remote stores for backup',
@@ -326,6 +334,8 @@ var messages = {
srForget: 'Forget this SR',
srRemoveButton: 'Remove this SR',
srNoVdis: 'No VDIs in this storage',
// ----- Pool general -----
poolRamUsage: '{used} used on {total}',
// ----- Pool tabs -----
hostsTabName: 'Hosts',
// ----- Pool advanced tab -----
@@ -337,6 +347,7 @@ var messages = {
hostDescription: 'Description',
hostMemory: 'Memory',
noHost: 'No hosts',
memoryLeftTooltip: '{used}% used ({free} free)',
// ----- Pool network tab -----
poolNetworkNameLabel: 'Name',
poolNetworkDescription: 'Description',
@@ -473,6 +484,8 @@ var messages = {
ctrlAltDelButtonLabel: 'Ctrl+Alt+Del',
tipLabel: 'Tip:',
tipConsoleLabel: 'non-US keyboard could have issues with console: switch your own layout to US.',
hideHeaderTooltip: 'Hide infos',
showHeaderTooltip: 'Show infos',
// ----- VM disk tab -----
vdiAction: 'Action',
@@ -508,6 +521,10 @@ var messages = {
noSnapshots: 'No snapshots',
snapshotCreateButton: 'New snapshot',
tipCreateSnapshotLabel: 'Just click on the snapshot button to create one!',
revertSnapshot: 'Revert VM to this snapshot',
deleteSnapshot: 'Remove this snapshot',
copySnapshot: 'Create a VM from this snapshot',
exportSnapshot: 'Export this snapshot',
snapshotDate: 'Creation date',
snapshotName: 'Name',
snapshotAction: 'Action',
@@ -613,6 +630,7 @@ var messages = {
alarmObject: 'Issue on',
alarmPool: 'Pool',
alarmRemoveAll: 'Remove all alarms',
spaceLeftTooltip: '{used}% used ({free} left)',
// ----- New VM -----
newVmCreateNewVmOn: 'Create a new VM on {select}',
@@ -659,6 +677,8 @@ var messages = {
newVmMultipleVmsPattern: 'Name pattern:',
newVmMultipleVmsPatternPlaceholder: 'e.g.: \\{name\\}_%',
newVmFirstIndex: 'First index:',
newVmNumberRecalculate: 'Recalculate VMs number',
newVmNameRefresh: 'Refresh VMs name',
// ----- Self -----
resourceSets: 'Resource sets',
@@ -719,15 +739,19 @@ var messages = {
lastBackupColumn: 'Last Backup',
availableBackupsColumn: 'Available Backups',
restoreColumn: 'Restore',
restoreTip: 'Restore VM',
restoreTip: 'View restore options',
displayBackup: 'Display backups',
importBackupTitle: 'Import VM',
importBackupMessage: 'Starting your backup import',
vmsToBackup: 'VMs to backup',
// ----- Modals -----
emergencyShutdownHostsModalTitle: 'Emergency shutdown Host{nHosts, plural, one {} other {s}}',
emergencyShutdownHostsModalMessage: 'Are you sure you want to shutdown {nHosts} Host{nHosts, plural, one {} other {s}}?',
stopHostModalTitle: 'Shutdown host',
stopHostModalMessage: 'This will shutdown your host. Do you want to continue?',
addHostModalTitle: 'Add host',
addHostModalMessage: 'Are you sure you want to add {host} to {pool}?',
restartHostModalTitle: 'Restart host',
restartHostModalMessage: 'This will restart your host. Do you want to continue?',
restartHostsAgentsModalTitle: 'Restart Host{nHosts, plural, one {} other {s}} agent{nHosts, plural, one {} other {s}}',
@@ -748,10 +772,10 @@ var messages = {
restartVmsModalMessage: 'Are you sure you want to restart {vms} VM{vms, plural, one {} other {s}}?',
snapshotVmsModalTitle: 'Snapshot VM{vms, plural, one {} other {s}}',
snapshotVmsModalMessage: 'Are you sure you want to snapshot {vms} VM{vms, plural, one {} other {s}}?',
deleteVmModalTitle: 'Delete VM',
deleteVmsModalTitle: 'Delete VM{vms, plural, one {} other {s}}',
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',
deleteVmModalTitle: 'Delete VM',
deleteVmModalMessage: 'Are you sure you want to delete this VM? ALL VM DISKS WILL BE REMOVED',
migrateVmModalTitle: 'Migrate VM',
migrateVmSelectHost: 'Select a destination host:',
migrateVmSelectMigrationNetwork: 'Select a migration network:',
@@ -767,6 +791,10 @@ var messages = {
migrateVmNetwork: 'Network',
migrateVmNoTargetHost: 'No target host',
migrateVmNoTargetHostMessage: 'A target host is required to migrate a VM',
deleteVdiModalTitle: 'Delete VDI',
deleteVdiModalMessage: 'Are you sure you want to delete this disk? ALL DATA ON THIS DISK WILL BE LOST',
revertVmModalTitle: 'Revert your VM',
revertVmModalMessage: 'You are about to revert your VM to the snapshot state. This operation is irreversible',
importBackupModalTitle: 'Import a {name} Backup',
importBackupModalStart: 'Start VM after restore',
importBackupModalSelectBackup: 'Select your backup…',
@@ -789,6 +817,8 @@ var messages = {
serverPassword: 'Password',
serverAction: 'Action',
serverReadOnly: 'Read Only',
serverConnect: 'Connect server',
serverDisconnect: 'Disconnect server',
// ----- Copy VM -----
copyVm: 'Copy VM',
@@ -802,6 +832,11 @@ var messages = {
copyVmsNoTargetSr: 'No target SR',
copyVmsNoTargetSrMessage: 'A target SR is required to copy a VM',
// ----- Detach host -----
detachHostModalTitle: 'Detach host',
detachHostModalMessage: 'Are you sure you want to detach {host} from its pool? THIS WILL REMOVE ALL VMs ON ITS LOCAL STORAGE AND REBOOT THE HOST.',
detachHost: 'Detach',
// ----- Network -----
newNetworkCreate: 'Create network',
newNetworkInterface: 'Interface',
@@ -814,6 +849,11 @@ var messages = {
deleteNetwork: 'Delete network',
deleteNetworkConfirm: 'Are you sure you want to delete this network?',
// ----- Add host -----
addHostSelectHost: 'Host',
addHostNoHost: 'No host',
addHostNoHostMessage: 'No host selected to be added',
// ----- About View -----
xenOrchestra: 'Xen Orchestra',
xenOrchestraServer: 'server',
@@ -848,13 +888,16 @@ var messages = {
registration: 'Registration',
trial: 'Trial',
settings: 'Settings',
proxySettings: 'Proxy settings',
update: 'Update',
refresh: 'Refresh',
upgrade: 'Upgrade',
noUpdaterCommunity: 'No updater available for Community Edition',
noUpdaterSubscribe: 'Please consider subscribe and try it with all features for free during 15 days on',
noUpdaterWarning: 'Manual update could break your current installation due to dependencies issues, do it with caution',
currentVersion: 'Current version:',
register: 'Register',
editRegistration: 'Edit registration',
trialRegistration: 'Please, take time to register in order to enjoy your trial.',
trialStartButton: 'Start trial',
trialAvailableUntil: 'You can use a trial version until {date, date, medium}. Upgrade your appliance to get it.',
@@ -910,7 +953,19 @@ var messages = {
deleteSshKeyConfirmMessage: 'Are you sure you want to delete the SSH key {title}?',
// ----- Usage -----
others: 'Others'
others: 'Others',
// ----- Logs -----
loadingLogs: 'Loading logs...',
logUser: 'User',
logMethod: 'Method',
logMessage: 'Message',
logStack: 'Stack trace',
logTime: 'Date',
logNoStackTrace: 'No stack trace',
logDeleteAll: 'Delete all logs',
logDeleteAllTitle: 'Delete all logs',
logDeleteAllMessage: 'Are you sure you want to delete all the logs?'
}
forEach(messages, function (message, id) {
if (isString(message)) {

View File

@@ -102,6 +102,14 @@ export default class NoVnc extends Component {
this._clean()
}
componentWillReceiveProps (props) {
const rfb = this._rfb
if (rfb && this.props.scale !== props.scale) {
rfb.get_display().set_scale(props.scale || 1)
rfb.get_mouse().set_scale(props.scale || 1)
}
}
_focus = () => {
const rfb = this._rfb
if (rfb) {

View File

@@ -55,13 +55,12 @@ export const SrItem = propTypes({
let label = `${sr.name_label || sr.id}`
if (isSrWritable(sr)) {
label += ` (${formatSize(sr.size)})`
label += ` (${formatSize(sr.size - sr.physical_usage)} free)`
}
return (
<span>
<Icon icon='sr' /> {label}
{container && ` (${container.name_label || container.id})`}
</span>
)
}))
@@ -113,6 +112,11 @@ const xoItemToRender = {
<Icon icon='resource-set' /> {resourceSet.name}
</span>
),
sshKey: key => (
<span>
<Icon icon='ssh-key' /> {key.label}
</span>
),
// XO objects.
pool: pool => (

View File

@@ -263,9 +263,14 @@ class TableSelect extends Component {
onChange: propTypes.func.isRequired,
range: propTypes.array,
labelId: propTypes.string.isRequired,
value: propTypes.any.isRequired
value: propTypes.any.isRequired,
valueRenderer: propTypes.func
})
class TimePicker extends Component {
static defaultProps = {
valueRenderer: e => +e
}
constructor () {
super()
this.state = {
@@ -275,20 +280,18 @@ class TimePicker extends Component {
}
_update (props) {
const { refs } = this
const { value } = props
const { value, valueRenderer } = props
if (value.indexOf('/') === 1) {
this.setState({
activeKey: NAV_EVERY_N
})
refs.range.value = value.split('/')[1]
}, () => { this.refs.range.value = value.split('/')[1] })
} else {
this.setState({
activeKey: NAV_EACH_SELECTED,
tableValue: value === '*'
? []
: map(value.split(','), e => +e)
: map(value.split(','), valueRenderer)
})
}
}
@@ -371,6 +374,8 @@ class TimePicker extends Component {
const HOURS_RANGE = [2, 12]
const MINUTES_RANGE = [2, 30]
const decrement = e => e - 1
@propTypes({
cronPattern: propTypes.string.isRequired,
onChange: propTypes.func,
@@ -434,12 +439,14 @@ export default class Scheduler extends Component {
options={MONTHS}
onChange={this._onMonthChange}
value={cronPatternArr[PICKTIME_TO_ID['month']]}
valueRenderer={decrement}
/>
<TimePicker
labelId='MonthDay'
options={DAYS}
onChange={this._onMonthDayChange}
value={cronPatternArr[PICKTIME_TO_ID['monthDay']]}
valueRenderer={decrement}
/>
<TimePicker
labelId='WeekDay'

View File

@@ -31,6 +31,7 @@ import {
} from './utils'
import {
isSrWritable,
subscribeCurrentUser,
subscribeGroups,
subscribeRemotes,
subscribeResourceSets,
@@ -82,7 +83,7 @@ export class GenericSelect extends Component {
// Returns the values of the selected objects
// if they are contained in xoObjectsById.
return mapPlus(value, (value, push) => {
const o = xoObjectsById[value.value || value]
const o = xoObjectsById[value.value !== undefined ? value.value : value]
if (o) {
push(o)
@@ -96,11 +97,11 @@ export class GenericSelect extends Component {
// Supports id strings and objects.
_setValue (value, props = this.props) {
if (props.multi) {
return map(value, object => object.id || object)
return map(value, object => object.id !== undefined ? object.id : object)
}
return (value != null)
? value.id || value
? value.id !== undefined ? value.id : value
: ''
}
@@ -202,7 +203,7 @@ export class GenericSelect extends Component {
this.setState({
value: this._setValue(value)
}, onChange && (() => { onChange(this.value) }))
}, onChange && (() => onChange(this.value)))
}
// GroupBy: Display option with margin if not disabled and containers exists.
@@ -766,3 +767,38 @@ export class SelectResourceSetsNetwork extends Component {
)
}
}
// ===================================================================
export class SelectSshKey extends Component {
get value () {
return this.refs.select.value
}
set value (value) {
this.refs.select.value = value
}
componentWillMount () {
this.componentWillUnmount = subscribeCurrentUser(user => {
this.setState({
sshKeys: user && user.preferences && map(user.preferences.sshKeys, (key, id) => ({
id,
label: key.title,
type: 'sshKey'
}))
})
})
}
render () {
return (
<GenericSelect
ref='select'
placeholder={_('selectSshKey')}
{...this.props}
xoObjects={this.state.sshKeys || []}
/>
)
}
}

View File

@@ -300,7 +300,7 @@ export default class SortedTable extends Component {
</thead>
<tbody>
{map(this._getVisibleItems(), (item, i) => {
const colums = map(props.columns, (column, key) => (
const columns = map(props.columns, (column, key) => (
<td key={key}>
{column.itemRenderer(item, userData)}
</td>
@@ -313,8 +313,8 @@ export default class SortedTable extends Component {
key={id}
tagName='tr'
to={isFunction(rowLink) ? rowLink(item, userData) : rowLink}
>{colums}</BlockLink>
: <tr key={id}>{colums}</tr>
>{columns}</BlockLink>
: <tr key={id}>{columns}</tr>
})}
</tbody>
</table>

View File

@@ -91,6 +91,7 @@ export default class Tooltip extends Component {
_removeListeners () {
const node = this._node
this._hideTooltip()
if (!node) {
return

View File

@@ -7,6 +7,7 @@ import XoRemoteInput from './xo-remote-input'
import XoRoleInput from './xo-role-input'
import XoSrInput from './xo-sr-input'
import XoSubjectInput from './xo-subject-input'
import XoTagInput from './xo-tag-input'
import XoVmInput from './xo-vm-input'
import { getType, getXoType } from '../json-schema-input/helpers'
@@ -14,13 +15,14 @@ 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
tag: XoTagInput,
vm: XoVmInput,
xoobject: XoHighLevelObjectInput
}
// ===================================================================

View File

@@ -0,0 +1,26 @@
import React from 'react'
import { SelectTag } from 'select-objects'
import XoAbstractInput from './xo-abstract-input'
import { PrimitiveInputWrapper } from '../json-schema-input/helpers'
// ===================================================================
export default class TagInput extends XoAbstractInput {
render () {
const { props } = this
return (
<PrimitiveInputWrapper {...props}>
<SelectTag
disabled={props.disabled}
multi={props.multi}
onChange={props.onChange}
ref='input'
required={props.required}
defaultValue={props.defaultValue}
/>
</PrimitiveInputWrapper>
)
}
}

View File

@@ -1,8 +1,7 @@
import React from 'react'
import {
Sparklines,
SparklinesLine,
SparklinesSpots
SparklinesLine
} from 'react-sparklines'
import propTypes from './prop-types'
@@ -14,7 +13,7 @@ import {
const STYLE = {}
const WIDTH = 120
const HEIGHT = 40
const HEIGHT = 20
// ===================================================================
@@ -36,8 +35,7 @@ export const CpuSparkLines = propTypes({
return (
<Sparklines style={STYLE} data={computeArraysAvg(cpus)} max={100} min={0} width={WIDTH} height={HEIGHT}>
<SparklinesLine style={{ strokeWidth: 1, stroke: '#366e98', fill: '#366e98', fillOpacity: 0.5 }} color='#2598d9' />
<SparklinesSpots />
<SparklinesLine style={{ strokeWidth: 0.5, stroke: '#366e98', fill: '#366e98', fillOpacity: 0.5 }} color='#2598d9' />
</Sparklines>
)
})
@@ -53,8 +51,7 @@ export const MemorySparkLines = propTypes({
return (
<Sparklines style={STYLE} data={memoryUsed} max={memory[memory.length - 1]} min={0} width={WIDTH} height={HEIGHT}>
<SparklinesLine style={{ strokeWidth: 1, stroke: '#990822', fill: '#990822', fillOpacity: 0.5 }} color='#cc0066' />
<SparklinesSpots />
<SparklinesLine style={{ strokeWidth: 0.5, stroke: '#990822', fill: '#990822', fillOpacity: 0.5 }} color='#cc0066' />
</Sparklines>
)
})
@@ -70,8 +67,7 @@ export const XvdSparkLines = propTypes({
return (
<Sparklines style={STYLE} data={computeObjectsAvg(xvds)} min={0} width={WIDTH} height={HEIGHT}>
<SparklinesLine style={{ strokeWidth: 1, stroke: '#089944', fill: '#089944', fillOpacity: 0.5 }} color='#33cc33' />
<SparklinesSpots />
<SparklinesLine style={{ strokeWidth: 0.5, stroke: '#089944', fill: '#089944', fillOpacity: 0.5 }} color='#33cc33' />
</Sparklines>
)
})
@@ -87,8 +83,7 @@ export const VifSparkLines = propTypes({
return (
<Sparklines style={STYLE} data={computeObjectsAvg(vifs)} min={0} width={WIDTH} height={HEIGHT}>
<SparklinesLine style={{ strokeWidth: 1, stroke: '#eca649', fill: '#eca649', fillOpacity: 0.5 }} color='#ffd633' />
<SparklinesSpots />
<SparklinesLine style={{ strokeWidth: 0.5, stroke: '#eca649', fill: '#eca649', fillOpacity: 0.5 }} color='#ffd633' />
</Sparklines>
)
})
@@ -104,8 +99,7 @@ export const PifSparkLines = propTypes({
return (
<Sparklines style={STYLE} data={computeObjectsAvg(pifs)} min={0} width={WIDTH} height={HEIGHT}>
<SparklinesLine style={{ strokeWidth: 1, stroke: '#eca649', fill: '#eca649', fillOpacity: 0.5 }} color='#ffd633' />
<SparklinesSpots />
<SparklinesLine style={{ strokeWidth: 0.5, stroke: '#eca649', fill: '#eca649', fillOpacity: 0.5 }} color='#ffd633' />
</Sparklines>
)
})
@@ -121,8 +115,7 @@ export const LoadSparkLines = propTypes({
return (
<Sparklines style={STYLE} data={load} min={0} width={WIDTH} height={HEIGHT}>
<SparklinesLine style={{ strokeWidth: 1, stroke: '#33cc33', fill: '#33cc33', fillOpacity: 0.5 }} color='#33cc33' />
<SparklinesSpots />
<SparklinesLine style={{ strokeWidth: 0.5, stroke: '#33cc33', fill: '#33cc33', fillOpacity: 0.5 }} color='#33cc33' />
</Sparklines>
)
})

View File

@@ -0,0 +1,37 @@
import _ from 'intl'
import BaseComponent from 'base-component'
import every from 'lodash/every'
import React from 'react'
import SingleLineRow from 'single-line-row'
import { SelectHost } from 'select-objects'
import { Col } from 'grid'
import { connectStore } from 'utils'
import { createGetObjectsOfType } from 'selectors'
@connectStore(() => ({
hosts: createGetObjectsOfType('host')
}), { withRef: true })
export default class AddHostModal extends BaseComponent {
get value () {
return this.state
}
_hostPredicate = host =>
host.$pool !== this.props.pool.id &&
every(this.props.hosts, h => h.$pool !== host.$pool || h.id === host.id)
render () {
return <div>
<SingleLineRow>
<Col size={6}>{_('addHostSelectHost')}</Col>
<Col size={6}>
<SelectHost
onChange={this.linkState('host')}
predicate={this._hostPredicate}
value={this.state.host}
/>
</Col>
</SingleLineRow>
</div>
}
}

View File

@@ -159,6 +159,12 @@ const createSubscription = cb => {
if (!isEqual(result, cache)) {
cache = result
/* FIXME: Edge case:
* 1) MyComponent has a subscription with subscribers[1]
* 2) subscribers[0] causes the MyComponent unmounting (and thus its unsubscription)
* When subscribers[1] will be executed, it will no longer exist,
* which will throw an error (Uncaught (in promise) TypeError: subscriber is not a function)
*/
forEach(subscribers, subscriber => {
subscriber(result)
})
@@ -206,6 +212,8 @@ export const subscribeJobs = createSubscription(() => _call('job.getAll'))
export const subscribeJobsLogs = createSubscription(() => _call('log.get', {namespace: 'jobs'}))
export const subscribeApiLogs = createSubscription(() => _call('log.get', {namespace: 'api'}))
export const subscribePermissions = createSubscription(() => _call('acl.getCurrentPermissions'))
export const subscribePlugins = createSubscription(() => _call('plugin.get'))
@@ -307,6 +315,41 @@ export const editPool = (pool, props) => (
_call('pool.set', { id: resolveId(pool), ...props })
)
import AddHostModalBody from './add-host-modal'
export const addHostToPool = (pool, host) => {
if (host) {
return confirm({
title: _('addHostModalTitle'),
body: _('addHostModalMessage', { pool: pool.name_label, host: host.name_label })
}).then(() =>
_call('pool.mergeInto', { source: host.$pool, target: resolveId(pool), force: true })
)
}
return confirm({
title: _('addHostModalTitle'),
body: <AddHostModalBody pool={pool} />
}).then(
params => {
if (!params.host) {
error(_('addHostNoHost'), _('addHostNoHostMessage'))
return
}
_call('pool.mergeInto', { source: params.host.$pool, target: resolveId(pool), force: true })
},
noop
)
}
export const detachHost = host => (
confirm({
icon: 'host-eject',
title: _('detachHostModalTitle'),
body: _('detachHostModalMessage', {host: <strong>{host.name_label}</strong>})
}).then(
() => _call('host.detach', { host: resolveId(host) })
)
)
// Host --------------------------------------------------------------
export const editHost = (host, props) => (
@@ -665,7 +708,13 @@ export const importDeltaBackup = ({remote, file, sr}) => (
)
export const revertSnapshot = vm => (
_call('vm.revert', { id: resolveId(vm) })
confirm({
title: _('revertVmModalTitle'),
body: _('revertVmModalMessage')
}).then(
() => _call('vm.revert', { id: resolveId(vm) }),
noop
)
)
export const editVm = (vm, props) => (
@@ -753,7 +802,13 @@ export const editVdi = (vdi, props) => (
)
export const deleteVdi = vdi => (
_call('vdi.delete', { id: resolveId(vdi) })
confirm({
title: _('deleteVdiModalTitle'),
body: _('deleteVdiModalMessage')
}).then(
() => _call('vdi.delete', { id: resolveId(vdi) }),
noop
)
)
export const migrateVdi = (vdi, sr) => (
@@ -1227,6 +1282,14 @@ export const deleteJobsLog = id => (
)
)
// Logs
export const deleteApiLog = id => (
_call('log.delete', {namespace: 'api', id})::tap(
subscribeApiLogs.forceRefresh
)
)
// Acls, users, groups ----------------------------------------------------------
export const addAcl = ({subject, object, action}) => (
@@ -1341,8 +1404,16 @@ const _setUserPreferences = preferences => (
)
import NewSshKeyModalBody from './new-ssh-key-modal'
export const addSshKey = () => (
confirm({
export const addSshKey = key => {
const { preferences } = xo.user
const otherKeys = preferences && preferences.sshKeys || []
if (key) {
return _setUserPreferences({ sshKeys: [
...otherKeys,
key
]})
}
return confirm({
icon: 'ssh-key',
title: _('newSshKeyModalTitle'),
body: <NewSshKeyModalBody />
@@ -1352,8 +1423,6 @@ export const addSshKey = () => (
error(_('sshKeyErrorTitle'), _('sshKeyErrorMessage'))
return
}
const { preferences } = xo.user
const otherKeys = preferences && preferences.sshKeys || []
return _setUserPreferences({ sshKeys: [
...otherKeys,
newKey
@@ -1361,7 +1430,7 @@ export const addSshKey = () => (
},
noop
)
)
}
export const deleteSshKey = key => (
confirm({

View File

@@ -1,9 +1,11 @@
import BaseComponent from 'base-component'
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 invoke from '../../invoke'
@@ -22,8 +24,12 @@ import {
import {
createGetObjectsOfType,
createPicker,
createSelector
createSelector,
getObject
} from '../../selectors'
import {
isSrShared
} from 'xo'
import { isSrWritable } from '../'
@@ -59,6 +65,7 @@ import styles from './index.css'
networks: getNetworks,
pifs: getPifs,
pools: getPools,
vbds: getVbds,
vdis: getVdis,
vifs: getVifs
}
@@ -85,7 +92,26 @@ export default class MigrateVmModalBody extends BaseComponent {
)
)
this._getNetworkPredicate = createSelector(
this._getTargetNetworkPredicate = createSelector(
createPicker(
() => this.props.pifs,
() => this.state.host.$PIFs
),
pifs => {
if (!pifs) {
return false
}
const networks = {}
forEach(pifs, pif => {
networks[pif.$network] = true
})
return network => networks[network.id]
}
)
this._getMigrationNetworkPredicate = createSelector(
createPicker(
() => this.props.pifs,
() => this.state.host.$PIFs
@@ -118,7 +144,12 @@ export default class MigrateVmModalBody extends BaseComponent {
}
}
_getObject (id) {
return getObject(store.getState(), id)
}
_selectHost = host => {
// No host selected
if (!host) {
this.setState({
host: undefined,
@@ -126,20 +157,40 @@ export default class MigrateVmModalBody extends BaseComponent {
})
return
}
const intraPool = this.props.vm.$pool === host.$pool
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) {
doNotMigrateVdis = true
} else {
const _doNotMigrateVdi = {}
forEach(vbds, vbd => {
if (vbd.VDI != null) {
_doNotMigrateVdi[vbd.VDI] = isSrShared(this._getObject(this._getObject(vbd.VDI).$SR))
}
})
doNotMigrateVdis = every(_doNotMigrateVdi)
}
this.setState({
doNotMigrateVdis,
host,
intraPool,
mapVdisSrs: undefined,
mapVdisSrs: doNotMigrateVdis ? undefined : mapValues(vdis, vdi => defaultSr),
mapVifsNetworks: undefined,
migrationNetwork: undefined
})
return
}
const { networks, pools, pifs, vdis, vifs } = this.props
// Inter-pool
const { networks, pifs, 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.
@@ -158,6 +209,7 @@ export default class MigrateVmModalBody extends BaseComponent {
})
this.setState({
doNotMigrateVdis: false,
host,
intraPool,
mapVdisSrs: mapValues(vdis, vdi => defaultSr),
@@ -171,6 +223,7 @@ export default class MigrateVmModalBody extends BaseComponent {
render () {
const { vdis, vifs, networks } = this.props
const {
doNotMigrateVdis,
host,
intraPool,
mapVdisSrs,
@@ -190,6 +243,28 @@ export default class MigrateVmModalBody extends BaseComponent {
</Col>
</SingleLineRow>
</div>
{host && !doNotMigrateVdis && <div className={styles.groupBlock}>
<SingleLineRow>
<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 &&
<div>
@@ -199,34 +274,12 @@ export default class MigrateVmModalBody extends BaseComponent {
<Col size={6}>
<SelectNetwork
onChange={this._selectMigrationNetwork}
predicate={this._getNetworkPredicate()}
predicate={this._getMigrationNetworkPredicate()}
value={migrationNetworkId}
/>
</Col>
</SingleLineRow>
</div>
<div className={styles.groupBlock}>
<SingleLineRow>
<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>
<div className={styles.groupBlock}>
<SingleLineRow>
<Col>{_('migrateVmSelectNetworks')}</Col>
@@ -242,7 +295,7 @@ export default class MigrateVmModalBody extends BaseComponent {
<Col size={6}>
<SelectNetwork
onChange={network => this.setState({ mapVifsNetworks: { ...mapVifsNetworks, [vif.id]: network.id } })}
predicate={this._getNetworkPredicate()}
predicate={this._getTargetNetworkPredicate()}
value={mapVifsNetworks[vif.id]}
/>
</Col>

View File

@@ -87,7 +87,26 @@ export default class MigrateVmsModalBody extends BaseComponent {
)
)
this._getNetworkPredicate = createSelector(
this._getTargetNetworkPredicate = createSelector(
createPicker(
() => this.props.pifs,
() => this.state.host.$PIFs
),
pifs => {
if (!pifs) {
return false
}
const networks = {}
forEach(pifs, pif => {
networks[pif.$network] = true
})
return network => networks[network.id]
}
)
this._getMigrationNetworkPredicate = createSelector(
createPicker(
() => this.props.pifs,
() => this.state.host.$PIFs
@@ -261,7 +280,7 @@ export default class MigrateVmsModalBody extends BaseComponent {
<Col size={6}>
<SelectNetwork
onChange={this._selectMigrationNetwork}
predicate={this._getNetworkPredicate()}
predicate={this._getMigrationNetworkPredicate()}
value={migrationNetworkId}
/>
</Col>
@@ -290,7 +309,7 @@ export default class MigrateVmsModalBody extends BaseComponent {
<SelectNetwork
disabled={smartVifMapping}
onChange={this._selectNetwork}
predicate={this._getNetworkPredicate()}
predicate={this._getTargetNetworkPredicate()}
value={networkId}
/>
</Col>

View File

@@ -676,6 +676,10 @@
@extend .fa;
@extend .fa-puzzle-piece;
}
&-logs {
@extend .fa;
@extend .fa-list;
}
}
&-menu-about {
@extend .fa;

View File

@@ -72,17 +72,27 @@ $select-input-height: 40px; // Bootstrap input height
width: 100%;
}
.Select-value-label {
color: #373a3c;
}
.Select-control {
border-radius: unset;
}
// Disabled option style.
.Select-menu-outer {
.Select-option.is-disabled {
cursor: default;
font-weight: bold;
color: #777;
}
.Select-menu-outer .Select-option.is-disabled {
cursor: default;
font-weight: bold;
color: #777;
}
.Select-placeholder {
color: #999;
}
.Select--single > .Select-control .Select-value {
color: #333;
}
// COLORS ======================================================================

View File

@@ -6,10 +6,6 @@
// error for usage > 90%
meter {
/* Reset the default appearance */
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
/* For Firefox */
background: #EEE;
box-shadow: 0 2px 3px rgba(0,0,0,0.2) inset;

View File

@@ -1 +0,0 @@
export const getJobValues = job => job.values || job.items

View File

@@ -1,4 +1,4 @@
import _, { messages } from 'intl'
import _ from 'intl'
import ActionButton from 'action-button'
import Component from 'base-component'
import delay from 'lodash/delay'
@@ -9,10 +9,9 @@ import React from 'react'
import Scheduler, { SchedulePreview } from 'scheduling'
import Upgrade from 'xoa-upgrade'
import Wizard, { Section } from 'wizard'
import { Container } from 'grid'
import { Container, Row, Col } from 'grid'
import { error } from 'notification'
import { generateUiSchema } from 'xo-json-schema-input'
import { injectIntl } from 'react-intl'
import {
createJob,
@@ -21,7 +20,55 @@ import {
updateSchedule
} from 'xo'
import { getJobValues } from '../helpers'
// ===================================================================
const NO_SMART_SCHEMA = {
type: 'object',
properties: {
vms: {
type: 'array',
items: {
type: 'string',
'xo:type': 'vm'
},
title: 'VMs',
description: 'Choose VMs to backup.'
}
},
required: [ 'vms' ]
}
const NO_SMART_UI_SCHEMA = generateUiSchema(NO_SMART_SCHEMA)
const SMART_SCHEMA = {
type: 'object',
properties: {
status: {
default: 'All',
enum: [ 'All', 'Running', 'Halted' ],
title: 'VMs statuses',
description: 'The statuses of VMs to backup.'
},
pools: {
type: 'array',
items: {
type: 'string',
'xo:type': 'pool'
},
title: 'Resident on'
},
tags: {
type: 'array',
items: {
type: 'string',
'xo:type': 'tag'
},
title: 'VMs Tags',
description: 'VMs which contains at least one of these tags. Not used if empty.'
}
},
required: [ 'status', 'pools' ]
}
const SMART_UI_SCHEMA = generateUiSchema(SMART_SCHEMA)
// ===================================================================
@@ -33,15 +80,6 @@ const COMMON_SCHEMA = {
title: 'Tag',
description: 'Back-up tag.'
},
vms: {
type: 'array',
items: {
type: 'string',
'xo:type': 'vm'
},
title: 'VMs',
description: 'Choose VMs to backup.'
},
_reportWhen: {
enum: [ 'never', 'always', 'failure' ],
title: 'Report',
@@ -191,7 +229,6 @@ const BACKUP_METHOD_TO_INFO = {
const DEFAULT_CRON_PATTERN = '0 0 * * *'
@injectIntl
export default class New extends Component {
constructor (props) {
super(props)
@@ -215,48 +252,107 @@ export default class New extends Component {
// Values are displayed, but html5 compliant browsers say the value is required and empty on submit
}
_populateForm = (job) => {
let values = getJobValues(job.paramsVector)
const { backupInput } = this.refs
_populateForm = job => {
let values = job.paramsVector.items
const {
backupInput,
vmsInput
} = this.refs
if (values.length === 1) {
// Older versions of XenOrchestra uses only values[0].
values = getJobValues(values[0])
const array = values[0].values
const config = array[0]
const reportWhen = config._reportWhen
backupInput.value = {
...values[0],
vms: map(values, value => value.id)
...config,
_reportWhen:
// Fix old reportWhen values...
(reportWhen === 'fail' && 'failure') ||
(reportWhen === 'alway' && 'always') ||
reportWhen
}
vmsInput.value = { vms: map(array, ({ id, vm }) => id || vm) }
} else {
backupInput.value = {
...getJobValues(values[1])[0],
vms: getJobValues(values[0])
if (values[1].type === 'map') {
// Smart backup.
const {
$pool: { __or: pools },
tags: { __or: tags } = {},
power_state: status = 'All'
} = values[1].collection.pattern
backupInput.value = values[0].values[0]
this.setState({
smartBackupMode: true
}, () => {
vmsInput.value = {
pools,
status,
tags: map(tags, tag => tag[0])
}
})
} else {
// Normal backup.
backupInput.value = values[1].values[0]
vmsInput.value = { vms: values[0].values }
}
}
}
_handleSubmit = () => {
const backup = this.refs.backupInput.value
const {
vms,
enabled,
...callArgs
} = backup
} = this.refs.backupInput.value
const vmsInputValue = this.refs.vmsInput.value
const { backupInfo, timezone } = this.state
const {
backupInfo,
smartBackupMode,
timezone
} = this.state
const paramsVector = !smartBackupMode
? {
type: 'crossProduct',
items: [{
type: 'set',
values: map(vmsInputValue.vms, vm => ({ id: vm }))
}, {
type: 'set',
values: [ callArgs ]
}]
} : {
type: 'crossProduct',
items: [{
type: 'set',
values: [ callArgs ]
}, {
type: 'map',
collection: {
type: 'fetchObjects',
pattern: {
$pool: !vmsInputValue.pools.length ? undefined : { __or: vmsInputValue.pools },
power_state: vmsInputValue.status === 'All' ? undefined : vmsInputValue.status,
tags: !vmsInputValue.tags.length ? undefined : { __or: map(vmsInputValue.tags, tag => [ tag ]) },
type: 'VM'
}
},
iteratee: {
type: 'extractProperties',
mapping: { id: 'id' }
}
}]
}
const job = {
type: 'call',
key: backupInfo.jobKey,
method: backupInfo.method,
paramsVector: {
type: 'crossProduct',
items: [{
type: 'set',
values: map(vms, vm => ({ id: vm }))
}, {
type: 'set',
values: [ callArgs ]
}]
}
paramsVector
}
// Update backup schedule.
@@ -299,77 +395,126 @@ export default class New extends Component {
})
}
_handleSmartBackupMode = event => {
this.setState({
smartBackupMode: event.target.value === 'smart'
})
}
render () {
const {
backupInfo,
cronPattern,
defaultValue,
smartBackupMode,
timezone
} = this.state
const { formatMessage } = this.props.intl
return process.env.XOA_PLAN > 1
? (
<Wizard>
<Section icon='backup' title={this.props.job ? 'editVmBackup' : 'newVmBackup'}>
<fieldset className='form-group'>
<label htmlFor='selectBackup'>{_('newBackupSelection')}</label>
<select
className='form-control'
defaultValue={(backupInfo && backupInfo.method) || null}
id='selectBackup'
onChange={this._handleBackupSelection}
required
>
<option value={null}>{formatMessage(messages.noSelectedValue)}</option>
{map(BACKUP_METHOD_TO_INFO, (info, key) =>
<option key={key} value={key}>{formatMessage(messages[info.label])}</option>
)}
</select>
</fieldset>
<form className='card-block' id='form-new-vm-backup'>
{backupInfo &&
<GenericInput
defaultValue={defaultValue}
label={<span><Icon icon={backupInfo.icon} /> {formatMessage(messages[backupInfo.label])}</span>}
required
schema={backupInfo.schema}
uiSchema={backupInfo.uiSchema}
ref='backupInput'
/>
}
</form>
<Container>
<Row>
<Col>
<fieldset className='form-group'>
<label htmlFor='selectBackup'>{_('newBackupSelection')}</label>
<select
className='form-control'
value={(backupInfo && backupInfo.method) || ''}
id='selectBackup'
onChange={this._handleBackupSelection}
required
>
{_('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>
<form id='form-new-vm-backup'>
{backupInfo && (
<div>
<GenericInput
label={<span><Icon icon={backupInfo.icon} /> {_(backupInfo.label)}</span>}
ref='backupInput'
required
schema={backupInfo.schema}
uiSchema={backupInfo.uiSchema}
/>
<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
? (process.env.XOA_PLAN > 2
? <GenericInput
label={<span><Icon icon='vm' /> {_('vmsToBackup')}</span>}
ref='vmsInput'
required
schema={SMART_SCHEMA}
uiSchema={SMART_UI_SCHEMA}
/>
: <Container><Upgrade place='newBackup' available={3} /></Container>
) : <GenericInput
label={<span><Icon icon='vm' /> {_('vmsToBackup')}</span>}
ref='vmsInput'
required
schema={NO_SMART_SCHEMA}
uiSchema={NO_SMART_UI_SCHEMA}
/>
}
</div>
)}
</form>
</Col>
</Row>
</Container>
</Section>
<Section icon='schedule' title='schedule'>
<Scheduler
cronPattern={cronPattern}
timezone={timezone}
onChange={this._updateCronPattern}
timezone={timezone}
/>
</Section>
<Section icon='preview' title='preview' summary>
<div className='card-block'>
<SchedulePreview cronPattern={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]} />
: <fieldset className='pull-xs-right p-t-1'>
<ActionButton
btnStyle='primary'
className='btn-lg m-r-1'
disabled={!backupInfo}
form='form-new-vm-backup'
handler={this._handleSubmit}
icon='save'
redirectOnSuccess='/backup/overview'
>
{_('saveBackupJob')}
</ActionButton>
<button type='button' className='btn btn-lg btn-secondary' onClick={this._handleReset}>
{_('selectTableReset')}
</button>
</fieldset>
}
</div>
<Container>
<Row>
<Col>
<SchedulePreview cronPattern={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-xs-right p-t-1'>
<ActionButton
btnStyle='primary'
className='btn-lg m-r-1'
disabled={!backupInfo}
form='form-new-vm-backup'
handler={this._handleSubmit}
icon='save'
redirectOnSuccess='/backup/overview'
>
{_('saveBackupJob')}
</ActionButton>
<button type='button' className='btn btn-lg btn-secondary' onClick={this._handleReset}>
{_('selectTableReset')}
</button>
</fieldset>)
}
</Col>
</Row>
</Container>
</Section>
</Wizard>
)

View File

@@ -25,8 +25,6 @@ import {
subscribeScheduleTable
} from 'xo'
import { getJobValues } from '../helpers'
// ===================================================================
const jobKeyToLabel = {
@@ -95,12 +93,12 @@ export default class Overview extends Component {
_getScheduleTag (schedule, job = {}) {
try {
const { paramsVector } = job
const values = getJobValues(paramsVector)
const values = paramsVector.items
// Old versions of XenOrchestra uses values[0]
return (
getJobValues(values[0])[0].tag ||
getJobValues(values[1])[0].tag
values[0].values[0].tag ||
values[1].values[0].tag
)
} catch (_) {}

View File

@@ -137,7 +137,7 @@ export default class Restore extends Component {
{r.enabled && <span className='tag tag-success'>{_('remoteEnabled')}</span>}
{r.error && <span className='tag tag-danger'>{_('remoteError')}</span>}
<span className='pull-right'>
<ActionButton disabled={!r.enabled} icon='refresh' btnStyle='default' handler={this._list} handlerParam={r.id} />
<Tooltip content={_('displayBackup')}><ActionButton disabled={!r.enabled} icon='refresh' btnStyle='default' handler={this._list} handlerParam={r.id} /></Tooltip>
</span>
{r.backupInfoByVm && <div>
<br />

View File

@@ -2,9 +2,11 @@ import _ from 'intl'
import ActionRowButton from 'action-row-button'
import Icon from 'icon'
import isEmpty from 'lodash/isEmpty'
import Link from 'link'
import map from 'lodash/map'
import SortedTable from 'sorted-table'
import TabButton from 'tab-button'
import Tooltip from 'tooltip'
import Upgrade from 'xoa-upgrade'
import React, { Component } from 'react'
import { Card, CardHeader, CardBlock } from 'card'
@@ -25,7 +27,7 @@ import {
const SrColContainer = connectStore(() => ({
container: createGetObject()
}))(({ container }) => <span>{container.name_label}</span>)
}))(({ container }) => <Link to={`pools/${container.id}`}>{container.name_label}</Link>)
const VdiColSr = connectStore(() => ({
sr: createGetObject()
@@ -65,7 +67,10 @@ const SR_COLUMNS = [
},
{
name: _('srUsage'),
itemRenderer: sr => sr.size > 1 && <meter value={(sr.physical_usage / sr.size) * 100} min='0' max='100' optimum='40' low='80' high='90'></meter>,
itemRenderer: sr => sr.size > 1 &&
<Tooltip content={_('spaceLeftTooltip', {used: Math.round((sr.physical_usage / sr.size) * 100), free: formatSize(sr.size - sr.physical_usage)})}>
<meter value={(sr.physical_usage / sr.size) * 100} min='0' max='100' optimum='40' low='80' high='90'></meter>
</Tooltip>,
sortCriteria: sr => sr.physical_usage / sr.size,
sortOrder: 'desc'
}
@@ -230,6 +235,8 @@ export default class Health extends Component {
)
)
_getSrUrl = sr => `srs/${sr.id}`
render () {
return process.env.XOA_PLAN > 3
? <Container>
@@ -244,7 +251,12 @@ export default class Health extends Component {
? <p className='text-xs-center'>{_('noSrs')}</p>
: <Row>
<Col>
<SortedTable collection={this.props.userSrs} columns={SR_COLUMNS} defaultColumn={4} />
<SortedTable
collection={this.props.userSrs}
columns={SR_COLUMNS}
defaultColumn={4}
rowLink={this._getSrUrl}
/>
</Col>
</Row>
}

View File

@@ -264,8 +264,8 @@ export default class Overview extends Component {
/>
<p className='text-xs-center'>
{_('ofUsage', {
total: formatSize(props.srMetrics.srUsage),
usage: formatSize(props.srMetrics.srTotal)
total: formatSize(props.srMetrics.srTotal),
usage: formatSize(props.srMetrics.srUsage)
})}
</p>
</BlockLink>

View File

@@ -129,6 +129,10 @@ const isRunning = host => host && host.power_state === 'Running'
}
})
export default class Host extends Component {
static contextTypes = {
router: React.PropTypes.object
}
loop (host = this.props.host) {
if (this.cancel) {
this.cancel()
@@ -182,6 +186,10 @@ export default class Host extends Component {
}
const hostCur = this.props.host
if (hostCur && !hostNext) {
this.context.router.push('/')
}
if (!hostCur) {
this._getMissingPatches(hostNext)
}

View File

@@ -3,7 +3,7 @@ import Copiable from 'copiable'
import React from 'react'
import TabButton from 'tab-button'
import { Toggle } from 'form'
import { enableHost, disableHost, restartHost } from 'xo'
import { enableHost, detachHost, disableHost, restartHost } from 'xo'
import { FormattedRelative, FormattedTime } from 'react-intl'
import { Container, Row, Col } from 'grid'
@@ -39,6 +39,13 @@ export default ({
labelId='enableHostLabel'
/>
}
<TabButton
btnStyle='danger'
handler={detachHost}
handlerParam={host}
icon='host-eject'
labelId='detachHost'
/>
</Col>
</Row>
<Row>

View File

@@ -38,7 +38,7 @@ export default ({
<BlockLink to={`/hosts/${host.id}/stats`}>{statsOverview && <PifSparkLines data={statsOverview} />}</BlockLink>
</Col>
<Col mediumSize={3}>
<BlockLink to={`/hosts/${host.id}/disks`}><h2>{host.$PBDs.length}x <Icon icon='disk' size='lg' /></h2></BlockLink>
<BlockLink to={`/hosts/${host.id}/storage`}><h2>{host.$PBDs.length}x <Icon icon='disk' size='lg' /></h2></BlockLink>
<BlockLink to={`/hosts/${host.id}/stats`}>{statsOverview && <LoadSparkLines data={statsOverview} />}</BlockLink>
</Col>
</Row>

View File

@@ -3,6 +3,7 @@ import React from 'react'
import _ from 'intl'
import isEmpty from 'lodash/isEmpty'
import map from 'lodash/map'
import Tooltip from 'tooltip'
import { BlockLink } from 'link'
import { TabButtonLink } from 'tab-button'
import { formatSize } from 'utils'
@@ -51,7 +52,9 @@ export default ({
<td>{formatSize(sr.size)}</td>
<td>
{sr.size > 1 &&
<meter value={(sr.physical_usage / sr.size) * 100} min='0' max='100' optimum='40' low='80' high='90'></meter>
<Tooltip content={_('spaceLeftTooltip', {used: Math.round((sr.physical_usage / sr.size) * 100), free: formatSize(sr.size - sr.physical_usage)})}>
<meter value={(sr.physical_usage / sr.size) * 100} min='0' max='100' optimum='40' low='80' high='90'></meter>
</Tooltip>
}
</td>
<td>

View File

@@ -114,9 +114,9 @@ export default class XoApp extends Component {
{blocked ? <XoaUpdates /> : this.props.children}
</div>
</div>
<TooltipViewer />
<Modal />
<Notification />
<TooltipViewer />
</div>
</IntlProvider>
}

View File

@@ -121,7 +121,8 @@ export default class Menu extends Component {
{ to: '/settings/groups', icon: 'menu-settings-groups', label: 'settingsGroupsPage' },
{ to: '/settings/acls', icon: 'menu-settings-acls', label: 'settingsAclsPage' },
{ to: '/settings/remotes', icon: 'menu-backup-remotes', label: 'backupRemotesPage' },
{ to: '/settings/plugins', icon: 'menu-settings-plugins', label: 'settingsPluginsPage' }
{ to: '/settings/plugins', icon: 'menu-settings-plugins', label: 'settingsPluginsPage' },
{ to: '/settings/logs', icon: 'menu-settings-logs', label: 'settingsLogsPage' }
]},
{ to: '/jobs/overview', icon: 'menu-jobs', label: 'jobsPage', subMenu: [
{ to: '/jobs/overview', icon: 'menu-jobs-overview', label: 'jobsOverviewPage' },

View File

@@ -54,14 +54,13 @@
.configDrive {
display: flex;
flex-direction: column;
background-color: #eee;
padding: 1em;
margin-bottom: 0.5em;
}
.configDriveToggle {
margin-left: auto;
margin-right: auto;
margin: auto;
}
.refreshNames {
@@ -71,3 +70,7 @@
.customConfig {
resize: both;
}
.fixedWidth {
width: 20em;
}

View File

@@ -14,20 +14,24 @@ import includes from 'lodash/includes'
import isArray from 'lodash/isArray'
import isEmpty from 'lodash/isEmpty'
import isObject from 'lodash/isObject'
import join from 'lodash/join'
import map from 'lodash/map'
import Page from '../page'
import React from 'react'
import size from 'lodash/size'
import slice from 'lodash/slice'
import store from 'store'
import Tooltip from 'tooltip'
import Wizard, { Section } from 'wizard'
import { Button } from 'react-bootstrap-4/lib'
import { Container, Row, Col } from 'grid'
import { injectIntl } from 'react-intl'
import {
addSshKey,
createVm,
createVms,
getCloudInitConfig,
subscribeCurrentUser,
subscribePermissions,
subscribeResourceSets,
XEN_DEFAULT_CPU_CAP,
@@ -42,6 +46,7 @@ import {
SelectResourceSetsVdi,
SelectResourceSetsVmTemplate,
SelectSr,
SelectSshKey,
SelectVdi,
SelectVmTemplate
} from 'select-objects'
@@ -50,6 +55,7 @@ import {
Toggle
} from 'form'
import {
addSubscriptions,
buildTemplate,
connectStore,
formatSize,
@@ -99,17 +105,32 @@ const Item = ({ label, children, className }) => (
const getObject = createGetObject((_, id) => id)
@addSubscriptions({
user: subscribeCurrentUser
})
@connectStore(() => ({
isAdmin: createSelector(
getUser,
user => user && user.permission === 'admin'
),
networks: createGetObjectsOfType('network').sort(),
pool: createGetObject((_, props) => props.location.query.pool),
pools: createGetObjectsOfType('pool'),
templates: createGetObjectsOfType('VM-template').sort()
templates: createGetObjectsOfType('VM-template').sort(),
userSshKeys: createSelector(
(_, props) => {
const user = props.user
return user && user.preferences && user.preferences.sshKeys
},
keys => keys
)
}))
@injectIntl
export default class NewVm extends BaseComponent {
static contextTypes = {
router: React.PropTypes.object
}
constructor () {
super()
@@ -163,6 +184,10 @@ export default class NewVm extends BaseComponent {
// Actions ---------------------------------------------------------------------
_reset = ({ pool, resourceSet } = { pool: this.state.pool, resourceSet: this.state.resourceSet }) => {
if (!pool) {
pool = this.props.pool
}
this.setState({ pool, resourceSet })
this._replaceState({
bootAfterCreate: true,
@@ -215,7 +240,11 @@ export default class NewVm extends BaseComponent {
if (state.configDrive) {
const hostname = state.name_label.replace(/^\s+|\s+$/g, '').replace(/\s+/g, '-')
if (state.installMethod === 'SSH') {
cloudConfig = '#cloud-config\nhostname: ' + hostname + '\nssh_authorized_keys:\n - ' + state.sshKey + '\n'
cloudConfig = `#cloud-config\nhostname: ${hostname}\nssh_authorized_keys:\n${
join(map(state.sshKeys, keyId => {
return this.props.userSshKeys[keyId] ? ` - ${this.props.userSshKeys[keyId].key}\n` : ''
}), '')
}`
} else {
cloudConfig = state.customConfig
}
@@ -310,7 +339,8 @@ export default class NewVm extends BaseComponent {
cpuCap: '',
cpuWeight: '',
// installation
installMethod: template.install_methods && template.install_methods[0] || state.installMethod,
installMethod: template.install_methods && template.install_methods[0] || 'SSH',
sshKeys: this.props.userSshKeys.length && [ 0 ],
customConfig: '#cloud-config\n#hostname: myhostname\n#ssh_authorized_keys:\n# - ssh-rsa <myKey>\n#packages:\n# - htop\n',
// interfaces
VIFs,
@@ -375,8 +405,8 @@ export default class NewVm extends BaseComponent {
(isInPool, isInResourceSet) => disk =>
(isInResourceSet(disk) || isInPool(disk)) && disk.content_type !== 'iso' && disk.size > 0
)
_getIsoPredicate = () => disk =>
disk.content_type === 'iso'
_getIsoPredicate = () => sr =>
sr.$pool === this.state.pool.id && sr.SR_type === 'iso'
_getNetworkPredicate = createSelector(
this._getIsInPool,
this._getIsInResourceSet,
@@ -467,11 +497,13 @@ export default class NewVm extends BaseComponent {
this._setState({ [prop]: value })
}
}
_onChangeSshKeys = keys => this._setState({ sshKeys: map(keys, key => key.id) })
_updateNbVms = () => {
const { nbVms, nameLabels, seqStart } = this.state.state
const nbVmsClamped = clamp(nbVms, NB_VMS_MIN, NB_VMS_MAX)
const newNameLabels = [ ...nameLabels ]
if (nbVmsClamped < nameLabels.length) {
this._setState({ nameLabels: slice(newNameLabels, 0, nbVmsClamped) })
} else {
@@ -487,6 +519,7 @@ export default class NewVm extends BaseComponent {
const nbVms = nameLabels.length
const newNameLabels = []
const replacer = this._buildTemplate()
for (let i = +seqStart; i <= +seqStart + nbVms - 1; i++) {
newNameLabels.push(replacer(this.state.state, i))
}
@@ -494,11 +527,19 @@ export default class NewVm extends BaseComponent {
}
_selectResourceSet = resourceSet =>
this._reset({ pool: undefined, resourceSet })
_selectPool = pool =>
_selectPool = pool => {
const { pathname, query } = this.props.location
this.context.router.push({
pathname,
query: { ...query, pool: pool.id }
})
this._reset({ pool, resourceSet: undefined })
}
_addVdi = () => {
const { pool, state } = this.state
const device = String(this.getUniqueId())
this._setState({ VDIs: [ ...state.VDIs, {
device,
name_description: 'Created by XO',
@@ -509,10 +550,12 @@ export default class NewVm extends BaseComponent {
}
_removeVdi = index => {
const { VDIs } = this.state.state
this._setState({ VDIs: [ ...VDIs.slice(0, index), ...VDIs.slice(index + 1) ] })
}
_addInterface = () => {
const networkId = this._getDefaultNetworkId()
this._setState({ VIFs: [ ...this.state.state.VIFs, {
id: this.getUniqueId(),
network: networkId
@@ -520,9 +563,29 @@ export default class NewVm extends BaseComponent {
}
_removeInterface = index => {
const { VIFs } = this.state.state
this._setState({ VIFs: [ ...VIFs.slice(0, index), ...VIFs.slice(index + 1) ] })
}
_addNewSshKey = () => {
const { newSshKey, sshKeys } = this.state.state
const { userSshKeys } = this.props
const splitKey = newSshKey.split(' ')
const title = splitKey.length === 3 ? splitKey[2].split('\n')[0] : newSshKey.substring(newSshKey.length - 10, newSshKey.length)
// save key
addSshKey({
title,
key: newSshKey
}).then(() => {
// select key
this._setState({
sshKeys: [ ...(sshKeys || []), userSshKeys ? userSshKeys.length : 0 ],
newSshKey: ''
})
})
}
_getRedirectionUrl = id =>
this.state.state.multipleVms ? '/home' : `/vms/${id}`
@@ -700,11 +763,11 @@ export default class NewVm extends BaseComponent {
value={nbVms}
/>
<span className='input-group-btn'>
<Button bsStyle='secondary' disabled={!multipleVms} onClick={this._updateNbVms}><Icon icon='arrow-right' /></Button>
<Tooltip content={_('newVmNumberRecalculate')}><Button bsStyle='secondary' disabled={!multipleVms} onClick={this._updateNbVms}><Icon icon='arrow-right' /></Button></Tooltip>
</span>
</Item>
<Item>
<a className={styles.refreshNames} onClick={this._updateNameLabels}><Icon icon='refresh' /></a>
<Tooltip content={_('newVmNameRefresh')}><a className={styles.refreshNames} onClick={this._updateNameLabels}><Icon icon='refresh' /></a></Tooltip>
</Item>
{multipleVms && <LineItem>
{map(nameLabels, (nameLabel, index) =>
@@ -784,44 +847,64 @@ export default class NewVm extends BaseComponent {
installIso,
installMethod,
installNetwork,
newSshKey,
pv_args,
sshKey
sshKeys
} = this.state.state
return <Section icon='new-vm-install-settings' title='newVmInstallSettingsPanel' done={this._isInstallSettingsDone()}>
{this._isDiskTemplate ? <SectionContent key='diskTemplate'>
<div className={styles.configDrive}>
<span className={styles.configDriveToggle}>
{_('newVmConfigDrive')}
</span>
<span className={styles.configDriveToggle}>
<Toggle
value={configDrive}
onChange={this._getOnChange('configDrive')}
{this._isDiskTemplate ? <SectionContent key='diskTemplate' column>
<LineItem>
<div className={styles.configDrive}>
<span className={styles.configDriveToggle}>
{_('newVmConfigDrive')}
</span>
&nbsp;
<span className={styles.configDriveToggle}>
<Toggle
value={configDrive}
onChange={this._getOnChange('configDrive')}
/>
</span>
</div>
</LineItem>
<LineItem>
<span>
<input
checked={installMethod === 'SSH'}
disabled={!configDrive}
name='installMethod'
onChange={this._getOnChange('installMethod')}
type='radio'
value='SSH'
/>
{' '}
<span>{_('newVmSshKey')}</span>
</span>
</div>
<Item>
<input
checked={installMethod === 'SSH'}
disabled={!configDrive}
name='installMethod'
onChange={this._getOnChange('installMethod')}
type='radio'
value='SSH'
/>
{' '}
<span>{_('newVmSshKey')}</span>
{' '}
<DebounceInput
className='form-control'
debounceTimeout={DEBOUNCE_TIMEOUT}
disabled={!configDrive || installMethod !== 'SSH'}
onChange={this._getOnChange('sshKey')}
type='text'
value={sshKey}
/>
</Item>
<Item>
&nbsp;
<span className={classNames('input-group', styles.fixedWidth)}>
<DebounceInput
className='form-control'
disabled={!configDrive || installMethod !== 'SSH'}
debounceTimeout={DEBOUNCE_TIMEOUT}
onChange={this._getOnChange('newSshKey')}
value={newSshKey}
/>
<span className='input-group-btn'>
<Button className='btn btn-secondary' onClick={this._addNewSshKey} disabled={!newSshKey}>
<Icon icon='add' />
</Button>
</span>
</span>
{this.props.userSshKeys.length > 0 && <span className={styles.fixedWidth}>
<SelectSshKey
disabled={!configDrive || installMethod !== 'SSH'}
onChange={this._onChangeSshKeys}
multi
value={sshKeys || []}
/>
</span>}
</LineItem>
<LineItem>
<input
checked={installMethod === 'customConfig'}
disabled={!configDrive}
@@ -830,9 +913,9 @@ export default class NewVm extends BaseComponent {
type='radio'
value='customConfig'
/>
{' '}
&nbsp;
<span>{_('newVmCustomConfig')}</span>
{' '}
&nbsp;
<DebounceInput
className={classNames('form-control', styles.customConfig)}
debounceTimeout={DEBOUNCE_TIMEOUT}
@@ -841,7 +924,7 @@ export default class NewVm extends BaseComponent {
onChange={this._getOnChange('customConfig')}
value={customConfig}
/>
</Item>
</LineItem>
</SectionContent>
: <SectionContent>
<Item>
@@ -873,36 +956,38 @@ export default class NewVm extends BaseComponent {
</span>
</span>
</Item>
<Item>
<input
checked={installMethod === 'network'}
name='installMethod'
onChange={this._getOnChange('installMethod')}
type='radio'
value='network'
/>
{' '}
<span>{_('newVmNetworkLabel')}</span>
{' '}
<DebounceInput
className='form-control'
debounceTimeout={DEBOUNCE_TIMEOUT}
disabled={installMethod !== 'network'}
key='networkInput'
onChange={this._getOnChange('installNetwork')}
placeholder='e.g: http://ftp.debian.org/debian'
value={installNetwork}
/>
</Item>
{template.virtualizationMode === 'pv'
? <Item label='newVmPvArgsLabel' key='pv'>
<DebounceInput
className='form-control'
debounceTimeout={DEBOUNCE_TIMEOUT}
onChange={this._getOnChange('pv_args')}
value={pv_args}
/>
</Item>
? <span>
<Item>
<input
checked={installMethod === 'network'}
name='installMethod'
onChange={this._getOnChange('installMethod')}
type='radio'
value='network'
/>
{' '}
<span>{_('newVmNetworkLabel')}</span>
{' '}
<DebounceInput
className='form-control'
debounceTimeout={DEBOUNCE_TIMEOUT}
disabled={installMethod !== 'network'}
key='networkInput'
onChange={this._getOnChange('installNetwork')}
placeholder='e.g: http://ftp.debian.org/debian'
value={installNetwork}
/>
</Item>
<Item label='newVmPvArgsLabel' key='pv'>
<DebounceInput
className='form-control'
debounceTimeout={DEBOUNCE_TIMEOUT}
onChange={this._getOnChange('pv_args')}
value={pv_args}
/>
</Item>
</span>
: <Item>
<input
checked={installMethod === 'PXE'}
@@ -936,7 +1021,7 @@ export default class NewVm extends BaseComponent {
installIso,
installMethod,
installNetwork,
sshKey,
sshKeys,
template
} = this.state.state
switch (installMethod) {
@@ -944,7 +1029,7 @@ export default class NewVm extends BaseComponent {
case 'ISO': return installIso
case 'network': return /^(http|ftp|nfs)/i.exec(installNetwork)
case 'PXE': return true
case 'SSH': return sshKey || !configDrive
case 'SSH': return !isEmpty(sshKeys) || !configDrive
default: return template && this._isDiskTemplate && !configDrive
}
}

View File

@@ -7,6 +7,7 @@ import styles from './index.css'
const Page = ({
children,
collapsedHeader,
formatTitle,
header,
intl,
@@ -16,9 +17,9 @@ const Page = ({
return (
<DocumentTitle title={formatTitle ? formatMessage(messages[title]) : title}>
<div className={styles.container}>
<nav className={'page-header ' + styles.header}>
{!collapsedHeader && <nav className={'page-header ' + styles.header}>
{header}
</nav>
</nav>}
<div className={styles.content}>
{children}
</div>
@@ -29,6 +30,7 @@ const Page = ({
Page.propTypes = {
children: React.PropTypes.node,
collapsedHeader: React.PropTypes.bool,
formatTitle: React.PropTypes.bool,
header: React.PropTypes.node,
title: React.PropTypes.string

View File

@@ -1,5 +1,8 @@
import ActionBar from 'action-bar'
import React from 'react'
import {
addHostToPool
} from 'xo'
const NOT_IMPLEMENTED = () => {
throw new Error('not implemented')
@@ -11,17 +14,17 @@ const PoolActionBar = ({ pool }) => (
{
icon: 'add-sr',
label: 'addSrLabel',
handler: NOT_IMPLEMENTED // TODO add sr
redirectOnSuccess: `new/sr?host=${pool.master}`
},
{
icon: 'add-vm',
label: 'addVmLabel',
handler: NOT_IMPLEMENTED // TODO add VM
redirectOnSuccess: `vms/new?pool=${pool.id}`
},
{
icon: 'add-host',
label: 'addHostLabel',
handler: NOT_IMPLEMENTED // TODO add host
handler: addHostToPool
},
{
icon: 'disconnect',

View File

@@ -95,6 +95,9 @@ import TabStorage from './tab-storage'
})
export default class Pool extends Component {
_setNameDescription = nameDescription => editPool(this.props.pool, { name_description: nameDescription })
_setNameLabel = nameLabel => editPool(this.props.pool, { name_label: nameLabel })
header () {
const { pool } = this.props
if (!pool) {
@@ -108,13 +111,13 @@ export default class Pool extends Component {
{' '}
<Text
value={pool.name_label}
onChange={nameLabel => editPool(pool, { nameLabel })}
onChange={this._setNameLabel}
/>
</h2>
<span>
<Text
value={pool.name_description}
onChange={nameDescription => editPool(pool, { nameDescription })}
onChange={this._setNameDescription}
/>
</span>
</Col>

View File

@@ -1,8 +1,14 @@
import _ from 'intl'
import Icon from 'icon'
import map from 'lodash/map'
import React from 'react'
import sumBy from 'lodash/sumBy'
import Tags from 'tags'
import { addTag, removeTag } from 'xo'
import { BlockLink } from 'link'
import { Container, Row, Col } from 'grid'
import Usage, { UsageElement } from 'usage'
import { formatSize } from 'utils'
export default ({
hosts,
@@ -10,15 +16,39 @@ export default ({
pool,
srs
}) => <Container>
<br />
<Row className='text-xs-center'>
<Col mediumSize={4}>
<h2>{hosts.length}x <Icon icon='host' size='lg' /></h2>
<BlockLink to={`/pools/${pool.id}/hosts`}><h2>{hosts.length}x <Icon icon='host' size='lg' /></h2></BlockLink>
</Col>
<Col mediumSize={4}>
<h2>{srs.length}x <Icon icon='sr' size='lg' /></h2>
<BlockLink to={`/pools/${pool.id}/storage`}><h2>{srs.length}x <Icon icon='sr' size='lg' /></h2></BlockLink>
</Col>
<Col mediumSize={4}>
<h2>{nVms}x <Icon icon='vm' size='lg' /></h2>
<BlockLink to={`/home?s=$pool:${pool.id}`}><h2>{nVms}x <Icon icon='vm' size='lg' /></h2></BlockLink>
</Col>
</Row>
<br />
<Row>
<Col className='text-xs-center'>
<h5>Pool RAM usage:</h5>
</Col>
</Row>
<Row>
<Col smallOffset={1} mediumSize={10}>
<Usage total={sumBy(hosts, 'memory.size')}>
{map(hosts, host => <UsageElement
tooltip={host.name_label}
key={host.id}
value={host.memory.usage}
href={`#/hosts/${host.id}`}
/>)}
</Usage>
</Col>
</Row>
<Row>
<Col className='text-xs-center'>
<h5>{_('poolRamUsage', {used: formatSize(sumBy(hosts, 'memory.usage')), total: formatSize(sumBy(hosts, 'memory.size'))})}</h5>
</Col>
</Row>
<Row className='text-xs-center'>

View File

@@ -3,9 +3,11 @@ import isEmpty from 'lodash/isEmpty'
import Link from 'link'
import React from 'react'
import SortedTable from 'sorted-table'
import Tooltip from 'tooltip'
import { Container, Row, Col } from 'grid'
import { editHost } from 'xo'
import { Text } from 'editable'
import { formatSize } from 'utils'
const HOST_COLUMNS = [
{
@@ -24,7 +26,10 @@ const HOST_COLUMNS = [
},
{
name: _('hostMemory'),
itemRenderer: ({ memory }) => <meter value={memory.usage} min='0' max={memory.size}></meter>,
itemRenderer: ({ memory }) =>
<Tooltip content={_('memoryLeftTooltip', {used: Math.round((memory.usage / memory.size) * 100), free: formatSize(memory.size - memory.usage)})}>
<meter value={memory.usage} min='0' max={memory.size}></meter>
</Tooltip>,
sortCriteria: ({ memory }) => memory.usage / memory.size,
sortOrder: 'desc'
}

View File

@@ -110,7 +110,7 @@ export default class TabNetworks extends Component {
<Text value={network.name_label} onChange={value => editNetwork(network, { name_label: value })} />
</td>
<td>
<Text value={network.name_description} onChange={value => editNetwork(network, { name_label: value })} />
<Text value={network.name_description} onChange={value => editNetwork(network, { name_description: value })} />
</td>
<td>{network.MTU}</td>
<td>

View File

@@ -3,6 +3,7 @@ import isEmpty from 'lodash/isEmpty'
import Link from 'link'
import React from 'react'
import SortedTable from 'sorted-table'
import Tooltip from 'tooltip'
import { Container, Row, Col } from 'grid'
import { editSr, isSrShared } from 'xo'
import { formatSize } from 'utils'
@@ -30,7 +31,10 @@ const SR_COLUMNS = [
},
{
name: _('srUsage'),
itemRenderer: sr => sr.size > 1 && <meter value={(sr.physical_usage / sr.size) * 100} min='0' max='100' optimum='40' low='80' high='90'></meter>,
itemRenderer: sr => sr.size > 1 &&
<Tooltip content={_('spaceLeftTooltip', {used: Math.round((sr.physical_usage / sr.size) * 100), free: formatSize(sr.size - sr.physical_usage)})}>
<meter value={(sr.physical_usage / sr.size) * 100} min='0' max='100' optimum='40' low='80' high='90'></meter>
</Tooltip>,
sortCriteria: sr => sr.physical_usage / sr.size,
sortOrder: 'desc'
},

View File

@@ -8,6 +8,7 @@ import { NavLink, NavTabs } from 'nav'
import Acls from './acls'
import Groups from './groups'
import Logs from './logs'
import Plugins from './plugins'
import Remotes from './remotes'
import Servers from './servers'
@@ -26,6 +27,7 @@ const HEADER = <Container>
<NavLink to={'/settings/acls'}><Icon icon='menu-settings-acls' /> {_('settingsAclsPage')}</NavLink>
<NavLink to={'/settings/remotes'}><Icon icon='menu-backup-remotes' /> {_('backupRemotesPage')}</NavLink>
<NavLink to={'/settings/plugins'}><Icon icon='menu-settings-plugins' /> {_('settingsPluginsPage')}</NavLink>
<NavLink to={'/settings/logs'}><Icon icon='menu-settings-logs' /> {_('settingsLogsPage')}</NavLink>
</NavTabs>
</Col>
</Row>
@@ -34,6 +36,7 @@ const HEADER = <Container>
const Settings = routes('servers', {
acls: Acls,
groups: Groups,
logs: Logs,
plugins: Plugins,
remotes: Remotes,
servers: Servers,

View File

@@ -0,0 +1,4 @@
.message {
overflow-wrap: break-word;
max-width: 20em;
}

View File

@@ -0,0 +1,93 @@
import _ from 'intl'
import ActionRowButton from 'action-row-button'
import BaseComponent from 'base-component'
import Copiable from 'copiable'
import find from 'lodash/find'
import forEach from 'lodash/forEach'
import map from 'lodash/map'
import React from 'react'
import size from 'lodash/size'
import SortedTable from 'sorted-table'
import TabButton from 'tab-button'
import { addSubscriptions } from 'utils'
import { subscribeApiLogs, subscribeUsers, deleteApiLog } from 'xo'
import { FormattedDate } from 'react-intl'
import { Button } from 'react-bootstrap-4/lib'
import { alert, confirm } from 'modal'
import styles from './index.css'
@addSubscriptions({
logs: subscribeApiLogs,
users: subscribeUsers
})
export default class Logs extends BaseComponent {
_showStack = log => alert(_('logStack'), <Copiable tagName='pre'>{log.data.error.stack}</Copiable>)
_deleteAllLogs = () =>
confirm({
title: _('logDeleteAllTitle'),
body: _('logDeleteAllMessage')
}).then(() =>
forEach(this.props.logs, (log, id) => deleteApiLog(id))
)
render () {
const columns = [
{
name: _('logUser'),
itemRenderer: log => {
if (log.data.userId == null) {
return _('unknownUser')
}
return this.props.users ? find(this.props.users, user => user.id === log.data.userId).email : '...'
},
sortCriteria: log => log.data.userId
},
{
name: _('logMethod'),
itemRenderer: log => log.data.method,
sortCriteria: log => log.data.method
},
{
name: _('logMessage'),
itemRenderer: log => <div className={styles.message}>{log.data.error && log.data.error.message}</div>,
sortCriteria: log => log.data.error && log.data.error.message
},
{
name: _('logStack'),
itemRenderer: log => log.data.error && log.data.error.stack ? <Button onClick={() => this._showStack(log)} bsStyle='secondary'>Show stack</Button> : _('logNoStackTrace')
},
{
name: _('logTime'),
itemRenderer: log => <span>
{log.time && <FormattedDate value={new Date(log.time)} month='long' day='numeric' year='numeric' hour='2-digit' minute='2-digit' second='2-digit' />}
{' '}
<span className='pull-right'>
<ActionRowButton btnStyle='default' handler={deleteApiLog} handlerParam={log.id} icon='delete' />
</span>
</span>,
sortCriteria: log => log.time,
sortOrder: 'desc'
}
]
const { logs } = this.props
if (!logs) {
return <h3>{_('loadingLogs')}</h3>
}
return <div>
{size(logs)
? <div>
<span className='pull-xs-right'>
<TabButton
btnStyle='danger'
handler={this._deleteAllLogs}
icon='delete'
labelId='logDeleteAll'
/>
</span>
{' '}
<SortedTable collection={map(logs, (log, id) => ({ ...log, id }))} columns={columns} defaultColumn={4} />
</div>
: <p>{_('noLogs')}</p>}
</div>
}
}

View File

@@ -2,6 +2,7 @@ import _ from 'intl'
import ActionButton from 'action-button'
import ActionRowButton from 'action-row-button'
import map from 'lodash/map'
import Tooltip from 'tooltip'
import React, { Component } from 'react'
import { addSubscriptions } from 'utils'
import { Container } from 'grid'
@@ -68,24 +69,28 @@ export default class Servers extends Component {
</td>
<td>
{server.status === 'disconnected'
? <ActionRowButton
btnStyle='secondary'
handler={connectServer}
handlerParam={server}
icon='connect'
style={{
marginRight: '0.5em'
}}
/>
: <ActionRowButton
btnStyle='warning'
handler={disconnectServer}
handlerParam={server}
icon='disconnect'
style={{
marginRight: '0.5em'
}}
/>
? <Tooltip content={_('serverConnect')}>
<ActionRowButton
btnStyle='secondary'
handler={connectServer}
handlerParam={server}
icon='connect'
style={{
marginRight: '0.5em'
}}
/>
</Tooltip>
: <Tooltip content={_('serverDisconnect')}>
<ActionRowButton
btnStyle='warning'
handler={disconnectServer}
handlerParam={server}
icon='disconnect'
style={{
marginRight: '0.5em'
}}
/>
</Tooltip>
}
<ActionRowButton
btnStyle='danger'

View File

@@ -1,5 +1,6 @@
import _ from 'intl'
import assign from 'lodash/assign'
import BaseComponent from 'base-component'
import forEach from 'lodash/forEach'
import Icon from 'icon'
import isEmpty from 'lodash/isEmpty'
@@ -8,7 +9,7 @@ import map from 'lodash/map'
import { NavLink, NavTabs } from 'nav'
import Page from '../page'
import pick from 'lodash/pick'
import React, { cloneElement, Component } from 'react'
import React, { cloneElement } from 'react'
import VmActionBar from './action-bar'
import { Select, Text } from 'editable'
import {
@@ -113,7 +114,7 @@ const isRunning = vm => vm && vm.power_state === 'Running'
}
}
})
export default class Vm extends Component {
export default class Vm extends BaseComponent {
static contextTypes = {
router: React.PropTypes.object
}
@@ -243,6 +244,8 @@ export default class Vm extends Component {
</Container>
}
_toggleHeader = () => this.setState({ collapsedHeader: !this.state.collapsedHeader })
render () {
const { container, vm } = this.props
@@ -262,8 +265,8 @@ export default class Vm extends Component {
]), pick(this.state, [
'statsOverview'
]))
return <Page header={this.header()} title={`${vm.name_label}${container ? ` (${container.name_label})` : ''}`}>
{cloneElement(this.props.children, childProps)}
return <Page header={this.header()} collapsedHeader={this.state.collapsedHeader} title={`${vm.name_label}${container ? ` (${container.name_label})` : ''}`}>
{cloneElement(this.props.children, { ...childProps, toggleHeader: this._toggleHeader })}
</Page>
}
}

View File

@@ -7,6 +7,8 @@ import invoke from 'invoke'
import IsoDevice from 'iso-device'
import NoVnc from 'react-novnc'
import React from 'react'
import Tooltip from 'tooltip'
import { Button } from 'react-bootstrap-4/lib'
import { resolveUrl } from 'xo'
import { Container, Row, Col } from 'grid'
import {
@@ -36,11 +38,20 @@ export default class TabConsole extends Component {
_getClipboardContent = () =>
this.refs.clipboard && this.refs.clipboard.value
_toggleMinimalLayout = () => {
this.props.toggleHeader()
this.setState({ minimalLayout: !this.state.minimalLayout })
}
render () {
const {
statsOverview,
vm
} = this.props
const {
minimalLayout,
scale
} = this.state
if (vm.power_state !== 'Running') {
return (
@@ -50,7 +61,7 @@ export default class TabConsole extends Component {
return (
<Container>
{statsOverview && <Row className='text-xs-center'>
{!minimalLayout && statsOverview && <Row className='text-xs-center'>
<Col mediumSize={3}>
<p>
<Icon icon='cpu' size={2} />
@@ -81,10 +92,10 @@ export default class TabConsole extends Component {
</Col>
</Row>}
<Row>
<Col mediumSize={5}>
<Col mediumSize={3}>
<IsoDevice vm={vm} />
</Col>
<Col mediumSize={5}>
<Col mediumSize={3}>
<div className='input-group'>
<input type='text' className='form-control' ref='clipboard' onChange={this._setRemoteClipboard} />
<span className='input-group-btn'>
@@ -104,11 +115,34 @@ export default class TabConsole extends Component {
<Icon icon='vm-keyboard' /> {_('ctrlAltDelButtonLabel')}
</button>
</Col>
<Col mediumSize={3}>
<input
className='form-control'
max={3}
min={0.1}
onChange={this.linkState('scale')}
step={0.1}
type='range'
value={scale}
/>
</Col>
<Col mediumSize={1}>
<Tooltip content={minimalLayout ? _('showHeaderTooltip') : _('hideHeaderTooltip')}>
<Button bsStyle='secondary' onClick={this._toggleMinimalLayout}>
<Icon icon={minimalLayout ? 'caret' : 'caret-up'} />
</Button>
</Tooltip>
</Col>
</Row>
<Row className='console'>
<Col>
<NoVnc ref='noVnc' url={resolveUrl(`consoles/${vm.id}`)} onClipboardChange={this._getRemoteClipboard} />
<p><em><Icon icon='info' /> {_('tipLabel')} {_('tipConsoleLabel')}</em></p>
<NoVnc
onClipboardChange={this._getRemoteClipboard}
ref='noVnc'
scale={scale}
url={resolveUrl(`consoles/${vm.id}`)}
/>
{!minimalLayout && <p><em><Icon icon='info' /> {_('tipLabel')} {_('tipConsoleLabel')}</em></p>}
</Col>
</Row>
</Container>

View File

@@ -24,6 +24,7 @@ import { XoSelect, Size, Text } from 'editable'
import {
attachDiskToVm,
createDisk,
connectVbd,
deleteVbd,
deleteVdi,
disconnectVbd,
@@ -145,7 +146,7 @@ class AttachDisk extends Component {
const { vm, vbds, onClose = noop } = this.props
const { vdi } = this.state
const { bootable, readOnly } = this.refs
const _isFreeForWriting = vdi => some(vdi.$VBDs, id => {
const _isFreeForWriting = vdi => vdi.$VBDs.length === 0 || some(vdi.$VBDs, id => {
const vbd = vbds[id]
return !vbd || !vbd.attached || vbd.read_only
})
@@ -444,6 +445,14 @@ export default class TabDisks extends Component {
{_('vbdStatusDisconnected')}
</span>
<ButtonGroup className='pull-xs-right'>
{vm.power_state === 'Running' &&
<ActionRowButton
btnStyle='default'
icon='connect'
handler={connectVbd}
handlerParam={vbd}
/>
}
<ActionRowButton
btnStyle='default'
icon='vdi-forget'

View File

@@ -5,6 +5,7 @@ import isEmpty from 'lodash/isEmpty'
import map from 'lodash/map'
import React, { Component } from 'react'
import TabButton from 'tab-button'
import Tooltip from 'tooltip'
import { connectStore } from 'utils'
import { ButtonGroup } from 'react-bootstrap-4/lib'
import { FormattedRelative, FormattedTime } from 'react-intl'
@@ -14,7 +15,9 @@ import {
createGetObjectsOfType
} from 'selectors'
import {
copyVm,
deleteVm,
exportVm,
editVm,
revertSnapshot,
snapshotVm
@@ -71,18 +74,38 @@ export default class TabSnapshot extends Component {
</td>
<td>
<ButtonGroup>
<ActionRowButton
btnStyle='warning'
handler={revertSnapshot}
handlerParam={snapshot}
icon='snapshot-revert'
/>
<ActionRowButton
btnStyle='danger'
handler={deleteVm}
handlerParam={snapshot}
icon='delete'
/>
<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='vm-export'
/>
</Tooltip>
<Tooltip content={_('revertSnapshot')}>
<ActionRowButton
btnStyle='warning'
handler={revertSnapshot}
handlerParam={snapshot}
icon='snapshot-revert'
/>
</Tooltip>
<Tooltip content={_('deleteSnapshot')}>
<ActionRowButton
btnStyle='danger'
handler={deleteVm}
handlerParam={snapshot}
icon='delete'
/>
</Tooltip>
</ButtonGroup>
</td>
</tr>

View File

@@ -12,7 +12,8 @@ import Tooltip from 'tooltip'
import xoaUpdater, { exposeTrial, isTrialRunning } from 'xoa-updater'
import { confirm } from 'modal'
import { connectStore } from 'utils'
import { Container } from 'grid'
import { Card, CardBlock, CardHeader } from 'card'
import { Container, Row, Col } from 'grid'
import { error } from 'notification'
import { Password } from 'form'
import { serverVersion } from 'xo'
@@ -76,6 +77,7 @@ export default class XoaUpdates extends Component {
const { registration } = this.props
const alreadyRegistered = (registration.state === 'registered')
if (alreadyRegistered) {
try {
await confirm({
@@ -85,6 +87,7 @@ export default class XoaUpdates extends Component {
return
}
}
this.setState({ askRegisterAgain: false })
return xoaUpdater.register(email.value, password.value, alreadyRegistered)
.then(() => { email.value = password.value = '' })
}
@@ -116,6 +119,7 @@ export default class XoaUpdates extends Component {
_trialAvailable = trial => trial.state === 'default' && isTrialRunning(trial.trial)
_trialConsumed = trial => trial.state === 'default' && !isTrialRunning(trial.trial) && !exposeTrial(trial.trial)
_updaterDown = trial => isEmpty(trial) || trial.state === 'ERROR'
_toggleAskRegisterAgain = () => this.setState({ askRegisterAgain: !this.state.askRegisterAgain })
_startTrial = async () => {
try {
@@ -132,6 +136,7 @@ export default class XoaUpdates extends Component {
}
componentWillMount () {
this.setState({ askRegisterAgain: false })
serverVersion.then(serverVersion => {
this.setState({ serverVersion })
})
@@ -154,6 +159,8 @@ export default class XoaUpdates extends Component {
} = this.props
let { configuration } = this.props // Configuration from the store
const alreadyRegistered = (registration.state === 'registered')
configuration = assign({}, configuration)
const {
proxyHost,
@@ -173,141 +180,168 @@ export default class XoaUpdates extends Component {
<p className='text-danger'>{_('noUpdaterWarning')}</p>
</div>
: <div>
<p>{_('currentVersion')} {`xo-server ${this.state.serverVersion}`} / {`xo-web ${pkg.version}`}</p>
<p>
<strong>{states[state]}</strong>
{' '}
<ActionButton
btnStyle='info'
handler={update}
icon='refresh'>
{_('update')}
</ActionButton>
{' '}
<ActionButton
btnStyle='primary'
handler={upgrade}
icon='upgrade'>
{_('upgrade')}
</ActionButton>
</p>
<div>
{map(log, (log, key) => (
<p key={key}>
<span className={textClasses[log.level]} >{log.date}</span>: <span dangerouslySetInnerHTML={{__html: ansiUp.ansi_to_html(log.message)}} />
</p>
))}
</div>
<h2>{_('settings')} {configEdited ? '*' : ''}</h2>
<form className='form-inline'>
<fieldset>
<div className='form-group'>
<input
className='form-control'
placeholder='Host (myproxy.example.org)'
type='text'
value={configuration.proxyHost}
onChange={this._handleProxyHostChange}
/>
</div>
{' '}
<div className='form-group'>
<input
className='form-control'
placeholder='Port (3128 ?...)'
type='text'
value={configuration.proxyPort}
onChange={this._handleProxyPortChange}
/>
</div>
{' '}
<div className='form-group'>
<input
className='form-control'
placeholder='User name'
type='text'
value={configuration.proxyUser}
onChange={this._handleProxyUserChange}
/>
</div>
{' '}
<div className='form-group'>
<Password
placeholder='password'
ref='proxyPassword'
/>
</div>
</fieldset>
<br />
<fieldset>
<ActionButton icon='save' btnStyle='primary' handler={this._configure}>{_('saveResourceSet')}</ActionButton>
{' '}
<button type='button' className='btn btn-default' onClick={this._handleConfigReset} disabled={!configEdited}>{_('resetResourceSet')}</button>
</fieldset>
</form>
<h2>{_('registration')}</h2>
<p>
<strong>{registration.state}</strong>
{registration.email && <span> to {registration.email}</span>}
<span className='text-danger'> {registration.error}</span>
</p>
<form id='registrationForm' className='form-inline'>
<div className='form-group'>
<input
className='form-control'
placeholder='account email'
ref='email'
required
type='text'
/>
</div>
{' '}
<div className='form-group'>
<Password
placeholder='password'
ref='password'
required
/>
</div>
{' '}
<ActionButton form='registrationForm' icon='success' btnStyle='primary' handler={this._register}>{_('register')}</ActionButton>
</form>
{+process.env.XOA_PLAN === 1 &&
<div>
<h2>{_('trial')}</h2>
{this._trialAllowed(trial) &&
<div>
{registration.state !== 'registered' && <p>{_('trialRegistration')}</p>}
{registration.state === 'registered' &&
<ActionButton btnStyle='success' handler={this._startTrial} icon='trial'>{_('trialStartButton')}</ActionButton>
<Row>
<Col mediumSize={12}>
<Card>
<CardHeader>
<UpdateTag /> {states[state]}
</CardHeader>
<CardBlock>
<p>{_('currentVersion')} {`xo-server ${this.state.serverVersion}`} / {`xo-web ${pkg.version}`}</p>
<ActionButton
btnStyle='info'
handler={update}
icon='refresh'>
{_('refresh')}
</ActionButton>
{' '}
<ActionButton
btnStyle='success'
handler={upgrade}
icon='upgrade'>
{_('upgrade')}
</ActionButton>
<hr />
<div>
{map(log, (log, key) => (
<p key={key}>
<span className={textClasses[log.level]} >{log.date}</span>: <span dangerouslySetInnerHTML={{__html: ansiUp.ansi_to_html(log.message)}} />
</p>
))}
</div>
</CardBlock>
</Card>
</Col>
</Row>
<Row>
<Col mediumSize={6}>
<Card>
<CardHeader>
{_('proxySettings')} {configEdited ? '*' : ''}
</CardHeader>
<CardBlock>
<form>
<fieldset>
<div className='form-group'>
<input
className='form-control'
placeholder='Host (myproxy.example.org)'
type='text'
value={configuration.proxyHost}
onChange={this._handleProxyHostChange}
/>
</div>
{' '}
<div className='form-group'>
<input
className='form-control'
placeholder='Port (eg: 3128)'
type='text'
value={configuration.proxyPort}
onChange={this._handleProxyPortChange}
/>
</div>
{' '}
<div className='form-group'>
<input
className='form-control'
placeholder='Username'
type='text'
value={configuration.proxyUser}
onChange={this._handleProxyUserChange}
/>
</div>
{' '}
<div className='form-group'>
<Password
placeholder='Password'
ref='proxyPassword'
/>
</div>
</fieldset>
<br />
<fieldset>
<ActionButton icon='save' btnStyle='primary' handler={this._configure}>{_('saveResourceSet')}</ActionButton>
{' '}
<button type='button' className='btn btn-default' onClick={this._handleConfigReset} disabled={!configEdited}>{_('resetResourceSet')}</button>
</fieldset>
</form>
</CardBlock>
</Card>
</Col>
<Col mediumSize={6}>
<Card>
<CardHeader>
{_('registration')}
</CardHeader>
<CardBlock>
<strong>{registration.state}</strong>
{registration.email && <span> to {registration.email}</span>}
<span className='text-danger'> {registration.error}</span>
{(!alreadyRegistered || this.state.askRegisterAgain)
? <form id='registrationForm'>
<div className='form-group'>
<input
className='form-control'
placeholder='Your email account'
ref='email'
required
type='text'
/>
</div>
{' '}
<div className='form-group'>
<Password
placeholder='Your password'
ref='password'
required
/>
</div>
{' '}
<ActionButton form='registrationForm' icon='success' btnStyle='primary' handler={this._register}>{_('register')}</ActionButton>
</form>
: <ActionButton icon='edit' btnStyle='primary' handler={this._toggleAskRegisterAgain}>{_('editRegistration')}</ActionButton>
}
</div>
}
{this._trialAvailable(trial) &&
<p className='text-success'>{_('trialAvailableUntil', {date: new Date(trial.trial.end)})}</p>
}
{this._trialConsumed(trial) &&
<p>{_('trialConsumed')}</p>
}
</div>
}
{(process.env.XOA_PLAN > 1 && process.env.XOA_PLAN < 5) &&
<div>
{trial.state === 'trustedTrial' &&
<p>{trial.message}</p>
}
{trial.state === 'untrustedTrial' &&
<p className='text-danger'>{trial.message}</p>
}
</div>
}
{process.env.XOA_PLAN < 5 &&
<div>
{this._updaterDown(trial) &&
<p className='text-danger'>{_('trialLocked')}</p>
}
</div>
}
{+process.env.XOA_PLAN === 1 &&
<div>
<h2>{_('trial')}</h2>
{this._trialAllowed(trial) &&
<div>
{registration.state !== 'registered' && <p>{_('trialRegistration')}</p>}
{registration.state === 'registered' &&
<ActionButton btnStyle='success' handler={this._startTrial} icon='trial'>{_('trialStartButton')}</ActionButton>
}
</div>
}
{this._trialAvailable(trial) &&
<p className='text-success'>{_('trialAvailableUntil', {date: new Date(trial.trial.end)})}</p>
}
{this._trialConsumed(trial) &&
<p>{_('trialConsumed')}</p>
}
</div>
}
{(process.env.XOA_PLAN > 1 && process.env.XOA_PLAN < 5) &&
<div>
{trial.state === 'trustedTrial' &&
<p>{trial.message}</p>
}
{trial.state === 'untrustedTrial' &&
<p className='text-danger'>{trial.message}</p>
}
</div>
}
{process.env.XOA_PLAN < 5 &&
<div>
{this._updaterDown(trial) &&
<p className='text-danger'>{_('trialLocked')}</p>
}
</div>
}
</CardBlock>
</Card>
</Col>
</Row>
</div>
}
</Container>