Compare commits
31 Commits
xo-web/v5.
...
xo-web/v5.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "xo-web",
|
||||
"version": "5.0.9",
|
||||
"version": "5.1.0",
|
||||
"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",
|
||||
@@ -113,7 +114,7 @@
|
||||
"vinyl": "^1.1.1",
|
||||
"watchify": "^3.7.0",
|
||||
"xo-acl-resolver": "^0.2.1",
|
||||
"xo-lib": "^0.8.0-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 = {
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
import styles from './index.css'
|
||||
|
||||
@propTypes({
|
||||
buttonTitle: propTypes.any,
|
||||
defaultValue: propTypes.any,
|
||||
disabled: propTypes.bool,
|
||||
options: propTypes.oneOfType([
|
||||
@@ -31,7 +30,6 @@ import styles from './index.css'
|
||||
})
|
||||
export default class Combobox extends Component {
|
||||
static defaultProps = {
|
||||
buttonTitle: 'Presets',
|
||||
type: 'text'
|
||||
}
|
||||
|
||||
@@ -45,7 +43,10 @@ export default class Combobox extends Component {
|
||||
|
||||
_handleChange = event => {
|
||||
const { onChange } = this.props
|
||||
onChange && (() => onChange(event.target.value))
|
||||
|
||||
if (onChange) {
|
||||
onChange(event.target.value)
|
||||
}
|
||||
}
|
||||
|
||||
_setText (value) {
|
||||
@@ -59,7 +60,7 @@ export default class Combobox extends Component {
|
||||
const Input = (
|
||||
<input
|
||||
className='form-control'
|
||||
defaultValue={props.defaultValue || ''}
|
||||
defaultValue={props.defaultValue}
|
||||
disabled={props.disabled}
|
||||
options={options}
|
||||
onChange={this._handleChange}
|
||||
@@ -84,7 +85,7 @@ export default class Combobox extends Component {
|
||||
className={styles.button}
|
||||
disabled={props.disabled}
|
||||
id='selectInput'
|
||||
title={props.buttonTitle}
|
||||
title=''
|
||||
>
|
||||
{map(options, option => (
|
||||
<MenuItem key={option} onClick={() => this._setText(option)}>
|
||||
|
||||
@@ -28,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
|
||||
@@ -157,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}
|
||||
@@ -271,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)}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
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:'
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
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
@@ -19,6 +19,9 @@ var messages = {
|
||||
|
||||
// ----- Titles -----
|
||||
homePage: 'Home',
|
||||
homeVmPage: 'VMs',
|
||||
homeHostPage: 'Hosts',
|
||||
homePoolPage: 'Pools',
|
||||
dashboardPage: 'Dashboard',
|
||||
overviewDashboardPage: 'Overview',
|
||||
overviewVisualizationDashboardPage: 'Visualizations',
|
||||
@@ -136,6 +139,7 @@ var messages = {
|
||||
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',
|
||||
@@ -143,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',
|
||||
@@ -174,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',
|
||||
@@ -273,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',
|
||||
@@ -504,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: {
|
||||
@@ -621,21 +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',
|
||||
@@ -867,6 +897,17 @@ var messages = {
|
||||
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'
|
||||
|
||||
@@ -29,8 +29,8 @@ export default class IntegerInput extends AbstractInput {
|
||||
defaultValue={props.defaultValue}
|
||||
disabled={props.disabled}
|
||||
onChange={props.onChange}
|
||||
options={schema.defaults || schema.default}
|
||||
placeholder={props.placeholder}
|
||||
options={schema.defaults}
|
||||
placeholder={props.placeholder || schema.default}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
step={1}
|
||||
|
||||
@@ -29,8 +29,8 @@ export default class NumberInput extends AbstractInput {
|
||||
defaultValue={props.defaultValue}
|
||||
disabled={props.disabled}
|
||||
onChange={props.onChange}
|
||||
options={schema.defaults || schema.default}
|
||||
placeholder={props.placeholder}
|
||||
options={schema.defaults}
|
||||
placeholder={props.placeholder || schema.default}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
step='any'
|
||||
|
||||
@@ -21,8 +21,8 @@ export default class StringInput extends AbstractInput {
|
||||
defaultValue={props.defaultValue}
|
||||
disabled={props.disabled}
|
||||
onChange={props.onChange}
|
||||
options={schema.defaults || schema.default}
|
||||
placeholder={props.placeholder}
|
||||
options={schema.defaults}
|
||||
placeholder={props.placeholder || schema.default}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
type={props.password && 'password'}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import forEach from 'lodash/forEach'
|
||||
import includes from 'lodash/includes'
|
||||
import join from 'lodash/join'
|
||||
import later from 'later'
|
||||
@@ -7,7 +6,6 @@ import React from 'react'
|
||||
import sortedIndex from 'lodash/sortedIndex'
|
||||
import { FormattedTime } from 'react-intl'
|
||||
import {
|
||||
Panel,
|
||||
Tab,
|
||||
Tabs
|
||||
} from 'react-bootstrap-4/lib'
|
||||
@@ -15,14 +13,20 @@ import {
|
||||
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
|
||||
@@ -78,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'
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
@@ -101,7 +121,7 @@ const getDayName = (dayNum) =>
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
cron: propTypes.string.isRequired
|
||||
cronPattern: propTypes.string.isRequired
|
||||
})
|
||||
export class SchedulePreview extends Component {
|
||||
_handleChange = value => {
|
||||
@@ -111,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>
|
||||
@@ -137,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>
|
||||
)
|
||||
}
|
||||
@@ -173,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)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
@@ -264,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)
|
||||
@@ -443,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>
|
||||
)
|
||||
}
|
||||
|
||||
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,133 @@
|
||||
import classNames from 'classnames'
|
||||
import React, { PropTypes } from 'react'
|
||||
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.any.isRequired,
|
||||
className: propTypes.string,
|
||||
content: propTypes.any.isRequired,
|
||||
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 () {
|
||||
return this.props.children
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import styles from './index.css'
|
||||
const N_LABELS_X = 5
|
||||
|
||||
const LABEL_OFFSET_X = 40
|
||||
const LABEL_OFFSET_Y = 75
|
||||
const LABEL_OFFSET_Y = 85
|
||||
|
||||
// ===================================================================
|
||||
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import assign from 'lodash/assign'
|
||||
import cookies from 'cookies-js'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import isEqual from 'lodash/isEqual'
|
||||
import filter from 'lodash/filter'
|
||||
import forEach from 'lodash/forEach'
|
||||
import map from 'lodash/map'
|
||||
import once from 'lodash/once'
|
||||
@@ -33,6 +34,11 @@ import {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const XEN_DEFAULT_CPU_WEIGHT = 256
|
||||
export const XEN_DEFAULT_CPU_CAP = 0
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const isSrWritable = sr => sr.content_type !== 'iso' && sr.size > 0
|
||||
export const isSrShared = sr => sr.$PBDs.length > 1
|
||||
|
||||
@@ -192,6 +198,8 @@ const createSubscription = cb => {
|
||||
|
||||
// Subscriptions -----------------------------------------------------
|
||||
|
||||
export const subscribeCurrentUser = createSubscription(() => xo.refreshUser())
|
||||
|
||||
export const subscribeAcls = createSubscription(() => _call('acl.get'))
|
||||
|
||||
export const subscribeJobs = createSubscription(() => _call('job.getAll'))
|
||||
@@ -242,6 +250,8 @@ export const apiMethods = _call('system.getMethodsInfo')
|
||||
|
||||
export const serverVersion = _call('system.getServerVersion')
|
||||
|
||||
export const getXoServerTimezone = _call('system.getServerTimezone')
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const resolveId = value =>
|
||||
@@ -958,8 +968,13 @@ export const destroyTask = task => (
|
||||
|
||||
// Backups -----------------------------------------------------------
|
||||
|
||||
export const createSchedule = (jobId, cron, enabled, name = undefined) => (
|
||||
_call('schedule.create', { jobId, cron, enabled, name })::tap(
|
||||
export const createSchedule = (jobId, {
|
||||
cron,
|
||||
enabled,
|
||||
name = undefined,
|
||||
timezone = undefined
|
||||
}) => (
|
||||
_call('schedule.create', { jobId, cron, enabled, name, timezone })::tap(
|
||||
subscribeSchedules.forceRefresh
|
||||
)
|
||||
)
|
||||
@@ -989,12 +1004,6 @@ export const getSchedule = id => (
|
||||
_call('schedule.get', { id })
|
||||
)
|
||||
|
||||
export const setSchedule = schedule => (
|
||||
_call('schedule.set', schedule)::tap(
|
||||
subscribeSchedules.forceRefresh
|
||||
)
|
||||
)
|
||||
|
||||
export const enableSchedule = id => (
|
||||
_call('scheduler.enable', { id })::tap(
|
||||
subscribeScheduleTable.forceRefresh
|
||||
@@ -1094,6 +1103,10 @@ export const deleteResourceSet = async id => {
|
||||
subscribeResourceSets.forceRefresh()
|
||||
}
|
||||
|
||||
export const recomputeResourceSetsLimits = () => (
|
||||
_call('resourceSet.recomputeAllLimits')
|
||||
)
|
||||
|
||||
// Remote ------------------------------------------------------------
|
||||
|
||||
export const createRemote = (name, url) => (
|
||||
@@ -1318,6 +1331,128 @@ export const changePassword = (oldPassword, newPassword) => (
|
||||
)
|
||||
)
|
||||
|
||||
const _setUserPreferences = preferences => (
|
||||
_call('user.set', {
|
||||
id: xo.user.id,
|
||||
preferences
|
||||
})::tap(
|
||||
subscribeCurrentUser.forceRefresh
|
||||
)
|
||||
)
|
||||
|
||||
import NewSshKeyModalBody from './new-ssh-key-modal'
|
||||
export const addSshKey = () => (
|
||||
confirm({
|
||||
icon: 'ssh-key',
|
||||
title: _('newSshKeyModalTitle'),
|
||||
body: <NewSshKeyModalBody />
|
||||
}).then(
|
||||
newKey => {
|
||||
if (!newKey.title || !newKey.key) {
|
||||
error(_('sshKeyErrorTitle'), _('sshKeyErrorMessage'))
|
||||
return
|
||||
}
|
||||
const { preferences } = xo.user
|
||||
const otherKeys = preferences && preferences.sshKeys || []
|
||||
return _setUserPreferences({ sshKeys: [
|
||||
...otherKeys,
|
||||
newKey
|
||||
]})
|
||||
},
|
||||
noop
|
||||
)
|
||||
)
|
||||
|
||||
export const deleteSshKey = key => (
|
||||
confirm({
|
||||
title: _('deleteSshKeyConfirm'),
|
||||
body: _('deleteSshKeyConfirmMessage', { title: <strong>{key.title}</strong> })
|
||||
}).then(
|
||||
() => {
|
||||
const { preferences } = xo.user
|
||||
return _setUserPreferences({
|
||||
sshKeys: filter(preferences && preferences.sshKeys, k => !isEqual(k, key))
|
||||
})
|
||||
},
|
||||
noop
|
||||
)
|
||||
)
|
||||
|
||||
// User filters --------------------------------------------------
|
||||
|
||||
import AddUserFilterModalBody from './add-user-filter-modal'
|
||||
export const addCustomFilter = (type, value) => {
|
||||
const { user } = xo
|
||||
return confirm({
|
||||
title: _('saveNewFilterTitle'),
|
||||
body: <AddUserFilterModalBody user={user} type={type} value={value} />
|
||||
}).then(name => {
|
||||
if (name.length === 0) {
|
||||
return error(_('saveNewUserFilterErrorTitle'), _('saveNewUserFilterErrorBody'))
|
||||
}
|
||||
|
||||
const { preferences } = user
|
||||
const filters = (preferences && preferences.filters) || {}
|
||||
|
||||
return _setUserPreferences({
|
||||
filters: {
|
||||
...filters,
|
||||
[type]: {
|
||||
...filters[type],
|
||||
[name]: value
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export const removeCustomFilter = (type, name) => (
|
||||
confirm({
|
||||
title: _('removeUserFilterTitle'),
|
||||
body: <p>{_('removeUserFilterBody')}</p>
|
||||
}).then(() => {
|
||||
const { user } = xo
|
||||
const { filters } = user.preferences
|
||||
|
||||
return _setUserPreferences({
|
||||
filters: {
|
||||
...filters,
|
||||
[type]: {
|
||||
...filters[type],
|
||||
[name]: undefined
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
export const editCustomFilter = (type, name, { newName = name, newValue }) => {
|
||||
const { filters } = xo.user.preferences
|
||||
return _setUserPreferences({
|
||||
filters: {
|
||||
...filters,
|
||||
[type]: {
|
||||
...filters[type],
|
||||
[name]: undefined,
|
||||
[newName]: newValue || filters[type][name]
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const setDefaultHomeFilter = (type, name) => {
|
||||
const { user } = xo
|
||||
const { preferences } = user
|
||||
const defaultFilters = (preferences && preferences.defaultHomeFilters) || {}
|
||||
|
||||
return _setUserPreferences({
|
||||
defaultHomeFilters: {
|
||||
...defaultFilters,
|
||||
[type]: name
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Jobs ----------------------------------------------------------
|
||||
|
||||
export const deleteJob = job => (
|
||||
@@ -1338,8 +1473,8 @@ export const updateJob = job => (
|
||||
)
|
||||
)
|
||||
|
||||
export const updateSchedule = ({id, job: jobId, cron, enabled, name}) => (
|
||||
_call('schedule.set', {id, jobId, cron, enabled, name})::tap(
|
||||
export const updateSchedule = ({ id, job: jobId, cron, enabled, name, timezone }) => (
|
||||
_call('schedule.set', { id, jobId, cron, enabled, name, timezone })::tap(
|
||||
subscribeSchedules.forceRefresh
|
||||
)
|
||||
)
|
||||
|
||||
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>
|
||||
}
|
||||
}
|
||||
@@ -112,6 +112,10 @@
|
||||
@extend .fa;
|
||||
@extend .fa-play;
|
||||
}
|
||||
&-ssh-key {
|
||||
@extend .fa;
|
||||
@extend .fa-key;
|
||||
}
|
||||
|
||||
&-shown {
|
||||
@extend .fa;
|
||||
@@ -537,6 +541,10 @@
|
||||
@extend .fa;
|
||||
@extend .fa-clock-o;
|
||||
}
|
||||
&-time {
|
||||
@extend .fa;
|
||||
@extend .fa-clock-o;
|
||||
}
|
||||
&-database {
|
||||
@extend .fa;
|
||||
@extend .fa-database;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
createJob,
|
||||
createSchedule,
|
||||
setJob,
|
||||
setSchedule
|
||||
updateSchedule
|
||||
} from 'xo'
|
||||
|
||||
import { getJobValues } from '../helpers'
|
||||
@@ -189,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 () {
|
||||
@@ -208,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
|
||||
@@ -240,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,
|
||||
@@ -262,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 => {
|
||||
@@ -298,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
|
||||
@@ -334,11 +341,15 @@ export default class New extends Component {
|
||||
</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'>
|
||||
|
||||
@@ -146,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>
|
||||
@@ -158,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'>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as ComplexMatcher from 'complex-matcher'
|
||||
import * as homeFilters from 'home-filters'
|
||||
import _ from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import ceil from 'lodash/ceil'
|
||||
@@ -19,6 +20,7 @@ import SingleLineRow from 'single-line-row'
|
||||
import size from 'lodash/size'
|
||||
import { Card, CardHeader, CardBlock } from 'card'
|
||||
import {
|
||||
addCustomFilter,
|
||||
copyVms,
|
||||
deleteVms,
|
||||
emergencyShutdownHosts,
|
||||
@@ -29,7 +31,8 @@ import {
|
||||
snapshotVms,
|
||||
startVms,
|
||||
stopHosts,
|
||||
stopVms
|
||||
stopVms,
|
||||
subscribeCurrentUser
|
||||
} from 'xo'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import {
|
||||
@@ -38,6 +41,7 @@ import {
|
||||
SelectTag
|
||||
} from 'select-objects'
|
||||
import {
|
||||
addSubscriptions,
|
||||
connectStore,
|
||||
noop
|
||||
} from 'utils'
|
||||
@@ -69,10 +73,7 @@ const ITEMS_PER_PAGE = 20
|
||||
const OPTIONS = {
|
||||
host: {
|
||||
defaultFilter: 'power_state:running ',
|
||||
filters: {
|
||||
homeFilterRunningHosts: 'power_state:running ',
|
||||
homeFilterTags: 'tags:'
|
||||
},
|
||||
filters: homeFilters.host,
|
||||
mainActions: [
|
||||
{ handler: stopHosts, icon: 'host-stop' },
|
||||
{ handler: restartHostsAgents, icon: 'host-restart-agent' },
|
||||
@@ -90,13 +91,7 @@ const OPTIONS = {
|
||||
},
|
||||
VM: {
|
||||
defaultFilter: 'power_state:running ',
|
||||
filters: {
|
||||
homeFilterPendingVms: 'current_operations:"" ',
|
||||
homeFilterNonRunningVms: '!power_state:running ',
|
||||
homeFilterHvmGuests: 'virtualizationMode:hvm ',
|
||||
homeFilterRunningVms: 'power_state:running ',
|
||||
homeFilterTags: 'tags:'
|
||||
},
|
||||
filters: homeFilters.VM,
|
||||
mainActions: [
|
||||
{ handler: stopVms, icon: 'vm-stop' },
|
||||
{ handler: startVms, icon: 'vm-start' },
|
||||
@@ -135,9 +130,7 @@ const OPTIONS = {
|
||||
},
|
||||
pool: {
|
||||
defaultFilter: '',
|
||||
filters: {
|
||||
homeFilterTags: 'tags:'
|
||||
},
|
||||
filters: homeFilters.pool,
|
||||
getActions: noop,
|
||||
Item: PoolItem,
|
||||
sortOptions: [
|
||||
@@ -154,6 +147,9 @@ const TYPES = {
|
||||
|
||||
const DEFAULT_TYPE = 'VM'
|
||||
|
||||
@addSubscriptions({
|
||||
user: subscribeCurrentUser
|
||||
})
|
||||
@connectStore(() => {
|
||||
const noServersConnected = invoke(
|
||||
createGetObjectsOfType('host'),
|
||||
@@ -200,14 +196,40 @@ export default class Home extends Component {
|
||||
pathname,
|
||||
query: { ...query, t: type, s: undefined }
|
||||
})
|
||||
this._focusFilterInput()
|
||||
}
|
||||
|
||||
_getDefaultFilter (props = this.props) {
|
||||
const { type, user } = props
|
||||
|
||||
const defaultFilter = OPTIONS[type].defaultFilter
|
||||
|
||||
// No user.
|
||||
if (!user) {
|
||||
return defaultFilter
|
||||
}
|
||||
|
||||
const { defaultHomeFilters = {}, filters = {} } = user.preferences || {}
|
||||
const filterName = defaultHomeFilters[type]
|
||||
|
||||
// No filter defined in preferences.
|
||||
if (!filterName) {
|
||||
return defaultFilter
|
||||
}
|
||||
|
||||
// Filter defined.
|
||||
return homeFilters[type][filterName] ||
|
||||
filters[type][filterName] ||
|
||||
defaultFilter
|
||||
}
|
||||
|
||||
_initFilter (props) {
|
||||
const filter = this._getFilter(props)
|
||||
|
||||
// If filter is null, set a default filter.
|
||||
if (filter == null) {
|
||||
const defaultFilter = OPTIONS[props.type].defaultFilter
|
||||
if (filter == null || (this.props.user == null && props.user != null)) {
|
||||
const defaultFilter = this._getDefaultFilter(props)
|
||||
|
||||
if (defaultFilter != null) {
|
||||
this._setFilter(defaultFilter, props)
|
||||
}
|
||||
@@ -363,13 +385,35 @@ export default class Home extends Component {
|
||||
this._updateMasterCheckbox()
|
||||
}
|
||||
|
||||
_focusFilterInput = () => this.refs.filterInput.focus()
|
||||
|
||||
_addCustomFilter = () => {
|
||||
return addCustomFilter(
|
||||
this._getType(),
|
||||
this._getFilter()
|
||||
)
|
||||
}
|
||||
|
||||
_getCustomFilters () {
|
||||
const { preferences } = this.props.user || {}
|
||||
|
||||
if (!preferences) {
|
||||
return
|
||||
}
|
||||
|
||||
const customFilters = preferences.filters || {}
|
||||
return customFilters[this._getType()]
|
||||
}
|
||||
|
||||
_renderHeader () {
|
||||
const { filters } = OPTIONS[this.props.type]
|
||||
const { type } = this.props
|
||||
const { filters } = OPTIONS[type]
|
||||
const customFilters = this._getCustomFilters()
|
||||
|
||||
return <Container>
|
||||
<Row className={styles.itemRowHeader}>
|
||||
<Col mediumSize={3}>
|
||||
<DropdownButton id='typeMenu' bsStyle='info' title={TYPES[this.props.type]}>
|
||||
<DropdownButton id='typeMenu' bsStyle='info' title={TYPES[this._getType()]}>
|
||||
<MenuItem onClick={() => this._setType('VM')}>
|
||||
VM
|
||||
</MenuItem>
|
||||
@@ -383,15 +427,25 @@ export default class Home extends Component {
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<div className='input-group'>
|
||||
{!isEmpty(filters) && <div className='input-group-btn'>
|
||||
<DropdownButton id='filter' bsStyle='info' title={_('homeFilters')}>
|
||||
{map(filters, (filter, label) =>
|
||||
<MenuItem key={label} onClick={() => this._setFilter(filter)}>
|
||||
{_(label)}
|
||||
</MenuItem>
|
||||
)}
|
||||
</DropdownButton>
|
||||
</div>}
|
||||
{!isEmpty(filters) && (
|
||||
<div className='input-group-btn'>
|
||||
<DropdownButton id='filter' bsStyle='info' title={_('homeFilters')}>
|
||||
{!isEmpty(customFilters) && [
|
||||
map(customFilters, (filter, name) =>
|
||||
<MenuItem key={`custom-${name}`} onClick={() => this._setFilter(filter)}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
),
|
||||
<MenuItem divider />
|
||||
]}
|
||||
{map(filters, (filter, label) =>
|
||||
<MenuItem key={label} onClick={() => this._setFilter(filter)}>
|
||||
{_(label)}
|
||||
</MenuItem>
|
||||
)}
|
||||
</DropdownButton>
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
autoFocus
|
||||
className='form-control'
|
||||
@@ -401,11 +455,18 @@ export default class Home extends Component {
|
||||
type='text'
|
||||
/>
|
||||
<div className='input-group-btn'>
|
||||
<button
|
||||
<a
|
||||
className='btn btn-secondary'
|
||||
onClick={this._clearFilter}>
|
||||
<Icon icon='clear-search' />
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
<div className='input-group-btn'>
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
handler={this._addCustomFilter}
|
||||
icon='save'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
@@ -624,7 +685,10 @@ export default class Home extends Component {
|
||||
{' '}
|
||||
<DropdownButton bsStyle='link' id='sort' title={_('homeSortBy')}>
|
||||
{map(options.sortOptions, ({ labelId, sortBy: _sortBy, sortOrder }, key) => (
|
||||
<MenuItem key={key} onClick={() => this.setState({ sortBy: _sortBy, sortOrder })}>
|
||||
<MenuItem key={key} onClick={() => {
|
||||
this.setState({ sortBy: _sortBy, sortOrder })
|
||||
this._focusFilterInput()
|
||||
}}>
|
||||
{this._tick(_sortBy === sortBy)}
|
||||
{_sortBy === sortBy
|
||||
? <strong>{_(labelId)}</strong>
|
||||
|
||||
@@ -86,16 +86,22 @@ export default class VmItem extends Component {
|
||||
<span className={styles.itemActionButons}>
|
||||
{this._isRunning
|
||||
? <span>
|
||||
<Tooltip content={_('vmConsoleLabel')}>
|
||||
<Link to={`/vms/${vm.id}/console`}>
|
||||
<Icon icon='vm-console' size='1' fixedWidth />
|
||||
</Link>
|
||||
</Tooltip>
|
||||
<Tooltip content={_('stopVmLabel')}>
|
||||
<a onClick={this._stop}>
|
||||
<Icon icon='vm-stop' size='1' />
|
||||
<Icon icon='vm-stop' size='1' fixedWidth />
|
||||
</a>
|
||||
</Tooltip>
|
||||
</span>
|
||||
: <span>
|
||||
<Icon fixedWidth />
|
||||
<Tooltip content={_('startVmLabel')}>
|
||||
<a onClick={this._start}>
|
||||
<Icon icon='vm-start' size='1' />
|
||||
<Icon icon='vm-start' size='1' fixedWidth />
|
||||
</a>
|
||||
</Tooltip>
|
||||
</span>
|
||||
|
||||
@@ -5,6 +5,7 @@ import _, { IntlProvider } from 'intl'
|
||||
import { blockXoaAccess } from 'xoa-updater'
|
||||
import { connectStore, routes } from 'utils'
|
||||
import { Notification } from 'notification'
|
||||
import { TooltipViewer } from 'tooltip'
|
||||
// import {
|
||||
// keyHandler
|
||||
// } from 'react-key-handler'
|
||||
@@ -113,6 +114,7 @@ export default class XoApp extends Component {
|
||||
{blocked ? <XoaUpdates /> : this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
<TooltipViewer />
|
||||
<Modal />
|
||||
<Notification />
|
||||
</div>
|
||||
|
||||
@@ -20,15 +20,18 @@ import {
|
||||
|
||||
const JOB_KEY = 'genericTask'
|
||||
|
||||
const DEFAULT_CRON_PATTERN = '0 0 * * *'
|
||||
|
||||
export default class Schedules extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
action: undefined,
|
||||
actions: undefined,
|
||||
cronPattern: DEFAULT_CRON_PATTERN,
|
||||
job: undefined,
|
||||
jobs: undefined,
|
||||
cron: '* * * * *'
|
||||
timezone: undefined
|
||||
}
|
||||
this.loaded = new Promise((resolve, reject) => {
|
||||
this._resolveLoaded = resolve
|
||||
@@ -79,17 +82,16 @@ export default class Schedules extends Component {
|
||||
|
||||
_handleSubmit = () => {
|
||||
const {name, job, enabled} = this.refs
|
||||
console.log(job)
|
||||
const { cron, schedule } = this.state
|
||||
const { cronPattern, schedule, timezone } = this.state
|
||||
let save
|
||||
if (schedule) {
|
||||
console.log('JOB', job, job.value)
|
||||
schedule.job = job.value.id
|
||||
schedule.cron = cron
|
||||
schedule.cron = cronPattern
|
||||
schedule.name = name.value
|
||||
schedule.timezone = timezone
|
||||
save = updateSchedule(schedule)
|
||||
} else {
|
||||
save = createSchedule(job.value.id, cron, enabled.value, name.value)
|
||||
save = createSchedule(job.value.id, { cron: cronPattern, enabled: enabled.value, name: name.value })
|
||||
}
|
||||
return save.then(this._reset).catch(err => error('Save Schedule', err.message || String(err)))
|
||||
}
|
||||
@@ -102,39 +104,40 @@ export default class Schedules extends Component {
|
||||
return
|
||||
}
|
||||
|
||||
const {name, job, scheduler} = this.refs
|
||||
const { name, job } = this.refs
|
||||
name.value = schedule.name
|
||||
job.value = schedule.job
|
||||
scheduler.value = schedule.cron
|
||||
this.setState({
|
||||
schedule
|
||||
cronPattern: schedule.cron,
|
||||
schedule,
|
||||
timezone: schedule.timezone || null
|
||||
})
|
||||
}
|
||||
|
||||
_reset = () => {
|
||||
this.setState({
|
||||
schedule: undefined
|
||||
cronPattern: DEFAULT_CRON_PATTERN,
|
||||
schedule: undefined,
|
||||
timezone: undefined
|
||||
}, () => {
|
||||
const {name, job, enabled, scheduler} = this.refs
|
||||
const { name, job, enabled } = this.refs
|
||||
name.value = ''
|
||||
enabled.value = false
|
||||
job.value = undefined
|
||||
scheduler.value = undefined
|
||||
})
|
||||
}
|
||||
|
||||
_updateCronPattern = cron => {
|
||||
this.setState({
|
||||
cron
|
||||
})
|
||||
_updateCronPattern = value => {
|
||||
this.setState(value)
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
cron,
|
||||
cronPattern,
|
||||
jobs,
|
||||
schedule,
|
||||
schedules
|
||||
schedules,
|
||||
timezone
|
||||
} = this.state
|
||||
return <div>
|
||||
<h1>Schedules</h1>
|
||||
@@ -154,8 +157,12 @@ export default class Schedules extends Component {
|
||||
}
|
||||
</form>
|
||||
<fieldset>
|
||||
<Scheduler ref='scheduler' onChange={this._updateCronPattern} />
|
||||
<SchedulePreview cron={cron} />
|
||||
<Scheduler
|
||||
cronPattern={cronPattern}
|
||||
onChange={this._updateCronPattern}
|
||||
timezone={timezone}
|
||||
/>
|
||||
<SchedulePreview cronPattern={cronPattern} />
|
||||
</fieldset>
|
||||
<br />
|
||||
<div className='form-group'>
|
||||
@@ -163,7 +170,7 @@ export default class Schedules extends Component {
|
||||
{process.env.XOA_PLAN > 3
|
||||
? <span><ActionButton form='newScheduleForm' handler={this._handleSubmit} icon='save' btnStyle='primary'>{_('saveBackupJob')}</ActionButton>
|
||||
{' '}
|
||||
<button type='button' className='btn btn-default' onClick={this._reset}>{_('selectTableReset')}</button></span>
|
||||
<button type='button' className='btn btn-secondary' onClick={this._reset}>{_('selectTableReset')}</button></span>
|
||||
: <span><Upgrade place='health' available={4} /></span>
|
||||
}
|
||||
</div>
|
||||
@@ -172,7 +179,8 @@ export default class Schedules extends Component {
|
||||
<tr>
|
||||
<th>{_('jobName')}</th>
|
||||
<th>{_('job')}</th>
|
||||
<th>{_('jobScheduling')}</th>
|
||||
<th className='hidden-xs-down'>{_('jobScheduling')}</th>
|
||||
<th className='hidden-xs-down'>{_('jobTimezone')}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -183,7 +191,8 @@ export default class Schedules extends Component {
|
||||
<span>{schedule.name} <span className='text-muted'>({schedule.id})</span></span>
|
||||
</td>
|
||||
<td>{jobs[schedule.job] && <span>{jobs[schedule.job].name} - {jobs[schedule.job].method} <span className='text-muted'>({schedule.job})</span></span>}</td>
|
||||
<td>{schedule.cron}</td>
|
||||
<td className='hidden-xs-down'>{schedule.cron}</td>
|
||||
<td className='hidden-xs-down'>{schedule.timezone || _('jobServerTimezone')}</td>
|
||||
<td>
|
||||
<button type='button' className='btn btn-primary' onClick={() => this._edit(schedule.id)}><Icon icon='edit' /></button>
|
||||
{' '}
|
||||
|
||||
@@ -94,7 +94,11 @@ export default class Menu extends Component {
|
||||
const noResourceSets = isEmpty(this.state.resourceSets)
|
||||
|
||||
const items = [
|
||||
{ to: '/home', icon: 'menu-home', label: 'homePage' },
|
||||
{ to: '/home', icon: 'menu-home', label: 'homePage', subMenu: [
|
||||
{ to: '/home?t=VM', icon: 'vm', label: 'homeVmPage' },
|
||||
{ to: '/home?t=host', icon: 'host', label: 'homeHostPage' },
|
||||
{ to: '/home?t=pool', icon: 'pool', label: 'homePoolPage' }
|
||||
]},
|
||||
{ to: '/dashboard/overview', icon: 'menu-dashboard', label: 'dashboardPage', subMenu: [
|
||||
{ to: '/dashboard/overview', icon: 'menu-dashboard-overview', label: 'overviewDashboardPage' },
|
||||
{ to: '/dashboard/visualizations', icon: 'menu-dashboard-visualization', label: 'overviewVisualizationDashboardPage' },
|
||||
|
||||
@@ -29,7 +29,9 @@ import {
|
||||
createVms,
|
||||
getCloudInitConfig,
|
||||
subscribePermissions,
|
||||
subscribeResourceSets
|
||||
subscribeResourceSets,
|
||||
XEN_DEFAULT_CPU_CAP,
|
||||
XEN_DEFAULT_CPU_WEIGHT
|
||||
} from 'xo'
|
||||
import {
|
||||
SelectNetwork,
|
||||
@@ -48,6 +50,7 @@ import {
|
||||
Toggle
|
||||
} from 'form'
|
||||
import {
|
||||
buildTemplate,
|
||||
connectStore,
|
||||
formatSize,
|
||||
noop,
|
||||
@@ -85,10 +88,10 @@ const LineItem = ({ children }) => (
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
const Item = ({ label, children }) => (
|
||||
const Item = ({ label, children, className }) => (
|
||||
<span className={styles.item}>
|
||||
{label && <span>{_(label)} </span>}
|
||||
<span className={styles.input}>{children}</span>
|
||||
<span className={classNames(styles.input, className)}>{children}</span>
|
||||
</span>
|
||||
)
|
||||
|
||||
@@ -165,17 +168,19 @@ export default class NewVm extends BaseComponent {
|
||||
bootAfterCreate: true,
|
||||
configDrive: false,
|
||||
CPUs: '',
|
||||
cpuWeight: 1,
|
||||
cpuCap: '',
|
||||
cpuWeight: '',
|
||||
existingDisks: {},
|
||||
fastClone: true,
|
||||
multipleVms: false,
|
||||
name_label: '',
|
||||
name_description: '',
|
||||
nameLabels: map(Array(NB_VMS_MIN), (_, index) => `VM_${index + 1}`),
|
||||
|
||||
namePattern: '{name}_%',
|
||||
nbVms: NB_VMS_MIN,
|
||||
VDIs: [],
|
||||
VIFs: []
|
||||
VIFs: [],
|
||||
seqStart: 1
|
||||
})
|
||||
}
|
||||
|
||||
@@ -230,7 +235,8 @@ export default class NewVm extends BaseComponent {
|
||||
// TODO: To be added in xo-server
|
||||
// vm.set parameters
|
||||
CPUs: state.CPUs,
|
||||
cpuWeight: state.cpuWeight,
|
||||
cpuWeight: state.cpuWeight === '' ? null : state.cpuWeight,
|
||||
cpuCap: state.cpuCap === '' ? null : state.cpuCap,
|
||||
name_description: state.name_description,
|
||||
memory: state.memory,
|
||||
pv_args: state.pv_args,
|
||||
@@ -290,16 +296,19 @@ export default class NewVm extends BaseComponent {
|
||||
})
|
||||
}
|
||||
const name_label = state.name_label === '' || !state.name_labelHasChanged ? template.name_label : state.name_label
|
||||
const name_description = state.name_description === '' || !state.name_descriptionHasChanged ? template.name_description || '' : state.name_description
|
||||
const replacer = this._buildTemplate()
|
||||
this._setState({
|
||||
// infos
|
||||
name_label,
|
||||
template,
|
||||
name_description: state.name_description === '' || !state.name_descriptionHasChanged ? template.name_description || '' : state.name_description,
|
||||
nameLabels: map(Array(+state.nbVms), (_, index) => `${name_label}_${index + 1}`),
|
||||
name_description,
|
||||
nameLabels: map(Array(+state.nbVms), (_, index) => replacer({ name_label, name_description, template }, index + 1)),
|
||||
// performances
|
||||
memory: template.memory.size,
|
||||
CPUs: template.CPUs.number,
|
||||
cpuWeight: 1,
|
||||
cpuCap: '',
|
||||
cpuWeight: '',
|
||||
// installation
|
||||
installMethod: template.install_methods && template.install_methods[0] || state.installMethod,
|
||||
customConfig: '#cloud-config\n#hostname: myhostname\n#ssh_authorized_keys:\n# - ssh-rsa <myKey>\n#packages:\n# - htop\n',
|
||||
@@ -398,6 +407,13 @@ export default class NewVm extends BaseComponent {
|
||||
})
|
||||
return network && network.id
|
||||
}
|
||||
_buildTemplate = createSelector(
|
||||
() => this.state.state.namePattern,
|
||||
namePattern => buildTemplate(namePattern, {
|
||||
'{name}': state => state.name_label || '',
|
||||
'%': (_, i) => i
|
||||
})
|
||||
)
|
||||
|
||||
// On change -------------------------------------------------------------------
|
||||
/*
|
||||
@@ -451,25 +467,28 @@ export default class NewVm extends BaseComponent {
|
||||
this._setState({ [prop]: value })
|
||||
}
|
||||
}
|
||||
|
||||
_updateNbVms = () => {
|
||||
const { nbVms, name_label, nameLabels } = this.state.state
|
||||
const { nbVms, nameLabels, seqStart } = this.state.state
|
||||
const nbVmsClamped = clamp(nbVms, NB_VMS_MIN, NB_VMS_MAX)
|
||||
const newNameLabels = [ ...nameLabels ]
|
||||
if (nbVmsClamped < nameLabels.length) {
|
||||
this._setState({ nameLabels: slice(newNameLabels, 0, nbVmsClamped) })
|
||||
} else {
|
||||
for (let i = nameLabels.length + 1; i <= nbVmsClamped; i++) {
|
||||
newNameLabels.push(`${name_label || 'VM'}_${i}`)
|
||||
const replacer = this._buildTemplate()
|
||||
for (let i = +seqStart + nameLabels.length; i <= +seqStart + nbVmsClamped - 1; i++) {
|
||||
newNameLabels.push(replacer(this.state.state, i))
|
||||
}
|
||||
this._setState({ nameLabels: newNameLabels })
|
||||
}
|
||||
}
|
||||
_updateNameLabels = () => {
|
||||
const { name_label, nameLabels } = this.state.state
|
||||
const { nameLabels, seqStart } = this.state.state
|
||||
const nbVms = nameLabels.length
|
||||
const newNameLabels = []
|
||||
for (let i = 1; i <= nbVms; i++) {
|
||||
newNameLabels.push(`${name_label || 'VM'}_${i}`)
|
||||
const replacer = this._buildTemplate()
|
||||
for (let i = +seqStart; i <= +seqStart + nbVms - 1; i++) {
|
||||
newNameLabels.push(replacer(this.state.state, i))
|
||||
}
|
||||
this._setState({ nameLabels: newNameLabels })
|
||||
}
|
||||
@@ -599,8 +618,11 @@ export default class NewVm extends BaseComponent {
|
||||
name_label,
|
||||
nameLabels,
|
||||
nbVms,
|
||||
namePattern,
|
||||
seqStart,
|
||||
template
|
||||
} = this.state.state
|
||||
const { formatMessage } = this.props.intl
|
||||
return <Section icon='new-vm-infos' title='newVmInfoPanel' done={this._isInfoDone()}>
|
||||
<SectionContent>
|
||||
<Item label='newVmTemplateLabel'>
|
||||
@@ -636,31 +658,54 @@ export default class NewVm extends BaseComponent {
|
||||
/>
|
||||
</Item>
|
||||
</SectionContent>
|
||||
<SectionContent column>
|
||||
<LineItem>
|
||||
<Item>
|
||||
{_('newVmMultipleVms')}
|
||||
|
||||
<Toggle value={multipleVms} onChange={this._getOnChange('multipleVms')} />
|
||||
<br />
|
||||
<div className='input-group'>
|
||||
<DebounceInput
|
||||
className='form-control'
|
||||
debounceTimeout={DEBOUNCE_TIMEOUT}
|
||||
disabled={!multipleVms}
|
||||
max={NB_VMS_MAX}
|
||||
min={NB_VMS_MIN}
|
||||
onChange={this._getOnChange('nbVms')}
|
||||
type='number'
|
||||
value={nbVms}
|
||||
/>
|
||||
<span className='input-group-btn'>
|
||||
<Button bsStyle='secondary' disabled={!multipleVms} onClick={this._updateNbVms}><Icon icon='arrow-right' /></Button>
|
||||
</span>
|
||||
</div>
|
||||
<a className={styles.refreshNames} onClick={this._updateNameLabels}><Icon icon='refresh' /></a>
|
||||
</Item>
|
||||
</LineItem>
|
||||
<SectionContent>
|
||||
<Item>
|
||||
{_('newVmMultipleVms')}
|
||||
|
||||
<Toggle value={multipleVms} onChange={this._getOnChange('multipleVms')} />
|
||||
</Item>
|
||||
<Item>
|
||||
{_('newVmMultipleVmsPattern')}
|
||||
|
||||
<DebounceInput
|
||||
className='form-control'
|
||||
debounceTimeout={DEBOUNCE_TIMEOUT}
|
||||
disabled={!multipleVms}
|
||||
onChange={this._getOnChange('namePattern')}
|
||||
placeholder={formatMessage(messages.newVmMultipleVmsPatternPlaceholder)}
|
||||
value={namePattern}
|
||||
/>
|
||||
</Item>
|
||||
<Item>
|
||||
{_('newVmFirstIndex')}
|
||||
|
||||
<DebounceInput
|
||||
className={'form-control'}
|
||||
debounceTimeout={DEBOUNCE_TIMEOUT}
|
||||
disabled={!multipleVms}
|
||||
onChange={this._getOnChange('seqStart')}
|
||||
type='number'
|
||||
value={seqStart}
|
||||
/>
|
||||
</Item>
|
||||
<Item className='input-group'>
|
||||
<DebounceInput
|
||||
className='form-control'
|
||||
debounceTimeout={DEBOUNCE_TIMEOUT}
|
||||
disabled={!multipleVms}
|
||||
max={NB_VMS_MAX}
|
||||
min={NB_VMS_MIN}
|
||||
onChange={this._getOnChange('nbVms')}
|
||||
type='number'
|
||||
value={nbVms}
|
||||
/>
|
||||
<span className='input-group-btn'>
|
||||
<Button bsStyle='secondary' disabled={!multipleVms} onClick={this._updateNbVms}><Icon icon='arrow-right' /></Button>
|
||||
</span>
|
||||
</Item>
|
||||
<Item>
|
||||
<a className={styles.refreshNames} onClick={this._updateNameLabels}><Icon icon='refresh' /></a>
|
||||
</Item>
|
||||
{multipleVms && <LineItem>
|
||||
{map(nameLabels, (nameLabel, index) =>
|
||||
<Item key={`nameLabel_${index}`}>
|
||||
@@ -677,7 +722,8 @@ export default class NewVm extends BaseComponent {
|
||||
}
|
||||
|
||||
_renderPerformances = () => {
|
||||
const { CPUs, cpuWeight, memory } = this.state.state
|
||||
const { CPUs, cpuCap, cpuWeight, memory } = this.state.state
|
||||
const { formatMessage } = this.props.intl
|
||||
return <Section icon='new-vm-perf' title='newVmPerfPanel' done={this._isPerformancesDone()}>
|
||||
<SectionContent>
|
||||
<Item label='newVmVcpusLabel'>
|
||||
@@ -694,16 +740,27 @@ export default class NewVm extends BaseComponent {
|
||||
<SizeInput value={memory} onChange={this._getOnChange('memory')} className={styles.sizeInput} />
|
||||
</Item>
|
||||
<Item label='newVmCpuWeightLabel'>
|
||||
<select
|
||||
<DebounceInput
|
||||
className='form-control'
|
||||
debounceTimeout={DEBOUNCE_TIMEOUT}
|
||||
min={0}
|
||||
max={65535}
|
||||
onChange={this._getOnChange('cpuWeight')}
|
||||
placeholder={formatMessage(messages.newVmDefaultCpuWeight, { value: XEN_DEFAULT_CPU_WEIGHT })}
|
||||
type='number'
|
||||
value={cpuWeight}
|
||||
>
|
||||
{_('newVmCpuWeightQuarter', message => <option value={0.25}>{message}</option>)}
|
||||
{_('newVmCpuWeightHalf', message => <option value={0.5}>{message}</option>)}
|
||||
{_('newVmCpuWeightNormal', message => <option value={1}>{message}</option>)}
|
||||
{_('newVmCpuWeightDouble', message => <option value={2}>{message}</option>)}
|
||||
</select>
|
||||
/>
|
||||
</Item>
|
||||
<Item label='newVmCpuCapLabel'>
|
||||
<DebounceInput
|
||||
className='form-control'
|
||||
debounceTimeout={DEBOUNCE_TIMEOUT}
|
||||
min={0}
|
||||
onChange={this._getOnChange('cpuCap')}
|
||||
placeholder={formatMessage(messages.newVmDefaultCpuCap, { value: XEN_DEFAULT_CPU_CAP })}
|
||||
type='number'
|
||||
value={cpuCap}
|
||||
/>
|
||||
</Item>
|
||||
</SectionContent>
|
||||
</Section>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import _, { messages } from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import Component from 'base-component'
|
||||
import filter from 'lodash/filter'
|
||||
import Icon from 'icon'
|
||||
import includes from 'lodash/includes'
|
||||
@@ -7,49 +8,23 @@ import info, { error } from 'notification'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import map from 'lodash/map'
|
||||
import Page from '../../page'
|
||||
import React, { Component } from 'react'
|
||||
import propTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import store from 'store'
|
||||
import trim from 'lodash/trim'
|
||||
import Wizard, { Section } from 'wizard'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { confirm } from 'modal'
|
||||
import { connectStore, formatSize } from 'utils'
|
||||
import { GenericSelect, SelectHost } from 'select-objects'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { injectIntl } from 'react-intl'
|
||||
import { Password } from 'form'
|
||||
import { Password, Select } from 'form'
|
||||
import { SelectHost } from 'select-objects'
|
||||
import {
|
||||
createFilter,
|
||||
createGetObjectsOfType,
|
||||
createSelector,
|
||||
getObject
|
||||
} from 'selectors'
|
||||
|
||||
class SelectIqn extends GenericSelect {
|
||||
_computeOptions (props) {
|
||||
return map(props.options, iqn => ({
|
||||
value: iqn,
|
||||
label: `${iqn.iqn} (${iqn.ip})`
|
||||
}))
|
||||
}
|
||||
get value () {
|
||||
const value = this.state.value
|
||||
return value && value.value || value
|
||||
}
|
||||
}
|
||||
|
||||
class SelectLun extends GenericSelect {
|
||||
_computeOptions (props) {
|
||||
return map(props.options, lun => ({
|
||||
value: lun,
|
||||
label: `LUN ${lun.id}: ${lun.serial} - ${formatSize(+lun.size)} - (${lun.vendor})`
|
||||
}))
|
||||
}
|
||||
get value () {
|
||||
const value = this.state.value
|
||||
return value && value.value || value
|
||||
}
|
||||
}
|
||||
|
||||
import {
|
||||
createSrIso,
|
||||
createSrIscsi,
|
||||
@@ -66,6 +41,98 @@ import {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
onChange: propTypes.func.isRequired,
|
||||
options: propTypes.array.isRequired
|
||||
})
|
||||
class SelectIqn extends Component {
|
||||
_computeOptions (props = this.props) {
|
||||
this.setState({
|
||||
options: map(props.options, (iqn, id) => ({
|
||||
value: `${iqn.ip}$${iqn.iqn}`,
|
||||
label: `${iqn.iqn} (${iqn.ip})`
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
_handleChange = value => {
|
||||
const { onChange } = this.props
|
||||
|
||||
value = value.value
|
||||
const index = value.indexOf('$')
|
||||
|
||||
this.setState({
|
||||
value
|
||||
}, () => onChange({
|
||||
ip: value.slice(0, index),
|
||||
iqn: value.slice(index + 1)
|
||||
}))
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this._computeOptions()
|
||||
}
|
||||
|
||||
componentWillReceiveProps (props) {
|
||||
this._computeOptions(props)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { state } = this
|
||||
return (
|
||||
<Select
|
||||
clearable={false}
|
||||
onChange={this._handleChange}
|
||||
options={state.options}
|
||||
value={state.value}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@propTypes({
|
||||
onChange: propTypes.func.isRequired,
|
||||
options: propTypes.array.isRequired
|
||||
})
|
||||
class SelectLun extends Component {
|
||||
_computeOptions (props = this.props) {
|
||||
this.setState({
|
||||
options: map(props.options, lun => ({
|
||||
value: lun.id,
|
||||
label: `LUN ${lun.id}: ${lun.serial} - ${formatSize(+lun.size)} - (${lun.vendor})`
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
_handleChange = value => {
|
||||
const { onChange, options } = this.props
|
||||
value = value.value
|
||||
this.setState({ value }, () => onChange(options[value]))
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this._computeOptions()
|
||||
}
|
||||
|
||||
componentWillReceiveProps (props) {
|
||||
this._computeOptions(props)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { state } = this
|
||||
return (
|
||||
<Select
|
||||
clearable={false}
|
||||
onChange={this._handleChange}
|
||||
options={state.options}
|
||||
value={state.value}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const SR_TYPE_TO_LABEL = {
|
||||
nfs: 'NFS',
|
||||
iscsi: 'iSCSI',
|
||||
@@ -160,7 +227,7 @@ export default class New extends Component {
|
||||
return createSrNfs(host.id, name.value, description.value, server.value, path)
|
||||
},
|
||||
iscsi: async () => {
|
||||
const previous = await probeSrIscsiExists(host.id, iqn.ip, iqn.iqn, lun.scsiId, port.value, username && username.value, password && password.value)
|
||||
const previous = await probeSrIscsiExists(host.id, iqn.ip, iqn.iqn, lun.scsiId, +port.value, username && username.value, password && password.value)
|
||||
if (previous && previous.length > 0) {
|
||||
try {
|
||||
await confirm({title: _('existingLunModalTitle'),
|
||||
@@ -170,7 +237,7 @@ export default class New extends Component {
|
||||
return
|
||||
}
|
||||
}
|
||||
return createSrIscsi(host.id, name.value, description.value, iqn.ip, iqn.iqn, lun.scsiId, port.value, username && username.value, password && password.value)
|
||||
return createSrIscsi(host.id, name.value, description.value, iqn.ip, iqn.iqn, lun.scsiId, +port.value, username && username.value, password && password.value)
|
||||
},
|
||||
lvm: () => createSrLvm(host.id, name.value, description.value, device.value),
|
||||
local: () => createSrIso(host.id, name.value, description.value, localPath.value, 'local'),
|
||||
@@ -238,7 +305,7 @@ export default class New extends Component {
|
||||
|
||||
try {
|
||||
this.setState({loading: true})
|
||||
const list = await probeSrIscsiExists(host.id, iqn.ip, iqn.iqn, lun.scsiId, port.value, username && username.value, password && password.value)
|
||||
const list = await probeSrIscsiExists(host.id, iqn.ip, iqn.iqn, lun.scsiId, +port.value, username && username.value, password && password.value)
|
||||
const srIds = map(this.getHostSrs(), sr => sr.id)
|
||||
const used = filter(list, item => includes(srIds, item.id))
|
||||
const unused = filter(list, item => !includes(srIds, item.id))
|
||||
@@ -284,7 +351,7 @@ export default class New extends Component {
|
||||
paths
|
||||
})
|
||||
} else if (type === 'iscsi') {
|
||||
const iqns = await probeSrIscsiIqns(host.id, server.value, port.value, username && username.value, password && password.value)
|
||||
const iqns = await probeSrIscsiIqns(host.id, server.value, +port.value, username && username.value, password && password.value)
|
||||
if (!iqns.length) {
|
||||
info('iSCSI Detection', 'No IQNs found')
|
||||
} else {
|
||||
|
||||
@@ -7,7 +7,7 @@ import pick from 'lodash/pick'
|
||||
import React, { cloneElement, Component } from 'react'
|
||||
import { NavLink, NavTabs } from 'nav'
|
||||
import { Text } from 'editable'
|
||||
import { editPool, isSrWritable } from 'xo'
|
||||
import { editPool } from 'xo'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import {
|
||||
connectStore,
|
||||
@@ -63,7 +63,7 @@ import TabStorage from './tab-storage'
|
||||
const getPoolSrs = createGetObjectsOfType('SR').filter(
|
||||
createSelector(
|
||||
getPool,
|
||||
({ id }) => sr => isSrWritable(sr) && sr.$pool === id
|
||||
({ id }) => sr => sr.$pool === id
|
||||
)
|
||||
).sort()
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
createResourceSet,
|
||||
deleteResourceSet,
|
||||
editRessourceSet,
|
||||
recomputeResourceSetsLimits,
|
||||
subscribeResourceSets
|
||||
} from 'xo'
|
||||
|
||||
@@ -488,47 +489,59 @@ export default class Administration extends Component {
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
{!isEmpty(resourceSets)
|
||||
? map(resourceSets, (resourceSet, key) => (
|
||||
<div key={key} className='p-b-1'>
|
||||
<h5 className='form-inline clearfix'>
|
||||
{resourceSet.name}
|
||||
<div className='form-group pull-xs-right'>
|
||||
<div className='btn-toolbar'>
|
||||
<div className='btn-group'>
|
||||
<button className='btn btn-primary' type='button' onClick={() => { this._editResourceSet(resourceSet) }}>
|
||||
<Icon icon='edit' /> {_('editResourceSet')}
|
||||
</button>
|
||||
</div>
|
||||
<div className='btn-group'>
|
||||
<button className='btn btn-danger' type='button' onClick={() => { this._deleteResourceSet(resourceSet) }}>
|
||||
<Icon icon='delete' /> {_('deleteResourceSet')}
|
||||
</button>
|
||||
? [
|
||||
<div className='text-xs-center'>
|
||||
<ActionButton
|
||||
className='btn btn-secondary'
|
||||
handler={recomputeResourceSetsLimits}
|
||||
icon='refresh'
|
||||
>
|
||||
{_('recomputeResourceSets')}
|
||||
</ActionButton>
|
||||
</div>,
|
||||
<br />,
|
||||
map(resourceSets, (resourceSet, key) => (
|
||||
<div key={key} className='p-b-1'>
|
||||
<h5 className='form-inline clearfix'>
|
||||
{resourceSet.name}
|
||||
<div className='form-group pull-xs-right'>
|
||||
<div className='btn-toolbar'>
|
||||
<div className='btn-group'>
|
||||
<button className='btn btn-primary' type='button' onClick={() => { this._editResourceSet(resourceSet) }}>
|
||||
<Icon icon='edit' /> {_('editResourceSet')}
|
||||
</button>
|
||||
</div>
|
||||
<div className='btn-group'>
|
||||
<button className='btn btn-danger' type='button' onClick={() => { this._deleteResourceSet(resourceSet) }}>
|
||||
<Icon icon='delete' /> {_('deleteResourceSet')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</h5>
|
||||
<ul key={key} className='list-group'>
|
||||
<li className='list-group-item'>
|
||||
<Subjects subjects={resourceSet.subjects} />
|
||||
</li>
|
||||
{map(resourceSet.objectsByType, (objectsSet, type) => (
|
||||
<li key={type} className='list-group-item'>
|
||||
{map(objectsSet, object => renderXoItem(object, { className: 'm-r-1' }))}
|
||||
</h5>
|
||||
<ul key={key} className='list-group'>
|
||||
<li className='list-group-item'>
|
||||
<Subjects subjects={resourceSet.subjects} />
|
||||
</li>
|
||||
))}
|
||||
<li className='list-group-item'>
|
||||
<Limits limits={resourceSet.limits} />
|
||||
</li>
|
||||
</ul>
|
||||
{resourceSet.missingObjects.length > 0 &&
|
||||
<div className='alert alert-danger m-t-1' role='alert'>
|
||||
<strong>{_('resourceSetMissingObjects')}</strong> {resourceSet.missingObjects.join(', ')}
|
||||
</div>
|
||||
}
|
||||
<hr />
|
||||
</div>
|
||||
))
|
||||
: _('noResourceSets')
|
||||
{map(resourceSet.objectsByType, (objectsSet, type) => (
|
||||
<li key={type} className='list-group-item'>
|
||||
{map(objectsSet, object => renderXoItem(object, { className: 'm-r-1' }))}
|
||||
</li>
|
||||
))}
|
||||
<li className='list-group-item'>
|
||||
<Limits limits={resourceSet.limits} />
|
||||
</li>
|
||||
</ul>
|
||||
{resourceSet.missingObjects.length > 0 &&
|
||||
<div className='alert alert-danger m-t-1' role='alert'>
|
||||
<strong>{_('resourceSetMissingObjects')}</strong> {resourceSet.missingObjects.join(', ')}
|
||||
</div>
|
||||
}
|
||||
<hr />
|
||||
</div>
|
||||
))
|
||||
]
|
||||
: _('noResourceSets')
|
||||
}
|
||||
</CardBlock>
|
||||
</Card>
|
||||
|
||||
@@ -159,7 +159,7 @@ export default class Dashboard extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { resourceSets } = this
|
||||
const { resourceSets } = this.state
|
||||
|
||||
return process.env.XOA_PLAN > 3
|
||||
? <Container>
|
||||
|
||||
@@ -2,6 +2,7 @@ import _ from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import ActionRowButton from 'action-row-button'
|
||||
import Component from 'base-component'
|
||||
import filter from 'lodash/filter'
|
||||
import forEach from 'lodash/forEach'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import keyBy from 'lodash/keyBy'
|
||||
@@ -10,11 +11,14 @@ import pickBy from 'lodash/pickBy'
|
||||
import React from 'react'
|
||||
import renderXoItem, { renderXoItemFromId } from 'render-xo-item'
|
||||
import SortedTable from 'sorted-table'
|
||||
import toArray from 'lodash/toArray'
|
||||
import Upgrade from 'xoa-upgrade'
|
||||
import store from 'store'
|
||||
import { connectStore } from 'utils'
|
||||
import { Container } from 'grid'
|
||||
import { error } from 'notification'
|
||||
import { SelectHighLevelObject, SelectRole, SelectSubject } from 'select-objects'
|
||||
import { ButtonGroup } from 'react-bootstrap-4/lib'
|
||||
|
||||
import {
|
||||
createGetObjectsOfType,
|
||||
@@ -31,6 +35,14 @@ import {
|
||||
subscribeUsers
|
||||
} from 'xo'
|
||||
|
||||
const TYPES = [
|
||||
'VM',
|
||||
'host',
|
||||
'pool',
|
||||
'SR',
|
||||
'network'
|
||||
]
|
||||
|
||||
const ACL_COLUMNS = [
|
||||
{
|
||||
name: _('subjectName'),
|
||||
@@ -125,6 +137,7 @@ export default class Acls extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
isAllSelected: {},
|
||||
subjects: [],
|
||||
objects: [],
|
||||
role: undefined
|
||||
@@ -135,6 +148,23 @@ export default class Acls extends Component {
|
||||
_handleSelectRole = action => this.setState({action})
|
||||
_handleSelectSubject = subjects => this.setState({subjects})
|
||||
|
||||
_toggleAll = type => {
|
||||
const { isAllSelected, objects } = this.state
|
||||
let newObjects
|
||||
if (!isAllSelected[type]) {
|
||||
newObjects = [ ...objects, ...toArray(createGetObjectsOfType(type)(store.getState())) ]
|
||||
} else {
|
||||
newObjects = filter(objects, object => object.type !== type)
|
||||
}
|
||||
this.refs.selectObject.value = newObjects
|
||||
this.setState({
|
||||
objects: newObjects,
|
||||
isAllSelected: {
|
||||
...isAllSelected,
|
||||
[type]: !isAllSelected[type] }
|
||||
})
|
||||
}
|
||||
|
||||
_addAcl = async () => {
|
||||
const {
|
||||
subjects,
|
||||
@@ -160,6 +190,7 @@ export default class Acls extends Component {
|
||||
|
||||
render () {
|
||||
const {
|
||||
isAllSelected,
|
||||
objects,
|
||||
action,
|
||||
subjects
|
||||
@@ -174,6 +205,11 @@ export default class Acls extends Component {
|
||||
<div className='form-group'>
|
||||
<SelectHighLevelObject ref='selectObject' multi onChange={this._handleSelectObjects} />
|
||||
</div>
|
||||
<ButtonGroup className='p-b-1'>
|
||||
{map(TYPES, type =>
|
||||
<ActionButton key={type} btnStyle={isAllSelected[type] ? 'success' : 'secondary'} size='small' icon={type.toLowerCase()} handler={this._toggleAll} handlerParam={type} />
|
||||
)}
|
||||
</ButtonGroup>
|
||||
<div className='form-group'>
|
||||
<SelectRole ref='selectAction' onChange={this._handleSelectRole} />
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import _ from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import ActionToggle from 'action-toggle'
|
||||
import GenericInput from 'json-schema-input'
|
||||
import Icon from 'icon'
|
||||
import React, { Component } from 'react'
|
||||
import _ from 'intl'
|
||||
import map from 'lodash/map'
|
||||
import React, { Component } from 'react'
|
||||
import size from 'lodash/size'
|
||||
import { addSubscriptions } from 'utils'
|
||||
import { generateUiSchema } from 'xo-json-schema-input'
|
||||
import { Row, Col } from 'grid'
|
||||
import { lastly } from 'promise-toolbox'
|
||||
import { Row, Col } from 'grid'
|
||||
import {
|
||||
configurePlugin,
|
||||
disablePluginAutoload,
|
||||
@@ -111,13 +112,21 @@ class Plugin extends Component {
|
||||
})
|
||||
}
|
||||
|
||||
_applyPredefinedConfiguration = () => {
|
||||
const configName = this.refs.selectPredefinedConfiguration.value
|
||||
this.refs.pluginInput.value = this.props.configurationPresets[configName]
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
props,
|
||||
state
|
||||
} = this
|
||||
const { expanded, edit } = state
|
||||
const { loaded } = props
|
||||
const {
|
||||
configurationPresets,
|
||||
loaded
|
||||
} = props
|
||||
const { formId } = this
|
||||
|
||||
return (
|
||||
@@ -149,6 +158,34 @@ class Plugin extends Component {
|
||||
</Row>
|
||||
{expanded &&
|
||||
<form id={formId}>
|
||||
{size(configurationPresets) > 0 && (
|
||||
<div>
|
||||
<legend>{_('pluginConfigurationPresetTitle')}</legend>
|
||||
<span className='text-muted'>
|
||||
<p>{_('pluginConfigurationChoosePreset')}</p>
|
||||
</span>
|
||||
<div className='input-group'>
|
||||
<select className='form-control' disabled={!edit} ref='selectPredefinedConfiguration'>
|
||||
{map(configurationPresets, (_, name) => (
|
||||
<option key={name} value={name}>
|
||||
{name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<span className='input-group-btn'>
|
||||
<button
|
||||
className='btn btn-primary'
|
||||
disabled={!edit}
|
||||
onClick={this._applyPredefinedConfiguration}
|
||||
type='button'
|
||||
>
|
||||
{_('applyPluginPreset')}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<hr />
|
||||
</div>
|
||||
)}
|
||||
<GenericInput
|
||||
disabled={!edit}
|
||||
label='Configuration'
|
||||
|
||||
@@ -1,17 +1,43 @@
|
||||
import * as FormGrid from 'form-grid'
|
||||
import * as homeFilters from 'home-filters'
|
||||
import _, { messages } from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import BaseComponent from 'base-component'
|
||||
import Component from 'base-component'
|
||||
import Icon from 'icon'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import map from 'lodash/map'
|
||||
import propTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import { Text } from 'editable'
|
||||
import { alert } from 'modal'
|
||||
import { connectStore } from 'utils'
|
||||
import { changePassword } from 'xo'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { getLang, getUser } from 'selectors'
|
||||
import { getLang } from 'selectors'
|
||||
import { injectIntl } from 'react-intl'
|
||||
import { Select } from 'form'
|
||||
import {
|
||||
Card,
|
||||
CardBlock,
|
||||
CardHeader
|
||||
} from 'card'
|
||||
import {
|
||||
addSubscriptions,
|
||||
connectStore,
|
||||
noop
|
||||
} from 'utils'
|
||||
import {
|
||||
addSshKey,
|
||||
changePassword,
|
||||
deleteSshKey,
|
||||
editCustomFilter,
|
||||
removeCustomFilter,
|
||||
setDefaultHomeFilter,
|
||||
subscribeCurrentUser
|
||||
} from 'xo'
|
||||
|
||||
import Page from '../page'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const HEADER = <Container>
|
||||
<Row>
|
||||
<Col>
|
||||
@@ -20,12 +46,185 @@ const HEADER = <Container>
|
||||
</Row>
|
||||
</Container>
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const FILTER_TYPE_TO_LABEL_ID = {
|
||||
VM: 'homeTypeVm',
|
||||
host: 'homeTypeHost',
|
||||
pool: 'homeTypePool'
|
||||
}
|
||||
|
||||
const getDefaultFilter = (defaultFilters, type) => {
|
||||
if (defaultFilters == null) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return defaultFilters[type] || ''
|
||||
}
|
||||
|
||||
const getUserPreferences = user => user.preferences || {}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
customFilters: propTypes.object,
|
||||
defaultFilter: propTypes.string.isRequired,
|
||||
filters: propTypes.object.isRequired,
|
||||
type: propTypes.string.isRequired
|
||||
})
|
||||
class DefaultFilterPicker extends Component {
|
||||
_computeOptions (props) {
|
||||
const {
|
||||
customFilters,
|
||||
filters
|
||||
} = props
|
||||
|
||||
// Custom filters.
|
||||
const options = [{
|
||||
label: _('customFilters'),
|
||||
disabled: true
|
||||
}]
|
||||
|
||||
options.push.apply(options, map(customFilters, (filter, name) => ({
|
||||
label: name,
|
||||
value: name
|
||||
})))
|
||||
|
||||
// Default filters
|
||||
options.push({
|
||||
label: _('defaultFilters'),
|
||||
disabled: true
|
||||
})
|
||||
|
||||
options.push.apply(options, map(filters, (filter, labelId) => ({
|
||||
label: _(labelId),
|
||||
value: labelId
|
||||
})))
|
||||
|
||||
this.setState({ options })
|
||||
}
|
||||
|
||||
_handleDefaultFilter = value => (
|
||||
setDefaultHomeFilter(
|
||||
this.props.type,
|
||||
value && value.value
|
||||
).catch(noop)
|
||||
)
|
||||
|
||||
componentWillMount () {
|
||||
this._computeOptions(this.props)
|
||||
}
|
||||
|
||||
componentWillReceiveProps (props) {
|
||||
this._computeOptions(props)
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<Row>
|
||||
<Col>
|
||||
<FormGrid.Row>
|
||||
<FormGrid.LabelCol>
|
||||
<strong>{_('defaultFilter')}</strong>
|
||||
</FormGrid.LabelCol>
|
||||
<FormGrid.InputCol>
|
||||
<Select
|
||||
onChange={this._handleDefaultFilter}
|
||||
options={this.state.options}
|
||||
value={this.props.defaultFilter}
|
||||
/>
|
||||
</FormGrid.InputCol>
|
||||
</FormGrid.Row>
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
user: propTypes.object.isRequired
|
||||
})
|
||||
class UserFilters extends Component {
|
||||
_removeFilter = ({ name, type }) => removeCustomFilter(type, name)
|
||||
|
||||
render () {
|
||||
const {
|
||||
defaultHomeFilters,
|
||||
filters: customFiltersByType
|
||||
} = getUserPreferences(this.props.user)
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Row>
|
||||
<Col>
|
||||
<h4>{_('customizeFilters')}</h4>
|
||||
<div>
|
||||
{map(homeFilters, (filters, type) => {
|
||||
const customFilters = customFiltersByType && customFiltersByType[type]
|
||||
const defaultFilter = getDefaultFilter(defaultHomeFilters, type)
|
||||
|
||||
return (
|
||||
<div key={type}>
|
||||
<h5>{_(FILTER_TYPE_TO_LABEL_ID[type])}</h5>
|
||||
<hr />
|
||||
<DefaultFilterPicker
|
||||
customFilters={customFilters}
|
||||
defaultFilter={defaultFilter}
|
||||
filters={filters}
|
||||
type={type}
|
||||
/>
|
||||
{map(customFilters, (filter, name) => (
|
||||
<Row key={name} className='p-b-1'>
|
||||
<Col mediumSize={4}>
|
||||
<div className='input-group'>
|
||||
<Text
|
||||
onChange={newName => editCustomFilter(type, name, { newName })}
|
||||
value={name}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col mediumSize={7}>
|
||||
<div className='input-group'>
|
||||
<Text
|
||||
onChange={newValue => editCustomFilter(type, name, { newValue })}
|
||||
value={filter}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col mediumSize={1}>
|
||||
<ActionButton
|
||||
btnStyle='danger'
|
||||
className='pull-right'
|
||||
handler={this._removeFilter}
|
||||
handlerParam={{ name, type }}
|
||||
icon='delete'
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@addSubscriptions({
|
||||
user: subscribeCurrentUser
|
||||
})
|
||||
@connectStore({
|
||||
lang: getLang,
|
||||
user: getUser
|
||||
lang: getLang
|
||||
})
|
||||
@injectIntl
|
||||
export default class User extends BaseComponent {
|
||||
export default class User extends Component {
|
||||
handleSelectLang = event => {
|
||||
this.props.selectLang(event.target.value)
|
||||
}
|
||||
@@ -47,23 +246,27 @@ export default class User extends BaseComponent {
|
||||
_handleConfirmPasswordChange = event => this.setState({ confirmPassword: event.target.value })
|
||||
|
||||
render () {
|
||||
const { lang, user } = this.props
|
||||
|
||||
if (!user) {
|
||||
return <p>Loading…</p>
|
||||
}
|
||||
|
||||
const { formatMessage } = this.props.intl
|
||||
const {
|
||||
lang,
|
||||
user
|
||||
} = this.props
|
||||
const {
|
||||
confirmPassword,
|
||||
newPassword,
|
||||
oldPassword
|
||||
} = this.state
|
||||
|
||||
const sshKeys = user && user.preferences && user.preferences.sshKeys
|
||||
|
||||
return <Page header={HEADER} title={user.email}>
|
||||
<Container>
|
||||
<Row>
|
||||
<Col smallSize={2}><strong>{_('username')}</strong></Col>
|
||||
<Col smallSize={10}>
|
||||
{user && user.email}
|
||||
{user.email}
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
@@ -71,11 +274,34 @@ export default class User extends BaseComponent {
|
||||
<Col smallSize={2}><strong>{_('password')}</strong></Col>
|
||||
<Col smallSize={10}>
|
||||
<form className='form-inline' id='changePassword'>
|
||||
<input type='password' onChange={this._handleOldPasswordChange} value={oldPassword} placeholder={formatMessage(messages.oldPasswordPlaceholder)} className='form-control' required />
|
||||
<input
|
||||
autocomplete='off'
|
||||
className='form-control'
|
||||
onChange={this._handleOldPasswordChange}
|
||||
placeholder={formatMessage(messages.oldPasswordPlaceholder)}
|
||||
required
|
||||
type='password'
|
||||
value={oldPassword || ''}
|
||||
/>
|
||||
{' '}
|
||||
<input type='password' onChange={this._handleNewPasswordChange} value={newPassword} placeholder={formatMessage(messages.newPasswordPlaceholder)} className='form-control' required />
|
||||
<input type='password'
|
||||
autocomplete='off'
|
||||
className='form-control'
|
||||
onChange={this._handleNewPasswordChange}
|
||||
placeholder={formatMessage(messages.newPasswordPlaceholder)}
|
||||
required
|
||||
value={newPassword}
|
||||
/>
|
||||
{' '}
|
||||
<input type='password' onChange={this._handleConfirmPasswordChange} value={confirmPassword} placeholder={formatMessage(messages.confirmPasswordPlaceholder)} className='form-control' required />
|
||||
<input
|
||||
autocomplete='off'
|
||||
className='form-control'
|
||||
onChange={this._handleConfirmPasswordChange}
|
||||
placeholder={formatMessage(messages.confirmPasswordPlaceholder)}
|
||||
required
|
||||
type='password'
|
||||
value={confirmPassword}
|
||||
/>
|
||||
{' '}
|
||||
<ActionButton icon='save' form='changePassword' btnStyle='primary' handler={this._handleSavePassword}>
|
||||
{_('changePasswordOk')}
|
||||
@@ -97,6 +323,49 @@ export default class User extends BaseComponent {
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
<br />
|
||||
<div>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Icon icon='ssh-key' /> {_('sshKeys')}
|
||||
<ActionButton
|
||||
className='btn-success pull-xs-right'
|
||||
icon='add'
|
||||
handler={addSshKey}
|
||||
>
|
||||
{_('newSshKey')}
|
||||
</ActionButton>
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
{!isEmpty(sshKeys)
|
||||
? <Container>
|
||||
{map(sshKeys, (sshKey, key) => (
|
||||
<Row key={key} className='p-b-1'>
|
||||
<Col size={2}>
|
||||
<strong>{sshKey.title}</strong>
|
||||
</Col>
|
||||
<Col size={8} style={{overflowWrap: 'break-word'}}>
|
||||
{sshKey.key}
|
||||
</Col>
|
||||
<Col size={2}>
|
||||
<ActionButton
|
||||
className='btn-secondary pull-xs-right'
|
||||
icon='delete'
|
||||
handler={() => deleteSshKey(sshKey)}
|
||||
>
|
||||
{_('deleteSshKey')}
|
||||
</ActionButton>
|
||||
</Col>
|
||||
</Row>
|
||||
))}
|
||||
</Container>
|
||||
: _('noSshKeys')
|
||||
}
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</div>
|
||||
<hr />
|
||||
<UserFilters user={user} />
|
||||
</Page>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@ import {
|
||||
restartVm,
|
||||
resumeVm,
|
||||
stopVm,
|
||||
suspendVm
|
||||
suspendVm,
|
||||
XEN_DEFAULT_CPU_CAP,
|
||||
XEN_DEFAULT_CPU_WEIGHT
|
||||
} from 'xo'
|
||||
|
||||
const forceReboot = vm => restartVm(vm, true)
|
||||
@@ -136,10 +138,19 @@ export default ({
|
||||
}
|
||||
<tr>
|
||||
<th>{_('cpuWeightLabel')}</th>
|
||||
{vm.cpuWeight
|
||||
? <td>{vm.cpuWeight}</td>
|
||||
: <td>{_('defaultCpuWeight')}</td>
|
||||
}
|
||||
<td>
|
||||
<Number value={vm.cpuWeight == null ? null : vm.cpuWeight} onChange={value => editVm(vm, { cpuWeight: value })} nullable>
|
||||
{vm.cpuWeight == null ? _('defaultCpuWeight', { value: XEN_DEFAULT_CPU_WEIGHT }) : vm.cpuWeight}
|
||||
</Number>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{_('cpuCapLabel')}</th>
|
||||
<td>
|
||||
<Number value={vm.cpuCap == null ? null : vm.cpuCap} onChange={value => editVm(vm, { cpuCap: value })} nullable>
|
||||
{vm.cpuCap == null ? _('defaultCpuCap', { value: XEN_DEFAULT_CPU_CAP }) : vm.cpuCap}
|
||||
</Number>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{_('autoPowerOn')}</th>
|
||||
|
||||
Reference in New Issue
Block a user