feat(ActionButton): better feedback on async actions
This commit is contained in:
@@ -1,45 +1,28 @@
|
||||
import _ from 'messages'
|
||||
import Icon from 'icon'
|
||||
import ActionButton from 'action-button'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import Tooltip from 'tooltip'
|
||||
import {
|
||||
Button,
|
||||
DropdownButton,
|
||||
ButtonGroup,
|
||||
ButtonToolbar,
|
||||
MenuItem
|
||||
ButtonToolbar
|
||||
} from 'react-bootstrap-4/lib'
|
||||
|
||||
const ActionBar = ({ actions, display = 'text' }) => (
|
||||
const ActionBar = ({ actions }) => (
|
||||
<ButtonToolbar>
|
||||
{map(actions, (action, index) =>
|
||||
!action.dropdownItems
|
||||
? <Tooltip content={_(action.label)}><Button bsStyle='secondary' key={index} onClick={() => action.handler()} style={{width: '2.3em', fontSize: '1.8em', borderRadius: '15px'}}>
|
||||
<Content key={index} display={display} label={action.label} icon={action.icon} />
|
||||
</Button></Tooltip>
|
||||
: <ButtonGroup bsStyle='secondary' pullRight key={index}>
|
||||
<DropdownButton
|
||||
key={index}
|
||||
id={`dropdown-${index}`}
|
||||
pullRight
|
||||
bsStyle='secondary'
|
||||
onClick={() => action.handler()}
|
||||
noCaret
|
||||
style={{width: '2.3em', fontSize: '1.8em', borderRadius: '15px'}}
|
||||
title={<Content display={display} label={action.label} icon={action.icon} />}
|
||||
>
|
||||
<MenuItem key={index} eventKey={index} onClick={action.handler}>
|
||||
<Content key={index} display='both' label={action.label} icon={action.icon} main />
|
||||
</MenuItem>
|
||||
{map(action.dropdownItems, ({label, icon, handler}, index) => (
|
||||
<MenuItem key={index} eventKey={index} onClick={handler}>
|
||||
<Content key={index} display='both' label={label} icon={icon} />
|
||||
</MenuItem>
|
||||
))}
|
||||
</DropdownButton>
|
||||
</ButtonGroup>
|
||||
)}
|
||||
{map(actions, ({ handler, label, icon }, index) => (
|
||||
<Tooltip key={index} content={_(label)}>
|
||||
<ActionButton
|
||||
btnStyle='secondary'
|
||||
handler={handler}
|
||||
icon={icon}
|
||||
style={{
|
||||
borderRadius: '15px',
|
||||
fontSize: '1.8em',
|
||||
width: '2.3em'
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
))}
|
||||
</ButtonToolbar>
|
||||
)
|
||||
ActionBar.propTypes = {
|
||||
@@ -47,18 +30,9 @@ ActionBar.propTypes = {
|
||||
React.PropTypes.shape({
|
||||
label: React.PropTypes.string.isRequired,
|
||||
icon: React.PropTypes.string.isRequired,
|
||||
handler: React.PropTypes.func,
|
||||
dropdownItems: React.PropTypes.array
|
||||
handler: React.PropTypes.func
|
||||
})
|
||||
).isRequired,
|
||||
display: React.PropTypes.oneOf(['icon', 'text', 'both'])
|
||||
}
|
||||
export default ActionBar
|
||||
|
||||
const Content = ({display, icon, label, main}) => (
|
||||
<span style={{fontWeight: main && 'bold'}}>
|
||||
{icon && (display === 'icon' || display === 'both') ? <Icon icon={icon} /> : null}
|
||||
{display === 'both' && <span> </span>}
|
||||
{label && (display === 'text' || display === 'both') ? _(label) : null}
|
||||
</span>
|
||||
)
|
||||
export { ActionBar as default }
|
||||
|
||||
71
src/common/action-button.js
Normal file
71
src/common/action-button.js
Normal file
@@ -0,0 +1,71 @@
|
||||
import Icon from 'icon'
|
||||
import React, { Component } from 'react'
|
||||
import { Button } from 'react-bootstrap-4/lib'
|
||||
import { autobind, propTypes } from 'utils'
|
||||
|
||||
@propTypes({
|
||||
btnStyle: propTypes.string,
|
||||
handler: propTypes.func.isRequired,
|
||||
icon: propTypes.string.isRequired,
|
||||
size: propTypes.oneOf([
|
||||
'large',
|
||||
'small'
|
||||
])
|
||||
})
|
||||
export default class ActionButton extends Component {
|
||||
constructor () {
|
||||
super()
|
||||
|
||||
this.state = {}
|
||||
}
|
||||
|
||||
@autobind
|
||||
async _execute () {
|
||||
if (this.state.working) {
|
||||
return
|
||||
}
|
||||
|
||||
const { handler } = this.props
|
||||
try {
|
||||
this.setState({
|
||||
error: null,
|
||||
working: true
|
||||
})
|
||||
await handler()
|
||||
this.setState({
|
||||
working: false
|
||||
})
|
||||
} catch (error) {
|
||||
this.setState({
|
||||
error,
|
||||
working: false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
props: {
|
||||
btnStyle,
|
||||
children,
|
||||
className,
|
||||
icon,
|
||||
size: bsSize,
|
||||
style
|
||||
},
|
||||
state: { error, working }
|
||||
} = this
|
||||
|
||||
return <Button
|
||||
bsStyle={error ? 'warning' : btnStyle}
|
||||
disabled={working}
|
||||
onClick={this._execute}
|
||||
|
||||
{...{ bsSize, className, style }}
|
||||
>
|
||||
<Icon icon={working ? 'loading' : icon} />
|
||||
{children && ' '}
|
||||
{children}
|
||||
</Button>
|
||||
}
|
||||
}
|
||||
@@ -775,11 +775,6 @@ $select-input-height: 40px; // Bootstrap input height
|
||||
|
||||
// CONTENT TAB STYLE ===========================================================
|
||||
|
||||
.btn-tab {
|
||||
margin-bottom: 1em;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.btn-huge {
|
||||
font-size: 4em;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,35 @@
|
||||
import _ from 'messages'
|
||||
import Icon from 'icon'
|
||||
import ActionButton from 'action-button'
|
||||
import CopyToClipboard from 'react-copy-to-clipboard'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import React from 'react'
|
||||
import { convertVm, deleteVm, restartVm, suspendVm, stopVm, recoveryStartVm, cloneVm } from 'xo'
|
||||
import { Row, Col } from 'grid'
|
||||
import { normalizeXenToolsStatus, osFamily } from 'utils'
|
||||
import {
|
||||
cloneVm,
|
||||
convertVm,
|
||||
deleteVm,
|
||||
recoveryStartVm,
|
||||
restartVm,
|
||||
stopVm,
|
||||
suspendVm
|
||||
} from 'xo'
|
||||
|
||||
const TabButton = ({
|
||||
btnStyle,
|
||||
handler,
|
||||
icon,
|
||||
labelId
|
||||
}) => (
|
||||
<ActionButton
|
||||
size='large'
|
||||
style={{
|
||||
marginBottom: '1em',
|
||||
marginLeft: '1em'
|
||||
}}
|
||||
{...{ btnStyle, handler, icon }}
|
||||
>{_(labelId)}</ActionButton>
|
||||
)
|
||||
|
||||
export default ({
|
||||
vm
|
||||
@@ -14,49 +38,56 @@ export default ({
|
||||
<Col smallSize={12} className='text-xs-right'>
|
||||
{vm.power_state === 'Running'
|
||||
? <span>
|
||||
<button className='btn btn-lg btn-primary btn-tab' onClick={() => {
|
||||
suspendVm(vm)
|
||||
}}>
|
||||
<Icon icon='vm-suspend' size={1} /> {_('suspendVmLabel')}
|
||||
</button>
|
||||
<button className='btn btn-lg btn-warning btn-tab' onClick={() => {
|
||||
restartVm(vm, true)
|
||||
}}>
|
||||
<Icon icon='vm-force-reboot' size={1} /> {_('forceRebootVmLabel')}
|
||||
</button>
|
||||
<button className='btn btn-lg btn-warning btn-tab' onClick={() => {
|
||||
stopVm(vm, true)
|
||||
}}>
|
||||
<Icon icon='vm-force-shutdown' size={1} /> {_('forceShutdownVmLabel')}
|
||||
</button>
|
||||
<TabButton
|
||||
btnStyle='primary'
|
||||
handler={() => suspendVm(vm)}
|
||||
icon='vm-suspend'
|
||||
labelId='suspendVmLabel'
|
||||
/>
|
||||
<TabButton
|
||||
btnStyle='warning'
|
||||
handler={() => restartVm(vm, true)}
|
||||
icon='vm-force-reboot'
|
||||
labelId='forceRebootVmLabel'
|
||||
/>
|
||||
<TabButton
|
||||
btnStyle='warning'
|
||||
handler={() => stopVm(vm, true)}
|
||||
icon='vm-force-shutdown'
|
||||
labelId='forceShutdownVmLabel'
|
||||
/>
|
||||
</span>
|
||||
: null
|
||||
}
|
||||
{vm.power_state === 'Halted'
|
||||
? <span>
|
||||
<button className='btn btn-lg btn-primary btn-tab' onClick={() => {
|
||||
recoveryStartVm(vm)
|
||||
}}>
|
||||
<Icon icon='vm-recovery-mode' size={1} /> {_('recoveryModeLabel')}
|
||||
</button>
|
||||
<button className='btn btn-lg btn-primary btn-tab' onClick={() => {
|
||||
cloneVm(vm, true)
|
||||
}}>
|
||||
<Icon icon='vm-clone' size={1} /> {_('cloneVmLabel')}
|
||||
</button>
|
||||
<button className='btn btn-lg btn-danger btn-tab' onClick={() => {
|
||||
convertVm(vm)
|
||||
}}>
|
||||
<Icon icon='vm-create-template' size={1} /> {_('vmConvertButton')}
|
||||
</button>
|
||||
<TabButton
|
||||
btnStyle='primary'
|
||||
handler={() => recoveryStartVm(vm)}
|
||||
icon='vm-recovery-mode'
|
||||
labelId='recoveryModeLabel'
|
||||
/>
|
||||
<TabButton
|
||||
btnStyle='primary'
|
||||
handler={() => cloneVm(vm, true)}
|
||||
icon='vm-clone'
|
||||
labelId='cloneVmLabel'
|
||||
/>
|
||||
<TabButton
|
||||
btnStyle='danger'
|
||||
handler={() => convertVm(vm)}
|
||||
icon='vm-create-template'
|
||||
labelId='vmConvertButton'
|
||||
/>
|
||||
</span>
|
||||
: null
|
||||
}
|
||||
<button className='btn btn-lg btn-danger btn-tab' onClick={() => {
|
||||
deleteVm(vm)
|
||||
}}>
|
||||
<Icon icon='vm-delete' size={1} /> {_('vmRemoveButton')}
|
||||
</button>
|
||||
<TabButton
|
||||
btnStyle='danger'
|
||||
handler={() => deleteVm(vm)}
|
||||
icon='vm-delete'
|
||||
labelId='vmRemoveButton'
|
||||
/>
|
||||
</Col>
|
||||
<Col smallSize={12}>
|
||||
<h3>{_('xenSettingsLabel')}</h3>
|
||||
|
||||
Reference in New Issue
Block a user