Add Node Expanded view

Adds ability to expand each Node in NodesListView to get more
information. This change lists only data provided by Node itself
Subsequent patch will add fetching Introspection data from
ironic-inspector

Change-Id: I3d34f43c5555b1884f34010e3b47b8ffb1691218
This commit is contained in:
Jiri Tomasek 2017-05-05 12:32:22 +02:00
parent 69cebd336f
commit 880ab278c6
10 changed files with 329 additions and 126 deletions

View File

@ -0,0 +1,5 @@
---
features:
- |
Nodes in list view can be expanded to grant access to additional information
about the node.

View File

@ -0,0 +1,51 @@
import { FormattedDate, FormattedTime } from 'react-intl';
import { startCase } from 'lodash';
import React, { PropTypes } from 'react';
import { Row, Col } from 'react-bootstrap';
class NodeExtendedInfo extends React.Component {
render() {
const { node } = this.props;
return (
<Row>
<Col lg={3} md={6}>
<dl className="dl-horizontal dl-horizontal-condensed">
<dt>UUID:</dt>
<dd>{node.uuid}</dd>
<dt>Registered:</dt>
<dd>
<FormattedDate value={node.created_at} />
&nbsp;
<FormattedTime value={node.created_at} />
</dd>
<dt>Architecture:</dt>
<dd>{node.properties.cpu_arch}</dd>
</dl>
</Col>
<Col lg={2} md={4}>
<dl>
<dt>Mac Addresses:</dt>
{node.macs.map(mac => <dd key={mac}>{mac}</dd>)}
</dl>
</Col>
<Col lg={4} md={6}>
<dl className="dl-horizontal dl-horizontal-condensed">
<dt>Driver:</dt>
<dd>{node.driver}</dd>
{Object.keys(node.driver_info).map(key => (
<span key={key}>
<dt>{startCase(key)}:</dt>
<dd>{node.driver_info[key]}</dd>
</span>
))}
</dl>
</Col>
</Row>
);
}
}
NodeExtendedInfo.propTypes = {
node: PropTypes.object.isRequired
};
export default NodeExtendedInfo;

View File

