Compare commits
31 Commits
v5.15.0
...
computed-d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
595c4bd5a8 | ||
|
|
1a2f553094 | ||
|
|
4d69866532 | ||
|
|
495c97b44b | ||
|
|
e817b3254e | ||
|
|
dd6987efe9 | ||
|
|
d7f8d12d88 | ||
|
|
504895a730 | ||
|
|
cde92836f3 | ||
|
|
c787988b06 | ||
|
|
898434b267 | ||
|
|
6e44c65a07 | ||
|
|
03028bca50 | ||
|
|
c8669dc88f | ||
|
|
82240979c2 | ||
|
|
db5d495105 | ||
|
|
6e8dfe8833 | ||
|
|
242d9e20c4 | ||
|
|
e446eb0cd0 | ||
|
|
b63efe579a | ||
|
|
f3410f1491 | ||
|
|
b27ac11d56 | ||
|
|
a55d73614e | ||
|
|
25cd1957c7 | ||
|
|
abd97abc24 | ||
|
|
6ddfd909f0 | ||
|
|
e054eec555 | ||
|
|
e253657770 | ||
|
|
102e629e16 | ||
|
|
242a02836c | ||
|
|
6936f223f3 |
@@ -131,9 +131,7 @@ const dest = lazyFn(function () {
|
||||
}
|
||||
|
||||
const opts = {
|
||||
sourcemaps: {
|
||||
path: '.',
|
||||
},
|
||||
sourcemaps: '.',
|
||||
}
|
||||
|
||||
return PRODUCTION
|
||||
@@ -238,7 +236,7 @@ function browserify (path, opts) {
|
||||
|
||||
gulp.task(function buildPages () {
|
||||
return pipe(
|
||||
src('index.pug', { sourcemaps: true }),
|
||||
src('index.pug'),
|
||||
require('gulp-pug')(),
|
||||
DEVELOPMENT &&
|
||||
require('gulp-embedlr')({
|
||||
@@ -254,9 +252,10 @@ gulp.task(function buildScripts () {
|
||||
plugins: [
|
||||
// ['css-modulesify', {
|
||||
[
|
||||
'modular-css/browserify',
|
||||
'modular-cssify',
|
||||
{
|
||||
css: DIST_DIR + '/modules.css',
|
||||
from: undefined,
|
||||
},
|
||||
],
|
||||
],
|
||||
|
||||
58
package.json
58
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "xo-web",
|
||||
"version": "5.15.0",
|
||||
"version": "5.15.1",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Web interface client for Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -34,7 +34,8 @@
|
||||
"@nraynaud/novnc": "0.6.1",
|
||||
"ansi_up": "^2.0.2",
|
||||
"asap": "^2.0.6",
|
||||
"babel-eslint": "^8.0.3",
|
||||
"babel-core": "^6.26.0",
|
||||
"babel-eslint": "^8.1.2",
|
||||
"babel-plugin-dev": "^1.0.0",
|
||||
"babel-plugin-lodash": "^3.2.11",
|
||||
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
||||
@@ -43,15 +44,15 @@
|
||||
"babel-plugin-transform-react-jsx-self": "^6.11.0",
|
||||
"babel-plugin-transform-react-jsx-source": "^6.9.0",
|
||||
"babel-plugin-transform-runtime": "^6.6.0",
|
||||
"babel-preset-es2015": "^6.6.0",
|
||||
"babel-preset-env": "^1.6.1",
|
||||
"babel-preset-react": "^6.5.0",
|
||||
"babel-preset-stage-0": "^6.5.0",
|
||||
"babel-preset-stage-0": "^6.24.1",
|
||||
"babel-register": "^6.26.0",
|
||||
"babel-runtime": "^6.26.0",
|
||||
"babelify": "^8.0.0",
|
||||
"benchmark": "^2.1.0",
|
||||
"bootstrap": "4.0.0-alpha.5",
|
||||
"browserify": "^14.5.0",
|
||||
"browserify": "^15.1.0",
|
||||
"bundle-collapser": "^1.3.0",
|
||||
"chartist": "^0.10.1",
|
||||
"chartist-plugin-legend": "^0.6.1",
|
||||
@@ -59,12 +60,12 @@
|
||||
"classnames": "^2.2.3",
|
||||
"complex-matcher": "^0.1.1",
|
||||
"cookies-js": "^1.2.2",
|
||||
"d3": "^4.12.0",
|
||||
"d3": "^4.12.2",
|
||||
"dependency-check": "^2.9.2",
|
||||
"enzyme": "^3.1.1",
|
||||
"enzyme": "^3.3.0",
|
||||
"enzyme-adapter-react-15": "^1.0.5",
|
||||
"enzyme-to-json": "^3.3.0",
|
||||
"eslint": "^4.13.1",
|
||||
"eslint": "^4.14.0",
|
||||
"eslint-config-standard": "^10.2.1",
|
||||
"eslint-config-standard-jsx": "^4.0.2",
|
||||
"eslint-plugin-import": "^2.8.0",
|
||||
@@ -77,33 +78,33 @@
|
||||
"font-mfizz": "^2.4.1",
|
||||
"get-stream": "^3.0.0",
|
||||
"globby": "^7.1.1",
|
||||
"gulp": "github:gulpjs/gulp#4.0",
|
||||
"gulp-autoprefixer": "^4.0.0",
|
||||
"gulp": "^4.0.0",
|
||||
"gulp-autoprefixer": "^4.1.0",
|
||||
"gulp-csso": "^3.0.0",
|
||||
"gulp-embedlr": "^0.5.2",
|
||||
"gulp-plumber": "^1.1.0",
|
||||
"gulp-pug": "^3.1.0",
|
||||
"gulp-refresh": "^1.1.0",
|
||||
"gulp-sass": "^3.0.0",
|
||||
"gulp-sourcemaps": "^2.2.3",
|
||||
"gulp-sourcemaps": "^2.6.2",
|
||||
"gulp-uglify": "^3.0.0",
|
||||
"gulp-watch": "^4.3.5",
|
||||
"human-format": "^0.9.2",
|
||||
"gulp-watch": "^5.0.0",
|
||||
"human-format": "^0.10.0",
|
||||
"husky": "^0.14.3",
|
||||
"immutable": "^3.8.2",
|
||||
"index-modules": "^0.3.0",
|
||||
"is-ip": "^2.0.0",
|
||||
"jest": "^22.0.0",
|
||||
"jest": "^22.0.4",
|
||||
"jsonrpc-websocket-client": "^0.2.0",
|
||||
"kindof": "^2.0.0",
|
||||
"later": "^1.2.0",
|
||||
"lint-staged": "^6.0.0",
|
||||
"lodash": "^4.6.1",
|
||||
"loose-envify": "^1.1.0",
|
||||
"make-error": "^1.2.1",
|
||||
"marked": "^0.3.7",
|
||||
"modular-css": "^7.2.0",
|
||||
"moment": "^2.20.0",
|
||||
"make-error": "^1.3.2",
|
||||
"marked": "^0.3.9",
|
||||
"modular-cssify": "^7.2.0",
|
||||
"moment": "^2.20.1",
|
||||
"moment-timezone": "^0.5.14",
|
||||
"notifyjs": "^3.0.0",
|
||||
"prettier": "^1.9.2",
|
||||
@@ -134,16 +135,13 @@
|
||||
"react-virtualized": "^8.0.8",
|
||||
"readable-stream": "^2.3.3",
|
||||
"redux": "^3.7.2",
|
||||
"redux-devtools": "^3.4.1",
|
||||
"redux-devtools-dock-monitor": "^1.1.0",
|
||||
"redux-devtools-log-monitor": "^1.4.0",
|
||||
"redux-thunk": "^2.0.1",
|
||||
"reselect": "^2.5.4",
|
||||
"semver": "^5.4.1",
|
||||
"styled-components": "^2.3.0",
|
||||
"styled-components": "^2.4.0",
|
||||
"tar-stream": "^1.5.5",
|
||||
"uglify-es": "^3.2.2",
|
||||
"uncontrollable-input": "^0.0.1",
|
||||
"uglify-es": "^3.3.4",
|
||||
"uncontrollable-input": "^0.1.1",
|
||||
"url-parse": "^1.2.0",
|
||||
"vinyl": "^2.1.0",
|
||||
"watchify": "^3.7.0",
|
||||
@@ -158,12 +156,15 @@
|
||||
"benchmarks": "./tools/run-benchmarks.js 'src/**/*.bench.js'",
|
||||
"build": "npm run build-indexes && NODE_ENV=production gulp build",
|
||||
"build-indexes": "index-modules --auto src",
|
||||
"clean": "gulp clean",
|
||||
"dev": "npm run build-indexes && NODE_ENV=development gulp build",
|
||||
"dev-test": "jest --watch",
|
||||
"lint-staged-stash": "touch .lint-staged && git stash save --include-untracked --keep-index && true",
|
||||
"lint-staged-unstash": "git stash pop && rm -f .lint-staged && true",
|
||||
"posttest": "eslint --ignore-path .gitignore src/",
|
||||
"prebuild": "npm run clean",
|
||||
"precommit": "lint-staged",
|
||||
"predev": "npm run clean",
|
||||
"prepublishOnly": "npm run build",
|
||||
"test": "jest"
|
||||
},
|
||||
@@ -195,7 +196,14 @@
|
||||
"transform-runtime"
|
||||
],
|
||||
"presets": [
|
||||
"es2015",
|
||||
[
|
||||
"env",
|
||||
{
|
||||
"targets": {
|
||||
"browsers": ">2%"
|
||||
}
|
||||
}
|
||||
],
|
||||
"react",
|
||||
"stage-0"
|
||||
]
|
||||
|
||||
29
src/common/add-subscriptions.js
Normal file
29
src/common/add-subscriptions.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
|
||||
const call = fn => fn()
|
||||
|
||||
// `subscriptions` can be a function if we want to ensure that the subscription
|
||||
// callbacks have been correctly initialized when there are circular dependencies
|
||||
const addSubscriptions = subscriptions => Component =>
|
||||
class SubscriptionWrapper extends React.PureComponent {
|
||||
_unsubscribes = null
|
||||
|
||||
componentWillMount () {
|
||||
this._unsubscribes = map(
|
||||
typeof subscriptions === 'function' ? subscriptions(this.props) : subscriptions,
|
||||
(subscribe, prop) =>
|
||||
subscribe(value => this.setState({ [prop]: value }))
|
||||
)
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this._unsubscribes.forEach(call)
|
||||
this._unsubscribes = null
|
||||
}
|
||||
|
||||
render () {
|
||||
return <Component {...this.props} {...this.state} />
|
||||
}
|
||||
}
|
||||
export { addSubscriptions as default }
|
||||
126
src/common/computed.js
Normal file
126
src/common/computed.js
Normal file
@@ -0,0 +1,126 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
|
||||
const {
|
||||
create,
|
||||
defineProperty,
|
||||
defineProperties,
|
||||
getOwnPropertyDescriptors = obj => {
|
||||
const descriptors = {}
|
||||
const { getOwnPropertyDescriptor } = Object
|
||||
for (const prop in obj) {
|
||||
const descriptor = getOwnPropertyDescriptor(obj, prop)
|
||||
if (descriptor !== undefined) {
|
||||
descriptors[prop] = descriptor
|
||||
}
|
||||
}
|
||||
return descriptors
|
||||
},
|
||||
prototype: { hasOwnProperty },
|
||||
} = Object
|
||||
|
||||
const makePropsSpy =
|
||||
typeof Proxy !== 'undefined'
|
||||
? (obj, spy) =>
|
||||
new Proxy(obj, {
|
||||
get: (target, property) => (spy[property] = target[property]),
|
||||
})
|
||||
: (obj, spy) => {
|
||||
const descriptors = {}
|
||||
const props = getOwnPropertyDescriptors(obj)
|
||||
for (const prop in props) {
|
||||
const { configurable, enumerable, get, value } = props[prop]
|
||||
descriptors[prop] = {
|
||||
configurable,
|
||||
enumerable,
|
||||
get:
|
||||
get !== undefined
|
||||
? () => (spy[prop] = get.call(obj))
|
||||
: () => (spy[prop] = value),
|
||||
}
|
||||
}
|
||||
return create(null, descriptors)
|
||||
}
|
||||
|
||||
// Decorator which provides computed properties for React components.
|
||||
//
|
||||
// ```js
|
||||
// const MyComponent = computed({
|
||||
// fullName: ({ firstName, lastName }) => `${lastName}, ${firstName}`
|
||||
// })(({ fullName }) =>
|
||||
// <p>{fullName}</p>
|
||||
// )
|
||||
// ```
|
||||
const computed = computed => Component =>
|
||||
class extends PureComponent {
|
||||
constructor () {
|
||||
super()
|
||||
|
||||
this._computedCache = create(null)
|
||||
this._computedDeps = create(null)
|
||||
|
||||
const descriptors = (this._descriptors = {})
|
||||
for (const name in computed) {
|
||||
if (!hasOwnProperty.call(computed, name)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const transform = computed[name]
|
||||
let running = false
|
||||
descriptors[name] = {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get: () => {
|
||||
// this is necessary to allow a computed value to depend on
|
||||
// itself
|
||||
if (running) {
|
||||
console.log(name, 'running')
|
||||
return this.props[name]
|
||||
}
|
||||
|
||||
const cache = this._computedCache
|
||||
const deps = this._computedDeps
|
||||
|
||||
const dependencies = deps[name]
|
||||
let needsRecompute = dependencies === undefined
|
||||
if (!needsRecompute) {
|
||||
const { props } = this
|
||||
for (const depName in dependencies) {
|
||||
const value =
|
||||
depName === name || !(depName in cache)
|
||||
? props[depName]
|
||||
: cache[depName]
|
||||
needsRecompute = value !== dependencies[depName]
|
||||
if (needsRecompute) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(name, needsRecompute)
|
||||
|
||||
if (needsRecompute) {
|
||||
running = true
|
||||
cache[name] = transform(
|
||||
makePropsSpy(this._props, (deps[name] = create(null)))
|
||||
)
|
||||
running = false
|
||||
}
|
||||
|
||||
const value = cache[name]
|
||||
defineProperty(this._props, name, {
|
||||
enumerable: true,
|
||||
value,
|
||||
})
|
||||
return value
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
this._props = defineProperties({ ...this.props }, this._descriptors)
|
||||
|
||||
return <Component {...this._props} />
|
||||
}
|
||||
}
|
||||
export { computed as default }
|
||||
@@ -160,9 +160,9 @@ class Editable extends Component {
|
||||
)}
|
||||
>
|
||||
<span
|
||||
onClick={!useLongClick && this._openEdition}
|
||||
onMouseDown={useLongClick && this.__startTimer}
|
||||
onMouseUp={useLongClick && this.__stopTimer}
|
||||
onClick={useLongClick ? undefined : this._openEdition}
|
||||
onMouseDown={useLongClick ? this.__startTimer : undefined}
|
||||
onMouseUp={useLongClick ? this.__stopTimer : undefined}
|
||||
>
|
||||
{this._renderDisplay()}
|
||||
</span>
|
||||
|
||||
@@ -101,7 +101,7 @@ export default class SelectPlainObject extends Component {
|
||||
|
||||
return (
|
||||
<Select
|
||||
autofocus={props.autoFocus}
|
||||
autoFocus={props.autoFocus}
|
||||
disabled={props.disabled}
|
||||
multi={props.multi}
|
||||
onChange={this._handleChange}
|
||||
|
||||
@@ -114,8 +114,8 @@ export default class Select extends Component {
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
onClick={!disabled && (() => selectValue(option))}
|
||||
onMouseOver={!disabled && (() => focusOption(option))}
|
||||
onClick={disabled ? undefined : () => selectValue(option)}
|
||||
onMouseOver={disabled ? undefined : () => focusOption(option)}
|
||||
style={style}
|
||||
key={key}
|
||||
>
|
||||
|
||||
@@ -9,12 +9,12 @@ import propTypes from '../prop-types-decorator'
|
||||
@uncontrollableInput()
|
||||
@propTypes({
|
||||
className: propTypes.string,
|
||||
onChange: propTypes.func,
|
||||
onChange: propTypes.func.isRequired,
|
||||
icon: propTypes.string,
|
||||
iconOn: propTypes.string,
|
||||
iconOff: propTypes.string,
|
||||
iconSize: propTypes.number,
|
||||
value: propTypes.bool,
|
||||
value: propTypes.bool.isRequired,
|
||||
})
|
||||
export default class Toggle extends Component {
|
||||
static defaultProps = {
|
||||
|
||||
@@ -868,6 +868,7 @@ const messages = {
|
||||
vmChooseCoresPerSocket: 'Default behavior',
|
||||
vmCoresPerSocket:
|
||||
'{nSockets, number} socket{nSockets, plural, one {} other {s}} with {nCores, number} core{nCores, plural, one {} other {s}} per socket',
|
||||
vmCoresPerSocketNone: 'None',
|
||||
vmCoresPerSocketIncorrectValue: 'Incorrect cores per socket value',
|
||||
vmCoresPerSocketIncorrectValueSolution:
|
||||
'Please change the selected value to fix it.',
|
||||
@@ -1166,6 +1167,9 @@ const messages = {
|
||||
restartVmModalMessage: 'Are you sure you want to restart {name}?',
|
||||
stopVmModalTitle: 'Stop VM',
|
||||
stopVmModalMessage: 'Are you sure you want to stop {name}?',
|
||||
suspendVmsModalTitle: 'Suspend VM{vms, plural, one {} other {s}}',
|
||||
suspendVmsModalMessage:
|
||||
'Are you sure you want to suspend {vms, number} VM{vms, plural, one {} other {s}}?',
|
||||
restartVmsModalTitle: 'Restart VM{vms, plural, one {} other {s}}',
|
||||
restartVmsModalMessage:
|
||||
'Are you sure you want to restart {vms, number} VM{vms, plural, one {} other {s}}?',
|
||||
@@ -1437,6 +1441,7 @@ const messages = {
|
||||
sshKeys: 'SSH keys',
|
||||
newSshKey: 'New SSH key',
|
||||
deleteSshKey: 'Delete',
|
||||
deleteSshKeys: 'Delete selected SSH keys',
|
||||
noSshKeys: 'No SSH keys',
|
||||
newSshKeyModalTitle: 'New SSH key',
|
||||
sshKeyErrorTitle: 'Invalid key',
|
||||
@@ -1446,6 +1451,9 @@ const messages = {
|
||||
deleteSshKeyConfirm: 'Delete SSH key',
|
||||
deleteSshKeyConfirmMessage:
|
||||
'Are you sure you want to delete the SSH key {title}?',
|
||||
deleteSshKeysConfirm: 'Delete SSH key{nKeys, plural, one {} other {s}}',
|
||||
deleteSshKeysConfirmMessage:
|
||||
'Are you sure you want to delete {nKeys, number} SSH key{nKeys, plural, one {} other {s}}?',
|
||||
|
||||
// ----- Usage -----
|
||||
others: 'Others',
|
||||
@@ -1462,6 +1470,10 @@ const messages = {
|
||||
logNoStackTrace: 'No stack trace',
|
||||
logNoParams: 'No params',
|
||||
logDelete: 'Delete log',
|
||||
logsDelete: 'Delete logs',
|
||||
logDeleteMultiple: 'Delete log{nLogs, plural, one {} other {s}}',
|
||||
logDeleteMultipleMessage:
|
||||
'Are you sure you want to delete {nLogs, number} log{nLogs, plural, one {} other {s}}?',
|
||||
logDeleteAll: 'Delete all logs',
|
||||
logDeleteAllTitle: 'Delete all logs',
|
||||
logDeleteAllMessage: 'Are you sure you want to delete all the logs?',
|
||||
@@ -1687,7 +1699,7 @@ const messages = {
|
||||
xosanNoLicense: 'No license.',
|
||||
xosanUnlockNow: 'Unlock now!',
|
||||
xosanBetaOverMessage:
|
||||
'XOSAN Beta is over. You may now delete and create this storage again to be able to manage it.',
|
||||
'XOSAN Beta is over. You may now delete and recreate previous existing XOSAN SRs.',
|
||||
selectLicense: 'Select a license',
|
||||
bindLicense: 'Bind license',
|
||||
expiresOn: 'expires on {date}',
|
||||
|
||||
@@ -13,19 +13,29 @@ import propTypes from './prop-types-decorator'
|
||||
// {children}
|
||||
// </NoObjects>
|
||||
// ````
|
||||
const NoObjects = ({ children, collection, emptyMessage }) =>
|
||||
collection == null ? (
|
||||
<img src='assets/loading.svg' alt='loading' />
|
||||
) : isEmpty(collection) ? (
|
||||
<p>{emptyMessage}</p>
|
||||
const NoObjects = props => {
|
||||
const { collection } = props
|
||||
|
||||
if (collection == null) {
|
||||
return <img src='assets/loading.svg' alt='loading' />
|
||||
}
|
||||
|
||||
if (isEmpty(collection)) {
|
||||
return <p>{props.emptyMessage}</p>
|
||||
}
|
||||
|
||||
const { children, component: Component, ...otherProps } = props
|
||||
return children !== undefined ? (
|
||||
children(otherProps)
|
||||
) : (
|
||||
<div>{children}</div>
|
||||
<Component {...otherProps} />
|
||||
)
|
||||
}
|
||||
|
||||
propTypes(NoObjects)({
|
||||
children: propTypes.node.isRequired,
|
||||
collection: propTypes.oneOfType([propTypes.array, propTypes.object])
|
||||
.isRequired,
|
||||
children: propTypes.func,
|
||||
collection: propTypes.oneOfType([propTypes.array, propTypes.object]),
|
||||
component: propTypes.func,
|
||||
emptyMessage: propTypes.node.isRequired,
|
||||
})
|
||||
export default NoObjects
|
||||
|
||||
125
src/common/pagination.js
Normal file
125
src/common/pagination.js
Normal file
@@ -0,0 +1,125 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
const PageItem = ({ active, children, disabled, onClick, value }) =>
|
||||
active ? (
|
||||
<li className='active page-item'>
|
||||
<span className='page-link'>{children}</span>
|
||||
</li>
|
||||
) : disabled ? (
|
||||
<li className='disabled page-item'>
|
||||
<span className='page-link'>{children}</span>
|
||||
</li>
|
||||
) : (
|
||||
<li className='page-item'>
|
||||
<a className='page-link' href='#' onClick={onClick} data-value={value}>
|
||||
{children}
|
||||
</a>
|
||||
</li>
|
||||
)
|
||||
|
||||
export default class Pagination extends React.PureComponent {
|
||||
static defaultProps = {
|
||||
ellipsis: true,
|
||||
maxButtons: 7,
|
||||
next: true,
|
||||
prev: true,
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
ariaLabel: PropTypes.string,
|
||||
ellipsis: PropTypes.bool,
|
||||
maxButtons: PropTypes.number,
|
||||
next: PropTypes.bool,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
pages: PropTypes.number.isRequired,
|
||||
prev: PropTypes.bool,
|
||||
value: PropTypes.number.isRequired,
|
||||
}
|
||||
|
||||
_onClick (event) {
|
||||
event.preventDefault()
|
||||
this.props.onChange(+event.currentTarget.dataset.value)
|
||||
}
|
||||
_onClick = this._onClick.bind(this)
|
||||
|
||||
render () {
|
||||
const {
|
||||
ariaLabel,
|
||||
ellipsis,
|
||||
maxButtons,
|
||||
next,
|
||||
pages,
|
||||
prev,
|
||||
value,
|
||||
} = this.props
|
||||
const onClick = this._onClick
|
||||
|
||||
let min, max
|
||||
if (pages <= maxButtons) {
|
||||
min = 1
|
||||
max = pages
|
||||
} else {
|
||||
min = Math.max(
|
||||
1,
|
||||
Math.min(value - Math.floor(maxButtons / 2), pages - maxButtons + 1)
|
||||
)
|
||||
max = min + maxButtons - 1
|
||||
}
|
||||
|
||||
const pageButtons = []
|
||||
if (ellipsis && min !== 1) {
|
||||
pageButtons.push(
|
||||
<PageItem disabled key='firstEllipsis'>
|
||||
…
|
||||
</PageItem>
|
||||
)
|
||||
}
|
||||
for (let page = min; page <= max; ++page) {
|
||||
pageButtons.push(
|
||||
<PageItem
|
||||
active={page === value}
|
||||
key={page}
|
||||
onClick={onClick}
|
||||
value={page}
|
||||
>
|
||||
{page}
|
||||
</PageItem>
|
||||
)
|
||||
}
|
||||
if (ellipsis && max !== pages) {
|
||||
pageButtons.push(
|
||||
<PageItem disabled key='lastEllipsis'>
|
||||
…
|
||||
</PageItem>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<nav aria-label={ariaLabel}>
|
||||
<ul className='pagination'>
|
||||
{prev && (
|
||||
<PageItem
|
||||
aria-label='Previous'
|
||||
disabled={value === 1}
|
||||
onClick={onClick}
|
||||
value={value - 1}
|
||||
>
|
||||
‹
|
||||
</PageItem>
|
||||
)}
|
||||
{pageButtons}
|
||||
{next && (
|
||||
<PageItem
|
||||
aria-label='Next'
|
||||
disabled={value === pages}
|
||||
onClick={onClick}
|
||||
value={value + 1}
|
||||
>
|
||||
›
|
||||
</PageItem>
|
||||
)}
|
||||
</ul>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,18 @@
|
||||
import assign from 'lodash/assign'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
// Deprecated because :
|
||||
// - unnecessary
|
||||
// - not standard in the React ecosystem
|
||||
if (__DEV__) {
|
||||
console.warn(`DEPRECATED: use prop-types directly:
|
||||
class MyComponent extends React.Component {
|
||||
static propTypes = {
|
||||
foo: PropTypes.string.isRequired
|
||||
}
|
||||
}`)
|
||||
}
|
||||
|
||||
// Decorators to help declaring properties and context types on React
|
||||
// components without using the tedious static properties syntax.
|
||||
//
|
||||
|
||||
@@ -248,7 +248,7 @@ export class GenericSelect extends Component {
|
||||
const select = (
|
||||
<Select
|
||||
{...{
|
||||
autofocus: autoFocus,
|
||||
autoFocus,
|
||||
clearable,
|
||||
disabled,
|
||||
multi,
|
||||
|
||||
@@ -8,7 +8,7 @@ import Shortcuts from 'shortcuts'
|
||||
import { Portal } from 'react-overlays'
|
||||
import { routerShape } from 'react-router/lib/PropTypes'
|
||||
import { Set } from 'immutable'
|
||||
import { Dropdown, MenuItem, Pagination } from 'react-bootstrap-4/lib'
|
||||
import { Dropdown, MenuItem } from 'react-bootstrap-4/lib'
|
||||
import {
|
||||
ceil,
|
||||
filter,
|
||||
@@ -25,6 +25,7 @@ import ButtonGroup from '../button-group'
|
||||
import Component from '../base-component'
|
||||
import defined, { get } from '../xo-defined'
|
||||
import Icon from '../icon'
|
||||
import Pagination from '../pagination'
|
||||
import propTypes from '../prop-types-decorator'
|
||||
import SingleLineRow from '../single-line-row'
|
||||
import Tooltip from '../tooltip'
|
||||
@@ -517,8 +518,9 @@ export default class SortedTable extends Component {
|
||||
return this._setPage(1)
|
||||
}
|
||||
|
||||
if (page * itemsPerPage > n) {
|
||||
return this._setPage(ceil(n / itemsPerPage))
|
||||
const last = ceil(n / itemsPerPage)
|
||||
if (page > last) {
|
||||
return this._setPage(last)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -526,8 +528,7 @@ export default class SortedTable extends Component {
|
||||
this._saveUrlState(this.state.filter, page)
|
||||
this.setState({ page })
|
||||
}
|
||||
|
||||
_onPageSelection = (_, event) => this._setPage(event.eventKey)
|
||||
_setPage = this._setPage.bind(this)
|
||||
|
||||
_selectAllVisibleItems = event => {
|
||||
this.setState({
|
||||
@@ -709,14 +710,9 @@ export default class SortedTable extends Component {
|
||||
|
||||
const paginationInstance = displayPagination && (
|
||||
<Pagination
|
||||
prev
|
||||
next
|
||||
ellipsis
|
||||
boundaryLinks
|
||||
maxButtons={7}
|
||||
items={ceil(nItems / itemsPerPage)}
|
||||
activePage={state.page}
|
||||
onSelect={this._onPageSelection}
|
||||
pages={ceil(nItems / itemsPerPage)}
|
||||
onChange={this._setPage}
|
||||
value={state.page}
|
||||
/>
|
||||
)
|
||||
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import DockMonitor from 'redux-devtools-dock-monitor'
|
||||
import LogMonitor from 'redux-devtools-log-monitor'
|
||||
import React from 'react'
|
||||
import { createDevTools } from 'redux-devtools'
|
||||
|
||||
export default createDevTools(
|
||||
<DockMonitor changePositionKey='ctrl-q' toggleVisibilityKey='ctrl-h'>
|
||||
<LogMonitor />
|
||||
</DockMonitor>
|
||||
)
|
||||
@@ -1 +0,0 @@
|
||||
module.exports = false // process.env.NODE_ENV !== 'production' && require('./dev-tools.dev')
|
||||
@@ -1,20 +1,13 @@
|
||||
import reduxThunk from 'redux-thunk'
|
||||
import { applyMiddleware, combineReducers, compose, createStore } from 'redux'
|
||||
import { applyMiddleware, combineReducers, createStore } from 'redux'
|
||||
|
||||
import { connectStore as connectXo } from '../xo'
|
||||
|
||||
import DevTools from './dev-tools'
|
||||
import reducer from './reducer'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const enhancers = [applyMiddleware(reduxThunk)]
|
||||
DevTools && enhancers.push(DevTools.instrument())
|
||||
|
||||
const store = createStore(
|
||||
combineReducers(reducer),
|
||||
compose.apply(null, enhancers)
|
||||
)
|
||||
const store = createStore(combineReducers(reducer), applyMiddleware(reduxThunk))
|
||||
|
||||
connectXo(store)
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
|
||||
import _ from './intl'
|
||||
import * as actions from './store/actions'
|
||||
import BaseComponent from './base-component'
|
||||
import invoke from './invoke'
|
||||
import store from './store'
|
||||
import { getObject } from './selectors'
|
||||
@@ -35,6 +34,10 @@ export const EMPTY_OBJECT = Object.freeze({})
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export addSubscriptions from './add-subscriptions'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const ensureArray = value => {
|
||||
if (value === undefined) {
|
||||
return []
|
||||
@@ -57,75 +60,6 @@ export const propsEqual = (o1, o2, props) => {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// `subscriptions` can be a function if we want to ensure that the subscription
|
||||
// callbacks have been correctly initialized when there are circular dependencies
|
||||
export const addSubscriptions = subscriptions => Component => {
|
||||
class SubscriptionWrapper extends BaseComponent {
|
||||
constructor () {
|
||||
super()
|
||||
|
||||
this._unsubscribes = null
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this._unsubscribes = map(
|
||||
isFunction(subscriptions) ? subscriptions(this.props) : subscriptions,
|
||||
(subscribe, prop) =>
|
||||
subscribe(value => this._setState({ [prop]: value }))
|
||||
)
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this._setState = this.setState
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
forEach(this._unsubscribes, unsubscribe => unsubscribe())
|
||||
this._unsubscribes = null
|
||||
delete this._setState
|
||||
}
|
||||
|
||||
_setState (nextState) {
|
||||
this.state = { ...this.state, nextState }
|
||||
}
|
||||
|
||||
render () {
|
||||
return <Component {...this.props} {...this.state} />
|
||||
}
|
||||
}
|
||||
|
||||
return SubscriptionWrapper
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export const checkPropsState = (propsNames, stateNames) => Component => {
|
||||
const nProps = propsNames && propsNames.length
|
||||
const nState = stateNames && stateNames.length
|
||||
|
||||
Component.prototype.shouldComponentUpdate = (newProps, newState) => {
|
||||
const { props, state } = this
|
||||
|
||||
for (let i = 0; i < nProps; ++i) {
|
||||
const name = propsNames[i]
|
||||
if (newProps[name] !== props[name]) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < nState; ++i) {
|
||||
const name = stateNames[i]
|
||||
if (newState[name] !== state[name]) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Component
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const _normalizeMapStateToProps = mapper => {
|
||||
if (isFunction(mapper)) {
|
||||
const factoryOrMapper = (state, props) => {
|
||||
|
||||
@@ -173,7 +173,7 @@ export const CpuLineChart = injectIntl(
|
||||
export const PoolCpuLineChart = injectIntl(
|
||||
propTypes({
|
||||
addSumSeries: propTypes.bool,
|
||||
data: propTypes.object.isRequired,
|
||||
data: propTypes.array.isRequired,
|
||||
options: propTypes.object,
|
||||
})(({ addSumSeries, data, options = {}, intl }) => {
|
||||
const firstHostData = data[0]
|
||||
@@ -261,7 +261,7 @@ export const MemoryLineChart = injectIntl(
|
||||
export const PoolMemoryLineChart = injectIntl(
|
||||
propTypes({
|
||||
addSumSeries: propTypes.bool,
|
||||
data: propTypes.object.isRequired,
|
||||
data: propTypes.array.isRequired,
|
||||
options: propTypes.object,
|
||||
})(({ addSumSeries, data, options = {}, intl }) => {
|
||||
const firstHostData = data[0]
|
||||
@@ -384,7 +384,7 @@ export const VifLineChart = injectIntl(
|
||||
export const PifLineChart = injectIntl(
|
||||
propTypes({
|
||||
addSumSeries: propTypes.bool,
|
||||
data: propTypes.object.isRequired,
|
||||
data: propTypes.array.isRequired,
|
||||
options: propTypes.object,
|
||||
})(({ addSumSeries, data, options = {}, intl }) => {
|
||||
const stats = data.stats.pifs
|
||||
@@ -419,7 +419,7 @@ const ios = ['rx', 'tx']
|
||||
export const PoolPifLineChart = injectIntl(
|
||||
propTypes({
|
||||
addSumSeries: propTypes.bool,
|
||||
data: propTypes.object.isRequired,
|
||||
data: propTypes.array.isRequired,
|
||||
options: propTypes.object,
|
||||
})(({ addSumSeries, data, options = {}, intl }) => {
|
||||
const firstHostData = data[0]
|
||||
@@ -508,7 +508,7 @@ export const LoadLineChart = injectIntl(
|
||||
export const PoolLoadLineChart = injectIntl(
|
||||
propTypes({
|
||||
addSumSeries: propTypes.bool,
|
||||
data: propTypes.object.isRequired,
|
||||
data: propTypes.array.isRequired,
|
||||
options: propTypes.object,
|
||||
})(({ addSumSeries, data, options = {}, intl }) => {
|
||||
const firstHostData = data[0]
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
import Collapse from 'collapse'
|
||||
import Component from 'base-component'
|
||||
import React from 'react'
|
||||
import { every, forEach, map } from 'lodash'
|
||||
import { map } from 'lodash'
|
||||
|
||||
import _ from '../../intl'
|
||||
import propTypes from '../../prop-types-decorator'
|
||||
import SingleLineRow from '../../single-line-row'
|
||||
import { createSelector } from '../../selectors'
|
||||
import { SelectSr } from '../../select-objects'
|
||||
import { isSrWritable } from 'xo'
|
||||
import { Container, Col } from 'grid'
|
||||
|
||||
// Can 2 SRs on the same pool have 2 VDIs used by the same VM
|
||||
const areSrsCompatible = (sr1, sr2) =>
|
||||
sr1.shared || sr2.shared || sr1.$container === sr2.$container
|
||||
import { isSrWritable } from 'xo'
|
||||
import { SelectSr } from '../../select-objects'
|
||||
|
||||
const Collapsible = ({ collapsible, children, ...props }) =>
|
||||
collapsible ? (
|
||||
@@ -32,96 +27,49 @@ Collapsible.propTypes = {
|
||||
}
|
||||
|
||||
@propTypes({
|
||||
vdis: propTypes.array.isRequired,
|
||||
predicate: propTypes.func,
|
||||
mainSrPredicate: propTypes.func,
|
||||
onChange: propTypes.func.isRequired,
|
||||
srPredicate: propTypes.func,
|
||||
value: propTypes.objectOf(
|
||||
propTypes.shape({
|
||||
mainSr: propTypes.object,
|
||||
mapVdisSrs: propTypes.object,
|
||||
})
|
||||
).isRequired,
|
||||
vdis: propTypes.object.isRequired,
|
||||
})
|
||||
export default class ChooseSrForEachVdisModal extends Component {
|
||||
state = {
|
||||
mapVdisSrs: {},
|
||||
}
|
||||
|
||||
componentWillReceiveProps (newProps) {
|
||||
if (
|
||||
this.props.predicate !== undefined &&
|
||||
newProps.predicate !== this.props.predicate
|
||||
) {
|
||||
this.state = {
|
||||
mainSr: undefined,
|
||||
mapVdisSrs: {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onChange = props => {
|
||||
this.setState(props)
|
||||
this.props.onChange(props)
|
||||
}
|
||||
|
||||
_onChangeMainSr = newSr => {
|
||||
const oldSr = this.state.mainSr
|
||||
|
||||
if (oldSr == null || newSr == null || oldSr.$pool !== newSr.$pool) {
|
||||
this.setState({
|
||||
mapVdisSrs: {},
|
||||
})
|
||||
} else if (!newSr.shared) {
|
||||
const mapVdisSrs = { ...this.state.mapVdisSrs }
|
||||
forEach(mapVdisSrs, (sr, vdi) => {
|
||||
if (
|
||||
sr != null &&
|
||||
newSr !== sr &&
|
||||
sr.$container !== newSr.$container &&
|
||||
!sr.shared
|
||||
) {
|
||||
delete mapVdisSrs[vdi]
|
||||
}
|
||||
})
|
||||
this._onChange({ mapVdisSrs })
|
||||
}
|
||||
|
||||
this._onChange({
|
||||
mainSr: newSr,
|
||||
_onChange = newValues => {
|
||||
this.props.onChange({
|
||||
...this.props.value,
|
||||
...newValues,
|
||||
})
|
||||
}
|
||||
|
||||
_getSrPredicate = createSelector(
|
||||
() => this.state.mainSr,
|
||||
() => this.state.mapVdisSrs,
|
||||
(mainSr, mapVdisSrs) => sr =>
|
||||
isSrWritable(sr) &&
|
||||
mainSr.$pool === sr.$pool &&
|
||||
areSrsCompatible(mainSr, sr) &&
|
||||
every(
|
||||
mapVdisSrs,
|
||||
selectedSr => selectedSr == null || areSrsCompatible(selectedSr, sr)
|
||||
)
|
||||
)
|
||||
_onChangeMainSr = mainSr => this._onChange({ mainSr })
|
||||
|
||||
render () {
|
||||
const { props, state } = this
|
||||
const { vdis } = props
|
||||
const { mainSr, mapVdisSrs } = state
|
||||
|
||||
const srPredicate = props.predicate || this._getSrPredicate()
|
||||
const { props } = this
|
||||
const {
|
||||
mainSrPredicate = isSrWritable,
|
||||
srPredicate = mainSrPredicate,
|
||||
value: { mainSr, mapVdisSrs },
|
||||
} = props
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SelectSr
|
||||
onChange={mainSr =>
|
||||
props.predicate !== undefined
|
||||
? this._onChange({ mainSr })
|
||||
: this._onChangeMainSr(mainSr)
|
||||
}
|
||||
predicate={props.predicate || isSrWritable}
|
||||
onChange={this._onChangeMainSr}
|
||||
placeholder={_('chooseSrForEachVdisModalMainSr')}
|
||||
predicate={mainSrPredicate}
|
||||
value={mainSr}
|
||||
/>
|
||||
<br />
|
||||
{vdis != null &&
|
||||
{props.vdis != null &&
|
||||
mainSr != null && (
|
||||
<Collapsible
|
||||
collapsible={vdis.length >= 3}
|
||||
buttonText={_('chooseSrForEachVdisModalSelectSr')}
|
||||
collapsible={props.vdis.length >= 3}
|
||||
>
|
||||
<br />
|
||||
<Container>
|
||||
@@ -133,7 +81,7 @@ export default class ChooseSrForEachVdisModal extends Component {
|
||||
<strong>{_('chooseSrForEachVdisModalSrLabel')}</strong>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
{map(vdis, vdi => (
|
||||
{map(props.vdis, vdi => (
|
||||
<SingleLineRow key={vdi.uuid}>
|
||||
<Col size={6}>{vdi.name_label || vdi.name}</Col>
|
||||
<Col size={6}>
|
||||
@@ -143,8 +91,8 @@ export default class ChooseSrForEachVdisModal extends Component {
|
||||
mapVdisSrs: { ...mapVdisSrs, [vdi.uuid]: sr },
|
||||
})
|
||||
}
|
||||
value={mapVdisSrs[vdi.uuid]}
|
||||
predicate={srPredicate}
|
||||
value={mapVdisSrs !== undefined && mapVdisSrs[vdi.uuid]}
|
||||
/>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
assign,
|
||||
filter,
|
||||
forEach,
|
||||
includes,
|
||||
isEmpty,
|
||||
isEqual,
|
||||
map,
|
||||
@@ -841,6 +842,16 @@ export const stopVms = (vms, force = false) =>
|
||||
|
||||
export const suspendVm = vm => _call('vm.suspend', { id: resolveId(vm) })
|
||||
|
||||
export const suspendVms = vms =>
|
||||
confirm({
|
||||
title: _('suspendVmsModalTitle', { nVms: vms.length }),
|
||||
body: _('suspendVmsModalMessage', { nVms: vms.length }),
|
||||
}).then(
|
||||
() =>
|
||||
Promise.all(map(vms, vm => _call('vm.suspend', { id: resolveId(vm) }))),
|
||||
noop
|
||||
)
|
||||
|
||||
export const resumeVm = vm => _call('vm.resume', { id: resolveId(vm) })
|
||||
|
||||
export const recoveryStartVm = vm =>
|
||||
@@ -1415,6 +1426,12 @@ export const deletePbd = pbd => _call('pbd.delete', { id: resolveId(pbd) })
|
||||
export const deleteMessage = message =>
|
||||
_call('message.delete', { id: resolveId(message) })
|
||||
|
||||
export const deleteMessages = logs =>
|
||||
confirm({
|
||||
title: _('logDeleteMultiple', { nLogs: logs.length }),
|
||||
body: _('logDeleteMultipleMessage', { nLogs: logs.length }),
|
||||
}).then(() => Promise.all(map(logs, deleteMessage)), noop)
|
||||
|
||||
// Tags --------------------------------------------------------------
|
||||
|
||||
export const addTag = (object, tag) =>
|
||||
@@ -1907,7 +1924,27 @@ export const deleteSshKey = key =>
|
||||
}).then(() => {
|
||||
const { preferences } = xo.user
|
||||
return _setUserPreferences({
|
||||
sshKeys: filter(preferences && preferences.sshKeys, k => !isEqual(k, key)),
|
||||
sshKeys: filter(
|
||||
preferences && preferences.sshKeys,
|
||||
k => k.key !== resolveId(key)
|
||||
),
|
||||
})
|
||||
}, noop)
|
||||
|
||||
export const deleteSshKeys = keys =>
|
||||
confirm({
|
||||
title: _('deleteSshKeysConfirm', { nKeys: keys.length }),
|
||||
body: _('deleteSshKeysConfirmMessage', {
|
||||
nKeys: keys.length,
|
||||
}),
|
||||
}).then(() => {
|
||||
const { preferences } = xo.user
|
||||
const keyIds = resolveIds(keys)
|
||||
return _setUserPreferences({
|
||||
sshKeys: filter(
|
||||
preferences && preferences.sshKeys,
|
||||
sshKey => !includes(keyIds, sshKey.key)
|
||||
),
|
||||
})
|
||||
}, noop)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import BaseComponent from 'base-component'
|
||||
import every from 'lodash/every'
|
||||
import forEach from 'lodash/forEach'
|
||||
import find from 'lodash/find'
|
||||
import forEach from 'lodash/forEach'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import store from 'store'
|
||||
@@ -11,18 +11,17 @@ import ChooseSrForEachVdisModal from '../choose-sr-for-each-vdis-modal'
|
||||
import invoke from '../../invoke'
|
||||
import SingleLineRow from '../../single-line-row'
|
||||
import { Col } from '../../grid'
|
||||
import { connectStore, mapPlus, resolveId, resolveIds } from '../../utils'
|
||||
import { getDefaultNetworkForVif } from '../utils'
|
||||
import { SelectHost, SelectNetwork } from '../../select-objects'
|
||||
import { connectStore, mapPlus, resolveIds } from '../../utils'
|
||||
import {
|
||||
createGetObjectsOfType,
|
||||
createPicker,
|
||||
createSelector,
|
||||
getObject,
|
||||
} from '../../selectors'
|
||||
import { isSrShared } from 'xo'
|
||||
|
||||
import { isSrWritable } from '../'
|
||||
import { isSrShared, isSrWritable } from '../'
|
||||
|
||||
import styles from './index.css'
|
||||
|
||||
@@ -68,8 +67,8 @@ export default class MigrateVmModalBody extends BaseComponent {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
mapVdisSrs: {},
|
||||
mapVifsNetworks: {},
|
||||
targetSrs: {},
|
||||
}
|
||||
|
||||
this._getHostPredicate = createSelector(
|
||||
@@ -126,11 +125,11 @@ export default class MigrateVmModalBody extends BaseComponent {
|
||||
|
||||
get value () {
|
||||
return {
|
||||
targetHost: this.state.host && this.state.host.id,
|
||||
sr: this.state.mainSr && this.state.mainSr.id,
|
||||
mapVdisSrs: resolveIds(this.state.mapVdisSrs),
|
||||
mapVdisSrs: resolveIds(this.state.targetSrs.mapVdisSrs),
|
||||
mapVifsNetworks: this.state.mapVifsNetworks,
|
||||
migrationNetwork: this.state.migrationNetworkId,
|
||||
sr: resolveId(this.state.targetSrs.mainSr),
|
||||
targetHost: this.state.host && this.state.host.id,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,6 +173,7 @@ export default class MigrateVmModalBody extends BaseComponent {
|
||||
intraPool,
|
||||
mapVifsNetworks: undefined,
|
||||
migrationNetwork: undefined,
|
||||
targetSrs: {},
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -205,6 +205,7 @@ export default class MigrateVmModalBody extends BaseComponent {
|
||||
intraPool,
|
||||
mapVifsNetworks: defaultNetworksForVif,
|
||||
migrationNetworkId: defaultMigrationNetworkId,
|
||||
targetSrs: {},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -219,6 +220,7 @@ export default class MigrateVmModalBody extends BaseComponent {
|
||||
intraPool,
|
||||
mapVifsNetworks,
|
||||
migrationNetworkId,
|
||||
targetSrs,
|
||||
} = this.state
|
||||
return (
|
||||
<div>
|
||||
@@ -240,8 +242,9 @@ export default class MigrateVmModalBody extends BaseComponent {
|
||||
<SingleLineRow>
|
||||
<Col size={12}>
|
||||
<ChooseSrForEachVdisModal
|
||||
onChange={props => this.setState(props)}
|
||||
predicate={this._getSrPredicate()}
|
||||
mainSrPredicate={this._getSrPredicate()}
|
||||
onChange={this.linkState('targetSrs')}
|
||||
value={targetSrs}
|
||||
vdis={vdis}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
@@ -32,7 +32,7 @@ const Upgrade = propTypes({
|
||||
<Icon icon='plan-upgrade' /> {_('upgradeNow')}
|
||||
</a>{' '}
|
||||
{_('or')}
|
||||
<Link className='btn btn-success btn-lg' to={'/xoa-update'}>
|
||||
<Link className='btn btn-success btn-lg' to='/xoa/update'>
|
||||
<Icon icon='plan-trial' /> {_('tryIt')}
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
20
src/index.js
20
src/index.js
@@ -1,6 +1,5 @@
|
||||
import './patch-react'
|
||||
|
||||
import DevTools from 'store/dev-tools'
|
||||
import hashHistory from 'react-router/lib/hashHistory'
|
||||
import React from 'react'
|
||||
import Router from 'react-router/lib/Router'
|
||||
@@ -12,17 +11,14 @@ import XoApp from './xo-app'
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<div>
|
||||
<Router
|
||||
history={hashHistory}
|
||||
routes={{
|
||||
...XoApp.route,
|
||||
component: XoApp,
|
||||
path: '/',
|
||||
}}
|
||||
/>
|
||||
{DevTools && <DevTools />}
|
||||
</div>
|
||||
<Router
|
||||
history={hashHistory}
|
||||
routes={{
|
||||
...XoApp.route,
|
||||
component: XoApp,
|
||||
path: '/',
|
||||
}}
|
||||
/>
|
||||
</Provider>,
|
||||
document.getElementById('xo-app')
|
||||
)
|
||||
|
||||
@@ -9,6 +9,7 @@ import { getUser } from 'selectors'
|
||||
import { serverVersion } from 'xo'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { connectStore, getXoaPlan } from 'utils'
|
||||
import computed from 'computed'
|
||||
|
||||
import pkg from '../../../package'
|
||||
|
||||
@@ -25,6 +26,20 @@ const HEADER = (
|
||||
</Container>
|
||||
)
|
||||
|
||||
let MyComponent = _ => <p>{_.firstName}</p>
|
||||
|
||||
MyComponent = computed({
|
||||
firstName: ({ firstName }) => firstName.toUpperCase(),
|
||||
fullName: _ =>
|
||||
_.firstName === 'Bob'
|
||||
? 'Bobinette'
|
||||
: `${_.title} ${_.lastName}, ${_.firstName}`,
|
||||
})(MyComponent)
|
||||
|
||||
MyComponent = computed({
|
||||
title: () => 'Mr',
|
||||
})(MyComponent)
|
||||
|
||||
@connectStore(() => ({
|
||||
user: getUser,
|
||||
}))
|
||||
@@ -34,12 +49,30 @@ export default class About extends Component {
|
||||
this.setState({ serverVersion })
|
||||
})
|
||||
}
|
||||
|
||||
state = {
|
||||
firstName: 'John',
|
||||
lastName: 'Smith',
|
||||
};
|
||||
|
||||
render () {
|
||||
const { user } = this.props
|
||||
const isAdmin = user && user.permission === 'admin'
|
||||
|
||||
return (
|
||||
<Page header={HEADER} title='aboutPage' formatTitle>
|
||||
<input
|
||||
value={this.state.firstName}
|
||||
onChange={this.linkState('firstName')}
|
||||
/>
|
||||
<input
|
||||
value={this.state.lastName}
|
||||
onChange={this.linkState('lastName')}
|
||||
/>
|
||||
<MyComponent
|
||||
firstName={this.state.firstName}
|
||||
lastName={this.state.lastName}
|
||||
/>
|
||||
<Container className='text-xs-center'>
|
||||
{isAdmin && (
|
||||
<Row>
|
||||
@@ -88,7 +121,7 @@ export default class About extends Component {
|
||||
<p className='text-muted'>{_('bugTrackerText')}</p>
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<a href='https://xen-orchestra.com/forum'>
|
||||
<a href='https://xen-orchestra.com/forum/'>
|
||||
<Icon icon='group' size={4} />
|
||||
<h4>{_('community')}</h4>
|
||||
</a>
|
||||
@@ -100,7 +133,7 @@ export default class About extends Component {
|
||||
<div>
|
||||
<Row>
|
||||
<Col>
|
||||
<Link to='/xoa-update'>
|
||||
<Link to='/xoa/update'>
|
||||
<h2>{_('freeTrial')}</h2>
|
||||
{_('freeTrialNow')}
|
||||
</Link>
|
||||
|
||||
@@ -22,16 +22,16 @@ const HEADER = (
|
||||
</Col>
|
||||
<Col mediumSize={9}>
|
||||
<NavTabs className='pull-right'>
|
||||
<NavLink to={'/backup/overview'}>
|
||||
<NavLink to='/backup/overview'>
|
||||
<Icon icon='menu-backup-overview' /> {_('backupOverviewPage')}
|
||||
</NavLink>
|
||||
<NavLink to={'/backup/new'}>
|
||||
<NavLink to='/backup/new'>
|
||||
<Icon icon='menu-backup-new' /> {_('backupNewPage')}
|
||||
</NavLink>
|
||||
<NavLink to={'/backup/restore'}>
|
||||
<NavLink to='/backup/restore'>
|
||||
<Icon icon='menu-backup-restore' /> {_('backupRestorePage')}
|
||||
</NavLink>
|
||||
<NavLink to={'/backup/file-restore'}>
|
||||
<NavLink to='/backup/file-restore'>
|
||||
<Icon icon='menu-backup-file-restore' />{' '}
|
||||
{_('backupFileRestorePage')}
|
||||
</NavLink>
|
||||
|
||||
@@ -226,11 +226,13 @@ export default class Overview extends Component {
|
||||
collection={schedules}
|
||||
emptyMessage={_('noScheduledJobs')}
|
||||
>
|
||||
<SortedTable
|
||||
columns={JOB_COLUMNS}
|
||||
collection={this._getScheduleCollection()}
|
||||
userData={isScheduleUserMissing}
|
||||
/>
|
||||
{() => (
|
||||
<SortedTable
|
||||
columns={JOB_COLUMNS}
|
||||
collection={this._getScheduleCollection()}
|
||||
userData={isScheduleUserMissing}
|
||||
/>
|
||||
)}
|
||||
</NoObjects>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
|
||||
@@ -5,7 +5,6 @@ import every from 'lodash/every'
|
||||
import filter from 'lodash/filter'
|
||||
import find from 'lodash/find'
|
||||
import forEach from 'lodash/forEach'
|
||||
import getEventValue from 'get-event-value'
|
||||
import groupBy from 'lodash/groupBy'
|
||||
import Icon from 'icon'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
@@ -125,8 +124,8 @@ const openImportModal = ({ backups }) =>
|
||||
body: <ImportModalBody vmName={backups[0].name} backups={backups} />,
|
||||
}).then(doImport)
|
||||
|
||||
const doImport = ({ backup, mainSr, start, mapVdisSrs }) => {
|
||||
if (!mainSr || !backup) {
|
||||
const doImport = ({ backup, targetSrs, start }) => {
|
||||
if (targetSrs.mainSr === undefined || backup === undefined) {
|
||||
error(_('backupRestoreErrorTitle'), _('backupRestoreErrorMessage'))
|
||||
return
|
||||
}
|
||||
@@ -137,10 +136,10 @@ const doImport = ({ backup, mainSr, start, mapVdisSrs }) => {
|
||||
info(_('importBackupTitle'), _('importBackupMessage'))
|
||||
try {
|
||||
const importPromise = importMethods[backup.type]({
|
||||
remote: backup.remoteId,
|
||||
sr: mainSr,
|
||||
file: backup.path,
|
||||
mapVdisSrs,
|
||||
mapVdisSrs: targetSrs.mapVdisSrs,
|
||||
remote: backup.remoteId,
|
||||
sr: targetSrs.mainSr,
|
||||
}).then(id => {
|
||||
return id
|
||||
})
|
||||
@@ -153,12 +152,8 @@ const doImport = ({ backup, mainSr, start, mapVdisSrs }) => {
|
||||
}
|
||||
|
||||
class _ModalBody extends Component {
|
||||
constructor () {
|
||||
super()
|
||||
|
||||
this.state = {
|
||||
mapVdisSrs: {},
|
||||
}
|
||||
state = {
|
||||
targetSrs: {},
|
||||
}
|
||||
|
||||
get value () {
|
||||
@@ -166,52 +161,52 @@ class _ModalBody extends Component {
|
||||
}
|
||||
|
||||
_getSrPredicate = createSelector(
|
||||
() => this.state.sr,
|
||||
() => this.state.mapVdisSrs,
|
||||
(defaultSr, mapVdisSrs) => sr =>
|
||||
sr !== defaultSr &&
|
||||
() => this.state.targetSrs.mainSr,
|
||||
() => this.state.targetSrs.mapVdisSrs,
|
||||
(mainSr, mapVdisSrs) => sr =>
|
||||
isSrWritable(sr) &&
|
||||
defaultSr.$pool === sr.$pool &&
|
||||
areSrsCompatible(defaultSr, sr) &&
|
||||
mainSr.$pool === sr.$pool &&
|
||||
areSrsCompatible(mainSr, sr) &&
|
||||
every(
|
||||
mapVdisSrs,
|
||||
selectedSr => selectedSr == null || areSrsCompatible(selectedSr, sr)
|
||||
)
|
||||
)
|
||||
|
||||
_onChangeDefaultSr = event => {
|
||||
const oldSr = this.state.sr
|
||||
const newSr = getEventValue(event)
|
||||
_onSrsChange = props => {
|
||||
const oldMainSr = this.state.targetSrs.mainSr
|
||||
const newMainSr = props.mainSr
|
||||
|
||||
if (oldSr == null || newSr == null || oldSr.$pool !== newSr.$pool) {
|
||||
this.setState({
|
||||
mapVdisSrs: {},
|
||||
})
|
||||
} else if (!newSr.shared) {
|
||||
const mapVdisSrs = { ...this.state.mapVdisSrs }
|
||||
forEach(mapVdisSrs, (sr, vdi) => {
|
||||
if (
|
||||
sr != null &&
|
||||
newSr !== sr &&
|
||||
sr.$container !== newSr.$container &&
|
||||
!sr.shared
|
||||
) {
|
||||
delete mapVdisSrs[vdi]
|
||||
}
|
||||
})
|
||||
this.setState({
|
||||
mapVdisSrs,
|
||||
})
|
||||
const targetSrs = { ...props }
|
||||
|
||||
// This code fixes the incompatibilities between the mapVdisSrs values
|
||||
if (oldMainSr !== newMainSr) {
|
||||
if (
|
||||
oldMainSr == null ||
|
||||
newMainSr == null ||
|
||||
oldMainSr.$pool !== newMainSr.$pool
|
||||
) {
|
||||
targetSrs.mapVdisSrs = {}
|
||||
} else if (!newMainSr.shared) {
|
||||
forEach(targetSrs.mapVdisSrs, (sr, vdi) => {
|
||||
if (
|
||||
sr != null &&
|
||||
newMainSr !== sr &&
|
||||
sr.$container !== newMainSr.$container &&
|
||||
!sr.shared
|
||||
) {
|
||||
delete targetSrs.mapVdisSrs[vdi]
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
sr: newSr,
|
||||
})
|
||||
this.setState({ targetSrs })
|
||||
}
|
||||
|
||||
render () {
|
||||
const { backups, intl } = this.props
|
||||
const vdis = this.state.backup && this.state.backup.vdis
|
||||
const { props, state } = this
|
||||
const vdis = state.backup && state.backup.vdis
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -219,15 +214,17 @@ class _ModalBody extends Component {
|
||||
onChange={this.linkState('backup')}
|
||||
optionKey='path'
|
||||
optionRenderer={backupOptionRenderer}
|
||||
options={backups}
|
||||
placeholder={intl.formatMessage(
|
||||
options={props.backups}
|
||||
placeholder={props.intl.formatMessage(
|
||||
messages.importBackupModalSelectBackup
|
||||
)}
|
||||
/>
|
||||
<br />
|
||||
<ChooseSrForEachVdisModal
|
||||
onChange={this._onSrsChange}
|
||||
srPredicate={this._getSrPredicate()}
|
||||
value={state.targetSrs}
|
||||
vdis={vdis}
|
||||
onChange={props => this.setState(props)}
|
||||
/>
|
||||
<br />
|
||||
<Toggle onChange={this.linkState('start')} />{' '}
|
||||
|
||||
@@ -461,16 +461,18 @@ export default class Health extends Component {
|
||||
collection={props.areObjectsFetched ? props.userSrs : null}
|
||||
emptyMessage={_('noSrs')}
|
||||
>
|
||||
<Row>
|
||||
<Col>
|
||||
<SortedTable
|
||||
collection={props.userSrs}
|
||||
columns={SR_COLUMNS}
|
||||
rowLink={this._getSrUrl}
|
||||
shortcutsTarget='body'
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
{() => (
|
||||
<Row>
|
||||
<Col>
|
||||
<SortedTable
|
||||
collection={props.userSrs}
|
||||
columns={SR_COLUMNS}
|
||||
rowLink={this._getSrUrl}
|
||||
shortcutsTarget='body'
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</NoObjects>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
@@ -489,26 +491,28 @@ export default class Health extends Component {
|
||||
}
|
||||
emptyMessage={_('noOrphanedObject')}
|
||||
>
|
||||
<div>
|
||||
<Row>
|
||||
<Col className='text-xs-right'>
|
||||
<TabButton
|
||||
btnStyle='danger'
|
||||
handler={this._deleteOrphanedVdis}
|
||||
icon='delete'
|
||||
labelId='removeAllOrphanedObject'
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<SortedTable
|
||||
collection={this.props.vdiOrphaned}
|
||||
columns={ORPHANED_VDI_COLUMNS}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
{() => (
|
||||
<div>
|
||||
<Row>
|
||||
<Col className='text-xs-right'>
|
||||
<TabButton
|
||||
btnStyle='danger'
|
||||
handler={this._deleteOrphanedVdis}
|
||||
icon='delete'
|
||||
labelId='removeAllOrphanedObject'
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<SortedTable
|
||||
collection={this.props.vdiOrphaned}
|
||||
columns={ORPHANED_VDI_COLUMNS}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
)}
|
||||
</NoObjects>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
@@ -525,13 +529,10 @@ export default class Health extends Component {
|
||||
collection={
|
||||
props.areObjectsFetched ? props.controlDomainVdis : null
|
||||
}
|
||||
columns={CONTROL_DOMAIN_VDI_COLUMNS}
|
||||
component={SortedTable}
|
||||
emptyMessage={_('noControlDomainVdis')}
|
||||
>
|
||||
<SortedTable
|
||||
collection={props.controlDomainVdis}
|
||||
columns={CONTROL_DOMAIN_VDI_COLUMNS}
|
||||
/>
|
||||
</NoObjects>
|
||||
/>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
@@ -545,14 +546,11 @@ export default class Health extends Component {
|
||||
<CardBlock>
|
||||
<NoObjects
|
||||
collection={props.areObjectsFetched ? props.vmOrphaned : null}
|
||||
columns={VM_COLUMNS}
|
||||
component={SortedTable}
|
||||
emptyMessage={_('noOrphanedObject')}
|
||||
>
|
||||
<SortedTable
|
||||
collection={props.vmOrphaned}
|
||||
columns={VM_COLUMNS}
|
||||
shortcutsTarget='.orphaned-vms'
|
||||
/>
|
||||
</NoObjects>
|
||||
shortcutsTarget='.orphaned-vms'
|
||||
/>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
@@ -570,26 +568,28 @@ export default class Health extends Component {
|
||||
}
|
||||
emptyMessage={_('noAlarms')}
|
||||
>
|
||||
<div>
|
||||
<Row>
|
||||
<Col className='text-xs-right'>
|
||||
<TabButton
|
||||
btnStyle='danger'
|
||||
handler={this._deleteAllLogs}
|
||||
icon='delete'
|
||||
labelId='logRemoveAll'
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<SortedTable
|
||||
collection={this.state.messages}
|
||||
columns={ALARM_COLUMNS}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
{() => (
|
||||
<div>
|
||||
<Row>
|
||||
<Col className='text-xs-right'>
|
||||
<TabButton
|
||||
btnStyle='danger'
|
||||
handler={this._deleteAllLogs}
|
||||
icon='delete'
|
||||
labelId='logRemoveAll'
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<SortedTable
|
||||
collection={this.state.messages}
|
||||
columns={ALARM_COLUMNS}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
)}
|
||||
</NoObjects>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
|
||||
@@ -21,18 +21,18 @@ const HEADER = (
|
||||
</Col>
|
||||
<Col mediumSize={9}>
|
||||
<NavTabs className='pull-right'>
|
||||
<NavLink to={'/dashboard/overview'}>
|
||||
<NavLink to='/dashboard/overview'>
|
||||
<Icon icon='menu-dashboard-overview' /> {_('overviewDashboardPage')}
|
||||
</NavLink>
|
||||
<NavLink to={'/dashboard/visualizations'}>
|
||||
<NavLink to='/dashboard/visualizations'>
|
||||
<Icon icon='menu-dashboard-visualization' />{' '}
|
||||
{_('overviewVisualizationDashboardPage')}
|
||||
</NavLink>
|
||||
<NavLink to={'/dashboard/stats'}>
|
||||
<NavLink to='/dashboard/stats'>
|
||||
<Icon icon='menu-dashboard-stats' />{' '}
|
||||
{_('overviewStatsDashboardPage')}
|
||||
</NavLink>
|
||||
<NavLink to={'/dashboard/health'}>
|
||||
<NavLink to='/dashboard/health'>
|
||||
<Icon icon='menu-dashboard-health' />{' '}
|
||||
{_('overviewHealthDashboardPage')}
|
||||
</NavLink>
|
||||
|
||||
@@ -10,6 +10,7 @@ import Icon from 'icon'
|
||||
import invoke from 'invoke'
|
||||
import Link from 'link'
|
||||
import Page from '../page'
|
||||
import Pagination from 'pagination'
|
||||
import propTypes from 'prop-types-decorator'
|
||||
import React from 'react'
|
||||
import Shortcuts from 'shortcuts'
|
||||
@@ -54,6 +55,7 @@ import {
|
||||
stopVms,
|
||||
subscribeResourceSets,
|
||||
subscribeServers,
|
||||
suspendVms,
|
||||
} from 'xo'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import {
|
||||
@@ -78,7 +80,6 @@ import {
|
||||
DropdownButton,
|
||||
MenuItem,
|
||||
OverlayTrigger,
|
||||
Pagination,
|
||||
Popover,
|
||||
} from 'react-bootstrap-4/lib'
|
||||
|
||||
@@ -137,6 +138,11 @@ const OPTIONS = {
|
||||
{ handler: copyVms, icon: 'vm-copy', tooltip: _('copyVmLabel') },
|
||||
],
|
||||
otherActions: [
|
||||
{
|
||||
handler: suspendVms,
|
||||
icon: 'vm-suspend',
|
||||
labelId: 'suspendVmLabel',
|
||||
},
|
||||
{
|
||||
handler: restartVms,
|
||||
icon: 'vm-force-reboot',
|
||||
@@ -580,8 +586,8 @@ export default class Home extends Component {
|
||||
|
||||
_expandAll = () => this.setState({ expandAll: !this.state.expandAll })
|
||||
|
||||
_onPageSelection = (_, event) => {
|
||||
this.page = event.eventKey
|
||||
_onPageSelection = page => {
|
||||
this.page = page
|
||||
}
|
||||
|
||||
_tick = isCriteria => (
|
||||
@@ -1077,7 +1083,9 @@ export default class Home extends Component {
|
||||
map(visibleItems, (item, index) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className={highlighted === index && styles.highlight}
|
||||
className={
|
||||
highlighted === index ? styles.highlight : undefined
|
||||
}
|
||||
>
|
||||
<Item
|
||||
expandAll={expandAll}
|
||||
@@ -1095,16 +1103,9 @@ export default class Home extends Component {
|
||||
<div style={{ display: 'flex', width: '100%' }}>
|
||||
<div style={{ margin: 'auto' }}>
|
||||
<Pagination
|
||||
first
|
||||
last
|
||||
prev
|
||||
next
|
||||
ellipsis
|
||||
boundaryLinks
|
||||
maxButtons={5}
|
||||
items={ceil(filteredItems.length / ITEMS_PER_PAGE)}
|
||||
activePage={activePage}
|
||||
onSelect={this._onPageSelection}
|
||||
onChange={this._onPageSelection}
|
||||
pages={ceil(filteredItems.length / ITEMS_PER_PAGE)}
|
||||
value={activePage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -17,14 +17,14 @@ import {
|
||||
import { FormattedRelative, FormattedTime } from 'react-intl'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { createGetObjectsOfType, createSelector } from 'selectors'
|
||||
import { map } from 'lodash'
|
||||
import { map, noop } from 'lodash'
|
||||
|
||||
const ALLOW_INSTALL_SUPP_PACK = process.env.XOA_PLAN > 1
|
||||
|
||||
const forceReboot = host => restartHost(host, true)
|
||||
|
||||
const formatPack = ({ name, author, description, version }) => (
|
||||
<tr>
|
||||
const formatPack = ({ name, author, description, version }, key) => (
|
||||
<tr key={key}>
|
||||
<th>{_('supplementalPackTitle', { author, name })}</th>
|
||||
<td>{description}</td>
|
||||
<td>{version}</td>
|
||||
@@ -116,7 +116,11 @@ export default connectStore(() => {
|
||||
<tr>
|
||||
<th>{_('hostPowerOnMode')}</th>
|
||||
<td>
|
||||
<Toggle value={host.powerOnMode} disabled />
|
||||
<Toggle
|
||||
disabled
|
||||
onChange={noop}
|
||||
value={Boolean(host.powerOnMode)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
@@ -22,13 +22,13 @@ const HEADER = (
|
||||
</Col>
|
||||
<Col mediumSize={9}>
|
||||
<NavTabs className='pull-right'>
|
||||
<NavLink to={'/jobs/overview'}>
|
||||
<NavLink to='/jobs/overview'>
|
||||
<Icon icon='menu-jobs-overview' /> {_('jobsOverviewPage')}
|
||||
</NavLink>
|
||||
<NavLink to={'/jobs/new'}>
|
||||
<NavLink to='/jobs/new'>
|
||||
<Icon icon='menu-jobs-new' /> {_('jobsNewPage')}
|
||||
</NavLink>
|
||||
<NavLink to={'/jobs/schedules'}>
|
||||
<NavLink to='/jobs/schedules'>
|
||||
<Icon icon='menu-jobs-schedule' /> {_('jobsSchedulingPage')}
|
||||
</NavLink>
|
||||
</NavTabs>
|
||||
|
||||
@@ -413,13 +413,13 @@ export default class LogList extends Component {
|
||||
</span>
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
<NoObjects collection={logs} emptyMessage={_('noLogs')}>
|
||||
<SortedTable
|
||||
collection={logs}
|
||||
columns={LOG_COLUMNS}
|
||||
filters={this.filters}
|
||||
/>
|
||||
</NoObjects>
|
||||
<NoObjects
|
||||
collection={logs}
|
||||
columns={LOG_COLUMNS}
|
||||
component={SortedTable}
|
||||
emptyMessage={_('noLogs')}
|
||||
filters={this.filters}
|
||||
/>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
)
|
||||
|
||||
@@ -339,7 +339,7 @@ export default class Menu extends Component {
|
||||
<Link
|
||||
className='nav-link'
|
||||
style={{ display: 'flex' }}
|
||||
to={'/about'}
|
||||
to='/about'
|
||||
>
|
||||
{+process.env.XOA_PLAN === 5 ? (
|
||||
<span>
|
||||
@@ -413,7 +413,7 @@ export default class Menu extends Component {
|
||||
</a>
|
||||
</li>
|
||||
<li className='nav-item xo-menu-item'>
|
||||
<Link className='nav-link text-xs-center' to={'/user'}>
|
||||
<Link className='nav-link text-xs-center' to='/user'>
|
||||
<Tooltip
|
||||
content={_('editUserProfile', {
|
||||
username: user ? user.email : '',
|
||||
|
||||
@@ -1491,7 +1491,7 @@ export default class NewVm extends BaseComponent {
|
||||
</Item>
|
||||
<Item label={_('newVmFirstIndex')}>
|
||||
<DebounceInput
|
||||
className={'form-control'}
|
||||
className='form-control'
|
||||
disabled={!multipleVms}
|
||||
onChange={this._linkState('seqStart')}
|
||||
type='number'
|
||||
|
||||
@@ -76,7 +76,9 @@ export default connectStore({
|
||||
<Col size={9}>
|
||||
<ul className='list-group'>
|
||||
{map(gpuGroups, gpuGroup => (
|
||||
<li className='list-group-item'>{renderXoItem(gpuGroup)}</li>
|
||||
<li key={gpuGroup.id} className='list-group-item'>
|
||||
{renderXoItem(gpuGroup)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Col>
|
||||
|
||||
@@ -1,122 +1,68 @@
|
||||
import _ from 'intl'
|
||||
import ActionRow from 'action-row-button'
|
||||
import React, { Component } from 'react'
|
||||
import TabButton from 'tab-button'
|
||||
import { deleteMessage } from 'xo'
|
||||
import { createPager, createSelector } from 'selectors'
|
||||
import SortedTable from 'sorted-table'
|
||||
import { FormattedRelative, FormattedTime } from 'react-intl'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { ceil, isEmpty, map } from 'lodash'
|
||||
import { deleteMessage, deleteMessages } from 'xo'
|
||||
|
||||
const LOGS_PER_PAGE = 10
|
||||
const LOG_COLUMNS = [
|
||||
{
|
||||
default: true,
|
||||
itemRenderer: log => (
|
||||
<div>
|
||||
<FormattedTime
|
||||
value={log.time * 1000}
|
||||
minute='numeric'
|
||||
hour='numeric'
|
||||
day='numeric'
|
||||
month='long'
|
||||
year='numeric'
|
||||
/>{' '}
|
||||
(<FormattedRelative value={log.time * 1000} />)
|
||||
</div>
|
||||
),
|
||||
name: _('logDate'),
|
||||
sortCriteria: 'time',
|
||||
},
|
||||
{
|
||||
itemRenderer: log => log.name,
|
||||
name: _('logName'),
|
||||
sortCriteria: 'name',
|
||||
},
|
||||
{
|
||||
itemRenderer: log => log.body,
|
||||
name: _('logContent'),
|
||||
sortCriteria: 'body',
|
||||
},
|
||||
]
|
||||
|
||||
const INDIVIDUAL_ACTIONS = [
|
||||
{
|
||||
handler: deleteMessage,
|
||||
icon: 'delete',
|
||||
label: _('logDelete'),
|
||||
level: 'danger',
|
||||
},
|
||||
]
|
||||
|
||||
const GROUPED_ACTIONS = [
|
||||
{
|
||||
handler: deleteMessages,
|
||||
icon: 'delete',
|
||||
label: _('logsDelete'),
|
||||
level: 'danger',
|
||||
},
|
||||
]
|
||||
|
||||
export default class TabLogs extends Component {
|
||||
constructor () {
|
||||
super()
|
||||
|
||||
this.getLogs = createPager(
|
||||
() => this.props.logs,
|
||||
() => this.state.page,
|
||||
LOGS_PER_PAGE
|
||||
)
|
||||
|
||||
this.getNPages = createSelector(
|
||||
() => (this.props.logs ? this.props.logs.length : 0),
|
||||
nLogs => ceil(nLogs / LOGS_PER_PAGE)
|
||||
)
|
||||
|
||||
this.state = {
|
||||
page: 1,
|
||||
}
|
||||
}
|
||||
|
||||
_deleteAllLogs = () => map(this.props.logs, deleteMessage)
|
||||
_nextPage = () =>
|
||||
this.setState({ page: Math.min(this.state.page + 1, this.getNPages()) })
|
||||
_previousPage = () =>
|
||||
this.setState({ page: Math.max(this.state.page - 1, 1) })
|
||||
|
||||
render () {
|
||||
const logs = this.getLogs()
|
||||
const { page } = this.state
|
||||
|
||||
return (
|
||||
<Container>
|
||||
{isEmpty(logs) ? (
|
||||
<Row>
|
||||
<Col mediumSize={6} className='text-xs-center'>
|
||||
<br />
|
||||
<h4>{_('noLogs')}</h4>
|
||||
</Col>
|
||||
</Row>
|
||||
) : (
|
||||
<div>
|
||||
<Row>
|
||||
<Col className='text-xs-right'>
|
||||
<TabButton
|
||||
btnStyle='secondary'
|
||||
disabled={page === 1}
|
||||
handler={this._previousPage}
|
||||
icon='previous'
|
||||
/>
|
||||
<TabButton
|
||||
btnStyle='secondary'
|
||||
disabled={page === this.getNPages()}
|
||||
handler={this._nextPage}
|
||||
icon='next'
|
||||
/>
|
||||
<TabButton
|
||||
btnStyle='danger'
|
||||
handler={this._removeAllLogs} // FIXME: define this method
|
||||
icon='delete'
|
||||
labelId='logRemoveAll'
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<table className='table'>
|
||||
<thead className='thead-default'>
|
||||
<tr>
|
||||
<th>{_('logDate')}</th>
|
||||
<th>{_('logName')}</th>
|
||||
<th>{_('logContent')}</th>
|
||||
<th>{_('logAction')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{map(logs, log => (
|
||||
<tr key={log.id}>
|
||||
<td>
|
||||
<FormattedTime
|
||||
value={log.time * 1000}
|
||||
minute='numeric'
|
||||
hour='numeric'
|
||||
day='numeric'
|
||||
month='long'
|
||||
year='numeric'
|
||||
/>{' '}
|
||||
(<FormattedRelative value={log.time * 1000} />)
|
||||
</td>
|
||||
<td>{log.name}</td>
|
||||
<td>{log.body}</td>
|
||||
<td>
|
||||
<ActionRow
|
||||
btnStyle='danger'
|
||||
handler={deleteMessage}
|
||||
handlerParam={log}
|
||||
icon='delete'
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
)}
|
||||
</Container>
|
||||
<SortedTable
|
||||
collection={this.props.logs}
|
||||
columns={LOG_COLUMNS}
|
||||
groupedActions={GROUPED_ACTIONS}
|
||||
individualActions={INDIVIDUAL_ACTIONS}
|
||||
stateUrlParam='s'
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -834,8 +834,11 @@ export default class Self extends Component {
|
||||
</ActionButton>
|
||||
</div>
|
||||
{showNewResourceSetForm && [
|
||||
<Edit onSave={this.toggleState('showNewResourceSetForm')} />,
|
||||
<hr />,
|
||||
<Edit
|
||||
key={0}
|
||||
onSave={this.toggleState('showNewResourceSetForm')}
|
||||
/>,
|
||||
<hr key={1} />,
|
||||
]}
|
||||
{resourceSets
|
||||
? isEmpty(resourceSets)
|
||||
|
||||
@@ -26,31 +26,31 @@ const HEADER = (
|
||||
</Col>
|
||||
<Col mediumSize={9}>
|
||||
<NavTabs className='pull-right'>
|
||||
<NavLink to={'/settings/servers'}>
|
||||
<NavLink to='/settings/servers'>
|
||||
<Icon icon='menu-settings-servers' /> {_('settingsServersPage')}
|
||||
</NavLink>
|
||||
<NavLink to={'/settings/users'}>
|
||||
<NavLink to='/settings/users'>
|
||||
<Icon icon='menu-settings-users' /> {_('settingsUsersPage')}
|
||||
</NavLink>
|
||||
<NavLink to={'/settings/groups'}>
|
||||
<NavLink to='/settings/groups'>
|
||||
<Icon icon='menu-settings-groups' /> {_('settingsGroupsPage')}
|
||||
</NavLink>
|
||||
<NavLink to={'/settings/acls'}>
|
||||
<NavLink to='/settings/acls'>
|
||||
<Icon icon='menu-settings-acls' /> {_('settingsAclsPage')}
|
||||
</NavLink>
|
||||
<NavLink to={'/settings/remotes'}>
|
||||
<NavLink to='/settings/remotes'>
|
||||
<Icon icon='menu-backup-remotes' /> {_('backupRemotesPage')}
|
||||
</NavLink>
|
||||
<NavLink to={'/settings/plugins'}>
|
||||
<NavLink to='/settings/plugins'>
|
||||
<Icon icon='menu-settings-plugins' /> {_('settingsPluginsPage')}
|
||||
</NavLink>
|
||||
<NavLink to={'/settings/logs'}>
|
||||
<NavLink to='/settings/logs'>
|
||||
<Icon icon='menu-settings-logs' /> {_('settingsLogsPage')}
|
||||
</NavLink>
|
||||
<NavLink to={'/settings/ips'}>
|
||||
<NavLink to='/settings/ips'>
|
||||
<Icon icon='ip' /> {_('settingsIpsPage')}
|
||||
</NavLink>
|
||||
<NavLink to={'/settings/config'}>
|
||||
<NavLink to='/settings/config'>
|
||||
<Icon icon='menu-settings-config' /> {_('settingsConfigPage')}
|
||||
</NavLink>
|
||||
</NavTabs>
|
||||
|
||||
@@ -161,21 +161,23 @@ export default class Logs extends BaseComponent {
|
||||
message={_('noLogs')}
|
||||
predicate={this._getPredicate}
|
||||
>
|
||||
<div>
|
||||
<span className='pull-right'>
|
||||
<TabButton
|
||||
btnStyle='danger'
|
||||
handler={this._deleteAllLogs}
|
||||
icon='delete'
|
||||
labelId='logDeleteAll'
|
||||
{() => (
|
||||
<div>
|
||||
<span className='pull-right'>
|
||||
<TabButton
|
||||
btnStyle='danger'
|
||||
handler={this._deleteAllLogs}
|
||||
icon='delete'
|
||||
labelId='logDeleteAll'
|
||||
/>
|
||||
</span>{' '}
|
||||
<SortedTable
|
||||
collection={logs}
|
||||
columns={COLUMNS}
|
||||
userData={this._getData()}
|
||||
/>
|
||||
</span>{' '}
|
||||
<SortedTable
|
||||
collection={logs}
|
||||
columns={COLUMNS}
|
||||
userData={this._getData()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</NoObjects>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -313,7 +313,7 @@ export default class Remotes extends Component {
|
||||
)
|
||||
? alert(
|
||||
<span>
|
||||
<Icon icon={'error'} /> {_('remoteTestName')}
|
||||
<Icon icon='error' /> {_('remoteTestName')}
|
||||
</span>,
|
||||
<p>{_('remoteTestNameFailure')}</p>
|
||||
)
|
||||
|
||||
@@ -137,7 +137,7 @@ const COLUMNS = [
|
||||
{
|
||||
itemRenderer: server => (
|
||||
<Toggle
|
||||
value={server.allowUnauthorized}
|
||||
value={Boolean(server.allowUnauthorized)}
|
||||
onChange={allowUnauthorized =>
|
||||
editServer(server, { allowUnauthorized })
|
||||
}
|
||||
|
||||
@@ -809,27 +809,25 @@ export default class TabXosan extends Component {
|
||||
<div>
|
||||
<h3>{_('xosanVolume')}</h3>
|
||||
<Container>
|
||||
<Field title={'Name'}>{strippedVolumeInfo.name}</Field>
|
||||
<Field title={'Status'}>
|
||||
{strippedVolumeInfo.statusStr}
|
||||
</Field>
|
||||
<Field title={'Type'}>{strippedVolumeInfo.typeStr}</Field>
|
||||
<Field title={'Brick Count'}>
|
||||
<Field title='Name'>{strippedVolumeInfo.name}</Field>
|
||||
<Field title='Status'>{strippedVolumeInfo.statusStr}</Field>
|
||||
<Field title='Type'>{strippedVolumeInfo.typeStr}</Field>
|
||||
<Field title='Brick Count'>
|
||||
{strippedVolumeInfo.brickCount}
|
||||
</Field>
|
||||
<Field title={'Stripe Count'}>
|
||||
<Field title='Stripe Count'>
|
||||
{strippedVolumeInfo.stripeCount}
|
||||
</Field>
|
||||
<Field title={'Replica Count'}>
|
||||
<Field title='Replica Count'>
|
||||
{strippedVolumeInfo.replicaCount}
|
||||
</Field>
|
||||
<Field title={'Arbiter Count'}>
|
||||
<Field title='Arbiter Count'>
|
||||
{strippedVolumeInfo.arbiterCount}
|
||||
</Field>
|
||||
<Field title={'Disperse Count'}>
|
||||
<Field title='Disperse Count'>
|
||||
{strippedVolumeInfo.disperseCount}
|
||||
</Field>
|
||||
<Field title={'Redundancy Count'}>
|
||||
<Field title='Redundancy Count'>
|
||||
{strippedVolumeInfo.redundancyCount}
|
||||
</Field>
|
||||
</Container>
|
||||
|
||||
@@ -143,7 +143,7 @@ export default class Tasks extends Component {
|
||||
props.pools,
|
||||
pool =>
|
||||
this._showPoolTasks(pool) && (
|
||||
<Row>
|
||||
<Row key={pool.id}>
|
||||
<Card>
|
||||
<CardHeader key={pool.id}>
|
||||
<Link to={`/pools/${pool.id}`}>{pool.name_label}</Link>
|
||||
|
||||
@@ -4,14 +4,14 @@ import _, { messages } from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import Component from 'base-component'
|
||||
import Icon from 'icon'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import map from 'lodash/map'
|
||||
import propTypes from 'prop-types-decorator'
|
||||
import React from 'react'
|
||||
import SortedTable from 'sorted-table'
|
||||
import { Text } from 'editable'
|
||||
import { alert } from 'modal'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { getLang } from 'selectors'
|
||||
import { map } from 'lodash'
|
||||
import { injectIntl } from 'react-intl'
|
||||
import { Select } from 'form'
|
||||
import { Card, CardBlock, CardHeader } from 'card'
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
addSshKey,
|
||||
changePassword,
|
||||
deleteSshKey,
|
||||
deleteSshKeys,
|
||||
editCustomFilter,
|
||||
removeCustomFilter,
|
||||
setDefaultHomeFilter,
|
||||
@@ -226,12 +227,44 @@ class UserFilters extends Component {
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
const COLUMNS = [
|
||||
{
|
||||
default: true,
|
||||
itemRenderer: sshKey => sshKey.title,
|
||||
name: _('title'),
|
||||
sortCriteria: 'title',
|
||||
},
|
||||
{
|
||||
itemRenderer: sshKey => <span style={SSH_KEY_STYLE}>{sshKey.key}</span>,
|
||||
name: _('key'),
|
||||
},
|
||||
]
|
||||
|
||||
const INDIVIDUAL_ACTIONS = [
|
||||
{
|
||||
handler: deleteSshKey,
|
||||
icon: 'delete',
|
||||
label: _('deleteSshKey'),
|
||||
level: 'danger',
|
||||
},
|
||||
]
|
||||
|
||||
const GROUPED_ACTIONS = [
|
||||
{
|
||||
handler: deleteSshKeys,
|
||||
icon: 'delete',
|
||||
label: _('deleteSshKeys'),
|
||||
level: 'danger',
|
||||
},
|
||||
]
|
||||
|
||||
const SshKeys = addSubscriptions({
|
||||
user: subscribeCurrentUser,
|
||||
})(({ user }) => {
|
||||
const sshKeys = user && user.preferences && user.preferences.sshKeys
|
||||
|
||||
const sshKeysWithIds = map(sshKeys, sshKey => ({ ...sshKey, id: sshKey.key }))
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Card>
|
||||
@@ -246,30 +279,13 @@ const SshKeys = addSubscriptions({
|
||||
</ActionButton>
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
{!isEmpty(sshKeys) ? (
|
||||
<Container>
|
||||
{map(sshKeys, (sshKey, key) => (
|
||||
<Row key={key} className='pb-1'>
|
||||
<Col size={2}>
|
||||
<strong>{sshKey.title}</strong>
|
||||
</Col>
|
||||
<Col size={8} style={SSH_KEY_STYLE}>
|
||||
{sshKey.key}
|
||||
</Col>
|
||||
<Col size={2} className='text-xs-right'>
|
||||
<ActionButton
|
||||
icon='delete'
|
||||
handler={() => deleteSshKey(sshKey)}
|
||||
>
|
||||
{_('deleteSshKey')}
|
||||
</ActionButton>
|
||||
</Col>
|
||||
</Row>
|
||||
))}
|
||||
</Container>
|
||||
) : (
|
||||
_('noSshKeys')
|
||||
)}
|
||||
<SortedTable
|
||||
collection={sshKeysWithIds}
|
||||
columns={COLUMNS}
|
||||
groupedActions={GROUPED_ACTIONS}
|
||||
individualActions={INDIVIDUAL_ACTIONS}
|
||||
stateUrlParam='s'
|
||||
/>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -178,7 +178,7 @@ class CoresPerSocket extends Component {
|
||||
_getCoresPerSocketPossibilities = createSelector(
|
||||
() => {
|
||||
const { container } = this.props
|
||||
if (container !== undefined) {
|
||||
if (container != null) {
|
||||
return container.cpus.cores
|
||||
}
|
||||
},
|
||||
@@ -198,42 +198,55 @@ class CoresPerSocket extends Component {
|
||||
editVm(this.props.vm, { coresPerSocket: getEventValue(event) || null })
|
||||
|
||||
render () {
|
||||
const vm = this.props.vm
|
||||
const { container, vm } = this.props
|
||||
const selectedCoresPerSocket = vm.coresPerSocket
|
||||
const options = this._getCoresPerSocketPossibilities()
|
||||
|
||||
return (
|
||||
<form className='form-inline'>
|
||||
<select
|
||||
className='form-control'
|
||||
onChange={this._onChange}
|
||||
value={selectedCoresPerSocket || ''}
|
||||
>
|
||||
{_('vmChooseCoresPerSocket', message => (
|
||||
<option key='none' value=''>
|
||||
{message}
|
||||
</option>
|
||||
))}
|
||||
{this._selectedValueIsNotInOptions() &&
|
||||
_('vmCoresPerSocketIncorrectValue', message => (
|
||||
<option key='incorrect' value={selectedCoresPerSocket}>
|
||||
{' '}
|
||||
{message}
|
||||
</option>
|
||||
))}
|
||||
{map(options, coresPerSocket => (
|
||||
<option key={coresPerSocket} value={coresPerSocket}>
|
||||
{_('vmCoresPerSocket', {
|
||||
nSockets: vm.CPUs.number / coresPerSocket,
|
||||
nCores: coresPerSocket,
|
||||
})}
|
||||
</option>
|
||||
))}
|
||||
</select>{' '}
|
||||
{this._selectedValueIsNotInOptions() && (
|
||||
<Tooltip content={_('vmCoresPerSocketIncorrectValueSolution')}>
|
||||
<Icon icon='error' size='lg' />
|
||||
</Tooltip>
|
||||
{container != null ? (
|
||||
<span>
|
||||
<select
|
||||
className='form-control'
|
||||
onChange={this._onChange}
|
||||
value={selectedCoresPerSocket || ''}
|
||||
>
|
||||
{_({ key: 'none' }, 'vmChooseCoresPerSocket', message => (
|
||||
<option value=''>{message}</option>
|
||||
))}
|
||||
{this._selectedValueIsNotInOptions() &&
|
||||
_(
|
||||
{ key: 'incorrect' },
|
||||
'vmCoresPerSocketIncorrectValue',
|
||||
message => (
|
||||
<option value={selectedCoresPerSocket}> {message}</option>
|
||||
)
|
||||
)}
|
||||
{map(options, coresPerSocket =>
|
||||
_(
|
||||
{ key: coresPerSocket },
|
||||
'vmCoresPerSocket',
|
||||
{
|
||||
nSockets: vm.CPUs.number / coresPerSocket,
|
||||
nCores: coresPerSocket,
|
||||
},
|
||||
message => <option value={coresPerSocket}>{message}</option>
|
||||
)
|
||||
)}
|
||||
</select>{' '}
|
||||
{this._selectedValueIsNotInOptions() && (
|
||||
<Tooltip content={_('vmCoresPerSocketIncorrectValueSolution')}>
|
||||
<Icon icon='error' size='lg' />
|
||||
</Tooltip>
|
||||
)}
|
||||
</span>
|
||||
) : selectedCoresPerSocket != null ? (
|
||||
_('vmCoresPerSocket', {
|
||||
nSockets: vm.CPUs.number / selectedCoresPerSocket,
|
||||
nCores: selectedCoresPerSocket,
|
||||
})
|
||||
) : (
|
||||
_('vmCoresPerSocketNone')
|
||||
)}
|
||||
</form>
|
||||
)
|
||||
@@ -398,7 +411,7 @@ export default connectStore(() => {
|
||||
<th>{_('autoPowerOn')}</th>
|
||||
<td>
|
||||
<Toggle
|
||||
value={vm.auto_poweron}
|
||||
value={Boolean(vm.auto_poweron)}
|
||||
onChange={value => editVm(vm, { auto_poweron: value })}
|
||||
/>
|
||||
</td>
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
import _ from 'intl'
|
||||
import ActionRowButton from 'action-row-button'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import map from 'lodash/map'
|
||||
import React, { Component } from 'react'
|
||||
import SortedTable from 'sorted-table'
|
||||
import TabButton from 'tab-button'
|
||||
import { connectStore } from 'utils'
|
||||
import { deleteMessage } from 'xo'
|
||||
import { FormattedRelative, FormattedTime } from 'react-intl'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { createGetObjectMessages } from 'selectors'
|
||||
import { FormattedRelative, FormattedTime } from 'react-intl'
|
||||
import { deleteMessage, deleteMessages } from 'xo'
|
||||
|
||||
const LOG_COLUMNS = [
|
||||
{
|
||||
name: _('logDate'),
|
||||
itemRenderer: log => (
|
||||
<span>
|
||||
<FormattedTime
|
||||
@@ -27,29 +21,37 @@ const LOG_COLUMNS = [
|
||||
(<FormattedRelative value={log.time * 1000} />)
|
||||
</span>
|
||||
),
|
||||
sortCriteria: log => log.time,
|
||||
name: _('logDate'),
|
||||
sortCriteria: 'time',
|
||||
sortOrder: 'desc',
|
||||
},
|
||||
{
|
||||
name: _('logName'),
|
||||
itemRenderer: log => log.name,
|
||||
sortCriteria: log => log.name,
|
||||
name: _('logName'),
|
||||
sortCriteria: 'name',
|
||||
},
|
||||
{
|
||||
name: _('logContent'),
|
||||
itemRenderer: log => log.body,
|
||||
sortCriteria: log => log.body,
|
||||
name: _('logContent'),
|
||||
sortCriteria: 'body',
|
||||
},
|
||||
]
|
||||
|
||||
const INDIVIDUAL_ACTIONS = [
|
||||
{
|
||||
name: _('logAction'),
|
||||
itemRenderer: log => (
|
||||
<ActionRowButton
|
||||
btnStyle='danger'
|
||||
handler={deleteMessage}
|
||||
handlerParam={log}
|
||||
icon='delete'
|
||||
/>
|
||||
),
|
||||
handler: deleteMessage,
|
||||
icon: 'delete',
|
||||
label: _('logDelete'),
|
||||
level: 'danger',
|
||||
},
|
||||
]
|
||||
|
||||
const GROUPED_ACTIONS = [
|
||||
{
|
||||
handler: deleteMessages,
|
||||
icon: 'delete',
|
||||
label: _('logsDelete'),
|
||||
level: 'danger',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -61,40 +63,15 @@ const LOG_COLUMNS = [
|
||||
})
|
||||
})
|
||||
export default class TabLogs extends Component {
|
||||
_deleteAllLogs = () => map(this.props.logs, deleteMessage)
|
||||
|
||||
render () {
|
||||
const { logs } = this.props
|
||||
|
||||
if (isEmpty(logs)) {
|
||||
return (
|
||||
<Row>
|
||||
<Col className='text-xs-center'>
|
||||
<br />
|
||||
<h4>{_('noLogs')}</h4>
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Row>
|
||||
<Col className='text-xs-right'>
|
||||
<TabButton
|
||||
btnStyle='danger'
|
||||
handler={this._deleteAllLogs}
|
||||
icon='delete'
|
||||
labelId='logRemoveAll'
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<SortedTable collection={logs} columns={LOG_COLUMNS} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
<SortedTable
|
||||
collection={this.props.logs}
|
||||
columns={LOG_COLUMNS}
|
||||
groupedActions={GROUPED_ACTIONS}
|
||||
individualActions={INDIVIDUAL_ACTIONS}
|
||||
stateUrlParam='s'
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -510,8 +510,8 @@ export default class TabNetwork extends BaseComponent {
|
||||
{!isEmpty(vm.addresses) ? (
|
||||
<span>
|
||||
<h4>{_('vifIpAddresses')}</h4>
|
||||
{map(vm.addresses, address => (
|
||||
<span key={address} className='tag tag-info tag-ip'>
|
||||
{map(vm.addresses, (address, key) => (
|
||||
<span key={key} className='tag tag-info tag-ip'>
|
||||
{address}
|
||||
</span>
|
||||
))}
|
||||
|
||||
@@ -19,10 +19,10 @@ const HEADER = (
|
||||
</Col>
|
||||
<Col mediumSize={9}>
|
||||
<NavTabs className='pull-right'>
|
||||
<NavLink to={'/xoa/update'}>
|
||||
<NavLink to='/xoa/update'>
|
||||
<Icon icon='menu-xoa-update' /> {_('updatePage')}
|
||||
</NavLink>
|
||||
<NavLink to={'/xoa/licenses'}>
|
||||
<NavLink to='/xoa/licenses'>
|
||||
<Icon icon='menu-xoa-licenses' /> {_('licensesPage')}
|
||||
</NavLink>
|
||||
</NavTabs>
|
||||
|
||||
@@ -73,6 +73,7 @@ export default class NewXosan extends Component {
|
||||
brickSize: DEFAULT_BRICKSIZE,
|
||||
ipRange: '172.31.100.0',
|
||||
memorySize: DEFAULT_MEMORY,
|
||||
suggestion: 0,
|
||||
}
|
||||
|
||||
_selectPool = pool => {
|
||||
@@ -280,7 +281,7 @@ export default class NewXosan extends Component {
|
||||
pool !== undefined &&
|
||||
hostsNeedRestartByPool !== undefined &&
|
||||
hostsNeedRestartByPool[pool.id]
|
||||
const architecture = suggestions !== undefined && suggestions[suggestion]
|
||||
const architecture = suggestions != null && suggestions[suggestion]
|
||||
|
||||
return (
|
||||
<Container>
|
||||
|
||||
Reference in New Issue
Block a user