Compare commits

...

29 Commits

Author SHA1 Message Date
Julien Fontanet
595c4bd5a8 feat(computed): decorator for computed props 2018-01-24 14:35:00 +01:00
badrAZ
1a2f553094 fix(vm): hide cores per socket selector when no container (#2221) 2018-01-18 15:35:03 +01:00
badrAZ
4d69866532 feat(migrate-vm-modal): controlled form (#2259) 2018-01-18 10:40:07 +01:00
Pierre Donias
495c97b44b fix(xo): links to /xoa/update & www-xo forum (#2571) 2018-01-16 16:53:59 +01:00
Pierre Donias
e817b3254e fix(vm/network): duplicate key error (#2570)
Fixes #2553
2018-01-16 11:41:27 +01:00
Julien Fontanet
dd6987efe9 fix(SortedTable): infinite loop when displaying last page (#2568)
Fixes #2569
2018-01-15 16:56:00 +01:00
Julien Fontanet
d7f8d12d88 chore(prop-types-decorator): deprecate 2018-01-15 12:48:22 +01:00
Julien Fontanet
504895a730 chore(package): update dependencies 2018-01-15 12:37:00 +01:00
Rajaa.BARHTAOUI
cde92836f3 feat(user/ssh): use SortedTable (#2514)
See #2416
2018-01-12 14:56:47 +01:00
Rajaa.BARHTAOUI
c787988b06 feat(pool,vm/logs): use SortedTable (#2513) 2018-01-11 14:51:59 +01:00
Julien Fontanet
898434b267 Revert "chore(package): use React 16 (#2552)"
This reverts commit c8669dc88f.

Let's go back to React 15 until we move away from react-bootstrap.
2018-01-09 14:21:03 +01:00
Julien Fontanet
6e44c65a07 chore: fixes various warnings (#2554) 2018-01-08 15:18:15 +01:00
Julien Fontanet
03028bca50 chore(Pagination): own implementation instead of react-bootstrap (#2549) 2018-01-08 14:43:50 +01:00
Julien Fontanet
c8669dc88f chore(package): use React 16 (#2552) 2018-01-08 09:46:08 +01:00
Julien Fontanet
82240979c2 chore(react-select): autofocus → autoFocus (#2550) 2018-01-08 09:40:35 +01:00
Julien Fontanet
db5d495105 fix(addSubscription): export as default 2018-01-05 15:52:35 +01:00
Julien Fontanet
6e8dfe8833 chore(utils): remove unused checkPropsState 2018-01-05 15:48:21 +01:00
Julien Fontanet
242d9e20c4 chore(addSubscriptions): move into own module 2018-01-05 15:48:21 +01:00
Julien Fontanet
e446eb0cd0 chore(build): fix PostCSS warning 2018-01-03 18:26:02 +01:00
Julien Fontanet
b63efe579a fix(build): temporary use stage-0
The compiled code has an issue with stage-3 and plugins
2018-01-03 15:50:08 +01:00
Julien Fontanet
f3410f1491 chore(package): remove unused redux devtool 2018-01-03 15:03:09 +01:00
Julien Fontanet
b27ac11d56 chore(jsx): remove unnecessary braces 2018-01-03 10:00:32 +01:00
Julien Fontanet
a55d73614e chore(package): missing peer dependency 2018-01-02 22:29:02 +01:00
Julien Fontanet
25cd1957c7 chore(package): update Babel conf
- preset-es2015 → preset-env
- preset-stage-0 → preset-stage-3 + handpicked plugins
2018-01-02 22:20:55 +01:00
Pierre Donias
abd97abc24 fix(xosan): beta-is-over message (#2546) 2018-01-02 21:50:40 +01:00
Julien Fontanet
6ddfd909f0 chore(package): add clean script 2018-01-02 19:01:33 +01:00
Julien Fontanet
e054eec555 chore(gulpfile): fix sourcemaps 2018-01-02 19:01:33 +01:00
Olivier Lambert
e253657770 feat(home): bulk suspend VMs (#2551)
Fixes #2547
2018-01-02 17:16:11 +01:00
Julien Fontanet
102e629e16 chore(package): update dependencies 2018-01-02 10:04:27 +01:00
51 changed files with 1873 additions and 1586 deletions

View File

@@ -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,
},
],
],

View File

@@ -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"
]

View 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
View 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 }

View File

@@ -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>

View File

@@ -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}

View File

@@ -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}
>

View File

@@ -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 = {

View File

@@ -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}',

View File

@@ -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
View 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>
)
}
}

View File

@@ -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.
//

View File

@@ -248,7 +248,7 @@ export class GenericSelect extends Component {
const select = (
<Select
{...{
autofocus: autoFocus,
autoFocus,
clearable,
disabled,
multi,

View File

@@ -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}
/>
)

View File

@@ -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>
)

View File

@@ -1 +0,0 @@
module.exports = false // process.env.NODE_ENV !== 'production' && require('./dev-tools.dev')

View File

@@ -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)

View File

@@ -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) => {

View File

@@ -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]

View File

@@ -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>

View File

@@ -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)

View File

@@ -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>

View File

@@ -32,7 +32,7 @@ const Upgrade = propTypes({
<Icon icon='plan-upgrade' /> {_('upgradeNow')}
</a>{' '}
{_('or')}&nbsp;
<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>

View File

@@ -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')
)

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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')} />{' '}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>
)

View File

@@ -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 : '',

View File

@@ -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'

View File

@@ -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>

View File

@@ -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'
/>
)
}
}

View File

@@ -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)

View File

@@ -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>

View File

@@ -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>
)
}

View File

@@ -313,7 +313,7 @@ export default class Remotes extends Component {
)
? alert(
<span>
<Icon icon={'error'} /> {_('remoteTestName')}
<Icon icon='error' /> {_('remoteTestName')}
</span>,
<p>{_('remoteTestNameFailure')}</p>
)

View File

@@ -137,7 +137,7 @@ const COLUMNS = [
{
itemRenderer: server => (
<Toggle
value={server.allowUnauthorized}
value={Boolean(server.allowUnauthorized)}
onChange={allowUnauthorized =>
editServer(server, { allowUnauthorized })
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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'
/>
)
}
}

View File

@@ -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>
))}

View File

@@ -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>

1874
yarn.lock

File diff suppressed because it is too large Load Diff