feat: move tabs in header for host and VM views (#927)

Fixes #926
This commit is contained in:
Pierre Donias
2016-05-11 15:13:12 +02:00
committed by Julien Fontanet
parent c548e08aea
commit 2f3e463aca
8 changed files with 186 additions and 292 deletions

16
src/common/nav.js Normal file
View File

@@ -0,0 +1,16 @@
import Link from 'react-router/lib/Link'
import React from 'react'
export const NavLink = ({ children, to }) => (
<li className='nav-item' role='tab'>
<Link className='nav-link' activeClassName='active' to={to}>
{children}
</Link>
</li>
)
export const NavTabs = ({ children }) => (
<ul className='nav nav-tabs' role='tablist'>
{children}
</ul>
)

View File

@@ -201,7 +201,6 @@ $select-input-height: 40px; // Bootstrap input height
// OJBECT TAB STYLE ============================================================
.nav-tabs {
margin-bottom: 1em;
font-size: 1.2em;
}
@@ -297,43 +296,6 @@ $select-input-height: 40px; // Bootstrap input height
visibility: visible;
}
// HEADER STYLE ================================================================
.xo-header {
background-color: #eee;
padding: 0.6em;
flex-shrink: 0;
}
// MAIN STYLE ==================================================================
.xo-main {
display: flex;
flex-direction: row;
min-height: 100vh;
/* FIXME: The size of `xo-main` matches the size of the window thanks to the
* flex growing feature.
* Therefore, when there is a scrollbar on the right side,
* `xo-main` is too large (since the scrollbar uses a few
* pixels) which makes an almost useless horizontal scrollbar appear.
*/
overflow: hidden;
}
.xo-body {
display: flex;
flex-direction: column;
flex: 1;
max-height: 100vh;
}
.xo-content {
flex: 1;
overflow-y: auto;
padding: 1em;
}
// DASHBOARD STYLE =============================================================
.card-dashboard {

View File

@@ -1,66 +0,0 @@
import Icon from 'icon'
import React, { Component } from 'react'
import { editHost } from 'xo'
import { Container, Row, Col } from 'grid'
import { Text } from 'editable'
import {
connectStore
} from 'utils'
import {
createGetObject
} from 'selectors'
import HostActionBar from './action-bar'
// ===================================================================
@connectStore(() => {
const getHost = createGetObject()
const getPool = createGetObject(
(...args) => getHost(...args).$pool
)
return (state, props) => {
const host = getHost(state, props)
if (!host) {
return {}
}
return {
host: getHost(state, props),
pool: getPool(state, props)
}
}
})
export default class Header extends Component {
render () {
const { host, pool } = this.props
if (!host) {
return <Icon icon='loading' />
}
return <Container>
<Row>
<Col smallSize={6}>
<h2>
<Icon icon={`host-${host.power_state.toLowerCase()}`} />&nbsp;
<Text
onChange={nameLabel => editHost(host, { nameLabel })}
>{host.name_label}</Text>
</h2>
<span>
<Text
onChange={nameDescription => editHost(host, { nameDescription })}
>{host.name_description}</Text>
<span className='text-muted'> - {pool.name_label}</span>
</span>
</Col>
<Col smallSize={6}>
<div className='pull-xs-right'>
<HostActionBar host={host} handlers={this.props} />
</div>
</Col>
</Row>
</Container>
}
}

View File

@@ -1,13 +1,17 @@
import _ from 'messages'
import assign from 'lodash/assign'
import HostActionBar from './action-bar'
import Icon from 'icon'
import isEmpty from 'lodash/isEmpty'
import Link from 'react-router/lib/Link'
import map from 'lodash/map'
import { NavLink, NavTabs } from 'nav'
import Page from '../page'
import pick from 'lodash/pick'
import sortBy from 'lodash/sortBy'
import React, { cloneElement, Component } from 'react'
import { fetchHostStats, getHostMissingPatches } from 'xo'
import { Row, Col } from 'grid'
import sortBy from 'lodash/sortBy'
import { Text } from 'editable'
import { editHost, fetchHostStats, getHostMissingPatches } from 'xo'
import { Container, Row, Col } from 'grid'
import {
autobind,
connectStore,
@@ -37,22 +41,6 @@ const isRunning = host => host && host.power_state === 'Running'
// ===================================================================
const NavLink = ({ children, to }) => (
<li className='nav-item' role='tab'>
<Link className='nav-link' activeClassName='active' to={to}>
{children}
</Link>
</li>
)
const NavTabs = ({ children }) => (
<ul className='nav nav-tabs' role='tablist'>
{children}
</ul>
)
// ===================================================================
@routes('general', {
advanced: TabAdvanced,
console: TabConsole,
@@ -211,9 +199,54 @@ export default class Host extends Component {
})
}
}
header () {
const { host, pool } = this.props
const { missingPatches } = this.state || {}
if (!host) {
return <Icon icon='loading' />
}
return <Container>
<Row>
<Col smallSize={6}>
<h2>
<Icon icon={`host-${host.power_state.toLowerCase()}`} />&nbsp;
<Text
onChange={nameLabel => editHost(host, { nameLabel })}
>{host.name_label}</Text>
</h2>
<span>
<Text
onChange={nameDescription => editHost(host, { nameDescription })}
>{host.name_description}</Text>
<span className='text-muted'> - {pool.name_label}</span>
</span>
</Col>
<Col smallSize={6}>
<div className='pull-xs-right'>
<HostActionBar host={host} handlers={this.props} />
</div>
</Col>
</Row>
<Row>
<Col size={12}>
<NavTabs>
<NavLink to={`/hosts/${host.id}/general`}>{_('generalTabName')}</NavLink>
<NavLink to={`/hosts/${host.id}/stats`}>{_('statsTabName')}</NavLink>
<NavLink to={`/hosts/${host.id}/console`}>{_('consoleTabName')}</NavLink>
<NavLink to={`/hosts/${host.id}/network`}>{_('networkTabName')}</NavLink>
<NavLink to={`/hosts/${host.id}/storage`}>{_('storageTabName')}</NavLink>
<NavLink to={`/hosts/${host.id}/patches`}>{_('patchesTabName')} {isEmpty(missingPatches) ? null : <span className='label label-pill label-danger'>{missingPatches.length}</span>}</NavLink>
<NavLink to={`/hosts/${host.id}/logs`}>{_('logsTabName')}</NavLink>
<NavLink to={`/hosts/${host.id}/advanced`}>{_('advancedTabName')}</NavLink>
</NavTabs>
</Col>
</Row>
</Container>
}
render () {
const { host } = this.props
const { missingPatches } = this.state || {}
if (!host) {
return <h1>Loading</h1>
}
@@ -231,22 +264,8 @@ export default class Host extends Component {
'statsOverview'
])
)
return <div>
<Row>
<Col size={12}>
<NavTabs>
<NavLink to={`/hosts/${host.id}/general`}>{_('generalTabName')}</NavLink>
<NavLink to={`/hosts/${host.id}/stats`}>{_('statsTabName')}</NavLink>
<NavLink to={`/hosts/${host.id}/console`}>{_('consoleTabName')}</NavLink>
<NavLink to={`/hosts/${host.id}/network`}>{_('networkTabName')}</NavLink>
<NavLink to={`/hosts/${host.id}/storage`}>{_('storageTabName')}</NavLink>
<NavLink to={`/hosts/${host.id}/patches`}>{_('patchesTabName')} {isEmpty(missingPatches) ? null : <span className='label label-pill label-danger'>{missingPatches.length}</span>}</NavLink>
<NavLink to={`/hosts/${host.id}/logs`}>{_('logsTabName')}</NavLink>
<NavLink to={`/hosts/${host.id}/advanced`}>{_('advancedTabName')}</NavLink>
</NavTabs>
</Col>
</Row>
return <Page header={this.header()}>
{cloneElement(this.props.children, childProps)}
</div>
</Page>
}
}

