/*
* Copyright 2016 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.
**/
import _ from 'underscore';
import $ from 'jquery';
import i18n from 'i18n';
import React from 'react';
import ReactDOM from 'react-dom';
import utils from 'utils';
import {Table, Tooltip, Popover, MultiSelectControl, DownloadFileButton} from 'views/controls';
import {DeploymentTaskDetailsDialog, ShowNodeInfoDialog} from 'views/dialogs';
import {
DEPLOYMENT_HISTORY_VIEW_MODES, DEPLOYMENT_TASK_STATUSES, DEPLOYMENT_TASK_ATTRIBUTES
} from 'consts';
var ns = 'cluster_page.deployment_history.';
var {parseRFC2822Date, parseISO8601Date, formatTimestamp} = utils;
var DeploymentHistory = React.createClass({
propTypes: {
width: React.PropTypes.number.isRequired
},
getDefaultProps() {
return {
timelineIntervalWidth: 75,
timelineRowHeight: 28
};
},
getInitialState() {
var {deploymentHistory} = this.props;
return {
viewMode: 'timeline',
filters: [
{
name: 'task_name',
label: i18n(ns + 'filter_by_task_name'),
values: [],
options: _.map(_.uniq(deploymentHistory.map('task_name')).sort(),
(taskName) => ({name: taskName, title: taskName})
),
addOptionsFilter: true
}, {
name: 'node_id',
label: i18n(ns + 'filter_by_node'),
values: [],
options: _.map(_.uniq(deploymentHistory.map('node_id')),
(nodeId) => ({name: nodeId, title: renderNodeName.call(this, nodeId, false)})
),
addOptionsFilter: true
}, {
name: 'status',
label: i18n(ns + 'filter_by_status'),
values: [],
options: _.map(DEPLOYMENT_TASK_STATUSES,
(status) => ({
name: status,
title: i18n(
'cluster_page.deployment_history.task_statuses.' + status,
{defaultValue: status}
)
})
)
}
],
millisecondsPerPixel:
this.getTimelineMaxMillisecondsPerPixel(...this.getTimelineTimeInterval())
};
},
getCurrentTime() {
// we don't get milliseconds from server, so add 1 second so that tasks end time
// won't be greater than current time
return parseRFC2822Date(this.props.deploymentHistory.lastFetchDate) + 1000;
},
getTimelineTimeInterval() {
var {transaction, deploymentHistory} = this.props;
var timelineTimeStart, timelineTimeEnd;
timelineTimeStart = this.getCurrentTime();
if (transaction.match({status: 'running'})) timelineTimeEnd = timelineTimeStart;
deploymentHistory.each((task) => {
var taskTimeStart = task.get('time_start');
if (taskTimeStart) {
taskTimeStart = parseISO8601Date(taskTimeStart);
if (!timelineTimeStart || taskTimeStart < timelineTimeStart) {
timelineTimeStart = taskTimeStart;
}
if (!timelineTimeEnd) timelineTimeEnd = timelineTimeStart;
if (taskTimeStart > timelineTimeEnd) timelineTimeEnd = taskTimeStart;
var taskTimeEnd = task.get('time_end');
if (taskTimeEnd) {
taskTimeEnd = parseISO8601Date(taskTimeEnd);
if (taskTimeEnd > timelineTimeEnd) timelineTimeEnd = taskTimeEnd;
}
}
});
return [timelineTimeStart, timelineTimeEnd];
},
getTimelineMaxMillisecondsPerPixel(timelineTimeStart, timelineTimeEnd) {
return _.max([
(timelineTimeEnd - timelineTimeStart) / this.getNodeTimelineContainerWidth(),
1000 / this.props.timelineIntervalWidth
]);
},
getNodeTimelineContainerWidth() {
return Math.floor(this.props.width * 0.8);
},
zoomInTimeline() {
this.setState({millisecondsPerPixel: this.state.millisecondsPerPixel / 2});
},
zoomOutTimeline() {
this.setState({millisecondsPerPixel: this.state.millisecondsPerPixel * 2});
},
changeViewMode(viewMode) {
if (viewMode !== this.state.viewMode) this.setState({viewMode});
},
changeFilter(filterName, values) {
var {filters} = this.state;
_.find(filters, {name: filterName}).values = values;
this.setState({filters});
},
resetFilters() {
var {filters} = this.state;
_.each(filters, (filter) => {
filter.values = [];
});
this.setState({filters});
},
render() {
var {viewMode, millisecondsPerPixel} = this.state;
var {transaction, timelineIntervalWidth} = this.props;
var [timelineTimeStart, timelineTimeEnd] = this.getTimelineTimeInterval();
// interval should be equal at least 1 second
var canTimelineBeZoommedIn = millisecondsPerPixel / 2 >= 1000 / timelineIntervalWidth;
var canTimelineBeZoommedOut =
millisecondsPerPixel * 2 <=
this.getTimelineMaxMillisecondsPerPixel(timelineTimeStart, timelineTimeEnd);
return (
{viewMode === 'timeline' &&
}
{viewMode === 'table' &&
}
);
}
});
var DeploymentHistoryManagementPanel = React.createClass({
getInitialState() {
return {
areFiltersVisible: false,
openFilter: null
};
},
toggleFilters() {
this.setState({
areFiltersVisible: !this.state.areFiltersVisible,
openFilter: null
});
},
toggleFilter(filterName, visible) {
var {openFilter} = this.state;
var isFilterOpen = openFilter === filterName;
visible = _.isBoolean(visible) ? visible : !isFilterOpen;
this.setState({
openFilter: visible ? filterName : isFilterOpen ? null : openFilter
});
},
render() {
var {
deploymentHistory, transaction,
viewMode, changeViewMode,
zoomInTimeline, zoomOutTimeline,
filters, resetFilters, changeFilter
} = this.props;
var {areFiltersVisible, openFilter} = this.state;
var areFiltersApplied = _.some(filters, ({values}) => values.length);
return (
{viewMode === 'timeline' &&
}
{areFiltersVisible && (
{i18n(ns + 'filter_by')}
{areFiltersApplied &&
}
{_.map(filters,
(filter) =>
)}
)}
{!areFiltersVisible && areFiltersApplied &&
{i18n(ns + 'filter_by')}
{_.map(filters, ({name, label, values, options}) => {
if (!values.length) return null;
return
{label + ':'}
{_.map(values, (value) => _.find(options, {name: value}).title).join(', ')}
;
})}
}
);
}
});
var DeploymentHistoryTask = React.createClass({
getDefaultProps() {
return {
popoverMinPadding: 10
};
},
getInitialState() {
return {isPopoverVisible: false};
},
onMouseEnter(e) {
var {width, popoverMinPadding} = this.props;
var anchorPosition;
if (width < popoverMinPadding * 2) {
anchorPosition = Math.round(width / 2);
} else {
var {left} = $(ReactDOM.findDOMNode(this)).offset();
var {pageX} = e;
anchorPosition = pageX - left - 1;
if (anchorPosition < popoverMinPadding) {
anchorPosition = popoverMinPadding;
} else if (anchorPosition > (width - popoverMinPadding)) {
anchorPosition = width - popoverMinPadding;
}
}
this.setState({anchorPosition});
this.togglePopover(true);
},
onMouseLeave() {
this.togglePopover(false);
},
onClick() {
var {task, deploymentHistory} = this.props;
this.togglePopover(false);
DeploymentTaskDetailsDialog.show({
task,
deploymentHistory,
nodeName: renderNodeName.call(this, task.get('node_id'), false)
});
},
togglePopover(isPopoverVisible) {
this.setState({isPopoverVisible});
},
getColorFromString(str) {
var color = (utils.getStringHashCode(str) & 0x00FFFFFF).toString(16).toUpperCase();
return '#' + ('00000' + color).substr(-6);
},
render() {
var {task, top, left, width} = this.props;
var taskName = task.get('task_name');
var taskStatus = task.get('status');
return
{this.state.isPopoverVisible &&
{_.without(DEPLOYMENT_TASK_ATTRIBUTES, 'node_id')
.map((attr) => {
if (_.isNull(task.get(attr))) return null;
return (
{i18n('dialog.deployment_task_details.task.' + attr)}
{attr === 'time_start' || attr === 'time_end' ?
formatTimestamp(parseISO8601Date(task.get(attr)))
: attr === 'status' ?
i18n(
'cluster_page.deployment_history.task_statuses.' + taskStatus,
{defaultValue: taskStatus}
)
:
task.get(attr)
}
);
})
}
}
{taskStatus === 'error' &&
}
;
}
});
// Prefer to keep this as a function, not a component, since components
// don't allow to return plain text and I'd really prefer not to create extra
// useless spans
function renderNodeName(nodeId, isClickable = true) {
if (nodeId === 'master') {
return i18n(ns + 'master_node');
}
var node = this.props.nodes.get(nodeId);
if (!node) {
return i18n(ns + 'deleted_node', {id: nodeId});
}
if (isClickable) {
return (
);
}
return node.get('name');
}
var DeploymentHistoryTimeline = React.createClass({
renderIntervalLabel(index) {
var {timelineIntervalWidth, millisecondsPerPixel} = this.props;
var seconds = Math.floor(millisecondsPerPixel / 1000 * timelineIntervalWidth * (index + 1));
var minutes = seconds < 60 ? 0 : Math.floor(seconds / 60);
seconds = seconds - (minutes * 60);
var hours = minutes < 60 ? 0 : Math.floor(minutes / 60);
minutes = minutes - (hours * 60);
if (hours) return i18n(ns + 'hours', {hours, minutes});
if (minutes) {
return i18n(ns + (seconds ? 'minutes_and_seconds' : 'minutes'), {minutes, seconds});
}
return i18n(ns + 'seconds', {seconds});
},
getTimeIntervalWidth(timeStart, timeEnd) {
return Math.floor((timeEnd - timeStart) / this.props.millisecondsPerPixel);
},
adjustOffsets(e) {
this.refs.scale.style.left = -e.target.scrollLeft + 'px';
this.refs.names.style.top = -e.target.scrollTop + 'px';
},
componentWillUpdate() {
var {scrollLeft, scrollWidth, clientWidth} = this.refs.timelines;
if (scrollLeft === (scrollWidth - clientWidth)) {
this.scrollToRight = true;
}
},
componentDidUpdate() {
if (this.scrollToRight) {
this.refs.timelines.scrollLeft = this.refs.timelines.scrollWidth;
delete this.scrollToRight;
}
},
render() {
var {
nodes, deploymentHistory, timeStart, timeEnd, isRunning, filters,
nodeTimelineContainerWidth, width, timelineIntervalWidth, timelineRowHeight
} = this.props;
var appliedFilters = _.filter(filters, ({values}) => values.length);
var filteredNodes = (_.find(appliedFilters, {name: 'node_id'}) || {}).values || [];
var nodeIds = [];
var nodeOffsets = {};
deploymentHistory.each((task) => {
var nodeId = task.get('node_id');
if (
filteredNodes.length && !_.includes(filteredNodes, nodeId) ||
_.has(nodeOffsets, nodeId)
) return;
nodeOffsets[nodeId] = nodeIds.length;
nodeIds.push(nodeId);
});
var nodeTimelineWidth = _.max([
this.getTimeIntervalWidth(timeStart, timeEnd),
nodeTimelineContainerWidth
]);
var intervals = Math.floor(nodeTimelineWidth / timelineIntervalWidth);
return (
{_.map(nodeIds,
(nodeId) =>
{renderNodeName.call(this, nodeId)}
)}
{_.times(intervals, (n) =>
{this.renderIntervalLabel(n)}
)}
{deploymentHistory.map((task) => {
if (!_.includes(['ready', 'error', 'running'], task.get('status'))) return null;
if (
_.some(appliedFilters, ({name, values}) => !_.includes(values, task.get(name)))
) return null;
var taskTimeStart = task.get('time_start') ?
parseISO8601Date(task.get('time_start')) : 0;
var taskTimeEnd = task.get('time_end') ?
parseISO8601Date(task.get('time_end')) : timeEnd;
var width = this.getTimeIntervalWidth(taskTimeStart, taskTimeEnd);
if (!width) return null;
var top = timelineRowHeight * nodeOffsets[task.get('node_id')];
var left = this.getTimeIntervalWidth(timeStart, taskTimeStart);
return
;
})}
{isRunning &&
}
);
}
});
var DeploymentHistoryTable = React.createClass({
render() {
var {deploymentHistory, filters} = this.props;
var deploymentTasks = deploymentHistory.filter((task) =>
_.every(filters, ({name, values}) => !values.length || _.includes(values, task.get(name)))
);
return (
{deploymentTasks.length ?
({label: i18n(ns + attr + '_header')}))
.concat([{label: ''}])
}
body={_.map(deploymentTasks,
(task) => DEPLOYMENT_TASK_ATTRIBUTES
.map((attr) => {
var taskStatus = task.get('status');
if (attr === 'time_start' || attr === 'time_end') {
return task.get(attr) ? formatTimestamp(parseISO8601Date(task.get(attr))) : '-';
} else if (attr === 'node_id') {
return renderNodeName.call(this, task.get('node_id'));
} else if (attr === 'status') {
return (
{i18n(ns + 'task_statuses.' + taskStatus, {defaultValue: taskStatus})}
);
} else {
return task.get(attr);
}
})
.concat([
])
)}
/>
:
{i18n(ns + 'no_tasks_matched_filters')}
}
);
}
});
export default DeploymentHistory;