Compare commits
123 Commits
xo-web/v5.
...
xo-web/v5.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d1e2c47d3 | ||
|
|
8b9b0346cb | ||
|
|
0d11817e3f | ||
|
|
a8cb209717 | ||
|
|
cf45ffddf1 | ||
|
|
2e0ea51c30 | ||
|
|
0f7f8c7330 | ||
|
|
808f72409f | ||
|
|
f8e2d29372 | ||
|
|
22dec27c65 | ||
|
|
89b3806a7a | ||
|
|
b6bedf9253 | ||
|
|
0d4983043b | ||
|
|
f9ff3fe168 | ||
|
|
4a25c5323f | ||
|
|
9b4e2d3bb8 | ||
|
|
3915efcf92 | ||
|
|
4591ff8522 | ||
|
|
e3491797f3 | ||
|
|
6eee167675 | ||
|
|
16b965b28a | ||
|
|
5125410efd | ||
|
|
1a4da2a8de | ||
|
|
991fbaec86 | ||
|
|
fb399278b3 | ||
|
|
b868092365 | ||
|
|
80fdc6849f | ||
|
|
25ffcb952b | ||
|
|
083ac1e2d6 | ||
|
|
5a4b553a60 | ||
|
|
b1135ef566 | ||
|
|
1928d1e00f | ||
|
|
a369f7f387 | ||
|
|
33d9801dfe | ||
|
|
8c7a031cca | ||
|
|
9484d87e76 | ||
|
|
4b6822d6e5 | ||
|
|
7241a0529b | ||
|
|
66083b4e50 | ||
|
|
f631b3cc64 | ||
|
|
bb58d9b4d6 | ||
|
|
93ebff1055 | ||
|
|
08aec1c09a | ||
|
|
8ca98a56fe | ||
|
|
705f53e3e5 | ||
|
|
adaf069d20 | ||
|
|
d7be7d8660 | ||
|
|
faddee86b6 | ||
|
|
c4fcc65d16 | ||
|
|
890631d33b | ||
|
|
8e8145bb48 | ||
|
|
d73d6719a5 | ||
|
|
3419bee198 | ||
|
|
4368fad393 | ||
|
|
ab93fdbf10 | ||
|
|
8fd7697a45 | ||
|
|
1121a60912 | ||
|
|
e7b4bd2fe4 | ||
|
|
fcd8bdd1b3 | ||
|
|
e6f140f575 | ||
|
|
bfe4c45fcf | ||
|
|
f95370124b | ||
|
|
2564343816 | ||
|
|
03734eb761 | ||
|
|
29d63a9fdd | ||
|
|
ca94b236a8 | ||
|
|
fa1ec30ba5 | ||
|
|
2b1423aebe | ||
|
|
373332141f | ||
|
|
ecf2cf15b5 | ||
|
|
4ee0831d93 | ||
|
|
7df2a88c13 | ||
|
|
3d52556c67 | ||
|
|
437b160a3f | ||
|
|
5c87b82e0c | ||
|
|
7f2bc79d5f | ||
|
|
837a61acf3 | ||
|
|
5971eed72a | ||
|
|
1b8224030b | ||
|
|
ed3ec3fa8b | ||
|
|
aa98ca49e5 | ||
|
|
44d35c2351 | ||
|
|
df8eb7a000 | ||
|
|
ac061c8750 | ||
|
|
656d3e55ac | ||
|
|
50641287f8 | ||
|
|
0bc072aa65 | ||
|
|
9d7d665520 | ||
|
|
819ea94e7b | ||
|
|
40753568df | ||
|
|
8793aed561 | ||
|
|
377a50bc09 | ||
|
|
fe5a43fbdf | ||
|
|
7f44220220 | ||
|
|
0df1610ca9 | ||
|
|
24c8b9e02d | ||
|
|
01b311f2ba | ||
|
|
a2bb3182f4 | ||
|
|
c86e15a310 | ||
|
|
862e5a95e7 | ||
|
|
73e2c7e849 | ||
|
|
0b0937e233 | ||
|
|
6bf114859f | ||
|
|
db6d67eeb7 | ||
|
|
a345d89aac | ||
|
|
e8f8ebb112 | ||
|
|
1dad5b5c3a | ||
|
|
5cc5ee4e87 | ||
|
|
e8d2b32a14 | ||
|
|
f492909e42 | ||
|
|
7ea17750a1 | ||
|
|
663e1f1a4b | ||
|
|
079310c67e | ||
|
|
5cf7f1f886 | ||
|
|
9f64af859e | ||
|
|
007aa776cb | ||
|
|
66bc092edd | ||
|
|
140a88ee12 | ||
|
|
f42758938d | ||
|
|
e19fd81536 | ||
|
|
73835ded96 | ||
|
|
1ec1a8bd94 | ||
|
|
f0b6d57ba8 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,5 +1,7 @@
|
||||
/dist/
|
||||
/node_modules/
|
||||
/src/common/intl/locales/index.js
|
||||
/src/common/themes/index.js
|
||||
|
||||
npm-debug.log
|
||||
npm-debug.log.*
|
||||
|
||||
81
CHANGELOG.md
81
CHANGELOG.md
@@ -1,5 +1,86 @@
|
||||
# ChangeLog
|
||||
|
||||
## **5.7.0** (2017-03-31)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Improve ActionButton error reporting [\#2048](https://github.com/vatesfr/xo-web/issues/2048)
|
||||
- Home view master checkbox UI issue [\#2027](https://github.com/vatesfr/xo-web/issues/2027)
|
||||
- HU Translation [\#2019](https://github.com/vatesfr/xo-web/issues/2019)
|
||||
- [Usage report] Add name for all objects [\#2017](https://github.com/vatesfr/xo-web/issues/2017)
|
||||
- [Home] Improve inter-types linkage [\#2012](https://github.com/vatesfr/xo-web/issues/2012)
|
||||
- Remove bootable checkboxes in VM creation [\#2007](https://github.com/vatesfr/xo-web/issues/2007)
|
||||
- Do not display bootable toggles for disks of non-PV VMs [\#1996](https://github.com/vatesfr/xo-web/issues/1996)
|
||||
- Try to match network VLAN for VM migration modal [\#1990](https://github.com/vatesfr/xo-web/issues/1990)
|
||||
- [Usage reports] Add VM names in addition to UUIDs [\#1984](https://github.com/vatesfr/xo-web/issues/1984)
|
||||
- Host affinity in "advanced" VM creation [\#1983](https://github.com/vatesfr/xo-web/issues/1983)
|
||||
- Add job tag in backup logs [\#1982](https://github.com/vatesfr/xo-web/issues/1982)
|
||||
- Possibility to add a label/description to servers [\#1965](https://github.com/vatesfr/xo-web/issues/1965)
|
||||
- Possibility to create shared VM in a resource set [\#1964](https://github.com/vatesfr/xo-web/issues/1964)
|
||||
- Clearer display of disabled (backup) jobs [\#1958](https://github.com/vatesfr/xo-web/issues/1958)
|
||||
- Job should have a configurable timeout [\#1956](https://github.com/vatesfr/xo-web/issues/1956)
|
||||
- Sort failed VMs in backup report [\#1950](https://github.com/vatesfr/xo-web/issues/1950)
|
||||
- Support for UNIX socket path [\#1944](https://github.com/vatesfr/xo-web/issues/1944)
|
||||
- Interface - Host Patching - Button Verbiage [\#1911](https://github.com/vatesfr/xo-web/issues/1911)
|
||||
- Display if a VM is in Self Service (and which group) [\#1905](https://github.com/vatesfr/xo-web/issues/1905)
|
||||
- Install supplemental pack on a whole pool [\#1896](https://github.com/vatesfr/xo-web/issues/1896)
|
||||
- Allow VM snapshots with ACLs [\#1865](https://github.com/vatesfr/xo-web/issues/1886)
|
||||
- Icon to indicate if a snapshot is quiesce [\#1858](https://github.com/vatesfr/xo-web/issues/1858)
|
||||
- Pool Ips input too permissive [\#1731](https://github.com/vatesfr/xo-web/issues/1731)
|
||||
- Select is going on top after each choice [\#1359](https://github.com/vatesfr/xo-web/issues/1359)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Missing objects should be displayed in backup edition [\#2052](https://github.com/vatesfr/xo-web/issues/2052)
|
||||
- Search bar content changes while typing [\#2035](https://github.com/vatesfr/xo-web/issues/2035)
|
||||
- VM.$guest_metrics.PV_drivers_up_to_date is deprecated in XS 7.1 [\#2024](https://github.com/vatesfr/xo-web/issues/2024)
|
||||
- Bootable flag selection checkbox for extra disk not fetched [\#1994](https://github.com/vatesfr/xo-web/issues/1994)
|
||||
- Home view − Changing type must reset paging [\#1993](https://github.com/vatesfr/xo-web/issues/1993)
|
||||
- XOSAN menu item should only be displayed to admins [\#1968](https://github.com/vatesfr/xo-web/issues/1968)
|
||||
- Object type change are not correctly handled in UI [\#1967](https://github.com/vatesfr/xo-web/issues/1967)
|
||||
- VM creation is stuck when using ISO/DVD as install method [\#1966](https://github.com/vatesfr/xo-web/issues/1966)
|
||||
- Install pack on whole pool fails [\#1957](https://github.com/vatesfr/xo-web/issues/1957)
|
||||
- Consoles are broken in next-release [\#1954](https://github.com/vatesfr/xo-web/issues/1954)
|
||||
- [VHD merge] Increase BAT when necessary [\#1939](https://github.com/vatesfr/xo-web/issues/1939)
|
||||
- Issue on VM restore time [\#1936](https://github.com/vatesfr/xo-web/issues/1936)
|
||||
- Two remotes should not be able to have the same name [\#1879](https://github.com/vatesfr/xo-web/issues/1879)
|
||||
- Selfservice limits not honored after VM creation [\#1695](https://github.com/vatesfr/xo-web/issues/1695)
|
||||
|
||||
## **5.6.0** (2017-01-27)
|
||||
|
||||
Reporting, LVM File level restore.
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Do not stop patches install if already applied [\#1904](https://github.com/vatesfr/xo-web/issues/1904)
|
||||
- Improve scheduling UI [\#1893](https://github.com/vatesfr/xo-web/issues/1893)
|
||||
- Smart backup and tag [\#1885](https://github.com/vatesfr/xo-web/issues/1885)
|
||||
- Missing embeded API documention [\#1882](https://github.com/vatesfr/xo-web/issues/1882)
|
||||
- Add local DVD in CD selector [\#1880](https://github.com/vatesfr/xo-web/issues/1880)
|
||||
- File level restore for LVM [\#1878](https://github.com/vatesfr/xo-web/issues/1878)
|
||||
- Restore multiple files from file level restore [\#1877](https://github.com/vatesfr/xo-web/issues/1877)
|
||||
- Add a VM tab for host & pool views [\#1864](https://github.com/vatesfr/xo-web/issues/1864)
|
||||
- Icon to indicate if a snapshot is quiesce [\#1858](https://github.com/vatesfr/xo-web/issues/1858)
|
||||
- UI for disconnect hosts comp [\#1833](https://github.com/vatesfr/xo-web/issues/1833)
|
||||
- Eject all xs-guest.iso in a pool [\#1798](https://github.com/vatesfr/xo-web/issues/1798)
|
||||
- Display installed supplemental pack on host [\#1506](https://github.com/vatesfr/xo-web/issues/1506)
|
||||
- Install supplemental pack on host comp [\#1460](https://github.com/vatesfr/xo-web/issues/1460)
|
||||
- Pool-wide combined stats [\#1324](https://github.com/vatesfr/xo-web/issues/1324)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- IP-address not released when VM removed [\#1906](https://github.com/vatesfr/xo-web/issues/1906)
|
||||
- Interface broken due to new Bootstrap Alpha [\#1871](https://github.com/vatesfr/xo-web/issues/1871)
|
||||
- Self service recompute all limits broken [\#1866](https://github.com/vatesfr/xo-web/issues/1866)
|
||||
- Patch not found error for XS 6.5 [\#1863](https://github.com/vatesfr/xo-web/issues/1863)
|
||||
- Convert To Template issues [\#1855](https://github.com/vatesfr/xo-web/issues/1855)
|
||||
- Removing PIF seems to fail [\#1853](https://github.com/vatesfr/xo-web/issues/1853)
|
||||
- Depth should be >= 1 in backup creation [\#1851](https://github.com/vatesfr/xo-web/issues/1851)
|
||||
- Wrong link in Dashboard > Health [\#1850](https://github.com/vatesfr/xo-web/issues/1850)
|
||||
- Incorrect file dates shown in new File Restore feature [\#1840](https://github.com/vatesfr/xo-web/issues/1840)
|
||||
- IP allocation problem [\#1747](https://github.com/vatesfr/xo-web/issues/1747)
|
||||
- Selfservice limits not honored after VM creation [\#1695](https://github.com/vatesfr/xo-web/issues/1695)
|
||||
|
||||
## **5.5.0** (2016-12-20)
|
||||
|
||||
File level restore.
|
||||
|
||||
33
package.json
33
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "xo-web",
|
||||
"version": "5.6.1",
|
||||
"version": "5.7.10",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Web interface client for Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -34,6 +34,7 @@
|
||||
"ansi_up": "^1.3.0",
|
||||
"asap": "^2.0.4",
|
||||
"babel-eslint": "^7.0.0",
|
||||
"babel-plugin-dev": "^1.0.0",
|
||||
"babel-plugin-lodash": "^3.2.11",
|
||||
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
||||
"babel-plugin-transform-react-constant-elements": "^6.5.0",
|
||||
@@ -67,7 +68,7 @@
|
||||
"globby": "^6.0.0",
|
||||
"gulp": "github:gulpjs/gulp#4.0",
|
||||
"gulp-autoprefixer": "^3.1.0",
|
||||
"gulp-csso": "^2.0.0",
|
||||
"gulp-csso": "^3.0.0",
|
||||
"gulp-embedlr": "^0.5.2",
|
||||
"gulp-plumber": "^1.1.0",
|
||||
"gulp-pug": "^3.1.0",
|
||||
@@ -77,10 +78,10 @@
|
||||
"gulp-uglify": "^2.0.0",
|
||||
"gulp-watch": "^4.3.5",
|
||||
"human-format": "^0.7.0",
|
||||
"husky": "^0.12.0",
|
||||
"index-modules": "^0.2.1",
|
||||
"husky": "^0.13.1",
|
||||
"index-modules": "^0.3.0",
|
||||
"is-ip": "^1.0.0",
|
||||
"jest": "^18.0.0",
|
||||
"jest": "^19.0.2",
|
||||
"jsonrpc-websocket-client": "^0.1.1",
|
||||
"kindof": "^2.0.0",
|
||||
"later": "^1.2.0",
|
||||
@@ -88,7 +89,7 @@
|
||||
"loose-envify": "^1.1.0",
|
||||
"make-error": "^1.2.1",
|
||||
"marked": "^0.3.5",
|
||||
"modular-css": "^3.0.0",
|
||||
"modular-css": "^4.1.1",
|
||||
"moment": "^2.13.0",
|
||||
"moment-timezone": "^0.5.4",
|
||||
"notifyjs": "^3.0.0",
|
||||
@@ -113,7 +114,7 @@
|
||||
"react-overlays": "^0.6.0",
|
||||
"react-redux": "^5.0.0",
|
||||
"react-router": "^3.0.0",
|
||||
"react-select": "^1.0.0-beta13",
|
||||
"react-select": "^1.0.0-rc.3",
|
||||
"react-shortcuts": "^1.3.1",
|
||||
"react-sparklines": "^1.5.0",
|
||||
"react-virtualized": "^8.0.8",
|
||||
@@ -124,9 +125,12 @@
|
||||
"redux-devtools-log-monitor": "^1.0.5",
|
||||
"redux-thunk": "^2.0.1",
|
||||
"reselect": "^2.2.1",
|
||||
"standard": "^8.4.0",
|
||||
"superagent": "^2.0.0",
|
||||
"semver": "^5.3.0",
|
||||
"standard": "^10.0.0",
|
||||
"styled-components": "^1.4.4",
|
||||
"superagent": "^3.5.0",
|
||||
"tar-stream": "^1.5.2",
|
||||
"uncontrollable-input": "^0.0.1",
|
||||
"vinyl": "^2.0.0",
|
||||
"watchify": "^3.7.0",
|
||||
"xml2js": "^0.4.17",
|
||||
@@ -139,6 +143,7 @@
|
||||
"benchmarks": "./tools/run-benchmarks.js 'src/**/*.bench.js'",
|
||||
"build": "npm run build-indexes && NODE_ENV=production gulp build",
|
||||
"build-indexes": "index-modules --auto src",
|
||||
"commitmsg": "npm test",
|
||||
"dev": "npm run build-indexes && NODE_ENV=development gulp build",
|
||||
"dev-test": "jest --watch",
|
||||
"lint": "standard",
|
||||
@@ -168,6 +173,8 @@
|
||||
}
|
||||
},
|
||||
"plugins": [
|
||||
"dev",
|
||||
"lodash",
|
||||
"transform-decorators-legacy",
|
||||
"transform-runtime"
|
||||
],
|
||||
@@ -177,17 +184,15 @@
|
||||
"stage-0"
|
||||
]
|
||||
},
|
||||
"config": {
|
||||
"ghooks": {
|
||||
"commit-msg": "npm test"
|
||||
}
|
||||
},
|
||||
"jest": {
|
||||
"snapshotSerializers": [
|
||||
"enzyme-to-json/serializer"
|
||||
]
|
||||
},
|
||||
"standard": {
|
||||
"globals": [
|
||||
"__DEV__"
|
||||
],
|
||||
"ignore": [
|
||||
"dist"
|
||||
],
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
exports[`test Col 1`] = `
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Col 1`] = `
|
||||
<div
|
||||
className="col-xs-12" />
|
||||
className="col-xs-12"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`test Container 1`] = `
|
||||
exports[`Container 1`] = `
|
||||
<div
|
||||
className="container-fluid" />
|
||||
className="container-fluid"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`test Row 1`] = `
|
||||
exports[`Row 1`] = `
|
||||
<div
|
||||
className=" row" />
|
||||
className=" row"
|
||||
/>
|
||||
`;
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import _ from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import Tooltip from 'tooltip'
|
||||
import {
|
||||
ButtonGroup
|
||||
} from 'react-bootstrap-4/lib'
|
||||
import {
|
||||
noop
|
||||
} from 'utils'
|
||||
import { map, noop } from 'lodash'
|
||||
|
||||
import _ from './intl'
|
||||
import ActionButton from './action-button'
|
||||
import ButtonGroup from './button-group'
|
||||
|
||||
const ActionBar = ({ actions, param }) => (
|
||||
<ButtonGroup>
|
||||
@@ -17,18 +12,24 @@ const ActionBar = ({ actions, param }) => (
|
||||
return
|
||||
}
|
||||
|
||||
const { handler, handlerParam = param, label, icon, redirectOnSuccess } = button
|
||||
return <Tooltip key={index} content={_(label)}>
|
||||
<ActionButton
|
||||
key={index}
|
||||
btnStyle='secondary'
|
||||
handler={handler || noop}
|
||||
handlerParam={handlerParam}
|
||||
icon={icon}
|
||||
redirectOnSuccess={redirectOnSuccess}
|
||||
size='large'
|
||||
/>
|
||||
</Tooltip>
|
||||
const {
|
||||
handler,
|
||||
handlerParam = param,
|
||||
icon,
|
||||
label,
|
||||
pending,
|
||||
redirectOnSuccess
|
||||
} = button
|
||||
return <ActionButton
|
||||
key={index}
|
||||
handler={handler || noop}
|
||||
handlerParam={handlerParam}
|
||||
icon={icon}
|
||||
pending={pending}
|
||||
redirectOnSuccess={redirectOnSuccess}
|
||||
size='large'
|
||||
tooltip={_(label)}
|
||||
/>
|
||||
})}
|
||||
</ButtonGroup>
|
||||
)
|
||||
|
||||
@@ -1,59 +1,82 @@
|
||||
import Icon from 'icon'
|
||||
import isFunction from 'lodash/isFunction'
|
||||
import React from 'react'
|
||||
import { Button } from 'react-bootstrap-4/lib'
|
||||
|
||||
import Button from './button'
|
||||
import Component from './base-component'
|
||||
import Icon from './icon'
|
||||
import logError from './log-error'
|
||||
import propTypes from './prop-types'
|
||||
import Tooltip from './tooltip'
|
||||
import { error as _error } from './notification'
|
||||
|
||||
@propTypes({
|
||||
btnStyle: propTypes.string,
|
||||
// React element to use as button content
|
||||
children: propTypes.node,
|
||||
|
||||
// whether this button is disabled (default to false)
|
||||
disabled: propTypes.bool,
|
||||
|
||||
// form identifier
|
||||
//
|
||||
// if provided, this button and its action are associated to this
|
||||
// form for the submit event
|
||||
form: propTypes.string,
|
||||
|
||||
// function to call when the action is triggered (via a clik on the
|
||||
// button or submit on the form)
|
||||
handler: propTypes.func.isRequired,
|
||||
|
||||
// optional value which will be passed as first param to the handler
|
||||
handlerParam: propTypes.any,
|
||||
|
||||
// XO icon to use for this button
|
||||
icon: propTypes.string.isRequired,
|
||||
|
||||
// whether the action of this action is already underway
|
||||
pending: propTypes.bool,
|
||||
|
||||
// path to redirect to when the triggered action finish successfully
|
||||
//
|
||||
// if a function, it will be called with the result of the action to
|
||||
// compute the path
|
||||
redirectOnSuccess: propTypes.oneOfType([
|
||||
propTypes.func,
|
||||
propTypes.string
|
||||
]),
|
||||
size: propTypes.oneOf([
|
||||
'large',
|
||||
'small'
|
||||
]),
|
||||
|
||||
// React element to use tooltip for the component
|
||||
tooltip: propTypes.node
|
||||
})
|
||||
export default class ActionButton extends Component {
|
||||
static contextTypes = {
|
||||
router: React.PropTypes.object
|
||||
router: propTypes.object
|
||||
}
|
||||
|
||||
async _execute () {
|
||||
if (this.state.working) {
|
||||
if (this.props.pending || this.state.working) {
|
||||
return
|
||||
}
|
||||
|
||||
const {
|
||||
children,
|
||||
handler,
|
||||
handlerParam
|
||||
handlerParam,
|
||||
tooltip
|
||||
} = this.props
|
||||
|
||||
try {
|
||||
this.setState({
|
||||
error: null,
|
||||
error: undefined,
|
||||
working: true
|
||||
})
|
||||
|
||||
const result = await handler(handlerParam)
|
||||
|
||||
let { redirectOnSuccess } = this.props
|
||||
const { redirectOnSuccess } = this.props
|
||||
if (redirectOnSuccess) {
|
||||
if (isFunction(redirectOnSuccess)) {
|
||||
redirectOnSuccess = redirectOnSuccess(result)
|
||||
}
|
||||
return this.context.router.push(redirectOnSuccess)
|
||||
return this.context.router.push(
|
||||
isFunction(redirectOnSuccess) ? redirectOnSuccess(result) : redirectOnSuccess
|
||||
)
|
||||
}
|
||||
|
||||
this.setState({
|
||||
@@ -68,6 +91,7 @@ export default class ActionButton extends Component {
|
||||
// ignore when undefined because it usually means that the action has been canceled
|
||||
if (error !== undefined) {
|
||||
logError(error)
|
||||
_error(children || tooltip || error.name, error.message || String(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,28 +121,30 @@ export default class ActionButton extends Component {
|
||||
render () {
|
||||
const {
|
||||
props: {
|
||||
btnStyle,
|
||||
children,
|
||||
className,
|
||||
disabled,
|
||||
form,
|
||||
icon,
|
||||
size: bsSize,
|
||||
style,
|
||||
tooltip
|
||||
pending,
|
||||
tooltip,
|
||||
...props
|
||||
},
|
||||
state: { error, working }
|
||||
} = this
|
||||
|
||||
const button = <Button
|
||||
bsStyle={error ? 'warning' : btnStyle}
|
||||
form={form}
|
||||
onClick={!form && this._execute}
|
||||
disabled={working || disabled}
|
||||
type={form ? 'submit' : 'button'}
|
||||
{...{ bsSize, className, style }}
|
||||
>
|
||||
<Icon icon={working ? 'loading' : icon} fixedWidth />
|
||||
if (error !== undefined) {
|
||||
props.btnStyle = 'warning'
|
||||
}
|
||||
if (pending || working) {
|
||||
props.disabled = true
|
||||
}
|
||||
delete props.handler
|
||||
delete props.handlerParam
|
||||
if (props.form === undefined) {
|
||||
props.onClick = this._execute
|
||||
}
|
||||
delete props.redirectOnSuccess
|
||||
|
||||
const button = <Button {...props}>
|
||||
<Icon icon={pending || working ? 'loading' : icon} fixedWidth />
|
||||
{children && ' '}
|
||||
{children}
|
||||
</Button>
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
import React from 'react'
|
||||
import { isFunction, omit } from 'lodash'
|
||||
|
||||
import Component from './base-component'
|
||||
import getEventValue from './get-event-value'
|
||||
|
||||
const __DEV__ = process.env.NODE_ENV !== 'production'
|
||||
|
||||
// This decorator can be used on a controlled input component to make
|
||||
// it able to automatically handled the uncontrolled mode.
|
||||
export default options => ControlledInput => {
|
||||
class AutoControlledInput extends Component {
|
||||
constructor (props) {
|
||||
super()
|
||||
|
||||
const opts = isFunction(options) ? options(props) : options
|
||||
const controlled = this._controlled = 'value' in props
|
||||
if (!controlled) {
|
||||
this.state.value = props.defaultValue || opts && opts.defaultValue
|
||||
|
||||
this._onChange = event => {
|
||||
let defaultPrevented = false
|
||||
|
||||
const { onChange } = this.props
|
||||
if (onChange) {
|
||||
onChange(event)
|
||||
defaultPrevented = event && event.defaultPrevented
|
||||
}
|
||||
|
||||
if (!defaultPrevented) {
|
||||
this.setState({ value: getEventValue(event) })
|
||||
}
|
||||
}
|
||||
} else if (__DEV__ && 'defaultValue' in props) {
|
||||
throw new Error(`${this.constructor.name}: controlled component should not have a default value`)
|
||||
}
|
||||
}
|
||||
|
||||
get value () {
|
||||
return this._controlled
|
||||
? this.props.value
|
||||
: this.state.value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
if (__DEV__ && this._controlled) {
|
||||
throw new Error(`${this.constructor.name}: should not set value on controlled component`)
|
||||
}
|
||||
|
||||
this.setState({ value })
|
||||
}
|
||||
|
||||
render () {
|
||||
if (this._controlled) {
|
||||
return <ControlledInput {...this.props} />
|
||||
}
|
||||
|
||||
return <ControlledInput
|
||||
{...omit(this.props, 'defaultValue')}
|
||||
onChange={this._onChange}
|
||||
value={this.state.value}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
AutoControlledInput.prototype.componentWillReceiveProps = function (newProps) {
|
||||
const { name } = this.constructor
|
||||
const controlled = this._controlled
|
||||
const newControlled = 'value' in newProps
|
||||
|
||||
if (!controlled) {
|
||||
if (newControlled) {
|
||||
throw new Error(`${name}: uncontrolled component should not become controlled`)
|
||||
}
|
||||
} else if (!newControlled) {
|
||||
throw new Error(`${name}: controlled component should not become uncontrolled`)
|
||||
}
|
||||
|
||||
if (newProps.defaultValue !== this.props.defaultValue) {
|
||||
throw new Error(`${name}: default value should not change`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return AutoControlledInput
|
||||
}
|
||||
@@ -3,11 +3,9 @@ import includes from 'lodash/includes'
|
||||
import isArray from 'lodash/isArray'
|
||||
import forEach from 'lodash/forEach'
|
||||
import map from 'lodash/map'
|
||||
import { Component } from 'react'
|
||||
import { PureComponent } from 'react'
|
||||
|
||||
import getEventValue from './get-event-value'
|
||||
import invoke from './invoke'
|
||||
import shallowEqual from './shallow-equal'
|
||||
|
||||
// Should components logs every renders?
|
||||
//
|
||||
@@ -19,7 +17,7 @@ const cowSet = (object, path, value, depth) => {
|
||||
return value
|
||||
}
|
||||
|
||||
object = clone(object)
|
||||
object = object != null ? clone(object) : {}
|
||||
const prop = path[depth]
|
||||
object[prop] = cowSet(object[prop], path, value, depth + 1)
|
||||
return object
|
||||
@@ -36,7 +34,7 @@ const get = (object, path, depth) => {
|
||||
: get(object[prop], path, depth)
|
||||
}
|
||||
|
||||
export default class BaseComponent extends Component {
|
||||
export default class BaseComponent extends PureComponent {
|
||||
constructor (props, context) {
|
||||
super(props, context)
|
||||
|
||||
@@ -46,11 +44,11 @@ export default class BaseComponent extends Component {
|
||||
this._linkedState = null
|
||||
|
||||
if (VERBOSE) {
|
||||
this.render = invoke(this.render, render => () => {
|
||||
this.render = (render => () => {
|
||||
console.log('render', this.constructor.name)
|
||||
|
||||
return render.call(this)
|
||||
})
|
||||
})(this.render)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,13 +110,6 @@ export default class BaseComponent extends Component {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
shouldComponentUpdate (newProps, newState) {
|
||||
return !(
|
||||
shallowEqual(this.props, newProps) &&
|
||||
shallowEqual(this.state, newState)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (VERBOSE) {
|
||||
|
||||
8
src/common/button-group.js
Normal file
8
src/common/button-group.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import React from 'react'
|
||||
|
||||
const ButtonGroup = ({ children }) =>
|
||||
<div className='btn-group' role='group'>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
export { ButtonGroup as default }
|
||||
56
src/common/button.js
Normal file
56
src/common/button.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import classNames from 'classnames'
|
||||
import React from 'react'
|
||||
|
||||
import propTypes from './prop-types'
|
||||
|
||||
const Button = ({
|
||||
active,
|
||||
block,
|
||||
btnStyle = 'secondary',
|
||||
children,
|
||||
outline,
|
||||
size,
|
||||
...props
|
||||
}) => {
|
||||
props.className = classNames(
|
||||
props.className,
|
||||
'btn',
|
||||
`btn${outline ? '-outline' : ''}-${btnStyle}`,
|
||||
active !== undefined && 'active',
|
||||
block && 'btn-block',
|
||||
size === 'large' ? 'btn-lg' : size === 'small' ? 'btn-sm' : null
|
||||
)
|
||||
if (props.type === undefined && props.form === undefined) {
|
||||
props.type = 'button'
|
||||
}
|
||||
|
||||
return <button {...props}>{children}</button>
|
||||
}
|
||||
|
||||
propTypes(Button)({
|
||||
active: propTypes.bool,
|
||||
block: propTypes.bool,
|
||||
|
||||
// Bootstrap button style
|
||||
//
|
||||
// See https://v4-alpha.getbootstrap.com/components/buttons/#examples
|
||||
//
|
||||
// The default value (secondary) is not listed here because it does
|
||||
// not make sense to explicit it.
|
||||
btnStyle: propTypes.oneOf([
|
||||
'danger',
|
||||
'info',
|
||||
'link',
|
||||
'primary',
|
||||
'success',
|
||||
'warning'
|
||||
]),
|
||||
|
||||
outline: propTypes.bool,
|
||||
size: propTypes.oneOf([
|
||||
'large',
|
||||
'small'
|
||||
])
|
||||
})
|
||||
|
||||
export { Button as default }
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react'
|
||||
|
||||
import Button from './button'
|
||||
import Component from './base-component'
|
||||
import Icon from './icon'
|
||||
import propTypes from './prop-types'
|
||||
@@ -27,9 +28,9 @@ export default class Collapse extends Component {
|
||||
|
||||
return (
|
||||
<div className={props.className}>
|
||||
<button className='btn btn-lg btn-primary btn-block' onClick={this._onClick}>
|
||||
<Button block btnStyle='primary' size='large' onClick={this._onClick}>
|
||||
{props.buttonText} <Icon icon={`chevron-${isOpened ? 'up' : 'down'}`} />
|
||||
</button>
|
||||
</Button>
|
||||
{isOpened && props.children}
|
||||
</div>
|
||||
)
|
||||
|
||||
64
src/common/combobox.js
Normal file
64
src/common/combobox.js
Normal file
@@ -0,0 +1,64 @@
|
||||
import React from 'react'
|
||||
import uncontrollableInput from 'uncontrollable-input'
|
||||
import { isEmpty, map } from 'lodash'
|
||||
import {
|
||||
DropdownButton,
|
||||
MenuItem
|
||||
} from 'react-bootstrap-4/lib'
|
||||
|
||||
import Component from './base-component'
|
||||
import propTypes from './prop-types'
|
||||
|
||||
@uncontrollableInput({
|
||||
defaultValue: ''
|
||||
})
|
||||
@propTypes({
|
||||
disabled: propTypes.bool,
|
||||
options: propTypes.oneOfType([
|
||||
propTypes.arrayOf(propTypes.string),
|
||||
propTypes.objectOf(propTypes.string)
|
||||
]),
|
||||
onChange: propTypes.func.isRequired,
|
||||
value: propTypes.string.isRequired
|
||||
})
|
||||
export default class Combobox extends Component {
|
||||
_handleChange = event => {
|
||||
this.props.onChange(event.target.value)
|
||||
}
|
||||
|
||||
_setText (value) {
|
||||
this.props.onChange(value)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { options, ...props } = this.props
|
||||
|
||||
props.className = 'form-control'
|
||||
props.onChange = this._handleChange
|
||||
const Input = <input {...props} />
|
||||
|
||||
if (isEmpty(options)) {
|
||||
return Input
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='input-group'>
|
||||
<div className='input-group-btn'>
|
||||
<DropdownButton
|
||||
bsStyle='secondary'
|
||||
disabled={props.disabled}
|
||||
id='selectInput'
|
||||
title=''
|
||||
>
|
||||
{map(options, option =>
|
||||
<MenuItem key={option} onClick={() => this._setText(option)}>
|
||||
{option}
|
||||
</MenuItem>
|
||||
)}
|
||||
</DropdownButton>
|
||||
</div>
|
||||
{Input}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
.button {
|
||||
border-radius: 0px;
|
||||
};
|
||||
@@ -1,105 +0,0 @@
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import size from 'lodash/size'
|
||||
|
||||
import Component from '../base-component'
|
||||
import propTypes from '../prop-types'
|
||||
import { ensureArray } from '../utils'
|
||||
import {
|
||||
DropdownButton,
|
||||
MenuItem
|
||||
} from 'react-bootstrap-4/lib'
|
||||
|
||||
import styles from './index.css'
|
||||
|
||||
@propTypes({
|
||||
defaultValue: propTypes.any,
|
||||
disabled: propTypes.bool,
|
||||
max: propTypes.number,
|
||||
min: propTypes.number,
|
||||
options: propTypes.oneOfType([
|
||||
propTypes.arrayOf(propTypes.string),
|
||||
propTypes.number,
|
||||
propTypes.objectOf(propTypes.string),
|
||||
propTypes.string
|
||||
]),
|
||||
onChange: propTypes.func,
|
||||
placeholder: propTypes.string,
|
||||
required: propTypes.bool,
|
||||
step: propTypes.any,
|
||||
type: propTypes.string,
|
||||
value: propTypes.any
|
||||
})
|
||||
export default class Combobox extends Component {
|
||||
static defaultProps = {
|
||||
type: 'text'
|
||||
}
|
||||
|
||||
get value () {
|
||||
return this.refs.input.value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.refs.input.value = value
|
||||
}
|
||||
|
||||
_handleChange = event => {
|
||||
const { onChange } = this.props
|
||||
|
||||
if (onChange) {
|
||||
onChange(event.target.value)
|
||||
}
|
||||
}
|
||||
|
||||
_setText (value) {
|
||||
this.refs.input.value = value
|
||||
}
|
||||
|
||||
render () {
|
||||
const { props } = this
|
||||
const options = ensureArray(props.options)
|
||||
|
||||
const Input = (
|
||||
<input
|
||||
className='form-control'
|
||||
defaultValue={props.defaultValue}
|
||||
disabled={props.disabled}
|
||||
max={props.max}
|
||||
min={props.min}
|
||||
options={options}
|
||||
onChange={this._handleChange}
|
||||
placeholder={props.placeholder}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
step={props.step}
|
||||
type={props.type}
|
||||
value={props.value}
|
||||
/>
|
||||
)
|
||||
|
||||
if (!size(options)) {
|
||||
return Input
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='input-group'>
|
||||
<div className='input-group-btn'>
|
||||
<DropdownButton
|
||||
bsStyle='secondary'
|
||||
className={styles.button}
|
||||
disabled={props.disabled}
|
||||
id='selectInput'
|
||||
title=''
|
||||
>
|
||||
{map(options, option => (
|
||||
<MenuItem key={option} onClick={() => this._setText(option)}>
|
||||
{option}
|
||||
</MenuItem>
|
||||
))}
|
||||
</DropdownButton>
|
||||
</div>
|
||||
{Input}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -294,10 +294,10 @@ export const getPropertyClausesStrings = function () {
|
||||
|
||||
export const removePropertyClause = function (name) {
|
||||
let type
|
||||
if (
|
||||
!this ||
|
||||
(type = this.type) === 'property' && this.name === name
|
||||
) {
|
||||
if (!this || (
|
||||
(type = this.type) === 'property' &&
|
||||
this.name === name
|
||||
)) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -335,7 +335,7 @@ export const setPropertyClause = function (name, child) {
|
||||
return _addAndClause(
|
||||
this,
|
||||
property,
|
||||
node => node.type === 'property' && node.name === name,
|
||||
node => node.type === 'property' && node.name === name
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import _ from 'intl'
|
||||
import CopyToClipboard from 'react-copy-to-clipboard'
|
||||
import classNames from 'classnames'
|
||||
import Tooltip from 'tooltip'
|
||||
import React, { createElement } from 'react'
|
||||
|
||||
import _ from '../intl'
|
||||
import Button from '../button'
|
||||
import Icon from '../icon'
|
||||
import propTypes from '../prop-types'
|
||||
import Tooltip from '../tooltip'
|
||||
|
||||
import styles from './index.css'
|
||||
|
||||
@@ -22,9 +23,9 @@ const Copiable = propTypes({
|
||||
' ',
|
||||
<Tooltip content={_('copyToClipboard')}>
|
||||
<CopyToClipboard text={props.data || props.children}>
|
||||
<button className={classNames('btn btn-sm btn-secondary', styles.button)}>
|
||||
<Button className={styles.button} size='small'>
|
||||
<Icon icon='clipboard' />
|
||||
</button>
|
||||
</Button>
|
||||
</CopyToClipboard>
|
||||
</Tooltip>
|
||||
))
|
||||
|
||||
@@ -394,7 +394,7 @@ const MAP_TYPE_SELECT = {
|
||||
value: propTypes.oneOfType([
|
||||
propTypes.string,
|
||||
propTypes.object
|
||||
]).isRequired
|
||||
])
|
||||
})
|
||||
export class XoSelect extends Editable {
|
||||
get value () {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
MenuItem
|
||||
} from 'react-bootstrap-4/lib'
|
||||
|
||||
import Button from '../button'
|
||||
import Component from '../base-component'
|
||||
import getEventValue from '../get-event-value'
|
||||
import propTypes from '../prop-types'
|
||||
@@ -70,9 +71,9 @@ export class Password extends Component {
|
||||
|
||||
return <div className='input-group'>
|
||||
{enableGenerator && <span className='input-group-btn'>
|
||||
<button type='button' className='btn btn-secondary' onClick={this._generate}>
|
||||
<Button onClick={this._generate}>
|
||||
<Icon icon='password' />
|
||||
</button>
|
||||
</Button>
|
||||
</span>}
|
||||
<input
|
||||
{...props}
|
||||
@@ -81,9 +82,9 @@ export class Password extends Component {
|
||||
type={visible ? 'text' : 'password'}
|
||||
/>
|
||||
<span className='input-group-btn'>
|
||||
<button type='button' className='btn btn-secondary' onClick={this._toggleVisibility}>
|
||||
<Button onClick={this._toggleVisibility}>
|
||||
<Icon icon={visible ? 'shown' : 'hidden'} />
|
||||
</button>
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import autoControlledInput from 'auto-controlled-input'
|
||||
import uncontrollableInput from 'uncontrollable-input'
|
||||
import Component from 'base-component'
|
||||
import find from 'lodash/find'
|
||||
import map from 'lodash/map'
|
||||
@@ -20,7 +20,7 @@ import Select from './select'
|
||||
required: propTypes.bool,
|
||||
value: propTypes.any
|
||||
})
|
||||
@autoControlledInput()
|
||||
@uncontrollableInput()
|
||||
export default class SelectPlainObject extends Component {
|
||||
componentDidMount () {
|
||||
const { options, value } = this.props
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react'
|
||||
import classNames from 'classnames'
|
||||
import uncontrollableInput from 'uncontrollable-input'
|
||||
|
||||
import Component from '../../base-component'
|
||||
import Icon from '../../icon'
|
||||
@@ -7,9 +8,9 @@ import propTypes from '../../prop-types'
|
||||
|
||||
import styles from './index.css'
|
||||
|
||||
@uncontrollableInput()
|
||||
@propTypes({
|
||||
className: propTypes.string,
|
||||
defaultValue: propTypes.bool,
|
||||
onChange: propTypes.func,
|
||||
icon: propTypes.string,
|
||||
iconOn: propTypes.string,
|
||||
@@ -24,64 +25,27 @@ export default class Toggle extends Component {
|
||||
iconSize: 2
|
||||
}
|
||||
|
||||
get value () {
|
||||
const { props } = this
|
||||
|
||||
const { value } = props
|
||||
if (value != null) {
|
||||
return value
|
||||
}
|
||||
|
||||
const { input } = this.refs
|
||||
if (input) {
|
||||
return input.checked
|
||||
}
|
||||
|
||||
return props.defaultValue || false
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
if (
|
||||
process.env.NODE_ENV !== 'production' &&
|
||||
this.props.value != null
|
||||
) {
|
||||
throw new Error('cannot set value of controlled Toggle')
|
||||
}
|
||||
|
||||
this.refs.input.checked = Boolean(value)
|
||||
this.forceUpdate()
|
||||
}
|
||||
|
||||
_onChange = event => {
|
||||
if (this.props.value == null) {
|
||||
this.forceUpdate()
|
||||
}
|
||||
|
||||
const { onChange } = this.props
|
||||
onChange && onChange(event.target.checked)
|
||||
}
|
||||
_onChange = event => this.props.onChange(event.target.checked)
|
||||
|
||||
render () {
|
||||
const { props, value } = this
|
||||
const { props } = this
|
||||
|
||||
return (
|
||||
<label
|
||||
className={classNames(
|
||||
props.disabled ? 'text-muted' : value ? 'text-success' : null,
|
||||
props.disabled ? 'text-muted' : props.value ? 'text-success' : null,
|
||||
props.className
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
icon={props.icon || (value ? props.iconOn : props.iconOff)}
|
||||
icon={props.icon || (props.value ? props.iconOn : props.iconOff)}
|
||||
size={props.iconSize}
|
||||
/>
|
||||
<input
|
||||
checked={props.value}
|
||||
checked={props.value || false}
|
||||
className={styles.checkbox}
|
||||
defaultChecked={props.defaultValue}
|
||||
disabled={props.disabled}
|
||||
onChange={this._onChange}
|
||||
ref='input'
|
||||
type='checkbox'
|
||||
/>
|
||||
</label>
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
const common = {
|
||||
homeFilterNone: ''
|
||||
}
|
||||
|
||||
export const VM = {
|
||||
...common,
|
||||
homeFilterPendingVms: 'current_operations:"" ',
|
||||
homeFilterNonRunningVms: '!power_state:running ',
|
||||
homeFilterHvmGuests: 'virtualizationMode:hvm ',
|
||||
@@ -7,18 +12,22 @@ export const VM = {
|
||||
}
|
||||
|
||||
export const host = {
|
||||
...common,
|
||||
homeFilterRunningHosts: 'power_state:running ',
|
||||
homeFilterTags: 'tags:'
|
||||
}
|
||||
|
||||
export const pool = {
|
||||
...common,
|
||||
homeFilterTags: 'tags:'
|
||||
}
|
||||
|
||||
export const vmTemplate = {
|
||||
...common,
|
||||
homeFilterTags: 'tags:'
|
||||
}
|
||||
|
||||
export const SR = {
|
||||
...common,
|
||||
homeFilterTags: 'tags:'
|
||||
}
|
||||
|
||||
@@ -64,6 +64,15 @@ const POOLS_MISSING_PATCHES_COLUMNS = [{
|
||||
sortCriteria: (host, { pools }) => pools[host.$pool].name_label
|
||||
}].concat(MISSING_PATCHES_COLUMNS)
|
||||
|
||||
// Small component to homogenize Button usage in HostsPatchesTable
|
||||
const ActionButton_ = ({ children, labelId, ...props }) =>
|
||||
<ActionButton
|
||||
{...props}
|
||||
tooltip={_(labelId)}
|
||||
>
|
||||
{children}
|
||||
</ActionButton>
|
||||
|
||||
// ===================================================================
|
||||
|
||||
class HostsPatchesTable extends Component {
|
||||
@@ -150,15 +159,17 @@ class HostsPatchesTable extends Component {
|
||||
const { props } = this
|
||||
|
||||
const Container = props.container || 'div'
|
||||
const Button = props.useTabButton ? TabButton : ActionButton
|
||||
|
||||
const Button = this.props.useTabButton
|
||||
? TabButton
|
||||
: ActionButton_
|
||||
|
||||
const Buttons = (
|
||||
<Container>
|
||||
<Button
|
||||
btnStyle='secondary'
|
||||
handler={this._refreshMissingPatches}
|
||||
icon='refresh'
|
||||
labelId='refreshPatches'
|
||||
labelId='checkForUpdates'
|
||||
/>
|
||||
<Button
|
||||
btnStyle='primary'
|
||||
|
||||
1
src/common/intl/locales/.gitignore
vendored
1
src/common/intl/locales/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
/index.js
|
||||
3592
src/common/intl/locales/hu.js
Normal file
3592
src/common/intl/locales/hu.js
Normal file
File diff suppressed because it is too large
Load Diff
3139
src/common/intl/locales/pl.js
Normal file
3139
src/common/intl/locales/pl.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -23,6 +23,8 @@ var messages = {
|
||||
// ----- Filters -----
|
||||
onError: 'On error',
|
||||
successful: 'Successful',
|
||||
filterNoSnapshots: 'Full disks only',
|
||||
filterOnlySnapshots: 'Snapshots only',
|
||||
|
||||
// ----- Copiable component -----
|
||||
copyToClipboard: 'Copy to clipboard',
|
||||
@@ -123,6 +125,7 @@ var messages = {
|
||||
homeAllHosts: 'Hosts',
|
||||
homeAllTags: 'Tags',
|
||||
homeNewVm: 'New VM',
|
||||
homeFilterNone: 'None',
|
||||
homeFilterRunningHosts: 'Running hosts',
|
||||
homeFilterDisabledHosts: 'Disabled hosts',
|
||||
homeFilterRunningVms: 'Running VMs',
|
||||
@@ -213,7 +216,9 @@ var messages = {
|
||||
backupEditNotFoundTitle: 'Cannot edit backup',
|
||||
backupEditNotFoundMessage: 'Missing required info for edition',
|
||||
job: 'Job',
|
||||
jobId: 'Job ID',
|
||||
jobModalTitle: 'Job {job}',
|
||||
jobId: 'ID',
|
||||
jobType: 'Type',
|
||||
jobName: 'Name',
|
||||
jobNamePlaceholder: 'Name of your job (forbidden: "_")',
|
||||
jobStart: 'Start',
|
||||
@@ -224,6 +229,8 @@ var messages = {
|
||||
jobTag: 'Tag',
|
||||
jobScheduling: 'Scheduling',
|
||||
jobState: 'State',
|
||||
jobStateEnabled: 'Enabled',
|
||||
jobStateDisabled: 'Disabled',
|
||||
jobTimezone: 'Timezone',
|
||||
jobServerTimezone: 'Server',
|
||||
runJob: 'Run job',
|
||||
@@ -240,6 +247,7 @@ var messages = {
|
||||
noJobs: 'No jobs found.',
|
||||
noSchedules: 'No schedules found',
|
||||
jobActionPlaceHolder: 'Select a xo-server API command',
|
||||
jobTimeoutPlaceHolder: ' Timeout (number of seconds after which a VM is considered failed)',
|
||||
jobSchedules: 'Schedules',
|
||||
jobScheduleNamePlaceHolder: 'Name of your schedule',
|
||||
jobScheduleJobPlaceHolder: 'Select a Job',
|
||||
@@ -283,7 +291,10 @@ var messages = {
|
||||
remoteTestError: 'Error',
|
||||
remoteTestStep: 'Test Step',
|
||||
remoteTestFile: 'Test file',
|
||||
remoteTestName: 'Test name',
|
||||
remoteTestNameFailure: 'Remote name already exists!',
|
||||
remoteTestSuccessMessage: 'The remote appears to work correctly',
|
||||
remoteConnectionFailed: 'Connection failed',
|
||||
|
||||
// ------ Remote -----
|
||||
remoteName: 'Name',
|
||||
@@ -291,11 +302,14 @@ var messages = {
|
||||
remoteState: 'State',
|
||||
remoteDevice: 'Device',
|
||||
remoteShare: 'Share',
|
||||
remoteAction: 'Action',
|
||||
remoteAuth: 'Auth',
|
||||
remoteMounted: 'Mounted',
|
||||
remoteUnmounted: 'Unmounted',
|
||||
remoteConnectTip: 'Connect',
|
||||
remoteDisconnectTip: 'Disconnect',
|
||||
remoteConnected: 'Connected',
|
||||
remoteDisconnected: 'Disconnected',
|
||||
remoteDeleteTip: 'Delete',
|
||||
remoteNamePlaceHolder: 'remote name *',
|
||||
remoteMyNamePlaceHolder: 'Name *',
|
||||
@@ -443,6 +457,8 @@ var messages = {
|
||||
displayAllVMs: 'Display all VMs of this pool',
|
||||
// ----- Pool tabs -----
|
||||
hostsTabName: 'Hosts',
|
||||
vmsTabName: 'Vms',
|
||||
srsTabName: 'Srs',
|
||||
// ----- Pool advanced tab -----
|
||||
poolHaStatus: 'High Availability',
|
||||
poolHaEnabled: 'Enabled',
|
||||
@@ -540,6 +556,7 @@ var messages = {
|
||||
pifStatusDisconnected: 'Disconnected',
|
||||
pifNoInterface: 'No physical interface detected',
|
||||
pifInUse: 'This interface is currently in use',
|
||||
pifAction: 'Action',
|
||||
defaultLockingMode: 'Default locking mode',
|
||||
pifConfigureIp: 'Configure IP address',
|
||||
configIpErrorTitle: 'Invalid parameters',
|
||||
@@ -552,6 +569,7 @@ var messages = {
|
||||
addSrDeviceButton: 'Add a storage',
|
||||
srNameLabel: 'Name',
|
||||
srType: 'Type',
|
||||
pbdAction: 'Action',
|
||||
pbdStatus: 'Status',
|
||||
pbdStatusConnected: 'Connected',
|
||||
pbdStatusDisconnected: 'Disconnected',
|
||||
@@ -580,6 +598,7 @@ var messages = {
|
||||
// ----- Pool patch tabs -----
|
||||
refreshPatches: 'Refresh patches',
|
||||
installPoolPatches: 'Install pool patches',
|
||||
checkForUpdates: 'Check for updates',
|
||||
// ----- Pool storage tabs -----
|
||||
defaultSr: 'Default SR',
|
||||
setAsDefaultSr: 'Set as default SR',
|
||||
@@ -674,9 +693,12 @@ var messages = {
|
||||
vbdDisconnect: 'Disconnect VBD',
|
||||
vdbBootable: 'Bootable',
|
||||
vdbReadonly: 'Readonly',
|
||||
vbdAction: 'Action',
|
||||
vdbCreate: 'Create',
|
||||
vdbNamePlaceHolder: 'Disk name',
|
||||
vdbSizePlaceHolder: 'Size',
|
||||
cdDriveNotInstalled: 'CD drive not completely installed',
|
||||
cdDriveInstallation: 'Stop and start the VM to install the CD drive',
|
||||
saveBootOption: 'Save',
|
||||
resetBootOption: 'Reset',
|
||||
|
||||
@@ -701,6 +723,7 @@ var messages = {
|
||||
vifLockedNetworkNoIps: 'Network locked and no IPs are allowed for this interface',
|
||||
vifUnLockedNetwork: 'Network not locked',
|
||||
vifUnknownNetwork: 'Unknown network',
|
||||
vifAction: 'Action',
|
||||
vifCreate: 'Create',
|
||||
|
||||
// ----- VM snapshot tab -----
|
||||
@@ -746,6 +769,8 @@ var messages = {
|
||||
osKernel: 'OS kernel',
|
||||
autoPowerOn: 'Auto power on',
|
||||
ha: 'HA',
|
||||
vmAffinityHost: 'Affinity host',
|
||||
noAffinityHost: 'None',
|
||||
originalTemplate: 'Original template',
|
||||
unknownOsName: 'Unknown',
|
||||
unknownOsKernel: 'Unknown',
|
||||
@@ -855,7 +880,6 @@ var messages = {
|
||||
newVmAddInterface: 'Add interface',
|
||||
newVmDisksPanel: 'Disks',
|
||||
newVmSrLabel: 'SR',
|
||||
newVmBootableLabel: 'Bootable',
|
||||
newVmSizeLabel: 'Size',
|
||||
newVmAddDisk: 'Add disk',
|
||||
newVmSummaryPanel: 'Summary',
|
||||
@@ -881,9 +905,11 @@ var messages = {
|
||||
newVmFirstIndex: 'First index:',
|
||||
newVmNumberRecalculate: 'Recalculate VMs number',
|
||||
newVmNameRefresh: 'Refresh VMs name',
|
||||
newVmAffinityHost: 'Affinity host',
|
||||
newVmAdvancedPanel: 'Advanced',
|
||||
newVmShowAdvanced: 'Show advanced settings',
|
||||
newVmHideAdvanced: 'Hide advanced settings',
|
||||
newVmShare: 'Share this VM',
|
||||
|
||||
// ----- Self -----
|
||||
resourceSets: 'Resource sets',
|
||||
@@ -1053,6 +1079,7 @@ var messages = {
|
||||
trialReadyModalText: 'During the trial period, XOA need to have a working internet connection. This limitation does not apply for our paid plans!',
|
||||
|
||||
// ----- Servers -----
|
||||
serverLabel: 'Label',
|
||||
serverHost: 'Host',
|
||||
serverUsername: 'Username',
|
||||
serverPassword: 'Password',
|
||||
@@ -1062,6 +1089,7 @@ var messages = {
|
||||
serverPlaceHolderUser: 'username',
|
||||
serverPlaceHolderPassword: 'password',
|
||||
serverPlaceHolderAddress: 'address[:port]',
|
||||
serverPlaceHolderLabel: 'label',
|
||||
serverConnect: 'Connect',
|
||||
serverError: 'Error',
|
||||
serverAddFailed: 'Adding server failed',
|
||||
@@ -1191,6 +1219,10 @@ var messages = {
|
||||
disconnectPifConfirm: 'Are you sure you want to disconnect this PIF?',
|
||||
deletePif: 'Delete PIF',
|
||||
deletePifConfirm: 'Are you sure you want to delete this PIF?',
|
||||
pifConnected: 'Connected',
|
||||
pifDisconnected: 'Disconnected',
|
||||
pifPhysicallyConnected: 'Physically connected',
|
||||
pifPhysicallyDisconnected: 'Physically disconnected',
|
||||
|
||||
// ----- User -----
|
||||
username: 'Username',
|
||||
@@ -1236,6 +1268,8 @@ var messages = {
|
||||
logDeleteAll: 'Delete all logs',
|
||||
logDeleteAllTitle: 'Delete all logs',
|
||||
logDeleteAllMessage: 'Are you sure you want to delete all the logs?',
|
||||
logIndicationToEnable: 'Click to enable',
|
||||
logIndicationToDisable: 'Click to disable',
|
||||
reportBug: 'Report a bug',
|
||||
|
||||
// ----- IPs ------
|
||||
@@ -1250,6 +1284,7 @@ var messages = {
|
||||
ipsVifs: 'VIFs',
|
||||
ipsNotUsed: 'Not used',
|
||||
ipPoolUnknownVif: 'unknown VIF',
|
||||
ipPoolNameAlreadyExists: 'Name already exists',
|
||||
|
||||
// ----- Shortcuts -----
|
||||
shortcutModalTitle: 'Keyboard shortcuts',
|
||||
@@ -1318,13 +1353,11 @@ var messages = {
|
||||
xosanSelect2Srs: 'Select at least 2 SRs',
|
||||
xosanLayout: 'Layout',
|
||||
xosanRedundancy: 'Redundancy',
|
||||
xosanRedundancyN: 'Redundancy {redundancy}',
|
||||
xosanCapacity: 'Capacity',
|
||||
xosanAvailableSpace: 'Available space',
|
||||
xosanDiskLossLegend: '* Can fail without data loss',
|
||||
xosanCreate: 'Create XOSAN',
|
||||
xosanCreate: 'Create',
|
||||
xosanInstalling: 'Installing XOSAN. Please wait...',
|
||||
xosanBadVersion: 'You need XenServer 7 to install XOSAN',
|
||||
xosanCommunity: 'No XOSAN available for Community Edition',
|
||||
// Pack download modal
|
||||
xosanInstallCloudPlugin: 'Install cloud plugin first',
|
||||
@@ -1334,7 +1367,9 @@ var messages = {
|
||||
xosanRegisterBeta: 'Register for the XOSAN beta',
|
||||
xosanSuccessfullyRegistered: 'You have successfully registered for the XOSAN beta. Please wait until your request has been approved.',
|
||||
xosanInstallPackOnHosts: 'Install XOSAN pack on these hosts:',
|
||||
xosanInstallPack: 'Install {pack} v{version}?'
|
||||
xosanInstallPack: 'Install {pack} v{version}?',
|
||||
xosanNoPackFound: 'No compatible XOSAN pack found for your XenServer versions.',
|
||||
xosanPackRequirements: 'At least one of these version requirements must be satisfied by all the hosts in this pool:'
|
||||
|
||||
}
|
||||
forEach(messages, function (message, id) {
|
||||
|
||||
@@ -28,7 +28,7 @@ function assertIpv4 (str, msg) {
|
||||
if (!ipv4.test(str)) { throw new Error(msg) }
|
||||
}
|
||||
|
||||
function *range (ip1, ip2) {
|
||||
function * range (ip1, ip2) {
|
||||
assertIpv4(ip1, 'argument "ip1" must be a valid IPv4 address')
|
||||
assertIpv4(ip2, 'argument "ip2" must be a valid IPv4 address')
|
||||
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import React from 'react'
|
||||
|
||||
import _ from 'intl'
|
||||
import ActionButton from './action-button'
|
||||
import Component from './base-component'
|
||||
import Icon from 'icon'
|
||||
import propTypes from './prop-types'
|
||||
import Tooltip from 'tooltip'
|
||||
import { alert } from 'modal'
|
||||
import { connectStore } from './utils'
|
||||
import { SelectVdi } from './select-objects'
|
||||
import {
|
||||
@@ -51,8 +55,9 @@ export default class IsoDevice extends Component {
|
||||
const samePool = vmPool === sr.$pool
|
||||
|
||||
return (
|
||||
samePool && (vmRunning ? sr.shared || sameHost : true) &&
|
||||
sr.SR_type === 'iso' || sr.SR_type === 'udev' && sr.size
|
||||
samePool &&
|
||||
(vmRunning ? sr.shared || sameHost : true) &&
|
||||
(sr.SR_type === 'iso' || (sr.SR_type === 'udev' && sr.size))
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -69,8 +74,10 @@ export default class IsoDevice extends Component {
|
||||
|
||||
_handleEject = () => ejectCd(this.props.vm)
|
||||
|
||||
_showWarning = () => alert(_('cdDriveNotInstalled'), _('cdDriveInstallation'))
|
||||
|
||||
render () {
|
||||
const { mountedIso } = this.props
|
||||
const {cdDrive, mountedIso} = this.props
|
||||
|
||||
return (
|
||||
<div className='input-group'>
|
||||
@@ -81,12 +88,24 @@ export default class IsoDevice extends Component {
|
||||
/>
|
||||
<span className='input-group-btn'>
|
||||
<ActionButton
|
||||
btnStyle='secondary'
|
||||
disabled={!mountedIso}
|
||||
handler={this._handleEject}
|
||||
icon='vm-eject'
|
||||
/>
|
||||
</span>
|
||||
{mountedIso && !cdDrive.device &&
|
||||
<Tooltip content={_('cdDriveNotInstalled')}>
|
||||
<a
|
||||
className='text-warning btn btn-link'
|
||||
onClick={this._showWarning}
|
||||
>
|
||||
<Icon
|
||||
icon='alarm'
|
||||
size='lg'
|
||||
/>
|
||||
</a>
|
||||
</Tooltip>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { Component } from 'react'
|
||||
|
||||
import propTypes from '../prop-types'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
disabled: propTypes.bool,
|
||||
label: propTypes.any.isRequired,
|
||||
onChange: propTypes.func,
|
||||
placeholder: propTypes.string,
|
||||
required: propTypes.bool,
|
||||
schema: propTypes.object.isRequired,
|
||||
uiSchema: propTypes.object,
|
||||
defaultValue: propTypes.any
|
||||
})
|
||||
export default class AbstractInput extends Component {
|
||||
set value (value) {
|
||||
this.refs.input.value = value === undefined ? '' : String(value)
|
||||
}
|
||||
|
||||
get value () {
|
||||
const { value } = this.refs.input
|
||||
return !value ? undefined : value
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
import React, { Component, cloneElement } from 'react'
|
||||
import map from 'lodash/map'
|
||||
import filter from 'lodash/filter'
|
||||
import React from 'react'
|
||||
import uncontrollableInput from 'uncontrollable-input'
|
||||
import { filter, map } from 'lodash'
|
||||
|
||||
import _ from '../intl'
|
||||
import Button from '../button'
|
||||
import Component from '../base-component'
|
||||
import propTypes from '../prop-types'
|
||||
import { propsEqual } from '../utils'
|
||||
import { EMPTY_ARRAY } from '../utils'
|
||||
|
||||
import GenericInput from './generic-input'
|
||||
import {
|
||||
@@ -12,175 +14,110 @@ import {
|
||||
forceDisplayOptionalAttr
|
||||
} from './helpers'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
class ArrayItem extends Component {
|
||||
get value () {
|
||||
return this.refs.input.value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.setState({
|
||||
use: true
|
||||
}, () => {
|
||||
this.refs.input.value = value
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
const { children } = this.props
|
||||
|
||||
return (
|
||||
<li className='list-group-item clearfix'>
|
||||
{cloneElement(children, {
|
||||
ref: 'input'
|
||||
})}
|
||||
<button disabled={children.props.disabled} className='btn btn-danger pull-right' type='button' onClick={this.props.onDelete}>
|
||||
{_('remove')}
|
||||
</button>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
depth: propTypes.number,
|
||||
disabled: propTypes.bool,
|
||||
label: propTypes.any.isRequired,
|
||||
required: propTypes.bool,
|
||||
schema: propTypes.object.isRequired,
|
||||
uiSchema: propTypes.object,
|
||||
defaultValue: propTypes.array
|
||||
uiSchema: propTypes.object
|
||||
})
|
||||
export default class ArrayInput extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this._nextChildKey = 0
|
||||
|
||||
this.state = {
|
||||
use: props.required || forceDisplayOptionalAttr(props),
|
||||
children: this._makeChildren(props)
|
||||
}
|
||||
@uncontrollableInput()
|
||||
export default class ObjectInput extends Component {
|
||||
state = {
|
||||
use: this.props.required || forceDisplayOptionalAttr(this.props)
|
||||
}
|
||||
|
||||
get value () {
|
||||
if (this.state.use) {
|
||||
return map(this.refs, 'value')
|
||||
}
|
||||
_onAddItem = () => {
|
||||
const { props } = this
|
||||
props.onChange((props.value || EMPTY_ARRAY).concat(undefined))
|
||||
}
|
||||
|
||||
set value (value = []) {
|
||||
this.setState({
|
||||
children: this._makeChildren({ ...this.props, value })
|
||||
})
|
||||
_onChangeItem = (value, name) => {
|
||||
const key = Number(name)
|
||||
|
||||
const { props } = this
|
||||
const newValue = (props.value || EMPTY_ARRAY).slice()
|
||||
newValue[key] = value
|
||||
props.onChange(newValue)
|
||||
}
|
||||
|
||||
_handleOptionalChange = event => {
|
||||
this.setState({
|
||||
use: event.target.checked
|
||||
})
|
||||
}
|
||||
|
||||
_handleAdd = () => {
|
||||
const { children } = this.state
|
||||
this.setState({
|
||||
children: children.concat(this._makeChild(this.props))
|
||||
})
|
||||
}
|
||||
|
||||
_remove (key) {
|
||||
this.setState({
|
||||
children: filter(this.state.children, child => child.key !== key)
|
||||
})
|
||||
}
|
||||
|
||||
_makeChild (props, defaultValue) {
|
||||
const key = String(this._nextChildKey++)
|
||||
const {
|
||||
schema: {
|
||||
items
|
||||
}
|
||||
} = props
|
||||
|
||||
return (
|
||||
<ArrayItem key={key} onDelete={() => { this._remove(key) }}>
|
||||
<GenericInput
|
||||
depth={props.depth}
|
||||
disabled={props.disabled}
|
||||
label={items.title || _('item')}
|
||||
required
|
||||
schema={items}
|
||||
uiSchema={props.uiSchema.items}
|
||||
defaultValue={defaultValue}
|
||||
/>
|
||||
</ArrayItem>
|
||||
)
|
||||
}
|
||||
|
||||
_makeChildren (props) {
|
||||
return map(props.defaultValue, defaultValue =>
|
||||
this._makeChild(props, defaultValue)
|
||||
)
|
||||
}
|
||||
|
||||
componentWillReceiveProps (props) {
|
||||
if (
|
||||
!propsEqual(
|
||||
this.props,
|
||||
props,
|
||||
[ 'depth', 'disabled', 'label', 'required', 'schema', 'uiSchema' ]
|
||||
)
|
||||
) {
|
||||
this.setState({
|
||||
children: this._makeChildren(props)
|
||||
})
|
||||
}
|
||||
_onRemoveItem = key => {
|
||||
const { props } = this
|
||||
props.onChange(filter(props.value, (_, i) => i !== key))
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
props,
|
||||
state
|
||||
props: {
|
||||
depth = 0,
|
||||
disabled,
|
||||
label,
|
||||
required,
|
||||
schema,
|
||||
uiSchema,
|
||||
value = EMPTY_ARRAY
|
||||
},
|
||||
state: { use }
|
||||
} = this
|
||||
const {
|
||||
disabled,
|
||||
schema
|
||||
} = props
|
||||
const { use } = state
|
||||
const depth = props.depth || 0
|
||||
|
||||
const childDepth = depth + 2
|
||||
const itemSchema = schema.items
|
||||
const itemUiSchema = uiSchema && uiSchema.items
|
||||
|
||||
const itemLabel = itemSchema.title || _('item')
|
||||
|
||||
return (
|
||||
<div style={{'paddingLeft': `${depth}em`}}>
|
||||
<legend>{props.label}</legend>
|
||||
<legend>{label}</legend>
|
||||
{descriptionRender(schema.description)}
|
||||
<hr />
|
||||
{!props.required &&
|
||||
<div className='checkbox'>
|
||||
<label>
|
||||
<input
|
||||
checked={use}
|
||||
disabled={disabled}
|
||||
onChange={this._handleOptionalChange}
|
||||
type='checkbox'
|
||||
/> {_('fillOptionalInformations')}
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
{use &&
|
||||
<div className={'card-block'}>
|
||||
<ul style={{'paddingLeft': 0}} >
|
||||
{map(this.state.children, (child, index) =>
|
||||
cloneElement(child, { ref: index })
|
||||
)}
|
||||
</ul>
|
||||
<button disabled={disabled} className='btn btn-primary pull-right mt-1 mr-1' type='button' onClick={this._handleAdd}>
|
||||
{_('add')}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
{!required && <div className='checkbox'>
|
||||
<label>
|
||||
<input
|
||||
checked={use}
|
||||
disabled={disabled}
|
||||
onChange={this.linkState('use')}
|
||||
type='checkbox'
|
||||
/> {_('fillOptionalInformations')}
|
||||
</label>
|
||||
</div>}
|
||||
{use && <div className='card-block'>
|
||||
<ul style={{'paddingLeft': 0}} >
|
||||
{map(value, (value, key) =>
|
||||
<li className='list-group-item clearfix' key={key}>
|
||||
<GenericInput
|
||||
depth={childDepth}
|
||||
disabled={disabled}
|
||||
label={itemLabel}
|
||||
name={key}
|
||||
onChange={this._onChangeItem}
|
||||
required
|
||||
schema={itemSchema}
|
||||
uiSchema={itemUiSchema}
|
||||
value={value}
|
||||
/>
|
||||
<Button
|
||||
btnStyle='danger'
|
||||
className='pull-right'
|
||||
disabled={disabled}
|
||||
name={key}
|
||||
onClick={() => this._onRemoveItem(key)}
|
||||
>
|
||||
{_('remove')}
|
||||
</Button>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
<Button
|
||||
btnStyle='primary'
|
||||
className='pull-right mt-1 mr-1'
|
||||
disabled={disabled}
|
||||
onClick={this._onAddItem}
|
||||
>
|
||||
{_('add')}
|
||||
</Button>
|
||||
</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,32 +1,30 @@
|
||||
import React from 'react'
|
||||
import { Toggle } from 'form'
|
||||
|
||||
import AbstractInput from './abstract-input'
|
||||
import uncontrollableInput from 'uncontrollable-input'
|
||||
import Component from '../base-component'
|
||||
import { Toggle } from '../form'
|
||||
|
||||
import { PrimitiveInputWrapper } from './helpers'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default class BooleanInput extends AbstractInput {
|
||||
|
||||
get value () {
|
||||
return this.refs.input.value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.refs.input.value = value
|
||||
}
|
||||
|
||||
@uncontrollableInput()
|
||||
export default class BooleanInput extends Component {
|
||||
render () {
|
||||
const { props } = this
|
||||
const {
|
||||
disabled,
|
||||
onChange,
|
||||
value,
|
||||
...props
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<div className='checkbox form-control'>
|
||||
<Toggle
|
||||
defaultValue={props.defaultValue}
|
||||
disabled={props.disabled}
|
||||
onChange={props.onChange}
|
||||
ref='input'
|
||||
disabled={disabled}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
</div>
|
||||
</PrimitiveInputWrapper>
|
||||
|
||||
@@ -1,33 +1,54 @@
|
||||
import React from 'react'
|
||||
import _ from 'intl'
|
||||
import map from 'lodash/map'
|
||||
import uncontrollableInput from 'uncontrollable-input'
|
||||
import Component from 'base-component'
|
||||
import React from 'react'
|
||||
import { createSelector } from 'reselect'
|
||||
import { findIndex, map } from 'lodash'
|
||||
|
||||
import AbstractInput from './abstract-input'
|
||||
import { PrimitiveInputWrapper } from './helpers'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default class EnumInput extends AbstractInput {
|
||||
@uncontrollableInput()
|
||||
export default class EnumInput extends Component {
|
||||
_getSelectedIndex = createSelector(
|
||||
() => this.props.schema.enum,
|
||||
() => {
|
||||
const {
|
||||
schema,
|
||||
value = schema.default
|
||||
} = this.props
|
||||
return value
|
||||
},
|
||||
(enumValues, value) => {
|
||||
const index = findIndex(enumValues, current => current === value)
|
||||
return index === -1 ? '' : index
|
||||
}
|
||||
)
|
||||
|
||||
_onChange = event => {
|
||||
this.props.onChange(this.props.schema.enum[event.target.value])
|
||||
}
|
||||
|
||||
render () {
|
||||
const { props } = this
|
||||
const {
|
||||
onChange,
|
||||
disabled,
|
||||
schema: { enum: enumValues, enumNames = enumValues },
|
||||
required
|
||||
} = props
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<PrimitiveInputWrapper {...this.props}>
|
||||
<select
|
||||
className='form-control'
|
||||
defaultValue={props.defaultValue || ''}
|
||||
disabled={props.disabled}
|
||||
onChange={onChange && (event => onChange(event.target.value))}
|
||||
ref='input'
|
||||
disabled={disabled}
|
||||
onChange={this._onChange}
|
||||
required={required}
|
||||
value={this._getSelectedIndex()}
|
||||
>
|
||||
{_('noSelectedValue', message => <option value=''>{message}</option>)}
|
||||
{map(props.schema.enum, (value, index) =>
|
||||
<option value={value} key={index}>{value}</option>
|
||||
{map(enumNames, (name, index) =>
|
||||
<option value={index} key={index}>{name}</option>
|
||||
)}
|
||||
</select>
|
||||
</PrimitiveInputWrapper>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React, { Component } from 'react'
|
||||
|
||||
import getEventValue from '../get-event-value'
|
||||
import propTypes from '../prop-types'
|
||||
import uncontrollableInput from 'uncontrollable-input'
|
||||
import { EMPTY_OBJECT } from '../utils'
|
||||
|
||||
import ArrayInput from './array-input'
|
||||
@@ -30,35 +32,31 @@ const InputByType = {
|
||||
depth: propTypes.number,
|
||||
disabled: propTypes.bool,
|
||||
label: propTypes.any.isRequired,
|
||||
onChange: propTypes.func,
|
||||
required: propTypes.bool,
|
||||
schema: propTypes.object.isRequired,
|
||||
uiSchema: propTypes.object,
|
||||
defaultValue: propTypes.any
|
||||
uiSchema: propTypes.object
|
||||
})
|
||||
@uncontrollableInput()
|
||||
export default class GenericInput extends Component {
|
||||
get value () {
|
||||
return this.refs.input.value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.refs.input.value = value
|
||||
_onChange = event => {
|
||||
const { name, onChange } = this.props
|
||||
onChange && onChange(getEventValue(event), name)
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
schema,
|
||||
defaultValue = schema.default,
|
||||
value = schema.default,
|
||||
uiSchema = EMPTY_OBJECT,
|
||||
...opts
|
||||
} = this.props
|
||||
|
||||
const props = {
|
||||
...opts,
|
||||
defaultValue,
|
||||
onChange: this._onChange,
|
||||
schema,
|
||||
uiSchema,
|
||||
ref: 'input'
|
||||
value
|
||||
}
|
||||
|
||||
// Enum, special case.
|
||||
|
||||
@@ -62,19 +62,19 @@ export const PrimitiveInputWrapper = ({ label, required = false, schema, childre
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const forceDisplayOptionalAttr = ({ schema, defaultValue }) => {
|
||||
if (!schema || !defaultValue) {
|
||||
export const forceDisplayOptionalAttr = ({ schema, value }) => {
|
||||
if (!schema || !value) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Array
|
||||
if (schema.items && Array.isArray(defaultValue)) {
|
||||
if (schema.items && Array.isArray(value)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Object
|
||||
for (const key in schema.properties) {
|
||||
if (defaultValue[key]) {
|
||||
if (value[key]) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,40 +1,42 @@
|
||||
import React from 'react'
|
||||
|
||||
import AbstractInput from './abstract-input'
|
||||
import uncontrollableInput from 'uncontrollable-input'
|
||||
import Combobox from '../combobox'
|
||||
import Component from '../base-component'
|
||||
import getEventValue from '../get-event-value'
|
||||
|
||||
import { PrimitiveInputWrapper } from './helpers'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default class IntegerInput extends AbstractInput {
|
||||
get value () {
|
||||
const { value } = this.refs.input
|
||||
return !value ? undefined : +value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
// Getter/Setter are always inherited together.
|
||||
// `get value` is defined in the subclass, so `set value`
|
||||
// must be defined too.
|
||||
super.value = value
|
||||
@uncontrollableInput()
|
||||
export default class IntegerInput extends Component {
|
||||
_onChange = event => {
|
||||
const value = getEventValue(event)
|
||||
this.props.onChange(value ? +value : undefined)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { props } = this
|
||||
const { schema } = props
|
||||
const { required, schema } = this.props
|
||||
const {
|
||||
disabled,
|
||||
onChange, // eslint-disable-line no-unused-vars
|
||||
placeholder = schema.default,
|
||||
value,
|
||||
...props
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<Combobox
|
||||
defaultValue={props.defaultValue}
|
||||
disabled={props.disabled}
|
||||
value={value === undefined ? '' : String(value)}
|
||||
disabled={disabled}
|
||||
max={schema.max}
|
||||
min={schema.min}
|
||||
onChange={props.onChange}
|
||||
onChange={this._onChange}
|
||||
options={schema.defaults}
|
||||
placeholder={props.placeholder || schema.default}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
placeholder={placeholder}
|
||||
required={required}
|
||||
step={1}
|
||||
type='number'
|
||||
/>
|
||||
|
||||
@@ -1,40 +1,42 @@
|
||||
import React from 'react'
|
||||
|
||||
import AbstractInput from './abstract-input'
|
||||
import uncontrollableInput from 'uncontrollable-input'
|
||||
import Combobox from '../combobox'
|
||||
import Component from '../base-component'
|
||||
import getEventValue from '../get-event-value'
|
||||
|
||||
import { PrimitiveInputWrapper } from './helpers'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default class NumberInput extends AbstractInput {
|
||||
get value () {
|
||||
const { value } = this.refs.input
|
||||
return !value ? undefined : +value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
// Getter/Setter are always inherited together.
|
||||
// `get value` is defined in the subclass, so `set value`
|
||||
// must be defined too.
|
||||
super.value = value
|
||||
@uncontrollableInput()
|
||||
export default class NumberInput extends Component {
|
||||
_onChange = event => {
|
||||
const value = getEventValue(event)
|
||||
this.props.onChange(value ? +value : undefined)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { props } = this
|
||||
const { schema } = props
|
||||
const { required, schema } = this.props
|
||||
const {
|
||||
disabled,
|
||||
onChange, // eslint-disable-line no-unused-vars
|
||||
placeholder = schema.default,
|
||||
value,
|
||||
...props
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<Combobox
|
||||
defaultValue={props.defaultValue}
|
||||
disabled={props.disabled}
|
||||
value={value === undefined ? '' : String(value)}
|
||||
disabled={disabled}
|
||||
max={schema.max}
|
||||
min={schema.min}
|
||||
onChange={props.onChange}
|
||||
onChange={this._onChange}
|
||||
options={schema.defaults}
|
||||
placeholder={props.placeholder || schema.default}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
placeholder={placeholder}
|
||||
required={required}
|
||||
step='any'
|
||||
type='number'
|
||||
/>
|
||||
|
||||
@@ -1,167 +1,97 @@
|
||||
import _ from 'intl'
|
||||
import React, { Component, cloneElement } from 'react'
|
||||
import forEach from 'lodash/forEach'
|
||||
import includes from 'lodash/includes'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import uncontrollableInput from 'uncontrollable-input'
|
||||
import { createSelector } from 'reselect'
|
||||
import { keyBy, map } from 'lodash'
|
||||
|
||||
import _ from '../intl'
|
||||
import Component from '../base-component'
|
||||
import propTypes from '../prop-types'
|
||||
import { propsEqual } from '../utils'
|
||||
import { EMPTY_OBJECT } from '../utils'
|
||||
|
||||
import GenericInput from './generic-input'
|
||||
|
||||
import {
|
||||
descriptionRender,
|
||||
forceDisplayOptionalAttr
|
||||
} from './helpers'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
class ObjectItem extends Component {
|
||||
get value () {
|
||||
return this.refs.input.value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.refs.input.value = value
|
||||
}
|
||||
|
||||
render () {
|
||||
const { props } = this
|
||||
|
||||
return (
|
||||
<div className='pb-1'>
|
||||
{cloneElement(props.children, {
|
||||
ref: 'input'
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
depth: propTypes.number,
|
||||
disabled: propTypes.bool,
|
||||
label: propTypes.any.isRequired,
|
||||
required: propTypes.bool,
|
||||
schema: propTypes.object.isRequired,
|
||||
uiSchema: propTypes.object,
|
||||
defaultValue: propTypes.object
|
||||
uiSchema: propTypes.object
|
||||
})
|
||||
@uncontrollableInput()
|
||||
export default class ObjectInput extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
use: Boolean(props.required) || forceDisplayOptionalAttr(props),
|
||||
children: this._makeChildren(props)
|
||||
}
|
||||
state = {
|
||||
use: this.props.required || forceDisplayOptionalAttr(this.props)
|
||||
}
|
||||
|
||||
get value () {
|
||||
if (!this.state.use) {
|
||||
return
|
||||
}
|
||||
|
||||
const obj = {}
|
||||
|
||||
forEach(this.refs, (instance, key) => {
|
||||
obj[key] = instance.value
|
||||
})
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
set value (value = {}) {
|
||||
this.setState({
|
||||
use: true
|
||||
}, () => {
|
||||
forEach(this.refs, (instance, id) => {
|
||||
instance.value = value[id]
|
||||
})
|
||||
_onChildChange = (value, key) => {
|
||||
this.props.onChange({
|
||||
...this.props.value,
|
||||
[key]: value
|
||||
})
|
||||
}
|
||||
|
||||
_handleOptionalChange = event => {
|
||||
const { checked } = event.target
|
||||
|
||||
this.setState({
|
||||
use: checked
|
||||
})
|
||||
}
|
||||
|
||||
_makeChildren (props) {
|
||||
const {
|
||||
depth = 0,
|
||||
schema,
|
||||
uiSchema = {},
|
||||
defaultValue = {}
|
||||
} = props
|
||||
const obj = {}
|
||||
const { properties } = uiSchema
|
||||
|
||||
forEach(schema.properties, (childSchema, key) => {
|
||||
obj[key] = (
|
||||
<ObjectItem key={key}>
|
||||
<GenericInput
|
||||
depth={depth + 2}
|
||||
disabled={props.disabled}
|
||||
label={childSchema.title || key}
|
||||
required={includes(schema.required, key)}
|
||||
schema={childSchema}
|
||||
uiSchema={properties && properties[key]}
|
||||
defaultValue={defaultValue[key]}
|
||||
/>
|
||||
</ObjectItem>
|
||||
)
|
||||
})
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
componentWillReceiveProps (props) {
|
||||
if (
|
||||
!propsEqual(
|
||||
this.props,
|
||||
props,
|
||||
[ 'depth', 'disabled', 'label', 'required', 'schema', 'uiSchema' ]
|
||||
)
|
||||
) {
|
||||
this.setState({
|
||||
children: this._makeChildren(props)
|
||||
})
|
||||
}
|
||||
}
|
||||
_getRequiredProps = createSelector(
|
||||
() => this.props.schema.required,
|
||||
required => required
|
||||
? keyBy(required)
|
||||
: EMPTY_OBJECT
|
||||
)
|
||||
|
||||
render () {
|
||||
const { props, state } = this
|
||||
const { use } = state
|
||||
const depth = props.depth || 0
|
||||
const {
|
||||
props: {
|
||||
depth = 0,
|
||||
disabled,
|
||||
label,
|
||||
required,
|
||||
schema,
|
||||
uiSchema,
|
||||
value = EMPTY_OBJECT
|
||||
},
|
||||
state: { use }
|
||||
} = this
|
||||
|
||||
const childDepth = depth + 2
|
||||
const properties = (uiSchema != null && uiSchema.properties) || EMPTY_OBJECT
|
||||
const requiredProps = this._getRequiredProps()
|
||||
|
||||
return (
|
||||
<div style={{'paddingLeft': `${depth}em`}}>
|
||||
<legend>{props.label}</legend>
|
||||
{descriptionRender(props.schema.description)}
|
||||
<legend>{label}</legend>
|
||||
{descriptionRender(schema.description)}
|
||||
<hr />
|
||||
{!props.required &&
|
||||
<div className='checkbox'>
|
||||
<label>
|
||||
<input
|
||||
checked={use}
|
||||
disabled={props.disabled}
|
||||
onChange={this._handleOptionalChange}
|
||||
type='checkbox'
|
||||
/> {_('fillOptionalInformations')}
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
{use &&
|
||||
<div className='card-block'>
|
||||
{map(state.children, (child, index) =>
|
||||
cloneElement(child, { ref: index })
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
{!required && <div className='checkbox'>
|
||||
<label>
|
||||
<input
|
||||
checked={use}
|
||||
disabled={disabled}
|
||||
onChange={this.linkState('use')}
|
||||
type='checkbox'
|
||||
/> {_('fillOptionalInformations')}
|
||||
</label>
|
||||
</div>}
|
||||
{use && <div className='card-block'>
|
||||
{map(schema.properties, (childSchema, key) =>
|
||||
<div className='pb-1' key={key}>
|
||||
<GenericInput
|
||||
depth={childDepth}
|
||||
disabled={disabled}
|
||||
label={childSchema.title || key}
|
||||
name={key}
|
||||
onChange={this._onChildChange}
|
||||
required={Boolean(requiredProps[key])}
|
||||
schema={childSchema}
|
||||
uiSchema={properties[key]}
|
||||
value={value[key]}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import React from 'react'
|
||||
|
||||
import AbstractInput from './abstract-input'
|
||||
import uncontrollableInput from 'uncontrollable-input'
|
||||
import Combobox from '../combobox'
|
||||
import Component from '../base-component'
|
||||
import propTypes from '../prop-types'
|
||||
|
||||
import { PrimitiveInputWrapper } from './helpers'
|
||||
|
||||
// ===================================================================
|
||||
@@ -10,22 +12,29 @@ import { PrimitiveInputWrapper } from './helpers'
|
||||
@propTypes({
|
||||
password: propTypes.bool
|
||||
})
|
||||
export default class StringInput extends AbstractInput {
|
||||
@uncontrollableInput()
|
||||
export default class StringInput extends Component {
|
||||
render () {
|
||||
const { props } = this
|
||||
const { schema } = props
|
||||
const { required, schema } = this.props
|
||||
const {
|
||||
disabled,
|
||||
onChange,
|
||||
password,
|
||||
placeholder = schema.default,
|
||||
value,
|
||||
...props
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<Combobox
|
||||
defaultValue={props.defaultValue}
|
||||
disabled={props.disabled}
|
||||
onChange={props.onChange}
|
||||
value={value || ''}
|
||||
disabled={disabled}
|
||||
onChange={onChange}
|
||||
options={schema.defaults}
|
||||
placeholder={props.placeholder || schema.default}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
type={props.password && 'password'}
|
||||
placeholder={placeholder || schema.default}
|
||||
required={required}
|
||||
type={password && 'password'}
|
||||
/>
|
||||
</PrimitiveInputWrapper>
|
||||
)
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import _ from 'intl'
|
||||
import Icon from 'icon'
|
||||
import isArray from 'lodash/isArray'
|
||||
import isString from 'lodash/isString'
|
||||
import React, { Component, cloneElement } from 'react'
|
||||
import { Button, Modal as ReactModal } from 'react-bootstrap-4/lib'
|
||||
import { Modal as ReactModal } from 'react-bootstrap-4/lib'
|
||||
|
||||
import _ from './intl'
|
||||
import Button from './button'
|
||||
import Icon from './icon'
|
||||
import propTypes from './prop-types'
|
||||
import {
|
||||
disable as disableShortcuts,
|
||||
@@ -75,8 +76,6 @@ class Confirm extends Component {
|
||||
instance.close()
|
||||
}
|
||||
|
||||
_style = { marginRight: '0.5em' }
|
||||
|
||||
render () {
|
||||
const { Body, Footer, Header, Title } = ReactModal
|
||||
const { title, icon } = this.props
|
||||
@@ -97,14 +96,14 @@ class Confirm extends Component {
|
||||
</Body>
|
||||
<Footer>
|
||||
<Button
|
||||
bsStyle='primary'
|
||||
btnStyle='primary'
|
||||
onClick={this._resolve}
|
||||
style={this._style}
|
||||
>
|
||||
{_('confirmOk')}
|
||||
</Button>
|
||||
{' '}
|
||||
<Button
|
||||
bsStyle='secondary'
|
||||
onClick={this._reject}
|
||||
>
|
||||
{_('confirmCancel')}
|
||||
|
||||
5
src/common/react-novnc.js
vendored
5
src/common/react-novnc.js
vendored
@@ -77,6 +77,11 @@ export default class NoVnc extends Component {
|
||||
_connect = () => {
|
||||
this._clean()
|
||||
|
||||
const { canvas } = this.refs
|
||||
if (!canvas) {
|
||||
return
|
||||
}
|
||||
|
||||
const url = parseRelativeUrl(this.props.url)
|
||||
fixProtocol(url)
|
||||
|
||||
|
||||
@@ -182,6 +182,10 @@ const renderXoItem = (item, {
|
||||
} = {}) => {
|
||||
const { id, type, label } = item
|
||||
|
||||
if (item.removed) {
|
||||
return <span key={id} className='text-danger'> <Icon icon='alarm' /> {id}</span>
|
||||
}
|
||||
|
||||
if (!type) {
|
||||
if (process.env.NODE_ENV !== 'production' && !label) {
|
||||
throw new Error(`an item must have at least either a type or a label`)
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import classNames from 'classnames'
|
||||
import Icon from 'icon'
|
||||
import later from 'later'
|
||||
import React from 'react'
|
||||
import Tooltip from 'tooltip'
|
||||
import { Toggle } from 'form'
|
||||
import { FormattedDate, FormattedTime } from 'react-intl'
|
||||
import {
|
||||
forEach,
|
||||
@@ -14,12 +11,15 @@ import {
|
||||
} from 'lodash'
|
||||
|
||||
import _ from './intl'
|
||||
import Button from './button'
|
||||
import Component from './base-component'
|
||||
import propTypes from './prop-types'
|
||||
import TimezonePicker from './timezone-picker'
|
||||
import Icon from './icon'
|
||||
import Tooltip from './tooltip'
|
||||
import { Card, CardHeader, CardBlock } from './card'
|
||||
import { Col, Row } from './grid'
|
||||
import { Range } from './form'
|
||||
import { Range, Toggle } from './form'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -259,9 +259,12 @@ class TableSelect extends Component {
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<button className='btn btn-secondary pull-right' onClick={this._reset}>
|
||||
<Button
|
||||
className='pull-right'
|
||||
onClick={this._reset}
|
||||
>
|
||||
{_(`selectTableAll${labelId}`)} {value && !value.length && <Icon icon='success' />}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -447,23 +450,27 @@ class DayPicker extends Component {
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
cronPattern: propTypes.string.isRequired,
|
||||
cronPattern: propTypes.string,
|
||||
onChange: propTypes.func,
|
||||
timezone: propTypes.string
|
||||
timezone: propTypes.string,
|
||||
value: propTypes.shape({
|
||||
cronPattern: propTypes.string.isRequired,
|
||||
timezone: propTypes.string
|
||||
})
|
||||
})
|
||||
export default class Scheduler extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this._onCronChange = newCrons => {
|
||||
const cronPattern = this.props.cronPattern.split(' ')
|
||||
const cronPattern = this._getCronPattern().split(' ')
|
||||
forEach(newCrons, (cron, unit) => {
|
||||
cronPattern[PICKTIME_TO_ID[unit]] = cron
|
||||
})
|
||||
|
||||
this.props.onChange({
|
||||
cronPattern: cronPattern.join(' '),
|
||||
timezone: this.props.timezone
|
||||
timezone: this._getTimezone()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -475,17 +482,24 @@ export default class Scheduler extends Component {
|
||||
|
||||
_onTimezoneChange = timezone => {
|
||||
this.props.onChange({
|
||||
cronPattern: this.props.cronPattern,
|
||||
cronPattern: this._getCronPattern(),
|
||||
timezone
|
||||
})
|
||||
}
|
||||
|
||||
_getCronPattern = () => {
|
||||
const { value, cronPattern = value.cronPattern } = this.props
|
||||
return cronPattern
|
||||
}
|
||||
|
||||
_getTimezone = () => {
|
||||
const { value, timezone = value && value.timezone } = this.props
|
||||
return timezone
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
cronPattern,
|
||||
timezone
|
||||
} = this.props
|
||||
const cronPatternArr = cronPattern.split(' ')
|
||||
const cronPatternArr = this._getCronPattern().split(' ')
|
||||
const timezone = this._getTimezone()
|
||||
|
||||
return (
|
||||
<div className='card-block'>
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import React from 'react'
|
||||
import classNames from 'classnames'
|
||||
import Icon from 'icon'
|
||||
import store from 'store'
|
||||
import Tooltip from 'tooltip'
|
||||
import { Button } from 'react-bootstrap-4/lib'
|
||||
import { parse as parseRemote } from 'xo-remote-parser'
|
||||
import {
|
||||
assign,
|
||||
@@ -26,10 +22,14 @@ import {
|
||||
} from 'lodash'
|
||||
|
||||
import _ from './intl'
|
||||
import autoControlledInput from './auto-controlled-input'
|
||||
import Button from './button'
|
||||
import Component from './base-component'
|
||||
import Icon from './icon'
|
||||
import propTypes from './prop-types'
|
||||
import renderXoItem from './render-xo-item'
|
||||
import store from './store'
|
||||
import Tooltip from './tooltip'
|
||||
import uncontrollableInput from 'uncontrollable-input'
|
||||
import { Select } from './form'
|
||||
import {
|
||||
createCollectionWrapper,
|
||||
@@ -42,7 +42,6 @@ import {
|
||||
import {
|
||||
addSubscriptions,
|
||||
connectStore,
|
||||
mapPlus,
|
||||
resolveResourceSets
|
||||
} from './utils'
|
||||
import {
|
||||
@@ -135,37 +134,6 @@ const options = props => ({
|
||||
]).isRequired
|
||||
})
|
||||
export class GenericSelect extends Component {
|
||||
componentDidUpdate (prevProps) {
|
||||
const { onChange, xoObjects } = this.props
|
||||
|
||||
if (!onChange || prevProps.xoObjects === xoObjects) {
|
||||
return
|
||||
}
|
||||
|
||||
const ids = this._getSelectValue()
|
||||
const objectsById = this._getObjectsById()
|
||||
|
||||
if (!isArray(ids)) {
|
||||
ids && !objectsById[ids] && onChange(undefined)
|
||||
} else {
|
||||
let shouldTriggerOnChange
|
||||
|
||||
const newValue = isArray(ids) && mapPlus(ids, (id, push) => {
|
||||
const object = objectsById[id]
|
||||
|
||||
if (object) {
|
||||
push(object)
|
||||
} else {
|
||||
shouldTriggerOnChange = true
|
||||
}
|
||||
})
|
||||
|
||||
if (shouldTriggerOnChange) {
|
||||
this.props.onChange(newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_getObjectsById = createSelector(
|
||||
() => this.props.xoObjects,
|
||||
objects => keyBy(
|
||||
@@ -180,22 +148,19 @@ export class GenericSelect extends Component {
|
||||
() => this.props.xoContainers,
|
||||
() => this.props.xoObjects,
|
||||
(containers, objects) => { // createCollectionWrapper with a depth?
|
||||
const __DEV__ = process.env.NODE_ENV !== 'production'
|
||||
const { name } = this.constructor
|
||||
|
||||
let options = []
|
||||
if (!containers) {
|
||||
if (__DEV__ && !isArray(objects)) {
|
||||
throw new Error(`${name}: without xoContainers, xoObjects must be an array`)
|
||||
}
|
||||
|
||||
return map(objects, getOption)
|
||||
}
|
||||
|
||||
if (__DEV__ && isArray(objects)) {
|
||||
options = map(objects, getOption)
|
||||
} else if (__DEV__ && isArray(objects)) {
|
||||
throw new Error(`${name}: with xoContainers, xoObjects must be an object`)
|
||||
}
|
||||
|
||||
const options = []
|
||||
forEach(containers, container => {
|
||||
options.push({
|
||||
disabled: true,
|
||||
@@ -206,6 +171,30 @@ export class GenericSelect extends Component {
|
||||
options.push(getOption(object, container))
|
||||
})
|
||||
})
|
||||
|
||||
const values = this._getSelectValue()
|
||||
const objectsById = this._getObjectsById()
|
||||
const addIfMissing = val => {
|
||||
if (val && !objectsById[val]) {
|
||||
options.push({
|
||||
disabled: true,
|
||||
id: val,
|
||||
label: val,
|
||||
value: val,
|
||||
xoItem: {
|
||||
id: val,
|
||||
removed: true
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (isArray(values)) {
|
||||
forEach(values, addIfMissing)
|
||||
} else {
|
||||
addIfMissing(values)
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
)
|
||||
@@ -289,7 +278,7 @@ export class GenericSelect extends Component {
|
||||
{select}
|
||||
<span className='input-group-btn'>
|
||||
<Tooltip content={_('selectAll')}>
|
||||
<Button type='button' bsStyle='secondary' onClick={this._selectAll} style={ADDON_BUTTON_STYLE}>
|
||||
<Button onClick={this._selectAll} style={ADDON_BUTTON_STYLE}>
|
||||
<Icon icon='add' />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
@@ -298,7 +287,7 @@ export class GenericSelect extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
const makeStoreSelect = (createSelectors, defaultProps) => autoControlledInput(options)(
|
||||
const makeStoreSelect = (createSelectors, defaultProps) => uncontrollableInput(options)(
|
||||
connectStore(createSelectors)(
|
||||
props =>
|
||||
<GenericSelect
|
||||
@@ -308,7 +297,7 @@ const makeStoreSelect = (createSelectors, defaultProps) => autoControlledInput(o
|
||||
)
|
||||
)
|
||||
|
||||
const makeSubscriptionSelect = (subscribe, props) => autoControlledInput(options)(
|
||||
const makeSubscriptionSelect = (subscribe, props) => uncontrollableInput(options)(
|
||||
class extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
@@ -347,7 +347,7 @@ export const createSortForType = invoke(() => {
|
||||
return (type, collection) => createSort(
|
||||
collection,
|
||||
autoSelector(type, getIteratees),
|
||||
autoSelector(type, getOrders),
|
||||
autoSelector(type, getOrders)
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import DropdownMenu from 'react-bootstrap-4/lib/DropdownMenu' // https://phabric
|
||||
import DropdownToggle from 'react-bootstrap-4/lib/DropdownToggle' // https://phabricator.babeljs.io/T6662 so Dropdown.Toggle won't work https://react-bootstrap.github.io/components.html#btn-dropdowns-custom
|
||||
import { Portal } from 'react-overlays'
|
||||
|
||||
import Button from '../button'
|
||||
import Component from '../base-component'
|
||||
import Icon from '../icon'
|
||||
import propTypes from '../prop-types'
|
||||
@@ -80,9 +81,9 @@ class TableFilter extends Component {
|
||||
className='form-control'
|
||||
/>
|
||||
<div className='input-group-btn'>
|
||||
<button className='btn btn-secondary' onClick={this._cleanFilter}>
|
||||
<Button onClick={this._cleanFilter}>
|
||||
<Icon icon='clear-search' />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -93,7 +94,7 @@ class TableFilter extends Component {
|
||||
|
||||
@propTypes({
|
||||
columnId: propTypes.number.isRequired,
|
||||
name: propTypes.any.isRequired,
|
||||
name: propTypes.node,
|
||||
sort: propTypes.func,
|
||||
sortIcon: propTypes.string
|
||||
})
|
||||
@@ -104,10 +105,10 @@ class ColumnHead extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { name, sortIcon } = this.props
|
||||
const { name, sortIcon, textAlign } = this.props
|
||||
|
||||
if (!this.props.sort) {
|
||||
return <th>{name}</th>
|
||||
return <th className={textAlign && `text-xs-${textAlign}`}>{name}</th>
|
||||
}
|
||||
|
||||
const isSelected = sortIcon === 'asc' || sortIcon === 'desc'
|
||||
@@ -115,6 +116,7 @@ class ColumnHead extends Component {
|
||||
return (
|
||||
<th
|
||||
className={classNames(
|
||||
textAlign && `text-xs-${textAlign}`,
|
||||
styles.clickableColumn,
|
||||
isSelected && classNames('text-white', 'bg-info')
|
||||
)}
|
||||
@@ -141,13 +143,14 @@ const DEFAULT_ITEMS_PER_PAGE = 10
|
||||
]).isRequired,
|
||||
columns: propTypes.arrayOf(propTypes.shape({
|
||||
default: propTypes.bool,
|
||||
name: propTypes.node.isRequired,
|
||||
name: propTypes.node,
|
||||
itemRenderer: propTypes.func.isRequired,
|
||||
sortCriteria: propTypes.oneOfType([
|
||||
propTypes.func,
|
||||
propTypes.string
|
||||
]),
|
||||
sortOrder: propTypes.string
|
||||
sortOrder: propTypes.string,
|
||||
textAlign: propTypes.string
|
||||
})).isRequired,
|
||||
filterContainer: propTypes.func,
|
||||
filters: propTypes.object,
|
||||
@@ -302,7 +305,9 @@ export default class SortedTable extends Component {
|
||||
<tr>
|
||||
{map(props.columns, (column, key) => (
|
||||
<ColumnHead
|
||||
textAlign={column.textAlign}
|
||||
columnId={key}
|
||||
|
||||
key={key}
|
||||
name={column.name}
|
||||
sort={column.sortCriteria && this._sort}
|
||||
@@ -314,7 +319,7 @@ export default class SortedTable extends Component {
|
||||
<tbody>
|
||||
{map(this._getVisibleItems(), (item, i) => {
|
||||
const columns = map(props.columns, (column, key) => (
|
||||
<td key={key}>
|
||||
<td key={key} className={column.textAlign && `text-xs-${column.textAlign}`}>
|
||||
{column.itemRenderer(item, userData)}
|
||||
</td>
|
||||
))
|
||||
|
||||
38
src/common/state-button.js
Normal file
38
src/common/state-button.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import ActionButton from './action-button'
|
||||
import propTypes from './prop-types'
|
||||
|
||||
const Button = styled(ActionButton)`
|
||||
background-color: ${p => p.theme[`${p.state ? 'enabled' : 'disabled'}StateBg`]}
|
||||
border: 2px solid ${p => p.theme[`${p.state ? 'enabled' : 'disabled'}StateColor`]}
|
||||
color: ${p => p.theme[`${p.state ? 'enabled' : 'disabled'}StateColor`]}
|
||||
`
|
||||
|
||||
const StateButton = ({
|
||||
disabledHandler,
|
||||
disabledLabel,
|
||||
disabledTooltip,
|
||||
|
||||
enabledLabel,
|
||||
enabledTooltip,
|
||||
enabledHandler,
|
||||
|
||||
state,
|
||||
...props
|
||||
}) =>
|
||||
<Button
|
||||
handler={state ? enabledHandler : disabledHandler}
|
||||
tooltip={state ? enabledTooltip : disabledTooltip}
|
||||
{...props}
|
||||
icon={state ? 'running' : 'halted'}
|
||||
size='small'
|
||||
state={state}
|
||||
>
|
||||
{state ? enabledLabel : disabledLabel}
|
||||
</Button>
|
||||
|
||||
export default propTypes({
|
||||
state: propTypes.bool.isRequired
|
||||
})(StateButton)
|
||||
@@ -1,33 +1,22 @@
|
||||
import isFunction from 'lodash/isFunction'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const createAction = (() => {
|
||||
const { defineProperty } = Object
|
||||
const noop = function () {
|
||||
if (arguments.length) {
|
||||
throw new Error('this action expects no payload!')
|
||||
}
|
||||
}
|
||||
|
||||
return (type, payloadCreator = noop) => {
|
||||
const createActionObject = payload => {
|
||||
// Thunks
|
||||
if (isFunction(payload)) {
|
||||
return payload
|
||||
}
|
||||
return (type, payloadCreator) => defineProperty(
|
||||
payloadCreator
|
||||
? (...args) => ({
|
||||
type,
|
||||
payload: payloadCreator(...args)
|
||||
})
|
||||
: (action => function () {
|
||||
if (arguments.length) {
|
||||
throw new Error('this action expects no payload!')
|
||||
}
|
||||
|
||||
return payload === undefined
|
||||
? { type }
|
||||
: { type, payload }
|
||||
}
|
||||
|
||||
return defineProperty(
|
||||
(...args) => createActionObject(payloadCreator(...args)),
|
||||
'toString',
|
||||
{ value: () => type }
|
||||
)
|
||||
}
|
||||
return action
|
||||
})({ type }),
|
||||
'toString',
|
||||
{ value: () => type }
|
||||
)
|
||||
})()
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -102,16 +102,20 @@ export default {
|
||||
|
||||
for (const id in updates) {
|
||||
const object = updates[id]
|
||||
const previous = all[id]
|
||||
|
||||
if (object) {
|
||||
const { type } = object
|
||||
|
||||
all[id] = object
|
||||
get(object.type)[id] = object
|
||||
} else {
|
||||
const previous = all[id]
|
||||
if (previous) {
|
||||
delete all[id]
|
||||
get(type)[id] = object
|
||||
|
||||
if (previous && previous.type !== type) {
|
||||
delete get(previous.type)[id]
|
||||
}
|
||||
} else if (previous) {
|
||||
delete all[id]
|
||||
delete get(previous.type)[id]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
0
src/common/themes/.index-modules
Normal file
0
src/common/themes/.index-modules
Normal file
6
src/common/themes/base.js
Normal file
6
src/common/themes/base.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
disabledStateBg: '#fff',
|
||||
disabledStateColor: '#c0392b',
|
||||
enabledStateBg: '#fff',
|
||||
enabledStateColor: '#27ae60'
|
||||
}
|
||||
@@ -56,7 +56,7 @@ export default class TimezonePicker extends Component {
|
||||
}
|
||||
|
||||
this.setState({
|
||||
timezone: option && option.value || SERVER_TIMEZONE_TAG
|
||||
timezone: (option != null && option.value) || SERVER_TIMEZONE_TAG
|
||||
}, () =>
|
||||
this.props.onChange(this.state.timezone === SERVER_TIMEZONE_TAG ? null : this.state.timezone)
|
||||
)
|
||||
@@ -81,7 +81,6 @@ export default class TimezonePicker extends Component {
|
||||
/>
|
||||
<div className='pull-right'>
|
||||
<ActionButton
|
||||
btnStyle='secondary'
|
||||
handler={this._useLocalTime}
|
||||
icon='time'
|
||||
>
|
||||
|
||||
@@ -280,8 +280,8 @@ const getParent = (currentTarget) => {
|
||||
currentParent = currentParent.parentElement
|
||||
}
|
||||
|
||||
const parentTop = currentParent && currentParent.getBoundingClientRect().top || 0
|
||||
const parentLeft = currentParent && currentParent.getBoundingClientRect().left || 0
|
||||
const parentTop = currentParent && currentParent.getBoundingClientRect().top
|
||||
const parentLeft = currentParent && currentParent.getBoundingClientRect().left
|
||||
|
||||
return {parentTop, parentLeft}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import mapValues from 'lodash/mapValues'
|
||||
import React from 'react'
|
||||
import ReadableStream from 'readable-stream'
|
||||
import replace from 'lodash/replace'
|
||||
import startsWith from 'lodash/startsWith'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import _ from './intl'
|
||||
@@ -63,13 +64,22 @@ export const addSubscriptions = subscriptions => Component => {
|
||||
|
||||
componentWillMount () {
|
||||
this._unsubscribes = map(isFunction(subscriptions) ? subscriptions() : subscriptions, (subscribe, prop) =>
|
||||
subscribe(value => this.setState({ [prop]: value }))
|
||||
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 () {
|
||||
@@ -482,7 +492,35 @@ export const resolveIds = params => {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const compareVersions = (v1, v2) => {
|
||||
const OPs = {
|
||||
'<': a => a < 0,
|
||||
'<=': a => a <= 0,
|
||||
'===': a => a === 0,
|
||||
'>': a => a > 0,
|
||||
'>=': a => a >= 0
|
||||
}
|
||||
|
||||
const makeNiceCompare = compare => function () {
|
||||
const { length } = arguments
|
||||
if (length === 2) {
|
||||
return compare(arguments[0], arguments[1])
|
||||
}
|
||||
|
||||
let i = 1
|
||||
let v1 = arguments[0]
|
||||
let op, v2
|
||||
while (i < length) {
|
||||
op = arguments[i++]
|
||||
v2 = arguments[i++]
|
||||
if (!OPs[op](compare(v1, v2))) {
|
||||
return false
|
||||
}
|
||||
v1 = v2
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export const compareVersions = makeNiceCompare((v1, v2) => {
|
||||
v1 = v1.split('.')
|
||||
v2 = v2.split('.')
|
||||
|
||||
@@ -495,4 +533,7 @@ export const compareVersions = (v1, v2) => {
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
})
|
||||
|
||||
export const isXosanPack = ({ name }) =>
|
||||
startsWith(name, 'XOSAN')
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import map from 'lodash/map'
|
||||
import AbstractInput from '../json-schema-input/abstract-input'
|
||||
import { PureComponent } from 'react'
|
||||
|
||||
import getEventValue from '../get-event-value'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default class XoAbstractInput extends AbstractInput {
|
||||
get value () {
|
||||
const value = this.refs.input.value
|
||||
const getId = value => (value != null && value.id) || value
|
||||
|
||||
if (this.props.schema.type === 'array') {
|
||||
return map(value, object => object.id || object)
|
||||
}
|
||||
export default class XoAbstractInput extends PureComponent {
|
||||
_onChange = event => {
|
||||
const value = getEventValue(event)
|
||||
const { props } = this
|
||||
|
||||
return value.id || value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.refs.input.value = value
|
||||
return props.onChange(
|
||||
props.schema.type === 'array'
|
||||
? map(value, getId)
|
||||
: getId(value)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,10 +16,10 @@ export default class HighLevelObjectInput extends XoAbstractInput {
|
||||
disabled={props.disabled}
|
||||
hasSelectAll
|
||||
multi={props.multi}
|
||||
onChange={props.onChange}
|
||||
onChange={this._onChange}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
defaultValue={props.defaultValue}
|
||||
value={props.value}
|
||||
/>
|
||||
</PrimitiveInputWrapper>
|
||||
)
|
||||
|
||||
@@ -16,10 +16,10 @@ export default class HostInput extends XoAbstractInput {
|
||||
disabled={props.disabled}
|
||||
hasSelectAll
|
||||
multi={props.multi}
|
||||
onChange={props.onChange}
|
||||
onChange={this._onChange}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
defaultValue={props.defaultValue}
|
||||
value={props.value}
|
||||
/>
|
||||
</PrimitiveInputWrapper>
|
||||
)
|
||||
|
||||
@@ -16,10 +16,10 @@ export default class PoolInput extends XoAbstractInput {
|
||||
disabled={props.disabled}
|
||||
hasSelectAll
|
||||
multi={props.multi}
|
||||
onChange={props.onChange}
|
||||
onChange={this._onChange}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
defaultValue={props.defaultValue}
|
||||
value={props.value}
|
||||
/>
|
||||
</PrimitiveInputWrapper>
|
||||
)
|
||||
|
||||
@@ -16,10 +16,10 @@ export default class RemoteInput extends XoAbstractInput {
|
||||
disabled={props.disabled}
|
||||
hasSelectAll
|
||||
multi={props.multi}
|
||||
onChange={props.onChange}
|
||||
onChange={this._onChange}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
defaultValue={props.defaultValue}
|
||||
value={props.value}
|
||||
/>
|
||||
</PrimitiveInputWrapper>
|
||||
)
|
||||
|
||||
@@ -16,10 +16,10 @@ export default class RoleInput extends XoAbstractInput {
|
||||
disabled={props.disabled}
|
||||
hasSelectAll
|
||||
multi={props.multi}
|
||||
onChange={props.onChange}
|
||||
onChange={this._onChange}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
defaultValue={props.defaultValue}
|
||||
value={props.value}
|
||||
/>
|
||||
</PrimitiveInputWrapper>
|
||||
)
|
||||
|
||||
@@ -16,10 +16,10 @@ export default class SrInput extends XoAbstractInput {
|
||||
disabled={props.disabled}
|
||||
hasSelectAll
|
||||
multi={props.multi}
|
||||
onChange={props.onChange}
|
||||
onChange={this._onChange}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
defaultValue={props.defaultValue}
|
||||
value={props.value}
|
||||
/>
|
||||
</PrimitiveInputWrapper>
|
||||
)
|
||||
|
||||
@@ -16,10 +16,10 @@ export default class SubjectInput extends XoAbstractInput {
|
||||
disabled={props.disabled}
|
||||
hasSelectAll
|
||||
multi={props.multi}
|
||||
onChange={props.onChange}
|
||||
onChange={this._onChange}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
defaultValue={props.defaultValue}
|
||||
value={props.value}
|
||||
/>
|
||||
</PrimitiveInputWrapper>
|
||||
)
|
||||
|
||||
@@ -16,10 +16,10 @@ export default class TagInput extends XoAbstractInput {
|
||||
disabled={props.disabled}
|
||||
hasSelectAll
|
||||
multi={props.multi}
|
||||
onChange={props.onChange}
|
||||
onChange={this._onChange}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
defaultValue={props.defaultValue}
|
||||
value={props.value}
|
||||
/>
|
||||
</PrimitiveInputWrapper>
|
||||
)
|
||||
|
||||
@@ -16,10 +16,10 @@ export default class VmInput extends XoAbstractInput {
|
||||
disabled={props.disabled}
|
||||
hasSelectAll
|
||||
multi={props.multi}
|
||||
onChange={props.onChange}
|
||||
onChange={this._onChange}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
defaultValue={props.defaultValue}
|
||||
value={props.value}
|
||||
/>
|
||||
</PrimitiveInputWrapper>
|
||||
)
|
||||
|
||||
@@ -381,7 +381,7 @@ export default class XoWeekCharts extends Component {
|
||||
<p className='mt-1'>
|
||||
{_('weeklyChartsScaleInfo')}
|
||||
{' '}
|
||||
<Toggle iconSize={1} icon='scale' className='btn btn-secondary' onChange={this._updateScale} />
|
||||
<Toggle iconSize={1} icon='scale' onChange={this._updateScale} />
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -128,6 +128,13 @@ export const connectStore = store => {
|
||||
sendUpdates()
|
||||
})
|
||||
subscribePermissions(permissions => store.dispatch(updatePermissions(permissions)))
|
||||
|
||||
// work around to keep the user in Redux store up to date
|
||||
//
|
||||
// FIXME: store subscriptions data directly in Redux
|
||||
subscribeUsers(user => {
|
||||
store.dispatch(signedIn(xo.user))
|
||||
})
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
@@ -299,14 +306,14 @@ export const exportConfig = () => (
|
||||
|
||||
// Server ------------------------------------------------------------
|
||||
|
||||
export const addServer = (host, username, password) => (
|
||||
_call('server.add', { host, username, password })::tap(
|
||||
export const addServer = (host, username, password, label) => (
|
||||
_call('server.add', { host, label, password, username })::tap(
|
||||
subscribeServers.forceRefresh
|
||||
)::rethrow(() => error(_('serverError'), _('serverAddFailed')))
|
||||
)
|
||||
|
||||
export const editServer = (server, { host, username, password, readOnly }) => (
|
||||
_call('server.set', { id: resolveId(server), host, username, password, readOnly })::tap(
|
||||
export const editServer = (server, props) => (
|
||||
_call('server.set', { ...props, id: resolveId(server) })::tap(
|
||||
subscribeServers.forceRefresh
|
||||
)
|
||||
)
|
||||
@@ -335,7 +342,7 @@ export const editPool = (pool, props) => (
|
||||
_call('pool.set', { id: resolveId(pool), ...props })
|
||||
)
|
||||
|
||||
import AddHostModalBody from './add-host-modal'
|
||||
import AddHostModalBody from './add-host-modal' // eslint-disable-line import/first
|
||||
export const addHostToPool = (pool, host) => {
|
||||
if (host) {
|
||||
return confirm({
|
||||
@@ -636,7 +643,7 @@ export const cloneVm = ({ id, name_label: nameLabel }, fullCopy = false) => (
|
||||
})
|
||||
)
|
||||
|
||||
import CopyVmModalBody from './copy-vm-modal'
|
||||
import CopyVmModalBody from './copy-vm-modal' // eslint-disable-line import/first
|
||||
export const copyVm = (vm, sr, name, compress) => {
|
||||
if (sr) {
|
||||
return confirm({
|
||||
@@ -660,7 +667,7 @@ export const copyVm = (vm, sr, name, compress) => {
|
||||
}
|
||||
}
|
||||
|
||||
import CopyVmsModalBody from './copy-vms-modal'
|
||||
import CopyVmsModalBody from './copy-vms-modal' // eslint-disable-line import/first
|
||||
export const copyVms = vms => {
|
||||
const _vms = resolveIds(vms)
|
||||
return confirm({
|
||||
@@ -678,7 +685,7 @@ export const copyVms = vms => {
|
||||
sr
|
||||
} = params
|
||||
Promise.all(map(_vms, (vm, index) =>
|
||||
_call('vm.copy', { vm, sr, compress, name: names[index] }),
|
||||
_call('vm.copy', { vm, sr, compress, name: names[index] })
|
||||
))
|
||||
},
|
||||
noop
|
||||
@@ -732,7 +739,7 @@ export const deleteSnapshot = vm => (
|
||||
)
|
||||
)
|
||||
|
||||
import MigrateVmModalBody from './migrate-vm-modal'
|
||||
import MigrateVmModalBody from './migrate-vm-modal' // eslint-disable-line import/first
|
||||
export const migrateVm = (vm, host) => (
|
||||
confirm({
|
||||
title: _('migrateVmModalTitle'),
|
||||
@@ -748,7 +755,7 @@ export const migrateVm = (vm, host) => (
|
||||
)
|
||||
)
|
||||
|
||||
import MigrateVmsModalBody from './migrate-vms-modal'
|
||||
import MigrateVmsModalBody from './migrate-vms-modal' // eslint-disable-line import/first
|
||||
export const migrateVms = vms => (
|
||||
confirm({
|
||||
title: _('migrateVmModalTitle'),
|
||||
@@ -831,7 +838,7 @@ export const importDeltaBackup = ({ remote, file, sr }) => (
|
||||
_call('vm.importDeltaBackup', resolveIds({ remote, filePath: file, sr }))
|
||||
)
|
||||
|
||||
import RevertSnapshotModalBody from './revert-snapshot-modal'
|
||||
import RevertSnapshotModalBody from './revert-snapshot-modal' // eslint-disable-line import/first
|
||||
export const revertSnapshot = vm => (
|
||||
confirm({
|
||||
title: _('revertVmModalTitle'),
|
||||
@@ -904,7 +911,7 @@ export const attachDiskToVm = (vdi, vm, { bootable, mode, position }) => (
|
||||
_call('vm.attachDisk', {
|
||||
bootable,
|
||||
mode,
|
||||
position: position && String(position) || undefined,
|
||||
position: (position && String(position)) || undefined,
|
||||
vdi: resolveId(vdi),
|
||||
vm: resolveId(vm)
|
||||
})
|
||||
@@ -1003,7 +1010,7 @@ export const editNetwork = (network, props) => (
|
||||
_call('network.set', { ...props, id: resolveId(network) })
|
||||
)
|
||||
|
||||
import CreateNetworkModalBody from './create-network-modal'
|
||||
import CreateNetworkModalBody from './create-network-modal' // eslint-disable-line import/first
|
||||
export const createNetwork = container => (
|
||||
confirm({
|
||||
icon: 'network',
|
||||
@@ -1023,7 +1030,7 @@ export const createNetwork = container => (
|
||||
export const getBondModes = () =>
|
||||
_call('network.getBondModes')
|
||||
|
||||
import CreateBondedNetworkModalBody from './create-bonded-network-modal'
|
||||
import CreateBondedNetworkModalBody from './create-bonded-network-modal' // eslint-disable-line import/first
|
||||
export const createBondedNetwork = container => (
|
||||
confirm({
|
||||
icon: 'network',
|
||||
@@ -1314,7 +1321,7 @@ export const loadPlugin = async id => (
|
||||
_call('plugin.load', { id })::tap(
|
||||
subscribePlugins.forceRefresh
|
||||
)::rethrow(
|
||||
err => error(_('pluginError'), err && err.message || _('unknownPluginError'))
|
||||
err => error(_('pluginError'), (err && err.message) || _('unknownPluginError'))
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1322,7 +1329,7 @@ export const unloadPlugin = id => (
|
||||
_call('plugin.unload', { id })::tap(
|
||||
subscribePlugins.forceRefresh
|
||||
)::rethrow(
|
||||
err => error(_('pluginError'), err && err.message || _('unknownPluginError'))
|
||||
err => error(_('pluginError'), (err && err.message) || _('unknownPluginError'))
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1338,7 +1345,7 @@ export const disablePluginAutoload = id => (
|
||||
)
|
||||
)
|
||||
|
||||
export const configurePlugin = (id, configuration) => {
|
||||
export const configurePlugin = (id, configuration) =>
|
||||
_call('plugin.configure', { id, configuration })::tap(
|
||||
() => {
|
||||
info(_('pluginConfigurationSuccess'), _('pluginConfigurationChanges'))
|
||||
@@ -1347,7 +1354,6 @@ export const configurePlugin = (id, configuration) => {
|
||||
)::rethrow(
|
||||
err => error(_('pluginError'), JSON.stringify(err.data) || _('unknownPluginError'))
|
||||
)
|
||||
}
|
||||
|
||||
export const purgePluginConfiguration = async id => {
|
||||
await confirm({
|
||||
@@ -1661,10 +1667,10 @@ const _setUserPreferences = preferences => (
|
||||
)
|
||||
)
|
||||
|
||||
import NewSshKeyModalBody from './new-ssh-key-modal'
|
||||
import NewSshKeyModalBody from './new-ssh-key-modal' // eslint-disable-line import/first
|
||||
export const addSshKey = key => {
|
||||
const { preferences } = xo.user
|
||||
const otherKeys = preferences && preferences.sshKeys || []
|
||||
const otherKeys = (preferences && preferences.sshKeys) || []
|
||||
if (key) {
|
||||
return _setUserPreferences({ sshKeys: [
|
||||
...otherKeys,
|
||||
@@ -1707,7 +1713,7 @@ export const deleteSshKey = key => (
|
||||
|
||||
// User filters --------------------------------------------------
|
||||
|
||||
import AddUserFilterModalBody from './add-user-filter-modal'
|
||||
import AddUserFilterModalBody from './add-user-filter-modal' // eslint-disable-line import/first
|
||||
export const addCustomFilter = (type, value) => {
|
||||
const { user } = xo
|
||||
return confirm({
|
||||
@@ -1817,7 +1823,7 @@ export const createXosanSR = ({ template, pif, vlan, srs, glusterType, redundanc
|
||||
|
||||
export const computeXosanPossibleOptions = lvmSrs => _call('xosan.computeXosanPossibleOptions', { lvmSrs })
|
||||
|
||||
import InstallXosanPackModal from './install-xosan-pack-modal'
|
||||
import InstallXosanPackModal from './install-xosan-pack-modal' // eslint-disable-line import/first
|
||||
export const downloadAndInstallXosanPack = pool =>
|
||||
confirm({
|
||||
title: _('xosanInstallPackTitle', { pool: pool.name_label }),
|
||||
|
||||
@@ -1,32 +1,53 @@
|
||||
import _ from 'intl'
|
||||
import Component from 'base-component'
|
||||
import React from 'react'
|
||||
import { connectStore, compareVersions } from 'utils'
|
||||
import { connectStore, compareVersions, isXosanPack } from 'utils'
|
||||
import { subscribeResourceCatalog, subscribePlugins } from 'xo'
|
||||
import { createGetObjectsOfType, createSelector } from 'selectors'
|
||||
import { createGetObjectsOfType, createSelector, createCollectionWrapper } from 'selectors'
|
||||
import { satisfies as versionSatisfies } from 'semver'
|
||||
import {
|
||||
every,
|
||||
filter,
|
||||
forEach,
|
||||
map
|
||||
map,
|
||||
some
|
||||
} from 'lodash'
|
||||
|
||||
const findLatestPack = packs => {
|
||||
let latestPack = packs[0]
|
||||
const findLatestPack = (packs, hostsVersions) => {
|
||||
const checkVersion = version =>
|
||||
every(hostsVersions, hostVersion => versionSatisfies(hostVersion, version))
|
||||
|
||||
let latestPack = { version: '0' }
|
||||
forEach(packs, pack => {
|
||||
if (compareVersions(pack.version, latestPack.version) > 0) {
|
||||
const xsVersionRequirement = pack.requirements && pack.requirements.xenserver
|
||||
|
||||
if (
|
||||
pack.type === 'iso' &&
|
||||
compareVersions(pack.version, latestPack.version) > 0 &&
|
||||
(!xsVersionRequirement || checkVersion(xsVersionRequirement))
|
||||
) {
|
||||
latestPack = pack
|
||||
}
|
||||
})
|
||||
|
||||
if (latestPack.version === '0') {
|
||||
// No compatible pack was found
|
||||
return
|
||||
}
|
||||
|
||||
return latestPack
|
||||
}
|
||||
|
||||
@connectStore({
|
||||
@connectStore(() => ({
|
||||
hosts: createGetObjectsOfType('host').filter(
|
||||
(_, { pool }) => host => pool && host.$pool === pool.id && !host.supplementalPacks['vates:XOSAN']
|
||||
createSelector(
|
||||
(_, { pool }) => pool != null && pool.id,
|
||||
poolId => poolId
|
||||
? host => host.$pool === poolId && !some(host.supplementalPacks, isXosanPack)
|
||||
: false
|
||||
)
|
||||
)
|
||||
}, { withRef: true })
|
||||
}), { withRef: true })
|
||||
export default class InstallXosanPackModal extends Component {
|
||||
componentDidMount () {
|
||||
this._unsubscribePlugins = subscribePlugins(plugins => this.setState({ plugins }))
|
||||
@@ -40,9 +61,16 @@ export default class InstallXosanPackModal extends Component {
|
||||
|
||||
_getXosanLatestPack = createSelector(
|
||||
() => this.state.catalog && this.state.catalog.xosan,
|
||||
xosanCatalog => findLatestPack(
|
||||
filter(xosanCatalog, (value, key) => key !== '_token' && value.type === 'iso')
|
||||
)
|
||||
createSelector(
|
||||
() => this.props.hosts,
|
||||
createCollectionWrapper(hosts => map(hosts, 'version'))
|
||||
),
|
||||
findLatestPack
|
||||
)
|
||||
|
||||
_getXosanPacks = createSelector(
|
||||
() => this.state.catalog && this.state.catalog.xosan,
|
||||
packs => filter(packs, ({ type }) => type === 'iso')
|
||||
)
|
||||
|
||||
get value () {
|
||||
@@ -54,13 +82,28 @@ export default class InstallXosanPackModal extends Component {
|
||||
const latestPack = this._getXosanLatestPack()
|
||||
|
||||
return <div>
|
||||
{_('xosanInstallPackOnHosts')}
|
||||
<ul>
|
||||
{map(hosts, host => <li key={host.id}>{host.name_label}</li>)}
|
||||
</ul>
|
||||
{latestPack && <div className='mt-1'>
|
||||
{_('xosanInstallPack', { pack: latestPack.name, version: latestPack.version })}
|
||||
</div>}
|
||||
{latestPack
|
||||
? <div>
|
||||
{_('xosanInstallPackOnHosts')}
|
||||
<ul>
|
||||
{map(hosts, host => <li key={host.id}>{host.name_label}</li>)}
|
||||
</ul>
|
||||
<div className='mt-1'>
|
||||
{_('xosanInstallPack', { pack: latestPack.name, version: latestPack.version })}
|
||||
</div>
|
||||
</div>
|
||||
: <div>
|
||||
<p>{_('xosanNoPackFound')}</p>
|
||||
<p>
|
||||
{_('xosanPackRequirements')}
|
||||
<ul>
|
||||
{map(this._getXosanPacks(), ({ name, requirements }) => <li>
|
||||
{name}: <strong>{requirements && requirements.xenserver ? requirements.xenserver : '/'}</strong>
|
||||
</li>)}
|
||||
</ul>
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,28 @@
|
||||
import forEach from 'lodash/forEach'
|
||||
import {
|
||||
forEach,
|
||||
includes,
|
||||
map
|
||||
} from 'lodash'
|
||||
|
||||
export const getDefaultNetworkForVif = (vif, destHost, pifs, networks) => {
|
||||
const originNetwork = networks[vif.$network]
|
||||
const originVlans = map(originNetwork.PIFs, pifId => pifs[pifId].vlan)
|
||||
|
||||
let destNetworkId = pifs[destHost.$PIFs[0]].$network
|
||||
|
||||
forEach(destHost.$PIFs, pifId => {
|
||||
const { $network, vlan } = pifs[pifId]
|
||||
|
||||
if (networks[$network].name_label === originNetwork.name_label) {
|
||||
destNetworkId = $network
|
||||
|
||||
export const getDefaultNetworkForVif = (vif, host, pifs, networks) => {
|
||||
const nameLabel = networks[vif.$network].name_label
|
||||
let defaultNetwork
|
||||
forEach(host.$PIFs, pifId => {
|
||||
const pif = pifs[pifId]
|
||||
if (networks[pif.$network].name_label === nameLabel) {
|
||||
defaultNetwork = pif.$network
|
||||
return false
|
||||
}
|
||||
|
||||
if (vlan !== -1 && includes(originVlans, vlan)) {
|
||||
destNetworkId = $network
|
||||
}
|
||||
})
|
||||
return defaultNetwork
|
||||
|
||||
return destNetworkId
|
||||
}
|
||||
|
||||
@@ -240,7 +240,7 @@ class XoaUpdater extends EventEmitter {
|
||||
this.registerState = 'error'
|
||||
}
|
||||
} finally {
|
||||
this.emit('registerState', {state: this.registerState, email: this.token && this.token.registrationEmail || '', error: this.registerError})
|
||||
this.emit('registerState', {state: this.registerState, email: (this.token && this.token.registrationEmail) || '', error: this.registerError})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,7 +262,7 @@ class XoaUpdater extends EventEmitter {
|
||||
this.registerState = 'error'
|
||||
}
|
||||
} finally {
|
||||
this.emit('registerState', {state: this.registerState, email: this.token && this.token.registrationEmail || '', error: this.registerError})
|
||||
this.emit('registerState', {state: this.registerState, email: (this.token && this.token.registrationEmail) || '', error: this.registerError})
|
||||
if (this.registerState === 'registered') {
|
||||
this.update()
|
||||
}
|
||||
@@ -351,7 +351,7 @@ class XoaUpdater extends EventEmitter {
|
||||
}
|
||||
|
||||
log (level, message) {
|
||||
message = message && message.message || String(message)
|
||||
message = (message != null && message.message) || String(message)
|
||||
const date = new Date()
|
||||
this._log.unshift({
|
||||
date: date.toLocaleString(),
|
||||
|
||||
@@ -9,20 +9,23 @@ import { connectStore, getXoaPlan } from './utils'
|
||||
import { isAdmin } from 'selectors'
|
||||
|
||||
const Upgrade = propTypes({
|
||||
available: propTypes.number.isRequired,
|
||||
place: propTypes.string.isRequired
|
||||
available: propTypes.number,
|
||||
place: propTypes.string.isRequired,
|
||||
required: propTypes.number
|
||||
})(connectStore({
|
||||
isAdmin
|
||||
}))(({
|
||||
available,
|
||||
children,
|
||||
isAdmin,
|
||||
place
|
||||
}) => (
|
||||
<Card>
|
||||
place,
|
||||
required = available
|
||||
}) => process.env.XOA_PLAN < required
|
||||
? <Card>
|
||||
<CardHeader>{_('upgradeNeeded')}</CardHeader>
|
||||
{isAdmin
|
||||
? <CardBlock className='text-xs-center'>
|
||||
<p>{_('availableIn', {plan: getXoaPlan(available)})}</p>
|
||||
<p>{_('availableIn', {plan: getXoaPlan(required)})}</p>
|
||||
<p>
|
||||
<a href={`https://xen-orchestra.com/#!/pricing?pk_campaign=xoa_${getXoaPlan()}_upgrade&pk_kwd=${place}`} className='btn btn-primary btn-lg'>
|
||||
<Icon icon='plan-upgrade' /> {_('upgradeNow')}
|
||||
@@ -37,6 +40,7 @@ const Upgrade = propTypes({
|
||||
</CardBlock>
|
||||
}
|
||||
</Card>
|
||||
))
|
||||
: children
|
||||
)
|
||||
|
||||
export { Upgrade as default }
|
||||
|
||||
@@ -268,7 +268,7 @@ export default class RestoreFileModalBody extends Component {
|
||||
value={partition}
|
||||
/>
|
||||
]}
|
||||
{(partition || disk && !scanDiskError && noPartitions) && [
|
||||
{(partition || (disk && !scanDiskError && noPartitions)) && [
|
||||
<br />,
|
||||
<Container>
|
||||
<Row>
|
||||
@@ -280,7 +280,7 @@ export default class RestoreFileModalBody extends Component {
|
||||
<Col size={2}>
|
||||
<span className='pull-right'>
|
||||
<Tooltip content={_('restoreFilesSelectAllFiles')}>
|
||||
<ActionButton btnStyle='secondary' handler={this._selectAllFolderFiles} icon='add' size='small' />
|
||||
<ActionButton handler={this._selectAllFolderFiles} icon='add' size='small' />
|
||||
</Tooltip>
|
||||
</span>
|
||||
</Col>
|
||||
@@ -322,12 +322,13 @@ export default class RestoreFileModalBody extends Component {
|
||||
<Col className='pl-0 pb-1' size={10}>
|
||||
<em>{_('restoreFilesSelectedFiles', { files: selectedFiles.length })}</em>
|
||||
</Col>
|
||||
<Col size={2}>
|
||||
<span className='pull-right'>
|
||||
<Tooltip content={_('restoreFilesUnselectAll')}>
|
||||
<ActionButton btnStyle='secondary' handler={this._unselectAllFiles} icon='remove' size='small' />
|
||||
</Tooltip>
|
||||
</span>
|
||||
<Col size={2} className='text-xs-right'>
|
||||
<ActionButton
|
||||
handler={this._unselectAllFiles}
|
||||
icon='remove'
|
||||
size='small'
|
||||
tooltip={_('restoreFilesUnselectAll')}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
{map(selectedFiles, file =>
|
||||
@@ -335,10 +336,8 @@ export default class RestoreFileModalBody extends Component {
|
||||
<Col size={10}>
|
||||
<pre>{file.path}</pre>
|
||||
</Col>
|
||||
<Col size={2}>
|
||||
<span className='pull-right'>
|
||||
<ActionButton btnStyle='secondary' handler={this._unselectFile} handlerParam={file} icon='remove' size='small' />
|
||||
</span>
|
||||
<Col size={2} className='text-xs-right'>
|
||||
<ActionButton handler={this._unselectFile} handlerParam={file} icon='remove' size='small' />
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
@@ -1,32 +1,39 @@
|
||||
import _ from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import Button from 'button'
|
||||
import Component from 'base-component'
|
||||
import delay from 'lodash/delay'
|
||||
import forEach from 'lodash/forEach'
|
||||
import GenericInput from 'json-schema-input'
|
||||
import getEventValue from 'get-event-value'
|
||||
import Icon from 'icon'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import map from 'lodash/map'
|
||||
import moment from 'moment-timezone'
|
||||
import React from 'react'
|
||||
import Scheduler, { SchedulePreview } from 'scheduling'
|
||||
import startsWith from 'lodash/startsWith'
|
||||
import uncontrollableInput from 'uncontrollable-input'
|
||||
import Upgrade from 'xoa-upgrade'
|
||||
import Wizard, { Section } from 'wizard'
|
||||
import { addSubscriptions } from 'utils'
|
||||
import { confirm } from 'modal'
|
||||
import { error } from 'notification'
|
||||
import { generateUiSchema } from 'xo-json-schema-input'
|
||||
import { SelectSubject } from 'select-objects'
|
||||
import { connectStore, EMPTY_OBJECT } from 'utils'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { createSelector } from 'reselect'
|
||||
import { generateUiSchema } from 'xo-json-schema-input'
|
||||
import { getUser } from 'selectors'
|
||||
import { SelectSubject } from 'select-objects'
|
||||
import {
|
||||
forEach,
|
||||
identity,
|
||||
isArray,
|
||||
map,
|
||||
mapValues,
|
||||
noop,
|
||||
startsWith
|
||||
} from 'lodash'
|
||||
|
||||
import {
|
||||
createJob,
|
||||
createSchedule,
|
||||
getRemote,
|
||||
editJob,
|
||||
editSchedule,
|
||||
subscribeCurrentUser
|
||||
editSchedule
|
||||
} from 'xo'
|
||||
|
||||
// ===================================================================
|
||||
@@ -52,13 +59,13 @@ const NO_SMART_UI_SCHEMA = generateUiSchema(NO_SMART_SCHEMA)
|
||||
const SMART_SCHEMA = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
status: {
|
||||
power_state: {
|
||||
default: 'All', // FIXME: can't translate
|
||||
enum: [ 'All', 'Running', 'Halted' ], // FIXME: can't translate
|
||||
title: _('editBackupSmartStatusTitle'),
|
||||
description: 'The statuses of VMs to backup.' // FIXME: can't translate
|
||||
},
|
||||
poolsOptions: {
|
||||
$pool: {
|
||||
type: 'object',
|
||||
title: _('editBackupSmartPools'),
|
||||
properties: {
|
||||
@@ -67,7 +74,7 @@ const SMART_SCHEMA = {
|
||||
title: _('editBackupNot'),
|
||||
description: 'Toggle on to backup VMs that are NOT resident on these pools'
|
||||
},
|
||||
pools: {
|
||||
values: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
@@ -78,7 +85,7 @@ const SMART_SCHEMA = {
|
||||
}
|
||||
}
|
||||
},
|
||||
tagsOptions: {
|
||||
tags: {
|
||||
type: 'object',
|
||||
title: _('editBackupSmartTags'),
|
||||
properties: {
|
||||
@@ -87,7 +94,7 @@ const SMART_SCHEMA = {
|
||||
title: _('editBackupNot'),
|
||||
description: 'Toggle on to backup VMs that do NOT contain these tags'
|
||||
},
|
||||
tags: {
|
||||
values: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
@@ -99,7 +106,7 @@ const SMART_SCHEMA = {
|
||||
}
|
||||
}
|
||||
},
|
||||
required: [ 'status', 'poolsOptions', 'tagsOptions' ]
|
||||
required: [ 'power_state', '$pool', 'tags' ]
|
||||
}
|
||||
const SMART_UI_SCHEMA = generateUiSchema(SMART_SCHEMA)
|
||||
|
||||
@@ -114,9 +121,14 @@ const COMMON_SCHEMA = {
|
||||
description: 'Back-up tag.' // FIXME: can't translate
|
||||
},
|
||||
_reportWhen: {
|
||||
default: 'failure',
|
||||
enum: [ 'never', 'always', 'failure' ], // FIXME: can't translate
|
||||
title: _('editBackupReportTitle'),
|
||||
description: 'When to send reports.' // FIXME: can't translate
|
||||
description: [
|
||||
'When to send reports.',
|
||||
'',
|
||||
'Plugins *tranport-email* and *backup-reports* need to be configured.'
|
||||
].join('\n')
|
||||
},
|
||||
enabled: {
|
||||
type: 'boolean',
|
||||
@@ -256,183 +268,177 @@ const BACKUP_METHOD_TO_INFO = {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const DEFAULT_CRON_PATTERN = '0 0 * * *'
|
||||
@uncontrollableInput()
|
||||
class TimeoutInput extends Component {
|
||||
_onChange = event => {
|
||||
const value = getEventValue(event).trim()
|
||||
this.props.onChange(value === '' ? null : +value * 1e3)
|
||||
}
|
||||
|
||||
function negatePattern (pattern, not = true) {
|
||||
render () {
|
||||
const { props } = this
|
||||
const { value } = props
|
||||
|
||||
return <input
|
||||
{...props}
|
||||
onChange={this._onChange}
|
||||
type='number'
|
||||
value={value == null ? '' : String(value / 1e3)}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const DEFAULT_CRON_PATTERN = '0 0 * * *'
|
||||
const DEFAULT_TIMEZONE = moment.tz.guess()
|
||||
|
||||
// xo-web v5.7.1 introduced a bug where an extra level
|
||||
// ({ id: { id: <id> } }) was introduced for the VM param.
|
||||
//
|
||||
// This code automatically unbox the ids.
|
||||
const extractId = value => {
|
||||
while (typeof value === 'object') {
|
||||
value = value.id
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
const destructPattern = (pattern, valueTransform = identity) => pattern && ({
|
||||
not: !!pattern.__not,
|
||||
values: valueTransform((pattern.__not || pattern).__or)
|
||||
})
|
||||
|
||||
const constructPattern = ({ not, values } = EMPTY_OBJECT, valueTransform = identity) => {
|
||||
if (values == null || !values.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const pattern = { __or: valueTransform(values) }
|
||||
return not
|
||||
? { __not: pattern }
|
||||
: pattern
|
||||
}
|
||||
|
||||
@addSubscriptions({
|
||||
currentUser: subscribeCurrentUser
|
||||
@connectStore({
|
||||
currentUser: getUser
|
||||
})
|
||||
export default class New extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state.cronPattern = DEFAULT_CRON_PATTERN
|
||||
}
|
||||
|
||||
componentWillReceiveProps (props) {
|
||||
const { currentUser } = props
|
||||
const { owner } = this.state
|
||||
|
||||
if (currentUser && !owner) {
|
||||
this.setState({ owner: currentUser.id })
|
||||
}
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
const { job, schedule } = this.props
|
||||
if (!job || !schedule) {
|
||||
if (job || schedule) { // Having only one of them is unexpected incomplete information
|
||||
error(_('backupEditNotFoundTitle'), _('backupEditNotFoundMessage'))
|
||||
_getParams = createSelector(
|
||||
() => this.props.job,
|
||||
job => {
|
||||
if (!job) {
|
||||
return { main: {}, vms: { vms: [] } }
|
||||
}
|
||||
this.setState({
|
||||
timezone: moment.tz.guess()
|
||||
})
|
||||
return
|
||||
}
|
||||
this.setState({
|
||||
backupInfo: BACKUP_METHOD_TO_INFO[job.method],
|
||||
cronPattern: schedule.cron,
|
||||
owner: job.userId,
|
||||
timezone: schedule.timezone || null
|
||||
}, () => delay(this._populateForm, 250, job)) // Work around.
|
||||
// Without the delay, some selects are not always ready to load a value
|
||||
// Values are displayed, but html5 compliant browsers say the value is required and empty on submit
|
||||
}
|
||||
|
||||
_populateForm = job => {
|
||||
let values = job.paramsVector.items
|
||||
const {
|
||||
backupInput,
|
||||
vmsInput
|
||||
} = this.refs
|
||||
const { items } = job.paramsVector
|
||||
|
||||
if (values.length === 1) {
|
||||
// Older versions of XenOrchestra uses only values[0].
|
||||
const array = values[0].values
|
||||
const config = array[0]
|
||||
const reportWhen = config._reportWhen
|
||||
// legacy backup jobs
|
||||
if (items.length === 1) {
|
||||
const { ...main } = items[0].values[0]
|
||||
|
||||
backupInput.value = {
|
||||
...config,
|
||||
_reportWhen:
|
||||
// Fix old reportWhen values...
|
||||
(reportWhen === 'fail' && 'failure') ||
|
||||
(reportWhen === 'alway' && 'always') ||
|
||||
reportWhen
|
||||
return {
|
||||
main,
|
||||
vms: { vms: map(items[0].values.slice(1), extractId) }
|
||||
}
|
||||
}
|
||||
vmsInput.value = { vms: map(array, ({ id, vm }) => id || vm) }
|
||||
} else {
|
||||
if (values[1].type === 'map') {
|
||||
// Smart backup.
|
||||
const {
|
||||
$pool: poolsOptions = {},
|
||||
tags: tagsOptions = {},
|
||||
power_state: status = 'All'
|
||||
} = values[1].collection.pattern
|
||||
|
||||
backupInput.value = values[0].values[0]
|
||||
// smart backup
|
||||
if (items[1].type === 'map') {
|
||||
const { pattern } = items[1].collection
|
||||
const { $pool, tags } = pattern
|
||||
|
||||
this.setState({
|
||||
smartBackupMode: true
|
||||
}, () => {
|
||||
vmsInput.value = {
|
||||
poolsOptions: {
|
||||
pools: poolsOptions.__not ? poolsOptions.__not.__or : poolsOptions.__or,
|
||||
not: !!poolsOptions.__not
|
||||
},
|
||||
status,
|
||||
tagsOptions: {
|
||||
tags: map(tagsOptions.__not ? tagsOptions.__not.__or : tagsOptions.__or, tag => tag[0]),
|
||||
not: !!tagsOptions.__not
|
||||
}
|
||||
return {
|
||||
main: items[0].values[0],
|
||||
vms: {
|
||||
$pool: destructPattern($pool),
|
||||
power_state: pattern.power_state,
|
||||
tags: destructPattern(tags, tags => map(tags, tag => isArray(tag) ? tag[0] : tag))
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// Normal backup.
|
||||
backupInput.value = values[1].values[0]
|
||||
vmsInput.value = { vms: values[0].values }
|
||||
}
|
||||
}
|
||||
|
||||
// normal backup
|
||||
return {
|
||||
main: items[1].values[0],
|
||||
vms: { vms: map(items[0].values, extractId) }
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
_getMainParams = () => this.state.mainParams || this._getParams().main
|
||||
_getVmsParam = () => this.state.vmsParam || this._getParams().vms
|
||||
|
||||
_getScheduling = createSelector(
|
||||
() => this.props.schedule,
|
||||
() => this.state.scheduling,
|
||||
(schedule, scheduling) => {
|
||||
if (scheduling !== undefined) {
|
||||
return scheduling
|
||||
}
|
||||
|
||||
const {
|
||||
cron = DEFAULT_CRON_PATTERN,
|
||||
timezone = DEFAULT_TIMEZONE
|
||||
} = schedule || EMPTY_OBJECT
|
||||
|
||||
return {
|
||||
cronPattern: cron,
|
||||
timezone
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
_handleSubmit = async () => {
|
||||
const { props, state } = this
|
||||
|
||||
const method = this._getValue('job', 'method')
|
||||
const backupInfo = BACKUP_METHOD_TO_INFO[method]
|
||||
|
||||
const {
|
||||
enabled,
|
||||
...callArgs
|
||||
} = this.refs.backupInput.value
|
||||
const vmsInputValue = this.refs.vmsInput.value
|
||||
|
||||
const {
|
||||
backupInfo,
|
||||
smartBackupMode,
|
||||
timezone,
|
||||
owner
|
||||
} = this.state
|
||||
|
||||
const { pools, not: notPools } = vmsInputValue.poolsOptions || {}
|
||||
const { tags, not: notTags } = vmsInputValue.tagsOptions || {}
|
||||
const formattedTags = map(tags, tag => [ tag ])
|
||||
|
||||
const paramsVector = !smartBackupMode
|
||||
? {
|
||||
type: 'crossProduct',
|
||||
items: [{
|
||||
type: 'set',
|
||||
values: map(vmsInputValue.vms, vm => ({ id: vm }))
|
||||
}, {
|
||||
type: 'set',
|
||||
values: [ callArgs ]
|
||||
}]
|
||||
} : {
|
||||
type: 'crossProduct',
|
||||
items: [{
|
||||
type: 'set',
|
||||
values: [ callArgs ]
|
||||
}, {
|
||||
type: 'map',
|
||||
collection: {
|
||||
type: 'fetchObjects',
|
||||
pattern: {
|
||||
$pool: isEmpty(pools)
|
||||
? undefined
|
||||
: negatePattern({ __or: pools }, notPools),
|
||||
power_state: vmsInputValue.status === 'All' ? undefined : vmsInputValue.status,
|
||||
tags: isEmpty(tags)
|
||||
? undefined
|
||||
: negatePattern({ __or: formattedTags }, notTags),
|
||||
type: 'VM'
|
||||
}
|
||||
},
|
||||
iteratee: {
|
||||
type: 'extractProperties',
|
||||
mapping: { id: 'id' }
|
||||
}
|
||||
}]
|
||||
}
|
||||
...mainParams
|
||||
} = this._getMainParams()
|
||||
const vms = this._getVmsParam()
|
||||
|
||||
const job = {
|
||||
...state.job,
|
||||
|
||||
type: 'call',
|
||||
key: backupInfo.jobKey,
|
||||
method: backupInfo.method,
|
||||
paramsVector,
|
||||
userId: owner
|
||||
paramsVector: {
|
||||
type: 'crossProduct',
|
||||
items: isArray(vms.vms)
|
||||
? [{
|
||||
type: 'set',
|
||||
values: map(vms.vms, vm => ({ id: extractId(vm) }))
|
||||
}, {
|
||||
type: 'set',
|
||||
values: [ mainParams ]
|
||||
}]
|
||||
: [{
|
||||
type: 'set',
|
||||
values: [ mainParams ]
|
||||
}, {
|
||||
type: 'map',
|
||||
collection: {
|
||||
type: 'fetchObjects',
|
||||
pattern: {
|
||||
$pool: constructPattern(vms.$pool),
|
||||
power_state: vms.power_state === 'All' ? undefined : vms.power_state,
|
||||
tags: constructPattern(vms.tags, tags => map(tags, tag => [ tag ])),
|
||||
type: 'VM'
|
||||
}
|
||||
},
|
||||
iteratee: {
|
||||
type: 'extractProperties',
|
||||
mapping: { id: 'id' }
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
// Update backup schedule.
|
||||
const { job: oldJob, schedule: oldSchedule } = this.props
|
||||
|
||||
if (oldJob && oldSchedule) {
|
||||
job.id = oldJob.id
|
||||
return editJob(job).then(() => editSchedule({
|
||||
...oldSchedule,
|
||||
cron: this.state.cronPattern,
|
||||
timezone
|
||||
}))
|
||||
}
|
||||
const scheduling = this._getScheduling()
|
||||
|
||||
let remoteId
|
||||
if (job.type === 'call') {
|
||||
@@ -465,57 +471,79 @@ export default class New extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
// Update backup schedule.
|
||||
const oldJob = props.job
|
||||
if (oldJob) {
|
||||
job.id = oldJob.id
|
||||
await editJob(job)
|
||||
|
||||
return editSchedule({
|
||||
id: props.schedule.id,
|
||||
cron: scheduling.cronPattern,
|
||||
timezone: scheduling.timezone
|
||||
})
|
||||
}
|
||||
|
||||
if (job.timeout === null) {
|
||||
delete job.timeout // only needed for job edition
|
||||
}
|
||||
|
||||
// Create backup schedule.
|
||||
return createSchedule(await createJob(job), { cron: this.state.cronPattern, enabled, timezone })
|
||||
return createSchedule(await createJob(job), {
|
||||
cron: scheduling.cronPattern,
|
||||
enabled,
|
||||
timezone: scheduling.timezone
|
||||
})
|
||||
}
|
||||
|
||||
_handleReset = () => {
|
||||
const { backupInput } = this.refs
|
||||
|
||||
if (backupInput) {
|
||||
backupInput.value = undefined
|
||||
}
|
||||
|
||||
this.setState({
|
||||
cronPattern: DEFAULT_CRON_PATTERN
|
||||
})
|
||||
}
|
||||
|
||||
_updateCronPattern = value => {
|
||||
this.setState(value)
|
||||
}
|
||||
|
||||
_handleBackupSelection = event => {
|
||||
const method = event.target.value
|
||||
|
||||
this.setState({
|
||||
showVersionWarning: method === 'vm.rollingDeltaBackup' || method === 'vm.deltaCopy',
|
||||
backupInfo: BACKUP_METHOD_TO_INFO[method]
|
||||
})
|
||||
this.setState(mapValues(this.state, noop))
|
||||
}
|
||||
|
||||
_handleSmartBackupMode = event => {
|
||||
this.setState({
|
||||
smartBackupMode: event.target.value === 'smart'
|
||||
})
|
||||
this.setState(
|
||||
event.target.value === 'smart'
|
||||
? { vmsParam: {} }
|
||||
: { vmsParam: { vms: [] } }
|
||||
)
|
||||
}
|
||||
|
||||
_subjectPredicate = ({ type, permission }) =>
|
||||
type === 'user' && permission === 'admin'
|
||||
|
||||
render () {
|
||||
const {
|
||||
backupInfo,
|
||||
cronPattern,
|
||||
smartBackupMode,
|
||||
timezone,
|
||||
owner,
|
||||
showVersionWarning
|
||||
} = this.state
|
||||
_getValue = (ns, key, defaultValue) => {
|
||||
let tmp
|
||||
|
||||
return process.env.XOA_PLAN > 1
|
||||
? (
|
||||
<Wizard>
|
||||
// look in the state
|
||||
if (
|
||||
(tmp = this.state[ns]) != null &&
|
||||
(tmp = tmp[key]) !== undefined
|
||||
) {
|
||||
return tmp
|
||||
}
|
||||
|
||||
// look in the props
|
||||
if (
|
||||
(tmp = this.props[ns]) != null &&
|
||||
(tmp = tmp[key]) !== undefined
|
||||
) {
|
||||
return tmp
|
||||
}
|
||||
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
render () {
|
||||
const method = this._getValue('job', 'method', '')
|
||||
const scheduling = this._getScheduling()
|
||||
const vms = this._getVmsParam()
|
||||
|
||||
const backupInfo = BACKUP_METHOD_TO_INFO[method]
|
||||
const smartBackupMode = !isArray(vms.vms)
|
||||
|
||||
return (
|
||||
<Upgrade place='newBackup' required={2}>
|
||||
<Wizard><form id='form-new-vm-backup'>
|
||||
<Section icon='backup' title={this.props.job ? 'editVmBackup' : 'newVmBackup'}>
|
||||
<Container>
|
||||
<Row>
|
||||
@@ -523,88 +551,96 @@ export default class New extends Component {
|
||||
<fieldset className='form-group'>
|
||||
<label>{_('backupOwner')}</label>
|
||||
<SelectSubject
|
||||
onChange={this.linkState('owner', 'id')}
|
||||
onChange={this.linkState('job.userId', 'id')}
|
||||
predicate={this._subjectPredicate}
|
||||
required
|
||||
value={owner || null}
|
||||
value={this._getValue('job', 'userId', this.props.currentUser.id)}
|
||||
/>
|
||||
</fieldset>
|
||||
<fieldset className='form-group'>
|
||||
<label>{_('jobTimeoutPlaceHolder')}</label>
|
||||
<TimeoutInput
|
||||
className='form-control'
|
||||
onChange={this.linkState('job.timeout')}
|
||||
value={this._getValue('job', 'timeout')}
|
||||
/>
|
||||
</fieldset>
|
||||
<fieldset className='form-group'>
|
||||
<label htmlFor='selectBackup'>{_('newBackupSelection')}</label>
|
||||
<select
|
||||
className='form-control'
|
||||
value={(backupInfo && backupInfo.method) || ''}
|
||||
id='selectBackup'
|
||||
onChange={this._handleBackupSelection}
|
||||
onChange={this.linkState('job.method')}
|
||||
required
|
||||
value={method}
|
||||
>
|
||||
{_('noSelectedValue', message => <option value=''>{message}</option>)}
|
||||
{map(BACKUP_METHOD_TO_INFO, (info, key) =>
|
||||
_(info.label, message => <option key={key} value={key}>{message}</option>)
|
||||
_(info.label, message => <option key={key} value={key}>{message}</option>)
|
||||
)}
|
||||
</select>
|
||||
</fieldset>
|
||||
{showVersionWarning && <div className='alert alert-warning' role='alert'>
|
||||
{(method === 'vm.rollingDeltaBackup' || method === 'vm.deltaCopy') && <div className='alert alert-warning' role='alert'>
|
||||
<Icon icon='error' /> {_('backupVersionWarning')}
|
||||
</div>}
|
||||
<form id='form-new-vm-backup'>
|
||||
{backupInfo && <div>
|
||||
<GenericInput
|
||||
label={<span><Icon icon={backupInfo.icon} /> {_(backupInfo.label)}</span>}
|
||||
ref='backupInput'
|
||||
{backupInfo && <div>
|
||||
<GenericInput
|
||||
label={<span><Icon icon={backupInfo.icon} /> {_(backupInfo.label)}</span>}
|
||||
required
|
||||
schema={backupInfo.schema}
|
||||
uiSchema={backupInfo.uiSchema}
|
||||
onChange={this.linkState('mainParams')}
|
||||
value={this._getMainParams()}
|
||||
/>
|
||||
<fieldset className='form-group'>
|
||||
<label htmlFor='smartMode'>{_('smartBackupModeSelection')}</label>
|
||||
<select
|
||||
className='form-control'
|
||||
id='smartMode'
|
||||
onChange={this._handleSmartBackupMode}
|
||||
required
|
||||
schema={backupInfo.schema}
|
||||
uiSchema={backupInfo.uiSchema}
|
||||
/>
|
||||
<fieldset className='form-group'>
|
||||
<label htmlFor='smartMode'>{_('smartBackupModeSelection')}</label>
|
||||
<select
|
||||
className='form-control'
|
||||
id='smartMode'
|
||||
onChange={this._handleSmartBackupMode}
|
||||
required
|
||||
value={smartBackupMode ? 'smart' : 'normal'}
|
||||
>
|
||||
{_('normalBackup', message => <option value='normal'>{message}</option>)}
|
||||
{_('smartBackup', message => <option value='smart'>{message}</option>)}
|
||||
</select>
|
||||
</fieldset>
|
||||
{smartBackupMode
|
||||
? (process.env.XOA_PLAN > 2
|
||||
? <GenericInput
|
||||
label={<span><Icon icon='vm' /> {_('vmsToBackup')}</span>}
|
||||
ref='vmsInput'
|
||||
required
|
||||
schema={SMART_SCHEMA}
|
||||
uiSchema={SMART_UI_SCHEMA}
|
||||
/>
|
||||
: <Container><Upgrade place='newBackup' available={3} /></Container>
|
||||
) : <GenericInput
|
||||
value={smartBackupMode ? 'smart' : 'normal'}
|
||||
>
|
||||
{_('normalBackup', message => <option value='normal'>{message}</option>)}
|
||||
{_('smartBackup', message => <option value='smart'>{message}</option>)}
|
||||
</select>
|
||||
</fieldset>
|
||||
{smartBackupMode
|
||||
? <Upgrade place='newBackup' required={3}>
|
||||
<GenericInput
|
||||
label={<span><Icon icon='vm' /> {_('vmsToBackup')}</span>}
|
||||
ref='vmsInput'
|
||||
onChange={this.linkState('vmsParam')}
|
||||
required
|
||||
schema={NO_SMART_SCHEMA}
|
||||
uiSchema={NO_SMART_UI_SCHEMA}
|
||||
/>
|
||||
}
|
||||
</div>}
|
||||
</form>
|
||||
schema={SMART_SCHEMA}
|
||||
uiSchema={SMART_UI_SCHEMA}
|
||||
value={vms}
|
||||
/>
|
||||
</Upgrade>
|
||||
: <GenericInput
|
||||
label={<span><Icon icon='vm' /> {_('vmsToBackup')}</span>}
|
||||
onChange={this.linkState('vmsParam')}
|
||||
required
|
||||
schema={NO_SMART_SCHEMA}
|
||||
uiSchema={NO_SMART_UI_SCHEMA}
|
||||
value={vms}
|
||||
/>
|
||||
}
|
||||
</div>}
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</Section>
|
||||
<Section icon='schedule' title='schedule'>
|
||||
<Scheduler
|
||||
cronPattern={cronPattern}
|
||||
onChange={this._updateCronPattern}
|
||||
timezone={timezone}
|
||||
onChange={this.linkState('scheduling')}
|
||||
value={scheduling}
|
||||
/>
|
||||
</Section>
|
||||
<Section icon='preview' title='preview' summary>
|
||||
<Container>
|
||||
<Row>
|
||||
<Col>
|
||||
<SchedulePreview cronPattern={cronPattern} />
|
||||
<SchedulePreview cronPattern={scheduling.cronPattern} />
|
||||
{process.env.XOA_PLAN < 4 && backupInfo && process.env.XOA_PLAN < REQUIRED_XOA_PLAN[backupInfo.jobKey]
|
||||
? <Upgrade place='newBackup' available={REQUIRED_XOA_PLAN[backupInfo.jobKey]} />
|
||||
: (smartBackupMode && process.env.XOA_PLAN < 3
|
||||
@@ -612,26 +648,27 @@ export default class New extends Component {
|
||||
: <fieldset className='pull-right pt-1'>
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
className='btn-lg mr-1'
|
||||
className='mr-1'
|
||||
disabled={!backupInfo}
|
||||
form='form-new-vm-backup'
|
||||
handler={this._handleSubmit}
|
||||
icon='save'
|
||||
redirectOnSuccess='/backup/overview'
|
||||
size='large'
|
||||
>
|
||||
{_('saveBackupJob')}
|
||||
</ActionButton>
|
||||
<button type='button' className='btn btn-lg btn-secondary' onClick={this._handleReset}>
|
||||
<Button onClick={this._handleReset} size='large'>
|
||||
{_('selectTableReset')}
|
||||
</button>
|
||||
</Button>
|
||||
</fieldset>)
|
||||
}
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</Section>
|
||||
</Wizard>
|
||||
)
|
||||
: <Container><Upgrade place='newBackup' available={2} /></Container>
|
||||
</form></Wizard>
|
||||
</Upgrade>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import _ from 'intl'
|
||||
import ActionRowButton from 'action-row-button'
|
||||
import ActionToggle from 'action-toggle'
|
||||
import ButtonGroup from 'button-group'
|
||||
import Component from 'base-component'
|
||||
import filter from 'lodash/filter'
|
||||
import find from 'lodash/find'
|
||||
@@ -13,9 +13,9 @@ import map from 'lodash/map'
|
||||
import orderBy from 'lodash/orderBy'
|
||||
import React from 'react'
|
||||
import SortedTable from 'sorted-table'
|
||||
import StateButton from 'state-button'
|
||||
import Tooltip from 'tooltip'
|
||||
import { addSubscriptions } from 'utils'
|
||||
import { ButtonGroup } from 'react-bootstrap-4/lib'
|
||||
import { createSelector } from 'selectors'
|
||||
import {
|
||||
Card,
|
||||
@@ -45,13 +45,20 @@ const jobKeyToLabel = {
|
||||
|
||||
const JOB_COLUMNS = [
|
||||
{
|
||||
name: _('job'),
|
||||
itemRenderer: ({ jobId, jobLabel }) => <span>{jobId.slice(4, 8)} ({jobLabel})</span>,
|
||||
name: _('jobId'),
|
||||
itemRenderer: ({ jobId }) => jobId.slice(4, 8),
|
||||
sortCriteria: 'jobId'
|
||||
},
|
||||
{
|
||||
name: _('jobType'),
|
||||
itemRenderer: ({ jobLabel }) => jobLabel,
|
||||
sortCriteria: 'jobLabel'
|
||||
},
|
||||
{
|
||||
name: _('jobTag'),
|
||||
itemRenderer: ({ scheduleTag }) => scheduleTag
|
||||
itemRenderer: ({ scheduleTag }) => scheduleTag,
|
||||
default: true,
|
||||
sortCriteria: ({ scheduleTag }) => scheduleTag
|
||||
},
|
||||
{
|
||||
name: _('jobScheduling'),
|
||||
@@ -65,16 +72,23 @@ const JOB_COLUMNS = [
|
||||
},
|
||||
{
|
||||
name: _('jobState'),
|
||||
itemRenderer: ({ schedule, scheduleToggleValue }) => <ActionToggle
|
||||
value={scheduleToggleValue}
|
||||
handler={scheduleToggleValue ? disableSchedule : enableSchedule}
|
||||
itemRenderer: ({ schedule, scheduleToggleValue }) => <StateButton
|
||||
disabledLabel={_('jobStateDisabled')}
|
||||
disabledHandler={enableSchedule}
|
||||
disabledTooltip={_('logIndicationToEnable')}
|
||||
|
||||
enabledLabel={_('jobStateEnabled')}
|
||||
enabledHandler={disableSchedule}
|
||||
enabledTooltip={_('logIndicationToDisable')}
|
||||
|
||||
handlerParam={schedule.id}
|
||||
size='small'
|
||||
state={scheduleToggleValue}
|
||||
/>,
|
||||
sortCriteria: 'scheduleToggleValue'
|
||||
},
|
||||
{
|
||||
itemRenderer: ({ schedule }, isScheduleUserMissing) => <fieldset className='pull-right'>
|
||||
name: _('jobAction'),
|
||||
itemRenderer: ({ schedule }, isScheduleUserMissing) => <fieldset>
|
||||
{!isScheduleUserMissing[schedule.id] && <Tooltip content={_('backupUserNotFound')}><Icon className='mr-1' icon='error' /></Tooltip>}
|
||||
<Link className='btn btn-sm btn-primary mr-1' to={`/backup/${schedule.id}/edit`}>
|
||||
<Icon icon='edit' />
|
||||
@@ -94,7 +108,8 @@ const JOB_COLUMNS = [
|
||||
handlerParam={schedule.job}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</fieldset>
|
||||
</fieldset>,
|
||||
textAlign: 'right'
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import _ from 'intl'
|
||||
import ButtonGroup from 'button-group'
|
||||
import ChartistGraph from 'react-chartist'
|
||||
import Component from 'base-component'
|
||||
import forEach from 'lodash/forEach'
|
||||
@@ -10,7 +11,6 @@ import HostsPatchesTable from 'hosts-patches-table'
|
||||
import React from 'react'
|
||||
import size from 'lodash/size'
|
||||
import Upgrade from 'xoa-upgrade'
|
||||
import { ButtonGroup } from 'react-bootstrap-4/lib'
|
||||
import { Card, CardBlock, CardHeader } from 'card'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import {
|
||||
|
||||
@@ -305,32 +305,22 @@ class SelectMetric extends Component {
|
||||
/>
|
||||
</div>
|
||||
<div className='btn-group mt-1' role='group'>
|
||||
<button
|
||||
className='btn btn-secondary'
|
||||
onClick={this._resetSelection}
|
||||
tooltip={_('dashboardStatsButtonRemoveAll')}
|
||||
type='button'
|
||||
>
|
||||
<Icon icon='remove' />
|
||||
</button>
|
||||
<button
|
||||
className='btn btn-secondary'
|
||||
onClick={this._selectAllHosts}
|
||||
tooltip={_('dashboardStatsButtonAddAllHost')}
|
||||
type='button'
|
||||
>
|
||||
<Icon icon='host' />
|
||||
</button>
|
||||
<button
|
||||
className='btn btn-secondary'
|
||||
onClick={this._selectAllVms}
|
||||
tooltip={_('dashboardStatsButtonAddAllVM')}
|
||||
type='button'
|
||||
>
|
||||
<Icon icon='vm' />
|
||||
</button>
|
||||
<ActionButton
|
||||
btnStyle='secondary'
|
||||
handler={this._resetSelection}
|
||||
icon='remove'
|
||||
tooltip={_('dashboardStatsButtonRemoveAll')}
|
||||
/>
|
||||
<ActionButton
|
||||
handler={this._selectAllHosts}
|
||||
icon='host'
|
||||
tooltip={_('dashboardStatsButtonAddAllHost')}
|
||||
/>
|
||||
<ActionButton
|
||||
handler={this._selectAllVms}
|
||||
icon='vm'
|
||||
tooltip={_('dashboardStatsButtonAddAllVM')}
|
||||
/>
|
||||
<ActionButton
|
||||
disabled={!objects.length}
|
||||
handler={this._validSelection}
|
||||
icon='success'
|
||||
|
||||
@@ -26,7 +26,9 @@ import {
|
||||
} from 'utils'
|
||||
import {
|
||||
createDoesHostNeedRestart,
|
||||
createGetObject
|
||||
createGetObject,
|
||||
createGetObjectsOfType,
|
||||
createSelector
|
||||
} from 'selectors'
|
||||
import {
|
||||
CpuSparkLines,
|
||||
@@ -79,7 +81,13 @@ class MiniStats extends Component {
|
||||
|
||||
@connectStore(({
|
||||
container: createGetObject((_, props) => props.item.$pool),
|
||||
needsRestart: createDoesHostNeedRestart((_, props) => props.item)
|
||||
needsRestart: createDoesHostNeedRestart((_, props) => props.item),
|
||||
nVms: createGetObjectsOfType('VM').count(
|
||||
createSelector(
|
||||
(_, props) => props.item.id,
|
||||
hostId => obj => obj.$container === hostId
|
||||
)
|
||||
)
|
||||
}))
|
||||
export default class HostItem extends Component {
|
||||
get _isRunning () {
|
||||
@@ -97,7 +105,7 @@ export default class HostItem extends Component {
|
||||
_onSelect = () => this.props.onSelect(this.props.item.id)
|
||||
|
||||
render () {
|
||||
const { item: host, container, expandAll, selected } = this.props
|
||||
const { item: host, container, expandAll, selected, nVms } = this.props
|
||||
return <div className={styles.item}>
|
||||
<BlockLink to={`/hosts/${host.id}`}>
|
||||
<SingleLineRow>
|
||||
@@ -129,6 +137,15 @@ export default class HostItem extends Component {
|
||||
<Col mediumSize={3} className='hidden-lg-down'>
|
||||
<EllipsisContainer>
|
||||
<span className={styles.itemActionButons}>
|
||||
<Tooltip content={<span>{nVms}x {_('vmsTabName')}</span>}>
|
||||
{(nVms > 0)
|
||||
? <Link to={`/home?s=$container:${host.id}&t=VM`}>
|
||||
<Icon icon='vm' size='1' fixedWidth />
|
||||
</Link>
|
||||
: <Icon icon='vm' size='1' fixedWidth />
|
||||
}
|
||||
</Tooltip>
|
||||
|
||||
{this._isRunning
|
||||
? <span>
|
||||
<Tooltip content={_('stopHostLabel')}>
|
||||
|
||||
@@ -2,27 +2,36 @@ import * as ComplexMatcher from 'complex-matcher'
|
||||
import * as homeFilters from 'home-filters'
|
||||
import _ from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import ceil from 'lodash/ceil'
|
||||
import Button from 'button'
|
||||
import CenterPanel from 'center-panel'
|
||||
import Component from 'base-component'
|
||||
import debounce from 'lodash/debounce'
|
||||
import find from 'lodash/find'
|
||||
import forEach from 'lodash/forEach'
|
||||
import Icon from 'icon'
|
||||
import invoke from 'invoke'
|
||||
import keys from 'lodash/keys'
|
||||
import includes from 'lodash/includes'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import isString from 'lodash/isString'
|
||||
import Link from 'link'
|
||||
import map from 'lodash/map'
|
||||
import Page from '../page'
|
||||
import React from 'react'
|
||||
import Shortcuts from 'shortcuts'
|
||||
import SingleLineRow from 'single-line-row'
|
||||
import size from 'lodash/size'
|
||||
import Tooltip from 'tooltip'
|
||||
import { Card, CardHeader, CardBlock } from 'card'
|
||||
import {
|
||||
ceil,
|
||||
debounce,
|
||||
filter,
|
||||
find,
|
||||
forEach,
|
||||
get,
|
||||
identity,
|
||||
includes,
|
||||
isEmpty,
|
||||
isString,
|
||||
keys,
|
||||
map,
|
||||
pick,
|
||||
pickBy,
|
||||
size,
|
||||
some
|
||||
} from 'lodash'
|
||||
import {
|
||||
addCustomFilter,
|
||||
copyVms,
|
||||
@@ -67,7 +76,6 @@ import {
|
||||
getUser
|
||||
} from 'selectors'
|
||||
import {
|
||||
Button,
|
||||
DropdownButton,
|
||||
MenuItem,
|
||||
OverlayTrigger,
|
||||
@@ -219,6 +227,10 @@ export default class Home extends Component {
|
||||
router: React.PropTypes.object
|
||||
}
|
||||
|
||||
state = {
|
||||
selectedItems: {}
|
||||
}
|
||||
|
||||
get page () {
|
||||
return this.state.page
|
||||
}
|
||||
@@ -231,13 +243,31 @@ export default class Home extends Component {
|
||||
}
|
||||
|
||||
componentWillReceiveProps (props) {
|
||||
this._initFilterAndSortBy(props)
|
||||
if (this._getFilter() !== this._getFilter(props)) {
|
||||
this._initFilterAndSortBy(props)
|
||||
}
|
||||
if (props.type !== this.props.type) {
|
||||
this.setState({ highlighted: undefined })
|
||||
this.setState({ activePage: undefined, highlighted: undefined })
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
const { selectedItems } = this.state
|
||||
|
||||
// Unselect items that are no longer visible
|
||||
if ((this._visibleItemsRecomputations || 0) < (this._visibleItemsRecomputations = this._getVisibleItems.recomputations())) {
|
||||
const newSelectedItems = pick(selectedItems, map(this._getVisibleItems(), 'id'))
|
||||
if (size(newSelectedItems) < this._getNumberOfSelectedItems()) {
|
||||
this.setState({ selectedItems: newSelectedItems })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_getNumberOfItems = createCounter(() => this.props.items)
|
||||
_getNumberOfSelectedItems = createCounter(
|
||||
() => this.state.selectedItems,
|
||||
[ identity ]
|
||||
)
|
||||
|
||||
_getType () {
|
||||
return this.props.type
|
||||
@@ -249,33 +279,20 @@ export default class Home extends Component {
|
||||
pathname,
|
||||
query: { ...query, t: type, s: undefined }
|
||||
})
|
||||
this.setState({ highlighted: undefined })
|
||||
}
|
||||
|
||||
// Filter and sort -----------------------------------------------------------
|
||||
|
||||
_getDefaultFilter (props = this.props) {
|
||||
const { type, user } = props
|
||||
|
||||
const defaultFilter = OPTIONS[type].defaultFilter
|
||||
|
||||
// No user.
|
||||
if (!user) {
|
||||
return defaultFilter
|
||||
}
|
||||
|
||||
const { defaultHomeFilters = {}, filters = {} } = user.preferences || {}
|
||||
const filterName = defaultHomeFilters[type]
|
||||
|
||||
// No filter defined in preferences.
|
||||
if (!filterName) {
|
||||
return defaultFilter
|
||||
}
|
||||
|
||||
// Filter defined.
|
||||
let tmp
|
||||
const { type } = props
|
||||
const preferences = get(props, 'user.preferences')
|
||||
const defaultFilterName = get(preferences, [ 'defaultHomeFilters', type ])
|
||||
return firstDefined(
|
||||
(tmp = homeFilters[type]) && tmp[filterName],
|
||||
(tmp = filters[type]) && tmp[filterName],
|
||||
defaultFilter
|
||||
defaultFilterName && firstDefined(
|
||||
get(homeFilters, [ type, defaultFilterName ]),
|
||||
get(preferences, [ 'filters', type, defaultFilterName ])
|
||||
),
|
||||
OPTIONS[type].defaultFilter
|
||||
)
|
||||
}
|
||||
|
||||
@@ -296,7 +313,7 @@ export default class Home extends Component {
|
||||
const defaultFilter = this._getDefaultFilter(props)
|
||||
|
||||
if (defaultFilter != null) {
|
||||
this._setFilter(defaultFilter, props)
|
||||
this._setFilter(defaultFilter, props, true)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -342,13 +359,13 @@ export default class Home extends Component {
|
||||
|
||||
// Optionally can take the props to be able to use it in
|
||||
// componentWillReceiveProps().
|
||||
_setFilter (filter, props = this.props) {
|
||||
_setFilter (filter, props = this.props, replace) {
|
||||
if (!isString(filter)) {
|
||||
filter = filter::ComplexMatcher.toString()
|
||||
}
|
||||
|
||||
const { pathname, query } = props.location
|
||||
this.context.router.push({
|
||||
this.context.router[replace ? 'replace' : 'push']({
|
||||
pathname,
|
||||
query: { ...query, s: filter }
|
||||
})
|
||||
@@ -385,6 +402,11 @@ export default class Home extends Component {
|
||||
|
||||
_tick = isCriteria => <Icon icon={isCriteria ? 'success' : undefined} fixedWidth />
|
||||
|
||||
// High level filters --------------------------------------------------------
|
||||
|
||||
_typesDropdownItems = map(TYPES, (label, type) =>
|
||||
<MenuItem key={type} onClick={() => this._setType(type)}>{label}</MenuItem>
|
||||
)
|
||||
_updateSelectedPools = pools => {
|
||||
const filter = this._getParsedFilter()
|
||||
|
||||
@@ -424,42 +446,12 @@ export default class Home extends Component {
|
||||
: filter::ComplexMatcher.removePropertyClause('tags')
|
||||
)
|
||||
}
|
||||
|
||||
// Checkboxes
|
||||
_selectedItems = {}
|
||||
_updateMasterCheckbox () {
|
||||
const masterCheckbox = this.refs.masterCheckbox
|
||||
if (!masterCheckbox) {
|
||||
return
|
||||
}
|
||||
const noneChecked = isEmpty(this._selectedItems)
|
||||
masterCheckbox.checked = !noneChecked
|
||||
masterCheckbox.indeterminate = !noneChecked && size(this._selectedItems) !== this._getFilteredItems().length
|
||||
this.setState({ displayActions: !noneChecked })
|
||||
}
|
||||
_selectItem = (id, checked) => {
|
||||
const shouldBeChecked = checked === undefined ? !this._selectedItems[id] : checked
|
||||
shouldBeChecked ? this._selectedItems[id] = true : delete this._selectedItems[id]
|
||||
this.forceUpdate()
|
||||
this._updateMasterCheckbox()
|
||||
}
|
||||
_selectAllItems = (checked) => {
|
||||
const shouldBeChecked = checked === undefined ? !size(this._selectedItems) : checked
|
||||
this._selectedItems = {}
|
||||
forEach(this._getFilteredItems(), item => {
|
||||
shouldBeChecked && (this._selectedItems[item.id] = true)
|
||||
})
|
||||
this.forceUpdate()
|
||||
this._updateMasterCheckbox()
|
||||
}
|
||||
|
||||
_addCustomFilter = () => {
|
||||
return addCustomFilter(
|
||||
this._getType(),
|
||||
this._getFilter()
|
||||
)
|
||||
}
|
||||
|
||||
_getCustomFilters () {
|
||||
const { preferences } = this.props.user || {}
|
||||
|
||||
@@ -471,6 +463,34 @@ export default class Home extends Component {
|
||||
return customFilters[this._getType()]
|
||||
}
|
||||
|
||||
// Checkboxes ----------------------------------------------------------------
|
||||
|
||||
_getIsAllSelected = createSelector(
|
||||
() => this.state.selectedItems,
|
||||
this._getVisibleItems,
|
||||
(selectedItems, visibleItems) =>
|
||||
size(visibleItems) > 0 && size(filter(selectedItems)) === size(visibleItems)
|
||||
)
|
||||
_getIsSomeSelected = createSelector(
|
||||
() => this.state.selectedItems,
|
||||
some
|
||||
)
|
||||
_toggleMaster = () => {
|
||||
const selectedItems = {}
|
||||
if (!this._getIsAllSelected()) {
|
||||
forEach(this._getVisibleItems(), ({ id }) => {
|
||||
selectedItems[id] = true
|
||||
})
|
||||
}
|
||||
this.setState({ selectedItems })
|
||||
}
|
||||
_getSelectedItemsIds = createSelector(
|
||||
() => this.state.selectedItems,
|
||||
items => keys(pickBy(items))
|
||||
)
|
||||
|
||||
// Shortcuts -----------------------------------------------------------------
|
||||
|
||||
_getShortcutsHandler = createSelector(
|
||||
() => this._getVisibleItems(),
|
||||
items => (command, event) => {
|
||||
@@ -486,7 +506,13 @@ export default class Home extends Component {
|
||||
this.setState({ highlighted: (this.state.highlighted + items.length - 1) % items.length || 0 })
|
||||
break
|
||||
case 'SELECT':
|
||||
this._selectItem(items[this.state.highlighted].id)
|
||||
const itemId = items[this.state.highlighted].id
|
||||
this.setState({
|
||||
selectedItems: {
|
||||
...this.state.selectedItems,
|
||||
[itemId]: !this.state.selectedItems[itemId]
|
||||
}
|
||||
})
|
||||
break
|
||||
case 'JUMP_INTO':
|
||||
const item = items[this.state.highlighted]
|
||||
@@ -499,9 +525,7 @@ export default class Home extends Component {
|
||||
}
|
||||
)
|
||||
|
||||
_typesDropdownItems = map(TYPES, (label, type) =>
|
||||
<MenuItem onClick={() => this._setType(type)}>{label}</MenuItem>
|
||||
)
|
||||
// Header --------------------------------------------------------------------
|
||||
|
||||
_renderHeader () {
|
||||
const { type } = this.props
|
||||
@@ -544,11 +568,9 @@ export default class Home extends Component {
|
||||
type='text'
|
||||
/>
|
||||
<div className='input-group-btn'>
|
||||
<a
|
||||
className='btn btn-secondary'
|
||||
onClick={this._clearFilter}>
|
||||
<Button onClick={this._clearFilter}>
|
||||
<Icon icon='clear-search' />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
<div className='input-group-btn'>
|
||||
<ActionButton
|
||||
@@ -570,19 +592,26 @@ export default class Home extends Component {
|
||||
</Container>
|
||||
}
|
||||
|
||||
render () {
|
||||
const { props } = this
|
||||
const { user } = this.props
|
||||
const isAdmin = user && user.permission === 'admin'
|
||||
const noRegisteredServers = !props.servers || !props.servers.length
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
if (!props.areObjectsFetched) {
|
||||
render () {
|
||||
const {
|
||||
areObjectsFetched,
|
||||
noServersConnected,
|
||||
servers,
|
||||
user
|
||||
} = this.props
|
||||
|
||||
const isAdmin = user && user.permission === 'admin'
|
||||
const noRegisteredServers = !servers || !servers.length
|
||||
|
||||
if (!areObjectsFetched) {
|
||||
return <CenterPanel>
|
||||
<h2><img src='assets/loading.svg' /></h2>
|
||||
</CenterPanel>
|
||||
}
|
||||
|
||||
if (props.noServersConnected && isAdmin) {
|
||||
if (noServersConnected && isAdmin) {
|
||||
return <CenterPanel>
|
||||
<Card shadow>
|
||||
<CardHeader>{_('homeWelcome')}</CardHeader>
|
||||
@@ -654,12 +683,35 @@ export default class Home extends Component {
|
||||
|
||||
const filteredItems = this._getFilteredItems()
|
||||
const visibleItems = this._getVisibleItems()
|
||||
const { activePage, sortBy, highlighted } = this.state
|
||||
const { type } = props
|
||||
|
||||
const {
|
||||
activePage,
|
||||
expandAll,
|
||||
highlighted,
|
||||
selectedHosts,
|
||||
selectedItems,
|
||||
selectedPools,
|
||||
selectedTags,
|
||||
sortBy
|
||||
} = this.state
|
||||
const {
|
||||
items,
|
||||
type
|
||||
} = this.props
|
||||
|
||||
const options = OPTIONS[type]
|
||||
const { Item } = options
|
||||
const { mainActions, otherActions } = options
|
||||
const selectedItemsIds = keys(this._selectedItems)
|
||||
const {
|
||||
Item,
|
||||
mainActions,
|
||||
otherActions,
|
||||
showHostsSelector,
|
||||
showPoolsSelector
|
||||
} = options
|
||||
|
||||
// Necessary because indeterminate cannot be used as an attribute
|
||||
if (this.refs.masterCheckbox) {
|
||||
this.refs.masterCheckbox.indeterminate = this._getIsSomeSelected() && !this._getIsAllSelected()
|
||||
}
|
||||
|
||||
return <Page header={this._renderHeader()}>
|
||||
<Shortcuts name='Home' handler={this._getShortcutsHandler()} targetNodeSelector='body' stopPropagation={false} />
|
||||
@@ -667,13 +719,18 @@ export default class Home extends Component {
|
||||
<div className={styles.itemContainer}>
|
||||
<SingleLineRow className={styles.itemContainerHeader}>
|
||||
<Col smallsize={11} mediumSize={3}>
|
||||
<input type='checkbox' onChange={() => this._selectAllItems()} ref='masterCheckbox' />
|
||||
<input
|
||||
checked={this._getIsAllSelected()}
|
||||
onChange={this._toggleMaster}
|
||||
ref='masterCheckbox'
|
||||
type='checkbox'
|
||||
/>
|
||||
{' '}
|
||||
<span className='text-muted'>
|
||||
{size(this._selectedItems)
|
||||
{this._getNumberOfSelectedItems()
|
||||
? _('homeSelectedItems', {
|
||||
icon: <Icon icon={type.toLowerCase()} />,
|
||||
selected: size(this._selectedItems),
|
||||
selected: this._getNumberOfSelectedItems(),
|
||||
total: nItems
|
||||
})
|
||||
: _('homeDisplayedItems', {
|
||||
@@ -685,16 +742,15 @@ export default class Home extends Component {
|
||||
</span>
|
||||
</Col>
|
||||
<Col mediumSize={8} className='text-xs-right hidden-sm-down'>
|
||||
{this.state.displayActions
|
||||
{this._getNumberOfSelectedItems()
|
||||
? (
|
||||
<div>
|
||||
{mainActions && <div className='btn-group'>
|
||||
{map(mainActions, (action, key) => (
|
||||
<Tooltip content={action.tooltip} key={key}>
|
||||
<ActionButton
|
||||
btnStyle='secondary'
|
||||
{...action}
|
||||
handlerParam={selectedItemsIds}
|
||||
handlerParam={this._getSelectedItemsIds()}
|
||||
/>
|
||||
</Tooltip>
|
||||
))}
|
||||
@@ -702,7 +758,7 @@ export default class Home extends Component {
|
||||
{otherActions && (
|
||||
<DropdownButton bsStyle='secondary' id='advanced' title={_('homeMore')}>
|
||||
{map(otherActions, (action, key) => (
|
||||
<MenuItem key={key} onClick={() => { action.handler(selectedItemsIds, action.params) }}>
|
||||
<MenuItem key={key} onClick={() => { action.handler(this._getSelectedItemsIds(), action.params) }}>
|
||||
<Icon icon={action.icon} fixedWidth /> {_(action.labelId)}
|
||||
</MenuItem>
|
||||
))}
|
||||
@@ -710,7 +766,7 @@ export default class Home extends Component {
|
||||
)}
|
||||
</div>
|
||||
) : <div>
|
||||
{options.showPoolsSelector && (
|
||||
{showPoolsSelector && (
|
||||
<OverlayTrigger
|
||||
trigger='click'
|
||||
rootClose
|
||||
@@ -721,16 +777,16 @@ export default class Home extends Component {
|
||||
autoFocus
|
||||
multi
|
||||
onChange={this._updateSelectedPools}
|
||||
value={this.state.selectedPools}
|
||||
value={selectedPools}
|
||||
/>
|
||||
</Popover>
|
||||
}
|
||||
>
|
||||
<Button className='btn-link'><Icon icon='pool' /> {_('homeAllPools')}</Button>
|
||||
<Button btnStyle='link'><Icon icon='pool' /> {_('homeAllPools')}</Button>
|
||||
</OverlayTrigger>
|
||||
)}
|
||||
{' '}
|
||||
{options.showHostsSelector && (
|
||||
{showHostsSelector && (
|
||||
<OverlayTrigger
|
||||
trigger='click'
|
||||
rootClose
|
||||
@@ -741,12 +797,12 @@ export default class Home extends Component {
|
||||
autoFocus
|
||||
multi
|
||||
onChange={this._updateSelectedHosts}
|
||||
value={this.state.selectedHosts}
|
||||
value={selectedHosts}
|
||||
/>
|
||||
</Popover>
|
||||
}
|
||||
>
|
||||
<Button className='btn-link'><Icon icon='host' /> {_('homeAllHosts')}</Button>
|
||||
<Button btnStyle='link'><Icon icon='host' /> {_('homeAllHosts')}</Button>
|
||||
</OverlayTrigger>
|
||||
)}
|
||||
{' '}
|
||||
@@ -760,14 +816,14 @@ export default class Home extends Component {
|
||||
<SelectTag
|
||||
autoFocus
|
||||
multi
|
||||
objects={props.items}
|
||||
objects={items}
|
||||
onChange={this._updateSelectedTags}
|
||||
value={this.state.selectedTags}
|
||||
value={selectedTags}
|
||||
/>
|
||||
</Popover>
|
||||
}
|
||||
>
|
||||
<Button className='btn-link'><Icon icon='tags' /> {_('homeAllTags')}</Button>
|
||||
<Button btnStyle='link'><Icon icon='tags' /> {_('homeAllTags')}</Button>
|
||||
</OverlayTrigger>
|
||||
{' '}
|
||||
<DropdownButton bsStyle='link' id='sort' title={_('homeSortBy')}>
|
||||
@@ -785,10 +841,9 @@ export default class Home extends Component {
|
||||
}
|
||||
</Col>
|
||||
<Col smallsize={1} mediumSize={1} className='text-xs-right'>
|
||||
<button className='btn btn-secondary'
|
||||
onClick={this._expandAll}>
|
||||
<Button onClick={this._expandAll}>
|
||||
<Icon icon='nav' />
|
||||
</button>
|
||||
</Button>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
{isEmpty(filteredItems)
|
||||
@@ -798,13 +853,13 @@ export default class Home extends Component {
|
||||
</a>
|
||||
</p>
|
||||
: map(visibleItems, (item, index) => (
|
||||
<div className={highlighted === index && styles.highlight}>
|
||||
<div key={item.id} className={highlighted === index && styles.highlight}>
|
||||
<Item
|
||||
expandAll={this.state.expandAll}
|
||||
expandAll={expandAll}
|
||||
item={item}
|
||||
key={item.id}
|
||||
onSelect={this._selectItem}
|
||||
selected={this._selectedItems[item.id]}
|
||||
onSelect={this.toggleState(`selectedItems.${item.id}`)}
|
||||
selected={selectedItems[item.id]}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
|
||||
@@ -46,6 +46,13 @@ import styles from './index.css'
|
||||
|
||||
const getHostMetrics = createGetHostMetrics(getPoolHosts)
|
||||
|
||||
const getNumberOfSrs = createGetObjectsOfType('SR').count(
|
||||
createSelector(
|
||||
(_, props) => props.item.id,
|
||||
poolId => obj => obj.$pool === poolId
|
||||
)
|
||||
)
|
||||
|
||||
const getNumberOfVms = createGetObjectsOfType('VM').count(
|
||||
createSelector(
|
||||
(_, props) => props.item.id,
|
||||
@@ -57,6 +64,7 @@ import styles from './index.css'
|
||||
hostMetrics: getHostMetrics,
|
||||
missingPaths: getMissingPatches,
|
||||
poolHosts: getPoolHosts,
|
||||
nSrs: getNumberOfSrs,
|
||||
nVms: getNumberOfVms
|
||||
}
|
||||
})
|
||||
@@ -73,7 +81,7 @@ export default class PoolItem extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { item: pool, expandAll, selected, hostMetrics, poolHosts, nVms } = this.props
|
||||
const { item: pool, expandAll, selected, hostMetrics, poolHosts, nSrs, nVms } = this.props
|
||||
const { missingPatchCount } = this.state
|
||||
return <div className={styles.item}>
|
||||
<BlockLink to={`/pools/${pool.id}`}>
|
||||
@@ -106,6 +114,38 @@ export default class PoolItem extends Component {
|
||||
}
|
||||
</EllipsisContainer>
|
||||
</Col>
|
||||
<Col mediumSize={1} className='hidden-md-down'>
|
||||
<EllipsisContainer>
|
||||
<span className={styles.itemActionButons}>
|
||||
<Tooltip content={<span>{hostMetrics.count}x {_('hostsTabName')}</span>}>
|
||||
{(hostMetrics.count > 0)
|
||||
? <Link to={`/home?s=$pool:${pool.id}&t=host`}>
|
||||
<Icon icon='host' size='1' fixedWidth />
|
||||
</Link>
|
||||
: <Icon icon='host' size='1' fixedWidth />
|
||||
}
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content={<span>{nVms}x {_('vmsTabName')}</span>}>
|
||||
{(nVms > 0)
|
||||
? <Link to={`/home?s=$pool:${pool.id}&t=VM`}>
|
||||
<Icon icon='vm' size='1' fixedWidth />
|
||||
</Link>
|
||||
: <Icon icon='vm' size='1' fixedWidth />
|
||||
}
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content={<span>{nSrs}x {_('srsTabName')}</span>}>
|
||||
{(nSrs > 0)
|
||||
? <Link to={`/home?s=$pool:${pool.id}&t=SR`}>
|
||||
<Icon icon='sr' size='1' fixedWidth />
|
||||
</Link>
|
||||
: <Icon icon='sr' size='1' fixedWidth />
|
||||
}
|
||||
</Tooltip>
|
||||
</span>
|
||||
</EllipsisContainer>
|
||||
</Col>
|
||||
<Col mediumSize={4} className='hidden-md-down'>
|
||||
<EllipsisContainer>
|
||||
<Ellipsis>
|
||||
@@ -132,9 +172,11 @@ export default class PoolItem extends Component {
|
||||
<SingleLineRow>
|
||||
<Col mediumSize={3} className={styles.itemExpanded}>
|
||||
<span>
|
||||
<Link to={`/home?s=$pool:${pool.id}&t=host`}>{hostMetrics.count}x <Icon icon='host' /></Link>
|
||||
{hostMetrics.count}x <Icon icon='host' />
|
||||
{' '}
|
||||
<Link to={`/home?s=$pool:${pool.id}&t=VM`}>{nVms}x <Icon icon='vm' /></Link>
|
||||
{nVms}x <Icon icon='vm' />
|
||||
{' '}
|
||||
{nSrs}x <Icon icon='sr' />
|
||||
{' '}
|
||||
{hostMetrics.cpus}x <Icon icon='cpu' />
|
||||
{' '}
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
import _ from 'intl'
|
||||
import assign from 'lodash/assign'
|
||||
import HostActionBar from './action-bar'
|
||||
import Icon from 'icon'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import map from 'lodash/map'
|
||||
import Link from 'link'
|
||||
import { NavLink, NavTabs } from 'nav'
|
||||
import Page from '../page'
|
||||
import pick from 'lodash/pick'
|
||||
import React, { cloneElement, Component } from 'react'
|
||||
import sortBy from 'lodash/sortBy'
|
||||
import sum from 'lodash/sum'
|
||||
import Tooltip from 'tooltip'
|
||||
import { Text } from 'editable'
|
||||
import { editHost, fetchHostStats, getHostMissingPatches, installAllHostPatches, installHostPatch } from 'xo'
|
||||
@@ -25,6 +19,15 @@ import {
|
||||
createGetObjectsOfType,
|
||||
createSelector
|
||||
} from 'selectors'
|
||||
import {
|
||||
assign,
|
||||
isEmpty,
|
||||
isString,
|
||||
map,
|
||||
pick,
|
||||
sortBy,
|
||||
sum
|
||||
} from 'lodash'
|
||||
|
||||
import TabAdvanced from './tab-advanced'
|
||||
import TabConsole from './tab-console'
|
||||
@@ -94,7 +97,7 @@ const isRunning = host => host && host.power_state === 'Running'
|
||||
const getHostPatches = createSelector(
|
||||
createGetObjectsOfType('pool_patch'),
|
||||
createGetObjectsOfType('host_patch').pick(
|
||||
createSelector(getHost, host => host.patches)
|
||||
createSelector(getHost, host => isString(host.patches[0]) ? host.patches : [])
|
||||
),
|
||||
(poolsPatches, hostsPatches) => map(hostsPatches, hostPatch => ({
|
||||
...hostPatch,
|
||||
|
||||
@@ -16,14 +16,11 @@ const ALLOW_INSTALL_SUPP_PACK = process.env.XOA_PLAN > 1
|
||||
|
||||
const forceReboot = host => restartHost(host, true)
|
||||
|
||||
const formatPack = (version, pack) => {
|
||||
const [ author, name ] = pack.split(':')
|
||||
|
||||
return <tr>
|
||||
<th>{_('supplementalPackTitle', { author, name })}</th>
|
||||
<td>{version}</td>
|
||||
</tr>
|
||||
}
|
||||
const formatPack = ({ name, author, description, version }) => <tr>
|
||||
<th>{_('supplementalPackTitle', { author, name })}</th>
|
||||
<td>{description}</td>
|
||||
<td>{version}</td>
|
||||
</tr>
|
||||
|
||||
export default ({
|
||||
host
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import _ from 'intl'
|
||||
import Button from 'button'
|
||||
import Component from 'base-component'
|
||||
import CopyToClipboard from 'react-copy-to-clipboard'
|
||||
import debounce from 'lodash/debounce'
|
||||
@@ -73,20 +74,19 @@ export default class extends Component {
|
||||
<input type='text' className='form-control' ref='clipboard' onChange={this._setRemoteClipboard} />
|
||||
<span className='input-group-btn'>
|
||||
<CopyToClipboard text={this.state.clipboard || ''}>
|
||||
<button className='btn btn-secondary'>
|
||||
<Button>
|
||||
<Icon icon='clipboard' /> {_('copyToClipboardLabel')}
|
||||
</button>
|
||||
</Button>
|
||||
</CopyToClipboard>
|
||||
</span>
|
||||
</div>
|
||||
</Col>
|
||||
<Col mediumSize={2}>
|
||||
<button
|
||||
className='btn btn-secondary'
|
||||
<Button
|
||||
onClick={this._sendCtrlAltDel}
|
||||
>
|
||||
<Icon icon='vm-keyboard' /> {_('ctrlAltDelButtonLabel')}
|
||||
</button>
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row className='console'>
|
||||
|
||||
@@ -8,9 +8,9 @@ import map from 'lodash/map'
|
||||
import pick from 'lodash/pick'
|
||||
import SingleLineRow from 'single-line-row'
|
||||
import some from 'lodash/some'
|
||||
import StateButton from 'state-button'
|
||||
import TabButton from 'tab-button'
|
||||
import Tooltip from 'tooltip'
|
||||
import { ButtonGroup } from 'react-bootstrap-4/lib'
|
||||
import { confirm } from 'modal'
|
||||
import { connectStore, noop } from 'utils'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
@@ -166,40 +166,42 @@ class PifItem extends Component {
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
{pif.carrier
|
||||
? <span className='tag tag-success'>
|
||||
{_('pifStatusConnected')}
|
||||
</span>
|
||||
: <span className='tag tag-default'>
|
||||
{_('pifStatusDisconnected')}
|
||||
</span>
|
||||
}
|
||||
<StateButton
|
||||
disabledLabel={_('pifDisconnected')}
|
||||
disabledHandler={connectPif}
|
||||
disabledTooltip={_('connectPif')}
|
||||
|
||||
enabledLabel={_('pifConnected')}
|
||||
enabledHandler={disconnectPif}
|
||||
enabledTooltip={_('disconnectPif')}
|
||||
|
||||
disabled={pif.attached && (pif.management || pif.disallowUnplug)}
|
||||
handlerParam={pif}
|
||||
state={pif.attached}
|
||||
/>
|
||||
{' '}
|
||||
<Tooltip content={pif.carrier ? _('pifPhysicallyConnected') : _('pifPhysicallyDisconnected')}>
|
||||
<Icon
|
||||
icon='network'
|
||||
size='lg'
|
||||
className={pif.carrier ? 'text-success' : 'text-muted'}
|
||||
/>
|
||||
</Tooltip>
|
||||
</td>
|
||||
<td>
|
||||
<ButtonGroup className='pull-right'>
|
||||
<ActionRowButton
|
||||
btnStyle='default'
|
||||
disabled={pif.attached && (pif.management || pif.disallowUnplug)}
|
||||
handler={pif.attached ? disconnectPif : connectPif}
|
||||
handlerParam={pif}
|
||||
icon={pif.attached ? 'disconnect' : 'connect'}
|
||||
tooltip={pif.attached ? _('disconnectPif') : _('connectPif')}
|
||||
/>
|
||||
<ActionRowButton
|
||||
btnStyle='default'
|
||||
disabled={pif.physical || pif.disallowUnplug || pif.management}
|
||||
handler={deletePif}
|
||||
handlerParam={pif}
|
||||
icon='delete'
|
||||
tooltip={_('deletePif')}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
<td className='text-xs-right'>
|
||||
<ActionRowButton
|
||||
disabled={pif.physical || pif.disallowUnplug || pif.management}
|
||||
handler={deletePif}
|
||||
handlerParam={pif}
|
||||
icon='delete'
|
||||
tooltip={_('deletePif')}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
|
||||
export default (({
|
||||
export default ({
|
||||
host,
|
||||
networks,
|
||||
pifs,
|
||||
@@ -232,7 +234,7 @@ export default (({
|
||||
<th>{_('pifMtuLabel')}</th>
|
||||
<th>{_('defaultLockingMode')}</th>
|
||||
<th>{_('pifStatusLabel')}</th>
|
||||
<th />
|
||||
<th className='text-xs-right'>{_('pifAction')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -244,4 +246,4 @@ export default (({
|
||||
}
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>)
|
||||
</Container>
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import _ from 'intl'
|
||||
import ActionRowButton from 'action-row-button'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import React, { Component } from 'react'
|
||||
import SortedTable from 'sorted-table'
|
||||
import TabButton from 'tab-button'
|
||||
import Upgrade from 'xoa-upgrade'
|
||||
import { connectStore, formatSize } from 'utils'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { createDoesHostNeedRestart } from 'selectors'
|
||||
import { createDoesHostNeedRestart, createSelector } from 'selectors'
|
||||
import { FormattedRelative, FormattedTime } from 'react-intl'
|
||||
import { restartHost } from 'xo'
|
||||
import {
|
||||
isEmpty,
|
||||
isString
|
||||
} from 'lodash'
|
||||
|
||||
const MISSING_PATCH_COLUMNS = [
|
||||
{
|
||||
@@ -84,12 +87,56 @@ const INSTALLED_PATCH_COLUMNS = [
|
||||
}
|
||||
]
|
||||
|
||||
// support for software_version.platform_version ^2.1.1
|
||||
const INSTALLED_PATCH_COLUMNS_2 = [
|
||||
{
|
||||
default: true,
|
||||
name: _('patchNameLabel'),
|
||||
itemRenderer: patch => patch.name,
|
||||
sortCriteria: patch => patch.name
|
||||
},
|
||||
{
|
||||
name: _('patchDescription'),
|
||||
itemRenderer: patch => patch.description,
|
||||
sortCriteria: patch => patch.description
|
||||
},
|
||||
{
|
||||
name: _('patchSize'),
|
||||
itemRenderer: patch => formatSize(patch.size),
|
||||
sortCriteria: patch => patch.size
|
||||
}
|
||||
]
|
||||
|
||||
@connectStore(() => ({
|
||||
needsRestart: createDoesHostNeedRestart((_, props) => props.host)
|
||||
}))
|
||||
export default class HostPatches extends Component {
|
||||
_getPatches = createSelector(
|
||||
() => this.props.host,
|
||||
() => this.props.hostPatches,
|
||||
(host, hostPatches) => {
|
||||
if (isEmpty(host.patches) && isEmpty(hostPatches)) {
|
||||
return { patches: null }
|
||||
}
|
||||
|
||||
if (isString(host.patches[0])) {
|
||||
return {
|
||||
patches: hostPatches,
|
||||
columns: INSTALLED_PATCH_COLUMNS
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
patches: host.patches,
|
||||
columns: INSTALLED_PATCH_COLUMNS_2
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
render () {
|
||||
const { host, hostPatches, missingPatches, installAllPatches, installPatch } = this.props
|
||||
const { host, missingPatches, installAllPatches, installPatch } = this.props
|
||||
const { patches, columns } = this._getPatches()
|
||||
|
||||
return process.env.XOA_PLAN > 1
|
||||
? <Container>
|
||||
<Row>
|
||||
@@ -125,13 +172,12 @@ export default class HostPatches extends Component {
|
||||
</Row>}
|
||||
<Row>
|
||||
<Col>
|
||||
{!isEmpty(hostPatches)
|
||||
? (
|
||||
<span>
|
||||
<h3>{_('hostAppliedPatches')}</h3>
|
||||
<SortedTable collection={hostPatches} columns={INSTALLED_PATCH_COLUMNS} />
|
||||
</span>
|
||||
) : <h4 className='text-xs-center'>{_('patchNothing')}</h4>
|
||||
{patches
|
||||
? <span>
|
||||
<h3>{_('hostAppliedPatches')}</h3>
|
||||
<SortedTable collection={patches} columns={columns} />
|
||||
</span>
|
||||
: <h4 className='text-xs-center'>{_('patchNothing')}</h4>
|
||||
}
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
@@ -5,8 +5,8 @@ import Link from 'link'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import SortedTable from 'sorted-table'
|
||||
import StateButton from 'state-button'
|
||||
import Tooltip from 'tooltip'
|
||||
import { ButtonGroup } from 'react-bootstrap-4/lib'
|
||||
import { connectPbd, disconnectPbd, deletePbd, editSr, isSrShared } from 'xo'
|
||||
import { connectStore, formatSize } from 'utils'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
@@ -54,42 +54,29 @@ const SR_COLUMNS = [
|
||||
},
|
||||
{
|
||||
name: _('pbdStatus'),
|
||||
itemRenderer: storage => storage.attached
|
||||
? <span>
|
||||
<span className='tag tag-success'>
|
||||
{_('pbdStatusConnected')}
|
||||
</span>
|
||||
<ButtonGroup className='pull-right'>
|
||||
<ActionRowButton
|
||||
btnStyle='default'
|
||||
handler={disconnectPbd}
|
||||
handlerParam={storage.pbdId}
|
||||
icon='disconnect'
|
||||
tooltip={_('pbdDisconnect')}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</span>
|
||||
: <span>
|
||||
<span className='tag tag-default'>
|
||||
{_('pbdStatusDisconnected')}
|
||||
</span>
|
||||
<ButtonGroup className='pull-right'>
|
||||
<ActionRowButton
|
||||
btnStyle='default'
|
||||
handler={connectPbd}
|
||||
handlerParam={storage.pbdId}
|
||||
icon='connect'
|
||||
tooltip={_('pbdConnect')}
|
||||
/>
|
||||
<ActionRowButton
|
||||
btnStyle='default'
|
||||
handler={deletePbd}
|
||||
handlerParam={storage.pbdId}
|
||||
icon='sr-forget'
|
||||
tooltip={_('pbdForget')}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</span>
|
||||
itemRenderer: storage => <StateButton
|
||||
disabledLabel={_('pbdStatusDisconnected')}
|
||||
disabledHandler={connectPbd}
|
||||
disabledTooltip={_('pbdConnect')}
|
||||
|
||||
enabledLabel={_('pbdStatusConnected')}
|
||||
enabledHandler={disconnectPbd}
|
||||
enabledTooltip={_('pbdDisconnect')}
|
||||
|
||||
handlerParam={storage.pbdId}
|
||||
state={storage.attached}
|
||||
/>
|
||||
},
|
||||
{
|
||||
name: _('pbdAction'),
|
||||
itemRenderer: storage => !storage.attached &&
|
||||
<ActionRowButton
|
||||
handler={deletePbd}
|
||||
handlerParam={storage.pbdId}
|
||||
icon='sr-forget'
|
||||
tooltip={_('pbdForget')}
|
||||
/>,
|
||||
textAlign: 'right'
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -6,11 +6,13 @@ import isArray from 'lodash/isArray'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import Shortcuts from 'shortcuts'
|
||||
import themes from 'themes'
|
||||
import _, { IntlProvider } from 'intl'
|
||||
import { blockXoaAccess } from 'xoa-updater'
|
||||
import { connectStore, routes } from 'utils'
|
||||
import { Notification } from 'notification'
|
||||
import { ShortcutManager } from 'react-shortcuts'
|
||||
import { ThemeProvider } from 'styled-components'
|
||||
import { TooltipViewer } from 'tooltip'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
// import {
|
||||
@@ -178,20 +180,24 @@ export default class XoApp extends Component {
|
||||
const blocked = signedUp && blockXoaAccess(trial) // If we are under expired or unstable trial (signed up only)
|
||||
|
||||
return <IntlProvider>
|
||||
<DocumentTitle title='Xen Orchestra'>
|
||||
<div style={CONTAINER_STYLE}>
|
||||
<Shortcuts name='XoApp' handler={this._shortcutsHandler} targetNodeSelector='body' stopPropagation={false} />
|
||||
<Menu ref='menu' />
|
||||
<div ref='bodyWrapper' style={BODY_WRAPPER_STYLE}>
|
||||
<div style={BODY_STYLE}>
|
||||
{blocked ? <XoaUpdates /> : this.props.children}
|
||||
<ThemeProvider theme={themes.base}>
|
||||
<DocumentTitle title='Xen Orchestra'>
|
||||
<div style={CONTAINER_STYLE}>
|
||||
<Shortcuts name='XoApp' handler={this._shortcutsHandler} targetNodeSelector='body' stopPropagation={false} />
|
||||
<Menu ref='menu' />
|
||||
<div ref='bodyWrapper' style={BODY_WRAPPER_STYLE}>
|
||||
<div style={BODY_STYLE}>
|
||||
{blocked
|
||||
? <XoaUpdates />
|
||||
: signedUp ? this.props.children : <p>Still loading</p>}
|
||||
</div>
|
||||
</div>
|
||||
<Modal />
|
||||
<Notification />
|
||||
<TooltipViewer />
|
||||
</div>
|
||||
<Modal />
|
||||
<Notification />
|
||||
<TooltipViewer />
|
||||
</div>
|
||||
</DocumentTitle>
|
||||
</DocumentTitle>
|
||||
</ThemeProvider>
|
||||
</IntlProvider>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import _, { messages } from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import ActionRowButton from 'action-row-button'
|
||||
import Button from 'button'
|
||||
import Component from 'base-component'
|
||||
import delay from 'lodash/delay'
|
||||
import find from 'lodash/find'
|
||||
@@ -55,7 +56,7 @@ const getType = function (param) {
|
||||
/**
|
||||
* Tries extracting Object targeted property
|
||||
*/
|
||||
const reduceObject = (value, propertyName = 'id') => value && value[propertyName] || value
|
||||
const reduceObject = (value, propertyName = 'id') => (value != null && value[propertyName]) || value
|
||||
|
||||
/**
|
||||
* Adapts all data "arrayed" by UI-multiple-selectors to job's cross-product trick
|
||||
@@ -257,8 +258,8 @@ export default class Jobs extends Component {
|
||||
|
||||
_handleSubmit = () => {
|
||||
const {name, method, params} = this.refs
|
||||
const { job, owner } = this.state
|
||||
|
||||
const { job, owner, timeout } = this.state
|
||||
const _job = {
|
||||
type: 'call',
|
||||
name: name.value,
|
||||
@@ -268,7 +269,8 @@ export default class Jobs extends Component {
|
||||
type: 'crossProduct',
|
||||
items: dataToParamVectorItems(method.value.info.properties, params.value)
|
||||
},
|
||||
userId: owner
|
||||
userId: owner,
|
||||
timeout: timeout ? timeout * 1e3 : undefined
|
||||
}
|
||||
|
||||
job && (_job.id = job.id)
|
||||
@@ -320,7 +322,10 @@ export default class Jobs extends Component {
|
||||
}
|
||||
const { params } = this.refs
|
||||
params.value = data
|
||||
this.setState({ owner: job.userId })
|
||||
this.setState({
|
||||
owner: job.userId,
|
||||
timeout: job.timeout && job.timeout / 1e3
|
||||
})
|
||||
}
|
||||
|
||||
_reset = () => {
|
||||
@@ -330,7 +335,8 @@ export default class Jobs extends Component {
|
||||
this.setState({
|
||||
action: undefined,
|
||||
job: undefined,
|
||||
owner: undefined
|
||||
owner: undefined,
|
||||
timeout: ''
|
||||
})
|
||||
}
|
||||
|
||||
@@ -351,13 +357,14 @@ export default class Jobs extends Component {
|
||||
type === 'user' && permission === 'admin'
|
||||
|
||||
render () {
|
||||
const { state } = this
|
||||
const {
|
||||
action,
|
||||
actions,
|
||||
job,
|
||||
jobs,
|
||||
owner
|
||||
} = this.state
|
||||
} = state
|
||||
const { formatMessage } = this.props.intl
|
||||
|
||||
const isJobUserMissing = this._getIsJobUserMissing()
|
||||
@@ -374,13 +381,14 @@ export default class Jobs extends Component {
|
||||
/>
|
||||
<input type='text' ref='name' className='form-control mb-1 mt-1' placeholder={formatMessage(messages.jobNamePlaceholder)} pattern='[^_]+' required />
|
||||
<SelectPlainObject ref='method' options={actions} optionKey='method' onChange={this._handleSelectMethod} placeholder={_('jobActionPlaceHolder')} />
|
||||
<input type='number' onChange={this.linkState('timeout')} value={state.timeout} className='form-control mb-1 mt-1' placeholder='Job timeout (seconds)' />
|
||||
{action && <fieldset>
|
||||
<GenericInput ref='params' schema={action.info} uiSchema={action.uiSchema} label={action.method} required />
|
||||
{job && <p className='text-warning'>{_('jobEditMessage', { name: job.name, id: job.id.slice(4, 8) })}</p>}
|
||||
{process.env.XOA_PLAN > 3
|
||||
? <span><ActionButton form='newJobForm' handler={this._handleSubmit} icon='save' btnStyle='primary'>{_('saveResourceSet')}</ActionButton>
|
||||
{' '}
|
||||
<button type='button' className='btn btn-default' onClick={this._reset}>{_('resetResourceSet')}</button></span>
|
||||
<Button onClick={this._reset}>{_('resetResourceSet')}</Button></span>
|
||||
: <span><Upgrade place='health' available={4} /></span>
|
||||
}
|
||||
</fieldset>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import _ from 'intl'
|
||||
import ActionRowButton from 'action-row-button'
|
||||
import ActionToggle from 'action-toggle'
|
||||
import filter from 'lodash/filter'
|
||||
import find from 'lodash/find'
|
||||
import forEach from 'lodash/forEach'
|
||||
@@ -10,10 +9,10 @@ import LogList from '../../logs'
|
||||
import map from 'lodash/map'
|
||||
import orderBy from 'lodash/orderBy'
|
||||
import React, { Component } from 'react'
|
||||
import StateButton from 'state-button'
|
||||
import Tooltip from 'tooltip'
|
||||
import Upgrade from 'xoa-upgrade'
|
||||
import { addSubscriptions } from 'utils'
|
||||
import { ButtonGroup } from 'react-bootstrap-4/lib'
|
||||
import { Container } from 'grid'
|
||||
import { createSelector } from 'selectors'
|
||||
import {
|
||||
@@ -104,21 +103,21 @@ export default class Overview extends Component {
|
||||
const { id } = schedule
|
||||
|
||||
return (
|
||||
<ActionToggle
|
||||
value={this.state.scheduleTable[id]}
|
||||
handler={this._updateScheduleState}
|
||||
<StateButton
|
||||
disabledLabel={_('jobStateDisabled')}
|
||||
disabledHandler={enableSchedule}
|
||||
disabledTooltip={_('logIndicationToEnable')}
|
||||
|
||||
enabledLabel={_('jobStateEnabled')}
|
||||
enabledHandler={disableSchedule}
|
||||
enabledTooltip={_('logIndicationToDisable')}
|
||||
|
||||
handlerParam={id}
|
||||
size='small' />
|
||||
state={this.state.scheduleTable[id]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
_updateScheduleState = id => {
|
||||
const enabled = this.state.scheduleTable[id]
|
||||
const method = enabled ? disableSchedule : enableSchedule
|
||||
|
||||
return method(id)
|
||||
}
|
||||
|
||||
_getIsScheduleUserMissing = createSelector(
|
||||
() => this.state.schedules,
|
||||
() => this.props.users,
|
||||
@@ -155,6 +154,7 @@ export default class Overview extends Component {
|
||||
<th>{_('job')}</th>
|
||||
<th className='hidden-xs-down'>{_('jobScheduling')}</th>
|
||||
<th>{_('jobState')}</th>
|
||||
<th className='text-xs-right'>{_('jobAction')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -176,25 +176,23 @@ export default class Overview extends Component {
|
||||
</Link>
|
||||
</td>
|
||||
<td className='hidden-xs-down'>{schedule.cron}</td>
|
||||
<td>
|
||||
{this._getScheduleToggle(schedule)}
|
||||
<fieldset className='pull-right'>
|
||||
<td>{this._getScheduleToggle(schedule)}</td>
|
||||
<td className='text-xs-right'>
|
||||
<fieldset>
|
||||
{!isScheduleUserMissing[schedule.id] && <Tooltip content={_('jobUserNotFound')}><Icon className='mr-1' icon='error' /></Tooltip>}
|
||||
<ButtonGroup>
|
||||
<ActionRowButton
|
||||
icon='delete'
|
||||
btnStyle='danger'
|
||||
handler={deleteSchedule}
|
||||
handlerParam={schedule}
|
||||
/>
|
||||
<ActionRowButton
|
||||
disabled={!isScheduleUserMissing[schedule.id]}
|
||||
icon='run-schedule'
|
||||
btnStyle='warning'
|
||||
handler={runJob}
|
||||
handlerParam={schedule.job}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
<ActionRowButton
|
||||
icon='delete'
|
||||
btnStyle='danger'
|
||||
handler={deleteSchedule}
|
||||
handlerParam={schedule}
|
||||
/>
|
||||
<ActionRowButton
|
||||
disabled={!isScheduleUserMissing[schedule.id]}
|
||||
icon='run-schedule'
|
||||
btnStyle='warning'
|
||||
handler={runJob}
|
||||
handlerParam={schedule.job}
|
||||
/>
|
||||
</fieldset>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import _, { messages } from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import Button from 'button'
|
||||
import find from 'lodash/find'
|
||||
import Icon from 'icon'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
@@ -171,7 +172,7 @@ export default class Schedules extends Component {
|
||||
{process.env.XOA_PLAN > 3
|
||||
? <span><ActionButton form='newScheduleForm' handler={this._handleSubmit} icon='save' btnStyle='primary'>{_('saveBackupJob')}</ActionButton>
|
||||
{' '}
|
||||
<button type='button' className='btn btn-secondary' onClick={this._reset}>{_('selectTableReset')}</button></span>
|
||||
<Button onClick={this._reset}>{_('selectTableReset')}</Button></span>
|
||||
: <span><Upgrade place='health' available={4} /></span>
|
||||
}
|
||||
</div>
|
||||
@@ -195,9 +196,9 @@ export default class Schedules extends Component {
|
||||
<td className='hidden-xs-down'>{schedule.cron}</td>
|
||||
<td className='hidden-xs-down'>{schedule.timezone || _('jobServerTimezone')}</td>
|
||||
<td>
|
||||
<button type='button' className='btn btn-primary' onClick={() => this._edit(schedule.id)}><Icon icon='edit' /></button>
|
||||
<Button btnStyle='primary' onClick={() => this._edit(schedule.id)}><Icon icon='edit' /></Button>
|
||||
{' '}
|
||||
<button type='button' className='btn btn-danger' onClick={() => deleteSchedule(schedule)}><Icon icon='delete' /></button>
|
||||
<Button btnStyle='danger' onClick={() => deleteSchedule(schedule)}><Icon icon='delete' /></Button>
|
||||
</td>
|
||||
</tr>)}
|
||||
</tbody>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import _, { FormattedDuration } from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import ActionRowButton from 'action-row-button'
|
||||
import ButtonGroup from 'button-group'
|
||||
import classnames from 'classnames'
|
||||
import forEach from 'lodash/forEach'
|
||||
import get from 'lodash/get'
|
||||
import Icon from 'icon'
|
||||
import includes from 'lodash/includes'
|
||||
import map from 'lodash/map'
|
||||
@@ -13,7 +15,6 @@ import renderXoItem from 'render-xo-item'
|
||||
import SortedTable from 'sorted-table'
|
||||
import Tooltip from 'tooltip'
|
||||
import { alert, confirm } from 'modal'
|
||||
import { ButtonGroup } from 'react-bootstrap-4/lib'
|
||||
import { connectStore } from 'utils'
|
||||
import { createGetObject } from 'selectors'
|
||||
import { FormattedDate } from 'react-intl'
|
||||
@@ -53,7 +54,7 @@ class JobParam extends Component {
|
||||
|
||||
return object
|
||||
? <span><strong>{object.type || paramKey}</strong>: {renderXoItem(object)} </span>
|
||||
: <span><strong>{paramKey}:</strong> {id} </span>
|
||||
: <span><strong>{paramKey}:</strong> {String(id)} </span>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,8 +72,8 @@ class JobReturn extends Component {
|
||||
|
||||
const Log = props => <ul className='list-group'>
|
||||
{map(props.log.calls, call => <li key={call.callKey} className='list-group-item'>
|
||||
<strong className='text-info'>{call.method}: </strong>
|
||||
{map(call.params, (value, key) => <JobParam id={value} paramKey={key} key={key} />)}
|
||||
<strong className='text-info'>{call.method}: </strong><br />
|
||||
{map(call.params, (value, key) => [ <JobParam id={value} paramKey={key} key={key} />, <br /> ])}
|
||||
{call.returnedValue && <span>{' '}<JobReturn id={call.returnedValue} /></span>}
|
||||
{call.error &&
|
||||
<span className='text-danger'>
|
||||
@@ -86,7 +87,7 @@ const Log = props => <ul className='list-group'>
|
||||
</li>)}
|
||||
</ul>
|
||||
|
||||
const showCalls = log => alert(<span>{_('job')} {log.jobId}</span>, <Log log={log} />)
|
||||
const showCalls = log => alert(_('jobModalTitle', { job: log.jobId }), <Log log={log} />)
|
||||
|
||||
const LOG_COLUMNS = [
|
||||
{
|
||||
@@ -95,10 +96,15 @@ const LOG_COLUMNS = [
|
||||
sortCriteria: log => log.jobId
|
||||
},
|
||||
{
|
||||
name: _('job'),
|
||||
name: _('jobType'),
|
||||
itemRenderer: log => jobKeyToLabel[log.key],
|
||||
sortCriteria: log => log.key
|
||||
},
|
||||
{
|
||||
name: _('jobTag'),
|
||||
itemRenderer: log => get(log, 'calls[0].params.tag'),
|
||||
sortCriteria: log => get(log, 'calls[0].params.tag')
|
||||
},
|
||||
{
|
||||
name: _('jobStart'),
|
||||
itemRenderer: log => log.start && <FormattedDate value={new Date(log.start)} month='short' day='numeric' year='numeric' hour='2-digit' minute='2-digit' second='2-digit' />,
|
||||
@@ -133,11 +139,11 @@ const LOG_COLUMNS = [
|
||||
<span className='pull-right'>
|
||||
<ButtonGroup>
|
||||
<Tooltip content={_('logDisplayDetails')}><ActionRowButton icon='preview' handler={showCalls} handlerParam={log} /></Tooltip>
|
||||
<Tooltip content={_('remove')}><ActionRowButton btnStyle='default' handler={deleteJobsLog} handlerParam={log.logKey} icon='delete' /></Tooltip>
|
||||
<Tooltip content={_('remove')}><ActionRowButton handler={deleteJobsLog} handlerParam={log.logKey} icon='delete' /></Tooltip>
|
||||
</ButtonGroup>
|
||||
</span>
|
||||
</span>,
|
||||
sortCriteria: log => log.hasErrors && ' ' || log.status
|
||||
sortCriteria: log => log.hasErrors ? ' ' : log.status
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import _ from 'intl'
|
||||
import Component from 'base-component'
|
||||
import classNames from 'classnames'
|
||||
import Component from 'base-component'
|
||||
import Icon from 'icon'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import Link from 'link'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import Tooltip from 'tooltip'
|
||||
import { Button } from 'react-bootstrap-4/lib'
|
||||
import { UpdateTag } from '../xoa-updates'
|
||||
import {
|
||||
addSubscriptions,
|
||||
@@ -32,6 +31,8 @@ import {
|
||||
|
||||
import styles from './index.css'
|
||||
|
||||
const returnTrue = () => true
|
||||
|
||||
@connectStore(() => ({
|
||||
isAdmin,
|
||||
nTasks: createGetObjectsOfType('task').count(
|
||||
@@ -70,8 +71,9 @@ export default class Menu extends Component {
|
||||
_checkPermissions = createSelector(
|
||||
() => this.props.isAdmin,
|
||||
() => this.props.permissions,
|
||||
(isAdmin, permissions) => ({ id }) =>
|
||||
isAdmin || permissions && permissions[id] && permissions[id].operate
|
||||
(isAdmin, permissions) => isAdmin
|
||||
? returnTrue
|
||||
: ({ id }) => permissions && permissions[id] && permissions[id].operate
|
||||
)
|
||||
|
||||
_getNoOperatablePools = createSelector(
|
||||
@@ -99,11 +101,22 @@ export default class Menu extends Component {
|
||||
return this.refs.content.offsetHeight
|
||||
}
|
||||
|
||||
_toggleCollapsed = () => {
|
||||
_toggleCollapsed = event => {
|
||||
event.preventDefault()
|
||||
this._removeListener()
|
||||
this.setState({ collapsed: !this.state.collapsed })
|
||||
}
|
||||
|
||||
_connect = event => {
|
||||
event.preventDefault()
|
||||
return connect()
|
||||
}
|
||||
|
||||
_signOut = event => {
|
||||
event.preventDefault()
|
||||
return signOut()
|
||||
}
|
||||
|
||||
render () {
|
||||
const { isAdmin, nTasks, status, user, pools, nHosts } = this.props
|
||||
const noOperatablePools = this._getNoOperatablePools()
|
||||
@@ -151,7 +164,7 @@ export default class Menu extends Component {
|
||||
]},
|
||||
{ to: '/about', icon: 'menu-about', label: 'aboutPage' },
|
||||
{ to: '/tasks', icon: 'task', label: 'taskMenu', pill: nTasks },
|
||||
{ to: '/xosan', icon: 'menu-xosan', label: 'xosan' },
|
||||
isAdmin && { to: '/xosan', icon: 'menu-xosan', label: 'xosan' },
|
||||
!(noOperatablePools && noResourceSets) && { to: '/vms/new', icon: 'menu-new', label: 'newMenu', subMenu: [
|
||||
{ to: '/vms/new', icon: 'menu-new-vm', label: 'newVmPage' },
|
||||
isAdmin && { to: '/new/sr', icon: 'menu-new-sr', label: 'newSrPage' },
|
||||
@@ -175,9 +188,9 @@ export default class Menu extends Component {
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<Button onClick={this._toggleCollapsed}>
|
||||
<a className='nav-link' onClick={this._toggleCollapsed} href='#'>
|
||||
<Icon icon='menu-collapse' size='lg' fixedWidth />
|
||||
</Button>
|
||||
</a>
|
||||
</li>
|
||||
{map(items, (item, index) =>
|
||||
item && <MenuLinkItem key={index} item={item} />
|
||||
@@ -218,10 +231,10 @@ export default class Menu extends Component {
|
||||
<li> </li>
|
||||
<li> </li>
|
||||
<li className='nav-item xo-menu-item'>
|
||||
<Button className='nav-link' onClick={signOut}>
|
||||
<a className='nav-link' onClick={this._signOut} href='#'>
|
||||
<Icon icon='sign-out' size='lg' fixedWidth />
|
||||
<span className={styles.hiddenCollapsed}>{' '}{_('signOut')}</span>
|
||||
</Button>
|
||||
</a>
|
||||
</li>
|
||||
<li className='nav-item xo-menu-item'>
|
||||
<Link className='nav-link text-xs-center' to={'/user'}>
|
||||
@@ -236,9 +249,9 @@ export default class Menu extends Component {
|
||||
? <li className='nav-item text-xs-center'>{_('statusConnecting')}</li>
|
||||
: status === 'disconnected' &&
|
||||
<li className='nav-item text-xs-center xo-menu-item'>
|
||||
<Button className='nav-link' onClick={connect}>
|
||||
<a className='nav-link' onClick={this._connect} href='#'>
|
||||
<Icon icon='alarm' size='lg' fixedWidth /> {_('statusDisconnected')}
|
||||
</Button>
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
|
||||
@@ -1,36 +1,38 @@
|
||||
import _, { messages } from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import BaseComponent from 'base-component'
|
||||
import clamp from 'lodash/clamp'
|
||||
import Button from 'button'
|
||||
import classNames from 'classnames'
|
||||
import DebounceInput from 'react-debounce-input'
|
||||
import every from 'lodash/every'
|
||||
import filter from 'lodash/filter'
|
||||
import find from 'lodash/find'
|
||||
import forEach from 'lodash/forEach'
|
||||
import get from 'lodash/get'
|
||||
import getEventValue from 'get-event-value'
|
||||
import Icon from 'icon'
|
||||
import includes from 'lodash/includes'
|
||||
import isArray from 'lodash/isArray'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import isIp from 'is-ip'
|
||||
import join from 'lodash/join'
|
||||
import map from 'lodash/map'
|
||||
import Page from '../page'
|
||||
import React from 'react'
|
||||
import size from 'lodash/size'
|
||||
import slice from 'lodash/slice'
|
||||
import store from 'store'
|
||||
import sum from 'lodash/sum'
|
||||
import sumBy from 'lodash/sumBy'
|
||||
import Tags from 'tags'
|
||||
import Tooltip from 'tooltip'
|
||||
import Wizard, { Section } from 'wizard'
|
||||
import { Button } from 'react-bootstrap-4/lib'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { injectIntl } from 'react-intl'
|
||||
import { Limits } from 'usage'
|
||||
import {
|
||||
clamp,
|
||||
every,
|
||||
filter,
|
||||
find,
|
||||
forEach,
|
||||
get,
|
||||
includes,
|
||||
isArray,
|
||||
isEmpty,
|
||||
join,
|
||||
map,
|
||||
slice,
|
||||
size,
|
||||
sum,
|
||||
sumBy
|
||||
} from 'lodash'
|
||||
import {
|
||||
addSshKey,
|
||||
createVm,
|
||||
@@ -43,6 +45,7 @@ import {
|
||||
XEN_DEFAULT_CPU_WEIGHT
|
||||
} from 'xo'
|
||||
import {
|
||||
SelectHost,
|
||||
SelectIp,
|
||||
SelectNetwork,
|
||||
SelectPool,
|
||||
@@ -88,6 +91,8 @@ const NB_VMS_MAX = 100
|
||||
|
||||
const getObject = createGetObject((_, id) => id)
|
||||
|
||||
const returnTrue = () => true
|
||||
|
||||
// Sub-components
|
||||
|
||||
const SectionContent = ({ column, children }) => (
|
||||
@@ -172,7 +177,7 @@ class Vif extends BaseComponent {
|
||||
</span>
|
||||
</LineItem>
|
||||
<Item>
|
||||
<Button onClick={onDelete} bsStyle='secondary'>
|
||||
<Button onClick={onDelete}>
|
||||
<Icon icon='new-vm-remove' />
|
||||
</Button>
|
||||
</Item>
|
||||
@@ -202,7 +207,8 @@ class Vif extends BaseComponent {
|
||||
return user && user.preferences && user.preferences.sshKeys
|
||||
},
|
||||
keys => keys
|
||||
)
|
||||
),
|
||||
srs: createGetObjectsOfType('SR')
|
||||
}))
|
||||
@injectIntl
|
||||
export default class NewVm extends BaseComponent {
|
||||
@@ -276,6 +282,7 @@ export default class NewVm extends BaseComponent {
|
||||
VDIs: [],
|
||||
VIFs: [],
|
||||
seqStart: 1,
|
||||
share: false,
|
||||
tags: []
|
||||
})
|
||||
}
|
||||
@@ -346,6 +353,7 @@ export default class NewVm extends BaseComponent {
|
||||
const resourceSet = this._getResourceSet()
|
||||
|
||||
const data = {
|
||||
affinityHost: state.affinityHost && state.affinityHost.id,
|
||||
clone: !this.isDiskTemplate && state.fastClone,
|
||||
existingDisks: state.existingDisks,
|
||||
installation,
|
||||
@@ -365,6 +373,7 @@ export default class NewVm extends BaseComponent {
|
||||
pv_args: state.pv_args,
|
||||
autoPoweron: state.autoPoweron,
|
||||
bootAfterCreate: state.bootAfterCreate,
|
||||
share: state.share,
|
||||
cloudConfig,
|
||||
coreOs: state.template.name_label === 'CoreOS',
|
||||
tags: state.tags
|
||||
@@ -437,7 +446,7 @@ export default class NewVm extends BaseComponent {
|
||||
cpuWeight: '',
|
||||
memoryDynamicMax: template.memory.dynamic[1],
|
||||
// installation
|
||||
installMethod: template.install_methods && template.install_methods[0] || 'SSH',
|
||||
installMethod: (template.install_methods != null && template.install_methods[0]) || 'SSH',
|
||||
sshKeys: this.props.userSshKeys && this.props.userSshKeys.length && [ 0 ],
|
||||
customConfig: '#cloud-config\n#hostname: myhostname\n#ssh_authorized_keys:\n# - ssh-rsa <myKey>\n#packages:\n# - htop\n',
|
||||
// interfaces
|
||||
@@ -485,9 +494,11 @@ export default class NewVm extends BaseComponent {
|
||||
)
|
||||
|
||||
_getCanOperate = createSelector(
|
||||
() => this.props.isAdmin,
|
||||
() => this.props.permissions,
|
||||
permissions => ({ id }) =>
|
||||
this.props.isAdmin || permissions && permissions[id] && permissions[id].operate
|
||||
(isAdmin, permissions) => isAdmin
|
||||
? returnTrue
|
||||
: ({ id }) => permissions && permissions[id] && permissions[id].operate
|
||||
)
|
||||
_getVmPredicate = createSelector(
|
||||
this._getIsInPool,
|
||||
@@ -542,6 +553,26 @@ export default class NewVm extends BaseComponent {
|
||||
this._getCanOperate,
|
||||
[ (pool, canOperate) => canOperate(pool) ]
|
||||
)
|
||||
_getAffinityHostPredicate = createSelector(
|
||||
() => this.props.pool,
|
||||
() => this.state.state.existingDisks,
|
||||
() => this.state.state.VDIs,
|
||||
() => this.props.srs,
|
||||
(pool, existingDisks, VDIs, srs) => {
|
||||
if (!srs) {
|
||||
return false
|
||||
}
|
||||
|
||||
const containers = [
|
||||
...map(existingDisks, disk => get(srs, `${disk.$SR}.$container`)),
|
||||
...map(VDIs, disk => get(srs, `${disk.SR}.$container`))
|
||||
]
|
||||
return host => host.$pool === pool.id &&
|
||||
every(containers, container =>
|
||||
container === pool.id || container === host.id
|
||||
)
|
||||
}
|
||||
)
|
||||
_getDefaultNetworkId = () => {
|
||||
const resourceSet = this._getResolvedResourceSet()
|
||||
if (resourceSet) {
|
||||
@@ -769,7 +800,6 @@ export default class NewVm extends BaseComponent {
|
||||
</Wizard>
|
||||
<div className={styles.submitSection}>
|
||||
<ActionButton
|
||||
btnStyle='secondary'
|
||||
className={styles.button}
|
||||
handler={this._reset}
|
||||
icon='new-vm-reset'
|
||||
@@ -936,7 +966,7 @@ export default class NewVm extends BaseComponent {
|
||||
value={newSshKey}
|
||||
/>
|
||||
<span className='input-group-btn'>
|
||||
<Button className='btn btn-secondary' onClick={this._addNewSshKey} disabled={!newSshKey}>
|
||||
<Button onClick={this._addNewSshKey} disabled={!newSshKey}>
|
||||
<Icon icon='add' />
|
||||
</Button>
|
||||
</span>
|
||||
@@ -1089,6 +1119,7 @@ export default class NewVm extends BaseComponent {
|
||||
<SectionContent column>
|
||||
{map(VIFs, (vif, index) => <div key={index}>
|
||||
<Vif
|
||||
networkPredicate={this._getNetworkPredicate()}
|
||||
onChangeAddresses={this._linkState(`VIFs.${index}.addresses`, '*.id')}
|
||||
onChangeMac={this._linkState(`VIFs.${index}.mac`)}
|
||||
onChangeNetwork={this._linkState(`VIFs.${index}.network`, 'id')}
|
||||
@@ -1100,7 +1131,7 @@ export default class NewVm extends BaseComponent {
|
||||
{index < VIFs.length - 1 && <hr />}
|
||||
</div>)}
|
||||
<Item>
|
||||
<Button onClick={this._addInterface} bsStyle='secondary'>
|
||||
<Button onClick={this._addInterface}>
|
||||
<Icon icon='new-vm-add' />
|
||||
{' '}
|
||||
{_('newVmAddInterface')}
|
||||
@@ -1195,18 +1226,6 @@ export default class NewVm extends BaseComponent {
|
||||
/>}
|
||||
</span>
|
||||
</Item>
|
||||
{' '}
|
||||
<Item className='checkbox'>
|
||||
<label>
|
||||
<input
|
||||
checked={!!vdi.bootable}
|
||||
onChange={this._getOnChangeCheckbox('VDIs', index, 'bootable')}
|
||||
type='checkbox'
|
||||
/>
|
||||
{' '}
|
||||
{_('newVmBootableLabel')}
|
||||
</label>
|
||||
</Item>
|
||||
<Item label={_('newVmNameLabel')}>
|
||||
<DebounceInput
|
||||
className='form-control'
|
||||
@@ -1231,7 +1250,7 @@ export default class NewVm extends BaseComponent {
|
||||
/>
|
||||
</Item>
|
||||
<Item>
|
||||
<Button onClick={() => this._removeVdi(index)} bsStyle='secondary'>
|
||||
<Button onClick={() => this._removeVdi(index)}>
|
||||
<Icon icon='new-vm-remove' />
|
||||
</Button>
|
||||
</Item>
|
||||
@@ -1239,7 +1258,7 @@ export default class NewVm extends BaseComponent {
|
||||
{index < VDIs.length - 1 && <hr />}
|
||||
</div>)}
|
||||
<Item>
|
||||
<Button onClick={this._addVdi} bsStyle='secondary'>
|
||||
<Button onClick={this._addVdi}>
|
||||
<Icon icon='new-vm-add' />
|
||||
{' '}
|
||||
{_('newVmAddDisk')}
|
||||
@@ -1259,6 +1278,7 @@ export default class NewVm extends BaseComponent {
|
||||
|
||||
_renderAdvanced = () => {
|
||||
const {
|
||||
affinityHost,
|
||||
autoPoweron,
|
||||
bootAfterCreate,
|
||||
cpuCap,
|
||||
@@ -1271,13 +1291,14 @@ export default class NewVm extends BaseComponent {
|
||||
namePattern,
|
||||
nbVms,
|
||||
seqStart,
|
||||
share,
|
||||
showAdvanced,
|
||||
tags
|
||||
} = this.state.state
|
||||
const { formatMessage } = this.props.intl
|
||||
return <Section icon='new-vm-advanced' title='newVmAdvancedPanel' done={this._isAdvancedDone()}>
|
||||
<SectionContent column>
|
||||
<Button bsStyle='secondary' onClick={this._toggleState('showAdvanced')}>
|
||||
<Button onClick={this._toggleState('showAdvanced')}>
|
||||
{showAdvanced ? _('newVmHideAdvanced') : _('newVmShowAdvanced')}
|
||||
</Button>
|
||||
</SectionContent>
|
||||
@@ -1306,6 +1327,17 @@ export default class NewVm extends BaseComponent {
|
||||
<Tags labels={tags} onChange={this._linkState('tags')} />
|
||||
</Item>
|
||||
</SectionContent>,
|
||||
this._getResourceSet() !== undefined && <SectionContent>
|
||||
<Item>
|
||||
<input
|
||||
checked={share}
|
||||
onChange={this._getOnChangeCheckbox('share')}
|
||||
type='checkbox'
|
||||
/>
|
||||
|
||||
{_('newVmShare')}
|
||||
</Item>
|
||||
</SectionContent>,
|
||||
<SectionContent>
|
||||
<Item label={_('newVmCpuWeightLabel')}>
|
||||
<DebounceInput
|
||||
@@ -1379,7 +1411,7 @@ export default class NewVm extends BaseComponent {
|
||||
/>
|
||||
<span className='input-group-btn'>
|
||||
<Tooltip content={_('newVmNumberRecalculate')}>
|
||||
<Button bsStyle='secondary' disabled={!multipleVms} onClick={this._updateNbVms}>
|
||||
<Button disabled={!multipleVms} onClick={this._updateNbVms}>
|
||||
<Icon icon='arrow-right' />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
@@ -1397,6 +1429,15 @@ export default class NewVm extends BaseComponent {
|
||||
</Item>
|
||||
)}
|
||||
</LineItem>}
|
||||
</SectionContent>,
|
||||
<SectionContent>
|
||||
<Item label={_('newVmAffinityHost')}>
|
||||
<SelectHost
|
||||
onChange={this._linkState('affinityHost')}
|
||||
predicate={this._getAffinityHostPredicate()}
|
||||
value={affinityHost}
|
||||
/>
|
||||
</Item>
|
||||
</SectionContent>
|
||||
]}
|
||||
</Section>
|
||||
|
||||
@@ -515,7 +515,7 @@ export default class New extends Component {
|
||||
type='text'
|
||||
/>
|
||||
<span className='input-group-btn'>
|
||||
<ActionButton icon='search' btnStyle='default' handler={this._handleSearchServer} />
|
||||
<ActionButton icon='search' handler={this._handleSearchServer} />
|
||||
</span>
|
||||
</div>
|
||||
</fieldset>
|
||||
@@ -560,7 +560,7 @@ export default class New extends Component {
|
||||
ref='port'
|
||||
type='text'
|
||||
/>
|
||||
<ActionButton icon='search' btnStyle='default' handler={this._handleSearchServer} />
|
||||
<ActionButton icon='search' handler={this._handleSearchServer} />
|
||||
</div>
|
||||
{auth &&
|
||||
<fieldset>
|
||||
|
||||
@@ -92,7 +92,6 @@ import TabPatches from './tab-patches'
|
||||
}
|
||||
})
|
||||
export default class Pool extends Component {
|
||||
|
||||
_setNameDescription = nameDescription => editPool(this.props.pool, { name_description: nameDescription })
|
||||
_setNameLabel = nameLabel => editPool(this.props.pool, { name_label: nameLabel })
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import _ from 'intl'
|
||||
import Copiable from 'copiable'
|
||||
import React from 'react'
|
||||
import SelectFiles from 'select-files'
|
||||
import Upgrade from 'xoa-upgrade'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { installSupplementalPackOnAllHosts } from 'xo'
|
||||
|
||||
@@ -33,7 +34,7 @@ export default ({
|
||||
</Row>
|
||||
</Container>
|
||||
<h3 className='mt-1 mb-1'>{_('supplementalPackPoolNew')}</h3>
|
||||
<div>
|
||||
<Upgrade place='poolSupplementalPacks' required={2}>
|
||||
<SelectFiles onChange={file => installSupplementalPackOnAllHosts(pool, file)} />
|
||||
</div>
|
||||
</Upgrade>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import _ from 'intl'
|
||||
import ActionRow from 'action-row-button'
|
||||
import Button from 'button'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import map from 'lodash/map'
|
||||
import TabButton from 'tab-button'
|
||||
import React, { Component } from 'react'
|
||||
import TabButton from 'tab-button'
|
||||
import { deleteMessage } from 'xo'
|
||||
import { createPager } from 'selectors'
|
||||
import { FormattedRelative, FormattedTime } from 'react-intl'
|
||||
@@ -42,12 +43,12 @@ export default class TabLogs extends Component {
|
||||
: <div>
|
||||
<Row>
|
||||
<Col className='text-xs-right'>
|
||||
<button className='btn btn-lg btn-tab' onClick={this._previousPage}>
|
||||
<Button size='large' onClick={this._previousPage}>
|
||||
<
|
||||
</button>
|
||||
<button className='btn btn-lg btn-tab' onClick={this._nextPage}>
|
||||
</Button>
|
||||
<Button size='large' onClick={this._nextPage}>
|
||||
>
|
||||
</button>
|
||||
</Button>
|
||||
<TabButton
|
||||
btnStyle='danger'
|
||||
handler={this._removeAllLogs}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import _ from 'intl'
|
||||
import ActionRowButton from 'action-row-button'
|
||||
import BaseComponent from 'base-component'
|
||||
import Button from 'button'
|
||||
import ButtonGroup from 'button-group'
|
||||
import Icon from 'icon'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import map from 'lodash/map'
|
||||
@@ -9,10 +11,9 @@ import some from 'lodash/some'
|
||||
import SortedTable from 'sorted-table'
|
||||
import TabButton from 'tab-button'
|
||||
import Tooltip from 'tooltip'
|
||||
import { Button, ButtonGroup } from 'react-bootstrap-4/lib'
|
||||
import { Text, Number } from 'editable'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { connectStore } from 'utils'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { Text, Number } from 'editable'
|
||||
import { Toggle } from 'form'
|
||||
import {
|
||||
createFinder,
|
||||
@@ -205,10 +206,9 @@ class PifItem extends Component {
|
||||
</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<ButtonGroup className='pull-right'>
|
||||
<td className='text-xs-right'>
|
||||
<ButtonGroup>
|
||||
<ActionRowButton
|
||||
btnStyle='default'
|
||||
disabled={disableUnplug}
|
||||
handler={pif.attached ? disconnectPif : connectPif}
|
||||
handlerParam={pif}
|
||||
@@ -228,7 +228,7 @@ class PifsItem extends BaseComponent {
|
||||
|
||||
return <div>
|
||||
<Tooltip content={showPifs ? _('hidePifs') : _('showPifs')}>
|
||||
<Button bsSize='small' bsStyle='secondary' className='mb-1 pull-right' onClick={this.toggleState('showPifs')}>
|
||||
<Button size='small' className='mb-1 pull-right' onClick={this.toggleState('showPifs')}>
|
||||
<Icon icon={showPifs ? 'hidden' : 'shown'} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
@@ -272,9 +272,8 @@ class NetworkActions extends Component {
|
||||
render () {
|
||||
const { network, disableNetworkDelete } = this.props
|
||||
|
||||
return <ButtonGroup className='pull-right'>
|
||||
return <ButtonGroup>
|
||||
<ActionRowButton
|
||||
btnStyle='default'
|
||||
disabled={disableNetworkDelete}
|
||||
handler={deleteNetwork}
|
||||
handlerParam={network}
|
||||
@@ -324,7 +323,8 @@ const NETWORKS_COLUMNS = [
|
||||
},
|
||||
{
|
||||
name: '',
|
||||
itemRenderer: network => <NetworkActions network={network} />
|
||||
itemRenderer: network => <NetworkActions network={network} />,
|
||||
textAlign: 'right'
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Component from 'base-component'
|
||||
import HostsPatchesTable from 'hosts-patches-table'
|
||||
import React from 'react'
|
||||
import Upgrade from 'xoa-upgrade'
|
||||
import { connectStore } from 'utils'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { createGetObjectsOfType } from 'selectors'
|
||||
@@ -20,7 +21,7 @@ export default class TabPatches extends Component {
|
||||
_getContainer = () => this.refs.container
|
||||
|
||||
render () {
|
||||
return (
|
||||
return <Upgrade place='poolPatches' required={2}>
|
||||
<Container>
|
||||
<Row>
|
||||
<Col className='text-xs-right'>
|
||||
@@ -37,6 +38,6 @@ export default class TabPatches extends Component {
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
)
|
||||
</Upgrade>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -454,7 +454,7 @@ export class Edit extends Component {
|
||||
<input className='form-control' type='number' min={0} onChange={this.linkState(`ipPools.${index}.quantity`)} value={firstDefined(ipPool.quantity, '')} placeholder='∞' />
|
||||
</Col>
|
||||
<Col mediumSize={2}>
|
||||
<ActionButton btnStyle='secondary' icon='delete' handler={this._removeIpPool} handlerParam={index} />
|
||||
<ActionButton icon='delete' handler={this._removeIpPool} handlerParam={index} />
|
||||
</Col>
|
||||
</Row>)}
|
||||
<Row>
|
||||
@@ -465,7 +465,7 @@ export class Edit extends Component {
|
||||
<input className='form-control' type='number' min={0} onChange={this.linkState('newIpPoolQuantity')} value={state.newIpPoolQuantity || ''} placeholder='∞' />
|
||||
</Col>
|
||||
<Col mediumSize={2}>
|
||||
<ActionButton btnStyle='secondary' icon='add' handler={this._addIpPool} />
|
||||
<ActionButton icon='add' handler={this._addIpPool} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
@@ -478,7 +478,7 @@ export class Edit extends Component {
|
||||
<li className='list-group-item text-xs-center'>
|
||||
<div className='btn-toolbar'>
|
||||
<ActionButton btnStyle='primary' icon='save' handler={this._save} type='submit'>{_('saveResourceSet')}</ActionButton>
|
||||
<ActionButton btnStyle='secondary' icon='reset' handler={this._reset}>{_('resetResourceSet')}</ActionButton>
|
||||
<ActionButton icon='reset' handler={this._reset}>{_('resetResourceSet')}</ActionButton>
|
||||
{resourceSet && <ActionButton btnStyle='danger' icon='delete' handler={deleteResourceSet} handlerParam={resourceSet}>{_('deleteResourceSet')}</ActionButton>}
|
||||
</div>
|
||||
</li>
|
||||
@@ -686,7 +686,6 @@ export default class Self extends Component {
|
||||
{_('resourceSetNew')}
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
btnStyle='secondary'
|
||||
handler={recomputeResourceSetsLimits}
|
||||
icon='refresh'
|
||||
>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import _ from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import ActionRowButton from 'action-row-button'
|
||||
import ButtonGroup from 'button-group'
|
||||
import Component from 'base-component'
|
||||
import filter from 'lodash/filter'
|
||||
import forEach from 'lodash/forEach'
|
||||
@@ -19,7 +20,6 @@ import { connectStore } from 'utils'
|
||||
import { Container } from 'grid'
|
||||
import { error } from 'notification'
|
||||
import { SelectHighLevelObject, SelectRole, SelectSubject } from 'select-objects'
|
||||
import { ButtonGroup } from 'react-bootstrap-4/lib'
|
||||
|
||||
import {
|
||||
createGetObjectsOfType,
|
||||
@@ -161,7 +161,7 @@ export default class Acls extends Component {
|
||||
const newSomeTypeFilters = some(newTypeFilters)
|
||||
|
||||
// If some objects need to be removed from the selected objects
|
||||
if (!newTypeFilters[type] || !someTypeFilters && newSomeTypeFilters) {
|
||||
if (!newTypeFilters[type] || (!someTypeFilters && newSomeTypeFilters)) {
|
||||
this.setState({
|
||||
objects: filter(objects, ({ type }) => !newSomeTypeFilters || newTypeFilters[type])
|
||||
})
|
||||
@@ -172,7 +172,7 @@ export default class Acls extends Component {
|
||||
someTypeFilters: some(newTypeFilters)
|
||||
}, () => {
|
||||
// If some objects need to be removed from the selected objects
|
||||
if (!this.state.typeFilters[type] || !someTypeFilters && this.state.someTypeFilters) {
|
||||
if (!this.state.typeFilters[type] || (!someTypeFilters && this.state.someTypeFilters)) {
|
||||
this.setState({
|
||||
objects: filter(objects, this._getObjectPredicate())
|
||||
})
|
||||
@@ -243,7 +243,7 @@ export default class Acls extends Component {
|
||||
<SelectHighLevelObject multi onChange={this.linkState('objects')} value={objects} predicate={this._getObjectPredicate()} />
|
||||
</div>
|
||||
<div className='form-group mb-1'>
|
||||
<ButtonGroup className='mr-1'>
|
||||
<ButtonGroup>
|
||||
{map(TYPES, type =>
|
||||
<ActionButton
|
||||
btnStyle={typeFilters[type] ? 'success' : 'secondary'}
|
||||
@@ -256,7 +256,8 @@ export default class Acls extends Component {
|
||||
/>
|
||||
)}
|
||||
</ButtonGroup>
|
||||
<ActionButton tooltip='Select all' btnStyle='secondary' size='small' icon='add' handler={this._selectAll} />
|
||||
{' '}
|
||||
<ActionButton tooltip='Select all' size='small' icon='add' handler={this._selectAll} />
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<SelectRole onChange={this.linkState('action')} value={action} />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import _ from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import { Button } from 'react-bootstrap-4/lib'
|
||||
import Button from 'button'
|
||||
import Component from 'base-component'
|
||||
import Dropzone from 'dropzone'
|
||||
import Icon from 'icon'
|
||||
@@ -77,7 +77,6 @@ export default class Config extends Component {
|
||||
{_('importConfig')}
|
||||
</ActionButton>
|
||||
<Button
|
||||
bsStyle='secondary'
|
||||
onClick={this._unselectFile}
|
||||
>
|
||||
{_('importVmsCleanList')}
|
||||
@@ -93,7 +92,7 @@ export default class Config extends Component {
|
||||
<br />
|
||||
<div className='mt-1'>
|
||||
<h2><Icon icon='export' /> {_('exportConfig')}</h2>
|
||||
<Button bsStyle='primary' onClick={exportConfig}>{_('downloadConfig')}</Button>
|
||||
<Button btnStyle='primary' onClick={exportConfig}>{_('downloadConfig')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ class UserDisplay extends Component {
|
||||
const { id, users } = this.props
|
||||
|
||||
return <span>
|
||||
{id && (users && users[id] && users[id].email) || <em><{_('unknownUser')}></em>}
|
||||
{(id && users && users[id] && users[id].email) || <em><{_('unknownUser')}></em>}
|
||||
{' '}
|
||||
<ActionButton className='pull-right' btnStyle='primary' size='small' icon='remove' handler={this._removeUser} />
|
||||
</span>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user