@ -0,0 +1,151 @@
import ClassNames from 'classnames';
import { defineMessages, FormattedMessage } from 'react-intl';
import React, { PropTypes } from 'react';
import {
ListViewAdditionalInfo,
ListViewAdditionalInfoItem,
ListViewBody,
ListViewCheckbox,
ListViewDescription,
ListViewDescriptionHeading,
ListViewDescriptionText,
ListViewExpand,
ListViewIcon,
ListViewItem,
ListViewItemContainer,
ListViewItemHeader,
ListViewLeft,
ListViewMainInfo
} from '../../ui/ListView';
import NodeExtendedInfo from './NodeExtendedInfo';
import {
NodeMaintenanceState,
NodePowerState,
NodeProvisionState
} from './NodeStates';
import { parseNodeCapabilities } from '../../../utils/nodes';
const messages = defineMessages({
profile: {
id: 'NodeListItem.Profile',
defaultMessage: 'Profile:'
},
cpuCores: {
id: 'NodeListItem.cpuCores',
defaultMessage: `CPU {cpuCores, plural,
one {Core} other {Cores}}`
},
ram: {
id: 'NodeListItem.mbRam',
defaultMessage: 'MB RAM'
},
disk: {
id: 'NodeListItem.gbDisk',
defaultMessage: 'GB Disk'
}
});
export default class NodeListItem extends React.Component {
constructor() {
super();
this.state = {
expanded: false
};
}
toggleExpanded() {
this.setState(prevState => ({ expanded: !prevState.expanded }));
}
render() {
const { node, inProgress } = this.props;
const iconClass = ClassNames({
'pficon pficon-server': true,
running: inProgress
});
return (
<ListViewItem expanded={this.state.expanded} stacked>
<ListViewItemHeader>
<ListViewExpand
expanded={this.state.expanded}
toggleExpanded={this.toggleExpanded.bind(this)}
/>
<ListViewCheckbox
disabled={inProgress}
name={`values.${node.uuid}`}
/>
<ListViewMainInfo>
<ListViewLeft>
<ListViewIcon size="sm" icon={iconClass} />
</ListViewLeft>
<ListViewBody>
<ListViewDescription>
<ListViewDescriptionHeading>
{node.name || node.uuid}
</ListViewDescriptionHeading>
<ListViewDescriptionText>
<NodePowerState
powerState={node.power_state}
targetPowerState={node.target_power_state}
/>
<NodeMaintenanceState
maintenance={node.maintenance}
reason={node.maintenance_reason}
/>
{' | '}
<NodeProvisionState
provisionState={node.provision_state}
targetProvisionState={node.target_provision_state}
/>
</ListViewDescriptionText>
</ListViewDescription>
<ListViewAdditionalInfo>
<ListViewAdditionalInfoItem>
<span className="pficon pficon-flavor" />
<FormattedMessage {...messages.profile} />
&nbsp;
{parseNodeCapabilities(node.properties.capabilities)
.profile || '-'}
</ListViewAdditionalInfoItem>
<ListViewAdditionalInfoItem>
<span className="pficon pficon-cpu" />
<strong>{node.properties.cpus || '-'}</strong>
&nbsp;
<FormattedMessage
{...messages.cpuCores}
values={{ cpuCores: node.properties.cpus }}
/>
</ListViewAdditionalInfoItem>
<ListViewAdditionalInfoItem>
<span className="pficon pficon-memory" />
<strong>{node.properties.memory_mb || '-'}</strong>
&nbsp;
<FormattedMessage {...messages.ram} />
</ListViewAdditionalInfoItem>
<ListViewAdditionalInfoItem>
<span className="fa fa-database" />
<strong>{node.properties.local_gb || '-'}</strong>
&nbsp;
<FormattedMessage {...messages.disk} />
</ListViewAdditionalInfoItem>
</ListViewAdditionalInfo>
</ListViewBody>
</ListViewMainInfo>
</ListViewItemHeader>
<ListViewItemContainer
onClose={this.toggleExpanded.bind(this)}
expanded={this.state.expanded}
>
<NodeExtendedInfo node={node} />
</ListViewItemContainer>
</ListViewItem>
);
}
}
NodeListItem.propTypes = {
inProgress: PropTypes.bool.isRequired,
node: PropTypes.object.isRequired
};

View File

