/* * 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 (
{_.map(DEPLOYMENT_HISTORY_VIEW_MODES, (mode) => { 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;