Show Node Introspection Data
* Fetches introspection data on expanding node * If introspection data are available, display them in NodeExtendedInfo Change-Id: I5feac2b64803e1cf2c31f0bbc259940a80ddae3c
This commit is contained in:
parent
7e65fba52f
commit
046188f13a
@ -0,0 +1,5 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Nodes List view now fetches and displays introspection data in expanded
|
||||
Node view
|
@ -121,6 +121,12 @@ let createResolvingPromise = data => {
|
||||
};
|
||||
};
|
||||
|
||||
let createRejectingPromise = error => {
|
||||
return () => {
|
||||
return when.reject(error);
|
||||
};
|
||||
};
|
||||
|
||||
describe('Asynchronous Nodes Actions', () => {
|
||||
beforeEach(done => {
|
||||
spyOn(utils, 'getAuthTokenId').and.returnValue('mock-auth-token');
|
||||
@ -184,6 +190,72 @@ describe('Asynchronous Nodes Actions', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Fetching Introspection data success', () => {
|
||||
const response = { interfaces: { eth0: { mac: '00:00:00:00:00:11' } } };
|
||||
const nodeId = '598612eb-f21b-435e-a868-7bb74e576cc2';
|
||||
|
||||
beforeEach(done => {
|
||||
spyOn(utils, 'getAuthTokenId').and.returnValue('mock-auth-token');
|
||||
spyOn(utils, 'getServiceUrl').and.returnValue('mock-url');
|
||||
spyOn(NodesActions, 'fetchNodeIntrospectionDataSuccess');
|
||||
spyOn(IronicInspectorApiService, 'getIntrospectionData').and.callFake(
|
||||
createResolvingPromise(response)
|
||||
);
|
||||
|
||||
NodesActions.fetchNodeIntrospectionData(nodeId)(() => {}, () => {});
|
||||
setTimeout(() => {
|
||||
done();
|
||||
}, 1);
|
||||
});
|
||||
|
||||
it('dispatches fetchNodeIntrospectionDataSuccess', () => {
|
||||
expect(IronicInspectorApiService.getIntrospectionData).toHaveBeenCalledWith(
|
||||
nodeId
|
||||
);
|
||||
expect(NodesActions.fetchNodeIntrospectionDataSuccess).toHaveBeenCalledWith(
|
||||
nodeId,
|
||||
response
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Fetching Introspection data error', () => {
|
||||
const nodeId = '598612eb-f21b-435e-a868-7bb74e576cc2';
|
||||
const message = 'Data for specified node not available';
|
||||
const error = {
|
||||
status: 404,
|
||||
responseText: `{ "error": { "message": "${message}" } }`
|
||||
};
|
||||
|
||||
beforeEach(done => {
|
||||
spyOn(utils, 'getAuthTokenId').and.returnValue('mock-auth-token');
|
||||
spyOn(utils, 'getServiceUrl').and.returnValue('mock-url');
|
||||
spyOn(NodesActions, 'fetchNodeIntrospectionDataFailed');
|
||||
spyOn(NotificationActions, 'notify');
|
||||
spyOn(IronicInspectorApiService, 'getIntrospectionData').and.callFake(
|
||||
createRejectingPromise(error)
|
||||
);
|
||||
|
||||
NodesActions.fetchNodeIntrospectionData(nodeId)(() => {}, () => {});
|
||||
setTimeout(() => {
|
||||
done();
|
||||
}, 1);
|
||||
});
|
||||
|
||||
it('dispatches fetchNodeIntrospectionDataFailed', () => {
|
||||
expect(IronicInspectorApiService.getIntrospectionData).toHaveBeenCalledWith(
|
||||
nodeId
|
||||
);
|
||||
expect(NodesActions.fetchNodeIntrospectionDataFailed).toHaveBeenCalledWith(
|
||||
nodeId
|
||||
);
|
||||
expect(NotificationActions.notify).toHaveBeenCalledWith({
|
||||
title: 'Introspection data not found',
|
||||
message: message
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Asynchronous Introspect Nodes Action', () => {
|
||||
beforeEach(done => {
|
||||
spyOn(utils, 'getAuthTokenId').and.returnValue('mock-auth-token');
|
||||
|
@ -14,31 +14,25 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { Map, Set } from 'immutable';
|
||||
import { fromJS, Map, Set } from 'immutable';
|
||||
|
||||
import NodesConstants from '../../js/constants/NodesConstants';
|
||||
import nodesReducer from '../../js/reducers/nodesReducer';
|
||||
import { NodesState } from '../../js/immutableRecords/nodes';
|
||||
|
||||
describe('nodesReducer', () => {
|
||||
const initialState = Map({
|
||||
const initialState = new NodesState({
|
||||
isFetching: false,
|
||||
nodesInProgress: Set(),
|
||||
allFilter: '',
|
||||
registeredFilter: '',
|
||||
introspectedFilter: '',
|
||||
deployedFilter: '',
|
||||
maintenanceFilter: '',
|
||||
all: Map()
|
||||
all: Map(),
|
||||
ports: Map(),
|
||||
introspectionStatuses: Map(),
|
||||
introspectionData: Map()
|
||||
});
|
||||
|
||||
const updatedState = Map({
|
||||
const updatedState = new NodesState({
|
||||
isFetching: false,
|
||||
nodesInProgress: Set(),
|
||||
allFilter: '',
|
||||
registeredFilter: '',
|
||||
introspectedFilter: '',
|
||||
deployedFilter: '',
|
||||
maintenanceFilter: '',
|
||||
all: Map({
|
||||
uuid1: Map({
|
||||
uuid: 'uuid1'
|
||||
@ -46,17 +40,15 @@ describe('nodesReducer', () => {
|
||||
uuid2: Map({
|
||||
uuid: 'uuid2'
|
||||
})
|
||||
})
|
||||
}),
|
||||
ports: Map(),
|
||||
introspectionStatuses: Map(),
|
||||
introspectionData: Map()
|
||||
});
|
||||
|
||||
const updatedNodeState = Map({
|
||||
const updatedNodeState = new NodesState({
|
||||
isFetching: false,
|
||||
nodesInProgress: Set(),
|
||||
allFilter: '',
|
||||
registeredFilter: '',
|
||||
introspectedFilter: '',
|
||||
deployedFilter: '',
|
||||
maintenanceFilter: '',
|
||||
all: Map({
|
||||
uuid1: Map({
|
||||
uuid: 'uuid1',
|
||||
@ -67,7 +59,10 @@ describe('nodesReducer', () => {
|
||||
uuid2: Map({
|
||||
uuid: 'uuid2'
|
||||
})
|
||||
})
|
||||
}),
|
||||
ports: Map(),
|
||||
introspectionStatuses: Map(),
|
||||
introspectionData: Map()
|
||||
});
|
||||
|
||||
it('should return initial state', () => {
|
||||
@ -144,4 +139,27 @@ describe('nodesReducer', () => {
|
||||
const newState = nodesReducer(initialState, action);
|
||||
expect(newState).toEqual(updatedState);
|
||||
});
|
||||
|
||||
it('should handle FETCH_NODE_INTROSPECTION_DATA_SUCCESS action', () => {
|
||||
const action = {
|
||||
type: NodesConstants.FETCH_NODE_INTROSPECTION_DATA_SUCCESS,
|
||||
payload: {
|
||||
nodeId: 'uuid1',
|
||||
data: { interfaces: { eth0: { mac: '00:00:00:00:00:11' } } }
|
||||
}
|
||||
};
|
||||
const newState = nodesReducer(updatedState, action);
|
||||
expect(newState.introspectionData.get('uuid1')).toEqual(
|
||||
fromJS(action.payload.data)
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle FETCH_NODE_INTROSPECTION_DATA_FAILED action', () => {
|
||||
const action = {
|
||||
type: NodesConstants.FETCH_NODE_INTROSPECTION_DATA_FAILED,
|
||||
payload: 'uuid1'
|
||||
};
|
||||
const newState = nodesReducer(updatedState, action);
|
||||
expect(newState.introspectionData.get('uuid1')).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
@ -20,6 +20,8 @@ import when from 'when';
|
||||
|
||||
import { getNodesByIds } from '../selectors/nodes';
|
||||
import IronicApiErrorHandler from '../services/IronicApiErrorHandler';
|
||||
import IronicInspectorApiErrorHandler
|
||||
from '../services/IronicInspectorApiErrorHandler';
|
||||
import IronicApiService from '../services/IronicApiService';
|
||||
import IronicInspectorApiService from '../services/IronicInspectorApiService';
|
||||
import MistralApiService from '../services/MistralApiService';
|
||||
@ -119,6 +121,40 @@ export default {
|
||||
};
|
||||
},
|
||||
|
||||
fetchNodeIntrospectionDataSuccess(nodeId, data) {
|
||||
return {
|
||||
type: NodesConstants.FETCH_NODE_INTROSPECTION_DATA_SUCCESS,
|
||||
payload: { nodeId, data }
|
||||
};
|
||||
},
|
||||
|
||||
fetchNodeIntrospectionDataFailed(nodeId) {
|
||||
return {
|
||||
type: NodesConstants.FETCH_NODE_INTROSPECTION_DATA_FAILED,
|
||||
payload: nodeId
|
||||
};
|
||||
},
|
||||
|
||||
fetchNodeIntrospectionData(nodeId) {
|
||||
return dispatch => {
|
||||
IronicInspectorApiService.getIntrospectionData(nodeId)
|
||||
.then(response => {
|
||||
dispatch(this.fetchNodeIntrospectionDataSuccess(nodeId, response));
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch(this.fetchNodeIntrospectionDataFailed(nodeId));
|
||||
logger.error(
|
||||
'Error in NodesActions.fetchNodeIntrospectionData',
|
||||
error.stack || error
|
||||
);
|
||||
let errorHandler = new IronicInspectorApiErrorHandler(error);
|
||||
errorHandler.errors.forEach(error => {
|
||||
dispatch(NotificationActions.notify(error));
|
||||
});
|
||||
});
|
||||
};
|
||||
},
|
||||
|
||||
/*
|
||||
* Poll fetchNodes until no node is in progress
|
||||
*/
|
||||
|
@ -69,6 +69,7 @@ class Nodes extends React.Component {
|
||||
? <NodesTableView />
|
||||
: <NodesListForm>
|
||||
<NodesListView
|
||||
fetchNodeIntrospectionData={this.props.fetchNodeIntrospectionData}
|
||||
nodes={this.props.nodes}
|
||||
nodesInProgress={this.props.nodesInProgress}
|
||||
/>
|
||||
@ -118,6 +119,7 @@ class Nodes extends React.Component {
|
||||
Nodes.propTypes = {
|
||||
contentView: PropTypes.string.isRequired,
|
||||
currentPlanName: PropTypes.string.isRequired,
|
||||
fetchNodeIntrospectionData: PropTypes.func.isRequired,
|
||||
fetchNodes: PropTypes.func.isRequired,
|
||||
fetchRoles: PropTypes.func.isRequired,
|
||||
fetchingNodes: PropTypes.bool.isRequired,
|
||||
@ -141,6 +143,8 @@ const mapStateToProps = state => ({
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
fetchNodes: () => dispatch(NodesActions.fetchNodes()),
|
||||
fetchNodeIntrospectionData: nodeId =>
|
||||
dispatch(NodesActions.fetchNodeIntrospectionData(nodeId)),
|
||||
fetchRoles: currentPlanName =>
|
||||
dispatch(RolesActions.fetchRoles(currentPlanName))
|
||||
});
|
||||
|
@ -1,52 +1,251 @@
|
||||
import { FormattedDate, FormattedTime } from 'react-intl';
|
||||
/**
|
||||
* 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,
|
||||
FormattedDate,
|
||||
FormattedMessage,
|
||||
FormattedTime,
|
||||
injectIntl
|
||||
} from 'react-intl';
|
||||
import { startCase } from 'lodash';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Row, Col } from 'react-bootstrap';
|
||||
|
||||
const messages = defineMessages({
|
||||
macAddresses: {
|
||||
id: 'NodeExtendedInfo.macAddresses',
|
||||
defaultMessage: 'Mac Addresses:'
|
||||
},
|
||||
interfaces: {
|
||||
id: 'nodeExtendedinfo.interfaces',
|
||||
defaultMessage: 'Interfaces:'
|
||||
},
|
||||
macAddress: {
|
||||
id: 'nodeExtendedinfo.interfaceMacAddress',
|
||||
defaultMessage: 'MAC Address'
|
||||
},
|
||||
ipAddress: {
|
||||
id: 'nodeExtendedinfo.interfaceIpAddress',
|
||||
defaultMessage: 'IP Address'
|
||||
},
|
||||
bios: {
|
||||
id: 'nodeExtendedinfo.bios',
|
||||
defaultMessage: 'Bios:'
|
||||
},
|
||||
rootDisk: {
|
||||
id: 'nodeExtendedinfo.rootDisk',
|
||||
defaultMessage: 'Root Disk:'
|
||||
},
|
||||
product: {
|
||||
id: 'nodeExtendedinfo.product',
|
||||
defaultMessage: 'Product:'
|
||||
},
|
||||
productName: {
|
||||
id: 'nodeExtendedinfo.productName',
|
||||
defaultMessage: 'Name'
|
||||
},
|
||||
productVendor: {
|
||||
id: 'nodeExtendedinfo.productVendor',
|
||||
defaultMessage: 'Vendor'
|
||||
},
|
||||
productVersion: {
|
||||
id: 'nodeExtendedinfo.productVersion',
|
||||
defaultMessage: 'Version'
|
||||
},
|
||||
kernel: {
|
||||
id: 'nodeExtendedinfo.kernel',
|
||||
defaultMessage: 'Kernel:'
|
||||
},
|
||||
uuid: {
|
||||
id: 'nodeExtendedinfo.uuid',
|
||||
defaultMessage: 'UUID:'
|
||||
},
|
||||
registered: {
|
||||
id: 'nodeExtendedinfo.registered',
|
||||
defaultMessage: 'Registered:'
|
||||
},
|
||||
architecture: {
|
||||
id: 'nodeExtendedinfo.architecture',
|
||||
defaultMessage: 'Architecture:'
|
||||
},
|
||||
driver: {
|
||||
id: 'nodeExtendedinfo.driver',
|
||||
defaultMessage: 'Driver:'
|
||||
}
|
||||
});
|
||||
|
||||
class NodeExtendedInfo extends React.Component {
|
||||
componentDidMount() {
|
||||
if (this.props.node.getIn(['introspectionStatus', 'finished'])) {
|
||||
this.props.fetchNodeIntrospectionData(this.props.node.get('uuid'));
|
||||
}
|
||||
}
|
||||
|
||||
renderInterfaces() {
|
||||
const { intl, node } = this.props;
|
||||
if (node.getIn(['introspectionData', 'interfaces']).isEmpty()) {
|
||||
return (
|
||||
<dl>
|
||||
<dt><FormattedMessage {...messages.macAddresses} /></dt>
|
||||
{node.get('macs').map(mac => <dd key={mac}>{mac}</dd>)}
|
||||
</dl>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<dl>
|
||||
<dt><FormattedMessage {...messages.interfaces} /></dt>
|
||||
<dd>
|
||||
{node
|
||||
.getIn(['introspectionData', 'interfaces'])
|
||||
.map((ifc, k) => {
|
||||
return (
|
||||
<div key={k}>
|
||||
{k}
|
||||
{' '} - {' '}
|
||||
<span title={intl.formatMessage(messages.macAddress)}>
|
||||
{ifc.get('mac')}
|
||||
</span>
|
||||
{' '} | {' '}
|
||||
<span title={intl.formatMessage(messages.ipAddress)}>
|
||||
{ifc.get('ip')}
|
||||
</span>
|
||||
{ifc.get('pxe') && '| PXE'}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
.toList()}
|
||||
</dd>
|
||||
</dl>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
renderBios() {
|
||||
const bios = this.props.node.getIn(['introspectionData', 'bios']);
|
||||
return (
|
||||
!bios.isEmpty() &&
|
||||
<div>
|
||||
<dt><FormattedMessage {...messages.bios} /></dt>
|
||||
<dd>
|
||||
{bios
|
||||
.map((i, k) => <span key={k} title={startCase(k)}>{i} </span>)
|
||||
.toList()}
|
||||
</dd>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderRootDisk() {
|
||||
const rootDisk = this.props.node.getIn(['introspectionData', 'rootDisk']);
|
||||
return (
|
||||
rootDisk &&
|
||||
<div>
|
||||
<dt><FormattedMessage {...messages.rootDisk} /></dt>
|
||||
<dd>{rootDisk}</dd>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderProduct() {
|
||||
const product = this.props.node.getIn(['introspectionData', 'product']);
|
||||
return (
|
||||
!product.isEmpty() &&
|
||||
<div>
|
||||
<dt><FormattedMessage {...messages.product} /></dt>
|
||||
<dd>
|
||||
<span title={this.props.intl.formatMessage(messages.productName)}>
|
||||
{product.get('name')}
|
||||
</span>
|
||||
{' '} - {' '}
|
||||
<span title={this.props.intl.formatMessage(messages.productVendor)}>
|
||||
{product.get('vendor')}
|
||||
</span>
|
||||
{' '} | {' '}
|
||||
<span title={this.props.intl.formatMessage(messages.productVersion)}>
|
||||
{product.get('version')}
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderKernel() {
|
||||
const kernelVersion = this.props.node.getIn([
|
||||
'introspectionData',
|
||||
'kernelVersion'
|
||||
]);
|
||||
return (
|
||||
kernelVersion &&
|
||||
<div>
|
||||
<dt><FormattedMessage {...messages.kernel} /></dt>
|
||||
<dd>{kernelVersion}</dd>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { node } = this.props;
|
||||
return (
|
||||
<Row>
|
||||
<Col lg={3} md={6}>
|
||||
<Col lg={4} md={6}>
|
||||
<dl className="dl-horizontal dl-horizontal-condensed">
|
||||
<dt>UUID:</dt>
|
||||
<dd>{node.uuid}</dd>
|
||||
<dt>Registered:</dt>
|
||||
<dt><FormattedMessage {...messages.uuid} /></dt>
|
||||
<dd>{node.get('uuid')}</dd>
|
||||
<dt><FormattedMessage {...messages.registered} /></dt>
|
||||
<dd>
|
||||
<FormattedDate value={node.created_at} />
|
||||
<FormattedDate value={node.get('created_at')} />
|
||||
|
||||
<FormattedTime value={node.created_at} />
|
||||
<FormattedTime value={node.get('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>)}
|
||||
<dt><FormattedMessage {...messages.architecture} /></dt>
|
||||
<dd>{node.getIn(['properties', 'cpu_arch'])}</dd>
|
||||
{this.renderRootDisk()}
|
||||
{this.renderBios()}
|
||||
{this.renderProduct()}
|
||||
{this.renderKernel()}
|
||||
</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>
|
||||
))}
|
||||
<dt><FormattedMessage {...messages.driver} /></dt>
|
||||
<dd>{node.get('driver')}</dd>
|
||||
{node
|
||||
.get('driver_info')
|
||||
.map((dInfo, key) => (
|
||||
<span key={key}>
|
||||
<dt>{startCase(key)}:</dt>
|
||||
<dd>{dInfo}</dd>
|
||||
</span>
|
||||
))
|
||||
.toList()}
|
||||
</dl>
|
||||
</Col>
|
||||
<Col lg={3} md={6}>
|
||||
{this.renderInterfaces()}
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
}
|
||||
NodeExtendedInfo.propTypes = {
|
||||
fetchNodeIntrospectionData: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
node: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default NodeExtendedInfo;
|
||||
export default injectIntl(NodeExtendedInfo);
|
||||
|
@ -1,7 +1,24 @@
|
||||
/**
|
||||
* 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 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 {
|
||||
ListViewAdditionalInfo,
|
||||
@ -61,7 +78,7 @@ export default class NodeListItem extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { node, inProgress } = this.props;
|
||||
const { fetchNodeIntrospectionData, node, inProgress } = this.props;
|
||||
|
||||
const iconClass = ClassNames({
|
||||
'pficon pficon-server': true,
|
||||
@ -73,7 +90,7 @@ export default class NodeListItem extends React.Component {
|
||||
<ListViewExpand expanded={this.state.expanded} />
|
||||
<ListViewCheckbox
|
||||
disabled={inProgress}
|
||||
name={`values.${node.uuid}`}
|
||||
name={`values.${node.get('uuid')}`}
|
||||
/>
|
||||
<ListViewMainInfo>
|
||||
<ListViewLeft>
|
||||
@ -82,23 +99,25 @@ export default class NodeListItem extends React.Component {
|
||||
<ListViewBody>
|
||||
<ListViewDescription>
|
||||
<ListViewDescriptionHeading>
|
||||
{node.name || node.uuid}
|
||||
{node.get('name') || node.get('uuid')}
|
||||
</ListViewDescriptionHeading>
|
||||
<ListViewDescriptionText>
|
||||
<NodePowerState
|
||||
powerState={node.power_state}
|
||||
targetPowerState={node.target_power_state}
|
||||
powerState={node.get('power_state')}
|
||||
targetPowerState={node.get('target_power_state')}
|
||||
/>
|
||||
<NodeMaintenanceState
|
||||
maintenance={node.maintenance}
|
||||
reason={node.maintenance_reason}
|
||||
maintenance={node.get('maintenance')}
|
||||
reason={node.get('maintenance_reason')}
|
||||
/>
|
||||
{' | '}
|
||||
<NodeIntrospectionStatus status={node.introspectionStatus} />
|
||||
<NodeIntrospectionStatus
|
||||
status={node.get('introspectionStatus').toJS()}
|
||||
/>
|
||||
{' | '}
|
||||
<NodeProvisionState
|
||||
provisionState={node.provision_state}
|
||||
targetProvisionState={node.target_provision_state}
|
||||
provisionState={node.get('provision_state')}
|
||||
targetProvisionState={node.get('target_provision_state')}
|
||||
/>
|
||||
</ListViewDescriptionText>
|
||||
</ListViewDescription>
|
||||
@ -107,27 +126,30 @@ export default class NodeListItem extends React.Component {
|
||||
<span className="pficon pficon-flavor" />
|
||||
<FormattedMessage {...messages.profile} />
|
||||
|
||||
{parseNodeCapabilities(node.properties.capabilities)
|
||||
.profile || '-'}
|
||||
{parseNodeCapabilities(
|
||||
node.getIn(['properties', 'capabilities'])
|
||||
).profile || '-'}
|
||||
</ListViewAdditionalInfoItem>
|
||||
<ListViewAdditionalInfoItem>
|
||||
<span className="pficon pficon-cpu" />
|
||||
<strong>{node.properties.cpus || '-'}</strong>
|
||||
<strong>{node.getIn(['properties', 'cpus'], '-')}</strong>
|
||||
|
||||
<FormattedMessage
|
||||
{...messages.cpuCores}
|
||||
values={{ cpuCores: node.properties.cpus }}
|
||||
values={{ cpuCores: node.getIn(['properties', 'cpus']) }}
|
||||
/>
|
||||
</ListViewAdditionalInfoItem>
|
||||
<ListViewAdditionalInfoItem>
|
||||
<span className="pficon pficon-memory" />
|
||||
<strong>{node.properties.memory_mb || '-'}</strong>
|
||||
<strong>
|
||||
{node.getIn(['properties', 'memory_mb'], '-')}
|
||||
</strong>
|
||||
|
||||
<FormattedMessage {...messages.ram} />
|
||||
</ListViewAdditionalInfoItem>
|
||||
<ListViewAdditionalInfoItem>
|
||||
<span className="fa fa-database" />
|
||||
<strong>{node.properties.local_gb || '-'}</strong>
|
||||
<strong>{node.getIn(['properties', 'local_gb'], '-')}</strong>
|
||||
|
||||
<FormattedMessage {...messages.disk} />
|
||||
</ListViewAdditionalInfoItem>
|
||||
@ -139,13 +161,17 @@ export default class NodeListItem extends React.Component {
|
||||
onClose={this.toggleExpanded.bind(this)}
|
||||
expanded={this.state.expanded}
|
||||
>
|
||||
<NodeExtendedInfo node={node} />
|
||||
<NodeExtendedInfo
|
||||
node={node}
|
||||
fetchNodeIntrospectionData={fetchNodeIntrospectionData}
|
||||
/>
|
||||
</ListViewItemContainer>
|
||||
</ListViewItem>
|
||||
);
|
||||
}
|
||||
}
|
||||
NodeListItem.propTypes = {
|
||||
fetchNodeIntrospectionData: PropTypes.func.isRequired,
|
||||
inProgress: PropTypes.bool.isRequired,
|
||||
node: PropTypes.object.isRequired
|
||||
node: ImmutablePropTypes.map.isRequired
|
||||
};
|
||||
|
@ -1,3 +1,19 @@
|
||||
/**
|
||||
* 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 ClassNames from 'classnames';
|
||||
import { defineMessages, FormattedMessage } from 'react-intl';
|
||||
import React from 'react';
|
||||
|
@ -1,3 +1,19 @@
|
||||
/**
|
||||
* 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, injectIntl } from 'react-intl';
|
||||
import React from 'react';
|
||||
|
@ -1,4 +1,21 @@
|
||||
/**
|
||||
* 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 ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
import { ListView } from '../../ui/ListView';
|
||||
@ -10,12 +27,12 @@ export default class NodesListView extends React.Component {
|
||||
<ListView>
|
||||
{this.props.nodes
|
||||
.toList()
|
||||
.toJS()
|
||||
.map(node => (
|
||||
<NodeListItem
|
||||
fetchNodeIntrospectionData={this.props.fetchNodeIntrospectionData}
|
||||
node={node}
|
||||
key={node.uuid}
|
||||
inProgress={this.props.nodesInProgress.includes(node.uuid)}
|
||||
key={node.get('uuid')}
|
||||
inProgress={this.props.nodesInProgress.includes(node.get('uuid'))}
|
||||
/>
|
||||
))}
|
||||
</ListView>
|
||||
@ -23,6 +40,7 @@ export default class NodesListView extends React.Component {
|
||||
}
|
||||
}
|
||||
NodesListView.propTypes = {
|
||||
fetchNodeIntrospectionData: PropTypes.func.isRequired,
|
||||
nodes: ImmutablePropTypes.map.isRequired,
|
||||
nodesInProgress: ImmutablePropTypes.set.isRequired
|
||||
};
|
||||
|
@ -20,6 +20,8 @@ export default keyMirror({
|
||||
REQUEST_NODES: null,
|
||||
RECEIVE_NODES: null,
|
||||
FETCH_NODE_MACS_SUCCESS: null,
|
||||
FETCH_NODE_INTROSPECTION_DATA_SUCCESS: null,
|
||||
FETCH_NODE_INTROSPECTION_DATA_FAILED: null,
|
||||
START_NODES_OPERATION: null,
|
||||
FINISH_NODES_OPERATION: null,
|
||||
UPDATE_NODE_PENDING: null,
|
||||
|
@ -22,7 +22,8 @@ export const NodesState = Record({
|
||||
nodesInProgress: Set(),
|
||||
all: Map(),
|
||||
ports: Map(),
|
||||
introspectionStatuses: Map()
|
||||
introspectionStatuses: Map(),
|
||||
introspectionData: Map()
|
||||
});
|
||||
|
||||
export const NodeToRegister = Record({
|
||||
|
@ -43,6 +43,14 @@ export default function nodesReducer(state = initialState, action) {
|
||||
.set('isFetching', false);
|
||||
}
|
||||
|
||||
case NodesConstants.FETCH_NODE_INTROSPECTION_DATA_SUCCESS: {
|
||||
const { nodeId, data } = action.payload;
|
||||
return state.setIn(['introspectionData', nodeId], fromJS(data));
|
||||
}
|
||||
|
||||
case NodesConstants.FETCH_NODE_INTROSPECTION_DATA_FAILED:
|
||||
return state.deleteIn(['introspectionData', action.payload]);
|
||||
|
||||
case NodesConstants.START_NODES_OPERATION:
|
||||
return state.update('nodesInProgress', nodesInProgress =>
|
||||
nodesInProgress.union(action.payload)
|
||||
|
@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { createSelector } from 'reselect';
|
||||
import { List, Set } from 'immutable';
|
||||
import { List, Map, Set } from 'immutable';
|
||||
|
||||
import { getFilterByName } from './filters';
|
||||
import { getRoles } from './roles';
|
||||
@ -30,6 +30,8 @@ export const getNodesByIds = (state, nodeIds) =>
|
||||
.filter((v, k) => nodeIds.includes(k))
|
||||
.sortBy(n => n.get('uuid'));
|
||||
export const getPorts = state => state.nodes.get('ports');
|
||||
export const getIntrospectionData = state =>
|
||||
state.nodes.get('introspectionData');
|
||||
export const getIntrospectionStatuses = state =>
|
||||
state.nodes.get('introspectionStatuses');
|
||||
export const nodesInProgress = state => state.nodes.get('nodesInProgress');
|
||||
@ -40,8 +42,8 @@ export const nodesToolbarFilter = state =>
|
||||
* Return Nodes including mac addresses as string at macs attribute
|
||||
*/
|
||||
export const getDetailedNodes = createSelector(
|
||||
[getNodes, getPorts, getIntrospectionStatuses],
|
||||
(nodes, ports, introspectionStatuses) =>
|
||||
[getNodes, getPorts, getIntrospectionStatuses, getIntrospectionData],
|
||||
(nodes, ports, introspectionStatuses, introspectionData) =>
|
||||
nodes.map(node =>
|
||||
node
|
||||
.set(
|
||||
@ -55,6 +57,38 @@ export const getDetailedNodes = createSelector(
|
||||
'introspectionStatus',
|
||||
introspectionStatuses.get(node.get('uuid'), new IntrospectionStatus())
|
||||
)
|
||||
.setIn(
|
||||
['introspectionData', 'interfaces'],
|
||||
introspectionData.getIn([node.get('uuid'), 'interfaces'], Map())
|
||||
)
|
||||
.setIn(
|
||||
['introspectionData', 'rootDisk'],
|
||||
introspectionData.getIn([node.get('uuid'), 'root_disk', 'name'])
|
||||
)
|
||||
.setIn(
|
||||
['introspectionData', 'product'],
|
||||
introspectionData.getIn(
|
||||
[node.get('uuid'), 'extra', 'system', 'product'],
|
||||
Map()
|
||||
)
|
||||
)
|
||||
.setIn(
|
||||
['introspectionData', 'kernelVersion'],
|
||||
introspectionData.getIn([
|
||||
node.get('uuid'),
|
||||
'extra',
|
||||
'system',
|
||||
'kernel',
|
||||
'version'
|
||||
])
|
||||
)
|
||||
.setIn(
|
||||
['introspectionData', 'bios'],
|
||||
introspectionData.getIn(
|
||||
[node.get('uuid'), 'extra', 'firmware', 'bios'],
|
||||
Map()
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
|
74
src/js/services/IronicInspectorApiErrorHandler.js
Normal file
74
src/js/services/IronicInspectorApiErrorHandler.js
Normal file
@ -0,0 +1,74 @@
|
||||
/**
|
||||
* 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 BaseHttpRequestErrorHandler
|
||||
from '../components/utils/BaseHttpRequestErrorHandler';
|
||||
import LoginActions from '../actions/LoginActions';
|
||||
import store from '../store';
|
||||
|
||||
export default class IronicInspectorApiErrorHandler
|
||||
extends BaseHttpRequestErrorHandler {
|
||||
_generateErrors(errorObj) {
|
||||
let errors = [];
|
||||
let error;
|
||||
// A weak check to find out if it's not an xhr object.
|
||||
if (!errorObj.status && errorObj.message) {
|
||||
errors.push({
|
||||
title: 'Error',
|
||||
message: errorObj.message
|
||||
});
|
||||
return errors;
|
||||
}
|
||||
switch (errorObj.status) {
|
||||
case 0:
|
||||
errors.push({
|
||||
title: 'Connection Error',
|
||||
message: 'Connection to Ironic Inspector is not available'
|
||||
});
|
||||
break;
|
||||
case 404:
|
||||
error = JSON.parse(errorObj.responseText).error.message;
|
||||
errors.push({
|
||||
title: 'Introspection data not found',
|
||||
message: error
|
||||
});
|
||||
break;
|
||||
case 401:
|
||||
error = JSON.parse(errorObj.responseText).error.message;
|
||||
errors.push({
|
||||
title: 'Unauthorized',
|
||||
message: error
|
||||
});
|
||||
store.dispatch(LoginActions.logoutUser());
|
||||
break;
|
||||
default:
|
||||
error = JSON.parse(errorObj.responseText).error.message;
|
||||
status = errorObj.status;
|
||||
errors.push({
|
||||
title: `Error ${status}`,
|
||||
message: error
|
||||
});
|
||||
break;
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
// TODO(jtomasek): remove this, I am leaving this here just for example reasons
|
||||
// this function should be implemented by form related subclass.
|
||||
_generateFormFieldErrors() {
|
||||
return {};
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user