View File

@@ -2,6 +2,7 @@ import React, {
Component
} from 'react'
import { IntlProvider } from 'messages'
import { Notification } from 'notification'
// import {
// keyHandler
// } from 'react-key-handler'
@@ -14,71 +15,53 @@ import {
import About from './about'
import Backup from './backup'
import Dashboard from './dashboard'
import Header from './header'
import Home from './home'
import Host from './host'
import HostHeader from './host/header'
import Menu from './menu'
import Modal from 'modal'
import New from './new'
import { Notification } from 'notification'
import Settings from './settings'
import Vm from './vm'
import VmHeader from './vm/header'
const makeHeaderRoutes = (content, header) => ({
...content.route,
components: { content, header }
})
@routes('home', {
about: About,
backup: Backup,
dashboard: Dashboard,
home: Home,
'hosts/:id': makeHeaderRoutes(Host, HostHeader),
'hosts/:id': Host,
new: New,
settings: Settings,
'vms/:id': makeHeaderRoutes(Vm, VmHeader)
'vms/:id': Vm
})
@connectStore([
'user'
])
@propTypes({
children: propTypes.node,
header: propTypes.node,
content: propTypes.node
children: propTypes.node
})
export default class XoApp extends Component {
componentDidMount () {
this.refs.body.style.minHeight = this.refs.menu.getWrappedInstance().height + 'px'
this.refs.bodyWrapper.style.minHeight = this.refs.menu.getWrappedInstance().height + 'px'
}
render () {
const {
children,
header,
content
} = this.props
return <IntlProvider>
<div className='xo-main'>
<div style={{
display: 'flex',
flexDirection: 'row',
minHeight: '100vh',
/* FIXME: 'The size of `xo-main` matches the size of the window thanks to the',
* flex growing feature.,
* Therefore, when there is a scrollbar on the right side,,
* `xo-main` is too large (since the scrollbar uses a few,
* pixels) which makes an almost useless horizontal scrollbar appear.,
*/
overflow: 'hidden'
}}>
<Modal />
<Notification />
<Menu ref='menu' />
<div className='xo-body' ref='body'>
{children
? <div className='xo-content'>
{children}
</div>
: [
<Header key='header'>
{header}
</Header>,
<div key='content' className='xo-content'>
{content}
</div>
]
}
<div ref='bodyWrapper' style={{flex: '1', padding: '1em'}}>
{this.props.children}
</div>
</div>
</IntlProvider>

31
src/xo-app/page/index.js Normal file
View File

@@ -0,0 +1,31 @@
import Header from '../header'
import React from 'react'
const Page = ({ children, header }) => {
return (
<div style={{
display: 'flex',
flexDirection: 'column',
flex: '1',
maxHeight: '100vh',
margin: '-1em' /* To offset the padding applied to the wrapper */
}}>
<Header style={{
backgroundColor: '#eee',
padding: '0.6em',
paddingBottom: '0',
flexShrink: '0'
}}>
{header}
</Header>
<div style={{
flex: '1',
overflowY: 'auto',
padding: '1em'
}}>
{children}
</div>
</div>
)
}
export { Page as default }

View File

@@ -1,79 +0,0 @@
import Icon from 'icon'
import isEmpty from 'lodash/isEmpty'
import React, { Component } from 'react'
import { editVm } from 'xo'
import { Container, Row, Col } from 'grid'
import { Text } from 'editable'
import {
connectStore
} from 'utils'
import {
createGetObject
} from 'selectors'
import VmActionBar from './action-bar'
// ===================================================================
@connectStore(() => {
const getVm = createGetObject()
const getContainer = createGetObject(
(...args) => getVm(...args).$container
)
const getPool = createGetObject(
(...args) => getVm(...args).$pool
)
return (state, props) => {
const vm = getVm(state, props)
if (!vm) {
return {}
}
return {
container: getContainer(state, props),
pool: getPool(state, props),
vm
}
}
})
export default class Header extends Component {
render () {
const { vm, container, pool } = this.props
if (!vm || !pool) {
return <Icon icon='loading' />
}
return <Container>
<Row>
<Col smallSize={6}>
<h2>
{isEmpty(vm.current_operations)
? <Icon icon={`vm-${vm.power_state.toLowerCase()}`} />
: <Icon icon='vm-busy' />
}
&nbsp;
<Text
onChange={value => editVm(vm, { name_label: value })}
>{vm.name_label}</Text>
</h2>
<span>
<Text
onChange={value => editVm(vm, { name_description: value })}
>{vm.name_description}</Text>
<span className='text-muted'>
{vm.power_state === 'Running' ? ' - ' + container.name_label : null}
{' '}({pool.name_label})
</span>
</span>
</Col>
<Col smallSize={6}>
<div className='pull-xs-right'>
<VmActionBar vm={vm} handlers={this.props} />
</div>
</Col>
</Row>
</Container>
}
}

View File

@@ -1,13 +1,20 @@
import _ from 'messages'
import assign from 'lodash/assign'
import forEach from 'lodash/forEach'
import Icon from 'icon'
import isEmpty from 'lodash/isEmpty'
import Link from 'react-router/lib/Link'
import map from 'lodash/map'
import { NavLink, NavTabs } from 'nav'
import Page from '../page'
import pick from 'lodash/pick'
import React, { cloneElement, Component } from 'react'
import { fetchVmStats } from 'xo'
import { Row, Col } from 'grid'
import VmActionBar from './action-bar'
import { Text } from 'editable'
import {
editVm,
fetchVmStats
} from 'xo'
import { Container, Row, Col } from 'grid'
import {
autobind,
connectStore,
@@ -36,22 +43,6 @@ const isRunning = vm => vm && vm.power_state === 'Running'
// ===================================================================
const NavLink = ({ children, to }) => (
<li className='nav-item' role='tab'>
<Link className='nav-link' activeClassName='active' to={to}>
{children}
</Link>
</li>
)
const NavTabs = ({ children }) => (
<ul className='nav nav-tabs' role='tablist'>
{children}
</ul>
)
// ===================================================================
@routes('general', {
advanced: TabAdvanced,
console: TabConsole,
@@ -207,8 +198,59 @@ export default class Vm extends Component {
}
}
header () {
const { vm, container, pool, snapshots } = this.props
if (!vm || !pool) {
return <Icon icon='loading' />
}
return <Container>
<Row>
<Col smallSize={6}>
<h2>
{isEmpty(vm.current_operations)
? <Icon icon={`vm-${vm.power_state.toLowerCase()}`} />
: <Icon icon='vm-busy' />
}
&nbsp;
<Text
onChange={value => editVm(vm, { name_label: value })}
>{vm.name_label}</Text>
</h2>
<span>
<Text
onChange={value => editVm(vm, { name_description: value })}
>{vm.name_description}</Text>
<span className='text-muted'>
{vm.power_state === 'Running' ? ' - ' + container.name_label : null}
{' '}({pool.name_label})
</span>
</span>
</Col>
<Col smallSize={6}>
<div className='pull-xs-right'>
<VmActionBar vm={vm} handlers={this.props} />
</div>
</Col>
</Row>
<Row>
<Col size={12}>
<NavTabs>
<NavLink to={`/vms/${vm.id}/general`}>{_('generalTabName')}</NavLink>
<NavLink to={`/vms/${vm.id}/stats`}>{_('statsTabName')}</NavLink>
<NavLink to={`/vms/${vm.id}/console`}>{_('consoleTabName')}</NavLink>
<NavLink to={`/vms/${vm.id}/network`}>{_('networkTabName')}</NavLink>
<NavLink to={`/vms/${vm.id}/disks`}>{_('disksTabName', { disks: vm.$VBDs.length })}</NavLink>
<NavLink to={`/vms/${vm.id}/snapshots`}>{_('snapshotsTabName')} {isEmpty(snapshots) ? null : <span className='label label-pill label-default'>{snapshots.length}</span>}</NavLink>
<NavLink to={`/vms/${vm.id}/logs`}>{_('logsTabName')}</NavLink>
<NavLink to={`/vms/${vm.id}/advanced`}>{_('advancedTabName')}</NavLink>
</NavTabs>
</Col>
</Row>
</Container>
}
render () {
const { snapshots, vm } = this.props
const { vm } = this.props
if (!vm) {
return <h1>Loading</h1>
@@ -230,22 +272,8 @@ export default class Vm extends Component {
]), pick(this.state, [
'statsOverview'
]))
return <div>
<Row>
<Col size={12}>
<NavTabs>
<NavLink to={`/vms/${vm.id}/general`}>{_('generalTabName')}</NavLink>
<NavLink to={`/vms/${vm.id}/stats`}>{_('statsTabName')}</NavLink>
<NavLink to={`/vms/${vm.id}/console`}>{_('consoleTabName')}</NavLink>
<NavLink to={`/vms/${vm.id}/network`}>{_('networkTabName')}</NavLink>
<NavLink to={`/vms/${vm.id}/disks`}>{_('disksTabName', { disks: vm.$VBDs.length })}</NavLink>
<NavLink to={`/vms/${vm.id}/snapshots`}>{_('snapshotsTabName')} {isEmpty(snapshots) ? null : <span className='label label-pill label-default'>{snapshots.length}</span>}</NavLink>
<NavLink to={`/vms/${vm.id}/logs`}>{_('logsTabName')}</NavLink>
<NavLink to={`/vms/${vm.id}/advanced`}>{_('advancedTabName')}</NavLink>
</NavTabs>
</Col>
</Row>
return <Page header={this.header()}>
{cloneElement(this.props.children, childProps)}
</div>
</Page>
}
}