475 lines
23 KiB
JavaScript
475 lines
23 KiB
JavaScript
/*
|
|
* Copyright 2015 Mirantis, 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.
|
|
**/
|
|
define(
|
|
[
|
|
'jquery',
|
|
'underscore',
|
|
'i18n',
|
|
'backbone',
|
|
'react',
|
|
'utils',
|
|
'models',
|
|
'dispatcher',
|
|
'views/controls',
|
|
'views/dialogs',
|
|
'component_mixins'
|
|
],
|
|
($, _, i18n, Backbone, React, utils, models, dispatcher, controls, dialogs, componentMixins) => {
|
|
'use strict';
|
|
|
|
var Node = React.createClass({
|
|
mixins: [componentMixins.renamingMixin('name')],
|
|
getInitialState() {
|
|
return {
|
|
actionInProgress: false,
|
|
extendedView: false,
|
|
labelsPopoverVisible: false
|
|
};
|
|
},
|
|
componentDidUpdate() {
|
|
if (!this.props.node.get('cluster') && !this.props.checked) {
|
|
this.props.node.set({pending_roles: []}, {assign: true});
|
|
}
|
|
},
|
|
getNodeLogsLink() {
|
|
var status = this.props.node.get('status'),
|
|
error = this.props.node.get('error_type'),
|
|
options = {type: 'remote', node: this.props.node.id};
|
|
if (status == 'discover') {
|
|
options.source = 'bootstrap/messages';
|
|
} else if (status == 'provisioning' || status == 'provisioned' || (status == 'error' && error == 'provision')) {
|
|
options.source = 'install/fuel-agent';
|
|
} else if (status == 'deploying' || status == 'ready' || (status == 'error' && error == 'deploy')) {
|
|
options.source = 'install/puppet';
|
|
}
|
|
return '#cluster/' + this.props.node.get('cluster') + '/logs/' + utils.serializeTabOptions(options);
|
|
},
|
|
applyNewNodeName(newName) {
|
|
if (newName && newName != this.props.node.get('name')) {
|
|
this.setState({actionInProgress: true});
|
|
this.props.node.save({name: newName}, {patch: true, wait: true}).always(this.endRenaming);
|
|
} else {
|
|
this.endRenaming();
|
|
}
|
|
},
|
|
onNodeNameInputKeydown(e) {
|
|
if (e.key == 'Enter') {
|
|
this.applyNewNodeName(this.refs.name.getInputDOMNode().value);
|
|
} else if (e.key == 'Escape') {
|
|
this.endRenaming();
|
|
}
|
|
},
|
|
discardNodeDeletion(e) {
|
|
e.preventDefault();
|
|
if (this.state.actionInProgress) return;
|
|
this.setState({actionInProgress: true});
|
|
var node = new models.Node(this.props.node.attributes),
|
|
data = {pending_deletion: false};
|
|
node.save(data, {patch: true})
|
|
.done(() => {
|
|
this.props.cluster.fetchRelated('nodes').done(() => {
|
|
this.setState({actionInProgress: false});
|
|
});
|
|
})
|
|
.fail((response) => {
|
|
utils.showErrorDialog({
|
|
title: i18n('cluster_page.nodes_tab.node.cant_discard'),
|
|
response: response
|
|
});
|
|
});
|
|
},
|
|
removeNode(e) {
|
|
e.preventDefault();
|
|
if (this.props.viewMode == 'compact') this.toggleExtendedNodePanel();
|
|
dialogs.RemoveOfflineNodeDialog
|
|
.show()
|
|
.done(() => {
|
|
// sync('delete') is used instead of node.destroy() because we want
|
|
// to keep showing the 'Removing' status until the node is truly removed
|
|
// Otherwise this node would disappear and might reappear again upon
|
|
// cluster nodes refetch with status 'Removing' which would look ugly
|
|
// to the end user
|
|
return Backbone
|
|
.sync('delete', this.props.node)
|
|
.then(
|
|
(task) => {
|
|
dispatcher.trigger('networkConfigurationUpdated updateNodeStats updateNotifications labelsConfigurationUpdated');
|
|
if (task.status == 'ready') {
|
|
// Do not send the 'DELETE' request again, just get rid
|
|
// of this node.
|
|
this.props.node.trigger('destroy', this.props.node);
|
|
return;
|
|
}
|
|
if (this.props.cluster) {
|
|
this.props.cluster.get('tasks').add(new models.Task(task), {parse: true});
|
|
}
|
|
this.props.node.set('status', 'removing');
|
|
},
|
|
(response) => {
|
|
utils.showErrorDialog({response: response});
|
|
}
|
|
);
|
|
});
|
|
},
|
|
showNodeDetails(e) {
|
|
e.preventDefault();
|
|
if (this.state.extendedView) this.toggleExtendedNodePanel();
|
|
dialogs.ShowNodeInfoDialog.show({
|
|
node: this.props.node,
|
|
cluster: this.props.cluster,
|
|
nodeNetworkGroup: this.props.nodeNetworkGroups.get(this.props.node.get('group_id')),
|
|
renderActionButtons: this.props.renderActionButtons
|
|
});
|
|
},
|
|
toggleExtendedNodePanel() {
|
|
var states = this.state.extendedView ? {extendedView: false, isRenaming: false} : {extendedView: true};
|
|
this.setState(states);
|
|
},
|
|
renderNameControl() {
|
|
if (this.state.isRenaming) return (
|
|
<controls.Input
|
|
ref='name'
|
|
type='text'
|
|
name='node-name'
|
|
defaultValue={this.props.node.get('name')}
|
|
inputClassName='form-control node-name-input'
|
|
disabled={this.state.actionInProgress}
|
|
onKeyDown={this.onNodeNameInputKeydown}
|
|
maxLength='100'
|
|
selectOnFocus
|
|
autoFocus
|
|
/>
|
|
);
|
|
return (
|
|
<controls.Tooltip text={i18n('cluster_page.nodes_tab.node.edit_name')}>
|
|
<p onClick={!this.state.actionInProgress && this.startRenaming}>
|
|
{this.props.node.get('name') || this.props.node.get('mac')}
|
|
</p>
|
|
</controls.Tooltip>
|
|
);
|
|
},
|
|
renderStatusLabel(status) {
|
|
return (
|
|
<span>
|
|
{i18n('cluster_page.nodes_tab.node.status.' + status, {
|
|
os: this.props.cluster && this.props.cluster.get('release').get('operating_system') || 'OS'
|
|
})}
|
|
</span>
|
|
);
|
|
},
|
|
renderNodeProgress(status) {
|
|
var nodeProgress = this.props.node.get('progress');
|
|
return (
|
|
<div className='progress'>
|
|
{status &&
|
|
<div className='progress-bar-title'>
|
|
{this.renderStatusLabel(status)}
|
|
{': ' + nodeProgress + '%'}
|
|
</div>
|
|
}
|
|
<div className='progress-bar' role='progressbar' style={{width: _.max([nodeProgress, 3]) + '%'}}></div>
|
|
</div>
|
|
);
|
|
},
|
|
renderNodeHardwareSummary() {
|
|
var htCores = this.props.node.resource('ht_cores'),
|
|
hdd = this.props.node.resource('hdd'),
|
|
ram = this.props.node.resource('ram');
|
|
return (
|
|
<div className='node-hardware'>
|
|
<span>{i18n('node_details.cpu')}: {this.props.node.resource('cores') || '0'} ({_.isUndefined(htCores) ? '?' : htCores})</span>
|
|
<span>{i18n('node_details.hdd')}: {_.isUndefined(hdd) ? '?' + i18n('common.size.gb') : utils.showDiskSize(hdd)}</span>
|
|
<span>{i18n('node_details.ram')}: {_.isUndefined(ram) ? '?' + i18n('common.size.gb') : utils.showMemorySize(ram)}</span>
|
|
</div>
|
|
);
|
|
},
|
|
renderLogsLink(iconRepresentation) {
|
|
return (
|
|
<controls.Tooltip key='logs' text={iconRepresentation ? i18n('cluster_page.nodes_tab.node.view_logs') : null}>
|
|
<a className={'btn-view-logs ' + (iconRepresentation ? 'icon icon-logs' : 'btn')} href={this.getNodeLogsLink()}>
|
|
{!iconRepresentation && i18n('cluster_page.nodes_tab.node.view_logs')}
|
|
</a>
|
|
</controls.Tooltip>
|
|
);
|
|
},
|
|
renderNodeCheckbox() {
|
|
return (
|
|
<controls.Input
|
|
type='checkbox'
|
|
name={this.props.node.id}
|
|
checked={this.props.checked}
|
|
disabled={this.props.locked || !this.props.node.isSelectable() || this.props.mode == 'edit'}
|
|
onChange={this.props.mode != 'edit' ? this.props.onNodeSelection : _.noop}
|
|
wrapperClassName='pull-left'
|
|
/>
|
|
);
|
|
},
|
|
renderRemoveButton() {
|
|
return (
|
|
<button onClick={this.removeNode} className='btn node-remove-button'>
|
|
{i18n('cluster_page.nodes_tab.node.remove')}
|
|
</button>
|
|
);
|
|
},
|
|
renderRoleList(roles) {
|
|
return (
|
|
<ul>
|
|
{_.map(roles, function(role) {
|
|
return (
|
|
<li
|
|
key={this.props.node.id + role}
|
|
className={utils.classNames({'text-success': !this.props.node.get('roles').length})}
|
|
>
|
|
{role}
|
|
</li>
|
|
);
|
|
}, this)}
|
|
</ul>
|
|
);
|
|
},
|
|
showDeleteNodesDialog(e) {
|
|
e.preventDefault();
|
|
if (this.props.viewMode == 'compact') this.toggleExtendedNodePanel();
|
|
dialogs.DeleteNodesDialog
|
|
.show({
|
|
nodes: new models.Nodes(this.props.node),
|
|
cluster: this.props.cluster
|
|
})
|
|
.done(this.props.onNodeSelection);
|
|
},
|
|
renderLabels() {
|
|
var labels = this.props.node.get('labels');
|
|
if (_.isEmpty(labels)) return null;
|
|
return (
|
|
<ul>
|
|
{_.map(_.keys(labels).sort(_.partialRight(utils.natsort, {insensitive: true})), (key) => {
|
|
var value = labels[key];
|
|
return (
|
|
<li key={key + value} className='label'>
|
|
{key + (_.isNull(value) ? '' : ' "' + value + '"')}
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
);
|
|
},
|
|
toggleLabelsPopover(visible) {
|
|
this.setState({
|
|
labelsPopoverVisible: _.isBoolean(visible) ? visible : !this.state.labelsPopoverVisible
|
|
});
|
|
},
|
|
render() {
|
|
var ns = 'cluster_page.nodes_tab.node.',
|
|
node = this.props.node,
|
|
isSelectable = node.isSelectable() && !this.props.locked && this.props.mode != 'edit',
|
|
status = node.getStatusSummary(),
|
|
roles = this.props.cluster ? node.sortedRoles(this.props.cluster.get('roles').pluck('name')) : [];
|
|
|
|
// compose classes
|
|
var nodePanelClasses = {
|
|
node: true,
|
|
selected: this.props.checked,
|
|
'col-xs-12': this.props.viewMode != 'compact',
|
|
unavailable: !isSelectable
|
|
};
|
|
nodePanelClasses[status] = status;
|
|
|
|
var manufacturer = node.get('manufacturer') || '',
|
|
logoClasses = {
|
|
'manufacturer-logo': true
|
|
};
|
|
logoClasses[manufacturer.toLowerCase()] = manufacturer;
|
|
|
|
var statusClasses = {
|
|
'node-status': true
|
|
},
|
|
statusClass = {
|
|
pending_addition: 'text-success',
|
|
pending_deletion: 'text-warning',
|
|
error: 'text-danger',
|
|
ready: 'text-info',
|
|
provisioning: 'text-info',
|
|
deploying: 'text-success',
|
|
provisioned: 'text-info'
|
|
}[status];
|
|
statusClasses[statusClass] = true;
|
|
|
|
if (this.props.viewMode == 'compact') return (
|
|
<div className='compact-node'>
|
|
<div className={utils.classNames(nodePanelClasses)}>
|
|
<label className='node-box'>
|
|
<div
|
|
className='node-box-inner clearfix'
|
|
onClick={isSelectable && _.partial(this.props.onNodeSelection, null, !this.props.checked)}
|
|
>
|
|
<div className='node-checkbox'>
|
|
{this.props.checked && <i className='glyphicon glyphicon-ok' />}
|
|
</div>
|
|
<div className='node-name'>
|
|
<p>{node.get('name') || node.get('mac')}</p>
|
|
</div>
|
|
<div className={utils.classNames(statusClasses)}>
|
|
{_.contains(['provisioning', 'deploying'], status) ?
|
|
this.renderNodeProgress()
|
|
:
|
|
this.renderStatusLabel(status)
|
|
}
|
|
</div>
|
|
</div>
|
|
<div className='node-hardware'>
|
|
<p>
|
|
<span>
|
|
{node.resource('cores') || '0'} ({node.resource('ht_cores') || '?'})
|
|
</span> / <span>
|
|
{node.resource('hdd') ? utils.showDiskSize(node.resource('hdd')) : '?' + i18n('common.size.gb')}
|
|
</span> / <span>
|
|
{node.resource('ram') ? utils.showMemorySize(node.resource('ram')) : '?' + i18n('common.size.gb')}
|
|
</span>
|
|
</p>
|
|
<p className='btn btn-link' onClick={this.toggleExtendedNodePanel}>
|
|
{i18n(ns + 'more_info')}
|
|
</p>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
{this.state.extendedView &&
|
|
<controls.Popover className='node-popover' toggle={this.toggleExtendedNodePanel}>
|
|
<div>
|
|
<div className='node-name clearfix'>
|
|
{this.renderNodeCheckbox()}
|
|
<div className='name pull-left'>
|
|
{this.renderNameControl()}
|
|
</div>
|
|
</div>
|
|
<div className='node-stats'>
|
|
{!!roles.length &&
|
|
<div className='role-list'>
|
|
<i className='glyphicon glyphicon-pushpin' />
|
|
{this.renderRoleList(roles)}
|
|
</div>
|
|
}
|
|
{!_.isEmpty(this.props.node.get('labels')) &&
|
|
<div className='node-labels'>
|
|
<i className='glyphicon glyphicon-tags pull-left' />
|
|
{this.renderLabels()}
|
|
</div>
|
|
}
|
|
<div className={utils.classNames(statusClasses)}>
|
|
<i className='glyphicon glyphicon-time' />
|
|
{_.contains(['provisioning', 'deploying'], status) ?
|
|
<div>
|
|
{this.renderStatusLabel(status)}
|
|
<div className='node-buttons'>
|
|
{this.renderLogsLink()}
|
|
</div>
|
|
{this.renderNodeProgress(status)}
|
|
</div>
|
|
:
|
|
<div>
|
|
{this.renderStatusLabel(status)}
|
|
<div className='node-buttons'>
|
|
{status == 'offline' && this.renderRemoveButton()}
|
|
{[
|
|
!!node.get('cluster') && this.renderLogsLink(),
|
|
this.props.renderActionButtons && node.hasChanges() && !this.props.locked &&
|
|
<button
|
|
className='btn btn-discard'
|
|
key='btn-discard'
|
|
onClick={node.get('pending_deletion') ? this.discardNodeDeletion : this.showDeleteNodesDialog}
|
|
>
|
|
{i18n(ns + (node.get('pending_deletion') ? 'discard_deletion' : 'delete_node'))}
|
|
</button>
|
|
]}
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
<div className='hardware-info clearfix'>
|
|
<div className={utils.classNames(logoClasses)} />
|
|
{this.renderNodeHardwareSummary()}
|
|
</div>
|
|
<div className='node-popover-buttons'>
|
|
<button className='btn btn-default node-settings' onClick={this.showNodeDetails}>Details</button>
|
|
</div>
|
|
</div>
|
|
</controls.Popover>
|
|
}
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<div className={utils.classNames(nodePanelClasses)}>
|
|
<label className='node-box'>
|
|
{this.renderNodeCheckbox()}
|
|
<div className={utils.classNames(logoClasses)} />
|
|
<div className='node-name'>
|
|
<div className='name'>
|
|
{this.renderNameControl()}
|
|
</div>
|
|
<div className='role-list'>
|
|
{this.renderRoleList(roles)}
|
|
</div>
|
|
</div>
|
|
<div className='node-labels'>
|
|
{!_.isEmpty(node.get('labels')) &&
|
|
<button className='btn btn-link' onClick={this.toggleLabelsPopover}>
|
|
<i className='glyphicon glyphicon-tag-alt' />
|
|
{_.keys(node.get('labels')).length}
|
|
</button>
|
|
}
|
|
{this.state.labelsPopoverVisible &&
|
|
<controls.Popover className='node-labels-popover' toggle={this.toggleLabelsPopover}>
|
|
{this.renderLabels()}
|
|
</controls.Popover>
|
|
}
|
|
</div>
|
|
<div className='node-action'>
|
|
{[
|
|
!!node.get('cluster') && this.renderLogsLink(true),
|
|
this.props.renderActionButtons && node.hasChanges() && !this.props.locked &&
|
|
<controls.Tooltip
|
|
key={'pending_addition_' + node.id}
|
|
text={i18n(ns + (node.get('pending_deletion') ? 'discard_deletion' : 'delete_node'))}
|
|
>
|
|
<div
|
|
className='icon btn-discard'
|
|
onClick={node.get('pending_deletion') ? this.discardNodeDeletion : this.showDeleteNodesDialog}
|
|
/>
|
|
</controls.Tooltip>
|
|
]}
|
|
</div>
|
|
<div className={utils.classNames(statusClasses)}>
|
|
{_.contains(['provisioning', 'deploying'], status) ?
|
|
this.renderNodeProgress(status)
|
|
:
|
|
<div>
|
|
{this.renderStatusLabel(status)}
|
|
{status == 'offline' && this.renderRemoveButton()}
|
|
</div>
|
|
}
|
|
</div>
|
|
{this.renderNodeHardwareSummary()}
|
|
<div className='node-settings' onClick={this.showNodeDetails} />
|
|
</label>
|
|
</div>
|
|
);
|
|
}
|
|
});
|
|
|
|
return Node;
|
|
});
|