Compare commits
55 Commits
xo-web/v5.
...
v5.1.8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8882df7939 | ||
|
|
185a554cd9 | ||
|
|
230e0dc2a5 | ||
|
|
f5b69fdfdc | ||
|
|
01dc0d8f1e | ||
|
|
8035886a3c | ||
|
|
0ab5f4b13f | ||
|
|
a1bc98def8 | ||
|
|
868cf6140b | ||
|
|
4b3473f480 | ||
|
|
7bc782cc62 | ||
|
|
e625a53e4a | ||
|
|
b31185d96d | ||
|
|
09d75e972f | ||
|
|
f33568951b | ||
|
|
8d8c442be5 | ||
|
|
f890b8ea7a | ||
|
|
1b80b3929c | ||
|
|
4f946293f6 | ||
|
|
36788cde2b | ||
|
|
1547c99e5a | ||
|
|
5c9606dad8 | ||
|
|
fdcb1dccf5 | ||
|
|
12812b8c23 | ||
|
|
0098497255 | ||
|
|
6562d2de7f | ||
|
|
1f0e88cdb0 | ||
|
|
197da91ef3 | ||
|
|
cbd59789e2 | ||
|
|
190ecf3d74 | ||
|
|
15b8f6bca2 | ||
|
|
5b406d731b | ||
|
|
4be9e67ac4 | ||
|
|
d047421685 | ||
|
|
f6f415a421 | ||
|
|
edfaaebac0 | ||
|
|
67df22a1bf | ||
|
|
7dc59a00f6 | ||
|
|
6214fe4c2e | ||
|
|
21610c3e0a | ||
|
|
87550b0189 | ||
|
|
b7c42d0a08 | ||
|
|
c15ad299ac | ||
|
|
48c56cd602 | ||
|
|
7957f621ef | ||
|
|
38ddbfdc9c | ||
|
|
3d2aae81da | ||
|
|
2227b9d061 | ||
|
|
12aab5fa8c | ||
|
|
7323e6e117 | ||
|
|
6f36869609 | ||
|
|
4a12419162 | ||
|
|
bf91938aa6 | ||
|
|
bd70bd2b45 | ||
|
|
bb26c8e449 |
10
package.json
10
package.json
@@ -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",
|
||||
|
||||
@@ -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'])
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -49,6 +49,7 @@ export default class Toggle extends Component {
|
||||
}
|
||||
|
||||
this.refs.input.checked = Boolean(value)
|
||||
this.forceUpdate()
|
||||
}
|
||||
|
||||
_onChange = event => {
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
8
src/common/react-novnc.js
vendored
8
src/common/react-novnc.js
vendored
@@ -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) {
|
||||
|
||||
@@ -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 => (
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 || []}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -91,6 +91,7 @@ export default class Tooltip extends Component {
|
||||
|
||||
_removeListeners () {
|
||||
const node = this._node
|
||||
this._hideTooltip()
|
||||
|
||||
if (!node) {
|
||||
return
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
26
src/common/xo-json-schema-input/xo-tag-input.js
Normal file
26
src/common/xo-json-schema-input/xo-tag-input.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
})
|
||||
|
||||
37
src/common/xo/add-host-modal/index.js
Normal file
37
src/common/xo/add-host-modal/index.js
Normal 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>
|
||||
}
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -676,6 +676,10 @@
|
||||
@extend .fa;
|
||||
@extend .fa-puzzle-piece;
|
||||
}
|
||||
&-logs {
|
||||
@extend .fa;
|
||||
@extend .fa-list;
|
||||
}
|
||||
}
|
||||
&-menu-about {
|
||||
@extend .fa;
|
||||
|
||||
@@ -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 ======================================================================
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const getJobValues = job => job.values || job.items
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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 (_) {}
|
||||
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -114,9 +114,9 @@ export default class XoApp extends Component {
|
||||
{blocked ? <XoaUpdates /> : this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
<TooltipViewer />
|
||||
<Modal />
|
||||
<Notification />
|
||||
<TooltipViewer />
|
||||
</div>
|
||||
</IntlProvider>
|
||||
}
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
<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>
|
||||
|
||||
<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'
|
||||
/>
|
||||
{' '}
|
||||
|
||||
<span>{_('newVmCustomConfig')}</span>
|
||||
{' '}
|
||||
|
||||
<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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'>
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
4
src/xo-app/settings/logs/index.css
Normal file
4
src/xo-app/settings/logs/index.css
Normal file
@@ -0,0 +1,4 @@
|
||||
.message {
|
||||
overflow-wrap: break-word;
|
||||
max-width: 20em;
|
||||
}
|
||||
93
src/xo-app/settings/logs/index.js
Normal file
93
src/xo-app/settings/logs/index.js
Normal 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>
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user