Compare commits
136 Commits
xo-web/v5.
...
v5.1.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93c7a01e62 | ||
|
|
9c2359e8ee | ||
|
|
5b9000012e | ||
|
|
bf00b4e8e3 | ||
|
|
ee7787f4ae | ||
|
|
0b88e743c9 | ||
|
|
f07a947580 | ||
|
|
0b8a9eedbc | ||
|
|
8d24e596ac | ||
|
|
c2378a44cd | ||
|
|
023f7fdef1 | ||
|
|
5d7a64bc28 | ||
|
|
8661957a97 | ||
|
|
7a15d265b7 | ||
|
|
2736881975 | ||
|
|
44a85f4e0c | ||
|
|
52a6e42e7e | ||
|
|
3dbe058d4e | ||
|
|
620139efc1 | ||
|
|
71464ac2e3 | ||
|
|
4a65489d39 | ||
|
|
65d7eac590 | ||
|
|
02bbc01dc4 | ||
|
|
3066237c86 | ||
|
|
53f3c0bef1 | ||
|
|
823c91b457 | ||
|
|
3bd7e20411 | ||
|
|
24d4610b04 | ||
|
|
b16097767a | ||
|
|
2ff74ffd39 | ||
|
|
f0bb464136 | ||
|
|
4767830386 | ||
|
|
ce23d4f164 | ||
|
|
c1380d1256 | ||
|
|
ed9a848858 | ||
|
|
5e4e15fc12 | ||
|
|
0dea952a2a | ||
|
|
a1818dd525 | ||
|
|
659e336f66 | ||
|
|
058f7ecd9f | ||
|
|
831d9cb49f | ||
|
|
a5d059b0b1 | ||
|
|
4c3b959869 | ||
|
|
d81a169a39 | ||
|
|
0d47332526 | ||
|
|
539d136936 | ||
|
|
4c28b5775d | ||
|
|
fe6f351f84 | ||
|
|
5dbeccf92f | ||
|
|
56bba1d84b | ||
|
|
af05d362b4 | ||
|
|
268ccf9a36 | ||
|
|
e77d4fafaa | ||
|
|
b88b99e342 | ||
|
|
f862d0df5b | ||
|
|
dac954155c | ||
|
|
cf9deceb15 | ||
|
|
72aed98088 | ||
|
|
ec92eddde8 | ||
|
|
e30b5ab6c3 | ||
|
|
0a5d26b001 | ||
|
|
7e4b881041 | ||
|
|
27a6af414f | ||
|
|
ba6204f811 | ||
|
|
d17b1050ad | ||
|
|
b70bc86f71 | ||
|
|
42b08633e9 | ||
|
|
bc898e1afd | ||
|
|
48d5f34ae6 | ||
|
|
67b8b15cd8 | ||
|
|
09d80afa69 | ||
|
|
c0d95304f6 | ||
|
|
5a0d67a9f6 | ||
|
|
08305b4b93 | ||
|
|
04d5612946 | ||
|
|
3dcb6f1f61 | ||
|
|
4e7684e38b | ||
|
|
a692b7571f | ||
|
|
a098618efa | ||
|
|
71381e75f1 | ||
|
|
05b345db4a | ||
|
|
f85f6eab9e | ||
|
|
b6dc8b507d | ||
|
|
831308ee05 | ||
|
|
eb5bcb759f | ||
|
|
8286570811 | ||
|
|
10b511f0ed | ||
|
|
751e335bc0 | ||
|
|
cb107521f2 | ||
|
|
e56af57b74 | ||
|
|
a2a1cbab6e | ||
|
|
306a021a8d | ||
|
|
d8c414af2f | ||
|
|
ec4c76b2e0 | ||
|
|
e23b8a6891 | ||
|
|
34006bcbf6 | ||
|
|
ed9aeabf6a | ||
|
|
799fc5089f | ||
|
|
683d510aa6 | ||
|
|
ebd7e58f61 | ||
|
|
9a498b54ac | ||
|
|
2687f45e6e | ||
|
|
f79a17fcec | ||
|
|
8fd377d1e2 | ||
|
|
fda06fbd29 | ||
|
|
cee4378e6d | ||
|
|
ab6d342886 | ||
|
|
9954c08993 | ||
|
|
3ae80aeab3 | ||
|
|
2a3534f659 | ||
|
|
fc39de0d5a | ||
|
|
64e4b79d41 | ||
|
|
53887da3da | ||
|
|
7c60d68f56 | ||
|
|
2ac1b991b1 | ||
|
|
8257714cdb | ||
|
|
1b8bacbf5a | ||
|
|
1d5b84389d | ||
|
|
f7dcf52977 | ||
|
|
e26dd5147a | ||
|
|
bb8f96c2e2 | ||
|
|
95d4cc9055 | ||
|
|
cb84a85f8b | ||
|
|
0a8aa2ecf5 | ||
|
|
5941321e84 | ||
|
|
8cf62280f4 | ||
|
|
4cea142b57 | ||
|
|
64d9245bc4 | ||
|
|
2d78c0c4c3 | ||
|
|
aa585e2d25 | ||
|
|
325ab17dcc | ||
|
|
443ea44bcd | ||
|
|
07958d8efa | ||
|
|
f19affe599 | ||
|
|
f7b7c27b6c | ||
|
|
c7af5b384c |
83
CHANGELOG.md
83
CHANGELOG.md
@@ -1,5 +1,88 @@
|
||||
# ChangeLog
|
||||
|
||||
## **5.1.0** (2016-07-26)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Improve backups timezone UI [\#1314](https://github.com/vatesfr/xo-web/issues/1314)
|
||||
- HOME view submenus [\#1306](https://github.com/vatesfr/xo-web/issues/1306)
|
||||
- Ability for a user to save SSH keys [\#1299](https://github.com/vatesfr/xo-web/issues/1299)
|
||||
- \[ACLs\] Ability to select all hosts/VMs [\#1296](https://github.com/vatesfr/xo-web/issues/1296)
|
||||
- Improve scheduling UI [\#1295](https://github.com/vatesfr/xo-web/issues/1295)
|
||||
- Plugins: Predefined configurations [\#1289](https://github.com/vatesfr/xo-web/issues/1289)
|
||||
- Button to recompute resource sets limits [\#1287](https://github.com/vatesfr/xo-web/issues/1287)
|
||||
- Credit scheduler CAP and weight configuration [\#1283](https://github.com/vatesfr/xo-web/issues/1283)
|
||||
- Migration form problem on the /v5/vms/\_\_UUID\_\_ page when doing xenmotion inside a pool [\#1254](https://github.com/vatesfr/xo-web/issues/1254)
|
||||
- /v5/\#/pools/\_\_UUID\_\_: patch table improvement [\#1246](https://github.com/vatesfr/xo-web/issues/1246)
|
||||
- /v5/\#/hosts/\_\_UUID\_\_: patch list improvements ? [\#1245](https://github.com/vatesfr/xo-web/issues/1245)
|
||||
- F\*cking patches, how do they work? [\#1236](https://github.com/vatesfr/xo-web/issues/1236)
|
||||
- Change Default Filter [\#1235](https://github.com/vatesfr/xo-web/issues/1235)
|
||||
- Add a property on jobs to know their state [\#1232](https://github.com/vatesfr/xo-web/issues/1232)
|
||||
- Spanish translation [\#1231](https://github.com/vatesfr/xo-web/issues/1231)
|
||||
- Home: "Filter" input and keyboard focus [\#1228](https://github.com/vatesfr/xo-web/issues/1228)
|
||||
- Display xenserver version [\#1225](https://github.com/vatesfr/xo-web/issues/1225)
|
||||
- Plugin config: presets & defaults [\#1222](https://github.com/vatesfr/xo-web/issues/1222)
|
||||
- Allow halted VM migration [\#1216](https://github.com/vatesfr/xo-web/issues/1216)
|
||||
- Missing confirm dialog on critical button [\#1211](https://github.com/vatesfr/xo-web/issues/1211)
|
||||
- Backup logs are not sortable [\#1196](https://github.com/vatesfr/xo-web/issues/1196)
|
||||
- Page title with the name of current object [\#1185](https://github.com/vatesfr/xo-web/issues/1185)
|
||||
- Existing VIF management [\#1176](https://github.com/vatesfr/xo-web/issues/1176)
|
||||
- Do not display fast clone option is there isn't template disks [\#1172](https://github.com/vatesfr/xo-web/issues/1172)
|
||||
- UI issue when adding a user [\#1159](https://github.com/vatesfr/xo-web/issues/1159)
|
||||
- Combined values on stats [\#1158](https://github.com/vatesfr/xo-web/issues/1158)
|
||||
- Parallel coordinates graph [\#1157](https://github.com/vatesfr/xo-web/issues/1157)
|
||||
- VM creation on self-service as user [\#1155](https://github.com/vatesfr/xo-web/issues/1155)
|
||||
- VM copy bulk action on home view [\#1154](https://github.com/vatesfr/xo-web/issues/1154)
|
||||
- Better VDI map [\#1151](https://github.com/vatesfr/xo-web/issues/1151)
|
||||
- Missing tooltips on buttons [\#1150](https://github.com/vatesfr/xo-web/issues/1150)
|
||||
- Patching from pool view [\#1149](https://github.com/vatesfr/xo-web/issues/1149)
|
||||
- Missing patches in dashboard [\#1148](https://github.com/vatesfr/xo-web/issues/1148)
|
||||
- Improve tasks view [\#1147](https://github.com/vatesfr/xo-web/issues/1147)
|
||||
- Home bulk VM migration [\#1146](https://github.com/vatesfr/xo-web/issues/1146)
|
||||
- LDAP plugin clear password field [\#1145](https://github.com/vatesfr/xo-web/issues/1145)
|
||||
- Cron default behavior [\#1144](https://github.com/vatesfr/xo-web/issues/1144)
|
||||
- Modal for migrate on home [\#1143](https://github.com/vatesfr/xo-web/issues/1143)
|
||||
- /v5/\#/srs/\_\_UUID\_\_: UI improvements [\#1142](https://github.com/vatesfr/xo-web/issues/1142)
|
||||
- /v5/\#/pools/: some name should be links [\#1141](https://github.com/vatesfr/xo-web/issues/1141)
|
||||
- create the page /v5/\#/pools/ [\#1140](https://github.com/vatesfr/xo-web/issues/1140)
|
||||
- Dashboard: add links to different part of XOA [\#1139](https://github.com/vatesfr/xo-web/issues/1139)
|
||||
- /v5/\#/dashboard/overview: add link on the "Top 5 SR Usage" graph [\#1135](https://github.com/vatesfr/xo-web/issues/1135)
|
||||
- /v5/\#/backup/overview: display the error when there is one returned by xenserver on failed job. [\#1134](https://github.com/vatesfr/xo-web/issues/1134)
|
||||
- /v5/: add an option to set the number of element displayed in tables [\#1133](https://github.com/vatesfr/xo-web/issues/1133)
|
||||
- Updater refresh page after update [\#1131](https://github.com/vatesfr/xo-web/issues/1131)
|
||||
- /v5/\#/settings/plugins [\#1130](https://github.com/vatesfr/xo-web/issues/1130)
|
||||
- /v5/\#/new/sr: layout issue [\#1129](https://github.com/vatesfr/xo-web/issues/1129)
|
||||
- v5 /v5/\#/vms/new: layout issue [\#1128](https://github.com/vatesfr/xo-web/issues/1128)
|
||||
- v5 user page missing style [\#1127](https://github.com/vatesfr/xo-web/issues/1127)
|
||||
- Remote helper/tester [\#1075](https://github.com/vatesfr/xo-web/issues/1075)
|
||||
- Generate uiSchema from custom schema properties [\#951](https://github.com/vatesfr/xo-web/issues/951)
|
||||
- Customizing VM names generation during batch creation [\#949](https://github.com/vatesfr/xo-web/issues/949)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Plugins: Don't use `default` attributes in presets list [\#1288](https://github.com/vatesfr/xo-web/issues/1288)
|
||||
- CPU weight must be an integer [\#1286](https://github.com/vatesfr/xo-web/issues/1286)
|
||||
- Overview of self service is always empty [\#1282](https://github.com/vatesfr/xo-web/issues/1282)
|
||||
- SR attach/creation issue [\#1281](https://github.com/vatesfr/xo-web/issues/1281)
|
||||
- Self service resources not modified after a VM deletion [\#1276](https://github.com/vatesfr/xo-web/issues/1276)
|
||||
- Scheduled jobs seems use GMT since 5.0 [\#1258](https://github.com/vatesfr/xo-web/issues/1258)
|
||||
- Can't create a VM with disks on 2 different SRs [\#1257](https://github.com/vatesfr/xo-web/issues/1257)
|
||||
- Graph display bug [\#1247](https://github.com/vatesfr/xo-web/issues/1247)
|
||||
- /v5/#/hosts/__UUID__: Patch list not limited to the current pool [\#1244](https://github.com/vatesfr/xo-web/issues/1244)
|
||||
- Replication issues [\#1233](https://github.com/vatesfr/xo-web/issues/1233)
|
||||
- VM creation install method disabled fields [\#1198](https://github.com/vatesfr/xo-web/issues/1198)
|
||||
- Update icon shouldn't be displayed when menu is collapsed [\#1188](https://github.com/vatesfr/xo-web/issues/1188)
|
||||
- /v5/ : Load average graph axis issue [\#1167](https://github.com/vatesfr/xo-web/issues/1167)
|
||||
- Some remote can't be opened [\#1164](https://github.com/vatesfr/xo-web/issues/1164)
|
||||
- Bulk action for hosts in home and pool view [\#1153](https://github.com/vatesfr/xo-web/issues/1153)
|
||||
- New Vif [\#1138](https://github.com/vatesfr/xo-web/issues/1138)
|
||||
- Missing SRs [\#1123](https://github.com/vatesfr/xo-web/issues/1123)
|
||||
- Continuous replication email alert does not obey per job setting [\#1121](https://github.com/vatesfr/xo-web/issues/1121)
|
||||
- Safari XO5 issue [\#1120](https://github.com/vatesfr/xo-web/issues/1120)
|
||||
- ACLs shoud be available in Enterprise Edition [\#1118](https://github.com/vatesfr/xo-web/issues/1118)
|
||||
- SR edit name or description doesn't work [\#1116](https://github.com/vatesfr/xo-web/issues/1116)
|
||||
- Bad RRD parsing for VIFs [\#969](https://github.com/vatesfr/xo-web/issues/969)
|
||||
|
||||
## **5.0.0** (2016-06-24)
|
||||
|
||||
### Enhancements
|
||||
|
||||
10
README.md
10
README.md
@@ -60,8 +60,8 @@ Otherwise, please consider using the [bugtracker of the general repository](http
|
||||
## Process for new release
|
||||
|
||||
```bash
|
||||
# Switch to the master branch.
|
||||
git checkout master
|
||||
# Switch to the stable branch.
|
||||
git checkout stable
|
||||
|
||||
# Fetches latest changes.
|
||||
git pull --ff-only
|
||||
@@ -75,12 +75,12 @@ npm version minor
|
||||
# Go back to the next-release branch.
|
||||
git checkout next-release
|
||||
|
||||
# Fetches the last changes (the merge and version bump) from master to
|
||||
# Fetches the last changes (the merge and version bump) from stable to
|
||||
# next-release.
|
||||
git merge --ff-only master
|
||||
git merge --ff-only stable
|
||||
|
||||
# Push the changes on git.
|
||||
git push --follow-tags origin master next-release
|
||||
git push --follow-tags origin stable next-release
|
||||
|
||||
# Publish this release to npm.
|
||||
npm publish
|
||||
|
||||
14
package.json
14
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"private": false,
|
||||
"name": "xo-web",
|
||||
"version": "5.0.1",
|
||||
"version": "5.1.1",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Web interface client for Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -58,7 +58,7 @@
|
||||
"font-awesome": "^4.5.0",
|
||||
"font-mfizz": "github:fizzed/font-mfizz",
|
||||
"ghooks": "^1.1.1",
|
||||
"globby": "^5.0.0",
|
||||
"globby": "^6.0.0",
|
||||
"gulp": "github:gulpjs/gulp#4.0",
|
||||
"gulp-autoprefixer": "^3.1.0",
|
||||
"gulp-csso": "^2.0.0",
|
||||
@@ -75,8 +75,9 @@
|
||||
"lodash": "^4.6.1",
|
||||
"loose-envify": "^1.1.0",
|
||||
"marked": "^0.3.5",
|
||||
"modular-css": "^0.22.1",
|
||||
"modular-css": "^0.23.2",
|
||||
"moment": "^2.13.0",
|
||||
"moment-timezone": "^0.5.4",
|
||||
"notifyjs": "^2.0.1",
|
||||
"novnc-node": "^0.5.3",
|
||||
"promise-toolbox": "^0.4.0",
|
||||
@@ -89,6 +90,7 @@
|
||||
"react-debounce-input": "^2.4.0",
|
||||
"react-dnd": "^2.1.4",
|
||||
"react-dnd-html5-backend": "^2.1.2",
|
||||
"react-document-title": "^2.0.2",
|
||||
"react-dom": "^15.0.0",
|
||||
"react-dropzone": "^3.5.0",
|
||||
"react-intl": "^2.0.1",
|
||||
@@ -111,8 +113,8 @@
|
||||
"superagent": "^2.0.0",
|
||||
"vinyl": "^1.1.1",
|
||||
"watchify": "^3.7.0",
|
||||
"xo-acl-resolver": "^0.2.0",
|
||||
"xo-lib": "^0.8.0-1",
|
||||
"xo-acl-resolver": "^0.2.1",
|
||||
"xo-lib": "^0.8.0",
|
||||
"xo-remote-parser": "^0.3"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -22,6 +22,11 @@ $ct-series-colors: (
|
||||
@import "../node_modules/chartist/dist/scss/settings/_chartist-settings";
|
||||
@import "../node_modules/chartist/dist/scss/chartist";
|
||||
|
||||
.ct-chart {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
// Line in charts with only 2px in width
|
||||
.ct-line {
|
||||
stroke-width: 2px;
|
||||
@@ -55,7 +60,6 @@ $ct-series-colors: (
|
||||
|
||||
// Arrow!
|
||||
&:before {
|
||||
position: absolute;
|
||||
bottom: -14px;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
@@ -80,28 +84,27 @@ $ct-series-colors: (
|
||||
// CHARTIST LEGEND =============================================================
|
||||
|
||||
.ct-legend {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
margin-bottom: -1em;
|
||||
|
||||
li {
|
||||
position: relative;
|
||||
padding-left: 1.4em;
|
||||
padding-left: 0.5em;
|
||||
list-style-type: none;
|
||||
display: inline;
|
||||
display: inline-block;
|
||||
margin-right: 0.5em;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
li:before {
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
content: '';
|
||||
border: 3px solid transparent;
|
||||
border-radius: 2px;
|
||||
margin-top: 0.5em;
|
||||
margin-right: 0.2em;
|
||||
}
|
||||
|
||||
li.inactive:before {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// import _ from 'intl' TODO: fix tooltip
|
||||
import _ from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
// import Tooltip from 'tooltip' TODO: fix tooltip
|
||||
import Tooltip from 'tooltip'
|
||||
import {
|
||||
ButtonGroup
|
||||
} from 'react-bootstrap-4/lib'
|
||||
@@ -10,17 +10,17 @@ import {
|
||||
const ActionBar = ({ actions, param }) => (
|
||||
<ButtonGroup>
|
||||
{map(actions, ({ handler, handlerParam = param, label, icon }, index) => (
|
||||
/* <Tooltip key={index} content={_(label)}> TODO: fix tooltip */
|
||||
<ActionButton
|
||||
key={index}
|
||||
btnStyle='secondary'
|
||||
handler={handler}
|
||||
handlerParam={handlerParam}
|
||||
icon={icon}
|
||||
size='large'
|
||||
/>
|
||||
/* </Tooltip> */
|
||||
))}
|
||||
<Tooltip key={index} content={_(label)}>
|
||||
<ActionButton
|
||||
key={index}
|
||||
btnStyle='secondary'
|
||||
handler={handler}
|
||||
handlerParam={handlerParam}
|
||||
icon={icon}
|
||||
size='large'
|
||||
/>
|
||||
</Tooltip>
|
||||
))}
|
||||
</ButtonGroup>
|
||||
)
|
||||
ActionBar.propTypes = {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Button } from 'react-bootstrap-4/lib'
|
||||
|
||||
import Component from './base-component'
|
||||
import logError from './log-error'
|
||||
import { autobind, propTypes } from './utils'
|
||||
import propTypes from './prop-types'
|
||||
|
||||
@propTypes({
|
||||
btnStyle: propTypes.string,
|
||||
@@ -28,7 +28,6 @@ export default class ActionButton extends Component {
|
||||
router: React.PropTypes.object
|
||||
}
|
||||
|
||||
@autobind
|
||||
async _execute () {
|
||||
if (this.state.working) {
|
||||
return
|
||||
@@ -66,6 +65,7 @@ export default class ActionButton extends Component {
|
||||
logError(error)
|
||||
}
|
||||
}
|
||||
_execute = ::this._execute
|
||||
|
||||
_eventListener = event => {
|
||||
event.preventDefault()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import ActionButton from 'action-button'
|
||||
import React from 'react'
|
||||
import { propTypes } from 'utils'
|
||||
|
||||
import ActionButton from './action-button'
|
||||
import propTypes from './prop-types'
|
||||
|
||||
const ActionToggle = ({ className, value, ...props }) =>
|
||||
<ActionButton
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import forEach from 'lodash/forEach'
|
||||
import { Component } from 'react'
|
||||
|
||||
import getEventValue from './get-event-value'
|
||||
import invoke from './invoke'
|
||||
import shallowEqual from './shallow-equal'
|
||||
|
||||
@@ -11,6 +12,8 @@ export default class BaseComponent extends Component {
|
||||
// It really should have been done in React.Component!
|
||||
this.state = {}
|
||||
|
||||
this._linkedState = null
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
this.render = invoke(this.render, render => () => {
|
||||
console.log('render', this.constructor.name)
|
||||
@@ -20,6 +23,23 @@ export default class BaseComponent extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
// See https://preactjs.com/guide/linked-state
|
||||
linkState (name) {
|
||||
let linkedState = this._linkedState
|
||||
let cb
|
||||
if (!linkedState) {
|
||||
linkedState = this._linkedState = {}
|
||||
} else if ((cb = linkedState[name])) {
|
||||
return cb
|
||||
}
|
||||
|
||||
return (linkedState[name] = event => {
|
||||
this.setState({
|
||||
[name]: getEventValue(event)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
shouldComponentUpdate (newProps, newState) {
|
||||
return !(
|
||||
shallowEqual(this.props, newProps) &&
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react'
|
||||
import { propTypes } from 'utils'
|
||||
|
||||
import propTypes from './prop-types'
|
||||
|
||||
const CARD_STYLE = {
|
||||
minHeight: '100%'
|
||||
@@ -33,9 +34,9 @@ export const CardHeader = propTypes({
|
||||
children,
|
||||
className
|
||||
}) => (
|
||||
<h3 className={`card-header ${className || ''}`} style={CARD_HEADER_STYLE}>
|
||||
<h4 className={`card-header ${className || ''}`} style={CARD_HEADER_STYLE}>
|
||||
{children}
|
||||
</h3>
|
||||
</h4>
|
||||
))
|
||||
|
||||
export const CardBlock = propTypes({
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import Component from 'base-component'
|
||||
import Icon from 'icon'
|
||||
import React from 'react'
|
||||
import { propTypes } from 'utils'
|
||||
|
||||
import Component from './base-component'
|
||||
import Icon from './icon'
|
||||
import propTypes from './prop-types'
|
||||
|
||||
@propTypes({
|
||||
children: propTypes.any.isRequired,
|
||||
|
||||
3
src/common/combobox/index.css
Normal file
3
src/common/combobox/index.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.button {
|
||||
border-radius: 0px;
|
||||
};
|
||||
101
src/common/combobox/index.js
Normal file
101
src/common/combobox/index.js
Normal file
@@ -0,0 +1,101 @@
|
||||
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,
|
||||
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}
|
||||
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>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import classNames from 'classnames'
|
||||
import React, { createElement } from 'react'
|
||||
|
||||
import Icon from '../icon'
|
||||
import { propTypes } from '../utils'
|
||||
import propTypes from '../prop-types'
|
||||
|
||||
import styles from './index.css'
|
||||
|
||||
|
||||
9
src/common/d3-utils.js
vendored
Normal file
9
src/common/d3-utils.js
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
import forEach from 'lodash/forEach'
|
||||
|
||||
export function setStyles (style) {
|
||||
forEach(style, (value, key) => {
|
||||
this.style(key, value)
|
||||
})
|
||||
|
||||
return this
|
||||
}
|
||||
@@ -7,10 +7,11 @@ import React from 'react'
|
||||
|
||||
import _ from './intl'
|
||||
import Component from './base-component'
|
||||
import logError from './log-error'
|
||||
import Icon from './icon'
|
||||
import logError from './log-error'
|
||||
import propTypes from './prop-types'
|
||||
import Tooltip from './tooltip'
|
||||
import { formatSize, propTypes } from './utils'
|
||||
import { formatSize } from './utils'
|
||||
import { SizeInput } from './form'
|
||||
import {
|
||||
SelectHost,
|
||||
@@ -27,7 +28,13 @@ import {
|
||||
const LONG_CLICK = 400
|
||||
const SELECT_STYLE = { padding: '0px' }
|
||||
const SIZE_STYLE = { width: '10rem' }
|
||||
const EDITABLE_STYLE = { borderBottom: '1px dashed #ccc' }
|
||||
const EDITABLE_STYLE = {
|
||||
borderBottom: '1px dashed #ccc',
|
||||
cursor: 'context-menu'
|
||||
}
|
||||
const LONG_EDITABLE_STYLE = {
|
||||
cursor: 'context-menu'
|
||||
}
|
||||
|
||||
@propTypes({
|
||||
alt: propTypes.node.isRequired
|
||||
@@ -156,7 +163,7 @@ class Editable extends Component {
|
||||
const { useLongClick } = props
|
||||
|
||||
const success = <Icon icon='success' />
|
||||
return <span style={useLongClick ? null : EDITABLE_STYLE}>
|
||||
return <span style={useLongClick ? LONG_EDITABLE_STYLE : EDITABLE_STYLE}>
|
||||
<span
|
||||
onClick={!useLongClick && this._openEdition}
|
||||
onMouseDown={useLongClick && this.__startTimer}
|
||||
@@ -270,20 +277,34 @@ export class Password extends Text {
|
||||
}
|
||||
|
||||
@propTypes({
|
||||
value: propTypes.number.isRequired
|
||||
nullable: propTypes.bool,
|
||||
value: propTypes.number
|
||||
})
|
||||
export class Number extends Component {
|
||||
get value () {
|
||||
return +this.refs.input.value
|
||||
}
|
||||
|
||||
_onChange = value => this.props.onChange(+value)
|
||||
_onChange = value => {
|
||||
if (value === '') {
|
||||
if (this.props.nullable) {
|
||||
value = null
|
||||
} else {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
value = +value
|
||||
}
|
||||
|
||||
this.props.onChange(value)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { value } = this.props
|
||||
return <Text
|
||||
{...this.props}
|
||||
onChange={this._onChange}
|
||||
value={String(this.props.value)}
|
||||
value={value === null ? '' : String(value)}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ export { Ellipsis as default }
|
||||
export const EllipsisContainer = ({ children }) => (
|
||||
<div style={ellipsisContainerStyle}>
|
||||
{React.Children.map(children, child =>
|
||||
child.type === Ellipsis ? child : <span>{child}</span>
|
||||
child == null || child.type === Ellipsis ? child : <span>{child}</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react'
|
||||
import * as Grid from 'grid'
|
||||
import { propTypes } from 'utils'
|
||||
|
||||
import * as Grid from './grid'
|
||||
import propTypes from './prop-types'
|
||||
|
||||
export const LabelCol = propTypes({
|
||||
children: propTypes.any.isRequired
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import BaseComponent from 'base-component'
|
||||
import classNames from 'classnames'
|
||||
import Icon from 'icon'
|
||||
import map from 'lodash/map'
|
||||
import randomPassword from 'random-password'
|
||||
import React from 'react'
|
||||
import round from 'lodash/round'
|
||||
import {
|
||||
DropdownButton,
|
||||
MenuItem
|
||||
} from 'react-bootstrap-4/lib'
|
||||
|
||||
import Component from '../base-component'
|
||||
import propTypes from '../prop-types'
|
||||
import {
|
||||
autobind,
|
||||
firstDefined,
|
||||
formatSizeRaw,
|
||||
parseSize,
|
||||
propTypes
|
||||
parseSize
|
||||
} from '../utils'
|
||||
|
||||
export Select from './select'
|
||||
@@ -33,16 +35,14 @@ export class Password extends Component {
|
||||
this.refs.field.value = value
|
||||
}
|
||||
|
||||
@autobind
|
||||
_generate () {
|
||||
_generate = () => {
|
||||
this.refs.field.value = randomPassword(8)
|
||||
this.setState({
|
||||
visible: true
|
||||
})
|
||||
}
|
||||
|
||||
@autobind
|
||||
_toggleVisibility () {
|
||||
_toggleVisibility = () => {
|
||||
this.setState({
|
||||
visible: !this.state.visible
|
||||
})
|
||||
@@ -107,8 +107,7 @@ export class Range extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
_handleChange (event) {
|
||||
_handleChange = event => {
|
||||
const { onChange } = this.props
|
||||
const { value } = event.target
|
||||
|
||||
@@ -162,38 +161,88 @@ const DEFAULT_UNIT = 'GiB'
|
||||
placeholder: propTypes.string,
|
||||
readOnly: propTypes.bool,
|
||||
required: propTypes.bool,
|
||||
style: propTypes.object
|
||||
style: propTypes.object,
|
||||
value: propTypes.number
|
||||
})
|
||||
export class SizeInput extends Component {
|
||||
export class SizeInput extends BaseComponent {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
const humanSize = props.defaultValue && formatSizeRaw(props.defaultValue)
|
||||
this._defaultValue = humanSize && humanSize.value
|
||||
this.state = { unit: humanSize ? humanSize.prefix + 'B' : props.defaultUnit || DEFAULT_UNIT }
|
||||
this.state = this._createStateFromBytes(firstDefined(props.value, props.defaultValue, 0))
|
||||
}
|
||||
|
||||
componentWillReceiveProps (newProps) {
|
||||
this.value = newProps.value
|
||||
const { value } = newProps
|
||||
if (value == null && value === this.props.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const { _bytes, _unit, _value } = this
|
||||
this._bytes = this._unit = this._value = null
|
||||
|
||||
if (value === _bytes) {
|
||||
// Update input value
|
||||
this.setState({
|
||||
unit: _unit,
|
||||
value: _value
|
||||
})
|
||||
} else {
|
||||
this.setState(this._createStateFromBytes(value))
|
||||
}
|
||||
}
|
||||
|
||||
_createStateFromBytes = bytes => {
|
||||
const humanSize = bytes && formatSizeRaw(bytes)
|
||||
return {
|
||||
unit: humanSize && humanSize.value ? humanSize.prefix + 'B' : this.props.defaultUnit || DEFAULT_UNIT,
|
||||
value: humanSize ? round(humanSize.value, 3) : ''
|
||||
}
|
||||
}
|
||||
|
||||
get value () {
|
||||
const value = this.refs.value.value
|
||||
return value ? parseSize(value + ' ' + this.state.unit) : undefined
|
||||
const { unit, value } = this.state
|
||||
return parseSize(value + ' ' + unit)
|
||||
}
|
||||
|
||||
set value (newValue) {
|
||||
const humanSize = newValue && formatSizeRaw(newValue)
|
||||
this.refs.value.value = humanSize ? humanSize.value : ''
|
||||
this.setState({ unit: humanSize ? humanSize.prefix + 'B' : DEFAULT_UNIT })
|
||||
if (
|
||||
process.env.NODE_ENV !== 'production' &&
|
||||
this.props.value != null
|
||||
) {
|
||||
throw new Error('cannot set value of controlled SizeInput')
|
||||
}
|
||||
this.setState(this._createStateFromBytes(newValue))
|
||||
}
|
||||
|
||||
_onChange = () =>
|
||||
this.props.onChange && this.props.onChange(this.value)
|
||||
_onChange = value =>
|
||||
this.props.onChange && this.props.onChange(value)
|
||||
|
||||
_updateValue = event => {
|
||||
const { value } = event.target
|
||||
if (this.props.value != null) {
|
||||
this._value = value
|
||||
this._unit = this.state.unit
|
||||
this._bytes = parseSize((value || 0) + ' ' + this.state.unit)
|
||||
|
||||
this._onChange(this._bytes)
|
||||
} else {
|
||||
this.setState({ value }, () => {
|
||||
this._onChange(this.value)
|
||||
})
|
||||
}
|
||||
}
|
||||
_updateUnit = unit => {
|
||||
this.setState({ unit })
|
||||
this._onChange()
|
||||
if (this.props.value != null) {
|
||||
this._value = this.state.value
|
||||
this._unit = unit
|
||||
this._bytes = parseSize((this.state.value || 0) + ' ' + unit)
|
||||
|
||||
this._onChange(this._bytes)
|
||||
} else {
|
||||
this.setState({ unit }, () => {
|
||||
this._onChange(this.value)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
@@ -206,6 +255,11 @@ export class SizeInput extends Component {
|
||||
style
|
||||
} = this.props
|
||||
|
||||
const {
|
||||
value,
|
||||
unit
|
||||
} = this.state
|
||||
|
||||
return <span
|
||||
className={classNames(className, 'input-group')}
|
||||
style={style}
|
||||
@@ -213,14 +267,13 @@ export class SizeInput extends Component {
|
||||
<input
|
||||
autoFocus={autoFocus}
|
||||
className='form-control'
|
||||
defaultValue={this._defaultValue}
|
||||
min={0}
|
||||
onChange={this._onChange}
|
||||
onChange={this._updateValue}
|
||||
placeholder={placeholder}
|
||||
readOnly={readOnly}
|
||||
required={required}
|
||||
ref='value'
|
||||
type='number'
|
||||
value={value}
|
||||
/>
|
||||
<span className='input-group-btn'>
|
||||
<DropdownButton
|
||||
@@ -228,7 +281,7 @@ export class SizeInput extends Component {
|
||||
disabled={readOnly}
|
||||
id='size'
|
||||
pullRight
|
||||
title={this.state.unit}
|
||||
title={unit}
|
||||
>
|
||||
{map(UNITS, unit =>
|
||||
<MenuItem
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import find from 'lodash/find'
|
||||
import map from 'lodash/map'
|
||||
import React, { Component } from 'react'
|
||||
import { Select } from 'form'
|
||||
import { propTypes } from 'utils'
|
||||
|
||||
import propTypes from '../prop-types'
|
||||
|
||||
import Select from './select'
|
||||
|
||||
@propTypes({
|
||||
autoFocus: propTypes.bool,
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React, { Component } from 'react'
|
||||
import ReactSelect from 'react-select'
|
||||
import { propTypes } from 'utils'
|
||||
import {
|
||||
AutoSizer,
|
||||
VirtualScroll
|
||||
} from 'react-virtualized'
|
||||
|
||||
import propTypes from '../prop-types'
|
||||
|
||||
const SELECT_MENU_STYLE = {
|
||||
overflow: 'hidden'
|
||||
}
|
||||
|
||||
17
src/common/get-event-value.js
Normal file
17
src/common/get-event-value.js
Normal file
@@ -0,0 +1,17 @@
|
||||
// If the param is an event, returns the value of it's target,
|
||||
// otherwise returns the param.
|
||||
const getEventValue = event => {
|
||||
let target
|
||||
if (!event || !(target = event.target)) {
|
||||
return event
|
||||
}
|
||||
|
||||
return (
|
||||
target.nodeName.toLowerCase() === 'input' &&
|
||||
target.type.toLowerCase() === 'checkbox'
|
||||
)
|
||||
? target.checked
|
||||
: target.value
|
||||
}
|
||||
|
||||
export { getEventValue as default }
|
||||
@@ -1,6 +1,7 @@
|
||||
import classNames from 'classnames'
|
||||
import React from 'react'
|
||||
import { propTypes } from 'utils'
|
||||
|
||||
import propTypes from './prop-types'
|
||||
|
||||
export const Col = propTypes({
|
||||
className: propTypes.string,
|
||||
|
||||
16
src/common/home-filters.js
Normal file
16
src/common/home-filters.js
Normal file
@@ -0,0 +1,16 @@
|
||||
export const VM = {
|
||||
homeFilterPendingVms: 'current_operations:"" ',
|
||||
homeFilterNonRunningVms: '!power_state:running ',
|
||||
homeFilterHvmGuests: 'virtualizationMode:hvm ',
|
||||
homeFilterRunningVms: 'power_state:running ',
|
||||
homeFilterTags: 'tags:'
|
||||
}
|
||||
|
||||
export const host = {
|
||||
homeFilterRunningHosts: 'power_state:running ',
|
||||
homeFilterTags: 'tags:'
|
||||
}
|
||||
|
||||
export const pool = {
|
||||
homeFilterTags: 'tags:'
|
||||
}
|
||||
216
src/common/hosts-patches-table.js
Normal file
216
src/common/hosts-patches-table.js
Normal file
@@ -0,0 +1,216 @@
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import { Portal } from 'react-overlays'
|
||||
|
||||
import _ from './intl'
|
||||
import ActionButton from './action-button'
|
||||
import Component from './base-component'
|
||||
import forEach from 'lodash/forEach'
|
||||
import Link from './link'
|
||||
import propTypes from './prop-types'
|
||||
import SortedTable from './sorted-table'
|
||||
import TabButton from './tab-button'
|
||||
import { connectStore } from './utils'
|
||||
import {
|
||||
createGetObjectsOfType,
|
||||
createFilter,
|
||||
createSelector
|
||||
} from './selectors'
|
||||
import {
|
||||
getHostMissingPatches,
|
||||
installAllHostPatches
|
||||
} from './xo'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const MISSING_PATCHES_COLUMNS = [
|
||||
{
|
||||
name: _('srHost'),
|
||||
itemRenderer: host => <Link to={`/hosts/${host.id}`}>{host.name_label}</Link>,
|
||||
sortCriteria: host => host.name_label
|
||||
},
|
||||
{
|
||||
name: _('hostDescription'),
|
||||
itemRenderer: host => host.name_description,
|
||||
sortCriteria: host => host.name_description
|
||||
},
|
||||
{
|
||||
name: _('hostMissingPatches'),
|
||||
itemRenderer: (host, { missingPatches }) => <Link to={`/hosts/${host.id}/patches`}>{missingPatches[host.id]}</Link>,
|
||||
sortCriteria: (host, { missingPatches }) => missingPatches[host.id]
|
||||
},
|
||||
{
|
||||
name: _('patchUpdateButton'),
|
||||
itemRenderer: (host, { installAllHostPatches }) => (
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
handler={installAllHostPatches}
|
||||
handlerParam={host}
|
||||
icon='host-patch-update'
|
||||
/>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
const POOLS_MISSING_PATCHES_COLUMNS = [{
|
||||
name: _('srPool'),
|
||||
itemRenderer: (host, { pools }) => {
|
||||
const pool = pools[host.$pool]
|
||||
return <Link to={`/pools/${pool.id}`}>{pool.name_label}</Link>
|
||||
},
|
||||
sortCriteria: (host, { pools }) => pools[host.$pool].name_label
|
||||
}].concat(MISSING_PATCHES_COLUMNS)
|
||||
|
||||
// ===================================================================
|
||||
|
||||
class HostsPatchesTable extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state.missingPatches = {}
|
||||
}
|
||||
|
||||
_getHosts = createFilter(
|
||||
() => this.props.hosts,
|
||||
createSelector(
|
||||
() => this.state.missingPatches,
|
||||
missingPatches => host => missingPatches[host.id]
|
||||
)
|
||||
)
|
||||
|
||||
_refreshMissingPatches = () => (
|
||||
Promise.all(
|
||||
map(this.props.hosts, this._refreshHostMissingPatches)
|
||||
)
|
||||
)
|
||||
|
||||
_installAllMissingPatches = () => (
|
||||
Promise.all(map(this._getHosts(), this._installAllHostPatches))
|
||||
)
|
||||
|
||||
_refreshHostMissingPatches = host => (
|
||||
getHostMissingPatches(host).then(patches => {
|
||||
this.setState({
|
||||
missingPatches: {
|
||||
...this.state.missingPatches,
|
||||
[host.id]: patches.length
|
||||
}
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
_installAllHostPatches = host => (
|
||||
installAllHostPatches(host).then(() =>
|
||||
this._refreshHostMissingPatches(host)
|
||||
)
|
||||
)
|
||||
|
||||
componentWillMount () {
|
||||
this._refreshMissingPatches()
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
// Force one Portal refresh.
|
||||
// Because Portal cannot see the container reference at first rendering.
|
||||
this.forceUpdate()
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
forEach(nextProps.hosts, host => {
|
||||
const { id } = host
|
||||
|
||||
if (this.state.missingPatches[id] !== undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({
|
||||
missingPatches: {
|
||||
...this.state.missingPatches,
|
||||
[id]: 0
|
||||
}
|
||||
})
|
||||
|
||||
this._refreshHostMissingPatches(host)
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
const hosts = this._getHosts()
|
||||
const noPatches = isEmpty(hosts)
|
||||
const { props } = this
|
||||
|
||||
const Container = props.container || 'div'
|
||||
const Button = props.useTabButton ? TabButton : ActionButton
|
||||
|
||||
const Buttons = (
|
||||
<Container>
|
||||
<Button
|
||||
btnStyle='secondary'
|
||||
handler={this._refreshMissingPatches}
|
||||
icon='refresh'
|
||||
labelId='refreshPatches'
|
||||
/>
|
||||
<Button
|
||||
btnStyle='primary'
|
||||
disabled={noPatches}
|
||||
handler={this._installAllMissingPatches}
|
||||
icon='host-patch-update'
|
||||
labelId='installPoolPatches'
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!noPatches
|
||||
? (
|
||||
<SortedTable
|
||||
collection={hosts}
|
||||
columns={props.displayPools ? POOLS_MISSING_PATCHES_COLUMNS : MISSING_PATCHES_COLUMNS}
|
||||
userData={{
|
||||
installAllHostPatches: this._installAllHostPatches,
|
||||
missingPatches: this.state.missingPatches,
|
||||
pools: props.pools
|
||||
}}
|
||||
/>
|
||||
) : <p>{_('patchNothing')}</p>
|
||||
}
|
||||
<Portal container={() => props.buttonsGroupContainer()}>
|
||||
{Buttons}
|
||||
</Portal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@connectStore(() => {
|
||||
const getPools = createGetObjectsOfType('pool')
|
||||
|
||||
return {
|
||||
pools: getPools
|
||||
}
|
||||
})
|
||||
class HostsPatchesTableByPool extends Component {
|
||||
render () {
|
||||
const { props } = this
|
||||
return <HostsPatchesTable {...props} pools={props.pools} />
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default propTypes({
|
||||
buttonsGroupContainer: propTypes.func.isRequired,
|
||||
container: propTypes.any,
|
||||
displayPools: propTypes.bool,
|
||||
hosts: propTypes.oneOfType([
|
||||
propTypes.arrayOf(propTypes.object),
|
||||
propTypes.objectOf(propTypes.object)
|
||||
]).isRequired,
|
||||
useTabButton: propTypes.bool
|
||||
})(props => props.displayPools
|
||||
? <HostsPatchesTableByPool {...props} />
|
||||
: <HostsPatchesTable {...props} />
|
||||
)
|
||||
2062
src/common/intl/locales/es.js
Normal file
2062
src/common/intl/locales/es.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -44,8 +44,8 @@ export default {
|
||||
newSrPage: 'Stockage',
|
||||
newImport: 'Importer',
|
||||
// ----- Home view -----
|
||||
homeDisplayedVms: '{displayed}x {vmIcon} (sur {total})',
|
||||
homeSelectedVms: '{selected}x {vmIcon} sélectionnée{selected, plural, zero {} one {} other {s}} (sur {total})',
|
||||
homeDisplayedItems: '{displayed}x {icon} (sur {total})',
|
||||
homeSelectedItems: '{selected}x {icon} sélectionnée{selected, plural, zero {} one {} other {s}} (sur {total})',
|
||||
homeMigrateTo: 'Migrer sur…',
|
||||
// ----- General Stuff -----
|
||||
homePage: 'Accueil',
|
||||
@@ -82,20 +82,15 @@ export default {
|
||||
fillOptionalInformations: 'Remplir informations (optionnel)',
|
||||
selectTableReset: 'Réinitialiser',
|
||||
schedulingMonth: 'Mois',
|
||||
schedulingEveryMonth: 'Tous les mois',
|
||||
schedulingEachSelectedMonth: 'Chaque mois sélectionné',
|
||||
schedulingMonthDay: 'Jour du mois',
|
||||
schedulingEveryMonthDay: 'Tous les jours',
|
||||
schedulingEachSelectedMonthDay: 'Chaque jour sélectionné',
|
||||
schedulingWeekDay: 'Jour de la semaine',
|
||||
schedulingEveryWeekDay: 'Tous les jours',
|
||||
schedulingEachSelectedWeekDay: 'Chaque jour sélectionné',
|
||||
schedulingHour: 'Heure',
|
||||
schedulingEveryHour: 'Toutes les heures',
|
||||
schedulingEveryNHour: 'Toutes les N heures',
|
||||
schedulingEachSelectedHour: 'Chaque heure sélectionnée',
|
||||
schedulingMinute: 'Minute',
|
||||
schedulingEveryMinute: 'Toutes les minutes',
|
||||
schedulingEveryNMinute: 'Toutes les N minutes',
|
||||
schedulingEachSelectedMinute: 'Chaque minute sélectionnée',
|
||||
schedulingReset: 'Reset',
|
||||
@@ -279,7 +274,7 @@ export default {
|
||||
srStatePanel: 'État du stockage',
|
||||
taskStatePanel: 'Tâches en cours',
|
||||
usersStatePanel: 'Utilisateurs',
|
||||
ofUsage: 'sur',
|
||||
ofUsage: '{usage} (sur {total})',
|
||||
noSrs: 'Aucun stockage',
|
||||
srName: 'Nom',
|
||||
srPool: 'Pool',
|
||||
@@ -310,7 +305,8 @@ export default {
|
||||
alarmPool: 'Pool',
|
||||
alarmRemoveAll: 'Supprimer toutes les alarmes',
|
||||
// ----- New VM -----
|
||||
newVmCreateNewVmOn: 'Créer une nouvelle VM sur {pool}',
|
||||
newVmCreateNewVmOn: 'Créer une nouvelle VM sur {select}',
|
||||
newVmCreateNewVmOn2: 'Créer une nouvelle VM sur {select1} ou {select2}',
|
||||
newVmInfoPanel: 'Informations',
|
||||
newVmNameLabel: 'Nom',
|
||||
newVmTemplateLabel: 'Modèle',
|
||||
@@ -360,14 +356,21 @@ export default {
|
||||
startVmImport: 'Lancement de l\'import…',
|
||||
startVmExport: 'Lancement de l\'export…',
|
||||
// ----- Modals -----
|
||||
stopHostModalTitle: 'Arrêter l\'hôte',
|
||||
stopHostModalMessage: 'Cette action va arrêter l\'hôte. Êtes vous sûr de vouloir continuer ?',
|
||||
restartHostModalTitle: 'Redémarrer l\'hôte',
|
||||
restartHostModalMessage: 'Cette action va redémarrer l\'hôte. Êtes vous sûr de vouloir continuer ?',
|
||||
startVmsModalTitle: 'Démarrer {vms, plural, one {la} other {les}} VM{vms, plural, one {} other {s}}',
|
||||
startVmsModalMessage: 'Voulez-vous vraiment démarrer {vms} VM{vms, plural, one {} other {s}} ?',
|
||||
stopVmsModalTitle: 'Arrêter {vms, plural, one {la} other {les}} VM{vms, plural, one {} other {s}}',
|
||||
restartVmModalTitle: 'Redémarrer la VM',
|
||||
restartVmModalMessage: 'Voulez-vous vraiment redémarrer {name} ?',
|
||||
stopVmModalTitle: 'Arrêter la VM',
|
||||
stopVmModalMessage: 'Voulez-vous vraiment arrêter {name} ?',
|
||||
stopVmsModalMessage: 'Voulez-vous vraiment arrêter {vms} VM{vms, plural, one {} other {s}} ?',
|
||||
restartVmsModalTitle: 'Redémarrer {vms, plural, one {la} other {les}} VM{vms, plural, one {} other {s}}',
|
||||
restartVmsModalMessage: 'Voulez-vous vraiment redémarrer {vms} VM{vms, plural, one {} other {s}} ?',
|
||||
migrateVmModalTitle: 'Migrer la VM',
|
||||
migrateVmModalBody: 'Voulez-vous vraiment migrer cette VM sur {hostName} ?',
|
||||
migrateVmAdvancedModalSelectHost: 'Sélectionnez un hôte de destination:',
|
||||
migrateVmAdvancedModalSelectNetwork: 'Sélectionnez un réseau pour la migration:',
|
||||
migrateVmAdvancedModalSelectSrs: 'Pour chaque VDI, sélectionnez un SR:',
|
||||
|
||||
@@ -272,11 +272,11 @@ export default {
|
||||
// Original text: "vCPUs"
|
||||
homeSortByvCPUs: 'כמות המאבדים',
|
||||
|
||||
// Original text: "{displayed, number}x {vmIcon} (on {total, number})"
|
||||
homeDisplayedVms: undefined,
|
||||
// Original text: "{displayed, number}x {icon} (on {total, number})"
|
||||
homeDisplayedItems: undefined,
|
||||
|
||||
// Original text: "{selected, number}x {vmIcon} selected (on {total, number})"
|
||||
homeSelectedVms: undefined,
|
||||
// Original text: "{selected, number}x {icon} selected (on {total, number})"
|
||||
homeSelectedItems: undefined,
|
||||
|
||||
// Original text: "More"
|
||||
homeMore: 'עוד',
|
||||
@@ -1676,6 +1676,30 @@ export default {
|
||||
// Original text: "No backups available"
|
||||
noBackup: undefined,
|
||||
|
||||
// Original text: "Shutdown host"
|
||||
stopHostModalTitle: undefined,
|
||||
|
||||
// Original text: "This will shutdown your host. Do you want to continue?"
|
||||
stopHostModalMessage: undefined,
|
||||
|
||||
// Original text: "Restart host"
|
||||
restartHostModalTitle: undefined,
|
||||
|
||||
// Original text: "This will restart your host. Do you want to continue?"
|
||||
restartHostModalMessage: undefined,
|
||||
|
||||
// Original text: "Restart VM"
|
||||
restartVmModalTitle: undefined,
|
||||
|
||||
// Original text: "Are you sure you want to restart {name}?"
|
||||
restartVmModalMessage: undefined,
|
||||
|
||||
// Original text: "Stop VM"
|
||||
stopVmModalTitle: undefined,
|
||||
|
||||
// Original text: "Are you sure you want to stop {name}?"
|
||||
stopVmModalMessage: undefined,
|
||||
|
||||
// Original text: "Start VM{vms, plural, one {} other {s}}"
|
||||
startVmsModalTitle: undefined,
|
||||
|
||||
@@ -1715,9 +1739,6 @@ export default {
|
||||
// Original text: "Migrate VM"
|
||||
migrateVmModalTitle: undefined,
|
||||
|
||||
// Original text: "Are you sure you want to migrate this VM to {hostName}?"
|
||||
migrateVmModalBody: undefined,
|
||||
|
||||
// Original text: "Select a destination host:"
|
||||
migrateVmAdvancedModalSelectHost: undefined,
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// See http://momentjs.com/docs/#/use-it/browserify/
|
||||
// See http://momentjs.com/docs/#/use-it/browserify/
|
||||
import 'moment/locale/pt'
|
||||
|
||||
import reactIntlData from 'react-intl/locale-data/pt'
|
||||
@@ -84,7 +84,7 @@ export default {
|
||||
newMenu: 'Novo(a)',
|
||||
|
||||
// Original text: "Tasks"
|
||||
taskMenu: 'Atividades',
|
||||
taskMenu: 'Tarefas',
|
||||
|
||||
// Original text: "VM"
|
||||
newVmPage: 'VM',
|
||||
@@ -146,6 +146,12 @@ export default {
|
||||
// Original text: "Custom Job"
|
||||
customJob: 'Personalização do Trabalho',
|
||||
|
||||
// Original text: "EN"
|
||||
enLang: 'Inglês',
|
||||
|
||||
// Original text: "FR"
|
||||
frLang: 'Francês',
|
||||
|
||||
// Original text: "Username:"
|
||||
usernameLabel: 'Usuário',
|
||||
|
||||
@@ -240,7 +246,7 @@ export default {
|
||||
homeFilterDisabledHosts: 'Hosts Desativados',
|
||||
|
||||
// Original text: "Running VMs"
|
||||
homeFilterRunningVms: 'Vms Ativas',
|
||||
homeFilterRunningVms: 'VMs Ativas',
|
||||
|
||||
// Original text: "Non running VMs"
|
||||
homeFilterNonRunningVms: 'VMs Paradas',
|
||||
@@ -270,10 +276,10 @@ export default {
|
||||
homeSortByvCPUs: 'vCPUs',
|
||||
|
||||
// Original text: "{displayed, number}x {vmIcon} (on {total, number})"
|
||||
homeDisplayedVms: '{displayed, number}x {vmIcon} (sobre {total, number})',
|
||||
homeDisplayedVms: '{displayed, number}x {vmIcon} (de {total, number})',
|
||||
|
||||
// Original text: "{selected, number}x {vmIcon} selected (on {total, number})"
|
||||
homeSelectedVms: '{selected, number}x {vmIcon} selected (sobre {total, number})',
|
||||
homeSelectedVms: '{selected, number}x {vmIcon} selected (de {total, number})',
|
||||
|
||||
// Original text: "More"
|
||||
homeMore: 'Mais',
|
||||
@@ -405,7 +411,7 @@ export default {
|
||||
unknownSchedule: 'Desconhecido',
|
||||
|
||||
// Original text: "Job"
|
||||
job: 'tarefa',
|
||||
job: 'Tarefa',
|
||||
|
||||
// Original text: "Job ID"
|
||||
jobId: 'ID tarefa',
|
||||
@@ -414,10 +420,10 @@ export default {
|
||||
jobName: 'Nome',
|
||||
|
||||
// Original text: "Start"
|
||||
jobStart: 'Iniciar',
|
||||
jobStart: 'Inicia',
|
||||
|
||||
// Original text: "End"
|
||||
jobEnd: 'Terminar',
|
||||
jobEnd: 'Termina',
|
||||
|
||||
// Original text: "Duration"
|
||||
jobDuration: 'Duração',
|
||||
@@ -507,7 +513,7 @@ export default {
|
||||
newSrTypeSelection: 'Selecionar o tipo de armazenamento (storage)',
|
||||
|
||||
// Original text: "Settings"
|
||||
newSrSettings: 'Configuraçõesé',
|
||||
newSrSettings: 'Configurações',
|
||||
|
||||
// Original text: "Storage Usage"
|
||||
newSrUsage: 'Uso de armazenamento (storage)',
|
||||
@@ -1308,7 +1314,7 @@ export default {
|
||||
vmPanel: 'VM{vms, plural, one {} other {s}}',
|
||||
|
||||
// Original text: "RAM Usage"
|
||||
memoryStatePanel: 'Utilização da RAM',
|
||||
memoryStatePanel: 'Utilização RAM',
|
||||
|
||||
// Original text: "CPUs Usage"
|
||||
cpuStatePanel: 'Utilização de CPU',
|
||||
@@ -1392,10 +1398,10 @@ export default {
|
||||
orphanedVms: 'VMs órfãs',
|
||||
|
||||
// Original text: "No orphans"
|
||||
noOrphanedObject: 'Sem órfãos',
|
||||
noOrphanedObject: 'Sem órfãs',
|
||||
|
||||
// Original text: "Remove all orphaned VDIs"
|
||||
removeAllOrphanedObject: 'Remover todos os VDIs órfãos',
|
||||
removeAllOrphanedObject: 'Remover todos as VDIs órfãs',
|
||||
|
||||
// Original text: "Name"
|
||||
vmNameLabel: 'Nome',
|
||||
@@ -1536,16 +1542,16 @@ export default {
|
||||
newVmCloudConfig: 'Configuração do Cloud',
|
||||
|
||||
// Origingal text: "Create VMs"
|
||||
newVmCreateVms: undefined,
|
||||
newVmCreateVms: 'Criar VMs',
|
||||
|
||||
// Original text : "Are you sure you want to create {nbVms} VMs?"
|
||||
newVmCreateVmsConfirm: undefined,
|
||||
newVmCreateVmsConfirm: 'Você tem certeza que deseja criar {nbVms} VMs?',
|
||||
|
||||
// Original text : "Multiple VMs"
|
||||
newVmMultipleVms: undefined,
|
||||
newVmMultipleVms: 'Multiplas VMs',
|
||||
|
||||
// Original text: "Resource sets"
|
||||
resourceSets: 'Ajustes dos recursos',
|
||||
resourceSets: 'Ajustes de recursos',
|
||||
|
||||
// Original text: "Resource set name"
|
||||
resourceSetName: 'Ajuste de nome do recurso',
|
||||
@@ -1569,7 +1575,7 @@ export default {
|
||||
deleteResourceSetWarning: 'Deletar grupo de recurso',
|
||||
|
||||
// Original text: "Are you sure you want to delete this resource set?"
|
||||
deleteResourceSetQuestion: 'Você tem certeza que deseja deletar este grupo de recurso?',
|
||||
deleteResourceSetQuestion: 'Você tem certeza que deseja deletar este ajuste?',
|
||||
|
||||
// Original text: "Missing objects:"
|
||||
resourceSetMissingObjects: 'Objetos faltando',
|
||||
@@ -1596,7 +1602,7 @@ export default {
|
||||
noHostsAvailable: 'Sem hosts disponiveis',
|
||||
|
||||
// Original text: "VMs created from this resource set shall run on the following hosts."
|
||||
availableHostsDescription: 'VMs criadas a partir desse conjunto de recursos deve ser executado nos seguintes hosts.',
|
||||
availableHostsDescription: 'VMs criadas a partir desse conjunto de recursos deve ser executado nos hosts indicados.',
|
||||
|
||||
// Original text: "Maximum CPUs"
|
||||
maxCpus: 'Limite de CPUs',
|
||||
@@ -1605,7 +1611,7 @@ export default {
|
||||
maxRam: 'Limite de RAM (GiB)',
|
||||
|
||||
// Original text: "Maximum disk space"
|
||||
maxDiskSpace: 'Limite de espaço do disco',
|
||||
maxDiskSpace: 'Limite de espaço de disco',
|
||||
|
||||
// Original text: "No limits."
|
||||
noResourceSetLimits: 'Sem limites',
|
||||
@@ -1620,7 +1626,7 @@ export default {
|
||||
usedResource: 'Usado:',
|
||||
|
||||
// Original text: "Try dropping some backups here, or click to select backups to upload. Accept only .xva files."
|
||||
importVmsList: 'Tente soltar alguns backups aqui, ou clique para selecionar backups que seja feito o upload. Apenas arquivos .xva são aceitos.',
|
||||
importVmsList: 'Tente soltar alguns backups aqui, ou clique para selecionar os backups para que seja feito o upload. Apenas arquivos .xva são aceitos.',
|
||||
|
||||
// Original text: "No selected VMs."
|
||||
noSelectedVms: 'Nenhuma VM selecionada',
|
||||
@@ -1673,6 +1679,30 @@ export default {
|
||||
// Original text: "No backups available"
|
||||
noBackup: 'Nenhum backup disponível',
|
||||
|
||||
// Original text: "Shutdown host"
|
||||
stopHostModalTitle: 'Desligar host',
|
||||
|
||||
// Original text: "This will shutdown your host. Do you want to continue?"
|
||||
stopHostModalMessage: 'O host será desligado. Você tem certeza que deseja continuar?',
|
||||
|
||||
// Original text: "Restart host"
|
||||
restartHostModalTitle: 'Reiniciar host',
|
||||
|
||||
// Original text: "This will restart your host. Do you want to continue?"
|
||||
restartHostModalMessage: 'O host será reiniciado. Você tem certeza que deseja continuar?',
|
||||
|
||||
// Original text: "Restart VM"
|
||||
restartVmModalTitle: 'Reiniciar VM',
|
||||
|
||||
// Original text: "Are you sure you want to restart {name}?"
|
||||
restartVmModalMessage: 'Você tem certeza que deseja reiniciar {name}?',
|
||||
|
||||
// Original text: "Stop VM"
|
||||
stopVmModalTitle: 'Parar VM',
|
||||
|
||||
// Original text: "Are you sure you want to stop {name}?"
|
||||
stopVmModalMessage: 'Você tem certeza que deseja parar {name}?',
|
||||
|
||||
// Original text: "Start VM{vms, plural, one {} other {s}}"
|
||||
startVmsModalTitle: 'Iniciar VM{vms, plural, one {} other {s}}',
|
||||
|
||||
@@ -1749,7 +1779,7 @@ export default {
|
||||
importBackupModalSelectBackup: 'Selecionar backup...',
|
||||
|
||||
// Original text: "Are you sure you want to remove all orphaned VDIs?"
|
||||
removeAllOrphanedModalWarning: 'Você tem certeza que deseja remover todos os VDIs orfãos?',
|
||||
removeAllOrphanedModalWarning: 'Você tem certeza que deseja remover todos as VDIs orfãs?',
|
||||
|
||||
// Original text: "Remove all logs"
|
||||
removeAllLogsModalTitle: 'Remover todos os logs',
|
||||
|
||||
2281
src/common/intl/locales/zh.js
Normal file
2281
src/common/intl/locales/zh.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -13,8 +13,15 @@ var messages = {
|
||||
confirmOk: 'OK',
|
||||
confirmCancel: 'Cancel',
|
||||
|
||||
// ----- Filters -----
|
||||
onError: 'On error',
|
||||
successful: 'Successful',
|
||||
|
||||
// ----- Titles -----
|
||||
homePage: 'Home',
|
||||
homeVmPage: 'VMs',
|
||||
homeHostPage: 'Hosts',
|
||||
homePoolPage: 'Pools',
|
||||
dashboardPage: 'Dashboard',
|
||||
overviewDashboardPage: 'Overview',
|
||||
overviewVisualizationDashboardPage: 'Visualizations',
|
||||
@@ -35,6 +42,7 @@ var messages = {
|
||||
aboutPage: 'About',
|
||||
newMenu: 'New',
|
||||
taskMenu: 'Tasks',
|
||||
taskPage: 'Tasks',
|
||||
newVmPage: 'VM',
|
||||
newSrPage: 'Storage',
|
||||
newServerPage: 'Server',
|
||||
@@ -55,11 +63,9 @@ var messages = {
|
||||
jobsNewPage: 'New',
|
||||
jobsSchedulingPage: 'Scheduling',
|
||||
customJob: 'Custom Job',
|
||||
userPage: 'User',
|
||||
|
||||
// ----- Sign in/out -----
|
||||
usernameLabel: 'Username:',
|
||||
passwordLabel: 'Password:',
|
||||
signInButton: 'Sign in',
|
||||
// ----- Sign out -----
|
||||
signOut: 'Sign out',
|
||||
|
||||
// ----- Home view ------
|
||||
@@ -100,10 +106,13 @@ var messages = {
|
||||
homeSortByPowerstate: 'Power state',
|
||||
homeSortByRAM: 'RAM',
|
||||
homeSortByvCPUs: 'vCPUs',
|
||||
homeDisplayedVms: '{displayed, number}x {vmIcon} (on {total, number})',
|
||||
homeSelectedVms: '{selected, number}x {vmIcon} selected (on {total, number})',
|
||||
homeSortByCpus: 'CPUs',
|
||||
homeDisplayedItems: '{displayed, number}x {icon} (on {total, number})',
|
||||
homeSelectedItems: '{selected, number}x {icon} selected (on {total, number})',
|
||||
homeMore: 'More',
|
||||
homeMigrateTo: 'Migrate to…',
|
||||
homeMissingPaths: 'Missing patches',
|
||||
highAvailability: 'High Availability',
|
||||
|
||||
// ----- Forms -----
|
||||
add: 'Add',
|
||||
@@ -120,11 +129,17 @@ var messages = {
|
||||
selectPifs: 'Select PIF(s)…',
|
||||
selectPools: 'Select Pool(s)…',
|
||||
selectRemotes: 'Select Remote(s)…',
|
||||
selectResourceSets: 'Select resource set(s)…',
|
||||
selectResourceSetsVmTemplate: 'Select template(s)…',
|
||||
selectResourceSetsSr: 'Select SR(s)…',
|
||||
selectResourceSetsNetwork: 'Select network(s)…',
|
||||
selectResourceSetsVdi: 'Select disk(s)…',
|
||||
selectSrs: 'Select SR(s)…',
|
||||
selectVms: 'Select VM(s)…',
|
||||
selectVmTemplates: 'Select VM template(s)…',
|
||||
selectTags: 'Select tag(s)…',
|
||||
selectVdis: 'Select disk(s)…',
|
||||
selectTimezone: 'Select timezone…',
|
||||
fillRequiredInformations: 'Fill required informations.',
|
||||
fillOptionalInformations: 'Fill informations (optional)',
|
||||
selectTableReset: 'Reset',
|
||||
@@ -132,24 +147,24 @@ var messages = {
|
||||
// --- Dates/Scheduler ---
|
||||
|
||||
schedulingMonth: 'Month',
|
||||
schedulingEveryMonth: 'Every month',
|
||||
schedulingEachSelectedMonth: 'Each selected month',
|
||||
schedulingMonthDay: 'Day of the month',
|
||||
schedulingEveryMonthDay: 'Every day',
|
||||
schedulingEachSelectedMonthDay: 'Each selected day',
|
||||
schedulingWeekDay: 'Day of the week',
|
||||
schedulingEveryWeekDay: 'Every day',
|
||||
schedulingEachSelectedWeekDay: 'Each selected day',
|
||||
schedulingHour: 'Hour',
|
||||
schedulingEveryHour: 'Every hour',
|
||||
schedulingEveryNHour: 'Every N hour',
|
||||
schedulingEachSelectedHour: 'Each selected hour',
|
||||
schedulingMinute: 'Minute',
|
||||
schedulingEveryMinute: 'Every minute',
|
||||
schedulingEveryNMinute: 'Every N minute',
|
||||
schedulingEachSelectedMinute: 'Each selected minute',
|
||||
schedulingReset: 'Reset',
|
||||
unknownSchedule: 'Unknown',
|
||||
timezonePickerServerValue: 'Xo-server timezone:',
|
||||
timezonePickerUseLocalTime: 'Web browser timezone',
|
||||
timezonePickerUseServerTime: 'Xo-server timezone',
|
||||
serverTimezoneOption: 'Server timezone ({value})',
|
||||
cronPattern: 'Cron Pattern:',
|
||||
backupEditNotFoundTitle: 'Cannot edit backup',
|
||||
backupEditNotFoundMessage: 'Missing required info for edition',
|
||||
job: 'Job',
|
||||
@@ -163,6 +178,8 @@ var messages = {
|
||||
jobTag: 'Tag',
|
||||
jobScheduling: 'Scheduling',
|
||||
jobState: 'State',
|
||||
jobTimezone: 'Timezone',
|
||||
jobServerTimezone: 'xo-server',
|
||||
runJob: 'Run job',
|
||||
runJobVerbose: 'One shot running started. See overview for logs.',
|
||||
jobStarted: 'Started',
|
||||
@@ -188,8 +205,17 @@ var messages = {
|
||||
remoteTypeNfs: 'NFS',
|
||||
remoteTypeSmb: 'SMB',
|
||||
remoteType: 'Type',
|
||||
remoteTestTip: 'Test your remote',
|
||||
testRemote: 'Test Remote',
|
||||
remoteTestFailure: 'Test failed for {name}',
|
||||
remoteTestSuccess: 'Test passed for {name}',
|
||||
remoteTestError: 'Error',
|
||||
remoteTestStep: 'Test Step',
|
||||
remoteTestFile: 'Test file',
|
||||
remoteTestSuccessMessage: 'The remote appears to work correctly',
|
||||
|
||||
// ------ New Storage -----
|
||||
newSrTitle: 'Create a new SR',
|
||||
newSrGeneral: 'General',
|
||||
newSrTypeSelection: 'Select Strorage Type:',
|
||||
newSrSettings: 'Settings',
|
||||
@@ -253,6 +279,25 @@ var messages = {
|
||||
cancelPluginEdition: 'Cancel',
|
||||
pluginConfigurationSuccess: 'Plugin configuration',
|
||||
pluginConfigurationChanges: 'Plugin configuration successfully saved!',
|
||||
pluginConfigurationPresetTitle: 'Predefined configuration',
|
||||
pluginConfigurationChoosePreset: 'Choose a predefined configuration.',
|
||||
applyPluginPreset: 'Apply',
|
||||
|
||||
// ----- User preferences -----
|
||||
saveNewUserFilterErrorTitle: 'Save filter error',
|
||||
saveNewUserFilterErrorBody: 'Bad parameter: name must be given.',
|
||||
filterName: 'Name:',
|
||||
filterValue: 'Value:',
|
||||
saveNewFilterTitle: 'Save new filter',
|
||||
setUserFiltersTitle: 'Set custom filters',
|
||||
setUserFiltersBody: 'Are you sure you want to set custom filters?',
|
||||
removeUserFilterTitle: 'Remove custom filter',
|
||||
removeUserFilterBody: 'Are you sure you want to remove custom filter?',
|
||||
defaultFilter: 'Default filter',
|
||||
defaultFilters: 'Default filters',
|
||||
customFilters: 'Custom filters',
|
||||
customizeFilters: 'Customize filters',
|
||||
saveCustomFilters: 'Save custom filters',
|
||||
|
||||
// ----- VM actions ------
|
||||
startVmLabel: 'Start',
|
||||
@@ -366,7 +411,7 @@ var messages = {
|
||||
patchNameLabel: 'Name',
|
||||
patchUpdateButton: 'Install all patches',
|
||||
patchDescription: 'Description',
|
||||
patchApplied: 'Release date',
|
||||
patchApplied: 'Applied date',
|
||||
patchSize: 'Size',
|
||||
patchStatus: 'Status',
|
||||
patchStatusApplied: 'Applied',
|
||||
@@ -375,9 +420,12 @@ var messages = {
|
||||
patchReleaseDate: 'Release date',
|
||||
patchGuidance: 'Guidance',
|
||||
patchAction: 'Action',
|
||||
hostInstalledPatches: 'Downloaded patches',
|
||||
hostAppliedPatches: 'Applied patches',
|
||||
hostMissingPatches: 'Missing patches',
|
||||
hostUpToDate: 'Host up-to-date!',
|
||||
// ----- Pool patch tabs -----
|
||||
refreshPatches: 'Refresh patches',
|
||||
installPoolPatches: 'Install pool patches',
|
||||
|
||||
// ----- VM tabs -----
|
||||
generalTabName: 'General',
|
||||
@@ -413,6 +461,7 @@ var messages = {
|
||||
statsCpu: 'CPU usage',
|
||||
statsMemory: 'Memory usage',
|
||||
statsNetwork: 'Network throughput',
|
||||
useStackedValuesOnStats: 'Stacked values',
|
||||
statDisk: 'Disk throughput',
|
||||
statLastTenMinutes: 'Last 10 minutes',
|
||||
statLastTwoHours: 'Last 2 hours',
|
||||
@@ -435,6 +484,7 @@ var messages = {
|
||||
vdiTags: 'Tags',
|
||||
vdiSize: 'Size',
|
||||
vdiSr: 'SR',
|
||||
vdiVm: 'VM',
|
||||
vdbBootableStatus: 'Boot flag',
|
||||
vdbStatus: 'Status',
|
||||
vbdStatusConnected: 'Connected',
|
||||
@@ -452,6 +502,7 @@ var messages = {
|
||||
vifStatusConnected: 'Connected',
|
||||
vifStatusDisconnected: 'Disconnected',
|
||||
vifIpAddresses: 'IP addresses',
|
||||
vifMacAutoGenerate: 'Auto-generated if empty',
|
||||
|
||||
// ----- VM snapshot tab -----
|
||||
noSnapshots: 'No snapshots',
|
||||
@@ -478,7 +529,9 @@ var messages = {
|
||||
uuid: 'UUID',
|
||||
virtualizationMode: 'Virtualization mode',
|
||||
cpuWeightLabel: 'CPU weight',
|
||||
defaultCpuWeight: 'Default',
|
||||
defaultCpuWeight: 'Default ({value, number})',
|
||||
cpuCapLabel: 'CPU cap',
|
||||
defaultCpuCap: 'Default ({value, number})',
|
||||
pvArgsLabel: 'PV args',
|
||||
xenToolsStatus: 'Xen tools status',
|
||||
xenToolsStatusValue: {
|
||||
@@ -516,7 +569,7 @@ var messages = {
|
||||
taskStatePanel: 'Pending tasks',
|
||||
usersStatePanel: 'Users',
|
||||
srStatePanel: 'Storage state',
|
||||
ofUsage: 'of',
|
||||
ofUsage: '{usage} (of {total})',
|
||||
noSrs: 'No storage',
|
||||
srName: 'Name',
|
||||
srPool: 'Pool',
|
||||
@@ -528,6 +581,7 @@ var messages = {
|
||||
srFree: 'free',
|
||||
srUsageStatePanel: 'Storage Usage',
|
||||
srTopUsageStatePanel: 'Top 5 SR Usage (in %)',
|
||||
vmsStates: '{running} running ({halted} halted)',
|
||||
|
||||
// --- Stats board --
|
||||
weekHeatmapData: '{value} {date, date, medium}',
|
||||
@@ -561,7 +615,9 @@ var messages = {
|
||||
alarmRemoveAll: 'Remove all alarms',
|
||||
|
||||
// ----- New VM -----
|
||||
newVmCreateNewVmOn: 'Create a new VM on {pool}',
|
||||
newVmCreateNewVmOn: 'Create a new VM on {select}',
|
||||
newVmCreateNewVmOn2: 'Create a new VM on {select1} or {select2}',
|
||||
newVmCreateNewVmNoPermission: 'You have no permission to create a VM',
|
||||
newVmInfoPanel: 'Infos',
|
||||
newVmNameLabel: 'Name',
|
||||
newVmTemplateLabel: 'Template',
|
||||
@@ -592,20 +648,24 @@ var messages = {
|
||||
newVmBootAfterCreate: 'Boot VM after creation',
|
||||
newVmMacPlaceholder: 'Auto-generated if empty',
|
||||
newVmCpuWeightLabel: 'CPU weight',
|
||||
newVmCpuWeightQuarter: 'Quarter (1/4)',
|
||||
newVmCpuWeightHalf: 'Half (1/2)',
|
||||
newVmCpuWeightNormal: 'Normal',
|
||||
newVmCpuWeightDouble: 'Double (x2)',
|
||||
newVmDefaultCpuWeight: 'Default: {value, number}',
|
||||
newVmCpuCapLabel: 'CPU cap',
|
||||
newVmDefaultCpuCap: 'Default: {value, number}',
|
||||
newVmCloudConfig: 'Cloud config',
|
||||
newVmCreateVms: 'Create VMs',
|
||||
newVmCreateVmsConfirm: 'Are you sure you want to create {nbVms} VMs?',
|
||||
newVmMultipleVms: 'Multiple VMs:',
|
||||
newVmSelectResourceSet: 'Select a resource set:',
|
||||
newVmMultipleVmsPattern: 'Name pattern:',
|
||||
newVmMultipleVmsPatternPlaceholder: 'e.g.: \\{name\\}_%',
|
||||
newVmFirstIndex: 'First index:',
|
||||
|
||||
// ----- Self -----
|
||||
resourceSets: 'Resource sets',
|
||||
noResourceSets: 'No resource sets.',
|
||||
resourceSetName: 'Resource set name',
|
||||
resourceSetCreation: 'Creation and edition',
|
||||
recomputeResourceSets: 'Recompute all limits',
|
||||
saveResourceSet: 'Save',
|
||||
resetResourceSet: 'Reset',
|
||||
editResourceSet: 'Edit',
|
||||
@@ -664,10 +724,26 @@ var messages = {
|
||||
importBackupMessage: 'Starting your backup import',
|
||||
|
||||
// ----- Modals -----
|
||||
emergencyShutdownHostsModalTitle: 'Emergency shutdown Host{nHosts, plural, one {} other {s}}',
|
||||
emergencyShutdownHostsModalMessage: 'Are you sure you want to shutdown {nHosts} Host{nHosts, plural, one {} other {s}}?',
|
||||
stopHostModalTitle: 'Shutdown host',
|
||||
stopHostModalMessage: 'This will shutdown your host. Do you want to continue?',
|
||||
restartHostModalTitle: 'Restart host',
|
||||
restartHostModalMessage: 'This will restart your host. Do you want to continue?',
|
||||
restartHostsAgentsModalTitle: 'Restart Host{nHosts, plural, one {} other {s}} agent{nHosts, plural, one {} other {s}}',
|
||||
restartHostsAgentsModalMessage: 'Are you sure you want to restart {nHosts} Host{nHosts, plural, one {} other {s}} agent{nHosts, plural, one {} other {s}}?',
|
||||
restartHostsModalTitle: 'Restart Host{nHosts, plural, one {} other {s}}',
|
||||
restartHostsModalMessage: 'Are you sure you want to restart {nHosts} Host{nHosts, plural, one {} other {s}}?',
|
||||
startVmsModalTitle: 'Start VM{vms, plural, one {} other {s}}',
|
||||
startVmsModalMessage: 'Are you sure you want to start {vms} VM{vms, plural, one {} other {s}}?',
|
||||
stopHostsModalTitle: 'Stop Host{nHosts, plural, one {} other {s}}',
|
||||
stopHostsModalMessage: 'Are you sure you want to stop {nHosts} Host{nHosts, plural, one {} other {s}}?',
|
||||
stopVmsModalTitle: 'Stop VM{vms, plural, one {} other {s}}',
|
||||
stopVmsModalMessage: 'Are you sure you want to stop {vms} VM{vms, plural, one {} other {s}}?',
|
||||
restartVmModalTitle: 'Restart VM',
|
||||
restartVmModalMessage: 'Are you sure you want to restart {name}?',
|
||||
stopVmModalTitle: 'Stop VM',
|
||||
stopVmModalMessage: 'Are you sure you want to stop {name}?',
|
||||
restartVmsModalTitle: 'Restart VM{vms, plural, one {} other {s}}',
|
||||
restartVmsModalMessage: 'Are you sure you want to restart {vms} VM{vms, plural, one {} other {s}}?',
|
||||
snapshotVmsModalTitle: 'Snapshot VM{vms, plural, one {} other {s}}',
|
||||
@@ -677,15 +753,20 @@ var messages = {
|
||||
deleteVmModalMessage: 'Are you sure you want to delete this VM? ALL VM DISKS WILL BE REMOVED',
|
||||
deleteVmsModalMessage: 'Are you sure you want to delete {vms} VM{vms, plural, one {} other {s}}? ALL VM DISKS WILL BE REMOVED',
|
||||
migrateVmModalTitle: 'Migrate VM',
|
||||
migrateVmModalBody: 'Are you sure you want to migrate this VM to {hostName}?',
|
||||
migrateVmAdvancedModalSelectHost: 'Select a destination host:',
|
||||
migrateVmAdvancedModalSelectNetwork: 'Select a migration network:',
|
||||
migrateVmAdvancedModalSelectSrs: 'For each VDI, select an SR:',
|
||||
migrateVmAdvancedModalSelectNetworks: 'For each VIF, select a network:',
|
||||
migrateVmAdvancedModalName: 'Name',
|
||||
migrateVmAdvancedModalSr: 'SR',
|
||||
migrateVmAdvancedModalVif: 'VIF',
|
||||
migrateVmAdvancedModalNetwork: 'Network',
|
||||
migrateVmSelectHost: 'Select a destination host:',
|
||||
migrateVmSelectMigrationNetwork: 'Select a migration network:',
|
||||
migrateVmSelectSrs: 'For each VDI, select an SR:',
|
||||
migrateVmSelectNetworks: 'For each VIF, select a network:',
|
||||
migrateVmsSelectSr: 'Select a destination SR:',
|
||||
migrateVmsSelectSrIntraPool: 'Select a destination SR for local disks:',
|
||||
migrateVmsSelectNetwork: 'Select a network on which to connect each VIF:',
|
||||
migrateVmsSmartMapping: 'Smart mapping',
|
||||
migrateVmName: 'Name',
|
||||
migrateVmSr: 'SR',
|
||||
migrateVmVif: 'VIF',
|
||||
migrateVmNetwork: 'Network',
|
||||
migrateVmNoTargetHost: 'No target host',
|
||||
migrateVmNoTargetHostMessage: 'A target host is required to migrate a VM',
|
||||
importBackupModalTitle: 'Import a {name} Backup',
|
||||
importBackupModalStart: 'Start VM after restore',
|
||||
importBackupModalSelectBackup: 'Select your backup…',
|
||||
@@ -713,9 +794,13 @@ var messages = {
|
||||
copyVm: 'Copy VM',
|
||||
copyVmConfirm: 'Are you sure you want to copy this VM to {SR}?',
|
||||
copyVmName: 'Name',
|
||||
copyVmNamePattern: 'Name pattern',
|
||||
copyVmNamePlaceholder: 'If empty: name of the copied VM',
|
||||
copyVmNamePatternPlaceholder: 'e.g.: "\\{name\\}_COPY"',
|
||||
copyVmSelectSr: 'Select SR',
|
||||
copyVmCompress: 'Use compression',
|
||||
copyVmsNoTargetSr: 'No target SR',
|
||||
copyVmsNoTargetSrMessage: 'A target SR is required to copy a VM',
|
||||
|
||||
// ----- Network -----
|
||||
newNetworkCreate: 'Create network',
|
||||
@@ -759,6 +844,7 @@ var messages = {
|
||||
availableIn: 'This feature is available starting from {plan} Edition',
|
||||
|
||||
// ----- Updates View -----
|
||||
updateTitle: 'Updates',
|
||||
registration: 'Registration',
|
||||
trial: 'Trial',
|
||||
settings: 'Settings',
|
||||
@@ -780,6 +866,8 @@ var messages = {
|
||||
mustUpgrade: 'You need to update your XOA (new version is available)',
|
||||
registerNeeded: 'Your XOA is not registered for updates',
|
||||
updaterError: 'Can\'t fetch update information',
|
||||
promptUpgradeReloadTitle: 'Upgrade successful',
|
||||
promptUpgradeReloadMessage: 'Your XOA has successfully upgraded, and your browser must reload the application. Do you want to reload now ?',
|
||||
|
||||
// ----- OS Disclaimer -----
|
||||
disclaimerTitle: 'Xen Orchestra from the sources',
|
||||
@@ -793,7 +881,36 @@ var messages = {
|
||||
disconnectPif: 'Disconnect PIF',
|
||||
disconnectPifConfirm: 'Are you sure you want to disconnect this PIF?',
|
||||
deletePif: 'Delete PIF',
|
||||
deletePifConfirm: 'Are you sure you want to delete this PIF?'
|
||||
deletePifConfirm: 'Are you sure you want to delete this PIF?',
|
||||
|
||||
// ----- User -----
|
||||
username: 'Username',
|
||||
password: 'Password',
|
||||
language: 'Language',
|
||||
oldPasswordPlaceholder: 'Old password',
|
||||
newPasswordPlaceholder: 'New password',
|
||||
confirmPasswordPlaceholder: 'Confirm new password',
|
||||
confirmationPasswordError: 'Confirmation password incorrect',
|
||||
confirmationPasswordErrorBody: 'Password does not match the confirm password.',
|
||||
pwdChangeSuccess: 'Password changed',
|
||||
pwdChangeSuccessBody: 'Your password has been successfully changed.',
|
||||
pwdChangeError: 'Incorrect password',
|
||||
pwdChangeErrorBody: 'The old password provided is incorrect. Your password has not been changed.',
|
||||
changePasswordOk: 'OK',
|
||||
sshKeys: 'SSH keys',
|
||||
newSshKey: 'New SSH key',
|
||||
deleteSshKey: 'Delete',
|
||||
noSshKeys: 'No SSH keys',
|
||||
newSshKeyModalTitle: 'New SSH key',
|
||||
sshKeyErrorTitle: 'Invalid key',
|
||||
sshKeyErrorMessage: 'An SSH key requires both a title and a key.',
|
||||
title: 'Title',
|
||||
key: 'Key',
|
||||
deleteSshKeyConfirm: 'Delete SSH key',
|
||||
deleteSshKeyConfirmMessage: 'Are you sure you want to delete the SSH key {title}?',
|
||||
|
||||
// ----- Usage -----
|
||||
others: 'Others'
|
||||
}
|
||||
forEach(messages, function (message, id) {
|
||||
if (isString(message)) {
|
||||
|
||||
@@ -2,6 +2,8 @@ import React from 'react'
|
||||
|
||||
import ActionButton from './action-button'
|
||||
import Component from './base-component'
|
||||
import propTypes from './prop-types'
|
||||
import { connectStore } from './utils'
|
||||
import { SelectVdi } from './select-objects'
|
||||
import {
|
||||
createGetObjectsOfType,
|
||||
@@ -9,10 +11,6 @@ import {
|
||||
createGetObject,
|
||||
createSelector
|
||||
} from './selectors'
|
||||
import {
|
||||
connectStore,
|
||||
propTypes
|
||||
} from './utils'
|
||||
import {
|
||||
ejectCd,
|
||||
insertCd
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Component } from 'react'
|
||||
import {
|
||||
propTypes
|
||||
} from 'utils'
|
||||
|
||||
import propTypes from '../prop-types'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import _ from 'intl'
|
||||
import React, { Component, cloneElement } from 'react'
|
||||
import map from 'lodash/map'
|
||||
import filter from 'lodash/filter'
|
||||
|
||||
import {
|
||||
autobind,
|
||||
propsEqual,
|
||||
propTypes
|
||||
} from 'utils'
|
||||
import _ from '../intl'
|
||||
import propTypes from '../prop-types'
|
||||
import { propsEqual } from '../utils'
|
||||
|
||||
import GenericInput from './generic-input'
|
||||
|
||||
import {
|
||||
descriptionRender,
|
||||
forceDisplayOptionalAttr
|
||||
@@ -76,15 +72,13 @@ export default class ArrayInput extends Component {
|
||||
})
|
||||
}
|
||||
|
||||
@autobind
|
||||
_handleOptionalChange (event) {
|
||||
_handleOptionalChange = event => {
|
||||
this.setState({
|
||||
use: event.target.checked
|
||||
})
|
||||
}
|
||||
|
||||
@autobind
|
||||
_handleAdd () {
|
||||
_handleAdd = () => {
|
||||
const { children } = this.state
|
||||
this.setState({
|
||||
children: children.concat(this._makeChild(this.props))
|
||||
|
||||
@@ -23,7 +23,7 @@ export default class BooleanInput extends AbstractInput {
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<div className='checkbox form-control'>
|
||||
<Toggle
|
||||
defaultValue={props.defaultValue || props.schema.default}
|
||||
defaultValue={props.defaultValue}
|
||||
disabled={props.disabled}
|
||||
onChange={props.onChange}
|
||||
ref='input'
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import React, { Component } from 'react'
|
||||
import includes from 'lodash/includes'
|
||||
|
||||
import {
|
||||
EMPTY_OBJECT,
|
||||
propTypes
|
||||
} from 'utils'
|
||||
import propTypes from '../prop-types'
|
||||
import { EMPTY_OBJECT } from '../utils'
|
||||
|
||||
import ArrayInput from './array-input'
|
||||
import BooleanInput from './boolean-input'
|
||||
@@ -13,55 +10,18 @@ import IntegerInput from './integer-input'
|
||||
import NumberInput from './number-input'
|
||||
import ObjectInput from './object-input'
|
||||
import StringInput from './string-input'
|
||||
import XoHighLevelObjectInput from './xo-highlevel-object-input'
|
||||
import XoHostInput from './xo-host-input'
|
||||
import XoPoolInput from './xo-pool-input'
|
||||
import XoRemoteInput from './xo-remote-input'
|
||||
import XoRoleInput from './xo-role-input'
|
||||
import XoSrInput from './xo-sr-input'
|
||||
import XoSubjectInput from './xo-subject-input'
|
||||
import XoVmInput from './xo-vm-input'
|
||||
|
||||
import { getType } from './helpers'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const getType = (schema, attr = 'type') => {
|
||||
if (!schema) {
|
||||
return
|
||||
}
|
||||
|
||||
const type = schema[attr]
|
||||
|
||||
if (Array.isArray(type)) {
|
||||
if (includes(type, 'integer')) {
|
||||
return 'integer'
|
||||
}
|
||||
if (includes(type, 'number')) {
|
||||
return 'number'
|
||||
}
|
||||
|
||||
return 'string'
|
||||
}
|
||||
|
||||
return type
|
||||
}
|
||||
|
||||
const getXoType = schema => getType(schema, 'xo:type')
|
||||
|
||||
const InputByType = {
|
||||
array: ArrayInput,
|
||||
boolean: BooleanInput,
|
||||
host: XoHostInput,
|
||||
integer: IntegerInput,
|
||||
number: NumberInput,
|
||||
object: ObjectInput,
|
||||
pool: XoPoolInput,
|
||||
remote: XoRemoteInput,
|
||||
sr: XoSrInput,
|
||||
string: StringInput,
|
||||
vm: XoVmInput,
|
||||
xoobject: XoHighLevelObjectInput,
|
||||
role: XoRoleInput,
|
||||
subject: XoSubjectInput
|
||||
string: StringInput
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
@@ -88,12 +48,14 @@ export default class GenericInput extends Component {
|
||||
render () {
|
||||
const {
|
||||
schema,
|
||||
defaultValue = schema.default,
|
||||
uiSchema = EMPTY_OBJECT,
|
||||
...opts
|
||||
} = this.props
|
||||
|
||||
const props = {
|
||||
...opts,
|
||||
defaultValue,
|
||||
schema,
|
||||
uiSchema,
|
||||
ref: 'input'
|
||||
@@ -104,14 +66,13 @@ export default class GenericInput extends Component {
|
||||
return <EnumInput {...props} />
|
||||
}
|
||||
|
||||
// $type = Job Creation Schemas && Old XO plugins.
|
||||
const type = getXoType(schema) || getType(schema, '$type') || getType(schema)
|
||||
const type = getType(schema)
|
||||
const Input = uiSchema.widget || InputByType[type.toLowerCase()]
|
||||
|
||||
if (!Input) {
|
||||
throw new Error(`Unsupported type: ${type}.`)
|
||||
}
|
||||
|
||||
return <Input {...props} />
|
||||
return <Input {...props} {...uiSchema.config} />
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,43 @@
|
||||
import React from 'react'
|
||||
import includes from 'lodash/includes'
|
||||
import isArray from 'lodash/isArray'
|
||||
import marked from 'marked'
|
||||
|
||||
import { Col, Row } from 'grid'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const getType = schema => {
|
||||
if (!schema) {
|
||||
return
|
||||
}
|
||||
|
||||
const type = schema.type
|
||||
|
||||
if (isArray(type)) {
|
||||
if (includes(type, 'integer')) {
|
||||
return 'integer'
|
||||
}
|
||||
if (includes(type, 'number')) {
|
||||
return 'number'
|
||||
}
|
||||
|
||||
return 'string'
|
||||
}
|
||||
|
||||
return type
|
||||
}
|
||||
|
||||
export const getXoType = schema => {
|
||||
const type = schema && (schema['xo:type'] || schema.$type)
|
||||
|
||||
if (type) {
|
||||
return type.toLowerCase()
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const descriptionRender = description =>
|
||||
<span className='text-muted' dangerouslySetInnerHTML={{__html: marked(description || '')}} />
|
||||
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
import GenericInput from './generic-input'
|
||||
export default GenericInput
|
||||
export default from './generic-input'
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react'
|
||||
|
||||
import AbstractInput from './abstract-input'
|
||||
import Combobox from '../combobox'
|
||||
import { PrimitiveInputWrapper } from './helpers'
|
||||
|
||||
// ===================================================================
|
||||
@@ -20,16 +21,16 @@ export default class IntegerInput extends AbstractInput {
|
||||
|
||||
render () {
|
||||
const { props } = this
|
||||
const { onChange } = props
|
||||
const { schema } = props
|
||||
|
||||
return (
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<input
|
||||
className='form-control'
|
||||
defaultValue={props.defaultValue || ''}
|
||||
<Combobox
|
||||
defaultValue={props.defaultValue}
|
||||
disabled={props.disabled}
|
||||
onChange={onChange && (event => onChange(event.target.value))}
|
||||
placeholder={props.placeholder}
|
||||
onChange={props.onChange}
|
||||
options={schema.defaults}
|
||||
placeholder={props.placeholder || schema.default}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
step={1}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react'
|
||||
|
||||
import AbstractInput from './abstract-input'
|
||||
import Combobox from '../combobox'
|
||||
import { PrimitiveInputWrapper } from './helpers'
|
||||
|
||||
// ===================================================================
|
||||
@@ -20,16 +21,16 @@ export default class NumberInput extends AbstractInput {
|
||||
|
||||
render () {
|
||||
const { props } = this
|
||||
const { onChange } = props
|
||||
const { schema } = props
|
||||
|
||||
return (
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<input
|
||||
className='form-control'
|
||||
defaultValue={props.defaultValue || ''}
|
||||
<Combobox
|
||||
defaultValue={props.defaultValue}
|
||||
disabled={props.disabled}
|
||||
onChange={onChange && (event => onChange(event.target.value))}
|
||||
placeholder={props.placeholder}
|
||||
onChange={props.onChange}
|
||||
options={schema.defaults}
|
||||
placeholder={props.placeholder || schema.default}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
step='any'
|
||||
|
||||
@@ -4,11 +4,8 @@ import forEach from 'lodash/forEach'
|
||||
import includes from 'lodash/includes'
|
||||
import map from 'lodash/map'
|
||||
|
||||
import {
|
||||
autobind,
|
||||
propsEqual,
|
||||
propTypes
|
||||
} from 'utils'
|
||||
import propTypes from '../prop-types'
|
||||
import { propsEqual } from '../utils'
|
||||
|
||||
import GenericInput from './generic-input'
|
||||
|
||||
@@ -81,8 +78,7 @@ export default class ObjectInput extends Component {
|
||||
})
|
||||
}
|
||||
|
||||
@autobind
|
||||
_handleOptionalChange (event) {
|
||||
_handleOptionalChange = event => {
|
||||
const { checked } = event.target
|
||||
|
||||
this.setState({
|
||||
@@ -98,6 +94,7 @@ export default class ObjectInput extends Component {
|
||||
defaultValue = {}
|
||||
} = props
|
||||
const obj = {}
|
||||
const { properties } = uiSchema
|
||||
|
||||
forEach(schema.properties, (childSchema, key) => {
|
||||
obj[key] = (
|
||||
@@ -108,7 +105,7 @@ export default class ObjectInput extends Component {
|
||||
label={childSchema.title || key}
|
||||
required={includes(schema.required, key)}
|
||||
schema={childSchema}
|
||||
uiSchema={uiSchema.properties}
|
||||
uiSchema={properties && properties[key]}
|
||||
defaultValue={defaultValue[key]}
|
||||
/>
|
||||
</ObjectItem>
|
||||
|
||||
@@ -1,26 +1,31 @@
|
||||
import React from 'react'
|
||||
|
||||
import AbstractInput from './abstract-input'
|
||||
import Combobox from '../combobox'
|
||||
import propTypes from '../prop-types'
|
||||
import { PrimitiveInputWrapper } from './helpers'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
password: propTypes.bool
|
||||
})
|
||||
export default class StringInput extends AbstractInput {
|
||||
render () {
|
||||
const { props } = this
|
||||
const { onChange } = props
|
||||
const { schema } = props
|
||||
|
||||
return (
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<input
|
||||
className='form-control'
|
||||
defaultValue={props.defaultValue || ''}
|
||||
<Combobox
|
||||
defaultValue={props.defaultValue}
|
||||
disabled={props.disabled}
|
||||
onChange={onChange && (event => onChange(event.target.value))}
|
||||
placeholder={props.placeholder}
|
||||
onChange={props.onChange}
|
||||
options={schema.defaults}
|
||||
placeholder={props.placeholder || schema.default}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
type={props.schema['xo:type'] === 'password' ? 'password' : 'text'}
|
||||
type={props.password && 'password'}
|
||||
/>
|
||||
</PrimitiveInputWrapper>
|
||||
)
|
||||
|
||||
59
src/common/link.js
Normal file
59
src/common/link.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import Link from 'react-router/lib/Link'
|
||||
import React from 'react'
|
||||
import { routerShape } from 'react-router/lib/PropTypes'
|
||||
|
||||
import Component from './base-component'
|
||||
import propTypes from './prop-types'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export { Link as default }
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const _IGNORED_TAGNAMES = {
|
||||
A: true,
|
||||
BUTTON: true,
|
||||
INPUT: true,
|
||||
SELECT: true
|
||||
}
|
||||
|
||||
@propTypes({
|
||||
tagName: propTypes.string
|
||||
})
|
||||
export class BlockLink extends Component {
|
||||
static contextTypes = {
|
||||
router: routerShape
|
||||
}
|
||||
|
||||
_style = { cursor: 'pointer' }
|
||||
_onClickCapture = event => {
|
||||
const { currentTarget } = event
|
||||
let element = event.target
|
||||
while (element !== currentTarget) {
|
||||
if (_IGNORED_TAGNAMES[element.tagName]) {
|
||||
return
|
||||
}
|
||||
element = element.parentNode
|
||||
}
|
||||
event.stopPropagation()
|
||||
if (event.ctrlKey || event.button === 1) {
|
||||
window.open(this.context.router.createHref(this.props.to))
|
||||
} else {
|
||||
this.context.router.push(this.props.to)
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { children, tagName = 'div' } = this.props
|
||||
const Component = tagName
|
||||
return (
|
||||
<Component
|
||||
style={this._style}
|
||||
onClickCapture={this._onClickCapture}
|
||||
>
|
||||
{children}
|
||||
</Component>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,8 @@ 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 { propTypes } from './utils'
|
||||
|
||||
import propTypes from './prop-types'
|
||||
|
||||
let instance
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import classNames from 'classnames'
|
||||
import Link from 'react-router/lib/Link'
|
||||
import React from 'react'
|
||||
|
||||
import Link from './link'
|
||||
|
||||
export const NavLink = ({ children, to }) => (
|
||||
<li className='nav-item' role='tab'>
|
||||
<Link className='nav-link' activeClassName='active' to={to}>
|
||||
|
||||
3
src/common/react-novnc.js
vendored
3
src/common/react-novnc.js
vendored
@@ -1,6 +1,5 @@
|
||||
import React, { Component } from 'react'
|
||||
import { createBackoff } from 'jsonrpc-websocket-client'
|
||||
import { propTypes } from 'utils'
|
||||
import { RFB } from 'novnc-node'
|
||||
import {
|
||||
format as formatUrl,
|
||||
@@ -8,6 +7,8 @@ import {
|
||||
resolve as resolveUrl
|
||||
} from 'url'
|
||||
|
||||
import propTypes from './prop-types'
|
||||
|
||||
const parseRelativeUrl = url => parseUrl(resolveUrl(String(window.location), url))
|
||||
|
||||
const PROTOCOL_ALIASES = {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import Icon from 'icon'
|
||||
import React, { Component } from 'react'
|
||||
import { createGetObject } from 'selectors'
|
||||
|
||||
import Icon from './icon'
|
||||
import propTypes from './prop-types'
|
||||
import { createGetObject } from './selectors'
|
||||
import { isSrWritable } from './xo'
|
||||
import {
|
||||
connectStore,
|
||||
formatSize,
|
||||
propTypes
|
||||
} from 'utils'
|
||||
formatSize
|
||||
} from './utils'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -53,7 +54,7 @@ export const SrItem = propTypes({
|
||||
})(({ sr, container }) => {
|
||||
let label = `${sr.name_label || sr.id}`
|
||||
|
||||
if (sr.content_type === 'user') {
|
||||
if (isSrWritable(sr)) {
|
||||
label += ` (${formatSize(sr.size)})`
|
||||
}
|
||||
|
||||
@@ -107,6 +108,11 @@ const xoItemToRender = {
|
||||
<Icon icon='user' /> {user.email}
|
||||
</span>
|
||||
),
|
||||
resourceSet: resourceSet => (
|
||||
<span>
|
||||
<Icon icon='resource-set' /> {resourceSet.name}
|
||||
</span>
|
||||
),
|
||||
|
||||
// XO objects.
|
||||
pool: pool => (
|
||||
@@ -132,6 +138,13 @@ const xoItemToRender = {
|
||||
// VM.
|
||||
VM: vm => <VmItem vm={vm} />,
|
||||
'VM-snapshot': vm => <VmItem vm={vm} />,
|
||||
'VM-controller': vm => (
|
||||
<span>
|
||||
<Icon icon='host' />
|
||||
{' '}
|
||||
<VmItem vm={vm} />
|
||||
</span>
|
||||
),
|
||||
|
||||
// PIF.
|
||||
PIF: pif => (
|
||||
|
||||
@@ -1,30 +1,32 @@
|
||||
import Component from 'base-component'
|
||||
import React from 'react'
|
||||
import _ from 'intl'
|
||||
import forEach from 'lodash/forEach'
|
||||
import includes from 'lodash/includes'
|
||||
import join from 'lodash/join'
|
||||
import later from 'later'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import sortedIndex from 'lodash/sortedIndex'
|
||||
import { Range } from 'form'
|
||||
import { FormattedTime } from 'react-intl'
|
||||
|
||||
import { Col, Row } from 'grid'
|
||||
import {
|
||||
Panel,
|
||||
Tab,
|
||||
Tabs
|
||||
} from 'react-bootstrap-4/lib'
|
||||
import {
|
||||
propTypes
|
||||
} from 'utils'
|
||||
|
||||
import _ from './intl'
|
||||
import Component from './base-component'
|
||||
import propTypes from './prop-types'
|
||||
import TimezonePicker from './timezone-picker'
|
||||
import { Card, CardHeader, CardBlock } from './card'
|
||||
import { Col, Row } from './grid'
|
||||
import { Range } from './form'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const NAV_EVERY = 1
|
||||
const NAV_EACH_SELECTED = 2
|
||||
const NAV_EVERY_N = 3
|
||||
// By default later use UTC but we use this line for futures versions.
|
||||
later.date.UTC()
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const NAV_EACH_SELECTED = 1
|
||||
const NAV_EVERY_N = 2
|
||||
|
||||
const MIN_PREVIEWS = 5
|
||||
const MAX_PREVIEWS = 20
|
||||
@@ -80,13 +82,29 @@ const MINS = (() => {
|
||||
return minutes
|
||||
})()
|
||||
|
||||
const PICKTIME_TO_ID = {
|
||||
minute: 0,
|
||||
hour: 1,
|
||||
monthDay: 2,
|
||||
month: 3,
|
||||
weekDay: 4
|
||||
}
|
||||
|
||||
const TIME_FORMAT = {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric'
|
||||
minute: 'numeric',
|
||||
|
||||
// The timezone is not significant for displaying the date previews
|
||||
// as long as it is the same used to generate the next occurrences
|
||||
// from the cron patterns.
|
||||
|
||||
// Therefore we can use UTC everywhere and say to the user that the
|
||||
// previews are in the configured timezone.
|
||||
timeZone: 'UTC'
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
@@ -103,7 +121,7 @@ const getDayName = (dayNum) =>
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
cron: propTypes.string.isRequired
|
||||
cronPattern: propTypes.string.isRequired
|
||||
})
|
||||
export class SchedulePreview extends Component {
|
||||
_handleChange = value => {
|
||||
@@ -113,12 +131,15 @@ export class SchedulePreview extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { props } = this
|
||||
const cronSched = later.parse.cron(props.cron)
|
||||
const { cronPattern } = this.props
|
||||
const cronSched = later.parse.cron(cronPattern)
|
||||
const dates = later.schedule(cronSched).next(this.state.value || MIN_PREVIEWS)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='alert alert-info' role='alert'>
|
||||
{_('cronPattern')} <strong>{cronPattern}</strong>
|
||||
</div>
|
||||
<div className='form-inline p-b-1'>
|
||||
<Range min={MIN_PREVIEWS} max={MAX_PREVIEWS} onChange={this._handleChange} />
|
||||
</div>
|
||||
@@ -139,34 +160,21 @@ export class SchedulePreview extends Component {
|
||||
|
||||
@propTypes({
|
||||
children: propTypes.any.isRequired,
|
||||
onChange: propTypes.func
|
||||
onChange: propTypes.func.isRequired,
|
||||
tdId: propTypes.number.isRequired,
|
||||
value: propTypes.bool.isRequired
|
||||
})
|
||||
class ToggleTd extends Component {
|
||||
get value () {
|
||||
return this.state.value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
const { onChange } = this.props
|
||||
|
||||
this.setState({
|
||||
value
|
||||
}, onChange && (() => onChange(value)))
|
||||
}
|
||||
|
||||
_onClick = () => {
|
||||
const { onChange } = this.props
|
||||
const value = !this.state.value
|
||||
|
||||
this.setState({
|
||||
value
|
||||
}, onChange && (() => onChange(value)))
|
||||
const { props } = this
|
||||
props.onChange(props.tdId, !props.value)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { props } = this
|
||||
return (
|
||||
<td style={{ cursor: 'pointer' }} className={this.state.value ? 'table-success' : ''} onClick={this._onClick}>
|
||||
{this.props.children}
|
||||
<td style={{ cursor: 'pointer' }} className={props.value ? 'table-success' : ''} onClick={this._onClick}>
|
||||
{props.children}
|
||||
</td>
|
||||
)
|
||||
}
|
||||
@@ -175,79 +183,64 @@ class ToggleTd extends Component {
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
data: propTypes.array.isRequired,
|
||||
dataRender: propTypes.func,
|
||||
onChange: propTypes.func
|
||||
options: propTypes.array.isRequired,
|
||||
optionsRenderer: propTypes.func,
|
||||
onChange: propTypes.func.isRequired,
|
||||
value: propTypes.array.isRequired
|
||||
})
|
||||
class TableSelect extends Component {
|
||||
constructor () {
|
||||
super()
|
||||
this.state = {
|
||||
value: []
|
||||
}
|
||||
}
|
||||
|
||||
get value () {
|
||||
return this.state.value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
const { onChange } = this.props
|
||||
|
||||
forEach(this.refs, (ref, id) => {
|
||||
// Don't call ref.input directly because onChange of each ToggleTd is called else!
|
||||
ref.setState({
|
||||
value: includes(value, +id)
|
||||
})
|
||||
})
|
||||
|
||||
this.setState({
|
||||
value
|
||||
}, onChange && (() => onChange(value)))
|
||||
static defaultProps = {
|
||||
optionsRenderer: value => value
|
||||
}
|
||||
|
||||
_reset = () => {
|
||||
this.value = []
|
||||
this.props.onChange([])
|
||||
}
|
||||
|
||||
_handleChange = (id, value) => {
|
||||
const { onChange } = this.props
|
||||
const newValue = this.state.value.slice()
|
||||
_handleChange = (tdId, tdValue) => {
|
||||
const { props } = this
|
||||
|
||||
if (value) {
|
||||
newValue.splice(sortedIndex(newValue, id), 0, id)
|
||||
const newValue = props.value.slice()
|
||||
const index = sortedIndex(newValue, tdId)
|
||||
|
||||
if (tdValue) {
|
||||
// Add
|
||||
if (newValue[index] !== tdId) {
|
||||
newValue.splice(index, 0, tdId)
|
||||
}
|
||||
} else {
|
||||
newValue.splice(sortedIndex(newValue, id), 1)
|
||||
// Remove
|
||||
if (newValue[index] === tdId) {
|
||||
newValue.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
value: newValue
|
||||
}, onChange && (() => onChange(newValue)))
|
||||
props.onChange(newValue)
|
||||
}
|
||||
|
||||
render () {
|
||||
const dataRender = this.props.dataRender || ((value) => value)
|
||||
const {
|
||||
props: {
|
||||
data
|
||||
}
|
||||
} = this
|
||||
const { length } = data[0]
|
||||
options,
|
||||
optionsRenderer,
|
||||
value
|
||||
} = this.props
|
||||
const { length } = options[0]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<table className='table table-bordered table-sm'>
|
||||
<tbody>
|
||||
{map(data, (line, i) => (
|
||||
{map(options, (line, i) => (
|
||||
<tr key={i}>
|
||||
{map(line, (value, j) => {
|
||||
const id = length * i + j
|
||||
{map(line, (tdOption, j) => {
|
||||
const tdId = length * i + j
|
||||
return (
|
||||
<ToggleTd
|
||||
key={id}
|
||||
ref={id}
|
||||
children={dataRender(value)}
|
||||
onChange={(value) => { this._handleChange(id, value) }}
|
||||
children={optionsRenderer(tdOption)}
|
||||
tdId={tdId}
|
||||
key={tdId}
|
||||
onChange={this._handleChange}
|
||||
value={includes(value, tdId)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
@@ -266,177 +259,148 @@ class TableSelect extends Component {
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
dataRender: propTypes.func,
|
||||
onChange: propTypes.func,
|
||||
optionsRenderer: propTypes.func,
|
||||
onChange: propTypes.func.isRequired,
|
||||
range: propTypes.array,
|
||||
type: propTypes.string.isRequired
|
||||
labelId: propTypes.string.isRequired,
|
||||
value: propTypes.any.isRequired
|
||||
})
|
||||
class TimePicker extends Component {
|
||||
constructor () {
|
||||
super()
|
||||
this.state = {
|
||||
activeKey: NAV_EVERY
|
||||
activeKey: NAV_EACH_SELECTED,
|
||||
tableValue: []
|
||||
}
|
||||
}
|
||||
|
||||
get value () {
|
||||
const { activeKey } = this.state
|
||||
_update (props) {
|
||||
const { refs } = this
|
||||
const { value } = props
|
||||
|
||||
if (activeKey === NAV_EVERY) {
|
||||
return 'all'
|
||||
}
|
||||
|
||||
if (activeKey === NAV_EACH_SELECTED) {
|
||||
return refs.select.value
|
||||
}
|
||||
|
||||
return refs.range.value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
const { refs } = this
|
||||
const { onChange } = this.props
|
||||
|
||||
if (value === 'all') {
|
||||
this.setState({
|
||||
activeKey: NAV_EVERY
|
||||
}, onChange && (() => onChange(value)))
|
||||
} else if (Array.isArray(value)) {
|
||||
this.setState({
|
||||
activeKey: NAV_EACH_SELECTED
|
||||
})
|
||||
refs.select.value = value
|
||||
} else {
|
||||
if (value.indexOf('/') === 1) {
|
||||
this.setState({
|
||||
activeKey: NAV_EVERY_N
|
||||
})
|
||||
refs.range.value = value
|
||||
refs.range.value = value.split('/')[1]
|
||||
} else {
|
||||
this.setState({
|
||||
activeKey: NAV_EACH_SELECTED,
|
||||
tableValue: value === '*'
|
||||
? []
|
||||
: map(value.split(','), e => +e)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
_updateOpen = () => {
|
||||
this.setState({
|
||||
open: !this.state.open
|
||||
})
|
||||
componentWillMount () {
|
||||
this._update(this.props)
|
||||
}
|
||||
|
||||
componentWillReceiveProps (props) {
|
||||
this._update(props)
|
||||
}
|
||||
|
||||
_selectTab = activeKey => {
|
||||
const { onChange } = this.props
|
||||
|
||||
this.setState({
|
||||
activeKey
|
||||
}, onChange && (() => onChange(this.value)))
|
||||
}, () => {
|
||||
const { activeKey, tableValue } = this.state
|
||||
const { onChange } = this.props
|
||||
const { refs } = this
|
||||
|
||||
if (activeKey === NAV_EACH_SELECTED) {
|
||||
onChange(tableValue)
|
||||
} else {
|
||||
onChange(refs.range.value)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
_handleTableValue = tableValue => {
|
||||
this.setState({
|
||||
tableValue
|
||||
}, () => this.props.onChange(tableValue))
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
props,
|
||||
state
|
||||
} = this
|
||||
|
||||
const {
|
||||
onChange,
|
||||
options,
|
||||
optionsRenderer,
|
||||
range,
|
||||
type
|
||||
} = props
|
||||
labelId
|
||||
} = this.props
|
||||
const { tableValue } = this.state
|
||||
|
||||
const tableSelect = (
|
||||
<TableSelect
|
||||
onChange={this._handleTableValue}
|
||||
options={options}
|
||||
optionsRenderer={optionsRenderer}
|
||||
value={tableValue}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className='card'>
|
||||
<button className='card-header btn btn-lg btn-block' onClick={this._updateOpen}>
|
||||
{_(`scheduling${type}`)}
|
||||
</button>
|
||||
<Panel collapsible expanded={state.open}>
|
||||
<div className='card-block'>
|
||||
<Tabs bsStyle='tabs' activeKey={state.activeKey} onSelect={this._selectTab}>
|
||||
<Tab tabClassName='nav-item' eventKey={NAV_EVERY} title={_(`schedulingEvery${type}`)} />
|
||||
<Tab tabClassName='nav-item' eventKey={NAV_EACH_SELECTED} title={_(`schedulingEachSelected${type}`)}>
|
||||
<TableSelect ref='select' data={props.data} dataRender={props.dataRender} onChange={onChange} />
|
||||
<Card>
|
||||
<CardHeader>
|
||||
{_(`scheduling${labelId}`)}
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
{range
|
||||
? (
|
||||
<Tabs bsStyle='tabs' activeKey={this.state.activeKey} onSelect={this._selectTab}>
|
||||
<Tab tabClassName='nav-item' eventKey={NAV_EACH_SELECTED} title={_(`schedulingEachSelected${labelId}`)}>
|
||||
{tableSelect}
|
||||
</Tab>
|
||||
<Tab tabClassName='nav-item' eventKey={NAV_EVERY_N} title={_(`schedulingEveryN${labelId}`)}>
|
||||
<Range ref='range' min={range[0]} max={range[1]} onChange={onChange} />
|
||||
</Tab>
|
||||
{range &&
|
||||
<Tab tabClassName='nav-item' eventKey={NAV_EVERY_N} title={_(`schedulingEveryN${type}`)}>
|
||||
<Range ref='range' min={range[0]} max={range[1]} onChange={onChange} />
|
||||
</Tab>}
|
||||
</Tabs>
|
||||
</div>
|
||||
</Panel>
|
||||
</div>
|
||||
) : tableSelect
|
||||
}
|
||||
</CardBlock>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const ID_TO_PICKTIME = [
|
||||
'minute',
|
||||
'hour',
|
||||
'monthDay',
|
||||
'month',
|
||||
'weekDay'
|
||||
]
|
||||
const HOURS_RANGE = [2, 12]
|
||||
const MINUTES_RANGE = [2, 30]
|
||||
|
||||
@propTypes({
|
||||
onChange: propTypes.func
|
||||
cronPattern: propTypes.string.isRequired,
|
||||
onChange: propTypes.func,
|
||||
timezone: propTypes.string
|
||||
})
|
||||
export default class Scheduler extends Component {
|
||||
constructor () {
|
||||
super()
|
||||
this.cron = {
|
||||
minute: '*',
|
||||
hour: '*',
|
||||
monthDay: '*',
|
||||
month: '*',
|
||||
weekDay: '*'
|
||||
}
|
||||
}
|
||||
|
||||
get value () {
|
||||
const { cron } = this
|
||||
return `${cron.minute} ${cron.hour} ${cron.monthDay} ${cron.month} ${cron.weekDay}`
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
if (!value) {
|
||||
value = '* * * * *'
|
||||
}
|
||||
|
||||
forEach(value.split(' '), (t, id) => {
|
||||
const ref = this.refs[ID_TO_PICKTIME[id]]
|
||||
|
||||
if (t === '*') {
|
||||
ref.value = 'all'
|
||||
} else if (t.indexOf('/') === 1) {
|
||||
ref.value = t.split('/')[1]
|
||||
} else {
|
||||
ref.value = map(t.split(','), e => +e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
_update (type, value) {
|
||||
const { cron } = this
|
||||
const { onChange } = this.props
|
||||
|
||||
if (value === 'all') {
|
||||
cron[type] = '*'
|
||||
} else if (Array.isArray(value)) {
|
||||
if (Array.isArray(value)) {
|
||||
if (!value.length) {
|
||||
cron[type] = '*'
|
||||
value = '*'
|
||||
} else {
|
||||
cron[type] = join(
|
||||
value = join(
|
||||
(type === 'monthDay' || type === 'month')
|
||||
? map(value, (n) => n + 1)
|
||||
? map(value, n => n + 1)
|
||||
: value,
|
||||
','
|
||||
)
|
||||
}
|
||||
} else {
|
||||
cron[type] = `*/${value}`
|
||||
value = `*/${value}`
|
||||
}
|
||||
|
||||
if (onChange) {
|
||||
onChange(this.value)
|
||||
}
|
||||
const { props } = this
|
||||
const cronPattern = props.cronPattern.split(' ')
|
||||
cronPattern[PICKTIME_TO_ID[type]] = value
|
||||
|
||||
this.props.onChange({
|
||||
cronPattern: cronPattern.join(' '),
|
||||
timezone: props.timezone
|
||||
})
|
||||
}
|
||||
|
||||
_onHourChange = value => this._update('hour', value)
|
||||
@@ -445,49 +409,69 @@ export default class Scheduler extends Component {
|
||||
_onMonthDayChange = value => this._update('monthDay', value)
|
||||
_onWeekDayChange = value => this._update('weekDay', value)
|
||||
|
||||
_onTimezoneChange = timezone => {
|
||||
const { props } = this
|
||||
props.onChange({
|
||||
cronPattern: props.cronPattern,
|
||||
timezone
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
cronPattern,
|
||||
timezone
|
||||
} = this.props
|
||||
const cronPatternArr = cronPattern.split(' ')
|
||||
|
||||
return (
|
||||
<div className='card-block'>
|
||||
<Row>
|
||||
<Col mediumSize={6}>
|
||||
<TimePicker
|
||||
ref='month'
|
||||
type='Month'
|
||||
dataRender={getMonthName}
|
||||
data={MONTHS}
|
||||
labelId='Month'
|
||||
optionsRenderer={getMonthName}
|
||||
options={MONTHS}
|
||||
onChange={this._onMonthChange}
|
||||
value={cronPatternArr[PICKTIME_TO_ID['month']]}
|
||||
/>
|
||||
<TimePicker
|
||||
ref='monthDay'
|
||||
type='MonthDay'
|
||||
data={DAYS}
|
||||
labelId='MonthDay'
|
||||
options={DAYS}
|
||||
onChange={this._onMonthDayChange}
|
||||
value={cronPatternArr[PICKTIME_TO_ID['monthDay']]}
|
||||
/>
|
||||
<TimePicker
|
||||
ref='weekDay'
|
||||
type='WeekDay'
|
||||
dataRender={getDayName}
|
||||
data={WEEK_DAYS}
|
||||
labelId='WeekDay'
|
||||
optionsRenderer={getDayName}
|
||||
options={WEEK_DAYS}
|
||||
onChange={this._onWeekDayChange}
|
||||
value={cronPatternArr[PICKTIME_TO_ID['weekDay']]}
|
||||
/>
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<TimePicker
|
||||
ref='hour'
|
||||
type='Hour'
|
||||
data={HOURS}
|
||||
range={[2, 12]}
|
||||
labelId='Hour'
|
||||
options={HOURS}
|
||||
range={HOURS_RANGE}
|
||||
onChange={this._onHourChange}
|
||||
value={cronPatternArr[PICKTIME_TO_ID['hour']]}
|
||||
/>
|
||||
<TimePicker
|
||||
ref='minute'
|
||||
type='Minute'
|
||||
data={MINS}
|
||||
range={[2, 30]}
|
||||
labelId='Minute'
|
||||
options={MINS}
|
||||
range={MINUTES_RANGE}
|
||||
onChange={this._onMinuteChange}
|
||||
value={cronPatternArr[PICKTIME_TO_ID['minute']]}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<hr />
|
||||
<TimezonePicker value={timezone} onChange={this._onTimezoneChange} />
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,38 +1,42 @@
|
||||
import Component from 'base-component'
|
||||
import React from 'react'
|
||||
import _ from 'intl'
|
||||
import assign from 'lodash/assign'
|
||||
import classNames from 'classnames'
|
||||
import filter from 'lodash/filter'
|
||||
import flatten from 'lodash/flatten'
|
||||
import forEach from 'lodash/forEach'
|
||||
import groupBy from 'lodash/groupBy'
|
||||
import keyBy from 'lodash/keyBy'
|
||||
import keys from 'lodash/keys'
|
||||
import map from 'lodash/map'
|
||||
import renderXoItem from 'render-xo-item'
|
||||
import sortBy from 'lodash/sortBy'
|
||||
import store from 'store'
|
||||
import { parse as parseRemote } from 'xo-remote-parser'
|
||||
import { Select } from 'form'
|
||||
|
||||
import _ from './intl'
|
||||
import Component from './base-component'
|
||||
import propTypes from './prop-types'
|
||||
import renderXoItem from './render-xo-item'
|
||||
import { Select } from './form'
|
||||
import {
|
||||
createFilter,
|
||||
createGetObjectsOfType,
|
||||
createGetTags,
|
||||
createSelector
|
||||
} from 'selectors'
|
||||
|
||||
createSelector,
|
||||
getObject
|
||||
} from './selectors'
|
||||
import {
|
||||
connectStore,
|
||||
mapPlus,
|
||||
propTypes
|
||||
} from 'utils'
|
||||
|
||||
resolveResourceSets
|
||||
} from './utils'
|
||||
import {
|
||||
isSrWritable,
|
||||
subscribeGroups,
|
||||
subscribeRemotes,
|
||||
subscribeResourceSets,
|
||||
subscribeRoles,
|
||||
subscribeUsers
|
||||
} from 'xo'
|
||||
} from './xo'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -325,11 +329,9 @@ export const SelectPool = makeStoreSelect(() => ({
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const userSrPredicate = sr => sr.content_type === 'user'
|
||||
|
||||
export const SelectSr = makeStoreSelect(() => {
|
||||
const getSrsByContainer = createGetObjectsOfType('SR').filter(
|
||||
(_, { predicate }) => predicate || userSrPredicate
|
||||
(_, { predicate }) => predicate || isSrWritable
|
||||
).sort().groupBy('$container')
|
||||
|
||||
const getContainerIds = createSelector(
|
||||
@@ -583,3 +585,184 @@ export const SelectRemote = makeSubscriptionSelect(subscriber => {
|
||||
|
||||
return unsubscribeRemotes
|
||||
}, { placeholder: _('selectRemotes') })
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const SelectResourceSet = makeSubscriptionSelect(subscriber => {
|
||||
const unsubscribeResourceSets = subscribeResourceSets(resourceSets => {
|
||||
const xoObjects = map(sortBy(resolveResourceSets(resourceSets), 'name'), resourceSet => ({...resourceSet, type: 'resourceSet'}))
|
||||
|
||||
subscriber({xoObjects})
|
||||
})
|
||||
|
||||
return unsubscribeResourceSets
|
||||
}, { placeholder: _('selectResourceSets') })
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export class SelectResourceSetsVmTemplate extends Component {
|
||||
get value () {
|
||||
return this.refs.select.value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.refs.select.value = value
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.componentWillUnmount = subscribeResourceSets(resourceSets => {
|
||||
this.setState({
|
||||
resourceSets: resolveResourceSets(resourceSets)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
_getTemplates = createSelector(
|
||||
() => this.props.resourceSet,
|
||||
({ objectsByType }) => {
|
||||
const { predicate } = this.props
|
||||
const templates = objectsByType['VM-template']
|
||||
return sortBy(predicate ? filter(templates, predicate) : templates, 'name_label')
|
||||
}
|
||||
)
|
||||
|
||||
render () {
|
||||
return (
|
||||
<GenericSelect
|
||||
ref='select'
|
||||
placeholder={_('selectResourceSetsVmTemplate')}
|
||||
{...this.props}
|
||||
xoObjects={this._getTemplates()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export class SelectResourceSetsSr extends Component {
|
||||
get value () {
|
||||
return this.refs.select.value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.refs.select.value = value
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.componentWillUnmount = subscribeResourceSets(resourceSets => {
|
||||
this.setState({
|
||||
resourceSets: resolveResourceSets(resourceSets)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
_getSrs = createSelector(
|
||||
() => this.props.resourceSet,
|
||||
({ objectsByType }) => {
|
||||
const { predicate } = this.props
|
||||
const srs = objectsByType['SR']
|
||||
return sortBy(predicate ? filter(srs, predicate) : srs, 'name_label')
|
||||
}
|
||||
)
|
||||
|
||||
render () {
|
||||
return (
|
||||
<GenericSelect
|
||||
ref='select'
|
||||
placeholder={_('selectResourceSetsSr')}
|
||||
{...this.props}
|
||||
xoObjects={this._getSrs()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export class SelectResourceSetsVdi extends Component {
|
||||
get value () {
|
||||
return this.refs.select.value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.refs.select.value = value
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.componentWillUnmount = subscribeResourceSets(resourceSets => {
|
||||
this.setState({
|
||||
resourceSets: resolveResourceSets(resourceSets)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
_getObject (id) {
|
||||
return getObject(store.getState(), id, true)
|
||||
}
|
||||
|
||||
_getSrs = createSelector(
|
||||
() => this.props.resourceSet,
|
||||
({ objectsByType }) => {
|
||||
const { srPredicate } = this.props
|
||||
const srs = objectsByType['SR']
|
||||
return srPredicate ? filter(srs, srPredicate) : srs
|
||||
}
|
||||
)
|
||||
|
||||
_getVdis = createSelector(
|
||||
this._getSrs,
|
||||
srs => sortBy(map(flatten(map(srs, sr => sr.VDIs)), this._getObject), 'name_label')
|
||||
)
|
||||
|
||||
render () {
|
||||
return (
|
||||
<GenericSelect
|
||||
ref='select'
|
||||
placeholder={_('selectResourceSetsVdi')}
|
||||
{...this.props}
|
||||
xoObjects={this._getVdis()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export class SelectResourceSetsNetwork extends Component {
|
||||
get value () {
|
||||
return this.refs.select.value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.refs.select.value = value
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.componentWillUnmount = subscribeResourceSets(resourceSets => {
|
||||
this.setState({
|
||||
resourceSets: resolveResourceSets(resourceSets)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
_getNetworks = createSelector(
|
||||
() => this.props.resourceSet,
|
||||
({ objectsByType }) => {
|
||||
const { predicate } = this.props
|
||||
const networks = objectsByType['network']
|
||||
return sortBy(predicate ? filter(networks, predicate) : networks, 'name_label')
|
||||
}
|
||||
)
|
||||
|
||||
render () {
|
||||
return (
|
||||
<GenericSelect
|
||||
ref='select'
|
||||
placeholder={_('selectResourceSetsNetwork')}
|
||||
{...this.props}
|
||||
xoObjects={this._getNetworks()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,9 @@ import size from 'lodash/size'
|
||||
import slice from 'lodash/slice'
|
||||
import { createSelector as create } from 'reselect'
|
||||
|
||||
import invoke from './invoke'
|
||||
import shallowEqual from './shallow-equal'
|
||||
import { EMPTY_ARRAY, EMPTY_OBJECT, invoke } from './utils'
|
||||
import { EMPTY_ARRAY, EMPTY_OBJECT } from './utils'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -244,13 +245,18 @@ const _getPermissionsPredicate = invoke(() => {
|
||||
|
||||
// Creates an object selector from an id selector.
|
||||
export const createGetObject = (idSelector = _getId) =>
|
||||
(state, props) => {
|
||||
(state, props, useResourceSet) => {
|
||||
const object = state.objects.all[idSelector(state, props)]
|
||||
if (!object) {
|
||||
return
|
||||
}
|
||||
|
||||
if (useResourceSet) {
|
||||
return object
|
||||
}
|
||||
|
||||
const predicate = _getPermissionsPredicate(state)
|
||||
|
||||
if (!predicate) {
|
||||
if (predicate == null) {
|
||||
return object // no filtering
|
||||
@@ -324,14 +330,18 @@ const _extendCollectionSelector = (selector, objectsType) => {
|
||||
}
|
||||
_addSort(selector)
|
||||
|
||||
// groupBy and sort can be chained.
|
||||
selector.pick = idsSelector => _addGroupBy(_addSort(
|
||||
createPicker(selector, idsSelector)
|
||||
))
|
||||
|
||||
// count, groupBy and sort can be chained.
|
||||
selector.filter = predicate => _addCount(_addGroupBy(_addSort(
|
||||
createFilter(selector, predicate)
|
||||
const _addFilter = selector => {
|
||||
selector.filter = predicate => _addCount(_addGroupBy(_addSort(
|
||||
createFilter(selector, predicate)
|
||||
)))
|
||||
return selector
|
||||
}
|
||||
_addFilter(selector)
|
||||
|
||||
// filter, groupBy and sort can be chained.
|
||||
selector.pick = idsSelector => _addFilter(_addGroupBy(_addSort(
|
||||
createPicker(selector, idsSelector)
|
||||
)))
|
||||
|
||||
return selector
|
||||
@@ -350,7 +360,7 @@ const _extendCollectionSelector = (selector, objectsType) => {
|
||||
// - groupBy: returns a selector which returns the objects grouped by
|
||||
// a value determined by a getter selector
|
||||
// - pick: returns a selector which returns only the objects with given
|
||||
// ids (groupBy and sort can be chained)
|
||||
// ids (filter, groupBy and sort can be chained)
|
||||
// - sort: returns a selector which returns the objects appropriately
|
||||
// sorted (groupBy can be chained)
|
||||
export const createGetObjectsOfType = type => {
|
||||
@@ -403,3 +413,24 @@ export const createGetObjectMessages = objectSelector =>
|
||||
// const object = getObject(store.getState(), objectId)
|
||||
// ...
|
||||
export const getObject = createGetObject((_, id) => id)
|
||||
|
||||
export const createGetHostMetrics = hostSelector => _createCollectionWrapper(
|
||||
create(
|
||||
hostSelector,
|
||||
hosts => {
|
||||
const metrics = {
|
||||
count: 0,
|
||||
cpus: 0,
|
||||
memoryTotal: 0,
|
||||
memoryUsage: 0
|
||||
}
|
||||
forEach(hosts, host => {
|
||||
metrics.count++
|
||||
metrics.cpus += host.cpus.cores
|
||||
metrics.memoryTotal += host.memory.size
|
||||
metrics.memoryUsage += host.memory.usage
|
||||
})
|
||||
return metrics
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React, { cloneElement } from 'react'
|
||||
import { propTypes } from 'utils'
|
||||
|
||||
import propTypes from './prop-types'
|
||||
|
||||
const SINGLE_LINE_STYLE = { display: 'flex' }
|
||||
const COL_STYLE = { margin: 'auto' }
|
||||
const COL_STYLE = { marginTop: 'auto', marginBottom: 'auto' }
|
||||
|
||||
const SingleLineRow = propTypes({
|
||||
className: propTypes.string
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
import Component from 'base-component'
|
||||
import Icon from 'icon'
|
||||
import React from 'react'
|
||||
import SingleLineRow from 'single-line-row'
|
||||
import _ from 'intl'
|
||||
import ceil from 'lodash/ceil'
|
||||
import debounce from 'lodash/debounce'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import isFunction from 'lodash/isFunction'
|
||||
import map from 'lodash/map'
|
||||
import { Pagination } from 'react-bootstrap-4/lib'
|
||||
import React from 'react'
|
||||
import { Dropdown, MenuItem, Pagination } from 'react-bootstrap-4/lib'
|
||||
import DropdownMenu from 'react-bootstrap-4/lib/DropdownMenu' // https://phabricator.babeljs.io/T6662 so Dropdown.Menu won't work like https://react-bootstrap.github.io/components.html#btn-dropdowns-custom
|
||||
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 { Container, Col } from 'grid'
|
||||
import { create as createMatcher } from 'complex-matcher'
|
||||
import { propTypes } from 'utils'
|
||||
|
||||
import Component from '../base-component'
|
||||
import Icon from '../icon'
|
||||
import propTypes from '../prop-types'
|
||||
import SingleLineRow from '../single-line-row'
|
||||
import { BlockLink } from '../link'
|
||||
import { Container, Col } from '../grid'
|
||||
import { create as createMatcher } from '../complex-matcher'
|
||||
import {
|
||||
createCounter,
|
||||
createFilter,
|
||||
createPager,
|
||||
createSelector,
|
||||
@@ -23,14 +30,19 @@ import styles from './index.css'
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
filters: propTypes.object,
|
||||
nFilteredItems: propTypes.number.isRequired,
|
||||
nItems: propTypes.number.isRequired,
|
||||
onChange: propTypes.func.isRequired
|
||||
})
|
||||
class TableFilter extends Component {
|
||||
_cleanFilter = () => {
|
||||
_cleanFilter = () => this._setFilter('')
|
||||
|
||||
_setFilter = filterValue => {
|
||||
const { filter } = this.refs
|
||||
filter.value = ''
|
||||
filter.value = filterValue
|
||||
filter.focus()
|
||||
this.props.onChange('')
|
||||
this.props.onChange(filterValue)
|
||||
}
|
||||
|
||||
_onChange = event => {
|
||||
@@ -38,9 +50,27 @@ class TableFilter extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { props } = this
|
||||
|
||||
return (
|
||||
<div className='input-group'>
|
||||
<span className='input-group-addon'><Icon icon='search' /></span>
|
||||
<span className='input-group-addon'>{props.nFilteredItems} / {props.nItems}</span>
|
||||
{isEmpty(props.filters)
|
||||
? <span className='input-group-addon'><Icon icon='search' /></span>
|
||||
: <div className='input-group-btn'>
|
||||
<Dropdown id='filter'>
|
||||
<DropdownToggle bsStyle='info'>
|
||||
<Icon icon='search' />
|
||||
</DropdownToggle>
|
||||
<DropdownMenu>
|
||||
{map(props.filters, (filter, label) =>
|
||||
<MenuItem key={label} onClick={() => this._setFilter(filter)}>
|
||||
{_(label)}
|
||||
</MenuItem>
|
||||
)}
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>}
|
||||
<input
|
||||
type='text'
|
||||
ref='filter'
|
||||
@@ -118,8 +148,13 @@ const DEFAULT_ITEMS_PER_PAGE = 10
|
||||
sortOrder: propTypes.string
|
||||
})).isRequired,
|
||||
filterContainer: propTypes.func,
|
||||
filters: propTypes.object,
|
||||
itemsPerPage: propTypes.number,
|
||||
paginationContainer: propTypes.func,
|
||||
rowLink: propTypes.oneOfType([
|
||||
propTypes.func,
|
||||
propTypes.string
|
||||
]),
|
||||
userData: propTypes.any
|
||||
})
|
||||
export default class SortedTable extends Component {
|
||||
@@ -134,6 +169,10 @@ export default class SortedTable extends Component {
|
||||
this._getSelectedColumn = () =>
|
||||
this.props.columns[this.state.selectedColumn]
|
||||
|
||||
this._getTotalNumberOfItems = createCounter(
|
||||
() => this.props.collection
|
||||
)
|
||||
|
||||
this._getAllItems = createSort(
|
||||
createFilter(
|
||||
() => this.props.collection,
|
||||
@@ -142,7 +181,14 @@ export default class SortedTable extends Component {
|
||||
createMatcher
|
||||
)
|
||||
),
|
||||
() => this._getSelectedColumn().sortCriteria,
|
||||
createSelector(
|
||||
() => this._getSelectedColumn().sortCriteria,
|
||||
() => this.props.userData,
|
||||
(sortCriteria, userData) =>
|
||||
(typeof sortCriteria === 'function')
|
||||
? object => sortCriteria(object, userData)
|
||||
: sortCriteria
|
||||
),
|
||||
() => this.state.sortOrder
|
||||
)
|
||||
|
||||
@@ -156,7 +202,9 @@ export default class SortedTable extends Component {
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this._sort(this.state.selectedColumn)
|
||||
this.setState({
|
||||
sortOrder: this.props.columns[this.state.selectedColumn].sortOrder === 'desc' ? 'desc' : 'asc'
|
||||
})
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
@@ -203,9 +251,13 @@ export default class SortedTable extends Component {
|
||||
const {
|
||||
paginationContainer,
|
||||
filterContainer,
|
||||
filters,
|
||||
rowLink,
|
||||
userData
|
||||
} = props
|
||||
|
||||
const nFilteredItems = this._getAllItems().length
|
||||
|
||||
const paginationInstance = (
|
||||
<Pagination
|
||||
first
|
||||
@@ -215,14 +267,19 @@ export default class SortedTable extends Component {
|
||||
ellipsis
|
||||
boundaryLinks
|
||||
maxButtons={10}
|
||||
items={ceil(this._getAllItems().length / state.itemsPerPage)}
|
||||
items={ceil(nFilteredItems / state.itemsPerPage)}
|
||||
activePage={this.state.activePage}
|
||||
onSelect={this._onPageSelection}
|
||||
/>
|
||||
)
|
||||
|
||||
const filterInstance = (
|
||||
<TableFilter onChange={this._onFilterChange} />
|
||||
<TableFilter
|
||||
filters={filters}
|
||||
nFilteredItems={nFilteredItems}
|
||||
nItems={this._getTotalNumberOfItems()}
|
||||
onChange={this._onFilterChange}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
@@ -242,15 +299,23 @@ export default class SortedTable extends Component {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{map(this._getVisibleItems(), (item, key) => (
|
||||
<tr key={key}>
|
||||
{map(props.columns, (column, key) => (
|
||||
<td key={key}>
|
||||
{column.itemRenderer(item, userData)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
{map(this._getVisibleItems(), (item, i) => {
|
||||
const colums = map(props.columns, (column, key) => (
|
||||
<td key={key}>
|
||||
{column.itemRenderer(item, userData)}
|
||||
</td>
|
||||
))
|
||||
|
||||
const { id = i } = item
|
||||
|
||||
return rowLink
|
||||
? <BlockLink
|
||||
key={id}
|
||||
tagName='tr'
|
||||
to={isFunction(rowLink) ? rowLink(item, userData) : rowLink}
|
||||
>{colums}</BlockLink>
|
||||
: <tr key={id}>{colums}</tr>
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
{(!paginationContainer || !filterContainer) && (
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import _ from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import Icon from 'icon'
|
||||
import React from 'react'
|
||||
import { Link } from 'react-router'
|
||||
|
||||
import _ from './intl'
|
||||
import ActionButton from './action-button'
|
||||
import Icon from './icon'
|
||||
import Link from './link'
|
||||
|
||||
const STYLE = {
|
||||
marginBottom: '1em',
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react'
|
||||
import Icon from 'icon'
|
||||
import map from 'lodash/map'
|
||||
|
||||
import Component from './base-component'
|
||||
import { propTypes } from './utils'
|
||||
import Icon from './icon'
|
||||
import propTypes from './prop-types'
|
||||
|
||||
@propTypes({
|
||||
labels: propTypes.arrayOf(React.PropTypes.string).isRequired,
|
||||
|
||||
106
src/common/timezone-picker.js
Normal file
106
src/common/timezone-picker.js
Normal file
@@ -0,0 +1,106 @@
|
||||
import ActionButton from 'action-button'
|
||||
import map from 'lodash/map'
|
||||
import moment from 'moment-timezone'
|
||||
import React from 'react'
|
||||
|
||||
import _ from './intl'
|
||||
import Component from './base-component'
|
||||
import propTypes from './prop-types'
|
||||
import { getXoServerTimezone } from './xo'
|
||||
import { Select } from './form'
|
||||
|
||||
const XO_SERVER_TIMEZONE = 'xo-server'
|
||||
|
||||
@propTypes({
|
||||
defaultValue: propTypes.string,
|
||||
onChange: propTypes.func.isRequired,
|
||||
value: propTypes.string
|
||||
})
|
||||
export default class TimezonePicker extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state.options = map(moment.tz.names(), value => ({ label: value, value }))
|
||||
}
|
||||
|
||||
get value () {
|
||||
const value = this.refs.select.value
|
||||
return (value === XO_SERVER_TIMEZONE) ? null : value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.refs.select.value = value || XO_SERVER_TIMEZONE
|
||||
}
|
||||
|
||||
_updateTimezone (value) {
|
||||
this.props.onChange(value)
|
||||
}
|
||||
_handleChange = option => {
|
||||
return this._updateTimezone(
|
||||
!option || option.value === XO_SERVER_TIMEZONE
|
||||
? null
|
||||
: option.value
|
||||
)
|
||||
}
|
||||
_useServerTime = () => {
|
||||
this._updateTimezone(null)
|
||||
}
|
||||
_useLocalTime = () => {
|
||||
this._updateTimezone(moment.tz.guess())
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
// Use local timezone (Web browser) if no default value.
|
||||
if (this.props.value === undefined) {
|
||||
this._useLocalTime()
|
||||
}
|
||||
|
||||
getXoServerTimezone.then(serverTimezone => {
|
||||
this.setState({
|
||||
options: [{
|
||||
label: _('serverTimezoneOption', {
|
||||
value: serverTimezone
|
||||
}),
|
||||
value: XO_SERVER_TIMEZONE
|
||||
}].concat(this.state.options),
|
||||
serverTimezone
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
const { props, state } = this
|
||||
return (
|
||||
<div>
|
||||
<div className='alert alert-info' role='alert'>
|
||||
{_('timezonePickerServerValue')} <strong>{state.serverTimezone}</strong>
|
||||
</div>
|
||||
<Select
|
||||
className='m-b-1'
|
||||
defaultValue={props.defaultValue}
|
||||
onChange={this._handleChange}
|
||||
options={state.options}
|
||||
placeholder={_('selectTimezone')}
|
||||
ref='select'
|
||||
value={props.value || XO_SERVER_TIMEZONE}
|
||||
/>
|
||||
<div className='pull-right'>
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
className='m-r-1'
|
||||
handler={this._useServerTime}
|
||||
icon='time'
|
||||
>
|
||||
{_('timezonePickerUseServerTime')}
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
btnStyle='secondary'
|
||||
handler={this._useLocalTime}
|
||||
icon='time'
|
||||
>
|
||||
{_('timezonePickerUseLocalTime')}
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
287
src/common/tooltip/get-position.js
Normal file
287
src/common/tooltip/get-position.js
Normal file
@@ -0,0 +1,287 @@
|
||||
// Source: https://github.com/wwayne/react-tooltip/blob/master/src/utils/getPosition.js
|
||||
|
||||
/**
|
||||
* Calculate the position of tooltip
|
||||
*
|
||||
* @params
|
||||
* - `e` {Event} the event of current mouse
|
||||
* - `target` {Element} the currentTarget of the event
|
||||
* - `node` {DOM} the react-tooltip object
|
||||
* - `place` {String} top / right / bottom / left
|
||||
* - `effect` {String} float / solid
|
||||
* - `offset` {Object} the offset to default position
|
||||
*
|
||||
* @return {Object
|
||||
* - `isNewState` {Bool} required
|
||||
* - `newState` {Object}
|
||||
* - `position` {OBject} {left: {Number}, top: {Number}}
|
||||
*/
|
||||
export default function (e, target, node, place, effect, offset) {
|
||||
const tipWidth = node.clientWidth
|
||||
const tipHeight = node.clientHeight
|
||||
const {mouseX, mouseY} = getCurrentOffset(e, target, effect)
|
||||
const defaultOffset = getDefaultPosition(effect, target.clientWidth, target.clientHeight, tipWidth, tipHeight)
|
||||
const {extraOffsetX, extraOffsetY} = calculateOffset(offset)
|
||||
|
||||
const windowWidth = window.innerWidth
|
||||
const windowHeight = window.innerHeight
|
||||
|
||||
const {parentTop, parentLeft} = getParent(target)
|
||||
|
||||
// Get the edge offset of the tooltip
|
||||
const getTipOffsetLeft = (place) => {
|
||||
const offsetX = defaultOffset[place].l
|
||||
return mouseX + offsetX + extraOffsetX
|
||||
}
|
||||
const getTipOffsetRight = (place) => {
|
||||
const offsetX = defaultOffset[place].r
|
||||
return mouseX + offsetX + extraOffsetX
|
||||
}
|
||||
const getTipOffsetTop = (place) => {
|
||||
const offsetY = defaultOffset[place].t
|
||||
return mouseY + offsetY + extraOffsetY
|
||||
}
|
||||
const getTipOffsetBottom = (place) => {
|
||||
const offsetY = defaultOffset[place].b
|
||||
return mouseY + offsetY + extraOffsetY
|
||||
}
|
||||
|
||||
// Judge if the tooltip has over the window(screen)
|
||||
const outsideVertical = () => {
|
||||
let result = false
|
||||
let newPlace
|
||||
if (getTipOffsetTop('left') < 0 &&
|
||||
getTipOffsetBottom('left') <= windowHeight &&
|
||||
getTipOffsetBottom('bottom') <= windowHeight) {
|
||||
result = true
|
||||
newPlace = 'bottom'
|
||||
} else if (getTipOffsetBottom('left') > windowHeight &&
|
||||
getTipOffsetTop('left') >= 0 &&
|
||||
getTipOffsetTop('top') >= 0) {
|
||||
result = true
|
||||
newPlace = 'top'
|
||||
}
|
||||
return {result, newPlace}
|
||||
}
|
||||
const outsideLeft = () => {
|
||||
let {result, newPlace} = outsideVertical() // Deal with vertical as first priority
|
||||
if (result && outsideHorizontal().result) {
|
||||
return {result: false} // No need to change, if change to vertical will out of space
|
||||
}
|
||||
if (!result && getTipOffsetLeft('left') < 0 && getTipOffsetRight('right') <= windowWidth) {
|
||||
result = true // If vertical ok, but let out of side and right won't out of side
|
||||
newPlace = 'right'
|
||||
}
|
||||
return {result, newPlace}
|
||||
}
|
||||
const outsideRight = () => {
|
||||
let {result, newPlace} = outsideVertical()
|
||||
if (result && outsideHorizontal().result) {
|
||||
return {result: false} // No need to change, if change to vertical will out of space
|
||||
}
|
||||
if (!result && getTipOffsetRight('right') > windowWidth && getTipOffsetLeft('left') >= 0) {
|
||||
result = true
|
||||
newPlace = 'left'
|
||||
}
|
||||
return {result, newPlace}
|
||||
}
|
||||
|
||||
const outsideHorizontal = () => {
|
||||
let result = false
|
||||
let newPlace
|
||||
if (getTipOffsetLeft('top') < 0 &&
|
||||
getTipOffsetRight('top') <= windowWidth &&
|
||||
getTipOffsetRight('right') <= windowWidth) {
|
||||
result = true
|
||||
newPlace = 'right'
|
||||
} else if (getTipOffsetRight('top') > windowWidth &&
|
||||
getTipOffsetLeft('top') >= 0 &&
|
||||
getTipOffsetLeft('left') >= 0) {
|
||||
result = true
|
||||
newPlace = 'left'
|
||||
}
|
||||
return {result, newPlace}
|
||||
}
|
||||
const outsideTop = () => {
|
||||
let {result, newPlace} = outsideHorizontal()
|
||||
if (result && outsideVertical().result) {
|
||||
return {result: false}
|
||||
}
|
||||
if (!result && getTipOffsetTop('top') < 0 && getTipOffsetBottom('bottom') <= windowHeight) {
|
||||
result = true
|
||||
newPlace = 'bottom'
|
||||
}
|
||||
return {result, newPlace}
|
||||
}
|
||||
const outsideBottom = () => {
|
||||
let {result, newPlace} = outsideHorizontal()
|
||||
if (result && outsideVertical().result) {
|
||||
return {result: false}
|
||||
}
|
||||
if (!result && getTipOffsetBottom('bottom') > windowHeight && getTipOffsetTop('top') >= 0) {
|
||||
result = true
|
||||
newPlace = 'top'
|
||||
}
|
||||
return {result, newPlace}
|
||||
}
|
||||
|
||||
// Return new state to change the placement to the reverse if possible
|
||||
const outsideLeftResult = outsideLeft()
|
||||
const outsideRightResult = outsideRight()
|
||||
const outsideTopResult = outsideTop()
|
||||
const outsideBottomResult = outsideBottom()
|
||||
|
||||
if (place === 'left' && outsideLeftResult.result) {
|
||||
return {
|
||||
isNewState: true,
|
||||
newState: {place: outsideLeftResult.newPlace}
|
||||
}
|
||||
} else if (place === 'right' && outsideRightResult.result) {
|
||||
return {
|
||||
isNewState: true,
|
||||
newState: {place: outsideRightResult.newPlace}
|
||||
}
|
||||
} else if (place === 'top' && outsideTopResult.result) {
|
||||
return {
|
||||
isNewState: true,
|
||||
newState: {place: outsideTopResult.newPlace}
|
||||
}
|
||||
} else if (place === 'bottom' && outsideBottomResult.result) {
|
||||
return {
|
||||
isNewState: true,
|
||||
newState: {place: outsideBottomResult.newPlace}
|
||||
}
|
||||
}
|
||||
|
||||
// Return tooltip offset position
|
||||
return {
|
||||
isNewState: false,
|
||||
position: {
|
||||
left: getTipOffsetLeft(place) - parentLeft,
|
||||
top: getTipOffsetTop(place) - parentTop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get current mouse offset
|
||||
const getCurrentOffset = (e, currentTarget, effect) => {
|
||||
const boundingClientRect = currentTarget.getBoundingClientRect()
|
||||
const targetTop = boundingClientRect.top
|
||||
const targetLeft = boundingClientRect.left
|
||||
const targetWidth = currentTarget.clientWidth
|
||||
const targetHeight = currentTarget.clientHeight
|
||||
|
||||
if (effect === 'float') {
|
||||
return {
|
||||
mouseX: e.clientX,
|
||||
mouseY: e.clientY
|
||||
}
|
||||
}
|
||||
return {
|
||||
mouseX: targetLeft + (targetWidth / 2),
|
||||
mouseY: targetTop + (targetHeight / 2)
|
||||
}
|
||||
}
|
||||
|
||||
// List all possibility of tooltip final offset
|
||||
// This is useful in judging if it is necessary for tooltip to switch position when out of window
|
||||
const getDefaultPosition = (effect, targetWidth, targetHeight, tipWidth, tipHeight) => {
|
||||
let top
|
||||
let right
|
||||
let bottom
|
||||
let left
|
||||
const disToMouse = 3
|
||||
const triangleHeight = 2
|
||||
const cursorHeight = 12 // Optimize for float bottom only, cause the cursor will hide the tooltip
|
||||
|
||||
if (effect === 'float') {
|
||||
top = {
|
||||
l: -(tipWidth / 2),
|
||||
r: tipWidth / 2,
|
||||
t: -(tipHeight + disToMouse + triangleHeight),
|
||||
b: -disToMouse
|
||||
}
|
||||
bottom = {
|
||||
l: -(tipWidth / 2),
|
||||
r: tipWidth / 2,
|
||||
t: disToMouse + cursorHeight,
|
||||
b: tipHeight + disToMouse + triangleHeight + cursorHeight
|
||||
}
|
||||
left = {
|
||||
l: -(tipWidth + disToMouse + triangleHeight),
|
||||
r: -disToMouse,
|
||||
t: -(tipHeight / 2),
|
||||
b: tipHeight / 2
|
||||
}
|
||||
right = {
|
||||
l: disToMouse,
|
||||
r: tipWidth + disToMouse + triangleHeight,
|
||||
t: -(tipHeight / 2),
|
||||
b: tipHeight / 2
|
||||
}
|
||||
} else if (effect === 'solid') {
|
||||
top = {
|
||||
l: -(tipWidth / 2),
|
||||
r: tipWidth / 2,
|
||||
t: -(targetHeight / 2 + tipHeight + triangleHeight),
|
||||
b: -(targetHeight / 2)
|
||||
}
|
||||
bottom = {
|
||||
l: -(tipWidth / 2),
|
||||
r: tipWidth / 2,
|
||||
t: targetHeight / 2,
|
||||
b: targetHeight / 2 + tipHeight + triangleHeight
|
||||
}
|
||||
left = {
|
||||
l: -(tipWidth + targetWidth / 2 + triangleHeight),
|
||||
r: -(targetWidth / 2),
|
||||
t: -(tipHeight / 2),
|
||||
b: tipHeight / 2
|
||||
}
|
||||
right = {
|
||||
l: targetWidth / 2,
|
||||
r: tipWidth + targetWidth / 2 + triangleHeight,
|
||||
t: -(tipHeight / 2),
|
||||
b: tipHeight / 2
|
||||
}
|
||||
}
|
||||
|
||||
return {top, bottom, left, right}
|
||||
}
|
||||
|
||||
// Consider additional offset into position calculation
|
||||
const calculateOffset = (offset) => {
|
||||
let extraOffsetX = 0
|
||||
let extraOffsetY = 0
|
||||
|
||||
if (Object.prototype.toString.apply(offset) === '[object String]') {
|
||||
offset = JSON.parse(offset.toString().replace(/'/g, '"'))
|
||||
}
|
||||
for (let key in offset) {
|
||||
if (key === 'top') {
|
||||
extraOffsetY -= parseInt(offset[key], 10)
|
||||
} else if (key === 'bottom') {
|
||||
extraOffsetY += parseInt(offset[key], 10)
|
||||
} else if (key === 'left') {
|
||||
extraOffsetX -= parseInt(offset[key], 10)
|
||||
} else if (key === 'right') {
|
||||
extraOffsetX += parseInt(offset[key], 10)
|
||||
}
|
||||
}
|
||||
|
||||
return {extraOffsetX, extraOffsetY}
|
||||
}
|
||||
|
||||
// Get the offset of the parent elements
|
||||
const getParent = (currentTarget) => {
|
||||
let currentParent = currentTarget
|
||||
while (currentParent) {
|
||||
if (currentParent.style.transform.length > 0) break
|
||||
currentParent = currentParent.parentElement
|
||||
}
|
||||
|
||||
const parentTop = currentParent && currentParent.getBoundingClientRect().top || 0
|
||||
const parentLeft = currentParent && currentParent.getBoundingClientRect().left || 0
|
||||
|
||||
return {parentTop, parentLeft}
|
||||
}
|
||||
@@ -1,45 +1,20 @@
|
||||
.container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.common {
|
||||
opacity: 0;
|
||||
transition: opacity .3s;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.container:hover .common {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
composes: common;
|
||||
|
||||
border-bottom: .5em solid rgba(0, 0, 0, .8);
|
||||
border-left: .5em solid transparent;
|
||||
border-right: .5em solid transparent;
|
||||
font-size: 1rem;
|
||||
left: 25%;
|
||||
margin-left: 1em;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
composes: common;
|
||||
|
||||
background: #333;
|
||||
background: rgba(0, 0, 0, .8);
|
||||
border-radius: .25em;
|
||||
.tooltipEnabled {
|
||||
background-color: #222;
|
||||
border-radius: 3px;
|
||||
border: 1px solid $fff;
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
left: 25%;
|
||||
margin-top: .5em;
|
||||
padding: .5em;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
min-width: fit-content;
|
||||
max-width: 20em;
|
||||
display: inline-block;
|
||||
font-size: 13px;
|
||||
margin-left: 0px;
|
||||
margin-top: 0px;
|
||||
opacity: 0.9;
|
||||
padding: 8px 21px;
|
||||
pointer-events: none;
|
||||
position: fixed;
|
||||
transition: opacity 0.3s ease-out, margin-top 0.3s ease-out, margin-left 0.3s ease-out;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.tooltipDisabled {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -1,30 +1,147 @@
|
||||
import classNames from 'classnames'
|
||||
import React, { PropTypes } from 'react'
|
||||
import isString from 'lodash/isString'
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
|
||||
import Component from '../base-component'
|
||||
import getPosition from './get-position'
|
||||
import propTypes from '../prop-types'
|
||||
|
||||
import styles from './index.css'
|
||||
|
||||
const Tooltip = ({
|
||||
children,
|
||||
className,
|
||||
content,
|
||||
style,
|
||||
tagName: Component = 'span'
|
||||
}) => (
|
||||
<Component className={classNames(className, styles.container)} style={style}>
|
||||
<div className={styles.arrow} />
|
||||
<div className={styles.tooltip}>
|
||||
{content}
|
||||
</div>
|
||||
{children}
|
||||
</Component>
|
||||
)
|
||||
// ===================================================================
|
||||
|
||||
Tooltip.propTypes = {
|
||||
children: PropTypes.node,
|
||||
className: PropTypes.string,
|
||||
content: PropTypes.any.isRequired,
|
||||
style: PropTypes.object,
|
||||
tagName: PropTypes.string
|
||||
let instance
|
||||
|
||||
export class TooltipViewer extends Component {
|
||||
constructor () {
|
||||
super()
|
||||
|
||||
if (instance) {
|
||||
throw new Error('Tooltip viewer is a singleton!')
|
||||
}
|
||||
instance = this
|
||||
this.state.place = 'top'
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
className,
|
||||
content,
|
||||
place,
|
||||
show,
|
||||
style
|
||||
} = this.state
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(show ? styles.tooltipEnabled : styles.tooltipDisabled, className)}
|
||||
style={{
|
||||
marginTop: (place === 'top' && '-10px') || (place === 'bottom' && '10px'),
|
||||
marginLeft: (place === 'left' && '-10px') || (place === 'right' && '10px'),
|
||||
...style
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export { Tooltip as default }
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
children: propTypes.oneOfType([
|
||||
propTypes.element,
|
||||
propTypes.string
|
||||
]),
|
||||
className: propTypes.string,
|
||||
content: propTypes.node,
|
||||
style: propTypes.object,
|
||||
tagName: propTypes.string
|
||||
})
|
||||
export default class Tooltip extends Component {
|
||||
componentDidMount () {
|
||||
this._addListeners()
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this._removeListeners()
|
||||
}
|
||||
|
||||
componentWillReceiveProps (props) {
|
||||
if (props.children !== this.props.children) {
|
||||
this._removeListeners()
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
if (prevProps.children !== this.props.children) {
|
||||
this._addListeners()
|
||||
}
|
||||
}
|
||||
|
||||
_addListeners () {
|
||||
const node = this._node = ReactDOM.findDOMNode(this)
|
||||
|
||||
node.addEventListener('mouseenter', this._showTooltip)
|
||||
node.addEventListener('mouseleave', this._hideTooltip)
|
||||
node.addEventListener('mousemove', this._updateTooltip)
|
||||
}
|
||||
|
||||
_removeListeners () {
|
||||
const node = this._node
|
||||
|
||||
if (!node) {
|
||||
return
|
||||
}
|
||||
|
||||
node.removeEventListener('mouseenter', this._showTooltip)
|
||||
node.removeEventListener('mouseleave', this._hideTooltip)
|
||||
node.removeEventListener('mousemove', this._updateTooltip)
|
||||
|
||||
this._node = null
|
||||
}
|
||||
|
||||
_showTooltip = () => {
|
||||
const { props } = this
|
||||
|
||||
instance.setState({
|
||||
className: props.className,
|
||||
content: props.content,
|
||||
show: true,
|
||||
style: props.style
|
||||
})
|
||||
}
|
||||
|
||||
_hideTooltip = () => {
|
||||
instance.setState({ show: false })
|
||||
}
|
||||
|
||||
_updateTooltip = event => {
|
||||
const node = ReactDOM.findDOMNode(instance)
|
||||
const result = getPosition(event, event.currentTarget, node, instance.state.place, 'solid', {})
|
||||
|
||||
if (result.isNewState) {
|
||||
return instance.setState(result.newState, () => this._updateTooltip(event))
|
||||
}
|
||||
|
||||
const { position } = result
|
||||
node.style.left = `${position.left}px`
|
||||
node.style.top = `${position.top}px`
|
||||
}
|
||||
|
||||
render () {
|
||||
const { children } = this.props
|
||||
|
||||
if (!children) {
|
||||
return <span />
|
||||
}
|
||||
|
||||
if (isString(children)) {
|
||||
return <span>{children}</span>
|
||||
}
|
||||
|
||||
return children
|
||||
}
|
||||
}
|
||||
|
||||
52
src/common/usage/index.js
Normal file
52
src/common/usage/index.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import _ from 'intl'
|
||||
import classNames from 'classnames'
|
||||
import React, { PropTypes, cloneElement } from 'react'
|
||||
import sum from 'lodash/sum'
|
||||
|
||||
import Tooltip from '../tooltip'
|
||||
|
||||
const Usage = ({ total, children }) => {
|
||||
const limit = total / 400
|
||||
const othersValues = React.Children.map(children, child => {
|
||||
const { value } = child.props
|
||||
return value < limit && value
|
||||
})
|
||||
const othersTotal = sum(othersValues)
|
||||
return <span className='usage'>
|
||||
{React.Children.map(children, (child, index) =>
|
||||
child.props.value > limit && cloneElement(child, { total })
|
||||
)}
|
||||
<Element
|
||||
others
|
||||
tooltip={_('others')}
|
||||
total={total}
|
||||
value={othersTotal}
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
Usage.propTypes = {
|
||||
total: PropTypes.number.isRequired
|
||||
}
|
||||
export { Usage as default }
|
||||
|
||||
const Element = ({ highlight, href, others, tooltip, total, value }) => (
|
||||
<Tooltip content={tooltip}>
|
||||
<a
|
||||
href={href}
|
||||
className={classNames(
|
||||
'usage-element',
|
||||
highlight && 'usage-element-highlight',
|
||||
others && 'usage-element-others'
|
||||
)}
|
||||
style={{ width: (value / total) * 100 + '%' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
)
|
||||
Element.propTypes = {
|
||||
highlight: PropTypes.bool,
|
||||
href: PropTypes.string,
|
||||
others: PropTypes.bool,
|
||||
tooltip: PropTypes.node,
|
||||
value: PropTypes.number.isRequired
|
||||
}
|
||||
export { Element as UsageElement }
|
||||
@@ -1,18 +1,22 @@
|
||||
import * as actions from 'store/actions'
|
||||
import escapeRegExp from 'lodash/escapeRegExp'
|
||||
import every from 'lodash/every'
|
||||
import forEach from 'lodash/forEach'
|
||||
import humanFormat from 'human-format'
|
||||
import includes from 'lodash/includes'
|
||||
import isArray from 'lodash/isArray'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import isFunction from 'lodash/isFunction'
|
||||
import isPlainObject from 'lodash/isPlainObject'
|
||||
import isString from 'lodash/isString'
|
||||
import join from 'lodash/join'
|
||||
import keys from 'lodash/keys'
|
||||
import map from 'lodash/map'
|
||||
import mapValues from 'lodash/mapValues'
|
||||
import propTypes from 'prop-types'
|
||||
import React, { cloneElement } from 'react'
|
||||
import React from 'react'
|
||||
import replace from 'lodash/replace'
|
||||
import store from 'store'
|
||||
import { connect } from 'react-redux'
|
||||
import { getObject } from 'selectors'
|
||||
|
||||
import BaseComponent from './base-component'
|
||||
import invoke from './invoke'
|
||||
@@ -20,8 +24,6 @@ import invoke from './invoke'
|
||||
export const EMPTY_ARRAY = Object.freeze([ ])
|
||||
export const EMPTY_OBJECT = Object.freeze({ })
|
||||
|
||||
export { propTypes }
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const ensureArray = (value) => {
|
||||
@@ -78,92 +80,6 @@ export const addSubscriptions = subscriptions => Component => {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const _bind = (fn, thisArg) => function bound () {
|
||||
return fn.apply(thisArg, arguments)
|
||||
}
|
||||
const _defineProperty = Object.defineProperty
|
||||
|
||||
export const autobind = (target, key, {
|
||||
configurable,
|
||||
enumerable,
|
||||
value: fn,
|
||||
writable
|
||||
}) => ({
|
||||
configurable,
|
||||
enumerable,
|
||||
|
||||
get () {
|
||||
if (this === target) {
|
||||
return fn
|
||||
}
|
||||
|
||||
const bound = _bind(fn, this)
|
||||
|
||||
_defineProperty(this, key, {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
value: bound,
|
||||
writable: true
|
||||
})
|
||||
|
||||
return bound
|
||||
},
|
||||
set (newValue) {
|
||||
// Cannot use assignment because it will call the setter on
|
||||
// the prototype.
|
||||
_defineProperty(this, key, {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
value: newValue,
|
||||
writable: true
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
@propTypes({
|
||||
tagName: propTypes.string
|
||||
})
|
||||
export class BlockLink extends React.Component {
|
||||
static contextTypes = {
|
||||
router: React.PropTypes.object
|
||||
}
|
||||
|
||||
_style = { cursor: 'pointer' }
|
||||
_onClickCapture = event => {
|
||||
const { currentTarget } = event
|
||||
let element = event.target
|
||||
while (element !== currentTarget) {
|
||||
if (includes(['A', 'INPUT', 'BUTTON', 'SELECT'], element.tagName)) {
|
||||
return
|
||||
}
|
||||
element = element.parentNode
|
||||
}
|
||||
event.stopPropagation()
|
||||
if (event.ctrlKey || event.button === 1) {
|
||||
window.open(this.context.router.createHref(this.props.to))
|
||||
} else {
|
||||
this.context.router.push(this.props.to)
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { children, tagName = 'div' } = this.props
|
||||
const Component = tagName
|
||||
return (
|
||||
<Component
|
||||
style={this._style}
|
||||
onClickCapture={this._onClickCapture}
|
||||
>
|
||||
{children}
|
||||
</Component>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export const checkPropsState = (propsNames, stateNames) => Component => {
|
||||
const nProps = propsNames && propsNames.length
|
||||
const nState = stateNames && stateNames.length
|
||||
@@ -255,18 +171,6 @@ export const connectStore = (mapStateToProps, opts = {}) => {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// Simple matcher to use in object filtering.
|
||||
export const createSimpleMatcher = (pattern, valueGetter) => {
|
||||
if (!pattern) {
|
||||
return
|
||||
}
|
||||
|
||||
pattern = pattern.toLowerCase()
|
||||
return item => valueGetter(item).toLowerCase().indexOf(pattern) !== -1
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export { default as Debug } from './debug'
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
@@ -344,24 +248,6 @@ export const osFamily = invoke({
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// Experimental!
|
||||
//
|
||||
// ```js
|
||||
// <If cond={user}>
|
||||
// <p>user.name</p>
|
||||
// <p>user.email</p>
|
||||
// </If>
|
||||
// ```
|
||||
export const If = ({ cond, children }) => cond && children
|
||||
? map(children, (child, key) => cloneElement(child, { key }))
|
||||
: null
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export { invoke }
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export const formatSize = bytes => humanFormat(bytes, { scale: 'binary', unit: 'B' })
|
||||
|
||||
export const formatSizeRaw = bytes => humanFormat.raw(bytes, { scale: 'binary', unit: 'B' })
|
||||
@@ -471,10 +357,63 @@ export function rethrow (cb) {
|
||||
)
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const resolveResourceSets = resourceSets => (
|
||||
map(resourceSets, resourceSet => {
|
||||
const { objects, ...attrs } = resourceSet
|
||||
const resolvedObjects = {}
|
||||
const resolvedSet = {
|
||||
...attrs,
|
||||
missingObjects: [],
|
||||
objectsByType: resolvedObjects
|
||||
}
|
||||
const state = store.getState()
|
||||
|
||||
forEach(objects, id => {
|
||||
const object = getObject(state, id, true) // true: useResourceSet to bypass permissions
|
||||
|
||||
// Error, missing resource.
|
||||
if (!object) {
|
||||
resolvedSet.missingObjects.push(id)
|
||||
return
|
||||
}
|
||||
|
||||
const { type } = object
|
||||
|
||||
if (!resolvedObjects[type]) {
|
||||
resolvedObjects[type] = [ object ]
|
||||
} else {
|
||||
resolvedObjects[type].push(object)
|
||||
}
|
||||
})
|
||||
|
||||
return resolvedSet
|
||||
})
|
||||
)
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// If param is an event: returns the value associated to it
|
||||
// Otherwise: returns param
|
||||
export function getEventValue (param) {
|
||||
return param && param.target ? param.target.value : param
|
||||
// Creates a string replacer based on a pattern and a list of rules
|
||||
//
|
||||
// ```js
|
||||
// const myReplacer = buildTemplate('{name}_COPY_{name}_{id}_%', {
|
||||
// '{name}': vm => vm.name_label,
|
||||
// '{id}': vm => vm.id,
|
||||
// '%': (_, i) => i
|
||||
// })
|
||||
//
|
||||
// const newString = myReplacer({
|
||||
// name_label: 'foo',
|
||||
// id: 42,
|
||||
// }, 32)
|
||||
//
|
||||
// newString === 'foo_COPY_foo_42_32'
|
||||
// ```
|
||||
export function buildTemplate (pattern, rules) {
|
||||
const regExp = new RegExp(join(map(keys(rules), escapeRegExp), '|'), 'g')
|
||||
return (...params) => replace(pattern, regExp, match => {
|
||||
const rule = rules[match]
|
||||
return isFunction(rule) ? rule(...params) : rule
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import _ from 'intl'
|
||||
import classNames from 'classnames'
|
||||
import every from 'lodash/every'
|
||||
import Icon from 'icon'
|
||||
import map from 'lodash/map'
|
||||
import { propTypes } from 'utils'
|
||||
import React, { Component, cloneElement } from 'react'
|
||||
|
||||
import _ from '../intl'
|
||||
import Icon from '../icon'
|
||||
import propTypes from '../prop-types'
|
||||
|
||||
import styles from './index.css'
|
||||
|
||||
const Wizard = ({ children }) => {
|
||||
|
||||
67
src/common/xo-json-schema-input/index.js
Normal file
67
src/common/xo-json-schema-input/index.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import forEach from 'lodash/forEach'
|
||||
|
||||
import XoHighLevelObjectInput from './xo-highlevel-object-input'
|
||||
import XoHostInput from './xo-host-input'
|
||||
import XoPoolInput from './xo-pool-input'
|
||||
import XoRemoteInput from './xo-remote-input'
|
||||
import XoRoleInput from './xo-role-input'
|
||||
import XoSrInput from './xo-sr-input'
|
||||
import XoSubjectInput from './xo-subject-input'
|
||||
import XoVmInput from './xo-vm-input'
|
||||
import { getType, getXoType } from '../json-schema-input/helpers'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const XO_TYPE_TO_COMPONENT = {
|
||||
host: XoHostInput,
|
||||
xoobject: XoHighLevelObjectInput,
|
||||
pool: XoPoolInput,
|
||||
remote: XoRemoteInput,
|
||||
role: XoRoleInput,
|
||||
sr: XoSrInput,
|
||||
subject: XoSubjectInput,
|
||||
vm: XoVmInput
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const buildStringInput = (uiSchema, key, xoType) => {
|
||||
if (key === 'password') {
|
||||
uiSchema.config = { password: true }
|
||||
}
|
||||
|
||||
uiSchema.widget = XO_TYPE_TO_COMPONENT[xoType]
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const _generateUiSchema = (schema, uiSchema, key) => {
|
||||
const type = getType(schema)
|
||||
|
||||
if (type === 'object') {
|
||||
const properties = uiSchema.properties = {}
|
||||
|
||||
forEach(schema.properties, (schema, key) => {
|
||||
const subUiSchema = properties[key] = {}
|
||||
_generateUiSchema(schema, subUiSchema, key)
|
||||
})
|
||||
} else if (type === 'array') {
|
||||
const widget = XO_TYPE_TO_COMPONENT[getXoType(schema.items)]
|
||||
|
||||
if (widget) {
|
||||
uiSchema.widget = widget
|
||||
uiSchema.config = { multi: true }
|
||||
} else {
|
||||
const subUiSchema = uiSchema.items = {}
|
||||
_generateUiSchema(schema.items, subUiSchema, key)
|
||||
}
|
||||
} else if (type === 'string') {
|
||||
buildStringInput(uiSchema, key, getXoType(schema))
|
||||
}
|
||||
}
|
||||
|
||||
export const generateUiSchema = schema => {
|
||||
const uiSchema = {}
|
||||
_generateUiSchema(schema, uiSchema, '')
|
||||
return uiSchema
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import map from 'lodash/map'
|
||||
import AbstractInput from './abstract-input'
|
||||
import AbstractInput from '../json-schema-input/abstract-input'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react'
|
||||
import { SelectHighLevelObject } from 'select-objects'
|
||||
|
||||
import XoAbstractInput from './xo-abstract-input'
|
||||
import { PrimitiveInputWrapper } from './helpers'
|
||||
import { PrimitiveInputWrapper } from '../json-schema-input/helpers'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -14,7 +14,7 @@ export default class HighLevelObjectInput extends XoAbstractInput {
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<SelectHighLevelObject
|
||||
disabled={props.disabled}
|
||||
multi={props.schema.type === 'array'}
|
||||
multi={props.multi}
|
||||
onChange={props.onChange}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
@@ -2,7 +2,7 @@ import React from 'react'
|
||||
import { SelectHost } from 'select-objects'
|
||||
|
||||
import XoAbstractInput from './xo-abstract-input'
|
||||
import { PrimitiveInputWrapper } from './helpers'
|
||||
import { PrimitiveInputWrapper } from '../json-schema-input/helpers'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -14,7 +14,7 @@ export default class HostInput extends XoAbstractInput {
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<SelectHost
|
||||
disabled={props.disabled}
|
||||
multi={props.schema.type === 'array'}
|
||||
multi={props.multi}
|
||||
onChange={props.onChange}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
@@ -2,7 +2,7 @@ import React from 'react'
|
||||
import { SelectPool } from 'select-objects'
|
||||
|
||||
import XoAbstractInput from './xo-abstract-input'
|
||||
import { PrimitiveInputWrapper } from './helpers'
|
||||
import { PrimitiveInputWrapper } from '../json-schema-input/helpers'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -14,7 +14,7 @@ export default class PoolInput extends XoAbstractInput {
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<SelectPool
|
||||
disabled={props.disabled}
|
||||
multi={props.schema.type === 'array'}
|
||||
multi={props.multi}
|
||||
onChange={props.onChange}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
@@ -2,7 +2,7 @@ import React from 'react'
|
||||
import { SelectRemote } from 'select-objects'
|
||||
|
||||
import XoAbstractInput from './xo-abstract-input'
|
||||
import { PrimitiveInputWrapper } from './helpers'
|
||||
import { PrimitiveInputWrapper } from '../json-schema-input/helpers'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -14,7 +14,7 @@ export default class RemoteInput extends XoAbstractInput {
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<SelectRemote
|
||||
disabled={props.disabled}
|
||||
multi={props.schema.type === 'array'}
|
||||
multi={props.multi}
|
||||
onChange={props.onChange}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
@@ -2,7 +2,7 @@ import React from 'react'
|
||||
import { SelectRole } from 'select-objects'
|
||||
|
||||
import XoAbstractInput from './xo-abstract-input'
|
||||
import { PrimitiveInputWrapper } from './helpers'
|
||||
import { PrimitiveInputWrapper } from '../json-schema-input/helpers'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -14,7 +14,7 @@ export default class RoleInput extends XoAbstractInput {
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<SelectRole
|
||||
disabled={props.disabled}
|
||||
multi={props.schema.type === 'array'}
|
||||
multi={props.multi}
|
||||
onChange={props.onChange}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
@@ -2,7 +2,7 @@ import React from 'react'
|
||||
import { SelectSr } from 'select-objects'
|
||||
|
||||
import XoAbstractInput from './xo-abstract-input'
|
||||
import { PrimitiveInputWrapper } from './helpers'
|
||||
import { PrimitiveInputWrapper } from '../json-schema-input/helpers'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -14,7 +14,7 @@ export default class SrInput extends XoAbstractInput {
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<SelectSr
|
||||
disabled={props.disabled}
|
||||
multi={props.schema.type === 'array'}
|
||||
multi={props.multi}
|
||||
onChange={props.onChange}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
@@ -2,7 +2,7 @@ import React from 'react'
|
||||
import { SelectSubject } from 'select-objects'
|
||||
|
||||
import XoAbstractInput from './xo-abstract-input'
|
||||
import { PrimitiveInputWrapper } from './helpers'
|
||||
import { PrimitiveInputWrapper } from '../json-schema-input/helpers'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -14,7 +14,7 @@ export default class SubjectInput extends XoAbstractInput {
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<SelectSubject
|
||||
disabled={props.disabled}
|
||||
multi={props.schema.type === 'array'}
|
||||
multi={props.multi}
|
||||
onChange={props.onChange}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
@@ -2,7 +2,7 @@ import React from 'react'
|
||||
import { SelectVm } from 'select-objects'
|
||||
|
||||
import XoAbstractInput from './xo-abstract-input'
|
||||
import { PrimitiveInputWrapper } from './helpers'
|
||||
import { PrimitiveInputWrapper } from '../json-schema-input/helpers'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -14,7 +14,7 @@ export default class VmInput extends XoAbstractInput {
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<SelectVm
|
||||
disabled={props.disabled}
|
||||
multi={props.schema.type === 'array'}
|
||||
multi={props.multi}
|
||||
onChange={props.onChange}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
4
src/common/xo-line-chart/index.css
Normal file
4
src/common/xo-line-chart/index.css
Normal file
@@ -0,0 +1,4 @@
|
||||
.dashedLine {
|
||||
stroke: black;
|
||||
stroke-dasharray: 4px 5px;
|
||||
}
|
||||
@@ -1,18 +1,32 @@
|
||||
import ChartistGraph from 'react-chartist'
|
||||
import ChartistLegend from 'chartist-plugin-legend'
|
||||
import ChartistTooltip from 'chartist-plugin-tooltip'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import size from 'lodash/size'
|
||||
import values from 'lodash/values'
|
||||
import { injectIntl } from 'react-intl'
|
||||
import {
|
||||
formatSize,
|
||||
propTypes
|
||||
} from 'utils'
|
||||
import find from 'lodash/find'
|
||||
|
||||
import propTypes from '../prop-types'
|
||||
import { computeArraysSum } from '../xo-stats'
|
||||
import { formatSize } from '../utils'
|
||||
|
||||
import styles from './index.css'
|
||||
|
||||
// Number of labels on axis X.
|
||||
const N_LABELS_X = 5
|
||||
|
||||
const LABEL_OFFSET_X = 40
|
||||
const LABEL_OFFSET_Y = 75
|
||||
const LABEL_OFFSET_Y = 85
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// See xo-stats.js, data can be null.
|
||||
// Return the size of the first non-null object.
|
||||
const getStatsLength = stats => size(find(stats, stats => stats != null))
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const makeOptions = ({ intl, nValues, endTimestamp, interval, valueTransform }) => ({
|
||||
showPoint: true,
|
||||
@@ -62,15 +76,29 @@ const makeLabelInterpolationFnc = (intl, nValues, endTimestamp, interval) => {
|
||||
: null
|
||||
}
|
||||
|
||||
const makeObjectSeries = (data, prefix) => {
|
||||
// Supported series: xvds, vifs, pifs.
|
||||
const buildSeries = ({ stats, label, addSumSeries }) => {
|
||||
const series = []
|
||||
|
||||
for (const io in data) {
|
||||
const ioData = data[io]
|
||||
for (const io in stats) {
|
||||
const ioData = stats[io]
|
||||
for (const letter in ioData) {
|
||||
const data = ioData[letter]
|
||||
|
||||
// See xo-stats.js, data can be null.
|
||||
if (data) {
|
||||
series.push({
|
||||
name: `${label}${letter} (${io})`,
|
||||
data
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (addSumSeries) {
|
||||
series.push({
|
||||
name: `${prefix}${letter} (${io})`,
|
||||
data: ioData[letter]
|
||||
name: `All ${io}`,
|
||||
data: computeArraysSum(values(ioData)),
|
||||
className: styles.dashedLine
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -86,22 +114,27 @@ const templateError =
|
||||
// ===================================================================
|
||||
|
||||
export const CpuLineChart = injectIntl(propTypes({
|
||||
addSumSeries: propTypes.bool,
|
||||
data: propTypes.object.isRequired,
|
||||
options: propTypes.object
|
||||
})(({ data, options = {}, intl }) => {
|
||||
})(({ addSumSeries, data, options = {}, intl }) => {
|
||||
const stats = data.stats.cpus
|
||||
const { length } = (stats && stats[0]) || {}
|
||||
const length = getStatsLength(stats)
|
||||
|
||||
if (!length) {
|
||||
return templateError
|
||||
}
|
||||
|
||||
const series = []
|
||||
const series = map(stats, (data, id) => ({
|
||||
name: `Cpu${id}`,
|
||||
data
|
||||
}))
|
||||
|
||||
for (const id in stats) {
|
||||
if (addSumSeries) {
|
||||
series.push({
|
||||
name: `Cpu${id}`,
|
||||
data: stats[id]
|
||||
name: 'All Cpus',
|
||||
data: computeArraysSum(stats),
|
||||
className: styles.dashedLine
|
||||
})
|
||||
}
|
||||
|
||||
@@ -119,7 +152,7 @@ export const CpuLineChart = injectIntl(propTypes({
|
||||
interval: data.interval,
|
||||
valueTransform: value => `${value}%`
|
||||
}),
|
||||
high: 100,
|
||||
high: !addSumSeries ? 100 : stats.length * 100,
|
||||
...options
|
||||
}}
|
||||
/>
|
||||
@@ -164,11 +197,12 @@ export const MemoryLineChart = injectIntl(propTypes({
|
||||
}))
|
||||
|
||||
export const XvdLineChart = injectIntl(propTypes({
|
||||
addSumSeries: propTypes.bool,
|
||||
data: propTypes.object.isRequired,
|
||||
options: propTypes.object
|
||||
})(({ data, options = {}, intl }) => {
|
||||
})(({ addSumSeries, data, options = {}, intl }) => {
|
||||
const stats = data.stats.xvds
|
||||
const { length } = (stats && stats.r.a) || {}
|
||||
const length = stats && getStatsLength(stats.r)
|
||||
|
||||
if (!length) {
|
||||
return templateError
|
||||
@@ -178,7 +212,7 @@ export const XvdLineChart = injectIntl(propTypes({
|
||||
<ChartistGraph
|
||||
type='Line'
|
||||
data={{
|
||||
series: makeObjectSeries(stats, 'Xvd')
|
||||
series: buildSeries({ addSumSeries, stats, label: 'Xvd' })
|
||||
}}
|
||||
options={{
|
||||
...makeOptions({
|
||||
@@ -195,11 +229,12 @@ export const XvdLineChart = injectIntl(propTypes({
|
||||
}))
|
||||
|
||||
export const VifLineChart = injectIntl(propTypes({
|
||||
addSumSeries: propTypes.bool,
|
||||
data: propTypes.object.isRequired,
|
||||
options: propTypes.object
|
||||
})(({ data, options = {}, intl }) => {
|
||||
})(({ addSumSeries, data, options = {}, intl }) => {
|
||||
const stats = data.stats.vifs
|
||||
const { length } = (stats && stats.rx[0]) || {}
|
||||
const length = stats && getStatsLength(stats.rx)
|
||||
|
||||
if (!length) {
|
||||
return templateError
|
||||
@@ -209,7 +244,7 @@ export const VifLineChart = injectIntl(propTypes({
|
||||
<ChartistGraph
|
||||
type='Line'
|
||||
data={{
|
||||
series: makeObjectSeries(stats, 'Vif')
|
||||
series: buildSeries({ addSumSeries, stats, label: 'Vif' })
|
||||
}}
|
||||
options={{
|
||||
...makeOptions({
|
||||
@@ -226,11 +261,12 @@ export const VifLineChart = injectIntl(propTypes({
|
||||
}))
|
||||
|
||||
export const PifLineChart = injectIntl(propTypes({
|
||||
addSumSeries: propTypes.bool,
|
||||
data: propTypes.object.isRequired,
|
||||
options: propTypes.object
|
||||
})(({ data, options = {}, intl }) => {
|
||||
})(({ addSumSeries, data, options = {}, intl }) => {
|
||||
const stats = data.stats.pifs
|
||||
const { length } = (stats && stats.rx[0]) || {}
|
||||
const length = stats && getStatsLength(stats.rx)
|
||||
|
||||
if (!length) {
|
||||
return templateError
|
||||
@@ -240,7 +276,7 @@ export const PifLineChart = injectIntl(propTypes({
|
||||
<ChartistGraph
|
||||
type='Line'
|
||||
data={{
|
||||
series: makeObjectSeries(stats, 'Pif')
|
||||
series: buildSeries({ addSumSeries, stats, label: 'Pif' })
|
||||
}}
|
||||
options={{
|
||||
...makeOptions({
|
||||
@@ -282,7 +318,7 @@ export const LoadLineChart = injectIntl(propTypes({
|
||||
nValues: length,
|
||||
endTimestamp: data.endTimestamp,
|
||||
interval: data.interval,
|
||||
valueTransform: value => `${value}`
|
||||
valueTransform: value => `${value.toPrecision(3)}`
|
||||
}),
|
||||
...options
|
||||
}}
|
||||
294
src/common/xo-parallel-chart.js
Normal file
294
src/common/xo-parallel-chart.js
Normal file
@@ -0,0 +1,294 @@
|
||||
import * as d3 from 'd3'
|
||||
import React from 'react'
|
||||
import forEach from 'lodash/forEach'
|
||||
import keys from 'lodash/keys'
|
||||
import map from 'lodash/map'
|
||||
import times from 'lodash/times'
|
||||
|
||||
import Component from './base-component'
|
||||
import propTypes from './prop-types'
|
||||
import { setStyles } from './d3-utils'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const CHART_WIDTH = 2000
|
||||
const CHART_HEIGHT = 800
|
||||
|
||||
const TICK_SIZE = CHART_WIDTH / 100
|
||||
|
||||
const N_TICKS = 4
|
||||
|
||||
const TOOLTIP_PADDING = 10
|
||||
|
||||
const DEFAULT_STROKE_WIDTH_FACTOR = 500
|
||||
const HIGHLIGHT_STROKE_WIDTH_FACTOR = 200
|
||||
|
||||
const BRUSH_SELECTION_WIDTH = 2 * CHART_WIDTH / 100
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const SVG_STYLE = {
|
||||
display: 'block',
|
||||
height: '100%',
|
||||
left: 0,
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
width: '100%'
|
||||
}
|
||||
|
||||
const SVG_CONTAINER_STYLE = {
|
||||
'padding-bottom': '50%',
|
||||
'vertical-align': 'middle',
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
width: '100%'
|
||||
}
|
||||
|
||||
const SVG_CONTENT = {
|
||||
'font-size': `${CHART_WIDTH / 100}px`
|
||||
}
|
||||
|
||||
const COLUMN_TITLE_STYLE = {
|
||||
'font-size': '100%',
|
||||
'font-weight': 'bold',
|
||||
'text-anchor': 'middle'
|
||||
}
|
||||
|
||||
const COLUMN_VALUES_STYLE = {
|
||||
'font-size': '100%'
|
||||
}
|
||||
|
||||
const LINES_CONTAINER_STYLE = {
|
||||
'stroke-opacity': 0.5,
|
||||
'stroke-width': CHART_WIDTH / DEFAULT_STROKE_WIDTH_FACTOR,
|
||||
fill: 'none',
|
||||
stroke: 'red'
|
||||
}
|
||||
|
||||
const TOOLTIP_STYLE = {
|
||||
'fill': 'white',
|
||||
'font-size': '125%',
|
||||
'font-weight': 'bold'
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
dataSet: propTypes.arrayOf(
|
||||
propTypes.shape({
|
||||
data: propTypes.object.isRequired,
|
||||
label: propTypes.string.isRequired,
|
||||
objectId: propTypes.string.isRequired
|
||||
})
|
||||
).isRequired,
|
||||
labels: propTypes.object.isRequired,
|
||||
renderers: propTypes.object
|
||||
})
|
||||
export default class XoParallelChart extends Component {
|
||||
_line = d3.line()
|
||||
|
||||
_color = d3.scaleOrdinal(d3.schemeCategory10)
|
||||
|
||||
_handleBrush = () => {
|
||||
// 1. Get selected brushes.
|
||||
const brushes = []
|
||||
this._svg.selectAll('.chartColumn')
|
||||
.selectAll('.brush')
|
||||
.each((_1, _2, [ brush ]) => {
|
||||
if (d3.brushSelection(brush) != null) {
|
||||
brushes.push(brush)
|
||||
}
|
||||
})
|
||||
|
||||
// 2. Change stroke of selected lines.
|
||||
const lines = this._svg.select('.linesContainer')
|
||||
.selectAll('path')
|
||||
|
||||
lines.each((elem, lineId, lines) => {
|
||||
const { data } = elem
|
||||
|
||||
const res = brushes.every(brush => {
|
||||
const selection = d3.brushSelection(brush)
|
||||
const columnId = brush.__data__
|
||||
const { invert } = this._y[columnId] // Range to domain.
|
||||
|
||||
return invert(selection[1]) <= data[columnId] && data[columnId] <= invert(selection[0])
|
||||
})
|
||||
|
||||
const line = d3.select(lines[lineId])
|
||||
|
||||
if (!res) {
|
||||
line.attr('stroke-opacity', 1.0).attr('stroke', '#e6e6e6')
|
||||
} else {
|
||||
line.attr('stroke-opacity', 0.5).attr('stroke', this._color(elem.label))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
_brush = d3.brushY()
|
||||
// Brush area: (x0, y0), (x1, y1)
|
||||
.extent([[-BRUSH_SELECTION_WIDTH / 2, 0], [BRUSH_SELECTION_WIDTH / 2, CHART_HEIGHT]])
|
||||
.on('brush', this._handleBrush)
|
||||
.on('end', this._handleBrush)
|
||||
|
||||
_highlight (elem, position) {
|
||||
const svg = this._svg
|
||||
|
||||
// Reset tooltip.
|
||||
svg
|
||||
.selectAll('.objectTooltip')
|
||||
.remove()
|
||||
|
||||
// Reset all lines.
|
||||
svg
|
||||
.selectAll('.chartLine')
|
||||
.attr('stroke-width', CHART_WIDTH / DEFAULT_STROKE_WIDTH_FACTOR)
|
||||
|
||||
if (!position) {
|
||||
return
|
||||
}
|
||||
|
||||
// Set stroke on selected line.
|
||||
svg
|
||||
.select('#chartLine-' + elem.objectId)
|
||||
.attr('stroke-width', CHART_WIDTH / HIGHLIGHT_STROKE_WIDTH_FACTOR)
|
||||
|
||||
const { label } = elem
|
||||
|
||||
const tooltip = svg.append('g')
|
||||
.attr('class', 'objectTooltip')
|
||||
|
||||
const bbox = tooltip.append('text')
|
||||
.text(label)
|
||||
.attr('x', position[0])
|
||||
.attr('y', position[1] - 30)
|
||||
::setStyles(TOOLTIP_STYLE)
|
||||
.node().getBBox()
|
||||
|
||||
tooltip.insert('rect', '*')
|
||||
.attr('x', bbox.x - TOOLTIP_PADDING)
|
||||
.attr('y', bbox.y - TOOLTIP_PADDING)
|
||||
.attr('width', bbox.width + TOOLTIP_PADDING * 2)
|
||||
.attr('height', bbox.height + TOOLTIP_PADDING * 2)
|
||||
.style('fill', this._color(label))
|
||||
}
|
||||
|
||||
_handleMouseOver = (elem, pathId, paths) => {
|
||||
this._highlight(elem, d3.mouse(paths[pathId]))
|
||||
}
|
||||
|
||||
_handleMouseOut = (elem) => {
|
||||
this._highlight()
|
||||
}
|
||||
|
||||
_draw (props = this.props) {
|
||||
const svg = this._svg
|
||||
const { labels, dataSet } = props
|
||||
|
||||
const columnsIds = keys(labels)
|
||||
const spacing = (CHART_WIDTH - 200) / (columnsIds.length - 1)
|
||||
const x = d3.scaleOrdinal()
|
||||
.domain(columnsIds).range(
|
||||
times(columnsIds.length, n => n * spacing)
|
||||
)
|
||||
|
||||
// 1. Remove old nodes.
|
||||
svg
|
||||
.selectAll('.chartColumn')
|
||||
.remove()
|
||||
|
||||
svg
|
||||
.selectAll('.linesContainer')
|
||||
.remove()
|
||||
|
||||
// 2. Build Ys.
|
||||
const y = this._y = {}
|
||||
forEach(columnsIds, (columnId, index) => {
|
||||
const max = d3.max(dataSet, elem => elem.data[columnId])
|
||||
|
||||
y[columnId] = d3.scaleLinear()
|
||||
.domain([0, max])
|
||||
.range([CHART_HEIGHT, 0])
|
||||
})
|
||||
|
||||
// 3. Build columns.
|
||||
const columns = svg.selectAll('.chartColumn')
|
||||
.data(columnsIds)
|
||||
.enter().append('g')
|
||||
.attr('class', 'chartColumn')
|
||||
.attr('transform', d => `translate(${x(d)})`)
|
||||
|
||||
// 4. Draw titles.
|
||||
columns.append('text')
|
||||
.text(columnId => labels[columnId])
|
||||
.attr('y', -50)
|
||||
::setStyles(COLUMN_TITLE_STYLE)
|
||||
|
||||
// 5. Draw axis.
|
||||
columns.append('g')
|
||||
.each((columnId, axisId, axes) => {
|
||||
const axis = d3.axisLeft()
|
||||
.ticks(N_TICKS, ',f')
|
||||
.tickSize(TICK_SIZE)
|
||||
.scale(y[columnId])
|
||||
|
||||
const renderer = props.renderers[columnId]
|
||||
|
||||
// Add optional renderer like formatSize.
|
||||
if (renderer) {
|
||||
axis.tickFormat(renderer)
|
||||
}
|
||||
|
||||
d3.select(axes[axisId]).call(axis)
|
||||
})
|
||||
::setStyles(COLUMN_VALUES_STYLE)
|
||||
|
||||
// 6. Draw lines.
|
||||
const path = elem => this._line(map(columnsIds.map(
|
||||
columnId => [x(columnId), y[columnId](elem.data[columnId])]
|
||||
)))
|
||||
svg.append('g')
|
||||
.attr('class', 'linesContainer')
|
||||
::setStyles(LINES_CONTAINER_STYLE)
|
||||
.selectAll('path')
|
||||
.data(dataSet)
|
||||
.enter().append('path')
|
||||
.attr('d', path)
|
||||
.attr('class', 'chartLine')
|
||||
.attr('id', elem => 'chartLine-' + elem.objectId)
|
||||
.attr('stroke', elem => this._color(elem.label))
|
||||
.attr('shape-rendering', 'optimizeQuality')
|
||||
.attr('stroke-linecap', 'round')
|
||||
.attr('stroke-linejoin', 'round')
|
||||
.on('mouseover', this._handleMouseOver)
|
||||
.on('mouseout', this._handleMouseOut)
|
||||
|
||||
// 7. Brushes.
|
||||
columns.append('g')
|
||||
.attr('class', 'brush')
|
||||
.each((_, brushId, brushes) => { d3.select(brushes[brushId]).call(this._brush) })
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this._svg = d3.select(this.refs.chart)
|
||||
.append('div')
|
||||
::setStyles(SVG_CONTAINER_STYLE)
|
||||
.append('svg')
|
||||
::setStyles(SVG_STYLE)
|
||||
.attr('preserveAspectRatio', 'xMinYMin meet')
|
||||
.attr('viewBox', `0 0 ${CHART_WIDTH} ${CHART_HEIGHT}`)
|
||||
.append('g')
|
||||
.attr('transform', `translate(${100}, ${100})`)
|
||||
::setStyles(SVG_CONTENT)
|
||||
|
||||
this._draw()
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
this._draw(nextProps)
|
||||
}
|
||||
|
||||
render () {
|
||||
return <div ref='chart' />
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,15 @@
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import {
|
||||
Sparklines,
|
||||
SparklinesLine,
|
||||
SparklinesSpots
|
||||
} from 'react-sparklines'
|
||||
import { propTypes } from 'utils'
|
||||
|
||||
import propTypes from './prop-types'
|
||||
import {
|
||||
computeArraysAvg,
|
||||
computeObjectsAvg
|
||||
} from './xo-stats'
|
||||
|
||||
const STYLE = {}
|
||||
|
||||
@@ -14,37 +18,6 @@ const HEIGHT = 40
|
||||
|
||||
// ===================================================================
|
||||
|
||||
function computeArraysAvg (arrays) {
|
||||
if (!arrays || !arrays.length || !arrays[0].length) {
|
||||
return []
|
||||
}
|
||||
|
||||
const n = arrays[0].length
|
||||
const m = arrays.length
|
||||
|
||||
const result = new Array(n)
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
result[i] = 0
|
||||
|
||||
for (let j = 0; j < m; j++) {
|
||||
result[i] += arrays[j][i]
|
||||
}
|
||||
|
||||
result[i] /= m
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function computeObjectsAvg (objects) {
|
||||
return computeArraysAvg(
|
||||
map(objects, object =>
|
||||
computeArraysAvg(map(object, arr => arr))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const templateError =
|
||||
<div>
|
||||
No stats.
|
||||
|
||||
79
src/common/xo-stats.js
Normal file
79
src/common/xo-stats.js
Normal file
@@ -0,0 +1,79 @@
|
||||
// ===================================================================
|
||||
// Tools to manipulate rrd stats
|
||||
// ===================================================================
|
||||
|
||||
import map from 'lodash/map'
|
||||
import values from 'lodash/values'
|
||||
import { mapPlus } from 'utils'
|
||||
|
||||
// Returns a new array with arrays sums.
|
||||
// Example: computeArraysSum([[1, 2], [3, 4], [5, 0]) = [9, 6]
|
||||
const _computeArraysSum = arrays => {
|
||||
if (!arrays || !arrays.length || !arrays[0].length) {
|
||||
return []
|
||||
}
|
||||
|
||||
const n = arrays[0].length // N items in each array
|
||||
const m = arrays.length // M arrays
|
||||
|
||||
const result = new Array(n)
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
result[i] = 0
|
||||
|
||||
for (let j = 0; j < m; j++) {
|
||||
result[i] += arrays[j][i]
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Returns a new array with arrays avgs.
|
||||
// Example: computeArraysAvg([[1, 2], [3, 4], [5, 0]) = [4.5, 2]
|
||||
const _computeArraysAvg = arrays => {
|
||||
const sums = _computeArraysSum(arrays)
|
||||
|
||||
if (!arrays[0]) {
|
||||
return []
|
||||
}
|
||||
const n = arrays && arrays[0].length
|
||||
const m = arrays.length
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
sums[i] /= m
|
||||
}
|
||||
|
||||
return sums
|
||||
}
|
||||
|
||||
// Arrays can be null.
|
||||
// See: https://github.com/vatesfr/xo-web/issues/969
|
||||
//
|
||||
// It's a fix to avoid error like `Uncaught TypeError: Cannot read property 'length' of null`.
|
||||
// FIXME: Repare this bug in xo-server. (Warning: Can break the stats of xo-web v4.)
|
||||
const removeUndefinedArrays = arrays => mapPlus(arrays, (array, push) => {
|
||||
if (array != null) {
|
||||
push(array)
|
||||
}
|
||||
})
|
||||
|
||||
export const computeArraysSum = arrays => _computeArraysSum(removeUndefinedArrays(arrays))
|
||||
export const computeArraysAvg = arrays => _computeArraysAvg(removeUndefinedArrays(arrays))
|
||||
|
||||
// More complex than computeArraysAvg.
|
||||
//
|
||||
// Take in parameter one object like:
|
||||
// { x: { a: [...], b: [...], c: [...] },
|
||||
// y: { d: [...], e: [...], f: [...] } }
|
||||
// and returns the avgs between a, b, c, d, e and f.
|
||||
// Useful for vifs, pifs, xvds.
|
||||
//
|
||||
// Note: The parameter can be also an 3D array.
|
||||
export const computeObjectsAvg = objects => {
|
||||
return _computeArraysAvg(
|
||||
map(objects, object =>
|
||||
computeArraysAvg(values(object))
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -1,19 +1,20 @@
|
||||
import React from 'react'
|
||||
import _ from 'intl'
|
||||
import * as d3 from 'd3'
|
||||
import forEach from 'lodash/forEach'
|
||||
import map from 'lodash/map'
|
||||
import { Toggle } from 'form'
|
||||
|
||||
import Component from '../base-component'
|
||||
import _ from '../intl'
|
||||
import propTypes from '../prop-types'
|
||||
import { Toggle } from '../form'
|
||||
import { setStyles } from '../d3-utils'
|
||||
import {
|
||||
createGetObject,
|
||||
createSelector
|
||||
} from '../selectors'
|
||||
import {
|
||||
connectStore,
|
||||
propsEqual,
|
||||
propTypes
|
||||
propsEqual
|
||||
} from '../utils'
|
||||
|
||||
import styles from './index.css'
|
||||
@@ -63,16 +64,6 @@ const HORIZON_AREA_PATH_STYLE = {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
function setStyles (style) {
|
||||
forEach(style, (value, key) => {
|
||||
this.style(key, value)
|
||||
})
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
chartHeight: propTypes.number,
|
||||
chartWidth: propTypes.number,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from 'react'
|
||||
import _ from 'intl'
|
||||
import forEach from 'lodash/forEach'
|
||||
import map from 'lodash/map'
|
||||
import moment from 'moment'
|
||||
@@ -12,9 +11,10 @@ import {
|
||||
} from 'd3'
|
||||
import { FormattedTime } from 'react-intl'
|
||||
|
||||
import _ from '../intl'
|
||||
import Component from '../base-component'
|
||||
import propTypes from '../prop-types'
|
||||
import Tooltip from '../tooltip'
|
||||
import { propTypes } from '../utils'
|
||||
|
||||
import styles from './index.css'
|
||||
|
||||
@@ -165,15 +165,17 @@ export default class XoWeekHeatmap extends Component {
|
||||
<th><FormattedTime value={day.timestamp} {...DAY_TIME_FORMAT} /></th>
|
||||
{map(day.hours, (hour, key) => (
|
||||
<Tooltip
|
||||
className={styles.cell}
|
||||
key={key}
|
||||
style={{ background: hour ? hour.color : '#ffffff' }}
|
||||
tagName='td'
|
||||
content={hour
|
||||
? _('weekHeatmapData', { date: hour.date, value: this.props.cellRenderer(hour.value) })
|
||||
: _('weekHeatmapNoData')
|
||||
}
|
||||
/>
|
||||
key={key}
|
||||
>
|
||||
<td
|
||||
className={styles.cell}
|
||||
style={{ background: hour ? hour.color : '#ffffff' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
|
||||
61
src/common/xo/add-user-filter-modal/index.js
Normal file
61
src/common/xo/add-user-filter-modal/index.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import keys from 'lodash/keys'
|
||||
import React from 'react'
|
||||
|
||||
import * as FormGrid from '../../form-grid'
|
||||
import _ from '../../intl'
|
||||
import Combobox from '../../combobox'
|
||||
import Component from '../../base-component'
|
||||
import propTypes from '../../prop-types'
|
||||
import { createSelector } from '../../selectors'
|
||||
|
||||
@propTypes({
|
||||
type: propTypes.string.isRequired,
|
||||
user: propTypes.object.isRequired,
|
||||
value: propTypes.string.isRequired
|
||||
})
|
||||
export default class SaveNewUserFilterModalBody extends Component {
|
||||
get value () {
|
||||
return this.state.name || ''
|
||||
}
|
||||
|
||||
_getFilterOptions = createSelector(
|
||||
tmp => (
|
||||
(tmp = this.props.user) &&
|
||||
(tmp = tmp.preferences) &&
|
||||
(tmp = tmp.filters) &&
|
||||
tmp[this.props.type]
|
||||
),
|
||||
keys
|
||||
)
|
||||
|
||||
render () {
|
||||
const { value } = this.props
|
||||
const options = this._getFilterOptions()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FormGrid.Row>
|
||||
<FormGrid.LabelCol>{_('filterName')}</FormGrid.LabelCol>
|
||||
<FormGrid.InputCol>
|
||||
<Combobox
|
||||
onChange={this.linkState('name')}
|
||||
options={options}
|
||||
value={this.state.name || ''}
|
||||
/>
|
||||
</FormGrid.InputCol>
|
||||
</FormGrid.Row>
|
||||
<FormGrid.Row>
|
||||
<FormGrid.LabelCol>{_('filterValue')}</FormGrid.LabelCol>
|
||||
<FormGrid.InputCol>
|
||||
<input
|
||||
className='form-control'
|
||||
disabled
|
||||
type='text'
|
||||
value={value}
|
||||
/>
|
||||
</FormGrid.InputCol>
|
||||
</FormGrid.Row>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
103
src/common/xo/copy-vms-modal/index.js
Normal file
103
src/common/xo/copy-vms-modal/index.js
Normal file
@@ -0,0 +1,103 @@
|
||||
import _, { messages } from 'intl'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import { injectIntl } from 'react-intl'
|
||||
|
||||
import BaseComponent from 'base-component'
|
||||
import SingleLineRow from 'single-line-row'
|
||||
import Upgrade from 'xoa-upgrade'
|
||||
import { Col } from 'grid'
|
||||
import { createGetObjectsOfType } from 'selectors'
|
||||
import { SelectSr } from 'select-objects'
|
||||
import { Toggle } from 'form'
|
||||
import {
|
||||
buildTemplate,
|
||||
connectStore
|
||||
} from 'utils'
|
||||
|
||||
@connectStore(() => {
|
||||
const getVms = createGetObjectsOfType('VM').pick(
|
||||
(_, props) => props.vms
|
||||
)
|
||||
return {
|
||||
vms: getVms
|
||||
}
|
||||
}, { withRef: true })
|
||||
class CopyVmsModalBody extends BaseComponent {
|
||||
get value () {
|
||||
const { state } = this
|
||||
if (!state || !state.sr) {
|
||||
return {}
|
||||
}
|
||||
const { vms } = this.props
|
||||
const { namePattern } = state
|
||||
|
||||
const names = namePattern
|
||||
? map(vms, buildTemplate(namePattern, {
|
||||
'{name}': vm => vm.name_label,
|
||||
'{id}': vm => vm.id
|
||||
}))
|
||||
: map(vms, vm => vm.name_label)
|
||||
return {
|
||||
compress: state.compress,
|
||||
names,
|
||||
sr: state.sr.id
|
||||
}
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.setState({
|
||||
compress: false,
|
||||
namePattern: '{name}_COPY'
|
||||
})
|
||||
}
|
||||
|
||||
_onChangeSr = sr =>
|
||||
this.setState({ sr })
|
||||
_onChangeNamePattern = event =>
|
||||
this.setState({ namePattern: event.target.value })
|
||||
_onChangeCompress = compress =>
|
||||
this.setState({ compress })
|
||||
|
||||
render () {
|
||||
const { formatMessage } = this.props.intl
|
||||
const { compress, namePattern, sr } = this.state
|
||||
return process.env.XOA_PLAN > 2
|
||||
? <div>
|
||||
<SingleLineRow>
|
||||
<Col size={6}>{_('copyVmSelectSr')}</Col>
|
||||
<Col size={6}>
|
||||
<SelectSr
|
||||
onChange={this.linkState('sr')}
|
||||
value={sr}
|
||||
/>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
|
||||
<SingleLineRow>
|
||||
<Col size={6}>{_('copyVmName')}</Col>
|
||||
<Col size={6}>
|
||||
<input
|
||||
className='form-control'
|
||||
onChange={this.linkState('namePattern')}
|
||||
placeholder={formatMessage(messages.copyVmNamePatternPlaceholder)}
|
||||
type='text'
|
||||
value={namePattern}
|
||||
/>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
|
||||
<SingleLineRow>
|
||||
<Col size={6}>{_('copyVmCompress')}</Col>
|
||||
<Col size={6}>
|
||||
<Toggle
|
||||
onChange={this.linkState('compress')}
|
||||
value={compress}
|
||||
/>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
</div>
|
||||
: <div><Upgrade place='vmCopy' available={3} /></div>
|
||||
}
|
||||
}
|
||||
export default injectIntl(CopyVmsModalBody, { withRef: true })
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,11 +7,11 @@
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.firstBlock {
|
||||
.block {
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
|
||||
.block {
|
||||
.groupBlock {
|
||||
padding-bottom: 1em;
|
||||
padding-top: 1em;
|
||||
border-top: 1px solid #e5e5e5;
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import BaseComponent from 'base-component'
|
||||
import forEach from 'lodash/forEach'
|
||||
import find from 'lodash/find'
|
||||
import map from 'lodash/map'
|
||||
import React, { Component } from 'react'
|
||||
import mapValues from 'lodash/mapValues'
|
||||
import React from 'react'
|
||||
|
||||
import _ from '../../intl'
|
||||
import invoke from '../../invoke'
|
||||
import SingleLineRow from '../../single-line-row'
|
||||
import { Col } from '../../grid'
|
||||
import { getDefaultNetworkForVif } from '../utils'
|
||||
import {
|
||||
SelectHost,
|
||||
SelectNetwork,
|
||||
@@ -21,6 +25,8 @@ import {
|
||||
createSelector
|
||||
} from '../../selectors'
|
||||
|
||||
import { isSrWritable } from '../'
|
||||
|
||||
import styles from './index.css'
|
||||
|
||||
@connectStore(() => {
|
||||
@@ -47,19 +53,17 @@ import styles from './index.css'
|
||||
|
||||
const getPifs = createGetObjectsOfType('PIF')
|
||||
const getNetworks = createGetObjectsOfType('network')
|
||||
const getSrs = createGetObjectsOfType('SR')
|
||||
const getPools = createGetObjectsOfType('pool')
|
||||
|
||||
return {
|
||||
networks: getNetworks,
|
||||
pifs: getPifs,
|
||||
pools: getPools,
|
||||
srs: getSrs,
|
||||
vdis: getVdis,
|
||||
vifs: getVifs
|
||||
}
|
||||
}, { withRef: true })
|
||||
export default class MigrateVmModalBody extends Component {
|
||||
export default class MigrateVmModalBody extends BaseComponent {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
@@ -76,8 +80,8 @@ export default class MigrateVmModalBody extends Component {
|
||||
this._getSrPredicate = createSelector(
|
||||
() => this.state.host,
|
||||
host => (host
|
||||
? sr => sr.content_type !== 'iso' && (sr.$container === host.id || sr.$container === host.$pool)
|
||||
: () => false
|
||||
? sr => isSrWritable(sr) && (sr.$container === host.id || sr.$container === host.$pool)
|
||||
: false
|
||||
)
|
||||
)
|
||||
|
||||
@@ -88,7 +92,7 @@ export default class MigrateVmModalBody extends Component {
|
||||
),
|
||||
pifs => {
|
||||
if (!pifs) {
|
||||
return () => false
|
||||
return false
|
||||
}
|
||||
|
||||
const networks = {}
|
||||
@@ -101,129 +105,145 @@ export default class MigrateVmModalBody extends Component {
|
||||
)
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this._selectHost(this.props.host)
|
||||
}
|
||||
|
||||
get value () {
|
||||
return {
|
||||
targetHost: this.state.host && this.state.host.id,
|
||||
mapVdisSrs: this.state.mapVdisSrs,
|
||||
mapVifsNetworks: this.state.mapVifsNetworks,
|
||||
migrationNetwork: this.state.network && this.state.network.id
|
||||
migrationNetwork: this.state.migrationNetworkId
|
||||
}
|
||||
}
|
||||
|
||||
_selectHost = host => {
|
||||
if (!host) {
|
||||
this.setState({ intraPool: undefined, targetHost: undefined })
|
||||
this.setState({
|
||||
host: undefined,
|
||||
intraPool: undefined
|
||||
})
|
||||
return
|
||||
}
|
||||
const { networks, pools, pifs, srs, vdis, vifs } = this.props
|
||||
const defaultMigrationNetwork = networks[find(pifs, pif => pif.$host === host.id && pif.management).$network]
|
||||
const defaultSr = srs[pools[host.$pool].default_SR]
|
||||
const defaultNetworks = {}
|
||||
// Default network...
|
||||
forEach(vifs, vif => {
|
||||
// ...is the one which has the same name_label as the VIF's previous network (if it has an IP)...
|
||||
const defaultPif = find(host.$PIFs, pifId => {
|
||||
const pif = pifs[pifId]
|
||||
return pif.ip && networks[vif.$network].name_label === networks[pif.$network].name_label
|
||||
const intraPool = this.props.vm.$pool === host.$pool
|
||||
if (intraPool) {
|
||||
this.setState({
|
||||
host,
|
||||
intraPool,
|
||||
mapVdisSrs: undefined,
|
||||
mapVifsNetworks: undefined,
|
||||
migrationNetwork: undefined
|
||||
})
|
||||
defaultNetworks[vif.id] = defaultPif && networks[defaultPif.$network]
|
||||
// ...or the first network in the target host networks list that has an IP.
|
||||
if (!defaultNetworks[vif.id]) {
|
||||
defaultNetworks[vif.id] = networks[pifs[find(host.$PIFs, pif => pifs[pif].ip)].$network]
|
||||
}
|
||||
return
|
||||
}
|
||||
const { networks, pools, pifs, vdis, vifs } = this.props
|
||||
const defaultMigrationNetworkId = find(pifs, pif => pif.$host === host.id && pif.management).$network
|
||||
const defaultSr = pools[host.$pool].default_SR
|
||||
|
||||
const defaultNetwork = invoke(() => {
|
||||
// First PIF with an IP.
|
||||
const pifId = find(host.$PIFs, pif => pifs[pif].ip)
|
||||
const pif = pifId && pifs[pifId]
|
||||
|
||||
return pif && pif.$network
|
||||
})
|
||||
|
||||
const defaultNetworksForVif = {}
|
||||
forEach(vifs, vif => {
|
||||
defaultNetworksForVif[vif.id] = (
|
||||
getDefaultNetworkForVif(vif, host, pifs, networks) ||
|
||||
defaultNetwork
|
||||
)
|
||||
})
|
||||
|
||||
this.setState({
|
||||
network: defaultMigrationNetwork,
|
||||
defaultNetworks,
|
||||
defaultSr,
|
||||
host,
|
||||
intraPool: this.props.vm.$pool === host.$pool
|
||||
}, () => {
|
||||
if (!this.state.intraPool) {
|
||||
this.refs.network.value = defaultMigrationNetwork
|
||||
forEach(vdis, vdi => {
|
||||
this.refs['sr_' + vdi.id].value = defaultSr
|
||||
})
|
||||
forEach(vifs, vif => {
|
||||
this.refs['network_' + vif.id].value = defaultNetworks[vif.id]
|
||||
})
|
||||
}
|
||||
intraPool,
|
||||
mapVdisSrs: mapValues(vdis, vdi => defaultSr),
|
||||
mapVifsNetworks: defaultNetworksForVif,
|
||||
migrationNetworkId: defaultMigrationNetworkId
|
||||
})
|
||||
}
|
||||
|
||||
_selectMigrationNetwork = network => this.setState({ network })
|
||||
_selectMigrationNetwork = migrationNetwork => this.setState({ migrationNetworkId: migrationNetwork.id })
|
||||
|
||||
render () {
|
||||
const { vdis, vifs, networks } = this.props
|
||||
const {
|
||||
host,
|
||||
intraPool,
|
||||
mapVdisSrs,
|
||||
mapVifsNetworks,
|
||||
migrationNetworkId
|
||||
} = this.state
|
||||
return <div>
|
||||
<div className={styles.firstBlock}>
|
||||
<div className={styles.block}>
|
||||
<SingleLineRow>
|
||||
<Col size={6}>{_('migrateVmAdvancedModalSelectHost')}</Col>
|
||||
<Col size={6}>{_('migrateVmSelectHost')}</Col>
|
||||
<Col size={6}>
|
||||
<SelectHost
|
||||
onChange={this._selectHost}
|
||||
predicate={this._getHostPredicate()}
|
||||
value={host}
|
||||
/>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
</div>
|
||||
{this.state.intraPool !== undefined &&
|
||||
(!this.state.intraPool &&
|
||||
{intraPool !== undefined &&
|
||||
(!intraPool &&
|
||||
<div>
|
||||
<div className={styles.block}>
|
||||
<div className={styles.groupBlock}>
|
||||
<SingleLineRow>
|
||||
<Col size={6}>{_('migrateVmAdvancedModalSelectNetwork')}</Col>
|
||||
<Col size={6}>{_('migrateVmSelectMigrationNetwork')}</Col>
|
||||
<Col size={6}>
|
||||
<SelectNetwork
|
||||
ref='network'
|
||||
defaultValue={this.state.network}
|
||||
onChange={this._selectMigrationNetwork}
|
||||
predicate={this._getNetworkPredicate()}
|
||||
value={migrationNetworkId}
|
||||
/>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
</div>
|
||||
<div className={styles.block}>
|
||||
<div className={styles.groupBlock}>
|
||||
<SingleLineRow>
|
||||
<Col>{_('migrateVmAdvancedModalSelectSrs')}</Col>
|
||||
<Col>{_('migrateVmSelectSrs')}</Col>
|
||||
</SingleLineRow>
|
||||
<br />
|
||||
<SingleLineRow>
|
||||
<Col size={6}><span className={styles.listTitle}>{_('migrateVmAdvancedModalName')}</span></Col>
|
||||
<Col size={6}><span className={styles.listTitle}>{_('migrateVmAdvancedModalSr')}</span></Col>
|
||||
<Col size={6}><span className={styles.listTitle}>{_('migrateVmName')}</span></Col>
|
||||
<Col size={6}><span className={styles.listTitle}>{_('migrateVmSr')}</span></Col>
|
||||
</SingleLineRow>
|
||||
{map(vdis, vdi => <div className={styles.listItem} key={vdi.id}>
|
||||
<SingleLineRow>
|
||||
<Col size={6}>{vdi.name_label}</Col>
|
||||
<Col size={6}>
|
||||
<SelectSr
|
||||
ref={'sr_' + vdi.id}
|
||||
defaultValue={this.state.defaultSr}
|
||||
onChange={sr => this.setState({ mapVdisSrs: { ...this.state.mapVdisSrs, [vdi.id]: sr.id } })}
|
||||
onChange={sr => this.setState({ mapVdisSrs: { ...mapVdisSrs, [vdi.id]: sr.id } })}
|
||||
predicate={this._getSrPredicate()}
|
||||
value={mapVdisSrs[vdi.id]}
|
||||
/>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
</div>)}
|
||||
</div>
|
||||
<div className={styles.block}>
|
||||
<div className={styles.groupBlock}>
|
||||
<SingleLineRow>
|
||||
<Col>{_('migrateVmAdvancedModalSelectNetworks')}</Col>
|
||||
<Col>{_('migrateVmSelectNetworks')}</Col>
|
||||
</SingleLineRow>
|
||||
<br />
|
||||
<SingleLineRow>
|
||||
<Col size={6}><span className={styles.listTitle}>{_('migrateVmAdvancedModalVif')}</span></Col>
|
||||
<Col size={6}><span className={styles.listTitle}>{_('migrateVmAdvancedModalNetwork')}</span></Col>
|
||||
<Col size={6}><span className={styles.listTitle}>{_('migrateVmVif')}</span></Col>
|
||||
<Col size={6}><span className={styles.listTitle}>{_('migrateVmNetwork')}</span></Col>
|
||||
</SingleLineRow>
|
||||
{map(vifs, vif => <div className={styles.listItem} key={vif.id}>
|
||||
<SingleLineRow>
|
||||
<Col size={6}>{vif.MAC} ({networks[vif.$network].name_label})</Col>
|
||||
<Col size={6}>
|
||||
<SelectNetwork
|
||||
ref={'network_' + vif.id}
|
||||
defaultValue={this.state.defaultNetworks[vif.id]}
|
||||
onChange={network => this.setState({ mapVifsNetworks: { ...this.state.mapVifsNetworks, [vif.id]: network.id } })}
|
||||
onChange={network => this.setState({ mapVifsNetworks: { ...mapVifsNetworks, [vif.id]: network.id } })}
|
||||
predicate={this._getNetworkPredicate()}
|
||||
value={mapVifsNetworks[vif.id]}
|
||||
/>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
|
||||
309
src/common/xo/migrate-vms-modal/index.js
Normal file
309
src/common/xo/migrate-vms-modal/index.js
Normal file
@@ -0,0 +1,309 @@
|
||||
import BaseComponent from 'base-component'
|
||||
import every from 'lodash/every'
|
||||
import flatten from 'lodash/flatten'
|
||||
import forEach from 'lodash/forEach'
|
||||
import filter from 'lodash/filter'
|
||||
import find from 'lodash/find'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import map from 'lodash/map'
|
||||
import mapValues from 'lodash/mapValues'
|
||||
import React from 'react'
|
||||
import some from 'lodash/some'
|
||||
import store from 'store'
|
||||
|
||||
import _ from '../../intl'
|
||||
import invoke from '../../invoke'
|
||||
import SingleLineRow from '../../single-line-row'
|
||||
import { Col } from '../../grid'
|
||||
import { getDefaultNetworkForVif } from '../utils'
|
||||
import {
|
||||
SelectHost,
|
||||
SelectNetwork,
|
||||
SelectSr
|
||||
} from '../../select-objects'
|
||||
import {
|
||||
connectStore
|
||||
} from '../../utils'
|
||||
import {
|
||||
createGetObjectsOfType,
|
||||
createPicker,
|
||||
createSelector,
|
||||
getObject
|
||||
} from '../../selectors'
|
||||
import {
|
||||
isSrShared
|
||||
} from 'xo'
|
||||
|
||||
import { isSrWritable } from '../'
|
||||
|
||||
const LINE_STYLE = { paddingBottom: '1em' }
|
||||
|
||||
@connectStore(() => {
|
||||
const getNetworks = createGetObjectsOfType('network')
|
||||
const getPifs = createGetObjectsOfType('PIF')
|
||||
const getPools = createGetObjectsOfType('pool')
|
||||
|
||||
const getVms = createGetObjectsOfType('VM').pick(
|
||||
(_, props) => props.vms
|
||||
)
|
||||
|
||||
const getVbdsByVm = createGetObjectsOfType('VBD').pick(
|
||||
createSelector(
|
||||
getVms,
|
||||
vms => flatten(map(vms, vm => vm.$VBDs))
|
||||
)
|
||||
).groupBy('VM')
|
||||
|
||||
const getVifsByVM = createGetObjectsOfType('VIF').pick(
|
||||
createSelector(
|
||||
getVms,
|
||||
vms => flatten(map(vms, vm => vm.VIFs))
|
||||
)
|
||||
).groupBy('$VM')
|
||||
|
||||
return {
|
||||
networks: getNetworks,
|
||||
pifs: getPifs,
|
||||
pools: getPools,
|
||||
vbdsByVm: getVbdsByVm,
|
||||
vifsByVm: getVifsByVM,
|
||||
vms: getVms
|
||||
}
|
||||
}, { withRef: true })
|
||||
export default class MigrateVmsModalBody extends BaseComponent {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this._getHostPredicate = createSelector(
|
||||
() => this.props.vms,
|
||||
vms => host => some(vms, vm => host.id !== vm.$container)
|
||||
)
|
||||
|
||||
this._getSrPredicate = createSelector(
|
||||
() => this.state.host,
|
||||
host => (host
|
||||
? sr => isSrWritable(sr) && (sr.$container === host.id || sr.$container === host.$pool)
|
||||
: false
|
||||
)
|
||||
)
|
||||
|
||||
this._getNetworkPredicate = createSelector(
|
||||
createPicker(
|
||||
() => this.props.pifs,
|
||||
() => this.state.host.$PIFs
|
||||
),
|
||||
pifs => {
|
||||
if (!pifs) {
|
||||
return false
|
||||
}
|
||||
|
||||
const networks = {}
|
||||
forEach(pifs, pif => {
|
||||
pif.ip && (networks[pif.$network] = true)
|
||||
})
|
||||
|
||||
return network => networks[network.id]
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this._selectHost(this.props.host)
|
||||
}
|
||||
|
||||
get value () {
|
||||
const { host } = this.state
|
||||
const vms = filter(this.props.vms, vm => vm.$container !== host.id)
|
||||
if (!host || isEmpty(vms)) {
|
||||
return { vms }
|
||||
}
|
||||
const {
|
||||
networks,
|
||||
pifs,
|
||||
vbdsByVm,
|
||||
vifsByVm
|
||||
} = this.props
|
||||
const {
|
||||
intraPool,
|
||||
doNotMigrateVdi,
|
||||
doNotMigrateVmVdis,
|
||||
migrationNetworkId,
|
||||
networkId,
|
||||
smartVifMapping,
|
||||
srId
|
||||
} = this.state
|
||||
|
||||
// Map VM --> ( Map VDI --> SR )
|
||||
const mapVmsMapVdisSrs = {}
|
||||
forEach(vbdsByVm, (vbds, vm) => {
|
||||
if (doNotMigrateVmVdis[vm]) {
|
||||
return
|
||||
}
|
||||
const mapVdisSrs = {}
|
||||
forEach(vbds, vbd => {
|
||||
const vdi = vbd.VDI
|
||||
if (!vbd.is_cd_drive && vdi) {
|
||||
mapVdisSrs[vdi] = intraPool && doNotMigrateVdi[vdi] ? this._getObject(vdi).SR : srId
|
||||
}
|
||||
})
|
||||
mapVmsMapVdisSrs[vm] = mapVdisSrs
|
||||
})
|
||||
|
||||
const defaultNetwork = smartVifMapping && invoke(() => {
|
||||
// First PIF with an IP.
|
||||
const pifId = find(host.$PIFs, pif => pifs[pif].ip)
|
||||
const pif = pifId && pifs[pifId]
|
||||
|
||||
return pif && pif.$network
|
||||
})
|
||||
|
||||
// Map VM --> ( Map VIF --> network )
|
||||
const mapVmsMapVifsNetworks = {}
|
||||
forEach(vms, vm => {
|
||||
if (vm.$pool === host.$pool) {
|
||||
return
|
||||
}
|
||||
const mapVifsNetworks = {}
|
||||
forEach(vifsByVm[vm.id], vif => {
|
||||
mapVifsNetworks[vif.id] = smartVifMapping
|
||||
? getDefaultNetworkForVif(vif, host, pifs, networks) || defaultNetwork
|
||||
: networkId
|
||||
})
|
||||
mapVmsMapVifsNetworks[vm.id] = mapVifsNetworks
|
||||
})
|
||||
|
||||
// Map VM --> migration network
|
||||
const mapVmsMigrationNetwork = mapValues(doNotMigrateVmVdis, doNotMigrateVdis =>
|
||||
doNotMigrateVdis ? undefined : migrationNetworkId
|
||||
)
|
||||
|
||||
return {
|
||||
mapVmsMapVdisSrs,
|
||||
mapVmsMapVifsNetworks,
|
||||
mapVmsMigrationNetwork,
|
||||
targetHost: host.id,
|
||||
vms
|
||||
}
|
||||
}
|
||||
|
||||
_getObject (id) {
|
||||
return getObject(store.getState(), id)
|
||||
}
|
||||
|
||||
_selectHost = host => {
|
||||
if (!host) {
|
||||
this.setState({ targetHost: undefined })
|
||||
return
|
||||
}
|
||||
const { pools, pifs } = this.props
|
||||
const defaultMigrationNetworkId = find(pifs, pif => pif.$host === host.id && pif.management).$network
|
||||
const defaultSrId = pools[host.$pool].default_SR
|
||||
const doNotMigrateVmVdis = {}
|
||||
const doNotMigrateVdi = {}
|
||||
forEach(this.props.vbdsByVm, (vbds, vm) => {
|
||||
if (this._getObject(vm).$container === host.id) {
|
||||
doNotMigrateVmVdis[vm] = true
|
||||
return
|
||||
}
|
||||
const _doNotMigrateVdi = {}
|
||||
forEach(vbds, vbd => {
|
||||
if (vbd.VDI != null) {
|
||||
doNotMigrateVdi[vbd.VDI] = _doNotMigrateVdi[vbd.VDI] = isSrShared(this._getObject(this._getObject(vbd.VDI).$SR))
|
||||
}
|
||||
})
|
||||
doNotMigrateVmVdis[vm] = every(_doNotMigrateVdi)
|
||||
})
|
||||
const noVdisMigration = every(doNotMigrateVmVdis)
|
||||
this.setState({
|
||||
host,
|
||||
intraPool: every(this.props.vms, vm => vm.$pool === host.$pool),
|
||||
doNotMigrateVdi,
|
||||
doNotMigrateVmVdis,
|
||||
migrationNetworkId: defaultMigrationNetworkId,
|
||||
networkId: defaultMigrationNetworkId,
|
||||
noVdisMigration,
|
||||
smartVifMapping: true,
|
||||
srId: defaultSrId
|
||||
})
|
||||
}
|
||||
_selectMigrationNetwork = migrationNetwork => this.setState({ migrationNetworkId: migrationNetwork.id })
|
||||
_selectNetwork = network => this.setState({ networkId: network.id })
|
||||
_selectSr = sr => this.setState({ srId: sr.id })
|
||||
_toggleSmartVifMapping = () => this.setState({ smartVifMapping: !this.state.smartVifMapping })
|
||||
|
||||
render () {
|
||||
const {
|
||||
host,
|
||||
intraPool,
|
||||
migrationNetworkId,
|
||||
networkId,
|
||||
noVdisMigration,
|
||||
smartVifMapping,
|
||||
srId
|
||||
} = this.state
|
||||
return <div>
|
||||
<div style={LINE_STYLE}>
|
||||
<SingleLineRow>
|
||||
<Col size={6}>{_('migrateVmSelectHost')}</Col>
|
||||
<Col size={6}>
|
||||
<SelectHost
|
||||
onChange={this._selectHost}
|
||||
predicate={this._getHostPredicate()}
|
||||
value={host}
|
||||
/>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
</div>
|
||||
{intraPool === false &&
|
||||
<div style={LINE_STYLE}>
|
||||
<SingleLineRow>
|
||||
<Col size={6}>{_('migrateVmSelectMigrationNetwork')}</Col>
|
||||
<Col size={6}>
|
||||
<SelectNetwork
|
||||
onChange={this._selectMigrationNetwork}
|
||||
predicate={this._getNetworkPredicate()}
|
||||
value={migrationNetworkId}
|
||||
/>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
</div>
|
||||
}
|
||||
{host && (!intraPool || !noVdisMigration) &&
|
||||
<div key='sr' style={LINE_STYLE}>
|
||||
<SingleLineRow>
|
||||
<Col size={6}>{!intraPool ? _('migrateVmsSelectSr') : _('migrateVmsSelectSrIntraPool')}</Col>
|
||||
<Col size={6}>
|
||||
<SelectSr
|
||||
onChange={this._selectSr}
|
||||
predicate={this._getSrPredicate()}
|
||||
value={srId}
|
||||
/>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
</div>
|
||||
}
|
||||
{host && !intraPool &&
|
||||
<div key='network' style={LINE_STYLE}>
|
||||
<SingleLineRow>
|
||||
<Col size={6}>{_('migrateVmsSelectNetwork')}</Col>
|
||||
<Col size={6}>
|
||||
<SelectNetwork
|
||||
disabled={smartVifMapping}
|
||||
onChange={this._selectNetwork}
|
||||
predicate={this._getNetworkPredicate()}
|
||||
value={networkId}
|
||||
/>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
<SingleLineRow>
|
||||
<Col size={6} offset={6}>
|
||||
<input type='checkbox' onChange={this._toggleSmartVifMapping} checked={smartVifMapping} />
|
||||
{' '}
|
||||
{_('migrateVmsSmartMapping')}
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
58
src/common/xo/new-ssh-key-modal/index.js
Normal file
58
src/common/xo/new-ssh-key-modal/index.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import BaseComponent from 'base-component'
|
||||
import React from 'react'
|
||||
|
||||
import _ from '../../intl'
|
||||
import SingleLineRow from '../../single-line-row'
|
||||
import { Col } from '../../grid'
|
||||
import getEventValue from '../../get-event-value'
|
||||
|
||||
export default class NewSshKeyModalBody extends BaseComponent {
|
||||
get value () {
|
||||
return this.state
|
||||
}
|
||||
|
||||
_onKeyChange = event => {
|
||||
const key = getEventValue(event)
|
||||
const splitKey = key.split(' ')
|
||||
if (!this.state.title && splitKey.length === 3) {
|
||||
this.setState({ title: splitKey[2].split('\n')[0] })
|
||||
}
|
||||
this.setState({ key })
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
key,
|
||||
title
|
||||
} = this.state
|
||||
|
||||
return <div>
|
||||
<div className='p-b-1'>
|
||||
<SingleLineRow>
|
||||
<Col size={4}>{_('title')}</Col>
|
||||
<Col size={8}>
|
||||
<input
|
||||
className='form-control'
|
||||
onChange={this.linkState('title')}
|
||||
type='text'
|
||||
value={title || ''}
|
||||
/>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
</div>
|
||||
<div className='p-b-1'>
|
||||
<SingleLineRow>
|
||||
<Col size={4}>{_('key')}</Col>
|
||||
<Col size={8}>
|
||||
<textarea
|
||||
className='form-control'
|
||||
onChange={this._onKeyChange}
|
||||
rows={10}
|
||||
value={key || ''}
|
||||
/>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
14
src/common/xo/utils.js
Normal file
14
src/common/xo/utils.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import forEach from 'lodash/forEach'
|
||||
|
||||
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 (pif.ip && networks[pif.$network].name_label === nameLabel) {
|
||||
defaultNetwork = pif.$network
|
||||
return false
|
||||
}
|
||||
})
|
||||
return defaultNetwork
|
||||
}
|
||||
@@ -98,8 +98,8 @@ class XoaUpdater extends EventEmitter {
|
||||
await this._update(true)
|
||||
}
|
||||
|
||||
_promptForReload () {
|
||||
this.emit('promptForReload')
|
||||
_upgradeSuccessful () {
|
||||
this.emit('upgradeSuccessful')
|
||||
}
|
||||
|
||||
async _open () {
|
||||
@@ -158,7 +158,7 @@ class XoaUpdater extends EventEmitter {
|
||||
if (this._lowState.state === 'updater-upgraded' || this._lowState.state === 'installer-upgraded') {
|
||||
this.update()
|
||||
} else if (this._lowState.state === 'xoa-upgraded') {
|
||||
this._promptForReload()
|
||||
this._upgradeSuccessful()
|
||||
}
|
||||
this.xoaState()
|
||||
})
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import _ from 'intl'
|
||||
import Icon from 'icon'
|
||||
import Link from 'react-router/lib/Link'
|
||||
import React from 'react'
|
||||
import { Card, CardHeader, CardBlock } from 'card'
|
||||
import { getXoaPlan, propTypes } from 'utils'
|
||||
|
||||
import _ from './intl'
|
||||
import Icon from './icon'
|
||||
import Link from './link'
|
||||
import propTypes from './prop-types'
|
||||
import { Card, CardHeader, CardBlock } from './card'
|
||||
import { getXoaPlan } from './utils'
|
||||
|
||||
const Upgrade = propTypes({
|
||||
available: propTypes.number.isRequired,
|
||||
|
||||
@@ -58,6 +58,10 @@
|
||||
@extend .fa;
|
||||
@extend .fa-wrench;
|
||||
}
|
||||
&-diagnosis {
|
||||
@extend .fa;
|
||||
@extend .fa-medkit;
|
||||
}
|
||||
&-chevron-up {
|
||||
@extend .fa;
|
||||
@extend .fa-chevron-up;
|
||||
@@ -108,6 +112,10 @@
|
||||
@extend .fa;
|
||||
@extend .fa-play;
|
||||
}
|
||||
&-ssh-key {
|
||||
@extend .fa;
|
||||
@extend .fa-key;
|
||||
}
|
||||
|
||||
&-shown {
|
||||
@extend .fa;
|
||||
@@ -140,11 +148,11 @@
|
||||
|
||||
&-asc {
|
||||
@extend .fa;
|
||||
@extend .fa-arrow-up;
|
||||
@extend .fa-arrow-down;
|
||||
}
|
||||
&-desc {
|
||||
@extend .fa;
|
||||
@extend .fa-arrow-down;
|
||||
@extend .fa-arrow-up;
|
||||
}
|
||||
&-sort {
|
||||
@extend .fa;
|
||||
@@ -343,6 +351,18 @@
|
||||
@extend .xo-status-busy;
|
||||
}
|
||||
|
||||
// Task
|
||||
&-task {
|
||||
&-cancel {
|
||||
@extend .fa;
|
||||
@extend .fa-ban;
|
||||
}
|
||||
&-destroy {
|
||||
@extend .fa;
|
||||
@extend .fa-trash;
|
||||
}
|
||||
}
|
||||
|
||||
// SR
|
||||
&-sr, &-vdi {
|
||||
&-reconnect-all {
|
||||
@@ -521,6 +541,10 @@
|
||||
@extend .fa;
|
||||
@extend .fa-clock-o;
|
||||
}
|
||||
&-time {
|
||||
@extend .fa;
|
||||
@extend .fa-clock-o;
|
||||
}
|
||||
&-database {
|
||||
@extend .fa;
|
||||
@extend .fa-database;
|
||||
@@ -827,6 +851,10 @@
|
||||
@extend .fa;
|
||||
@extend .fa-server;
|
||||
}
|
||||
&-connect {
|
||||
@extend .fa;
|
||||
@extend .fa-link;
|
||||
}
|
||||
&-disconnect {
|
||||
@extend .fa;
|
||||
@extend .fa-unlink;
|
||||
|
||||
@@ -21,6 +21,8 @@ html.no-js(
|
||||
//- .visible-js to display content only when JavaScript is ENABLED.
|
||||
//- .hidden-js to display content only when JavaScript is DISABLED.
|
||||
script !function(d){d.className=d.className.replace(/\bno-js\b/,'js')}(document.documentElement)
|
||||
script(src = 'https://cdn.polyfill.io/v2/polyfill.min.js?features=Intl.~locale.en')
|
||||
|
||||
style .no-js .visible-js,.js .hidden-js{display:none}
|
||||
|
||||
//- (TODO: confirm) For smartphones and tablets: sets the page
|
||||
|
||||
@@ -202,27 +202,9 @@ $select-input-height: 40px; // Bootstrap input height
|
||||
background: $gray-lighter;
|
||||
}
|
||||
|
||||
// DASHBOARD STYLE =============================================================
|
||||
|
||||
.card-dashboard {
|
||||
@extend .card;
|
||||
}
|
||||
|
||||
.card-header-dashboard {
|
||||
@extend .card-header;
|
||||
font-size: 1.2em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.card-block-dashboard {
|
||||
@extend .card-block;
|
||||
font-size: 4em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
// MEMORY/DISK BAR STYLE =======================================================
|
||||
|
||||
.progress-usage {
|
||||
.usage {
|
||||
@extend .progress;
|
||||
background-color: #eee;
|
||||
height: 2em;
|
||||
@@ -231,22 +213,24 @@ $select-input-height: 40px; // Bootstrap input height
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
.progress-dom0 {
|
||||
display: inline-block;
|
||||
background-color: #337ab7;
|
||||
height: 2em;
|
||||
}
|
||||
|
||||
.progress-object {
|
||||
.usage-element {
|
||||
background-color: #5cb85c;
|
||||
box-shadow: -1px 0 0 0 white;
|
||||
height: 2em;
|
||||
margin-right: 0px;
|
||||
display: inline-block;
|
||||
transition: all 0.3s ease 0s;
|
||||
}
|
||||
|
||||
.progress-object:hover {
|
||||
opacity: 0.5;
|
||||
.usage-element-highlight {
|
||||
background-color: $brand-primary;
|
||||
}
|
||||
|
||||
.usage-element-others {
|
||||
background-color: $brand-info;
|
||||
}
|
||||
|
||||
.usage-element:hover {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
// NOTIFICATIONS STYLE =========================================================
|
||||
|
||||
@@ -2,7 +2,7 @@ import _ from 'intl'
|
||||
import Component from 'base-component'
|
||||
import Copiable from 'copiable'
|
||||
import Icon from 'icon'
|
||||
import Link from 'react-router/lib/Link'
|
||||
import Link from 'link'
|
||||
import Page from '../page'
|
||||
import React from 'react'
|
||||
import { getUser } from 'selectors'
|
||||
@@ -33,7 +33,7 @@ export default class About extends Component {
|
||||
const { user } = this.props
|
||||
const isAdmin = user && user.permission === 'admin'
|
||||
|
||||
return <Page header={HEADER}>
|
||||
return <Page header={HEADER} title='aboutPage' formatTitle>
|
||||
<Container className='text-xs-center'>
|
||||
{isAdmin && <Row>
|
||||
<Col mediumSize={6}>
|
||||
|
||||
@@ -32,7 +32,7 @@ const Backup = routes('overview', {
|
||||
overview: Overview,
|
||||
restore: Restore
|
||||
})(
|
||||
({ children }) => <Page header={HEADER}>{children}</Page>
|
||||
({ children }) => <Page header={HEADER} title='backupPage' formatTitle>{children}</Page>
|
||||
)
|
||||
|
||||
export default Backup
|
||||
|
||||
@@ -11,13 +11,14 @@ import Upgrade from 'xoa-upgrade'
|
||||
import Wizard, { Section } from 'wizard'
|
||||
import { Container } from 'grid'
|
||||
import { error } from 'notification'
|
||||
import { generateUiSchema } from 'xo-json-schema-input'
|
||||
import { injectIntl } from 'react-intl'
|
||||
|
||||
import {
|
||||
createJob,
|
||||
createSchedule,
|
||||
setJob,
|
||||
setSchedule
|
||||
updateSchedule
|
||||
} from 'xo'
|
||||
|
||||
import { getJobValues } from '../helpers'
|
||||
@@ -34,7 +35,10 @@ const COMMON_SCHEMA = {
|
||||
},
|
||||
vms: {
|
||||
type: 'array',
|
||||
'xo:type': 'vm',
|
||||
items: {
|
||||
type: 'string',
|
||||
'xo:type': 'vm'
|
||||
},
|
||||
title: 'VMs',
|
||||
description: 'Choose VMs to backup.'
|
||||
},
|
||||
@@ -83,7 +87,7 @@ const BACKUP_SCHEMA = {
|
||||
required: COMMON_SCHEMA.required.concat([ 'depth', 'remoteId' ])
|
||||
}
|
||||
|
||||
const ROLLING_SNAPHOT_SCHEMA = {
|
||||
const ROLLING_SNAPSHOT_SCHEMA = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
...COMMON_SCHEMA.properties,
|
||||
@@ -143,13 +147,15 @@ if (process.env.XOA_PLAN < 4) {
|
||||
const BACKUP_METHOD_TO_INFO = {
|
||||
'vm.rollingBackup': {
|
||||
schema: BACKUP_SCHEMA,
|
||||
uiSchema: generateUiSchema(BACKUP_SCHEMA),
|
||||
label: 'backup',
|
||||
icon: 'backup',
|
||||
jobKey: 'rollingBackup',
|
||||
method: 'vm.rollingBackup'
|
||||
},
|
||||
'vm.rollingSnapshot': {
|
||||
schema: ROLLING_SNAPHOT_SCHEMA,
|
||||
schema: ROLLING_SNAPSHOT_SCHEMA,
|
||||
uiSchema: generateUiSchema(ROLLING_SNAPSHOT_SCHEMA),
|
||||
label: 'rollingSnapshot',
|
||||
icon: 'rolling-snapshot',
|
||||
jobKey: 'rollingSnapshot',
|
||||
@@ -157,6 +163,7 @@ const BACKUP_METHOD_TO_INFO = {
|
||||
},
|
||||
'vm.rollingDeltaBackup': {
|
||||
schema: DELTA_BACKUP_SCHEMA,
|
||||
uiSchema: generateUiSchema(DELTA_BACKUP_SCHEMA),
|
||||
label: 'deltaBackup',
|
||||
icon: 'delta-backup',
|
||||
jobKey: 'deltaBackup',
|
||||
@@ -164,6 +171,7 @@ const BACKUP_METHOD_TO_INFO = {
|
||||
},
|
||||
'vm.rollingDrCopy': {
|
||||
schema: DISASTER_RECOVERY_SCHEMA,
|
||||
uiSchema: generateUiSchema(DISASTER_RECOVERY_SCHEMA),
|
||||
label: 'disasterRecovery',
|
||||
icon: 'disaster-recovery',
|
||||
jobKey: 'disasterRecovery',
|
||||
@@ -171,6 +179,7 @@ const BACKUP_METHOD_TO_INFO = {
|
||||
},
|
||||
'vm.deltaCopy': {
|
||||
schema: CONTINUOUS_REPLICATION_SCHEMA,
|
||||
uiSchema: generateUiSchema(CONTINUOUS_REPLICATION_SCHEMA),
|
||||
label: 'continuousReplication',
|
||||
icon: 'continuous-replication',
|
||||
jobKey: 'continuousReplication',
|
||||
@@ -180,13 +189,13 @@ const BACKUP_METHOD_TO_INFO = {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const DEFAULT_CRON_PATTERN = '0 0 * * *'
|
||||
|
||||
@injectIntl
|
||||
export default class New extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
const { state } = this
|
||||
state.cronPattern = '* * * * *'
|
||||
this.state.cronPattern = DEFAULT_CRON_PATTERN
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
@@ -199,7 +208,8 @@ export default class New extends Component {
|
||||
}
|
||||
this.setState({
|
||||
backupInfo: BACKUP_METHOD_TO_INFO[job.method],
|
||||
cronPattern: schedule.cron
|
||||
cronPattern: schedule.cron,
|
||||
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
|
||||
@@ -231,7 +241,8 @@ export default class New extends Component {
|
||||
...callArgs
|
||||
} = backup
|
||||
|
||||
const { backupInfo } = this.state
|
||||
const { backupInfo, timezone } = this.state
|
||||
|
||||
const job = {
|
||||
type: 'call',
|
||||
key: backupInfo.jobKey,
|
||||
@@ -253,33 +264,33 @@ export default class New extends Component {
|
||||
|
||||
if (oldJob && oldSchedule) {
|
||||
job.id = oldJob.id
|
||||
oldSchedule.cron = this.state.cronPattern
|
||||
return setJob(job).then(() => setSchedule(oldSchedule))
|
||||
return setJob(job).then(() => updateSchedule({
|
||||
...oldSchedule,
|
||||
cron: this.state.cronPattern,
|
||||
timezone
|
||||
}))
|
||||
}
|
||||
|
||||
// Create backup schedule.
|
||||
return createJob(job).then(jobId => {
|
||||
createSchedule(jobId, this.state.cronPattern, enabled)
|
||||
createSchedule(jobId, { cron: this.state.cronPattern, enabled, timezone })
|
||||
})
|
||||
}
|
||||
|
||||
_handleReset = () => {
|
||||
const {
|
||||
backupInput,
|
||||
scheduler
|
||||
} = this.refs
|
||||
const { backupInput } = this.refs
|
||||
|
||||
if (backupInput) {
|
||||
backupInput.value = undefined
|
||||
}
|
||||
|
||||
scheduler.value = '* * * * *'
|
||||
this.setState({
|
||||
cronPattern: DEFAULT_CRON_PATTERN
|
||||
})
|
||||
}
|
||||
|
||||
_updateCronPattern = value => {
|
||||
this.setState({
|
||||
cronPattern: value
|
||||
})
|
||||
this.setState(value)
|
||||
}
|
||||
|
||||
_handleBackupSelection = event => {
|
||||
@@ -289,7 +300,12 @@ export default class New extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { backupInfo, defaultValue } = this.state
|
||||
const {
|
||||
backupInfo,
|
||||
cronPattern,
|
||||
defaultValue,
|
||||
timezone
|
||||
} = this.state
|
||||
const { formatMessage } = this.props.intl
|
||||
|
||||
return process.env.XOA_PLAN > 1
|
||||
@@ -318,17 +334,22 @@ export default class New extends Component {
|
||||
label={<span><Icon icon={backupInfo.icon} /> {formatMessage(messages[backupInfo.label])}</span>}
|
||||
required
|
||||
schema={backupInfo.schema}
|
||||
uiSchema={backupInfo.uiSchema}
|
||||
ref='backupInput'
|
||||
/>
|
||||
}
|
||||
</form>
|
||||
</Section>
|
||||
<Section icon='schedule' title='schedule'>
|
||||
<Scheduler ref='scheduler' onChange={this._updateCronPattern} />
|
||||
<Scheduler
|
||||
cronPattern={cronPattern}
|
||||
timezone={timezone}
|
||||
onChange={this._updateCronPattern}
|
||||
/>
|
||||
</Section>
|
||||
<Section icon='preview' title='preview' summary>
|
||||
<div className='card-block'>
|
||||
<SchedulePreview cron={this.state.cronPattern} />
|
||||
<SchedulePreview cronPattern={cronPattern} />
|
||||
{process.env.XOA_PLAN < 4 && backupInfo && process.env.XOA_PLAN < REQUIRED_XOA_PLAN[backupInfo.jobKey]
|
||||
? <Upgrade place='newBackup' available={REQUIRED_XOA_PLAN[backupInfo.jobKey]} />
|
||||
: <fieldset className='pull-xs-right p-t-1'>
|
||||
|
||||
@@ -4,19 +4,17 @@ import ActionToggle from 'action-toggle'
|
||||
import filter from 'lodash/filter'
|
||||
import forEach from 'lodash/forEach'
|
||||
import Icon from 'icon'
|
||||
import Link from 'link'
|
||||
import LogList from '../../logs'
|
||||
import map from 'lodash/map'
|
||||
import orderBy from 'lodash/orderBy'
|
||||
import React, { Component } from 'react'
|
||||
import { ButtonGroup } from 'react-bootstrap-4/lib'
|
||||
import { Link } from 'react-router'
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardBlock
|
||||
} from 'card'
|
||||
|
||||
import {
|
||||
deleteBackupSchedule,
|
||||
disableSchedule,
|
||||
@@ -148,6 +146,7 @@ export default class Overview extends Component {
|
||||
<th>{_('job')}</th>
|
||||
<th>{_('jobTag')}</th>
|
||||
<th className='hidden-xs-down'>{_('jobScheduling')}</th>
|
||||
<th className='hidden-xs-down'>{_('jobTimezone')}</th>
|
||||
<th>{_('jobState')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -160,6 +159,7 @@ export default class Overview extends Component {
|
||||
<td>{this._getJobLabel(job)}</td>
|
||||
<td>{this._getScheduleTag(schedule, job)}</td>
|
||||
<td className='hidden-xs-down'>{schedule.cron}</td>
|
||||
<td className='hidden-xs-down'>{schedule.timezone || _('jobServerTimezone')}</td>
|
||||
<td>
|
||||
{this._getScheduleToggle(schedule)}
|
||||
<fieldset className='pull-xs-right'>
|
||||
|
||||
@@ -3,7 +3,7 @@ import ActionButton from 'action-button'
|
||||
import ActionRowButton from 'action-row-button'
|
||||
import find from 'lodash/find'
|
||||
import forEach from 'lodash/forEach'
|
||||
import Link from 'react-router/lib/Link'
|
||||
import Link from 'link'
|
||||
import map from 'lodash/map'
|
||||
import moment from 'moment'
|
||||
import orderBy from 'lodash/orderBy'
|
||||
@@ -25,6 +25,7 @@ import { SelectSr } from 'select-objects'
|
||||
import {
|
||||
importBackup,
|
||||
importDeltaBackup,
|
||||
isSrWritable,
|
||||
listRemote,
|
||||
startVm,
|
||||
subscribeRemotes
|
||||
@@ -43,7 +44,7 @@ const backupOptionRenderer = backup => <span>
|
||||
|
||||
@connectStore(() => ({
|
||||
writableSrs: createGetObjectsOfType('SR').filter(
|
||||
[ sr => sr.content_type !== 'iso' ]
|
||||
[ isSrWritable ]
|
||||
).sort()
|
||||
}))
|
||||
export default class Restore extends Component {
|
||||
@@ -102,8 +103,10 @@ export default class Restore extends Component {
|
||||
}
|
||||
}
|
||||
}
|
||||
backupInfoByVm[backup.name] || (backupInfoByVm[backup.name] = [])
|
||||
backupInfoByVm[backup.name].push(backup)
|
||||
if (backup) {
|
||||
backupInfoByVm[backup.name] || (backupInfoByVm[backup.name] = [])
|
||||
backupInfoByVm[backup.name].push(backup)
|
||||
}
|
||||
})
|
||||
for (let vm in backupInfoByVm) {
|
||||
const bks = backupInfoByVm[vm]
|
||||
@@ -192,7 +195,8 @@ const BK_COLUMNS = [
|
||||
{
|
||||
name: _('lastBackupColumn'),
|
||||
itemRenderer: info => <span><FormattedDate value={info.last.date} month='long' day='numeric' year='numeric' hour='2-digit' minute='2-digit' second='2-digit' /> ({info.last.type})</span>,
|
||||
sortCriteria: info => info.last.date
|
||||
sortCriteria: info => info.last.date,
|
||||
sortOrder: 'desc'
|
||||
},
|
||||
{
|
||||
name: _('availableBackupsColumn'),
|
||||
@@ -208,12 +212,11 @@ const BK_COLUMNS = [
|
||||
}
|
||||
]
|
||||
|
||||
const srWritablePredicate = sr => sr.content_type !== 'iso'
|
||||
const notifyImportStart = () => info(_('importBackupTitle'), _('importBackupMessage'))
|
||||
|
||||
@connectStore(() => ({
|
||||
writableSrs: createGetObjectsOfType('SR').filter(
|
||||
[ sr => sr.content_type !== 'iso' ]
|
||||
[ isSrWritable ]
|
||||
).sort()
|
||||
}), { withRef: true })
|
||||
class _ModalBody extends Component {
|
||||
@@ -274,7 +277,7 @@ class _ModalBody extends Component {
|
||||
|
||||
render () {
|
||||
return <div>
|
||||
<SelectSr ref='sr' predicate={srWritablePredicate} />
|
||||
<SelectSr ref='sr' predicate={isSrWritable} />
|
||||
<br />
|
||||
<SelectPlainObject ref='backup' options={this.state.options} optionKey='path' optionRenderer={backupOptionRenderer} placeholder={this.props.intl.formatMessage(messages.importBackupModalSelectBackup)} />
|
||||
<br />
|
||||
|
||||
@@ -7,8 +7,9 @@ import SortedTable from 'sorted-table'
|
||||
import TabButton from 'tab-button'
|
||||
import Upgrade from 'xoa-upgrade'
|
||||
import React, { Component } from 'react'
|
||||
import { Card, CardHeader, CardBlock } from 'card'
|
||||
import { confirm } from 'modal'
|
||||
import { deleteMessage, deleteVdi, deleteVm } from 'xo'
|
||||
import { deleteMessage, deleteVdi, deleteVm, isSrWritable } from 'xo'
|
||||
import { FormattedRelative, FormattedTime } from 'react-intl'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import {
|
||||
@@ -65,7 +66,8 @@ const SR_COLUMNS = [
|
||||
{
|
||||
name: _('srUsage'),
|
||||
itemRenderer: sr => sr.size > 1 && <meter value={(sr.physical_usage / sr.size) * 100} min='0' max='100' optimum='40' low='80' high='90'></meter>,
|
||||
sortCriteria: sr => sr.physical_usage / sr.size
|
||||
sortCriteria: sr => sr.physical_usage / sr.size,
|
||||
sortOrder: 'desc'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -73,7 +75,8 @@ const VDI_COLUMNS = [
|
||||
{
|
||||
name: _('snapshotDate'),
|
||||
itemRenderer: vdi => <span><FormattedTime value={vdi.snapshot_time * 1000} minute='numeric' hour='numeric' day='numeric' month='long' year='numeric' /> (<FormattedRelative value={vdi.snapshot_time * 1000} />)</span>,
|
||||
sortCriteria: vdi => vdi.snapshot_time
|
||||
sortCriteria: vdi => vdi.snapshot_time,
|
||||
sortOrder: 'desc'
|
||||
},
|
||||
{
|
||||
name: _('vdiNameLabel'),
|
||||
@@ -111,7 +114,8 @@ const VM_COLUMNS = [
|
||||
{
|
||||
name: _('snapshotDate'),
|
||||
itemRenderer: vm => <span><FormattedTime value={vm.snapshot_time * 1000} minute='numeric' hour='numeric' day='numeric' month='long' year='numeric' /> (<FormattedRelative value={vm.snapshot_time * 1000} />)</span>,
|
||||
sortCriteria: vm => vm.snapshot_time
|
||||
sortCriteria: vm => vm.snapshot_time,
|
||||
sortOrder: 'desc'
|
||||
},
|
||||
{
|
||||
name: _('vmNameLabel'),
|
||||
@@ -146,7 +150,8 @@ const ALARM_COLUMNS = [
|
||||
itemRenderer: message => (
|
||||
<span><FormattedTime value={message.time * 1000} minute='numeric' hour='numeric' day='numeric' month='long' year='numeric' /> (<FormattedRelative value={message.time * 1000} />)</span>
|
||||
),
|
||||
sortCriteria: message => message.time
|
||||
sortCriteria: message => message.time,
|
||||
sortOrder: 'desc'
|
||||
},
|
||||
{
|
||||
name: _('alarmContent'),
|
||||
@@ -182,7 +187,7 @@ const ALARM_COLUMNS = [
|
||||
.filter([ snapshot => !snapshot.$snapshot_of ])
|
||||
.sort()
|
||||
const getUserSrs = createGetObjectsOfType('SR')
|
||||
.filter([ sr => sr.content_type === 'user' ])
|
||||
.filter([ isSrWritable ])
|
||||
const getVdiSrs = createGetObjectsOfType('SR')
|
||||
.pick(createSelector(
|
||||
getOrphanVdiSnapshots,
|
||||
@@ -230,11 +235,11 @@ export default class Health extends Component {
|
||||
? <Container>
|
||||
<Row>
|
||||
<Col>
|
||||
<div className='card-dashboard'>
|
||||
<div className='card-header-dashboard'>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Icon icon='disk' /> {_('srStatePanel')}
|
||||
</div>
|
||||
<div className='card-block'>
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
{isEmpty(this.props.userSrs)
|
||||
? <p className='text-xs-center'>{_('noSrs')}</p>
|
||||
: <Row>
|
||||
@@ -243,17 +248,17 @@ export default class Health extends Component {
|
||||
</Col>
|
||||
</Row>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<div className='card-dashboard'>
|
||||
<div className='card-header-dashboard'>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Icon icon='disk' /> {_('orphanedVdis')}
|
||||
</div>
|
||||
<div className='card-block'>
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
{isEmpty(this.props.vdiOrphaned)
|
||||
? <p className='text-xs-center'>{_('noOrphanedObject')}</p>
|
||||
: <div>
|
||||
@@ -274,32 +279,32 @@ export default class Health extends Component {
|
||||
</Row>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<div className='card-dashboard'>
|
||||
<div className='card-header-dashboard'>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Icon icon='vm' /> {_('orphanedVms')}
|
||||
</div>
|
||||
<div className='card-block'>
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
{isEmpty(this.props.vmOrphaned)
|
||||
? <p className='text-xs-center'>{_('noOrphanedObject')}</p>
|
||||
: <SortedTable collection={this.props.vmOrphaned} columns={VM_COLUMNS} />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<div className='card-dashboard'>
|
||||
<div className='card-header-dashboard'>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Icon icon='alarm' /> {_('alarmMessage')}
|
||||
</div>
|
||||
<div className='card-block'>
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
{isEmpty(this.props.alertMessages)
|
||||
? <p className='text-xs-center'>{_('noAlarms')}</p>
|
||||
: <div>
|
||||
@@ -320,8 +325,8 @@ export default class Health extends Component {
|
||||
</Row>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
|
||||
@@ -33,7 +33,7 @@ const Dashboard = routes('overview', {
|
||||
stats: Stats,
|
||||
visualizations: Visualizations
|
||||
})(
|
||||
({ children }) => <Page header={HEADER}>{children}</Page>
|
||||
({ children }) => <Page header={HEADER} title='dashboardPage' formatTitle>{children}</Page>
|
||||
)
|
||||
|
||||
export default Dashboard
|
||||
|
||||
4
src/xo-app/dashboard/overview/index.css
Normal file
4
src/xo-app/dashboard/overview/index.css
Normal file
@@ -0,0 +1,4 @@
|
||||
.bigCardContent {
|
||||
font-size: 4em;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -1,15 +1,23 @@
|
||||
import _ from 'intl'
|
||||
import ChartistGraph from 'react-chartist'
|
||||
import Component from 'base-component'
|
||||
import forEach from 'lodash/forEach'
|
||||
import Icon from 'icon'
|
||||
import propTypes from 'prop-types'
|
||||
import Link, { BlockLink } from 'link'
|
||||
import map from 'lodash/map'
|
||||
import HostsPatchesTable from 'hosts-patches-table'
|
||||
import React from 'react'
|
||||
import size from 'lodash/size'
|
||||
import Upgrade from 'xoa-upgrade'
|
||||
import React, { Component } from 'react'
|
||||
import { ButtonGroup } from 'react-bootstrap-4/lib'
|
||||
import { Card, CardBlock, CardHeader } from 'card'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import {
|
||||
createCollectionWrapper,
|
||||
createCounter,
|
||||
createGetObjectsOfType,
|
||||
createGetHostMetrics,
|
||||
createSelector,
|
||||
createTop
|
||||
} from 'selectors'
|
||||
@@ -18,35 +26,51 @@ import {
|
||||
formatSize
|
||||
} from 'utils'
|
||||
import {
|
||||
isSrWritable,
|
||||
subscribeUsers
|
||||
} from 'xo'
|
||||
|
||||
import styles from './index.css'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
hosts: propTypes.object.isRequired
|
||||
})
|
||||
class PatchesCard extends Component {
|
||||
_getContainer = () => this.refs.container
|
||||
|
||||
render () {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Icon icon='host-patch-update' /> {_('update')}
|
||||
<div ref='container' className='pull-right' />
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
<HostsPatchesTable
|
||||
buttonsGroupContainer={this._getContainer}
|
||||
container={ButtonGroup}
|
||||
displayPools
|
||||
hosts={this.props.hosts}
|
||||
/>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@connectStore(() => {
|
||||
const getHosts = createGetObjectsOfType('host')
|
||||
const getVms = createGetObjectsOfType('VM')
|
||||
|
||||
const getHostMetrics = createCollectionWrapper(
|
||||
createSelector(
|
||||
getHosts,
|
||||
hosts => {
|
||||
const metrics = {
|
||||
cpus: 0,
|
||||
memoryTotal: 0,
|
||||
memoryUsage: 0
|
||||
}
|
||||
forEach(hosts, host => {
|
||||
metrics.cpus += host.cpus.cores
|
||||
metrics.memoryTotal += host.memory.size
|
||||
metrics.memoryUsage += host.memory.usage
|
||||
})
|
||||
return metrics
|
||||
}
|
||||
)
|
||||
)
|
||||
const getHostMetrics = createGetHostMetrics(getHosts)
|
||||
|
||||
const userSrs = createTop(
|
||||
createGetObjectsOfType('SR').filter(
|
||||
[ sr => sr.content_type === 'user' ]
|
||||
[ isSrWritable ]
|
||||
),
|
||||
[ sr => sr.physical_usage / sr.size ],
|
||||
5
|
||||
@@ -111,6 +135,7 @@ import {
|
||||
|
||||
return {
|
||||
hostMetrics: getHostMetrics,
|
||||
hosts: getHosts,
|
||||
nAlarmMessages: getNumberOfAlarmMessages,
|
||||
nHosts: getNumberOfHosts,
|
||||
nPools: getNumberOfPools,
|
||||
@@ -128,167 +153,212 @@ export default class Overview extends Component {
|
||||
})
|
||||
}
|
||||
render () {
|
||||
const { state } = this
|
||||
const { props, state } = this
|
||||
const users = state && state.users
|
||||
const nUsers = users && Object.keys(users).length
|
||||
const nUsers = size(users)
|
||||
|
||||
return process.env.XOA_PLAN > 2
|
||||
? <Container>
|
||||
<Row>
|
||||
<Col mediumSize={4}>
|
||||
<div className='card-dashboard'>
|
||||
<div className='card-header-dashboard'>
|
||||
<Icon icon='pool' /> {_('poolPanel', { pools: this.props.nPools })}
|
||||
</div>
|
||||
<div className='card-block-dashboard'>
|
||||
<p>{this.props.nPools}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Icon icon='pool' /> {_('poolPanel', { pools: props.nPools })}
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
<p className={styles.bigCardContent}>
|
||||
<Link to='/home?t=pool'>{props.nPools}</Link>
|
||||
</p>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col mediumSize={4}>
|
||||
<div className='card-dashboard'>
|
||||
<div className='card-header-dashboard'>
|
||||
<Icon icon='host' /> {_('hostPanel', { hosts: this.props.nHosts })}
|
||||
</div>
|
||||
<div className='card-block-dashboard'>
|
||||
<p>{this.props.nHosts}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Icon icon='host' /> {_('hostPanel', { hosts: props.nHosts })}
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
<p className={styles.bigCardContent}>
|
||||
<Link to='/home?t=host'>{props.nHosts}</Link>
|
||||
</p>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col mediumSize={4}>
|
||||
<div className='card-dashboard'>
|
||||
<div className='card-header-dashboard'>
|
||||
<Icon icon='vm' /> {_('vmPanel', { vms: this.props.nVms })}
|
||||
</div>
|
||||
<div className='card-block-dashboard'>
|
||||
<p>{this.props.nVms}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Icon icon='vm' /> {_('vmPanel', { vms: props.nVms })}
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
<p className={styles.bigCardContent}>
|
||||
<Link to='/home?s=&t=VM'>{props.nVms}</Link>
|
||||
</p>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col mediumSize={4}>
|
||||
<div className='card-dashboard'>
|
||||
<div className='card-header-dashboard'>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Icon icon='memory' /> {_('memoryStatePanel')}
|
||||
</div>
|
||||
<div className='card-block'>
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
<ChartistGraph
|
||||
data={{
|
||||
labels: ['Used Memory', 'Total Memory'],
|
||||
series: [this.props.hostMetrics.memoryUsage, this.props.hostMetrics.memoryTotal - this.props.hostMetrics.memoryUsage]
|
||||
series: [props.hostMetrics.memoryUsage, props.hostMetrics.memoryTotal - props.hostMetrics.memoryUsage]
|
||||
}}
|
||||
options={{ donut: true, donutWidth: 40, showLabel: false }}
|
||||
type='Pie' />
|
||||
<p className='text-xs-center'>{formatSize(this.props.hostMetrics.memoryUsage)} ({_('ofUsage')} {formatSize(this.props.hostMetrics.memoryTotal)})</p>
|
||||
</div>
|
||||
</div>
|
||||
type='Pie'
|
||||
/>
|
||||
<p className='text-xs-center'>
|
||||
{_('ofUsage', {
|
||||
total: formatSize(props.hostMetrics.memoryTotal),
|
||||
usage: formatSize(props.hostMetrics.memoryUsage)
|
||||
})}
|
||||
</p>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col mediumSize={4}>
|
||||
<div className='card-dashboard'>
|
||||
<div className='card-header-dashboard'>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Icon icon='cpu' /> {_('cpuStatePanel')}
|
||||
</div>
|
||||
<div className='card-block'>
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
<div className='ct-chart'>
|
||||
<ChartistGraph
|
||||
data={{
|
||||
labels: ['vCPUs', 'CPUs'],
|
||||
series: [this.props.vmMetrics.vcpus, this.props.hostMetrics.cpus]
|
||||
series: [props.vmMetrics.vcpus, props.hostMetrics.cpus]
|
||||
}}
|
||||
options={{ showLabel: false, showGrid: false, distributeSeries: true }}
|
||||
type='Bar' />
|
||||
<p className='text-xs-center'>{this.props.vmMetrics.vcpus} vCPUS ({_('ofUsage')} {this.props.hostMetrics.cpus} CPUs)</p>
|
||||
type='Bar'
|
||||
/>
|
||||
<p className='text-xs-center'>
|
||||
{_('ofUsage', {
|
||||
total: `${props.vmMetrics.vcpus} vCPUS`,
|
||||
usage: `${props.hostMetrics.cpus} CPUs`
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col mediumSize={4}>
|
||||
<div className='card-dashboard'>
|
||||
<div className='card-header-dashboard'>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Icon icon='disk' /> {_('srUsageStatePanel')}
|
||||
</div>
|
||||
<div className='card-block'>
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
<div className='ct-chart'>
|
||||
<BlockLink to='/dashboard/health'>
|
||||
<ChartistGraph
|
||||
data={{
|
||||
labels: ['Used Space', 'Total Space'],
|
||||
series: [props.srMetrics.srUsage, props.srMetrics.srTotal - props.srMetrics.srUsage]
|
||||
}}
|
||||
options={{ donut: true, donutWidth: 40, showLabel: false }}
|
||||
type='Pie'
|
||||
/>
|
||||
<p className='text-xs-center'>
|
||||
{_('ofUsage', {
|
||||
total: formatSize(props.srMetrics.srUsage),
|
||||
usage: formatSize(props.srMetrics.srTotal)
|
||||
})}
|
||||
</p>
|
||||
</BlockLink>
|
||||
</div>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col mediumSize={4}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Icon icon='alarm' /> {_('alarmMessage')}
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
<p className={styles.bigCardContent}>
|
||||
<Link to='/dashboard/health' className={props.nAlarmMessages > 0 ? 'text-warning' : ''}>{props.nAlarmMessages}</Link>
|
||||
</p>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col mediumSize={4}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Icon icon='task' /> {_('taskStatePanel')}
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
<p className={styles.bigCardContent}>
|
||||
<Link to='/tasks'>{props.nTasks}</Link>
|
||||
</p>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col mediumSize={4}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Icon icon='user' /> {_('usersStatePanel')}
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
<p className={styles.bigCardContent}>
|
||||
<Link to='/settings/users'>{nUsers}</Link>
|
||||
</p>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col mediumSize={4}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Icon icon='vm-force-shutdown' /> {_('vmStatePanel')}
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
<BlockLink to='/home?t=VM'>
|
||||
<ChartistGraph
|
||||
data={{
|
||||
labels: ['Used Space', 'Total Space'],
|
||||
series: [this.props.srMetrics.srUsage, this.props.srMetrics.srTotal - this.props.srMetrics.srUsage]
|
||||
labels: ['Running', 'Halted', 'Other'],
|
||||
series: [props.vmMetrics.running, props.vmMetrics.halted, props.vmMetrics.other]
|
||||
}}
|
||||
options={{ donut: true, donutWidth: 40, showLabel: false }}
|
||||
type='Pie' />
|
||||
<p className='text-xs-center'>{formatSize(this.props.srMetrics.srUsage)} ({_('ofUsage')} {formatSize(this.props.srMetrics.srTotal)})</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col mediumSize={4}>
|
||||
<div className='card-dashboard'>
|
||||
<div className='card-header-dashboard'>
|
||||
<Icon icon='alarm' /> {_('alarmMessage')}
|
||||
</div>
|
||||
<div className='card-block-dashboard'>
|
||||
<p className={this.props.nAlarmMessages > 0 ? 'text-warning' : ''}>{this.props.nAlarmMessages}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col mediumSize={4}>
|
||||
<div className='card-dashboard'>
|
||||
<div className='card-header-dashboard'>
|
||||
<Icon icon='task' /> {_('taskStatePanel')}
|
||||
</div>
|
||||
<div className='card-block-dashboard'>
|
||||
<p>{this.props.nTasks}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
<Col mediumSize={4}>
|
||||
<div className='card-dashboard'>
|
||||
<div className='card-header-dashboard'>
|
||||
<Icon icon='user' /> {_('usersStatePanel')}
|
||||
</div>
|
||||
<div className='card-block-dashboard'>
|
||||
<p>{nUsers}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col mediumSize={4}>
|
||||
<div className='card-dashboard'>
|
||||
<div className='card-header-dashboard'>
|
||||
<Icon icon='vm-force-shutdown' /> {_('vmStatePanel')}
|
||||
</div>
|
||||
<div className='card-block'>
|
||||
<ChartistGraph
|
||||
data={{
|
||||
labels: ['Running', 'Halted', 'Other'],
|
||||
series: [this.props.vmMetrics.running, this.props.vmMetrics.halted, this.props.vmMetrics.other]
|
||||
}}
|
||||
options={{ showLabel: false }}
|
||||
type='Pie' />
|
||||
<p className='text-xs-center'>{this.props.vmMetrics.running} running ({this.props.vmMetrics.halted} halted)</p>
|
||||
</div>
|
||||
</div>
|
||||
options={{ showLabel: false }}
|
||||
type='Pie'
|
||||
/>
|
||||
<p className='text-xs-center'>
|
||||
{_('vmsStates', { running: props.vmMetrics.running, halted: props.vmMetrics.halted })}
|
||||
</p>
|
||||
</BlockLink>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col mediumSize={8}>
|
||||
<div className='card-dashboard'>
|
||||
<div className='card-header-dashboard'>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Icon icon='disk' /> {_('srTopUsageStatePanel')}
|
||||
</div>
|
||||
<div className='card-block'>
|
||||
<ChartistGraph
|
||||
style={{strokeWidth: '30px'}}
|
||||
data={{
|
||||
labels: map(this.props.userSrs, 'name_label'),
|
||||
series: map(this.props.userSrs, sr => (sr.physical_usage / sr.size) * 100)
|
||||
}}
|
||||
options={{ showLabel: false, showGrid: false, distributeSeries: true, high: 100 }}
|
||||
type='Bar' />
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
<BlockLink to='/dashboard/health'>
|
||||
<ChartistGraph
|
||||
style={{strokeWidth: '30px'}}
|
||||
data={{
|
||||
labels: map(props.userSrs, 'name_label'),
|
||||
series: map(props.userSrs, sr => (sr.physical_usage / sr.size) * 100)
|
||||
}}
|
||||
options={{ showLabel: false, showGrid: false, distributeSeries: true, high: 100 }}
|
||||
type='Bar'
|
||||
/>
|
||||
</BlockLink>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<PatchesCard hosts={props.hosts} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import ActionButton from 'action-button'
|
||||
import Component from 'base-component'
|
||||
import Icon from 'icon'
|
||||
import React from 'react'
|
||||
import XoWeekCharts from 'xo-week-charts'
|
||||
import XoWeekHeatmap from 'xo-week-heatmap'
|
||||
import _ from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import cloneDeep from 'lodash/cloneDeep'
|
||||
import Component from 'base-component'
|
||||
import forEach from 'lodash/forEach'
|
||||
import Icon from 'icon'
|
||||
import map from 'lodash/map'
|
||||
import propTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import renderXoItem from 'render-xo-item'
|
||||
import sortBy from 'lodash/sortBy'
|
||||
import XoWeekCharts from 'xo-week-charts'
|
||||
import XoWeekHeatmap from 'xo-week-heatmap'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { error } from 'notification'
|
||||
import { SelectHostVm } from 'select-objects'
|
||||
@@ -17,8 +18,7 @@ import { createGetObjectsOfType } from 'selectors'
|
||||
import {
|
||||
connectStore,
|
||||
formatSize,
|
||||
mapPlus,
|
||||
propTypes
|
||||
mapPlus
|
||||
} from 'utils'
|
||||
import {
|
||||
fetchHostStats,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user