Node Drives list
* Adds Drives List modal which lists relevant information about all Node disks. This list is accessible for Introspected node through Node's action menu in Nodes list view Change-Id: I5985388527e307cd4d31235c79abc014332ffb7c
This commit is contained in:
parent
046188f13a
commit
85f04eb9af
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
features:
|
||||
- |
|
||||
Information about Drives is now available for each Node. List of Drives is
|
||||
accessible through Node action menu in Nodes list view.
|
|
@ -0,0 +1,168 @@
|
|||
/**
|
||||
* Copyright 2017 Red Hat Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License. You may obtain
|
||||
* a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { defineMessages, FormattedMessage } from 'react-intl';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { Col, Row } from 'react-bootstrap';
|
||||
|
||||
import { formatBytes } from '../../utils';
|
||||
import {
|
||||
ListViewAdditionalInfo,
|
||||
ListViewAdditionalInfoItem,
|
||||
ListViewBody,
|
||||
ListViewDescription,
|
||||
ListViewDescriptionHeading,
|
||||
ListViewDescriptionText,
|
||||
ListViewExpand,
|
||||
ListViewIcon,
|
||||
ListViewItem,
|
||||
ListViewItemContainer,
|
||||
ListViewItemHeader,
|
||||
ListViewLeft,
|
||||
ListViewMainInfo
|
||||
} from '../../ui/ListView';
|
||||
|
||||
const messages = defineMessages({
|
||||
type: {
|
||||
id: 'NodeDrive.type',
|
||||
defaultMessage: 'Type:'
|
||||
},
|
||||
size: {
|
||||
id: 'NodeDrive.size',
|
||||
defaultMessage: 'Size:'
|
||||
},
|
||||
model: {
|
||||
id: 'NodeDrive.model',
|
||||
defaultMessage: 'Model:'
|
||||
},
|
||||
serial: {
|
||||
id: 'NodeDrive.serial',
|
||||
defaultMessage: 'Serial:'
|
||||
},
|
||||
vendor: {
|
||||
id: 'NodeDrive.vendor',
|
||||
defaultMessage: 'Vendor:'
|
||||
},
|
||||
wwn: {
|
||||
id: 'NodeDrive.wwn',
|
||||
defaultMessage: 'WWN:'
|
||||
},
|
||||
wwnVendorExtension: {
|
||||
id: 'NodeDrive.wwnVendorExtension',
|
||||
defaultMessage: 'WWN Vendor Extension:'
|
||||
},
|
||||
wwnWithExtension: {
|
||||
id: 'NodeDrive.wwnWithExtension',
|
||||
defaultMessage: 'WWN with Extension:'
|
||||
},
|
||||
rootDisk: {
|
||||
id: 'NodeDrive.rootDisk',
|
||||
defaultMessage: 'Root Device'
|
||||
},
|
||||
na: {
|
||||
id: 'NodeDrive.notAvailable',
|
||||
defaultMessage: 'n/a'
|
||||
}
|
||||
});
|
||||
|
||||
export default class NodeDrive extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
expanded: false
|
||||
};
|
||||
}
|
||||
|
||||
toggleExpanded() {
|
||||
this.setState(prevState => ({ expanded: !prevState.expanded }));
|
||||
}
|
||||
|
||||
render() {
|
||||
const { drive } = this.props;
|
||||
const [driveSize, driveSizeUnit] = formatBytes(drive.size);
|
||||
return (
|
||||
<ListViewItem stacked={false} expanded={this.state.expanded}>
|
||||
|
||||
<ListViewItemHeader toggleExpanded={this.toggleExpanded.bind(this)}>
|
||||
<ListViewExpand expanded={this.state.expanded} />
|
||||
<ListViewMainInfo>
|
||||
<ListViewLeft>
|
||||
<ListViewIcon size="sm" icon="pficon pficon-volume" />
|
||||
</ListViewLeft>
|
||||
<ListViewBody>
|
||||
<ListViewDescription>
|
||||
<ListViewDescriptionHeading>
|
||||
{drive.name}
|
||||
</ListViewDescriptionHeading>
|
||||
<ListViewDescriptionText>
|
||||
{drive.rootDisk &&
|
||||
<FormattedMessage {...messages.rootDisk} />}
|
||||
</ListViewDescriptionText>
|
||||
</ListViewDescription>
|
||||
<ListViewAdditionalInfo>
|
||||
<ListViewAdditionalInfoItem>
|
||||
<FormattedMessage {...messages.type} />
|
||||
<strong>{drive.rotational ? 'HDD' : 'SSD'}</strong>
|
||||
</ListViewAdditionalInfoItem>
|
||||
<ListViewAdditionalInfoItem>
|
||||
<FormattedMessage {...messages.size} />
|
||||
<strong>{driveSize}</strong>
|
||||
{driveSizeUnit}
|
||||
</ListViewAdditionalInfoItem>
|
||||
</ListViewAdditionalInfo>
|
||||
</ListViewBody>
|
||||
</ListViewMainInfo>
|
||||
</ListViewItemHeader>
|
||||
|
||||
<ListViewItemContainer
|
||||
onClose={this.toggleExpanded.bind(this)}
|
||||
expanded={this.state.expanded}
|
||||
>
|
||||
<Row>
|
||||
<Col sm={11}>
|
||||
<dl className="dl-horizontal">
|
||||
<dt><FormattedMessage {...messages.model} /></dt>
|
||||
<dd>{drive.model || <FormattedMessage {...messages.na} />}</dd>
|
||||
<dt><FormattedMessage {...messages.serial} /></dt>
|
||||
<dd>{drive.serial || <FormattedMessage {...messages.na} />}</dd>
|
||||
<dt><FormattedMessage {...messages.vendor} /></dt>
|
||||
<dd>{drive.vendor || <FormattedMessage {...messages.na} />}</dd>
|
||||
<dt><FormattedMessage {...messages.wwn} /></dt>
|
||||
<dd>{drive.wwn || <FormattedMessage {...messages.na} />}</dd>
|
||||
<dt><FormattedMessage {...messages.wwnVendorExtension} /></dt>
|
||||
<dd>
|
||||
{drive.wwn_vendor_extension ||
|
||||
<FormattedMessage {...messages.na} />}
|
||||
</dd>
|
||||
<dt><FormattedMessage {...messages.wwnWithExtension} /></dt>
|
||||
<dd>
|
||||
{drive.wwn_with_extension ||
|
||||
<FormattedMessage {...messages.na} />}
|
||||
</dd>
|
||||
</dl>
|
||||
</Col>
|
||||
</Row>
|
||||
</ListViewItemContainer>
|
||||
|
||||
</ListViewItem>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
NodeDrive.propTypes = {
|
||||
drive: PropTypes.object.isRequired
|
||||
};
|
|
@ -0,0 +1,90 @@
|
|||
/**
|
||||
* Copyright 2017 Red Hat Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License. You may obtain
|
||||
* a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { defineMessages, FormattedMessage } from 'react-intl';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { Link } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { getNodeDrives } from '../../../selectors/nodes';
|
||||
import { ListView } from '../../ui/ListView';
|
||||
import Modal from '../../ui/Modal';
|
||||
import NodeDrive from './NodeDrive';
|
||||
import NodesActions from '../../../actions/NodesActions';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: {
|
||||
id: 'NodeDrives.title',
|
||||
defaultMessage: 'Node Drives - {nodeId}'
|
||||
},
|
||||
close: {
|
||||
id: 'NodeDrives.close',
|
||||
defaultMessage: 'Close'
|
||||
}
|
||||
});
|
||||
|
||||
class NodeDrives extends Component {
|
||||
componentDidMount() {
|
||||
this.props.fetchNodeIntrospectionData();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Modal dialogClasses="modal-xl">
|
||||
<div className="modal-header">
|
||||
<Link to="/nodes" type="button" className="close">
|
||||
<span aria-hidden="true" className="pficon pficon-close" />
|
||||
</Link>
|
||||
<h4 className="modal-title">
|
||||
<FormattedMessage
|
||||
{...messages.title}
|
||||
values={{ nodeId: this.props.match.params.nodeId }}
|
||||
/>
|
||||
</h4>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<ListView>
|
||||
{this.props.drives
|
||||
.toJS()
|
||||
.map(drive => <NodeDrive key={drive.name} drive={drive} />)}
|
||||
</ListView>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<Link to="/nodes" type="button" className="btn btn-default">
|
||||
<FormattedMessage {...messages.close} />
|
||||
</Link>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
NodeDrives.propTypes = {
|
||||
drives: ImmutablePropTypes.list.isRequired,
|
||||
fetchNodeIntrospectionData: PropTypes.func.isRequired,
|
||||
match: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
drives: getNodeDrives(state, props.match.params.nodeId)
|
||||
});
|
||||
const mapDispatchToProps = (dispatch, props) => ({
|
||||
fetchNodeIntrospectionData: () =>
|
||||
dispatch(NodesActions.fetchNodeIntrospectionData(props.match.params.nodeId))
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(NodeDrives);
|
|
@ -25,6 +25,7 @@ import { Route } from 'react-router-dom';
|
|||
import { getFilterByName } from '../../selectors/filters';
|
||||
import { getFilteredNodes, nodesInProgress } from '../../selectors/nodes';
|
||||
import Loader from '../ui/Loader';
|
||||
import NodeDrives from './NodeDrives/NodeDrives';
|
||||
import NodesActions from '../../actions/NodesActions';
|
||||
import NodesListForm from './NodesListView/NodesListForm';
|
||||
import NodesListView from './NodesListView/NodesListView';
|
||||
|
@ -112,6 +113,7 @@ class Nodes extends React.Component {
|
|||
{this.renderContentView()}
|
||||
</Loader>
|
||||
<Route path="/nodes/register" component={RegisterNodesDialog} />
|
||||
<Route path="/nodes/:nodeId/drives" component={NodeDrives} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -91,7 +91,9 @@ const messages = defineMessages({
|
|||
|
||||
class NodeExtendedInfo extends React.Component {
|
||||
componentDidMount() {
|
||||
if (this.props.node.getIn(['introspectionStatus', 'finished'])) {
|
||||
if (
|
||||
this.props.node.getIn(['introspectionStatus', 'state']) === 'finished'
|
||||
) {
|
||||
this.props.fetchNodeIntrospectionData(this.props.node.get('uuid'));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,11 +16,13 @@
|
|||
|
||||
import ClassNames from 'classnames';
|
||||
import { defineMessages, FormattedMessage } from 'react-intl';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
import DropdownKebab from '../../ui/dropdown/DropdownKebab';
|
||||
import {
|
||||
ListViewActions,
|
||||
ListViewAdditionalInfo,
|
||||
ListViewAdditionalInfoItem,
|
||||
ListViewBody,
|
||||
|
@ -36,6 +38,7 @@ import {
|
|||
ListViewLeft,
|
||||
ListViewMainInfo
|
||||
} from '../../ui/ListView';
|
||||
import MenuItemLink from '../../ui/dropdown/MenuItemLink';
|
||||
import NodeExtendedInfo from './NodeExtendedInfo';
|
||||
import {
|
||||
NodeIntrospectionStatus,
|
||||
|
@ -62,6 +65,10 @@ const messages = defineMessages({
|
|||
disk: {
|
||||
id: 'NodeListItem.gbDisk',
|
||||
defaultMessage: 'GB Disk'
|
||||
},
|
||||
manageDrives: {
|
||||
id: 'NodeListItem.actions.manageDrives',
|
||||
defaultMessage: 'Manage Drives'
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -92,6 +99,14 @@ export default class NodeListItem extends React.Component {
|
|||
disabled={inProgress}
|
||||
name={`values.${node.get('uuid')}`}
|
||||
/>
|
||||
{node.getIn(['introspectionStatus', 'state']) === 'finished' &&
|
||||
<ListViewActions>
|
||||
<DropdownKebab id={`${node.get('uuid')}Actions`} pullRight>
|
||||
<MenuItemLink to={`/nodes/${node.get('uuid')}/drives`}>
|
||||
<FormattedMessage {...messages.manageDrives} />
|
||||
</MenuItemLink>
|
||||
</DropdownKebab>
|
||||
</ListViewActions>}
|
||||
<ListViewMainInfo>
|
||||
<ListViewLeft>
|
||||
<ListViewIcon size="sm" icon={iconClass} />
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ClassNames from 'classnames';
|
||||
import Timer from '../utils/Timer';
|
||||
import { Timer } from '../utils';
|
||||
|
||||
export default class Notification extends React.Component {
|
||||
constructor() {
|
||||
|
|
|
@ -8,8 +8,8 @@ import PropTypes from 'prop-types';
|
|||
<ListView>
|
||||
<ListViewItem stacked expanded>
|
||||
|
||||
<ListViewItemHeader> // required only if the ListViewItem is supposed to be expandable
|
||||
<ListViewExpand toggleExpanded={functionToToggle} expanded />
|
||||
<ListViewItemHeader toggleExpanded={functionToToggle}> // required only if the ListViewItem is supposed to be expandable
|
||||
<ListViewExpand expanded />
|
||||
<ListViewCheckbox disabled={inProgress} name={`values.${node.uuid}`} />
|
||||
<ListViewActions>
|
||||
// buttons, dropdowns...
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
/**
|
||||
* Copyright 2017 Red Hat Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License. You may obtain
|
||||
* a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { Link, Route } from 'react-router-dom';
|
||||
|
||||
const MenuItemLink = ({ children, to, exact, location }) => {
|
||||
return (
|
||||
<Route
|
||||
path={to}
|
||||
exact={exact}
|
||||
children={({ match, location }) => (
|
||||
<li onClick={e => e.stopPropagation()}>
|
||||
<Link to={to}>{children}</Link>
|
||||
</li>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
MenuItemLink.propTypes = {
|
||||
children: PropTypes.node,
|
||||
exact: PropTypes.bool.isRequired,
|
||||
location: PropTypes.object,
|
||||
to: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired
|
||||
};
|
||||
MenuItemLink.defaultProps = {
|
||||
exact: false
|
||||
};
|
||||
|
||||
export default MenuItemLink;
|
|
@ -14,7 +14,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export default function Timer(callback, delay) {
|
||||
export function Timer(callback, delay) {
|
||||
var timerId;
|
||||
var start;
|
||||
var remaining = delay;
|
||||
|
@ -36,3 +36,15 @@ export default function Timer(callback, delay) {
|
|||
|
||||
this.resume();
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function to format value in Bytes to human readable format
|
||||
*/
|
||||
export const formatBytes = (bytes, decimals) => {
|
||||
if (bytes == 0) return '0 Bytes';
|
||||
var k = 1000,
|
||||
dm = decimals || 2,
|
||||
sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
|
||||
i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return [parseFloat((bytes / Math.pow(k, i)).toFixed(dm)), sizes[i]];
|
||||
};
|
|
@ -32,6 +32,8 @@ export const getNodesByIds = (state, nodeIds) =>
|
|||
export const getPorts = state => state.nodes.get('ports');
|
||||
export const getIntrospectionData = state =>
|
||||
state.nodes.get('introspectionData');
|
||||
export const getNodeIntrospectionData = (state, nodeId) =>
|
||||
state.nodes.introspectionData.get(nodeId, Map());
|
||||
export const getIntrospectionStatuses = state =>
|
||||
state.nodes.get('introspectionStatuses');
|
||||
export const nodesInProgress = state => state.nodes.get('nodesInProgress');
|
||||
|
@ -145,6 +147,20 @@ export const getNodesOperationInProgress = createSelector(
|
|||
nodesInProgress => !nodesInProgress.isEmpty()
|
||||
);
|
||||
|
||||
export const getNodeDrives = createSelector(
|
||||
getNodeIntrospectionData,
|
||||
nodeIntrospectionData =>
|
||||
nodeIntrospectionData
|
||||
.getIn(['inventory', 'disks'], List())
|
||||
.map(disk =>
|
||||
disk.set(
|
||||
'rootDisk',
|
||||
nodeIntrospectionData.getIn(['root_disk', 'name']) ===
|
||||
disk.get('name')
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* Helper function to get node capabilities object
|
||||
* @param node
|
||||
|
|
Loading…
Reference in New Issue