@ -37,10 +37,10 @@ const messages = defineMessages({
}
});
export const NodeMaintenanceState = ({ maintenance }) => {
export const NodeMaintenanceState = ({ maintenance, reason }) => {
if (maintenance) {
return (
<span>
<span title={reason}>
{' | '}
<span className="pficon pficon-warning-triangle-o" />
&nbsp;
@ -51,7 +51,11 @@ export const NodeMaintenanceState = ({ maintenance }) => {
return null;
};
NodeMaintenanceState.propTypes = {
maintenance: PropTypes.bool.isRequired
maintenance: PropTypes.bool.isRequired,
reason: PropTypes.string
};
NodeMaintenanceState.defaultProps = {
reason: ''
};
export const NodeProvisionState = ({

View File

@ -1,48 +1,8 @@
import ClassNames from 'classnames';
import { defineMessages, FormattedMessage } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import React, { PropTypes } from 'react';
import React from 'react';
import {
ListView,
ListViewAdditionalInfo,
ListViewAdditionalInfoItem,
ListViewBody,
ListViewCheckbox,
ListViewDescription,
ListViewDescriptionHeading,
ListViewDescriptionText,
ListViewIcon,
ListViewItem,
ListViewLeft,
ListViewMainInfo
} from '../../ui/ListView';
import {
NodeMaintenanceState,
NodePowerState,
NodeProvisionState
} from './NodeStates';
import { parseNodeCapabilities } from '../../../utils/nodes';
const messages = defineMessages({
profile: {
id: 'NodeListItem.Profile',
defaultMessage: 'Profile:'
},
cpuCores: {
id: 'NodeListItem.cpuCores',
defaultMessage: `CPU {cpuCores, plural,
one {Core} other {Cores}}`
},
ram: {
id: 'NodeListItem.mbRam',
defaultMessage: 'MB RAM'
},
disk: {
id: 'NodeListItem.gbDisk',
defaultMessage: 'GB Disk'
}
});
import { ListView } from '../../ui/ListView';
import NodeListItem from './NodeListItem';
export default class NodesListView extends React.Component {
render() {
@ -66,78 +26,3 @@ NodesListView.propTypes = {
nodes: ImmutablePropTypes.map.isRequired,
nodesInProgress: ImmutablePropTypes.set.isRequired
};
export class NodeListItem extends React.Component {
render() {
const { node, inProgress } = this.props;
const iconClass = ClassNames({
'pficon pficon-server': true,
running: inProgress
});
return (
<ListViewItem stacked>
<ListViewCheckbox disabled={inProgress} name={`values.${node.uuid}`} />
<ListViewMainInfo>
<ListViewLeft>
<ListViewIcon size="sm" icon={iconClass} />
</ListViewLeft>
<ListViewBody>
<ListViewDescription>
<ListViewDescriptionHeading>
{node.name || node.uuid}
</ListViewDescriptionHeading>
<ListViewDescriptionText>
<NodePowerState
powerState={node.power_state}
targetPowerState={node.target_power_state}
/>
<NodeMaintenanceState maintenance={node.maintenance} />
{' | '}
<NodeProvisionState
provisionState={node.provision_state}
targetProvisionState={node.target_provision_state}
/>
</ListViewDescriptionText>
</ListViewDescription>
<ListViewAdditionalInfo>
<ListViewAdditionalInfoItem>
<span className="pficon pficon-flavor" />
<FormattedMessage {...messages.profile} />
&nbsp;
{parseNodeCapabilities(node.properties.capabilities).profile ||
'-'}
</ListViewAdditionalInfoItem>
<ListViewAdditionalInfoItem>
<span className="pficon pficon-cpu" />
<strong>{node.properties.cpus || '-'}</strong>
&nbsp;
<FormattedMessage
{...messages.cpuCores}
values={{ cpuCores: node.properties.cpus }}
/>
</ListViewAdditionalInfoItem>
<ListViewAdditionalInfoItem>
<span className="pficon pficon-memory" />
<strong>{node.properties.memory_mb || '-'}</strong>
&nbsp;
<FormattedMessage {...messages.ram} />
</ListViewAdditionalInfoItem>
<ListViewAdditionalInfoItem>
<span className="fa fa-database" />
<strong>{node.properties.local_gb || '-'}</strong>
&nbsp;
<FormattedMessage {...messages.disk} />
</ListViewAdditionalInfoItem>
</ListViewAdditionalInfo>
</ListViewBody>
</ListViewMainInfo>
</ListViewItem>
);
}
}
NodeListItem.propTypes = {
inProgress: PropTypes.bool.isRequired,
node: PropTypes.object.isRequired
};

View File

@ -157,7 +157,7 @@ class NodesTable extends React.Component {
<FormattedMessage {...messages.macAddresses} />
</DataTableHeaderCell>
}
cell={<DataTableDataFieldCell data={filteredData} field="macs" />}
cell={<NodesTableMacsCell data={filteredData} field="macs" />}
/>
<DataTableColumn
key="name"
@ -299,6 +299,20 @@ NodesTableMaintenanceCell.propTypes = {
rowIndex: React.PropTypes.number
};
export const NodesTableMacsCell = props => {
const value = _.result(props.data[props.rowIndex], props.field).join(', ');
return (
<DataTableCell {...props}>
{value}
</DataTableCell>
);
};
NodesTableMacsCell.propTypes = {
data: React.PropTypes.array.isRequired,
field: React.PropTypes.string.isRequired,
rowIndex: React.PropTypes.number
};
export class NodesTableCheckBoxCell extends React.Component {
render() {
const nodeId = _.result(

View File

@ -2,6 +2,50 @@ import ClassNames from 'classnames';
import { Field } from 'redux-form';
import React, { PropTypes } from 'react';
/* ListView example usage:
<ListView>
<ListViewItem stacked expanded>
<ListViewItemHeader> // required only if the ListViewItem is supposed to be expandable
<ListViewExpand toggleExpanded={functionToToggle} expanded />
<ListViewCheckbox disabled={inProgress} name={`values.${node.uuid}`} />
<ListViewMainInfo>
<ListViewLeft>
<ListViewIcon size="sm" icon={iconClass} />
</ListViewLeft>
<ListViewBody>
<ListViewDescription>
<ListViewDescriptionHeading>
{name}
</ListViewDescriptionHeading>
<ListViewDescriptionText>
{description}
</ListViewDescriptionText>
</ListViewDescription>
<ListViewAdditionalInfo>
<ListViewAdditionalInfoItem>
<span className="pficon pficon-flavor" />
{Item1}
</ListViewAdditionalInfoItem>
<ListViewAdditionalInfoItem>
<span className="pficon pficon-cpu" />
{Item2}
</ListViewAdditionalInfoItem>
</ListViewAdditionalInfo>
</ListViewBody>
</ListViewMainInfo>
</ListViewItemHeader>
<ListViewItemContainer onClose={functionWhichClosesMe} expanded> // expandable content
<Row>Some content goes here</Row>
</ListViewItemContainer>
</ListViewItem>
...
</ListView>
*/
export const ListView = ({ children }) => (
<div className="list-group list-view-pf list-view-pf-view">
{children}
@ -33,6 +77,39 @@ ListViewItem.defaultProps = {
stacked: false
};
export const ListViewItemHeader = ({ children }) => (
<div className="list-group-item-header">
{children}
</div>
);
ListViewItemHeader.propTypes = {
children: PropTypes.node
};
export const ListViewItemContainer = ({ children, expanded, onClose }) => {
const classes = ClassNames({
'list-group-item-container container-fluid': true,
hidden: !expanded
});
return (
<div className={classes}>
{onClose &&
<div className="close">
<span className="pficon pficon-close" onClick={() => onClose()} />
</div>}
{expanded && children}
</div>
);
};
ListViewItemContainer.propTypes = {
children: PropTypes.node,
expanded: PropTypes.bool.isRequired,
onClose: PropTypes.func
};
ListViewItemContainer.defaultProps = {
expanded: false
};
export const ListViewCheckbox = ({ disabled, name }) => (
<div className="list-view-pf-checkbox">
<Field name={name} type="checkbox" component="input" disabled={disabled} />
@ -46,19 +123,20 @@ ListViewCheckbox.defaultProps = {
disabled: false
};
export const ListViewExpand = ({ expanded }) => {
export const ListViewExpand = ({ expanded, toggleExpanded }) => {
const classes = ClassNames({
'fa fa-angle-right': true,
'fa-angle-down': expanded
});
return (
<a className="list-view-pf-expand">
<a className="list-view-pf-expand" onClick={() => toggleExpanded()}>
<span className={classes} />
</a>
);
};
ListViewExpand.propTypes = {
expanded: PropTypes.bool.isRequired
expanded: PropTypes.bool.isRequired,
toggleExpanded: PropTypes.func.isRequired
};
ListViewExpand.defaultProps = {
expanded: false

View File

@ -28,7 +28,8 @@ export const getNodesWithMacs = createSelector(
'macs',
ports
.filter(p => node.get('uuid') === p.node_uuid)
.reduce((str, v) => str + v.address, '')
.map(port => port.address)
.toList()
)
)
);

View File

@ -120,6 +120,7 @@
@import "ui/ListView";
@import "ui/Modals";
@import "ui/Tables";
@import "ui/Type";
@import "ui/Navs";
@import "ui/FixedContainer";
@import "ui/Sidebar";

13
src/less/ui/Type.less Normal file
View File

@ -0,0 +1,13 @@
//
// Typography overrides
// --------------------------------------------------
.dl-horizontal.dl-horizontal-condensed {
@media (min-width: @dl-horizontal-breakpoint) {
dt {
width: 110px;
}
dd {
margin-left: 120px;
}
}
}