fuel-ui/static/views/cluster_page_tabs/nodes_tab_screens/node.js

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;
});