From b9849295cfff0444001b81993851fad4700007dc Mon Sep 17 00:00:00 2001 From: tpetr Date: Sun, 27 Mar 2016 22:00:42 -0400 Subject: [PATCH 01/44] initial work, almost there --- SingularityUI/app/actions/activeTasks.coffee | 15 ++ SingularityUI/app/actions/log.coffee | 212 ++++++++++++++++++ SingularityUI/app/assets/index.mustache | 1 + .../app/components/logs/ColorDropdown.cjsx | 37 +++ SingularityUI/app/components/logs/Header.cjsx | 82 +++++++ .../app/components/logs/LogContainer.cjsx | 41 ++++ .../app/components/logs/LogLine.cjsx | 74 ++++++ .../app/components/logs/LogLines.cjsx | 79 +++++++ .../app/components/logs/SearchDropdown.cjsx | 46 ++++ .../components/logs/TaskGroupContainer.cjsx | 52 +++++ .../app/components/logs/TaskGroupHeader.cjsx | 33 +++ .../app/components/logs/TasksDropdown.cjsx | 43 ++++ SingularityUI/app/controllers/LogViewer.cjsx | 54 +++++ SingularityUI/app/reducers/index.coffee | 4 + SingularityUI/app/reducers/log.coffee | 162 +++++++++++++ SingularityUI/app/router.coffee | 27 ++- SingularityUI/app/views/logView.cjsx | 21 ++ SingularityUI/gulpfile.js | 3 +- SingularityUI/package.json | 8 + SingularityUI/webpack.config.js | 29 ++- 20 files changed, 1014 insertions(+), 9 deletions(-) create mode 100644 SingularityUI/app/actions/activeTasks.coffee create mode 100644 SingularityUI/app/actions/log.coffee create mode 100644 SingularityUI/app/components/logs/ColorDropdown.cjsx create mode 100644 SingularityUI/app/components/logs/Header.cjsx create mode 100644 SingularityUI/app/components/logs/LogContainer.cjsx create mode 100644 SingularityUI/app/components/logs/LogLine.cjsx create mode 100644 SingularityUI/app/components/logs/LogLines.cjsx create mode 100644 SingularityUI/app/components/logs/SearchDropdown.cjsx create mode 100644 SingularityUI/app/components/logs/TaskGroupContainer.cjsx create mode 100644 SingularityUI/app/components/logs/TaskGroupHeader.cjsx create mode 100644 SingularityUI/app/components/logs/TasksDropdown.cjsx create mode 100644 SingularityUI/app/controllers/LogViewer.cjsx create mode 100644 SingularityUI/app/reducers/index.coffee create mode 100644 SingularityUI/app/reducers/log.coffee create mode 100644 SingularityUI/app/views/logView.cjsx diff --git a/SingularityUI/app/actions/activeTasks.coffee b/SingularityUI/app/actions/activeTasks.coffee new file mode 100644 index 0000000000..c12165dc28 --- /dev/null +++ b/SingularityUI/app/actions/activeTasks.coffee @@ -0,0 +1,15 @@ +Q = require 'q' + +fetchTasksForRequest = (requestId, state='active') -> + params = { + property: 'taskId' + } + $.ajax + url: "#{ config.apiRoot }/history/request/#{ requestId }/tasks/#{ state }?#{ $.param(params) }" + +updateActiveTasks = (requestId) -> + (dispatch) -> + fetchTasksForRequest(requestId).done (tasks) -> + dispatch({tasks, type: 'REQUEST_ACTIVE_TASKS'}) + +module.exports = { updateActiveTasks, fetchTasksForRequest } diff --git a/SingularityUI/app/actions/log.coffee b/SingularityUI/app/actions/log.coffee new file mode 100644 index 0000000000..7533b802bd --- /dev/null +++ b/SingularityUI/app/actions/log.coffee @@ -0,0 +1,212 @@ +Q = require 'q' +{ fetchTasksForRequest } = require './activeTasks' + +fetchData = (taskId, path, offset=undefined, length=0) -> + $.ajax + url: "#{ config.apiRoot }/sandbox/#{ taskId }/read?#{$.param({path, length, offset})}" + +initializeUsingActiveTasks = (requestId, path, search) -> + (dispatch) -> + deferred = Q.defer() + fetchTasksForRequest(requestId).done (tasks) -> + taskIds = _.sortBy(_.pluck(tasks, 'taskId'), (taskId) -> taskId.instanceNo).map((taskId) -> taskId.id) + dispatch(initialize(requestId, path, search, taskIds)).then -> + deferred.resolve() + deferred.promise + +initialize = (requestId, path, search, taskIds) -> + (dispatch, getState) -> + { viewMode } = getState() + + if viewMode is 'unified' + taskIdGroups = [taskIds] + else + taskIdGroups = taskIds.map (taskId) -> [taskId] + + dispatch(init(requestId, taskIdGroups, path, search)) + + groupPromises = taskIdGroups.map (taskIds, taskGroupId) -> + taskPromises = taskIds.map (taskId) -> + resolvedPath = path.replace('$TASK_ID', taskId) + fetchData(taskId, resolvedPath).done ({offset}) -> + dispatch(initTask(taskGroupId, taskId, offset, resolvedPath)) + + Promise.all(taskPromises).then -> + fetchPromises = dispatch(taskGroupFetchPrevious(taskGroupId)) + Promise.all(fetchPromises).then -> + dispatch(taskGroupReady(taskGroupId)) + + Promise.all(groupPromises) + +init = (requestId, taskIdGroups, path, search) -> + { + requestId + taskIdGroups + path + search + type: 'LOG_INIT' + } + +addTaskGroup = (path, search, taskIds) -> + { + path + taskIds + search + type: 'LOG_ADD_TASK_GROUP' + } + +initTask = (taskGroupId, taskId, offset, path) -> + { + taskId + taskGroupId + offset + path + type: 'LOG_TASK_INIT' + } + +taskGroupReady = (taskGroupId) -> + { + taskGroupId + type: 'LOG_TASK_GROUP_READY' + } + +updateFilesizes = -> + (dispatch, getState) -> + for taskId, {path} of getState().tasks + fetchData(taskId, path).done ({offset}) -> + dispatch(taskFilesize(taskId, offset)) + +updateGroups = -> + (dispatch, getState) -> + getState().taskGroups.map (taskGroup, taskGroupId) -> + unless taskGroup.pendingRequests + if taskGroup.top + dispatch(taskGroupFetchPrevious(taskGroupId)) + if taskGroup.bottom + dispatch(taskGroupFetchNext(taskGroupId)) + +taskGroupFetchNext = (taskGroupId) -> + (dispatch, getState) -> + {tasks, taskGroups, logRequestLength} = getState() + dispatch({taskGroupId, type: 'LOG_TASK_GROUP_REQUEST_START'}) + promises = taskGroups[taskGroupId].taskIds.map (taskId) -> + {maxOffset, path, initialDataLoaded} = tasks[taskId] + if initialDataLoaded + xhr = fetchData(taskId, path, maxOffset, maxOffset + logRequestLength) + xhr.done ({data, offset, nextOffset}) -> + if data.length > 0 + nextOffset = nextOffset || offset + data.length + dispatch(taskData(taskGroupId, taskId, data, offset, nextOffset, true)) + else + Promise.resolve() # reject("initialDataLoaded is false for task #{taskId}") + Promise.all(promises).then -> + dispatch({taskGroupId, type: 'LOG_TASK_GROUP_REQUEST_END'}) + +taskGroupFetchPrevious = (taskGroupId) -> + (dispatch, getState) -> + {tasks, taskGroups, logRequestLength} = getState() + taskGroups[taskGroupId].taskIds.map (taskId) -> + {minOffset, path, initialDataLoaded} = tasks[taskId] + if minOffset > 0 and initialDataLoaded + xhr = fetchData(taskId, path, Math.max(minOffset - logRequestLength, 0), Math.min(logRequestLength, minOffset - logRequestLength)) + xhr.done ({data, offset, nextOffset}) -> + if data.length > 0 + nextOffset = nextOffset || offset + data.length + dispatch(taskData(taskGroupId, taskId, data, offset, nextOffset, false)) + else + Promise.resolve() # reject("initialDataLoaded is false for task #{taskId}") + +taskData = (taskGroupId, taskId, data, offset, nextOffset, append) -> + { + taskGroupId + taskId + data + offset + nextOffset + append + type: 'LOG_TASK_DATA' + } + +taskFilesize = (taskId, filesize) -> + { + taskId + filesize + type: 'LOG_TASK_FILESIZE' + } + +taskGroupTop = (taskGroupId, visible) -> + { + taskGroupId + visible + type: 'LOG_TASK_GROUP_TOP' + } + +taskGroupBottom = (taskGroupId, visible) -> + { + taskGroupId + visible + type: 'LOG_TASK_GROUP_BOTTOM' + } + +clickPermalink = (offset) -> + { + offset + type: 'LOG_CLICK_OFFSET_LINK' + } + +selectLogColor = (color) -> + { + color + type: 'LOG_SELECT_COLOR' + } + +switchViewMode = (newViewMode) -> + (dispatch, getState) -> + { taskGroups, path, requestId, search, viewMode } = getState() + + if newViewMode in ['custom', viewMode] + return + + taskIds = _.flatten(_.pluck(taskGroups, 'taskIds')) + + dispatch({viewMode: newViewMode, type: 'LOG_SWITCH_VIEW_MODE'}) + + initialize(requestId, path, search, newViewMode, taskIds)(dispatch) + +setCurrentSearch = (newSearch) -> + (dispatch, getState) -> + {requestId, path, taskGroups, currentSearch} = getState() + if newSearch != currentSearch + initialize(requestId, path, newSearch, _.pluck(taskGroups, 'taskIds'))(dispatch) + +toggleTaskLog = (taskId) -> + (dispatch, getState) -> + {path, taskGroups, tasks, viewMode} = getState() + if tasks[taskId] + dispatch({taskId, type: 'LOG_REMOVE_TASK'}) + else + if viewMode is 'unified' + taskGroupId = 0 + else + taskGroupId = dispatch(addTaskGroup(path, '', [taskId])) + resolvedPath = path.replace('$TASK_ID', taskId) + fetchData(taskId, resolvedPath).done ({offset}) -> + dispatch(initTask(taskGroupId, taskId, offset, resolvedPath)) + dispatch(taskGroupFetchPrevious(taskGroupId)).then -> + dispatch(taskGroupReady(taskGroupId)) + +module.exports = { + initialize + initializeUsingActiveTasks + taskGroupFetchNext + taskGroupFetchPrevious + clickPermalink + updateGroups + updateFilesizes + taskGroupTop + taskGroupBottom + selectLogColor + switchViewMode + setCurrentSearch + toggleTaskLog +} diff --git a/SingularityUI/app/assets/index.mustache b/SingularityUI/app/assets/index.mustache index bfdbd5ca45..87c9597660 100644 --- a/SingularityUI/app/assets/index.mustache +++ b/SingularityUI/app/assets/index.mustache @@ -43,6 +43,7 @@ shellCommands: {{{shellCommands}}}, }; + {{#navColor}} diff --git a/SingularityUI/app/components/logs/ColorDropdown.cjsx b/SingularityUI/app/components/logs/ColorDropdown.cjsx new file mode 100644 index 0000000000..2b80838b54 --- /dev/null +++ b/SingularityUI/app/components/logs/ColorDropdown.cjsx @@ -0,0 +1,37 @@ +React = require 'react' +classNames = require 'classnames' + +{ connect } = require 'react-redux' +{ selectLogColor } = require '../../actions/log' + +class ColorDropdown extends React.Component + renderColorChoices: -> + activeColor = @props.activeColor + + @props.colors.map (color, index) => + colorClass = color.toLowerCase().replace(' ', '-') + className = classNames + active: @props.activeColor is colorClass +
  • + @props.setLogColor(colorClass)}> + {color} + +
  • + + render: -> + console.log @props +
    + + +
    + +mapStateToProps = (state, ownProps) -> ownProps + +mapDispatchToProps = (dispatch) -> + setLogColor: (color) -> dispatch(selectLogColor(color)) + +module.exports = connect(mapStateToProps, mapDispatchToProps)(ColorDropdown) \ No newline at end of file diff --git a/SingularityUI/app/components/logs/Header.cjsx b/SingularityUI/app/components/logs/Header.cjsx new file mode 100644 index 0000000000..ebf77ddebe --- /dev/null +++ b/SingularityUI/app/components/logs/Header.cjsx @@ -0,0 +1,82 @@ +React = require 'react' +ColorDropdown = require './ColorDropdown' +SearchDropdown = require './SearchDropdown' +TasksDropdown = require './TasksDropdown' + +{ connect } = require 'react-redux' +{ switchViewMode } = require '../../actions/log' + +class Header extends React.Component + @propTypes: + requestId: React.PropTypes.string.isRequired + path: React.PropTypes.string.isRequired + taskIdCount: React.PropTypes.number.isRequired + + toggleHelp: -> + # TODO + + renderBreadcrumbs: -> + @props.path.split('/').map (subpath, i) -> +
  • {subpath}
  • + + renderViewButtons: -> + if @props.taskIdCount > 1 +
    + + +
    + + renderAnchorButtons: -> + + + + + + + + + + renderHelpButton: -> + + + + + render: -> +
    +
    +
    + +
    +
    +
      + {@renderBreadcrumbs()} +
    +
    +
    + + + + {@renderViewButtons()} + {@renderAnchorButtons()} + {@renderHelpButton()} +
    +
    +
    + +mapStateToProps = (state) -> + viewMode: state.viewMode + requestId: state.activeRequest.requestId + +mapDispatchToProps = (dispatch) -> + switchViewMode: (viewMode) -> dispatch(switchViewMode(viewMode)) + scrollToBottom: -> dispatch() + scrollToTop: -> dispatch() + +module.exports = connect(mapStateToProps, mapDispatchToProps)(Header) diff --git a/SingularityUI/app/components/logs/LogContainer.cjsx b/SingularityUI/app/components/logs/LogContainer.cjsx new file mode 100644 index 0000000000..a77d3b959f --- /dev/null +++ b/SingularityUI/app/components/logs/LogContainer.cjsx @@ -0,0 +1,41 @@ +React = require 'react' +Header = require './Header' +TaskGroupContainer = require './TaskGroupContainer' +BackboneReactComponent = require 'backbone-react-component' +MergedLogLines = require '../../collections/MergedLogLines' + +{ connect } = require 'react-redux' + +class LogContainer extends React.Component + mixins: [Backbone.React.Component.mixin] + + @propTypes: + taskGroups: React.PropTypes.array.isRequired + tasks: React.PropTypes.object.isRequired + path: React.PropTypes.string.isRequired + + renderTaskIdGroups: -> + componentProps = @props + @props.taskGroups.map (taskGroup, i) -> + + + render: -> +
    +
    +
    + {@renderTaskIdGroups()} +
    +
    + +mapStateToProps = (state, ownProps) -> + colors: state.colors + activeColor: state.activeColor + requestId: state.requestId + taskGroups: state.taskGroups + tasks: state.tasks + path: state.path + +module.exports = connect(mapStateToProps)(LogContainer) diff --git a/SingularityUI/app/components/logs/LogLine.cjsx b/SingularityUI/app/components/logs/LogLine.cjsx new file mode 100644 index 0000000000..6406b20d30 --- /dev/null +++ b/SingularityUI/app/components/logs/LogLine.cjsx @@ -0,0 +1,74 @@ +React = require 'react' +classNames = require 'classnames' + +class LogLine extends React.Component + @propTypes: + offset: React.PropTypes.number.isRequired + isHighlighted: React.PropTypes.bool.isRequired + content: React.PropTypes.string.isRequired + onPermalinkClick: React.PropTypes.func.isRequired + permalinkEnabled: React.PropTypes.bool.isRequired + search: React.PropTypes.string + + shouldComponentUpdate: (nextProps) -> + (@props.offset isnt nextProps.offset) or + (@props.isHighlighted isnt nextProps.isHighlighted) or + (@props.search isnt nextProps.search) + + highlightContent: (content) -> + search = @props.search + if not search or _.isEmpty(search) + return content + + regex = RegExp(search, 'g') + matches = [] + + while m = regex.exec(content) + matches.push(m) + + sections = [] + lastEnd = 0 + for m in matches + last = + text: content.slice(lastEnd, m.index) + match: false + sect = + text: content.slice(m.index, m.index + m[0].length) + match: true + sections.push last, sect + lastEnd = m.index + m[0].length + sections.push + text: content.slice(lastEnd) + match: false + + sections.map (s, i) => + spanClass = classNames + 'search-match': s.match + {s.text} + + handlePermalinkClick: (e) -> + e.preventDefault() + @props.onPermalinkClick(@props.offset) + + renderPermalink: -> + if @props.permalinkEnabled + +
    + +
    +
    + + render: -> + divClass = classNames + line: true + highlightLine: @props.isHighlighted + +
    + {@renderPermalink()} + + {@props.offset} | + {@highlightContent(@props.content)} + +
    + +module.exports = LogLine diff --git a/SingularityUI/app/components/logs/LogLines.cjsx b/SingularityUI/app/components/logs/LogLines.cjsx new file mode 100644 index 0000000000..5369b89bf9 --- /dev/null +++ b/SingularityUI/app/components/logs/LogLines.cjsx @@ -0,0 +1,79 @@ +React = require 'react' +ReactDOM = require 'react-dom' +Waypoint = require 'react-waypoint' +LogLine = require './LogLine' +Humanize = require 'humanize' +LogLines = require '../../collections/LogLines' + +Utils = require '../../utils' + +class LogLines extends React.Component + @propTypes: + onEnterTop: React.PropTypes.func.isRequired + onEnterBottom: React.PropTypes.func.isRequired + onLeaveTop: React.PropTypes.func.isRequired + onLeaveBottom: React.PropTypes.func.isRequired + onPermalinkClick: React.PropTypes.func.isRequired + + taskGroupId: React.PropTypes.number.isRequired + logLines: React.PropTypes.array.isRequired + search: React.PropTypes.string.isRequired + + initialDataLoaded: React.PropTypes.bool.isRequired + reachedStartOfFile: React.PropTypes.bool.isRequired + reachedEndOfFile: React.PropTypes.bool.isRequired + bytesRemainingBefore: React.PropTypes.number.isRequired + bytesRemainingAfter: React.PropTypes.number.isRequired + permalinkEnabled: React.PropTypes.bool.isRequired + activeColor: React.PropTypes.string.isRequired + + componentWillUpdate: -> + @shouldScrollToBottom = @refs.tailContents.scrollTop + @refs.tailContents.offsetHeight is @refs.tailContents.scrollHeight + + componentDidUpdate: -> + if @shouldScrollToBottom + @refs.tailContents.scrollTop = @refs.tailContents.scrollHeight + + renderLoadingPrevious: -> + if @props.initialDataLoaded + if @props.reachedStartOfFile +
    At beginning of file
    + else +
    Loading previous... ({Humanize.filesize(@props.bytesRemainingBefore)} remaining)
    + + renderLogLines: -> + @props.logLines.map ({data, offset, taskId}) => + + + renderLoadingMore: -> + if @props.initialDataLoaded + if @props.reachedEndOfFile +
    Tailing...
    + else +
    Loading more... ({Humanize.filesize(@props.bytesRemainingAfter)} remaining)
    + + handleEnterTop: => @props.onEnterTop(@props.taskGroupId) + handleEnterBottom: => @props.onEnterBottom(@props.taskGroupId) + handleLeaveTop: => @props.onLeaveTop(@props.taskGroupId) + handleLeaveBottom: => @props.onLeaveBottom(@props.taskGroupId) + + render: -> +
    +
    + {@renderLoadingPrevious()} + + {@renderLogLines()} + + {@renderLoadingMore()} +
    +
    + +module.exports = LogLines diff --git a/SingularityUI/app/components/logs/SearchDropdown.cjsx b/SingularityUI/app/components/logs/SearchDropdown.cjsx new file mode 100644 index 0000000000..7ca92e4a01 --- /dev/null +++ b/SingularityUI/app/components/logs/SearchDropdown.cjsx @@ -0,0 +1,46 @@ +React = require 'react' + +{ connect } = require 'react-redux' +{ setCurrentSearch } = require '../../actions/log' + +class SearchDropdown extends React.Component + @propTypes: + currentSearch: React.PropTypes.string.isRequired + + constructor: (props) -> + super(props) + @state = { + searchValue: 'xxx' + } + + handleSearchToggle: -> + # TODO + + handleSearchUpdate: => + console.log @props.setCurrentSearch + @props.setCurrentSearch(@state.searchValue) + + render: -> +
    + +
      +
    • +
      + @setState({searchValue: e.target.value})} /> + + + +
      +
    • +
    +
    + +mapStateToProps = (state) -> + currentSearch: state.currentSearch + +mapDispatchToProps = (dispatch) -> + setCurrentSearch: (value) -> dispatch(setCurrentSearch(value)) + +module.exports = connect(mapStateToProps, mapDispatchToProps)(SearchDropdown) \ No newline at end of file diff --git a/SingularityUI/app/components/logs/TaskGroupContainer.cjsx b/SingularityUI/app/components/logs/TaskGroupContainer.cjsx new file mode 100644 index 0000000000..0a82167d42 --- /dev/null +++ b/SingularityUI/app/components/logs/TaskGroupContainer.cjsx @@ -0,0 +1,52 @@ +React = require 'react' +TaskGroupHeader = require './TaskGroupHeader' +LogLines = require './LogLines' +classNames = require 'classnames' + +{ connect } = require 'react-redux' +{ taskGroupTop, taskGroupBottom, clickPermalink } = require '../../actions/log' + +sum = (numbers) -> + total = 0 + for n in numbers + total += n + total + +class TaskGroupContainer extends React.Component + @propTypes: + taskGroupId: React.PropTypes.number.isRequired + path: React.PropTypes.string.isRequired + + render: -> + className = "col-md-#{ 12 / @props.taskGroupCount } tail-column" +
    + + +
    + + +mapStateToProps = (state, ownProps) -> + taskGroup = state.taskGroups[ownProps.taskGroupId] + tasks = taskGroup.taskIds.map (taskId) -> ownProps.tasks[taskId] + + taskIds: taskGroup.taskIds + logLines: taskGroup.logLines + search: taskGroup.search + ready: taskGroup.ready + taskGroupCount: state.taskGroups.length + initialDataLoaded: _.all(_.pluck(tasks, 'initialDataLoaded')) + reachedStartOfFile: _.all(tasks.map (task) -> task.minOffset is 0) + reachedEndOfFile: _.all(tasks.map (task) -> task.maxOffset >= task.filesize) + bytesRemainingBefore: sum(_.pluck(tasks, 'minOffset')) + bytesRemainingAfter: sum(tasks.map (task) -> Math.max(task.filesize - task.maxOffset, 0)) + permalinkEnabled: tasks.length is 1 + activeColor: state.activeColor + +mapDispatchToProps = (dispatch) -> + onEnterTop: (taskGroupId) -> dispatch(taskGroupTop(taskGroupId, true)) + onEnterBottom: (taskGroupId) -> dispatch(taskGroupBottom(taskGroupId, true)) + onLeaveTop: (taskGroupId) -> dispatch(taskGroupTop(taskGroupId, false)) + onLeaveBottom: (taskGroupId) -> dispatch(taskGroupBottom(taskGroupId, false)) + onPermalinkClick: (offset) -> dispatch(clickPermalink(offset)) + +module.exports = connect(mapStateToProps, mapDispatchToProps)(TaskGroupContainer) \ No newline at end of file diff --git a/SingularityUI/app/components/logs/TaskGroupHeader.cjsx b/SingularityUI/app/components/logs/TaskGroupHeader.cjsx new file mode 100644 index 0000000000..a22a0bf662 --- /dev/null +++ b/SingularityUI/app/components/logs/TaskGroupHeader.cjsx @@ -0,0 +1,33 @@ +React = require 'react' + +class TaskGroupHeader extends React.Component + @propTypes: + taskIds: React.PropTypes.array.isRequired + + toggleLegend: -> + # TODO + + getInstanceNumberFromTaskId: (taskId) -> + splits = taskId.split('-') + splits[splits.length - 3] + + renderInstanceInfo: -> + if @props.taskIds.length > 1 + Viewing Instances {@props.taskIds.map(@getInstanceNumberFromTaskId).join(', ')} + else + Instance {@getInstanceNumberFromTaskId(@props.taskIds[0])} + + renderTaskLegend: -> + if @props.taskIds.length > 1 + + + + + render: -> +
    + {@renderInstanceInfo()} + {@renderTaskLegend()} +
    + # TODO: renderLegend() + +module.exports = TaskGroupHeader \ No newline at end of file diff --git a/SingularityUI/app/components/logs/TasksDropdown.cjsx b/SingularityUI/app/components/logs/TasksDropdown.cjsx new file mode 100644 index 0000000000..e426c3272d --- /dev/null +++ b/SingularityUI/app/components/logs/TasksDropdown.cjsx @@ -0,0 +1,43 @@ +React = require 'react' +{ toggleTaskLog } = require '../../actions/log' + +{ connect } = require 'react-redux' + +class TasksDropdown extends React.Component + handleTasksKeyDown: -> + # TODO + + renderListItems: -> + if @props.activeTasks and @props.taskIds + tasks = _.sortBy(@props.activeTasks, (t) => t.taskId.instanceNo).map (task, i) => +
  • + @props.onToggleViewingInstance(task.taskId.id)}> + + Instance {task.taskId.instanceNo} + +
  • + if tasks.length > 0 + return tasks + else + return
  • No running instances
  • + else +
  • Loading active tasks...
  • + + render: -> +
    + +
      + {@renderListItems()} +
    +
    + +mapStateToProps = (state) -> + activeTasks: state.activeRequest.activeTasks + taskIds: state.tasks + +mapDispatchToProps = (dispatch) -> + onToggleViewingInstance: (taskId) -> dispatch(toggleTaskLog(taskId)) + +module.exports = connect(mapStateToProps, mapDispatchToProps)(TasksDropdown) \ No newline at end of file diff --git a/SingularityUI/app/controllers/LogViewer.cjsx b/SingularityUI/app/controllers/LogViewer.cjsx new file mode 100644 index 0000000000..69141d32e9 --- /dev/null +++ b/SingularityUI/app/controllers/LogViewer.cjsx @@ -0,0 +1,54 @@ +Controller = require './Controller' + +LogLines = require '../collections/LogLines' +MergedLogLines = require '../collections/MergedLogLines' + +LogView = require '../views/logView' + +Redux = require 'redux' +thunk = require 'redux-thunk' +logger = require 'redux-logger' +rootReducer = require '../reducers' +LogActions = require '../actions/log' +ActiveTasks = require '../actions/activeTasks' + +class LogViewer extends Controller + initialize: ({@requestId, @path, @initialOffset, taskIds, viewMode, search}) -> + window.lv = @ + @title 'Tail of ' + @path + + initialState = { + viewMode, + colors: ['Default', 'Light', 'Dark'], + logRequestLength: 30000, + activeRequest: { + @requestId + } + } + + @store = Redux.createStore(rootReducer, initialState, Redux.compose(Redux.applyMiddleware(thunk.default, logger()))) + + if taskIds + initPromise = @store.dispatch(LogActions.initialize(@requestId, @path, search, taskIds)) + else + initPromise = @store.dispatch(LogActions.initializeUsingActiveTasks(@requestId, @path, search)) + + initPromise.then => + @store.dispatch(ActiveTasks.updateActiveTasks(@requestId)) + + setInterval @update, 1000 + setInterval @updateFilesizes, 10000 + + # create log view + @view = new LogView @store + + @setView @view + + @view.render() + app.showView @view + + update: => @store.dispatch(LogActions.updateGroups()) + + updateFilesizes: => @store.dispatch(LogActions.updateFilesizes()) + +module.exports = LogViewer diff --git a/SingularityUI/app/reducers/index.coffee b/SingularityUI/app/reducers/index.coffee new file mode 100644 index 0000000000..ee8476690d --- /dev/null +++ b/SingularityUI/app/reducers/index.coffee @@ -0,0 +1,4 @@ +{ combineReducers } = require 'redux' +log = require './log' + +module.exports = log \ No newline at end of file diff --git a/SingularityUI/app/reducers/log.coffee b/SingularityUI/app/reducers/log.coffee new file mode 100644 index 0000000000..22cb708e33 --- /dev/null +++ b/SingularityUI/app/reducers/log.coffee @@ -0,0 +1,162 @@ +{ combineReducers } = require 'redux' + +updateTask = (state, taskId, update) -> + newState = Object.assign({}, state) + newState[taskId] = Object.assign({}, state[taskId], update) + return newState + +updateTaskGroup = (state, taskGroupId, update) -> + newState = Object.assign([], state) + newState[taskGroupId] = Object.assign({}, state[taskGroupId], update) + return newState + +filterLogLines = (lines, search) -> + _.filter lines, ({data}) -> + new RegExp(search).test(data) + +tasks = (state={}, action) -> + if action.type is 'LOG_INIT' + newState = {} + for taskId in _.flatten(action.taskIdGroups) + newState[taskId] = { + minOffset: 0 + maxOffset: 0 + filesize: 0 + initialDataLoaded: false + } + return newState + else if action.type is 'LOG_ADD_TASK_GROUP' + newState = Object.assign({}, state) + for taskId in action.taskIds + newState[taskId] = { + minOffset: 0 + maxOffset: 0 + filesize: 0 + initialDataLoaded: false + } + return newState + else if action.type is 'LOG_REMOVE_TASK' + newState = Object.assign({}, state) + delete newState[action.taskId] + return newState + else if action.type is 'LOG_TASK_INIT' + return updateTask(state, action.taskId, { + path: action.path + minOffset: action.offset + maxOffset: action.offset + filesize: action.offset + initialDataLoaded: true + }) + else if action.type is 'LOG_TASK_DATA' + return updateTask(state, action.taskId, { + minOffset: Math.min(state[action.taskId].minOffset, action.offset) + maxOffset: Math.max(state[action.taskId].maxOffset, action.nextOffset) + filesize: Math.max(state[action.taskId].filesize, action.nextOffset) + }) + else if action.type is 'LOG_TASK_FILESIZE' + return updateTask(state, action.taskId, {filesize: action.filesize}) + return state + +taskGroups = (state=[], action) -> + if action.type is 'LOG_INIT' + return action.taskIdGroups.map (taskIds) -> { + taskIds, + logLines: [], + top: false, + bottom: false, + search: action.search, + ready: false + pendingRequests: false + } + else if action.type is 'LOG_ADD_TASK_GROUP' + newState = Object.assign([], state) + newState.push({ + taskIds: action.taskIds, + logLines: [], + top: false, + bottom: false, + search: action.search, + ready: false + pendingRequests: false + }) + return newState + else if action.type is 'LOG_REMOVE_TASK' + newState = [] + for taskGroup in state + if action.taskId in taskGroup.taskIds + if taskGroup.taskIds.length is 1 + continue + else + newTaskGroup = Object.assign({}, taskGroup) + newTaskGroup.taskIds = taskGroup.filter (taskId) -> taskId isnt action.taskId + newState.push(newTaskGroup) + else + newState.push(taskGroup) + return newState + else if action.type is 'LOG_TASK_GROUP_REQUEST_START' + return updateTaskGroup(state, action.taskGroupId, {pendingRequests: true}) + else if action.type is 'LOG_TASK_GROUP_REQUEST_END' + return updateTaskGroup(state, action.taskGroupId, {pendingRequests: false}) + else if action.type is 'LOG_TASK_GROUP_TOP' + return updateTaskGroup(state, action.taskGroupId, {top: action.visible}) + else if action.type is 'LOG_TASK_GROUP_BOTTOM' + return updateTaskGroup(state, action.taskGroupId, {bottom: action.visible}) + else if action.type is 'LOG_TASK_GROUP_READY' + return updateTaskGroup(state, action.taskGroupId, {ready: true}) + else if action.type is 'LOG_TASK_DATA' + taskGroup = state[action.taskGroupId] + + offset = action.offset + lines = _.initial(action.data.match /[^\n]*(\n|$)/g).map (data) -> + offset += data.length + {data, offset: offset - data.length} + + if taskGroup.search + lines = filterLogLines(lines, taskGroup.search) + + if action.append + logLines = state[action.taskGroupId].logLines.concat(lines) + else + logLines = lines.concat(state[action.taskGroupId].logLines) + + return updateTaskGroup(state, action.taskGroupId, {logLines}) + + return newState + return state + +path = (state='', action) -> + if action.type is 'LOG_INIT' + return action.path + return state + +activeColor = (state='default', action) -> + if action.type is 'LOG_INIT' + return window.localStorage.logColor || 'default' + else if action.type is 'LOG_SELECT_COLOR' + window.localStorage.logColor = action.color + return action.color + return state + +colors = (state=[]) -> state + +viewMode = (state='custom', action) -> + if action.type is 'LOG_SWITCH_VIEW_MODE' + return action.viewMode + return state + +currentSearch = (state='', action) -> + if action.type is 'LOG_SET_SEARCH' + return action.currentSearch + return state + +logRequestLength = (state=30000, action) -> + return state + +activeRequest = (state={}, action) -> + if action.type is 'LOG_INIT' + return {requestId: action.requestId} + if action.type is 'REQUEST_ACTIVE_TASKS' + return Object.assign({}, state, {activeTasks: action.tasks}) + return state + +module.exports = combineReducers({tasks, taskGroups, activeRequest, path, activeColor, colors, viewMode, currentSearch, logRequestLength}) \ No newline at end of file diff --git a/SingularityUI/app/router.coffee b/SingularityUI/app/router.coffee index b9016f84df..ca5bb7a6b2 100644 --- a/SingularityUI/app/router.coffee +++ b/SingularityUI/app/router.coffee @@ -11,7 +11,6 @@ RequestsTableController = require 'controllers/RequestsTable' TasksTableController = require 'controllers/TasksTable' TaskDetailController = require 'controllers/TaskDetail' -TailController = require 'controllers/Tail' RacksController = require 'controllers/Racks' SlavesController = require 'controllers/Slaves' @@ -20,7 +19,9 @@ NotFoundController = require 'controllers/NotFound' DeployDetailController = require 'controllers/DeployDetail' -AggregateTailController = require 'controllers/AggregateTail' +LogViewerController = require 'controllers/LogViewer' + +Utils = require './utils' class Router extends Backbone.Router @@ -90,8 +91,16 @@ class Router extends Backbone.Router app.bootstrapController new TaskDetailController {taskId, filePath} tail: (taskId, path = '') -> - offset = parseInt(window.location.hash.substr(1), 10) || null - app.bootstrapController new TailController {taskId, path, offset} + initialOffset = parseInt(window.location.hash.substr(1), 10) || null + splits = taskId.split('-') + requestId = splits.slice(0, splits.length - 5).join('-') + params = Utils.getQueryParams() + + search = params.search || '' + + path = path.replace(taskId, '$TASK_ID') + + app.bootstrapController new LogViewerController {requestId, path, initialOffset, taskIds: [taskId], search, viewMode: 'split'} racks: (state = 'all') -> app.bootstrapController new RacksController {state} @@ -106,7 +115,13 @@ class Router extends Backbone.Router app.bootstrapController new DeployDetailController {requestId, deployId} aggregateTail: (requestId, path = '') -> - offset = parseInt(window.location.hash.substr(1), 10) || null - app.bootstrapController new AggregateTailController {requestId, path, offset} + initialOffset = parseInt(window.location.hash.substr(1), 10) || null + + params = Utils.getQueryParams() + taskIds = (params.taskIds || '').split(',') + viewMode = params.viewMode || 'split' + search = params.search || '' + + app.bootstrapController new LogViewerController {requestId, path, initialOffset, taskIds, viewMode, search} module.exports = Router diff --git a/SingularityUI/app/views/logView.cjsx b/SingularityUI/app/views/logView.cjsx new file mode 100644 index 0000000000..699fd60a47 --- /dev/null +++ b/SingularityUI/app/views/logView.cjsx @@ -0,0 +1,21 @@ +View = require './view' +React = require 'react' +ReactDOM = require 'react-dom' +LogContainer = require '../components/logs/LogContainer' +{ Provider } = require 'react-redux' + +class LogView extends View + initialize: (store) -> + window.addEventListener 'viewChange', @handleViewChange + @component = + + handleViewChange: => + unmounted = ReactDOM.unmountComponentAtNode @el + if unmounted + window.removeEventListener 'viewChange', @handleViewChange + + render: -> + $(@el).addClass 'tail-root' + ReactDOM.render @component, @el + +module.exports = LogView diff --git a/SingularityUI/gulpfile.js b/SingularityUI/gulpfile.js index 16d85d186a..9a0c690bfd 100644 --- a/SingularityUI/gulpfile.js +++ b/SingularityUI/gulpfile.js @@ -9,6 +9,7 @@ var stylus = require('gulp-stylus'); var nib = require('nib'); var concat = require('gulp-concat'); +var merge = require('webpack-merge'); var serverBase = process.env.SINGULARITY_BASE_URI || '/singularity' @@ -103,7 +104,7 @@ gulp.task('build', ['clean'], function () { gulp.task('serve', ['html', 'styles', 'fonts', 'images', 'css-images'], function () { gulp.watch('app/**/*.styl', ['styles']) - new WebpackDevServer(require('webpack')(webpackConfig), { + new WebpackDevServer(require('webpack')(merge(webpackConfig, {devtool: 'eval'})), { contentBase: dest, historyApiFallback: true }).listen(3334, "localhost", function (err) { diff --git a/SingularityUI/package.json b/SingularityUI/package.json index d7e1ac5f7b..bdf520cd12 100644 --- a/SingularityUI/package.json +++ b/SingularityUI/package.json @@ -26,13 +26,20 @@ "datatables-bootstrap3-plugin": "~0.4.0", "fuse.js": "~1.2.2", "fuzzy": "~0.1.1", + "humanize-plus": "^1.8.1", "jquery": "~1.11.1", "juration": "*", "messenger": "git://github.com/HubSpot/messenger#set-main-field", "moment": "2.11.2", + "q": "^1.4.1", "react": "~0.14.2", "react-dom": "~0.14.2", "react-list": "~0.7.3", + "react-redux": "^4.4.1", + "react-waypoint": "^1.3.0", + "redux": "^3.3.1", + "redux-logger": "^2.6.1", + "redux-thunk": "^2.0.1", "select2": "~3.5.1", "sortable": "git://github.com/HubSpot/sortable.git#v0.6.0", "typeahead.js": "~0.10.4", @@ -56,6 +63,7 @@ "nib": "^1.1.0", "webpack": "^1.12.14", "webpack-dev-server": "^1.14.1", + "webpack-merge": "^0.8.4", "webpack-stream": "^3.1.0" } } diff --git a/SingularityUI/webpack.config.js b/SingularityUI/webpack.config.js index aa4827d8be..c83f0b5d3c 100644 --- a/SingularityUI/webpack.config.js +++ b/SingularityUI/webpack.config.js @@ -4,7 +4,31 @@ var path = require('path'); dest = path.resolve(__dirname, '../SingularityService/target/generated-resources/assets'); module.exports = { - entry: './app/initialize.coffee', + entry: { + app: './app/initialize.coffee', + vendor: [ + 'react', + 'jquery', + 'underscore', + 'clipboard', + 'select2', + 'handlebars', + 'moment', + 'messenger', + 'bootstrap', + 'classnames', + 'react-interval', + 'react-waypoint', + 'backbone-react-component', + 'react-dom', + 'fuzzy', + 'datatables', + 'sortable', + 'juration', + 'backbone', + 'vex-js' + ], + }, output: { path: dest, filename: 'app.js' @@ -43,6 +67,7 @@ module.exports = { compress: { warnings: false } - }) + }), + new webpack.optimize.CommonsChunkPlugin('vendor', 'vendor.bundle.js'), ] }; From b06227013cfa5fbf90259dd1b5330ea8435d2a3f Mon Sep 17 00:00:00 2001 From: tpetr Date: Sun, 27 Mar 2016 22:08:07 -0400 Subject: [PATCH 02/44] fix --- SingularityUI/app/components/logs/LogContainer.cjsx | 1 - SingularityUI/app/controllers/LogViewer.cjsx | 1 - 2 files changed, 2 deletions(-) diff --git a/SingularityUI/app/components/logs/LogContainer.cjsx b/SingularityUI/app/components/logs/LogContainer.cjsx index a77d3b959f..af03ce9f27 100644 --- a/SingularityUI/app/components/logs/LogContainer.cjsx +++ b/SingularityUI/app/components/logs/LogContainer.cjsx @@ -2,7 +2,6 @@ React = require 'react' Header = require './Header' TaskGroupContainer = require './TaskGroupContainer' BackboneReactComponent = require 'backbone-react-component' -MergedLogLines = require '../../collections/MergedLogLines' { connect } = require 'react-redux' diff --git a/SingularityUI/app/controllers/LogViewer.cjsx b/SingularityUI/app/controllers/LogViewer.cjsx index 69141d32e9..939554a37d 100644 --- a/SingularityUI/app/controllers/LogViewer.cjsx +++ b/SingularityUI/app/controllers/LogViewer.cjsx @@ -1,7 +1,6 @@ Controller = require './Controller' LogLines = require '../collections/LogLines' -MergedLogLines = require '../collections/MergedLogLines' LogView = require '../views/logView' From 4f16f0f28574dc2fb2609682e2f5f1a9612d1d1a Mon Sep 17 00:00:00 2001 From: tpetr Date: Mon, 28 Mar 2016 09:34:07 -0400 Subject: [PATCH 03/44] fix build --- SingularityUI/gulpfile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SingularityUI/gulpfile.js b/SingularityUI/gulpfile.js index 9a0c690bfd..61060f9077 100644 --- a/SingularityUI/gulpfile.js +++ b/SingularityUI/gulpfile.js @@ -59,7 +59,7 @@ gulp.task('fonts', function() { }); gulp.task('scripts', function () { - return gulp.src(webpackConfig.entry) + return gulp.src(webpackConfig.entry.app) .pipe(webpack(webpackConfig)) .pipe(gulp.dest(dest + '/static/js')) }); From e0c9b0c09a62ffa765dfc434224af72550a79ffd Mon Sep 17 00:00:00 2001 From: tpetr Date: Mon, 28 Mar 2016 14:45:22 -0400 Subject: [PATCH 04/44] more fixes and improvements --- SingularityUI/app/actions/log.coffee | 32 +++---- .../app/components/logs/ColorDropdown.cjsx | 9 +- SingularityUI/app/components/logs/Header.cjsx | 15 +++- .../app/components/logs/LogContainer.cjsx | 36 ++++---- .../app/components/logs/LogLine.cjsx | 38 ++++---- .../app/components/logs/LogLines.cjsx | 89 +++++++++++++++---- .../app/components/logs/SearchDropdown.cjsx | 31 ++++--- .../components/logs/TaskGroupContainer.cjsx | 27 ++---- SingularityUI/app/controllers/LogViewer.cjsx | 7 -- SingularityUI/app/reducers/log.coffee | 15 ++-- SingularityUI/package.json | 1 + 11 files changed, 173 insertions(+), 127 deletions(-) diff --git a/SingularityUI/app/actions/log.coffee b/SingularityUI/app/actions/log.coffee index 7533b802bd..925f6c3bbb 100644 --- a/SingularityUI/app/actions/log.coffee +++ b/SingularityUI/app/actions/log.coffee @@ -32,8 +32,7 @@ initialize = (requestId, path, search, taskIds) -> dispatch(initTask(taskGroupId, taskId, offset, resolvedPath)) Promise.all(taskPromises).then -> - fetchPromises = dispatch(taskGroupFetchPrevious(taskGroupId)) - Promise.all(fetchPromises).then -> + dispatch(taskGroupFetchPrevious(taskGroupId)).then -> dispatch(taskGroupReady(taskGroupId)) Promise.all(groupPromises) @@ -88,7 +87,7 @@ updateGroups = -> taskGroupFetchNext = (taskGroupId) -> (dispatch, getState) -> {tasks, taskGroups, logRequestLength} = getState() - dispatch({taskGroupId, type: 'LOG_TASK_GROUP_REQUEST_START'}) + promises = taskGroups[taskGroupId].taskIds.map (taskId) -> {maxOffset, path, initialDataLoaded} = tasks[taskId] if initialDataLoaded @@ -99,13 +98,14 @@ taskGroupFetchNext = (taskGroupId) -> dispatch(taskData(taskGroupId, taskId, data, offset, nextOffset, true)) else Promise.resolve() # reject("initialDataLoaded is false for task #{taskId}") - Promise.all(promises).then -> - dispatch({taskGroupId, type: 'LOG_TASK_GROUP_REQUEST_END'}) + + Promise.all(promises) taskGroupFetchPrevious = (taskGroupId) -> (dispatch, getState) -> {tasks, taskGroups, logRequestLength} = getState() - taskGroups[taskGroupId].taskIds.map (taskId) -> + + promises = taskGroups[taskGroupId].taskIds.map (taskId) -> {minOffset, path, initialDataLoaded} = tasks[taskId] if minOffset > 0 and initialDataLoaded xhr = fetchData(taskId, path, Math.max(minOffset - logRequestLength, 0), Math.min(logRequestLength, minOffset - logRequestLength)) @@ -116,6 +116,8 @@ taskGroupFetchPrevious = (taskGroupId) -> else Promise.resolve() # reject("initialDataLoaded is false for task #{taskId}") + Promise.all(promises) + taskData = (taskGroupId, taskId, data, offset, nextOffset, append) -> { taskGroupId @@ -162,7 +164,7 @@ selectLogColor = (color) -> switchViewMode = (newViewMode) -> (dispatch, getState) -> - { taskGroups, path, requestId, search, viewMode } = getState() + { taskGroups, path, activeRequest, search, viewMode } = getState() if newViewMode in ['custom', viewMode] return @@ -170,25 +172,25 @@ switchViewMode = (newViewMode) -> taskIds = _.flatten(_.pluck(taskGroups, 'taskIds')) dispatch({viewMode: newViewMode, type: 'LOG_SWITCH_VIEW_MODE'}) - - initialize(requestId, path, search, newViewMode, taskIds)(dispatch) + dispatch(initialize(activeRequest.requestId, path, search, taskIds)) setCurrentSearch = (newSearch) -> (dispatch, getState) -> - {requestId, path, taskGroups, currentSearch} = getState() + {activeRequest, path, taskGroups, currentSearch} = getState() if newSearch != currentSearch - initialize(requestId, path, newSearch, _.pluck(taskGroups, 'taskIds'))(dispatch) + dispatch(initialize(activeRequest.requestId, path, newSearch, _.flatten(_.pluck(taskGroups, 'taskIds')))) toggleTaskLog = (taskId) -> (dispatch, getState) -> - {path, taskGroups, tasks, viewMode} = getState() + {search, path, tasks, viewMode} = getState() if tasks[taskId] dispatch({taskId, type: 'LOG_REMOVE_TASK'}) else - if viewMode is 'unified' - taskGroupId = 0 + if viewMode is 'split' + dispatch(addTaskGroup(path, search, [taskId])) + taskGroupId = getState().taskGroups.length - 1 else - taskGroupId = dispatch(addTaskGroup(path, '', [taskId])) + taskGroupId = 0 resolvedPath = path.replace('$TASK_ID', taskId) fetchData(taskId, resolvedPath).done ({offset}) -> dispatch(initTask(taskGroupId, taskId, offset, resolvedPath)) diff --git a/SingularityUI/app/components/logs/ColorDropdown.cjsx b/SingularityUI/app/components/logs/ColorDropdown.cjsx index 2b80838b54..9404129d3d 100644 --- a/SingularityUI/app/components/logs/ColorDropdown.cjsx +++ b/SingularityUI/app/components/logs/ColorDropdown.cjsx @@ -13,7 +13,7 @@ class ColorDropdown extends React.Component className = classNames active: @props.activeColor is colorClass
  • - @props.setLogColor(colorClass)}> + @props.selectLogColor(colorClass)}> {color}
  • @@ -29,9 +29,10 @@ class ColorDropdown extends React.Component -mapStateToProps = (state, ownProps) -> ownProps +mapStateToProps = (state) -> + colors: state.colors + activeColor: state.activeColor -mapDispatchToProps = (dispatch) -> - setLogColor: (color) -> dispatch(selectLogColor(color)) +mapDispatchToProps = { selectLogColor } module.exports = connect(mapStateToProps, mapDispatchToProps)(ColorDropdown) \ No newline at end of file diff --git a/SingularityUI/app/components/logs/Header.cjsx b/SingularityUI/app/components/logs/Header.cjsx index ebf77ddebe..9fabb840bd 100644 --- a/SingularityUI/app/components/logs/Header.cjsx +++ b/SingularityUI/app/components/logs/Header.cjsx @@ -6,11 +6,18 @@ TasksDropdown = require './TasksDropdown' { connect } = require 'react-redux' { switchViewMode } = require '../../actions/log' +scrollToBottom = -> +scrollToTop = -> + class Header extends React.Component @propTypes: requestId: React.PropTypes.string.isRequired path: React.PropTypes.string.isRequired taskIdCount: React.PropTypes.number.isRequired + viewMode: React.PropTypes.string.isRequired + + scrollToBottom: React.PropTypes.func.isRequired + scrollToTop: React.PropTypes.func.isRequired toggleHelp: -> # TODO @@ -62,7 +69,7 @@ class Header extends React.Component
    - + {@renderViewButtons()} {@renderAnchorButtons()} {@renderHelpButton()} @@ -71,12 +78,14 @@ class Header extends React.Component
    mapStateToProps = (state) -> + taskIdCount: Object.keys(state.tasks).length + path: state.path viewMode: state.viewMode requestId: state.activeRequest.requestId mapDispatchToProps = (dispatch) -> switchViewMode: (viewMode) -> dispatch(switchViewMode(viewMode)) - scrollToBottom: -> dispatch() - scrollToTop: -> dispatch() + scrollToBottom: -> dispatch(scrollToBottom()) + scrollToTop: -> dispatch(scrollToTop()) module.exports = connect(mapStateToProps, mapDispatchToProps)(Header) diff --git a/SingularityUI/app/components/logs/LogContainer.cjsx b/SingularityUI/app/components/logs/LogContainer.cjsx index af03ce9f27..65a7384788 100644 --- a/SingularityUI/app/components/logs/LogContainer.cjsx +++ b/SingularityUI/app/components/logs/LogContainer.cjsx @@ -1,40 +1,36 @@ React = require 'react' +Interval = require 'react-interval' Header = require './Header' TaskGroupContainer = require './TaskGroupContainer' -BackboneReactComponent = require 'backbone-react-component' { connect } = require 'react-redux' -class LogContainer extends React.Component - mixins: [Backbone.React.Component.mixin] +{ updateGroups } = require '../../actions/log' +class LogContainer extends React.Component @propTypes: taskGroups: React.PropTypes.array.isRequired - tasks: React.PropTypes.object.isRequired - path: React.PropTypes.string.isRequired + ready: React.PropTypes.bool.isRequired - renderTaskIdGroups: -> - componentProps = @props + updateGroups: React.PropTypes.func.isRequired + + renderTaskGroups: -> @props.taskGroups.map (taskGroup, i) -> - + render: ->
    -
    + +
    - {@renderTaskIdGroups()} + {@renderTaskGroups()}
    -mapStateToProps = (state, ownProps) -> - colors: state.colors - activeColor: state.activeColor - requestId: state.requestId +mapStateToProps = (state) -> taskGroups: state.taskGroups - tasks: state.tasks - path: state.path + ready: _.all(_.pluck(state.taskGroups, 'ready')) + +mapDispatchToProps = { updateGroups } -module.exports = connect(mapStateToProps)(LogContainer) +module.exports = connect(mapStateToProps, mapDispatchToProps)(LogContainer) diff --git a/SingularityUI/app/components/logs/LogLine.cjsx b/SingularityUI/app/components/logs/LogLine.cjsx index 6406b20d30..095c33762a 100644 --- a/SingularityUI/app/components/logs/LogLine.cjsx +++ b/SingularityUI/app/components/logs/LogLine.cjsx @@ -1,19 +1,18 @@ React = require 'react' classNames = require 'classnames' +{ connect } = require 'react-redux' +{ clickPermalink } = require '../../actions/log' + class LogLine extends React.Component @propTypes: offset: React.PropTypes.number.isRequired isHighlighted: React.PropTypes.bool.isRequired content: React.PropTypes.string.isRequired - onPermalinkClick: React.PropTypes.func.isRequired - permalinkEnabled: React.PropTypes.bool.isRequired - search: React.PropTypes.string + taskId: React.PropTypes.string.isRequired - shouldComponentUpdate: (nextProps) -> - (@props.offset isnt nextProps.offset) or - (@props.isHighlighted isnt nextProps.isHighlighted) or - (@props.search isnt nextProps.search) + search: React.PropTypes.string + clickPermalink: React.PropTypes.func.isRequired highlightContent: (content) -> search = @props.search @@ -46,29 +45,26 @@ class LogLine extends React.Component 'search-match': s.match {s.text} - handlePermalinkClick: (e) -> - e.preventDefault() - @props.onPermalinkClick(@props.offset) - - renderPermalink: -> - if @props.permalinkEnabled - -
    - -
    -
    - render: -> divClass = classNames line: true highlightLine: @props.isHighlighted
    - {@renderPermalink()} + @props.clickPermalink(@props.offset)}> +
    + +
    +
    {@props.offset} | {@highlightContent(@props.content)}
    -module.exports = LogLine +mapStateToProps = (state, ownProps) -> + search: state.search + +mapDispatchToProps = { clickPermalink } + +module.exports = connect(mapStateToProps, mapDispatchToProps)(LogLine) diff --git a/SingularityUI/app/components/logs/LogLines.cjsx b/SingularityUI/app/components/logs/LogLines.cjsx index 5369b89bf9..a650f86821 100644 --- a/SingularityUI/app/components/logs/LogLines.cjsx +++ b/SingularityUI/app/components/logs/LogLines.cjsx @@ -1,11 +1,19 @@ React = require 'react' -ReactDOM = require 'react-dom' Waypoint = require 'react-waypoint' LogLine = require './LogLine' Humanize = require 'humanize' LogLines = require '../../collections/LogLines' -Utils = require '../../utils' +{ connect } = require 'react-redux' +{ taskGroupTop, taskGroupBottom } = require '../../actions/log' + +scrollThreshold = 100 + +sum = (numbers) -> + total = 0 + for n in numbers + total += n + total class LogLines extends React.Component @propTypes: @@ -13,27 +21,48 @@ class LogLines extends React.Component onEnterBottom: React.PropTypes.func.isRequired onLeaveTop: React.PropTypes.func.isRequired onLeaveBottom: React.PropTypes.func.isRequired - onPermalinkClick: React.PropTypes.func.isRequired taskGroupId: React.PropTypes.number.isRequired logLines: React.PropTypes.array.isRequired - search: React.PropTypes.string.isRequired initialDataLoaded: React.PropTypes.bool.isRequired reachedStartOfFile: React.PropTypes.bool.isRequired reachedEndOfFile: React.PropTypes.bool.isRequired bytesRemainingBefore: React.PropTypes.number.isRequired bytesRemainingAfter: React.PropTypes.number.isRequired - permalinkEnabled: React.PropTypes.bool.isRequired activeColor: React.PropTypes.string.isRequired + constructor: (props) -> + super(props) + @state = { + atTop: false + atBottom: false + } + + componentDidMount: -> + window.addEventListener 'resize', @handleScroll + + componentWillUnmount: -> + window.removeEventListener 'resize', @handleScroll + componentWillUpdate: -> @shouldScrollToBottom = @refs.tailContents.scrollTop + @refs.tailContents.offsetHeight is @refs.tailContents.scrollHeight - componentDidUpdate: -> + componentDidUpdate: (prevProps, prevState) -> if @shouldScrollToBottom @refs.tailContents.scrollTop = @refs.tailContents.scrollHeight + if prevState.atTop != @state.atTop + if @state.atTop + @props.onEnterTop(@props.taskGroupId) + else + @props.onLeaveTop(@props.taskGroupId) + if prevState.atBottom != @state.atBottom + if @state.atBottom + @props.onEnterBottom(@props.taskGroupId) + else + @props.onLeaveBottom(@props.taskGroupId) + renderLoadingPrevious: -> if @props.initialDataLoaded if @props.reachedStartOfFile @@ -47,11 +76,8 @@ class LogLines extends React.Component content={data} key={offset} offset={offset} - isHighlighted={offset is @props.initialOffset} - onPermalinkClick={@props.onPermalinkClick} taskId={taskId} - permalinkEnabled={@props.permalinkEnabled} - search={@props.search} /> + isHighlighted={offset is @props.initialOffset} /> renderLoadingMore: -> if @props.initialDataLoaded @@ -60,20 +86,47 @@ class LogLines extends React.Component else
    Loading more... ({Humanize.filesize(@props.bytesRemainingAfter)} remaining)
    - handleEnterTop: => @props.onEnterTop(@props.taskGroupId) - handleEnterBottom: => @props.onEnterBottom(@props.taskGroupId) - handleLeaveTop: => @props.onLeaveTop(@props.taskGroupId) - handleLeaveBottom: => @props.onLeaveBottom(@props.taskGroupId) + handleScroll: => + newState = {} + if @state.atTop and @refs.tailContents.scrollTop > @props.scrollThreshold + newState.atTop = false + @props.onLeaveTop(@props.taskGroupId) + else if not @state.atTop and @refs.tailContents.scrollTop < @props.scrollThreshold + newState.atTop = true + @props.onEnterTop(@props.taskGroupId) + + if @state.atBottom and (@refs.tailContents.scrollTop + @refs.tailContents.clientHeight) > (@refs.tailContents.scrollHeight - scrollThreshold) + newState.atBottom = false + @props.onLeaveBottom(@props.taskGroupId) + else if not @state.atBottom and (@refs.tailContents.scrollTop + @refs.tailContents.clientHeight) < (@refs.tailContents.scrollHeight - scrollThreshold) + newState.atBottom = true + @props.onEnterBottom(@props.taskGroupId) render: ->
    -
    +
    {@renderLoadingPrevious()} - {@renderLogLines()} - {@renderLoadingMore()}
    -module.exports = LogLines +mapStateToProps = (state, ownProps) -> + taskGroup = state.taskGroups[ownProps.taskGroupId] + tasks = taskGroup.taskIds.map (taskId) -> state.tasks[taskId] + + logLines: taskGroup.logLines + activeColor: state.activeColor + initialDataLoaded: _.all(_.pluck(tasks, 'initialDataLoaded')) + reachedStartOfFile: _.all(tasks.map (task) -> task.minOffset is 0) + reachedEndOfFile: _.all(tasks.map (task) -> task.maxOffset >= task.filesize) + bytesRemainingBefore: sum(_.pluck(tasks, 'minOffset')) + bytesRemainingAfter: sum(tasks.map (task) -> Math.max(task.filesize - task.maxOffset, 0)) + +mapDispatchToProps = (dispatch, ownProps) -> + onEnterTop: -> dispatch(taskGroupTop(ownProps.taskGroupId, true)) + onLeaveTop: -> dispatch(taskGroupTop(ownProps.taskGroupId, false)) + onEnterBottom: -> dispatch(taskGroupBottom(ownProps.taskGroupId, true)) + onLeaveBottom: -> dispatch(taskGroupBottom(ownProps.taskGroupId, false)) + +module.exports = connect(mapStateToProps, mapDispatchToProps)(LogLines) diff --git a/SingularityUI/app/components/logs/SearchDropdown.cjsx b/SingularityUI/app/components/logs/SearchDropdown.cjsx index 7ca92e4a01..7ab0c82cbc 100644 --- a/SingularityUI/app/components/logs/SearchDropdown.cjsx +++ b/SingularityUI/app/components/logs/SearchDropdown.cjsx @@ -1,34 +1,46 @@ React = require 'react' +ReactDOM = require 'react-dom' { connect } = require 'react-redux' { setCurrentSearch } = require '../../actions/log' class SearchDropdown extends React.Component @propTypes: - currentSearch: React.PropTypes.string.isRequired + search: React.PropTypes.string.isRequired constructor: (props) -> super(props) @state = { - searchValue: 'xxx' + searchValue: props.search } - handleSearchToggle: -> - # TODO + handleSearchToggle: => + ReactDOM.findDOMNode(@refs.searchInput).focus() handleSearchUpdate: => - console.log @props.setCurrentSearch @props.setCurrentSearch(@state.searchValue) + toggleSearchDropdown: => + $(ReactDOM.findDOMNode(@refs.searchButton)).dropdown("toggle") + + handleSearchKeyDown: (event) => + if event.keyCode is 13 # Enter: commit search and close + @handleSearchUpdate() + @toggleSearchDropdown() + else if event.keyCode is 27 # Escape: clear search and commit + @setState + searchValue: @props.search + @toggleSearchDropdown() + render: ->
    -
    • - @setState({searchValue: e.target.value})} /> + @setState({searchValue: e.target.value})} /> @@ -38,9 +50,8 @@ class SearchDropdown extends React.Component
      mapStateToProps = (state) -> - currentSearch: state.currentSearch + search: state.search -mapDispatchToProps = (dispatch) -> - setCurrentSearch: (value) -> dispatch(setCurrentSearch(value)) +mapDispatchToProps = { setCurrentSearch } module.exports = connect(mapStateToProps, mapDispatchToProps)(SearchDropdown) \ No newline at end of file diff --git a/SingularityUI/app/components/logs/TaskGroupContainer.cjsx b/SingularityUI/app/components/logs/TaskGroupContainer.cjsx index 0a82167d42..9297c059b2 100644 --- a/SingularityUI/app/components/logs/TaskGroupContainer.cjsx +++ b/SingularityUI/app/components/logs/TaskGroupContainer.cjsx @@ -4,49 +4,32 @@ LogLines = require './LogLines' classNames = require 'classnames' { connect } = require 'react-redux' -{ taskGroupTop, taskGroupBottom, clickPermalink } = require '../../actions/log' - -sum = (numbers) -> - total = 0 - for n in numbers - total += n - total class TaskGroupContainer extends React.Component @propTypes: taskGroupId: React.PropTypes.number.isRequired path: React.PropTypes.string.isRequired + taskGroupCount: React.PropTypes.number.isRequired render: -> className = "col-md-#{ 12 / @props.taskGroupCount } tail-column"
      - +
      mapStateToProps = (state, ownProps) -> taskGroup = state.taskGroups[ownProps.taskGroupId] - tasks = taskGroup.taskIds.map (taskId) -> ownProps.tasks[taskId] + tasks = taskGroup.taskIds.map (taskId) -> state.tasks[taskId] taskIds: taskGroup.taskIds logLines: taskGroup.logLines search: taskGroup.search ready: taskGroup.ready taskGroupCount: state.taskGroups.length - initialDataLoaded: _.all(_.pluck(tasks, 'initialDataLoaded')) - reachedStartOfFile: _.all(tasks.map (task) -> task.minOffset is 0) - reachedEndOfFile: _.all(tasks.map (task) -> task.maxOffset >= task.filesize) - bytesRemainingBefore: sum(_.pluck(tasks, 'minOffset')) - bytesRemainingAfter: sum(tasks.map (task) -> Math.max(task.filesize - task.maxOffset, 0)) permalinkEnabled: tasks.length is 1 activeColor: state.activeColor + path: state.path -mapDispatchToProps = (dispatch) -> - onEnterTop: (taskGroupId) -> dispatch(taskGroupTop(taskGroupId, true)) - onEnterBottom: (taskGroupId) -> dispatch(taskGroupBottom(taskGroupId, true)) - onLeaveTop: (taskGroupId) -> dispatch(taskGroupTop(taskGroupId, false)) - onLeaveBottom: (taskGroupId) -> dispatch(taskGroupBottom(taskGroupId, false)) - onPermalinkClick: (offset) -> dispatch(clickPermalink(offset)) - -module.exports = connect(mapStateToProps, mapDispatchToProps)(TaskGroupContainer) \ No newline at end of file +module.exports = connect(mapStateToProps)(TaskGroupContainer) \ No newline at end of file diff --git a/SingularityUI/app/controllers/LogViewer.cjsx b/SingularityUI/app/controllers/LogViewer.cjsx index 939554a37d..cac313c90a 100644 --- a/SingularityUI/app/controllers/LogViewer.cjsx +++ b/SingularityUI/app/controllers/LogViewer.cjsx @@ -35,9 +35,6 @@ class LogViewer extends Controller initPromise.then => @store.dispatch(ActiveTasks.updateActiveTasks(@requestId)) - setInterval @update, 1000 - setInterval @updateFilesizes, 10000 - # create log view @view = new LogView @store @@ -46,8 +43,4 @@ class LogViewer extends Controller @view.render() app.showView @view - update: => @store.dispatch(LogActions.updateGroups()) - - updateFilesizes: => @store.dispatch(LogActions.updateFilesizes()) - module.exports = LogViewer diff --git a/SingularityUI/app/reducers/log.coffee b/SingularityUI/app/reducers/log.coffee index 22cb708e33..0a79024a73 100644 --- a/SingularityUI/app/reducers/log.coffee +++ b/SingularityUI/app/reducers/log.coffee @@ -88,7 +88,8 @@ taskGroups = (state=[], action) -> continue else newTaskGroup = Object.assign({}, taskGroup) - newTaskGroup.taskIds = taskGroup.filter (taskId) -> taskId isnt action.taskId + newTaskGroup.taskIds = taskGroup.taskIds.filter (taskId) -> taskId isnt action.taskId + newTaskGroup.logLines = taskGroup.logLines.filter (logLine) -> logLine.taskId isnt action.taskId newState.push(newTaskGroup) else newState.push(taskGroup) @@ -109,7 +110,7 @@ taskGroups = (state=[], action) -> offset = action.offset lines = _.initial(action.data.match /[^\n]*(\n|$)/g).map (data) -> offset += data.length - {data, offset: offset - data.length} + {data, offset: offset - data.length, taskId: action.taskId} if taskGroup.search lines = filterLogLines(lines, taskGroup.search) @@ -144,9 +145,9 @@ viewMode = (state='custom', action) -> return action.viewMode return state -currentSearch = (state='', action) -> - if action.type is 'LOG_SET_SEARCH' - return action.currentSearch +search = (state='', action) -> + if action.type is 'LOG_INIT' + return action.search return state logRequestLength = (state=30000, action) -> @@ -154,9 +155,9 @@ logRequestLength = (state=30000, action) -> activeRequest = (state={}, action) -> if action.type is 'LOG_INIT' - return {requestId: action.requestId} + return Object.assign({}, state, {requestId: action.requestId}) if action.type is 'REQUEST_ACTIVE_TASKS' return Object.assign({}, state, {activeTasks: action.tasks}) return state -module.exports = combineReducers({tasks, taskGroups, activeRequest, path, activeColor, colors, viewMode, currentSearch, logRequestLength}) \ No newline at end of file +module.exports = combineReducers({tasks, taskGroups, activeRequest, path, activeColor, colors, viewMode, search, logRequestLength}) \ No newline at end of file diff --git a/SingularityUI/package.json b/SingularityUI/package.json index bdf520cd12..2ea86f7976 100644 --- a/SingularityUI/package.json +++ b/SingularityUI/package.json @@ -34,6 +34,7 @@ "q": "^1.4.1", "react": "~0.14.2", "react-dom": "~0.14.2", + "react-interval": "^1.2.1", "react-list": "~0.7.3", "react-redux": "^4.4.1", "react-waypoint": "^1.3.0", From 0127a1e50eab3043951b969a35bfdbd147b38467 Mon Sep 17 00:00:00 2001 From: tpetr Date: Mon, 28 Mar 2016 15:14:27 -0400 Subject: [PATCH 05/44] fix scrolling + add log line buffer size --- SingularityUI/app/actions/log.coffee | 11 +++---- .../app/components/logs/LogLines.cjsx | 30 +++++++++---------- SingularityUI/app/reducers/log.coffee | 9 +++++- 3 files changed, 28 insertions(+), 22 deletions(-) diff --git a/SingularityUI/app/actions/log.coffee b/SingularityUI/app/actions/log.coffee index 925f6c3bbb..1d783eb3b9 100644 --- a/SingularityUI/app/actions/log.coffee +++ b/SingularityUI/app/actions/log.coffee @@ -86,7 +86,7 @@ updateGroups = -> taskGroupFetchNext = (taskGroupId) -> (dispatch, getState) -> - {tasks, taskGroups, logRequestLength} = getState() + {tasks, taskGroups, logRequestLength, maxLines} = getState() promises = taskGroups[taskGroupId].taskIds.map (taskId) -> {maxOffset, path, initialDataLoaded} = tasks[taskId] @@ -95,7 +95,7 @@ taskGroupFetchNext = (taskGroupId) -> xhr.done ({data, offset, nextOffset}) -> if data.length > 0 nextOffset = nextOffset || offset + data.length - dispatch(taskData(taskGroupId, taskId, data, offset, nextOffset, true)) + dispatch(taskData(taskGroupId, taskId, data, offset, nextOffset, true, maxLines)) else Promise.resolve() # reject("initialDataLoaded is false for task #{taskId}") @@ -103,7 +103,7 @@ taskGroupFetchNext = (taskGroupId) -> taskGroupFetchPrevious = (taskGroupId) -> (dispatch, getState) -> - {tasks, taskGroups, logRequestLength} = getState() + {tasks, taskGroups, logRequestLength, maxLines} = getState() promises = taskGroups[taskGroupId].taskIds.map (taskId) -> {minOffset, path, initialDataLoaded} = tasks[taskId] @@ -112,13 +112,13 @@ taskGroupFetchPrevious = (taskGroupId) -> xhr.done ({data, offset, nextOffset}) -> if data.length > 0 nextOffset = nextOffset || offset + data.length - dispatch(taskData(taskGroupId, taskId, data, offset, nextOffset, false)) + dispatch(taskData(taskGroupId, taskId, data, offset, nextOffset, false, maxLines)) else Promise.resolve() # reject("initialDataLoaded is false for task #{taskId}") Promise.all(promises) -taskData = (taskGroupId, taskId, data, offset, nextOffset, append) -> +taskData = (taskGroupId, taskId, data, offset, nextOffset, append, maxLines) -> { taskGroupId taskId @@ -126,6 +126,7 @@ taskData = (taskGroupId, taskId, data, offset, nextOffset, append) -> offset nextOffset append + maxLines type: 'LOG_TASK_DATA' } diff --git a/SingularityUI/app/components/logs/LogLines.cjsx b/SingularityUI/app/components/logs/LogLines.cjsx index a650f86821..1c7bf46888 100644 --- a/SingularityUI/app/components/logs/LogLines.cjsx +++ b/SingularityUI/app/components/logs/LogLines.cjsx @@ -7,7 +7,7 @@ LogLines = require '../../collections/LogLines' { connect } = require 'react-redux' { taskGroupTop, taskGroupBottom } = require '../../actions/log' -scrollThreshold = 100 +SCROLL_THRESHOLD = 10 sum = (numbers) -> total = 0 @@ -52,17 +52,6 @@ class LogLines extends React.Component if @shouldScrollToBottom @refs.tailContents.scrollTop = @refs.tailContents.scrollHeight - if prevState.atTop != @state.atTop - if @state.atTop - @props.onEnterTop(@props.taskGroupId) - else - @props.onLeaveTop(@props.taskGroupId) - if prevState.atBottom != @state.atBottom - if @state.atBottom - @props.onEnterBottom(@props.taskGroupId) - else - @props.onLeaveBottom(@props.taskGroupId) - renderLoadingPrevious: -> if @props.initialDataLoaded if @props.reachedStartOfFile @@ -87,21 +76,30 @@ class LogLines extends React.Component
      Loading more... ({Humanize.filesize(@props.bytesRemainingAfter)} remaining)
      handleScroll: => + changedState = false newState = {} - if @state.atTop and @refs.tailContents.scrollTop > @props.scrollThreshold + + if @state.atTop and @refs.tailContents.scrollTop > SCROLL_THRESHOLD + changedState = true newState.atTop = false @props.onLeaveTop(@props.taskGroupId) - else if not @state.atTop and @refs.tailContents.scrollTop < @props.scrollThreshold + else if not @state.atTop and @refs.tailContents.scrollTop < SCROLL_THRESHOLD + changedState = true newState.atTop = true @props.onEnterTop(@props.taskGroupId) - if @state.atBottom and (@refs.tailContents.scrollTop + @refs.tailContents.clientHeight) > (@refs.tailContents.scrollHeight - scrollThreshold) + if @state.atBottom and @refs.tailContents.scrollTop + @refs.tailContents.clientHeight < @refs.tailContents.scrollHeight - SCROLL_THRESHOLD + changedState = true newState.atBottom = false @props.onLeaveBottom(@props.taskGroupId) - else if not @state.atBottom and (@refs.tailContents.scrollTop + @refs.tailContents.clientHeight) < (@refs.tailContents.scrollHeight - scrollThreshold) + else if not @state.atBottom and @refs.tailContents.scrollTop + @refs.tailContents.clientHeight > @refs.tailContents.scrollHeight - SCROLL_THRESHOLD + changedState = true newState.atBottom = true @props.onEnterBottom(@props.taskGroupId) + if changedState + @setState Object.assign({}, @state, newState) + render: ->
      diff --git a/SingularityUI/app/reducers/log.coffee b/SingularityUI/app/reducers/log.coffee index 0a79024a73..3f8fb45a7b 100644 --- a/SingularityUI/app/reducers/log.coffee +++ b/SingularityUI/app/reducers/log.coffee @@ -117,8 +117,12 @@ taskGroups = (state=[], action) -> if action.append logLines = state[action.taskGroupId].logLines.concat(lines) + if logLines.length > action.maxLines + logLines = logLines.slice(logLines.length - action.maxLines) else logLines = lines.concat(state[action.taskGroupId].logLines) + if logLines.length > action.maxLines + logLines = logLines.slice(0, action.maxLines) return updateTaskGroup(state, action.taskGroupId, {logLines}) @@ -153,6 +157,9 @@ search = (state='', action) -> logRequestLength = (state=30000, action) -> return state +maxLines = (state=1000, action) -> + return state + activeRequest = (state={}, action) -> if action.type is 'LOG_INIT' return Object.assign({}, state, {requestId: action.requestId}) @@ -160,4 +167,4 @@ activeRequest = (state={}, action) -> return Object.assign({}, state, {activeTasks: action.tasks}) return state -module.exports = combineReducers({tasks, taskGroups, activeRequest, path, activeColor, colors, viewMode, search, logRequestLength}) \ No newline at end of file +module.exports = combineReducers({tasks, taskGroups, activeRequest, path, activeColor, colors, viewMode, search, logRequestLength, maxLines}) \ No newline at end of file From 2083a77afa2d71d927b3eebd52b95215213a7a64 Mon Sep 17 00:00:00 2001 From: tpetr Date: Mon, 28 Mar 2016 15:16:55 -0400 Subject: [PATCH 06/44] fix humanize import --- SingularityUI/app/components/logs/LogLines.cjsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SingularityUI/app/components/logs/LogLines.cjsx b/SingularityUI/app/components/logs/LogLines.cjsx index 1c7bf46888..26e9a6f8e5 100644 --- a/SingularityUI/app/components/logs/LogLines.cjsx +++ b/SingularityUI/app/components/logs/LogLines.cjsx @@ -1,7 +1,7 @@ React = require 'react' Waypoint = require 'react-waypoint' LogLine = require './LogLine' -Humanize = require 'humanize' +Humanize = require 'humanize-plus' LogLines = require '../../collections/LogLines' { connect } = require 'react-redux' From 86e687d04ea18d37f8f1a8442e992514b508eb0a Mon Sep 17 00:00:00 2001 From: tpetr Date: Mon, 28 Mar 2016 15:37:21 -0400 Subject: [PATCH 07/44] misc cleanup --- SingularityUI/app/actions/activeTasks.coffee | 2 -- SingularityUI/app/actions/log.coffee | 1 + SingularityUI/app/components/logs/Header.cjsx | 6 ------ 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/SingularityUI/app/actions/activeTasks.coffee b/SingularityUI/app/actions/activeTasks.coffee index c12165dc28..c8d87613af 100644 --- a/SingularityUI/app/actions/activeTasks.coffee +++ b/SingularityUI/app/actions/activeTasks.coffee @@ -1,5 +1,3 @@ -Q = require 'q' - fetchTasksForRequest = (requestId, state='active') -> params = { property: 'taskId' diff --git a/SingularityUI/app/actions/log.coffee b/SingularityUI/app/actions/log.coffee index 1d783eb3b9..2c6bfa2dae 100644 --- a/SingularityUI/app/actions/log.coffee +++ b/SingularityUI/app/actions/log.coffee @@ -1,4 +1,5 @@ Q = require 'q' + { fetchTasksForRequest } = require './activeTasks' fetchData = (taskId, path, offset=undefined, length=0) -> diff --git a/SingularityUI/app/components/logs/Header.cjsx b/SingularityUI/app/components/logs/Header.cjsx index 9fabb840bd..49d0c5ddf6 100644 --- a/SingularityUI/app/components/logs/Header.cjsx +++ b/SingularityUI/app/components/logs/Header.cjsx @@ -43,11 +43,6 @@ class Header extends React.Component - renderHelpButton: -> - - - - render: ->
      @@ -72,7 +67,6 @@ class Header extends React.Component {@renderViewButtons()} {@renderAnchorButtons()} - {@renderHelpButton()}
      From a241f2c036d624776f7b4f4af22ca1c83a17ba68 Mon Sep 17 00:00:00 2001 From: tpetr Date: Mon, 28 Mar 2016 16:17:58 -0400 Subject: [PATCH 08/44] fix logger unmount --- SingularityUI/app/controllers/LogViewer.cjsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/SingularityUI/app/controllers/LogViewer.cjsx b/SingularityUI/app/controllers/LogViewer.cjsx index cac313c90a..1f616a98fb 100644 --- a/SingularityUI/app/controllers/LogViewer.cjsx +++ b/SingularityUI/app/controllers/LogViewer.cjsx @@ -38,9 +38,7 @@ class LogViewer extends Controller # create log view @view = new LogView @store - @setView @view - - @view.render() + @setView @view # does nothing app.showView @view module.exports = LogViewer From 81f4d991f2dfa0f5a7672f91af668fb9b1854722 Mon Sep 17 00:00:00 2001 From: tpetr Date: Mon, 28 Mar 2016 16:18:51 -0400 Subject: [PATCH 09/44] set proper path when adding new task group --- SingularityUI/app/reducers/log.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/SingularityUI/app/reducers/log.coffee b/SingularityUI/app/reducers/log.coffee index 3f8fb45a7b..52eb995814 100644 --- a/SingularityUI/app/reducers/log.coffee +++ b/SingularityUI/app/reducers/log.coffee @@ -29,6 +29,7 @@ tasks = (state={}, action) -> newState = Object.assign({}, state) for taskId in action.taskIds newState[taskId] = { + path: action.path.replace('$TASK_ID', taskId) minOffset: 0 maxOffset: 0 filesize: 0 From 9703fcd3a8e5fd356416fd68df122cebfaf2e021 Mon Sep 17 00:00:00 2001 From: tpetr Date: Mon, 28 Mar 2016 16:47:16 -0400 Subject: [PATCH 10/44] implement scroll to top / scroll to bottom --- SingularityUI/app/actions/log.coffee | 14 ++++++++++++++ SingularityUI/app/components/logs/Header.cjsx | 5 +---- SingularityUI/app/reducers/log.coffee | 12 ++++++++++++ 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/SingularityUI/app/actions/log.coffee b/SingularityUI/app/actions/log.coffee index 2c6bfa2dae..f72d70797c 100644 --- a/SingularityUI/app/actions/log.coffee +++ b/SingularityUI/app/actions/log.coffee @@ -199,6 +199,18 @@ toggleTaskLog = (taskId) -> dispatch(taskGroupFetchPrevious(taskGroupId)).then -> dispatch(taskGroupReady(taskGroupId)) +scrollToTop = () -> + (dispatch, getState) -> + getState().taskGroups.map (taskGroup, taskGroupId) -> + dispatch({taskGroupId, type: 'LOG_SCROLL_TO_TOP'}) + dispatch(taskGroupFetchNext(taskGroupId)) + +scrollToBottom = () -> + (dispatch, getState) -> + getState().taskGroups.map (taskGroup, taskGroupId) -> + dispatch({taskGroupId, type: 'LOG_SCROLL_TO_BOTTOM'}) + dispatch(taskGroupFetchPrevious(taskGroupId)) + module.exports = { initialize initializeUsingActiveTasks @@ -213,4 +225,6 @@ module.exports = { switchViewMode setCurrentSearch toggleTaskLog + scrollToTop + scrollToBottom } diff --git a/SingularityUI/app/components/logs/Header.cjsx b/SingularityUI/app/components/logs/Header.cjsx index 49d0c5ddf6..401bd982c2 100644 --- a/SingularityUI/app/components/logs/Header.cjsx +++ b/SingularityUI/app/components/logs/Header.cjsx @@ -4,10 +4,7 @@ SearchDropdown = require './SearchDropdown' TasksDropdown = require './TasksDropdown' { connect } = require 'react-redux' -{ switchViewMode } = require '../../actions/log' - -scrollToBottom = -> -scrollToTop = -> +{ switchViewMode, scrollToTop, scrollToBottom } = require '../../actions/log' class Header extends React.Component @propTypes: diff --git a/SingularityUI/app/reducers/log.coffee b/SingularityUI/app/reducers/log.coffee index 52eb995814..6ed6716606 100644 --- a/SingularityUI/app/reducers/log.coffee +++ b/SingularityUI/app/reducers/log.coffee @@ -56,6 +56,16 @@ tasks = (state={}, action) -> }) else if action.type is 'LOG_TASK_FILESIZE' return updateTask(state, action.taskId, {filesize: action.filesize}) + else if action.type is 'LOG_SCROLL_TO_TOP' + newState = {} + for taskId of state + newState[taskId] = Object.assign({}, state[taskId], {minOffset: 0, maxOffset: 0}) + return newState + else if action.type is 'LOG_SCROLL_TO_BOTTOM' + newState = {} + for taskId of state + newState[taskId] = Object.assign({}, state[taskId], {minOffset: state[taskId].filesize, maxOffset: state[taskId].filesize}) + return newState return state taskGroups = (state=[], action) -> @@ -105,6 +115,8 @@ taskGroups = (state=[], action) -> return updateTaskGroup(state, action.taskGroupId, {bottom: action.visible}) else if action.type is 'LOG_TASK_GROUP_READY' return updateTaskGroup(state, action.taskGroupId, {ready: true}) + else if action.type in ['LOG_SCROLL_TO_TOP', 'LOG_SCROLL_TO_BOTTOM'] + return updateTaskGroup(state, action.taskGroupId, {logLines: []}) else if action.type is 'LOG_TASK_DATA' taskGroup = state[action.taskGroupId] From f38417bbe228e2a462ca38e1412a9d71bece2e56 Mon Sep 17 00:00:00 2001 From: tpetr Date: Mon, 28 Mar 2016 17:03:55 -0400 Subject: [PATCH 11/44] dont allow users to deselect all tasks --- SingularityUI/app/actions/log.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/SingularityUI/app/actions/log.coffee b/SingularityUI/app/actions/log.coffee index f72d70797c..d95a27fce6 100644 --- a/SingularityUI/app/actions/log.coffee +++ b/SingularityUI/app/actions/log.coffee @@ -186,7 +186,8 @@ toggleTaskLog = (taskId) -> (dispatch, getState) -> {search, path, tasks, viewMode} = getState() if tasks[taskId] - dispatch({taskId, type: 'LOG_REMOVE_TASK'}) + if Object.keys(tasks).length > 1 + dispatch({taskId, type: 'LOG_REMOVE_TASK'}) else if viewMode is 'split' dispatch(addTaskGroup(path, search, [taskId])) From 530ceeefc93925a743453624f97b4c925bbd9e13 Mon Sep 17 00:00:00 2001 From: tpetr Date: Mon, 28 Mar 2016 17:04:10 -0400 Subject: [PATCH 12/44] label-ify task id placeholder --- SingularityUI/app/components/logs/Header.cjsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/SingularityUI/app/components/logs/Header.cjsx b/SingularityUI/app/components/logs/Header.cjsx index 401bd982c2..f0c2e0d762 100644 --- a/SingularityUI/app/components/logs/Header.cjsx +++ b/SingularityUI/app/components/logs/Header.cjsx @@ -21,7 +21,10 @@ class Header extends React.Component renderBreadcrumbs: -> @props.path.split('/').map (subpath, i) -> -
    • {subpath}
    • + if subpath is '$TASK_ID' +
    • Task ID
    • + else +
    • {subpath}
    • renderViewButtons: -> if @props.taskIdCount > 1 From 6dceeccb818fabbd6e538d3a519dafb6a267f7dd Mon Sep 17 00:00:00 2001 From: tpetr Date: Mon, 28 Mar 2016 17:04:17 -0400 Subject: [PATCH 13/44] add link to instance --- SingularityUI/app/components/logs/TaskGroupHeader.cjsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SingularityUI/app/components/logs/TaskGroupHeader.cjsx b/SingularityUI/app/components/logs/TaskGroupHeader.cjsx index a22a0bf662..84b8542f2e 100644 --- a/SingularityUI/app/components/logs/TaskGroupHeader.cjsx +++ b/SingularityUI/app/components/logs/TaskGroupHeader.cjsx @@ -15,7 +15,7 @@ class TaskGroupHeader extends React.Component if @props.taskIds.length > 1 Viewing Instances {@props.taskIds.map(@getInstanceNumberFromTaskId).join(', ')} else - Instance {@getInstanceNumberFromTaskId(@props.taskIds[0])} + Instance {@getInstanceNumberFromTaskId(@props.taskIds[0])} renderTaskLegend: -> if @props.taskIds.length > 1 From 85f47d42a9b563e47ebed4fc574b772eeeecdd25 Mon Sep 17 00:00:00 2001 From: tpetr Date: Mon, 28 Mar 2016 17:37:16 -0400 Subject: [PATCH 14/44] sort task loggers by instance number --- SingularityUI/app/components/logs/TaskGroupHeader.cjsx | 10 ++++------ SingularityUI/app/controllers/LogViewer.cjsx | 2 +- SingularityUI/app/reducers/log.coffee | 8 +++++--- SingularityUI/app/router.coffee | 5 ++++- SingularityUI/app/utils.coffee | 4 ++++ 5 files changed, 18 insertions(+), 11 deletions(-) diff --git a/SingularityUI/app/components/logs/TaskGroupHeader.cjsx b/SingularityUI/app/components/logs/TaskGroupHeader.cjsx index 84b8542f2e..6bf9ad747b 100644 --- a/SingularityUI/app/components/logs/TaskGroupHeader.cjsx +++ b/SingularityUI/app/components/logs/TaskGroupHeader.cjsx @@ -1,5 +1,7 @@ React = require 'react' +{ getInstanceNumberFromTaskId } = require '../../utils' + class TaskGroupHeader extends React.Component @propTypes: taskIds: React.PropTypes.array.isRequired @@ -7,15 +9,11 @@ class TaskGroupHeader extends React.Component toggleLegend: -> # TODO - getInstanceNumberFromTaskId: (taskId) -> - splits = taskId.split('-') - splits[splits.length - 3] - renderInstanceInfo: -> if @props.taskIds.length > 1 - Viewing Instances {@props.taskIds.map(@getInstanceNumberFromTaskId).join(', ')} + Viewing Instances {@props.taskIds.map(getInstanceNumberFromTaskId).join(', ')} else - Instance {@getInstanceNumberFromTaskId(@props.taskIds[0])} + Instance {getInstanceNumberFromTaskId(@props.taskIds[0])} renderTaskLegend: -> if @props.taskIds.length > 1 diff --git a/SingularityUI/app/controllers/LogViewer.cjsx b/SingularityUI/app/controllers/LogViewer.cjsx index 1f616a98fb..c9ee1bb67b 100644 --- a/SingularityUI/app/controllers/LogViewer.cjsx +++ b/SingularityUI/app/controllers/LogViewer.cjsx @@ -27,7 +27,7 @@ class LogViewer extends Controller @store = Redux.createStore(rootReducer, initialState, Redux.compose(Redux.applyMiddleware(thunk.default, logger()))) - if taskIds + if taskIds.length > 0 initPromise = @store.dispatch(LogActions.initialize(@requestId, @path, search, taskIds)) else initPromise = @store.dispatch(LogActions.initializeUsingActiveTasks(@requestId, @path, search)) diff --git a/SingularityUI/app/reducers/log.coffee b/SingularityUI/app/reducers/log.coffee index 6ed6716606..bc4daeb975 100644 --- a/SingularityUI/app/reducers/log.coffee +++ b/SingularityUI/app/reducers/log.coffee @@ -1,5 +1,7 @@ { combineReducers } = require 'redux' +{ getInstanceNumberFromTaskId } = require '../utils' + updateTask = (state, taskId, update) -> newState = Object.assign({}, state) newState[taskId] = Object.assign({}, state[taskId], update) @@ -80,8 +82,7 @@ taskGroups = (state=[], action) -> pendingRequests: false } else if action.type is 'LOG_ADD_TASK_GROUP' - newState = Object.assign([], state) - newState.push({ + newState = state.concat({ taskIds: action.taskIds, logLines: [], top: false, @@ -90,7 +91,8 @@ taskGroups = (state=[], action) -> ready: false pendingRequests: false }) - return newState + + return _.sortBy(newState, (taskGroup) -> getInstanceNumberFromTaskId(taskGroup.taskIds[0])) else if action.type is 'LOG_REMOVE_TASK' newState = [] for taskGroup in state diff --git a/SingularityUI/app/router.coffee b/SingularityUI/app/router.coffee index ca5bb7a6b2..f018409c22 100644 --- a/SingularityUI/app/router.coffee +++ b/SingularityUI/app/router.coffee @@ -118,7 +118,10 @@ class Router extends Backbone.Router initialOffset = parseInt(window.location.hash.substr(1), 10) || null params = Utils.getQueryParams() - taskIds = (params.taskIds || '').split(',') + if params.taskIds + taskIds = params.taskIds.split(',') + else + taskIds = [] viewMode = params.viewMode || 'split' search = params.search || '' diff --git a/SingularityUI/app/utils.coffee b/SingularityUI/app/utils.coffee index 810543ddfe..8a22505a45 100644 --- a/SingularityUI/app/utils.coffee +++ b/SingularityUI/app/utils.coffee @@ -198,5 +198,9 @@ class Utils else fuzzyObject.score + @getInstanceNumberFromTaskId: (taskId) -> + splits = taskId.split('-') + splits[splits.length - 3] + module.exports = Utils From 678bf64d79f22296a973ae94bb0345fe0d1a0190 Mon Sep 17 00:00:00 2001 From: tpetr Date: Tue, 29 Mar 2016 13:57:41 -0400 Subject: [PATCH 15/44] fix bug causing negative lengths to be requested --- SingularityUI/app/actions/log.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SingularityUI/app/actions/log.coffee b/SingularityUI/app/actions/log.coffee index d95a27fce6..c7a61a2507 100644 --- a/SingularityUI/app/actions/log.coffee +++ b/SingularityUI/app/actions/log.coffee @@ -92,7 +92,7 @@ taskGroupFetchNext = (taskGroupId) -> promises = taskGroups[taskGroupId].taskIds.map (taskId) -> {maxOffset, path, initialDataLoaded} = tasks[taskId] if initialDataLoaded - xhr = fetchData(taskId, path, maxOffset, maxOffset + logRequestLength) + xhr = fetchData(taskId, path, maxOffset, logRequestLength) xhr.done ({data, offset, nextOffset}) -> if data.length > 0 nextOffset = nextOffset || offset + data.length @@ -109,7 +109,7 @@ taskGroupFetchPrevious = (taskGroupId) -> promises = taskGroups[taskGroupId].taskIds.map (taskId) -> {minOffset, path, initialDataLoaded} = tasks[taskId] if minOffset > 0 and initialDataLoaded - xhr = fetchData(taskId, path, Math.max(minOffset - logRequestLength, 0), Math.min(logRequestLength, minOffset - logRequestLength)) + xhr = fetchData(taskId, path, Math.max(minOffset - logRequestLength, 0), Math.min(logRequestLength, minOffset)) xhr.done ({data, offset, nextOffset}) -> if data.length > 0 nextOffset = nextOffset || offset + data.length From f40047953eb4ef7c503ccc594617f4bf2ffa71ea Mon Sep 17 00:00:00 2001 From: tpetr Date: Tue, 29 Mar 2016 18:50:32 -0400 Subject: [PATCH 16/44] make sure we never request a negative length, ever! --- SingularityUI/app/actions/log.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/SingularityUI/app/actions/log.coffee b/SingularityUI/app/actions/log.coffee index c7a61a2507..b67c041683 100644 --- a/SingularityUI/app/actions/log.coffee +++ b/SingularityUI/app/actions/log.coffee @@ -3,6 +3,7 @@ Q = require 'q' { fetchTasksForRequest } = require './activeTasks' fetchData = (taskId, path, offset=undefined, length=0) -> + length = Math.max(length, 0) # API breaks if you request a negative length $.ajax url: "#{ config.apiRoot }/sandbox/#{ taskId }/read?#{$.param({path, length, offset})}" From 32a955c79893c0320e2b640c09cf556dfefde2ca Mon Sep 17 00:00:00 2001 From: tpetr Date: Wed, 30 Mar 2016 12:36:37 -0400 Subject: [PATCH 17/44] make paging work better + tweak mapDispatchToProps --- SingularityUI/app/actions/log.coffee | 20 +++--- SingularityUI/app/components/logs/Header.cjsx | 6 +- .../app/components/logs/LogContainer.cjsx | 2 +- .../app/components/logs/LogLines.cjsx | 67 ++++++------------- .../components/logs/TaskGroupContainer.cjsx | 15 +---- .../app/components/logs/TaskGroupHeader.cjsx | 9 ++- .../app/components/logs/TasksDropdown.cjsx | 3 +- SingularityUI/app/reducers/log.coffee | 43 ++++++------ 8 files changed, 65 insertions(+), 100 deletions(-) diff --git a/SingularityUI/app/actions/log.coffee b/SingularityUI/app/actions/log.coffee index b67c041683..d39eaed15c 100644 --- a/SingularityUI/app/actions/log.coffee +++ b/SingularityUI/app/actions/log.coffee @@ -140,18 +140,18 @@ taskFilesize = (taskId, filesize) -> } taskGroupTop = (taskGroupId, visible) -> - { - taskGroupId - visible - type: 'LOG_TASK_GROUP_TOP' - } + (dispatch, getState) -> + if getState().taskGroups[taskGroupId].top != visible + dispatch({taskGroupId, visible, type: 'LOG_TASK_GROUP_TOP'}) + if visible + dispatch(taskGroupFetchPrevious(taskGroupId)) taskGroupBottom = (taskGroupId, visible) -> - { - taskGroupId - visible - type: 'LOG_TASK_GROUP_BOTTOM' - } + (dispatch, getState) -> + if getState().taskGroups[taskGroupId].bottom != visible + dispatch({taskGroupId, visible, type: 'LOG_TASK_GROUP_BOTTOM'}) + if visible + dispatch(taskGroupFetchNext(taskGroupId)) clickPermalink = (offset) -> { diff --git a/SingularityUI/app/components/logs/Header.cjsx b/SingularityUI/app/components/logs/Header.cjsx index f0c2e0d762..5b977c4ab3 100644 --- a/SingularityUI/app/components/logs/Header.cjsx +++ b/SingularityUI/app/components/logs/Header.cjsx @@ -13,6 +13,7 @@ class Header extends React.Component taskIdCount: React.PropTypes.number.isRequired viewMode: React.PropTypes.string.isRequired + switchViewMode: React.PropTypes.func.isRequired scrollToBottom: React.PropTypes.func.isRequired scrollToTop: React.PropTypes.func.isRequired @@ -77,9 +78,6 @@ mapStateToProps = (state) -> viewMode: state.viewMode requestId: state.activeRequest.requestId -mapDispatchToProps = (dispatch) -> - switchViewMode: (viewMode) -> dispatch(switchViewMode(viewMode)) - scrollToBottom: -> dispatch(scrollToBottom()) - scrollToTop: -> dispatch(scrollToTop()) +mapDispatchToProps = { switchViewMode, scrollToBottom, scrollToTop } module.exports = connect(mapStateToProps, mapDispatchToProps)(Header) diff --git a/SingularityUI/app/components/logs/LogContainer.cjsx b/SingularityUI/app/components/logs/LogContainer.cjsx index 65a7384788..14e1ae6866 100644 --- a/SingularityUI/app/components/logs/LogContainer.cjsx +++ b/SingularityUI/app/components/logs/LogContainer.cjsx @@ -20,7 +20,7 @@ class LogContainer extends React.Component render: ->
      - +
      {@renderTaskGroups()} diff --git a/SingularityUI/app/components/logs/LogLines.cjsx b/SingularityUI/app/components/logs/LogLines.cjsx index 26e9a6f8e5..a6ae230996 100644 --- a/SingularityUI/app/components/logs/LogLines.cjsx +++ b/SingularityUI/app/components/logs/LogLines.cjsx @@ -7,8 +7,6 @@ LogLines = require '../../collections/LogLines' { connect } = require 'react-redux' { taskGroupTop, taskGroupBottom } = require '../../actions/log' -SCROLL_THRESHOLD = 10 - sum = (numbers) -> total = 0 for n in numbers @@ -17,10 +15,8 @@ sum = (numbers) -> class LogLines extends React.Component @propTypes: - onEnterTop: React.PropTypes.func.isRequired - onEnterBottom: React.PropTypes.func.isRequired - onLeaveTop: React.PropTypes.func.isRequired - onLeaveBottom: React.PropTypes.func.isRequired + taskGroupTop: React.PropTypes.func.isRequired + taskGroupBottom: React.PropTypes.func.isRequired taskGroupId: React.PropTypes.number.isRequired logLines: React.PropTypes.array.isRequired @@ -32,25 +28,16 @@ class LogLines extends React.Component bytesRemainingAfter: React.PropTypes.number.isRequired activeColor: React.PropTypes.string.isRequired - constructor: (props) -> - super(props) - @state = { - atTop: false - atBottom: false - } - componentDidMount: -> window.addEventListener 'resize', @handleScroll componentWillUnmount: -> window.removeEventListener 'resize', @handleScroll - componentWillUpdate: -> - @shouldScrollToBottom = @refs.tailContents.scrollTop + @refs.tailContents.offsetHeight is @refs.tailContents.scrollHeight - componentDidUpdate: (prevProps, prevState) -> - if @shouldScrollToBottom - @refs.tailContents.scrollTop = @refs.tailContents.scrollHeight + if prevProps.updatedAt isnt @props.updatedAt + if @props.prependedLineCount > 0 + @refs.tailContents.scrollTop += 20 * @props.prependedLineCount renderLoadingPrevious: -> if @props.initialDataLoaded @@ -76,29 +63,17 @@ class LogLines extends React.Component
      Loading more... ({Humanize.filesize(@props.bytesRemainingAfter)} remaining)
      handleScroll: => - changedState = false - newState = {} - - if @state.atTop and @refs.tailContents.scrollTop > SCROLL_THRESHOLD - changedState = true - newState.atTop = false - @props.onLeaveTop(@props.taskGroupId) - else if not @state.atTop and @refs.tailContents.scrollTop < SCROLL_THRESHOLD - changedState = true - newState.atTop = true - @props.onEnterTop(@props.taskGroupId) - - if @state.atBottom and @refs.tailContents.scrollTop + @refs.tailContents.clientHeight < @refs.tailContents.scrollHeight - SCROLL_THRESHOLD - changedState = true - newState.atBottom = false - @props.onLeaveBottom(@props.taskGroupId) - else if not @state.atBottom and @refs.tailContents.scrollTop + @refs.tailContents.clientHeight > @refs.tailContents.scrollHeight - SCROLL_THRESHOLD - changedState = true - newState.atBottom = true - @props.onEnterBottom(@props.taskGroupId) - - if changedState - @setState Object.assign({}, @state, newState) + {scrollTop, scrollHeight, clientHeight} = @refs.tailContents + + if scrollTop < clientHeight + @props.taskGroupTop(@props.taskGroupId, true) + else + @props.taskGroupTop(@props.taskGroupId, false) + + if scrollTop + clientHeight > scrollHeight - clientHeight + @props.taskGroupBottom(@props.taskGroupId, true) + else + @props.taskGroupBottom(@props.taskGroupId, false) render: ->
      @@ -114,17 +89,17 @@ mapStateToProps = (state, ownProps) -> tasks = taskGroup.taskIds.map (taskId) -> state.tasks[taskId] logLines: taskGroup.logLines + updatedAt: taskGroup.updatedAt + prependedLineCount: taskGroup.prependedLineCount activeColor: state.activeColor + top: taskGroup.top + bottom: taskGroup.bottom initialDataLoaded: _.all(_.pluck(tasks, 'initialDataLoaded')) reachedStartOfFile: _.all(tasks.map (task) -> task.minOffset is 0) reachedEndOfFile: _.all(tasks.map (task) -> task.maxOffset >= task.filesize) bytesRemainingBefore: sum(_.pluck(tasks, 'minOffset')) bytesRemainingAfter: sum(tasks.map (task) -> Math.max(task.filesize - task.maxOffset, 0)) -mapDispatchToProps = (dispatch, ownProps) -> - onEnterTop: -> dispatch(taskGroupTop(ownProps.taskGroupId, true)) - onLeaveTop: -> dispatch(taskGroupTop(ownProps.taskGroupId, false)) - onEnterBottom: -> dispatch(taskGroupBottom(ownProps.taskGroupId, true)) - onLeaveBottom: -> dispatch(taskGroupBottom(ownProps.taskGroupId, false)) +mapDispatchToProps = { taskGroupTop, taskGroupBottom } module.exports = connect(mapStateToProps, mapDispatchToProps)(LogLines) diff --git a/SingularityUI/app/components/logs/TaskGroupContainer.cjsx b/SingularityUI/app/components/logs/TaskGroupContainer.cjsx index 9297c059b2..34f0ef0bc4 100644 --- a/SingularityUI/app/components/logs/TaskGroupContainer.cjsx +++ b/SingularityUI/app/components/logs/TaskGroupContainer.cjsx @@ -8,28 +8,17 @@ classNames = require 'classnames' class TaskGroupContainer extends React.Component @propTypes: taskGroupId: React.PropTypes.number.isRequired - path: React.PropTypes.string.isRequired taskGroupCount: React.PropTypes.number.isRequired render: -> className = "col-md-#{ 12 / @props.taskGroupCount } tail-column"
      - +
      -mapStateToProps = (state, ownProps) -> - taskGroup = state.taskGroups[ownProps.taskGroupId] - tasks = taskGroup.taskIds.map (taskId) -> state.tasks[taskId] - - taskIds: taskGroup.taskIds - logLines: taskGroup.logLines - search: taskGroup.search - ready: taskGroup.ready +mapStateToProps = (state) -> taskGroupCount: state.taskGroups.length - permalinkEnabled: tasks.length is 1 - activeColor: state.activeColor - path: state.path module.exports = connect(mapStateToProps)(TaskGroupContainer) \ No newline at end of file diff --git a/SingularityUI/app/components/logs/TaskGroupHeader.cjsx b/SingularityUI/app/components/logs/TaskGroupHeader.cjsx index 6bf9ad747b..d403c6fbbd 100644 --- a/SingularityUI/app/components/logs/TaskGroupHeader.cjsx +++ b/SingularityUI/app/components/logs/TaskGroupHeader.cjsx @@ -2,8 +2,11 @@ React = require 'react' { getInstanceNumberFromTaskId } = require '../../utils' +{ connect } = require 'react-redux' + class TaskGroupHeader extends React.Component @propTypes: + taskGroupId: React.PropTypes.number.isRequired taskIds: React.PropTypes.array.isRequired toggleLegend: -> @@ -26,6 +29,8 @@ class TaskGroupHeader extends React.Component {@renderInstanceInfo()} {@renderTaskLegend()}
      - # TODO: renderLegend() -module.exports = TaskGroupHeader \ No newline at end of file +mapStateToProps = (state, ownProps) -> + taskIds: state.taskGroups[ownProps.taskGroupId].taskIds + +module.exports = connect(mapStateToProps)(TaskGroupHeader) \ No newline at end of file diff --git a/SingularityUI/app/components/logs/TasksDropdown.cjsx b/SingularityUI/app/components/logs/TasksDropdown.cjsx index e426c3272d..7043d93fcf 100644 --- a/SingularityUI/app/components/logs/TasksDropdown.cjsx +++ b/SingularityUI/app/components/logs/TasksDropdown.cjsx @@ -37,7 +37,6 @@ mapStateToProps = (state) -> activeTasks: state.activeRequest.activeTasks taskIds: state.tasks -mapDispatchToProps = (dispatch) -> - onToggleViewingInstance: (taskId) -> dispatch(toggleTaskLog(taskId)) +mapDispatchToProps = { toggleTaskLog } module.exports = connect(mapStateToProps, mapDispatchToProps)(TasksDropdown) \ No newline at end of file diff --git a/SingularityUI/app/reducers/log.coffee b/SingularityUI/app/reducers/log.coffee index bc4daeb975..430dfd7c65 100644 --- a/SingularityUI/app/reducers/log.coffee +++ b/SingularityUI/app/reducers/log.coffee @@ -2,6 +2,19 @@ { getInstanceNumberFromTaskId } = require '../utils' +buildTaskGroup = (taskIds, search) -> + { + taskIds, + search, + logLines: [], + prependedLineCount: 0, + updatedAt: +new Date(), + top: false, + bottom: false, + ready: false + pendingRequests: false + } + updateTask = (state, taskId, update) -> newState = Object.assign({}, state) newState[taskId] = Object.assign({}, state[taskId], update) @@ -13,8 +26,7 @@ updateTaskGroup = (state, taskGroupId, update) -> return newState filterLogLines = (lines, search) -> - _.filter lines, ({data}) -> - new RegExp(search).test(data) + _.filter lines, ({data}) -> new RegExp(search).test(data) tasks = (state={}, action) -> if action.type is 'LOG_INIT' @@ -72,26 +84,9 @@ tasks = (state={}, action) -> taskGroups = (state=[], action) -> if action.type is 'LOG_INIT' - return action.taskIdGroups.map (taskIds) -> { - taskIds, - logLines: [], - top: false, - bottom: false, - search: action.search, - ready: false - pendingRequests: false - } + return action.taskIdGroups.map (taskIds) -> buildTaskGroup(taskIds, action.search) else if action.type is 'LOG_ADD_TASK_GROUP' - newState = state.concat({ - taskIds: action.taskIds, - logLines: [], - top: false, - bottom: false, - search: action.search, - ready: false - pendingRequests: false - }) - + newState = state.concat(buildTaskGroup(action.taskIds, action.search)) return _.sortBy(newState, (taskGroup) -> getInstanceNumberFromTaskId(taskGroup.taskIds[0])) else if action.type is 'LOG_REMOVE_TASK' newState = [] @@ -130,16 +125,20 @@ taskGroups = (state=[], action) -> if taskGroup.search lines = filterLogLines(lines, taskGroup.search) + prependedLineCount = 0 + updatedAt = +new Date() + if action.append logLines = state[action.taskGroupId].logLines.concat(lines) if logLines.length > action.maxLines logLines = logLines.slice(logLines.length - action.maxLines) else logLines = lines.concat(state[action.taskGroupId].logLines) + prependedLineCount = Math.min(lines.length, action.maxLines) if logLines.length > action.maxLines logLines = logLines.slice(0, action.maxLines) - return updateTaskGroup(state, action.taskGroupId, {logLines}) + return updateTaskGroup(state, action.taskGroupId, {logLines, prependedLineCount, updatedAt}) return newState return state From b666f51ece7675d46f5a50a41a44261f6a27de1f Mon Sep 17 00:00:00 2001 From: tpetr Date: Sun, 3 Apr 2016 14:47:56 -0400 Subject: [PATCH 18/44] more work --- SingularityUI/app/actions/log.coffee | 28 +-- .../app/components/logs/ColorDropdown.cjsx | 1 - SingularityUI/app/components/logs/Header.cjsx | 2 +- .../app/components/logs/LogLine.cjsx | 3 +- .../app/components/logs/LogLines.cjsx | 19 +- .../app/components/logs/TasksDropdown.cjsx | 6 +- SingularityUI/app/reducers/index.coffee | 44 +++- SingularityUI/app/reducers/log.coffee | 184 -------------- SingularityUI/app/reducers/taskGroups.coffee | 233 ++++++++++++++++++ SingularityUI/app/reducers/taskIds.coffee | 17 ++ 10 files changed, 319 insertions(+), 218 deletions(-) delete mode 100644 SingularityUI/app/reducers/log.coffee create mode 100644 SingularityUI/app/reducers/taskGroups.coffee create mode 100644 SingularityUI/app/reducers/taskIds.coffee diff --git a/SingularityUI/app/actions/log.coffee b/SingularityUI/app/actions/log.coffee index d39eaed15c..1dcd3ef2c4 100644 --- a/SingularityUI/app/actions/log.coffee +++ b/SingularityUI/app/actions/log.coffee @@ -48,11 +48,9 @@ init = (requestId, taskIdGroups, path, search) -> type: 'LOG_INIT' } -addTaskGroup = (path, search, taskIds) -> +addTaskGroup = (taskIds) -> { - path taskIds - search type: 'LOG_ADD_TASK_GROUP' } @@ -73,9 +71,10 @@ taskGroupReady = (taskGroupId) -> updateFilesizes = -> (dispatch, getState) -> - for taskId, {path} of getState().tasks - fetchData(taskId, path).done ({offset}) -> - dispatch(taskFilesize(taskId, offset)) + getState().taskGroups.map (taskGroup) -> + taskGroup.tasks.map ({taskId, path}) -> + fetchData(taskId, path).done ({offset}) -> + dispatch(taskFilesize(taskId, offset)) updateGroups = -> (dispatch, getState) -> @@ -88,10 +87,9 @@ updateGroups = -> taskGroupFetchNext = (taskGroupId) -> (dispatch, getState) -> - {tasks, taskGroups, logRequestLength, maxLines} = getState() + {taskGroups, logRequestLength, maxLines} = getState() - promises = taskGroups[taskGroupId].taskIds.map (taskId) -> - {maxOffset, path, initialDataLoaded} = tasks[taskId] + promises = taskGroups[taskGroupId].tasks.map ({taskId, maxOffset, path, initialDataLoaded}) -> if initialDataLoaded xhr = fetchData(taskId, path, maxOffset, logRequestLength) xhr.done ({data, offset, nextOffset}) -> @@ -105,10 +103,9 @@ taskGroupFetchNext = (taskGroupId) -> taskGroupFetchPrevious = (taskGroupId) -> (dispatch, getState) -> - {tasks, taskGroups, logRequestLength, maxLines} = getState() + {taskGroups, logRequestLength, maxLines} = getState() - promises = taskGroups[taskGroupId].taskIds.map (taskId) -> - {minOffset, path, initialDataLoaded} = tasks[taskId] + promises = taskGroups[taskGroupId].tasks.map ({taskId, minOffset, path, initialDataLoaded}) -> if minOffset > 0 and initialDataLoaded xhr = fetchData(taskId, path, Math.max(minOffset - logRequestLength, 0), Math.min(logRequestLength, minOffset)) xhr.done ({data, offset, nextOffset}) -> @@ -185,13 +182,12 @@ setCurrentSearch = (newSearch) -> toggleTaskLog = (taskId) -> (dispatch, getState) -> - {search, path, tasks, viewMode} = getState() - if tasks[taskId] - if Object.keys(tasks).length > 1 + {search, path, taskIds, viewMode} = getState() + if taskIds.length > 0 and taskId in taskIds dispatch({taskId, type: 'LOG_REMOVE_TASK'}) else if viewMode is 'split' - dispatch(addTaskGroup(path, search, [taskId])) + dispatch(addTaskGroup(path, [taskId])) taskGroupId = getState().taskGroups.length - 1 else taskGroupId = 0 diff --git a/SingularityUI/app/components/logs/ColorDropdown.cjsx b/SingularityUI/app/components/logs/ColorDropdown.cjsx index 9404129d3d..5b0f6427b6 100644 --- a/SingularityUI/app/components/logs/ColorDropdown.cjsx +++ b/SingularityUI/app/components/logs/ColorDropdown.cjsx @@ -19,7 +19,6 @@ class ColorDropdown extends React.Component render: -> - console.log @props
      mapStateToProps = (state) -> - taskIdCount: Object.keys(state.tasks).length + taskIdCount: state.taskIds.length path: state.path viewMode: state.viewMode requestId: state.activeRequest.requestId diff --git a/SingularityUI/app/components/logs/LogLine.cjsx b/SingularityUI/app/components/logs/LogLine.cjsx index 095c33762a..86e45b6cc3 100644 --- a/SingularityUI/app/components/logs/LogLine.cjsx +++ b/SingularityUI/app/components/logs/LogLine.cjsx @@ -57,8 +57,7 @@ class LogLine extends React.Component
      - {@props.offset} | - {@highlightContent(@props.content)} + {@props.offset} | {@props.timestamp} | {@highlightContent(@props.content)}
      diff --git a/SingularityUI/app/components/logs/LogLines.cjsx b/SingularityUI/app/components/logs/LogLines.cjsx index a6ae230996..d480fc7303 100644 --- a/SingularityUI/app/components/logs/LogLines.cjsx +++ b/SingularityUI/app/components/logs/LogLines.cjsx @@ -36,8 +36,8 @@ class LogLines extends React.Component componentDidUpdate: (prevProps, prevState) -> if prevProps.updatedAt isnt @props.updatedAt - if @props.prependedLineCount > 0 - @refs.tailContents.scrollTop += 20 * @props.prependedLineCount + if @props.prependedLineCount > 0 or @props.linesRemovedFromTop > 0 + @refs.tailContents.scrollTop += 20 * (@props.prependedLineCount - @props.linesRemovedFromTop) renderLoadingPrevious: -> if @props.initialDataLoaded @@ -47,12 +47,13 @@ class LogLines extends React.Component
      Loading previous... ({Humanize.filesize(@props.bytesRemainingBefore)} remaining)
      renderLogLines: -> - @props.logLines.map ({data, offset, taskId}) => + @props.logLines.map ({data, offset, taskId, timestamp}) => renderLoadingMore: -> @@ -86,19 +87,19 @@ class LogLines extends React.Component mapStateToProps = (state, ownProps) -> taskGroup = state.taskGroups[ownProps.taskGroupId] - tasks = taskGroup.taskIds.map (taskId) -> state.tasks[taskId] logLines: taskGroup.logLines updatedAt: taskGroup.updatedAt prependedLineCount: taskGroup.prependedLineCount + linesRemovedFromTop: taskGroup.linesRemovedFromTop activeColor: state.activeColor top: taskGroup.top bottom: taskGroup.bottom - initialDataLoaded: _.all(_.pluck(tasks, 'initialDataLoaded')) - reachedStartOfFile: _.all(tasks.map (task) -> task.minOffset is 0) - reachedEndOfFile: _.all(tasks.map (task) -> task.maxOffset >= task.filesize) - bytesRemainingBefore: sum(_.pluck(tasks, 'minOffset')) - bytesRemainingAfter: sum(tasks.map (task) -> Math.max(task.filesize - task.maxOffset, 0)) + initialDataLoaded: _.all(_.pluck(taskGroup.tasks, 'initialDataLoaded')) + reachedStartOfFile: _.all(taskGroup.tasks.map ({minOffset}) -> minOffset is 0) + reachedEndOfFile: _.all(taskGroup.tasks.map ({maxOffset, filesize}) -> maxOffset >= filesize) + bytesRemainingBefore: sum(_.pluck(taskGroup.tasks, 'minOffset')) + bytesRemainingAfter: sum(taskGroup.tasks.map ({filesize, maxOffset}) -> Math.max(filesize - maxOffset, 0)) mapDispatchToProps = { taskGroupTop, taskGroupBottom } diff --git a/SingularityUI/app/components/logs/TasksDropdown.cjsx b/SingularityUI/app/components/logs/TasksDropdown.cjsx index 7043d93fcf..35139a38a5 100644 --- a/SingularityUI/app/components/logs/TasksDropdown.cjsx +++ b/SingularityUI/app/components/logs/TasksDropdown.cjsx @@ -11,8 +11,8 @@ class TasksDropdown extends React.Component if @props.activeTasks and @props.taskIds tasks = _.sortBy(@props.activeTasks, (t) => t.taskId.instanceNo).map (task, i) =>
    • - @props.onToggleViewingInstance(task.taskId.id)}> - + @props.toggleTaskLog(task.taskId.id)}> + Instance {task.taskId.instanceNo}
    • @@ -35,7 +35,7 @@ class TasksDropdown extends React.Component mapStateToProps = (state) -> activeTasks: state.activeRequest.activeTasks - taskIds: state.tasks + taskIds: state.taskIds mapDispatchToProps = { toggleTaskLog } diff --git a/SingularityUI/app/reducers/index.coffee b/SingularityUI/app/reducers/index.coffee index ee8476690d..7d8b9e9085 100644 --- a/SingularityUI/app/reducers/index.coffee +++ b/SingularityUI/app/reducers/index.coffee @@ -1,4 +1,44 @@ { combineReducers } = require 'redux' -log = require './log' -module.exports = log \ No newline at end of file +taskGroups = require './taskGroups' +taskIds = require './taskIds' + +path = (state='', action) -> + if action.type is 'LOG_INIT' + return action.path + return state + +activeColor = (state='default', action) -> + if action.type is 'LOG_INIT' + return window.localStorage.logColor || 'default' + else if action.type is 'LOG_SELECT_COLOR' + window.localStorage.logColor = action.color + return action.color + return state + +colors = (state=[]) -> state + +viewMode = (state='custom', action) -> + if action.type is 'LOG_SWITCH_VIEW_MODE' + return action.viewMode + return state + +search = (state='', action) -> + if action.type is 'LOG_INIT' + return action.search + return state + +logRequestLength = (state=30000, action) -> + return state + +maxLines = (state=1000, action) -> + return state + +activeRequest = (state={}, action) -> + if action.type is 'LOG_INIT' + return Object.assign({}, state, {requestId: action.requestId}) + if action.type is 'REQUEST_ACTIVE_TASKS' + return Object.assign({}, state, {activeTasks: action.tasks}) + return state + +module.exports = combineReducers({taskGroups, taskIds, activeRequest, path, activeColor, colors, viewMode, search, logRequestLength, maxLines}) \ No newline at end of file diff --git a/SingularityUI/app/reducers/log.coffee b/SingularityUI/app/reducers/log.coffee deleted file mode 100644 index 430dfd7c65..0000000000 --- a/SingularityUI/app/reducers/log.coffee +++ /dev/null @@ -1,184 +0,0 @@ -{ combineReducers } = require 'redux' - -{ getInstanceNumberFromTaskId } = require '../utils' - -buildTaskGroup = (taskIds, search) -> - { - taskIds, - search, - logLines: [], - prependedLineCount: 0, - updatedAt: +new Date(), - top: false, - bottom: false, - ready: false - pendingRequests: false - } - -updateTask = (state, taskId, update) -> - newState = Object.assign({}, state) - newState[taskId] = Object.assign({}, state[taskId], update) - return newState - -updateTaskGroup = (state, taskGroupId, update) -> - newState = Object.assign([], state) - newState[taskGroupId] = Object.assign({}, state[taskGroupId], update) - return newState - -filterLogLines = (lines, search) -> - _.filter lines, ({data}) -> new RegExp(search).test(data) - -tasks = (state={}, action) -> - if action.type is 'LOG_INIT' - newState = {} - for taskId in _.flatten(action.taskIdGroups) - newState[taskId] = { - minOffset: 0 - maxOffset: 0 - filesize: 0 - initialDataLoaded: false - } - return newState - else if action.type is 'LOG_ADD_TASK_GROUP' - newState = Object.assign({}, state) - for taskId in action.taskIds - newState[taskId] = { - path: action.path.replace('$TASK_ID', taskId) - minOffset: 0 - maxOffset: 0 - filesize: 0 - initialDataLoaded: false - } - return newState - else if action.type is 'LOG_REMOVE_TASK' - newState = Object.assign({}, state) - delete newState[action.taskId] - return newState - else if action.type is 'LOG_TASK_INIT' - return updateTask(state, action.taskId, { - path: action.path - minOffset: action.offset - maxOffset: action.offset - filesize: action.offset - initialDataLoaded: true - }) - else if action.type is 'LOG_TASK_DATA' - return updateTask(state, action.taskId, { - minOffset: Math.min(state[action.taskId].minOffset, action.offset) - maxOffset: Math.max(state[action.taskId].maxOffset, action.nextOffset) - filesize: Math.max(state[action.taskId].filesize, action.nextOffset) - }) - else if action.type is 'LOG_TASK_FILESIZE' - return updateTask(state, action.taskId, {filesize: action.filesize}) - else if action.type is 'LOG_SCROLL_TO_TOP' - newState = {} - for taskId of state - newState[taskId] = Object.assign({}, state[taskId], {minOffset: 0, maxOffset: 0}) - return newState - else if action.type is 'LOG_SCROLL_TO_BOTTOM' - newState = {} - for taskId of state - newState[taskId] = Object.assign({}, state[taskId], {minOffset: state[taskId].filesize, maxOffset: state[taskId].filesize}) - return newState - return state - -taskGroups = (state=[], action) -> - if action.type is 'LOG_INIT' - return action.taskIdGroups.map (taskIds) -> buildTaskGroup(taskIds, action.search) - else if action.type is 'LOG_ADD_TASK_GROUP' - newState = state.concat(buildTaskGroup(action.taskIds, action.search)) - return _.sortBy(newState, (taskGroup) -> getInstanceNumberFromTaskId(taskGroup.taskIds[0])) - else if action.type is 'LOG_REMOVE_TASK' - newState = [] - for taskGroup in state - if action.taskId in taskGroup.taskIds - if taskGroup.taskIds.length is 1 - continue - else - newTaskGroup = Object.assign({}, taskGroup) - newTaskGroup.taskIds = taskGroup.taskIds.filter (taskId) -> taskId isnt action.taskId - newTaskGroup.logLines = taskGroup.logLines.filter (logLine) -> logLine.taskId isnt action.taskId - newState.push(newTaskGroup) - else - newState.push(taskGroup) - return newState - else if action.type is 'LOG_TASK_GROUP_REQUEST_START' - return updateTaskGroup(state, action.taskGroupId, {pendingRequests: true}) - else if action.type is 'LOG_TASK_GROUP_REQUEST_END' - return updateTaskGroup(state, action.taskGroupId, {pendingRequests: false}) - else if action.type is 'LOG_TASK_GROUP_TOP' - return updateTaskGroup(state, action.taskGroupId, {top: action.visible}) - else if action.type is 'LOG_TASK_GROUP_BOTTOM' - return updateTaskGroup(state, action.taskGroupId, {bottom: action.visible}) - else if action.type is 'LOG_TASK_GROUP_READY' - return updateTaskGroup(state, action.taskGroupId, {ready: true}) - else if action.type in ['LOG_SCROLL_TO_TOP', 'LOG_SCROLL_TO_BOTTOM'] - return updateTaskGroup(state, action.taskGroupId, {logLines: []}) - else if action.type is 'LOG_TASK_DATA' - taskGroup = state[action.taskGroupId] - - offset = action.offset - lines = _.initial(action.data.match /[^\n]*(\n|$)/g).map (data) -> - offset += data.length - {data, offset: offset - data.length, taskId: action.taskId} - - if taskGroup.search - lines = filterLogLines(lines, taskGroup.search) - - prependedLineCount = 0 - updatedAt = +new Date() - - if action.append - logLines = state[action.taskGroupId].logLines.concat(lines) - if logLines.length > action.maxLines - logLines = logLines.slice(logLines.length - action.maxLines) - else - logLines = lines.concat(state[action.taskGroupId].logLines) - prependedLineCount = Math.min(lines.length, action.maxLines) - if logLines.length > action.maxLines - logLines = logLines.slice(0, action.maxLines) - - return updateTaskGroup(state, action.taskGroupId, {logLines, prependedLineCount, updatedAt}) - - return newState - return state - -path = (state='', action) -> - if action.type is 'LOG_INIT' - return action.path - return state - -activeColor = (state='default', action) -> - if action.type is 'LOG_INIT' - return window.localStorage.logColor || 'default' - else if action.type is 'LOG_SELECT_COLOR' - window.localStorage.logColor = action.color - return action.color - return state - -colors = (state=[]) -> state - -viewMode = (state='custom', action) -> - if action.type is 'LOG_SWITCH_VIEW_MODE' - return action.viewMode - return state - -search = (state='', action) -> - if action.type is 'LOG_INIT' - return action.search - return state - -logRequestLength = (state=30000, action) -> - return state - -maxLines = (state=1000, action) -> - return state - -activeRequest = (state={}, action) -> - if action.type is 'LOG_INIT' - return Object.assign({}, state, {requestId: action.requestId}) - if action.type is 'REQUEST_ACTIVE_TASKS' - return Object.assign({}, state, {activeTasks: action.tasks}) - return state - -module.exports = combineReducers({tasks, taskGroups, activeRequest, path, activeColor, colors, viewMode, search, logRequestLength, maxLines}) \ No newline at end of file diff --git a/SingularityUI/app/reducers/taskGroups.coffee b/SingularityUI/app/reducers/taskGroups.coffee new file mode 100644 index 0000000000..05c4dda14a --- /dev/null +++ b/SingularityUI/app/reducers/taskGroups.coffee @@ -0,0 +1,233 @@ +{ combineReducers } = require 'redux' + +{ getInstanceNumberFromTaskId } = require '../utils' + +moment = require 'moment' + +buildTask = (taskId, offset=0) -> + { + taskId + minOffset: offset + maxOffset: offset + filesize: offset + initialDataLoaded: false + } + +buildTaskGroup = (taskIds, search) -> + taskGroup = { + search + logLines: [] + prependedLineCount: 0 + linesRemovedFromTop: 0 + updatedAt: +new Date() + top: false + bottom: false + ready: false + pendingRequests: false + tasks: taskIds.map(buildTask) + taskIdLookup: buildTaskLookup(taskIds) + } + +buildTaskLookup = (tasks) -> + lookup = {} + tasks.map (taskId, i) -> lookup[taskId] = i + return lookup + +updateTask = (state, taskId, update) -> + newTasks = Object.assign([], state.tasks) + index = state.taskIdLookup[taskId] + newTasks[index] = Object.assign({}, state.tasks[index], update) + newState = Object.assign({}, state) + newState.tasks = newTasks + return newState + +updateTaskGroup = (state, taskGroupId, update) -> + newState = Object.assign([], state) + newState[taskGroupId] = Object.assign({}, state[taskGroupId], update) + return newState + +filterLogLines = (lines, search) -> + _.filter lines, ({data}) -> new RegExp(search).test(data) + + +ACTIONS = { + # The logger is being initialized + LOG_INIT: (state, {taskIdGroups, search}) -> + return taskIdGroups.map (taskIds) -> buildTaskGroup(taskIds, search) + + # Add a group of tasks to the logger + LOG_ADD_TASK_GROUP: (state, {taskIds}) -> + newState = state.concat(buildTaskGroup(taskIds, state.search)) + return _.sortBy(newState, (taskGroup) -> getInstanceNumberFromTaskId(taskGroup.taskIds[0])) + + # Remove a task from the logger + LOG_REMOVE_TASK: (state, {taskId}) -> + newState = [] + for taskGroup in state + if taskGroup.tasks[taskId] + if taskGroup.taskIdsLookup.length is 1 + continue + + # remove task + newTasks = Object.assign({}, taskGroup.tasks) + delete newTasks[taskId] + + # update lookup map + newTaskLookup = buildTaskLookup(newTasks) + + # remove task loglines + newLogLines = taskGroup.logLines.filter (logLine) -> logLine.taskId isnt taskId + + newTaskGroup = Object.assign({}, taskGroup) + newTaskGroup.tasks = newTasks + newTaskGroup.taskIdLookup = newTaskLookup + newTaskGroup.logLines = newLogLines + + newState.push(newTaskGroup) + else + newState.push(taskGroup) + return newState + + # A task has been initialized + LOG_TASK_INIT: (state, {taskGroupId, taskId, path, offset}) -> + newTaskGroup = Object.assign({}, state[taskGroupId]) + newTaskGroup = updateTask(newTaskGroup, taskId, { + path + minOffset: offset + maxOffset: offset + filesize: offset + initialDataLoaded: true + }) + newState = Object.assign([], state) + newState[taskGroupId] = newTaskGroup + return newState + + # The logger has either entered or exited the top + LOG_TASK_GROUP_TOP: (state, {taskGroupId, visible}) -> + return updateTaskGroup(state, taskGroupId, {top: visible}) + + # The logger has either entered or exited the bottom + LOG_TASK_GROUP_BOTTOM: (state, {taskGroupId, visible}) -> + updateTaskGroup(state, taskGroupId, {bottom: visible}) + + # An entire task group is ready + LOG_TASK_GROUP_READY: (state, {taskGroupId}) -> + return updateTaskGroup(state, taskGroupId, {ready: true}) + + # The logger has been asked to scroll to the top + LOG_SCROLL_TO_TOP: (state, {}) -> + return state.map (taskGroup) -> + Object.assign({}, taskGroup, { + tasks: taskGroup.tasks.map (task) -> Object.assign({}, task, {minOffset: 0, maxOffset: 0, prependedLineCount: 0}) + logLines: [] + top: false + bottom: false + }) + + # The logger has been asked to scroll to the bottom + LOG_SCROLL_TO_BOTTOM: (state, {}) -> + return state.map (taskGroup) -> + Object.assign({}, taskGroup, { + tasks: taskGroup.tasks.map (task) -> Object.assign({}, task, {minOffset: task.filesize, maxOffset: task.filesize, prependedLineCount: 0}) + logLines: [] + top: false + bottom: false + }) + + # We've received new filesize information for a task + LOG_TASK_FILESIZE: (state, {taskGroupId, taskId, filesize}) -> + newTaskGroup = Object.assign({}, state[taskGroupId]) + newTaskGroup.tasks = updateTask(state[taskGroupId], taskId, {filesize}) + return updateTaskGroup(state, taskGroupId, newTaskGroup) + + # We've received logging data for a task + LOG_TASK_DATA: (state, {taskGroupId, taskId, offset, nextOffset, maxLines, data, append}) -> + # bail early if no data + if data.length is 0 + return state + + taskGroup = state[taskGroupId] + task = taskGroup.tasks[taskGroup.taskIdLookup[taskId]] + + # split task data into separate lines + currentOffset = offset + lines = _.initial(data.match /[^\n]*(\n|$)/g).map (data) -> + currentOffset += data.length + parsedTimestamp = moment(data) + unless parsedTimestamp.isValid() + parsedTimestamp = moment(data, 'HH:mm:ss.SSS') + if parsedTimestamp.isValid() + timestamp = parsedTimestamp.valueOf() + else + timestamp = null + {timestamp, data, offset: currentOffset - data.length, taskId} + + # backfill old timestamps + if taskGroup.logLines.length > 0 + lastTimestamp = _.last(taskGroup.logLines).timestamp + else + lastTimestamp = 0 + lines = lines.map (line) -> + if line.timestamp + lastTimestamp = line.timestamp + else + line.timestamp = lastTimestamp + return line + + newLogLines = Object.assign([], taskGroup.logLines) + + prependedLineCount = 0 + updatedAt = +new Date() + + # merge in tail + if offset > 0 and offset is task.maxOffset and not _.last(taskGroup.logLines).data.endsWith('\n') + newLastLine = Object.assign({}, _.last(taskGroup.logLines)) + + newLastLine.data = newLastLine.data + lines[0].data + + newLogLines = _.initial(newLogLines).concat(newLastLine) + lines = _.rest(lines) + + # merge in head + if offset + data.length is task.minOffset + newFirstLine = Object.assign({}, taskGroup.logLines[0]) + lastLine = _.last(lines) + unless lastLine.data.endsWith('\n') + newFirstLine.data = lastLine.data + newFirstLine.data + newFirstLine.offset = newFirstLine.offset - lastLine.data.length + lines = _.initial(lines) + + # TODO: find a better location + if taskGroup.search + lines = filterLogLines(lines, taskGroup.search) + + # merge lines + if append + newLogLines = newLogLines.concat(lines) + if newLogLines.length > maxLines + newLogLines = newLogLines.slice(newLogLines.length - maxLines) + else + newLogLines = lines.concat(newLogLines) + prependedLineCount = lines.length + if newLogLines.length > maxLines + newLogLines = newLogLines.slice(0, maxLines) + + newLogLines = _.sortBy(newLogLines, ({timestamp, offset}) -> [timestamp, offset]) + + # update state + newState = Object.assign([], state) + newState[taskGroupId] = Object.assign({}, state[taskGroupId], {logLines: newLogLines, prependedLineCount, updatedAt}) + newState[taskGroupId] = updateTask(newState[taskGroupId], taskId, { + minOffset: _.min(newLogLines.map (line) -> line.offset), + maxOffset: _.max(newLogLines.map (line) -> line.offset + line.data.length), + filesize: Math.max(task.filesize, nextOffset) + }) + + return newState +} + +module.exports = (state=[], action) -> + if action.type of ACTIONS + return ACTIONS[action.type](state, action) + else + return state \ No newline at end of file diff --git a/SingularityUI/app/reducers/taskIds.coffee b/SingularityUI/app/reducers/taskIds.coffee new file mode 100644 index 0000000000..c9be502002 --- /dev/null +++ b/SingularityUI/app/reducers/taskIds.coffee @@ -0,0 +1,17 @@ +ACTIONS = { + # The logger is being initialized + LOG_INIT: (state, {taskIdGroups}) -> + _.flatten(taskIdGroups) + # Add a group of tasks to the logger + LOG_ADD_TASK_GROUP: (state, {taskIds}) -> + state.concat(taskIds) + # Remove a task from the logger + LOG_REMOVE_TASK: (state, {taskId}) -> + _.without(state, taskId) +} + +module.exports = (state=[], action) -> + if action.type of ACTIONS + return ACTIONS[action.type](state, action) + else + return state \ No newline at end of file From f1ceb347e084dfca1d6aa9f07a00ef2a7581e91a Mon Sep 17 00:00:00 2001 From: tpetr Date: Mon, 4 Apr 2016 11:38:54 -0400 Subject: [PATCH 19/44] fix task group header --- SingularityUI/app/components/logs/TaskGroupHeader.cjsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SingularityUI/app/components/logs/TaskGroupHeader.cjsx b/SingularityUI/app/components/logs/TaskGroupHeader.cjsx index d403c6fbbd..44ba828560 100644 --- a/SingularityUI/app/components/logs/TaskGroupHeader.cjsx +++ b/SingularityUI/app/components/logs/TaskGroupHeader.cjsx @@ -31,6 +31,6 @@ class TaskGroupHeader extends React.Component
    mapStateToProps = (state, ownProps) -> - taskIds: state.taskGroups[ownProps.taskGroupId].taskIds + taskIds: _.pluck(state.taskGroups[ownProps.taskGroupId].tasks, 'taskId') module.exports = connect(mapStateToProps)(TaskGroupHeader) \ No newline at end of file From 63e4b55dbd65e344c5a75894fde1bf03b0469310 Mon Sep 17 00:00:00 2001 From: tpetr Date: Fri, 8 Apr 2016 16:25:39 -0400 Subject: [PATCH 20/44] more work --- SingularityUI/app/actions/log.coffee | 140 ++++++++--- SingularityUI/app/components/logs/Header.cjsx | 32 +-- .../app/components/logs/LoadingSpinner.cjsx | 22 ++ .../app/components/logs/LogContainer.cjsx | 36 ++- .../app/components/logs/LogLine.cjsx | 9 +- .../app/components/logs/LogLines.cjsx | 15 +- .../components/logs/TaskGroupContainer.cjsx | 32 ++- .../app/components/logs/TaskGroupHeader.cjsx | 42 +++- .../app/components/logs/TasksDropdown.cjsx | 2 +- .../app/reducers/activeRequest.coffee | 12 + SingularityUI/app/reducers/index.coffee | 14 +- SingularityUI/app/reducers/taskGroups.coffee | 219 ++++++++---------- SingularityUI/app/reducers/taskIds.coffee | 17 -- SingularityUI/app/reducers/tasks.coffee | 98 ++++++++ 14 files changed, 477 insertions(+), 213 deletions(-) create mode 100644 SingularityUI/app/components/logs/LoadingSpinner.cjsx create mode 100644 SingularityUI/app/reducers/activeRequest.coffee delete mode 100644 SingularityUI/app/reducers/taskIds.coffee create mode 100644 SingularityUI/app/reducers/tasks.coffee diff --git a/SingularityUI/app/actions/log.coffee b/SingularityUI/app/actions/log.coffee index 1dcd3ef2c4..3b37341b2a 100644 --- a/SingularityUI/app/actions/log.coffee +++ b/SingularityUI/app/actions/log.coffee @@ -7,6 +7,10 @@ fetchData = (taskId, path, offset=undefined, length=0) -> $.ajax url: "#{ config.apiRoot }/sandbox/#{ taskId }/read?#{$.param({path, length, offset})}" +fetchTaskHistory = (taskId) -> + $.ajax + url: "#{ config.apiRoot }/history/task/#{ taskId }" + initializeUsingActiveTasks = (requestId, path, search) -> (dispatch) -> deferred = Q.defer() @@ -28,12 +32,20 @@ initialize = (requestId, path, search, taskIds) -> dispatch(init(requestId, taskIdGroups, path, search)) groupPromises = taskIdGroups.map (taskIds, taskGroupId) -> - taskPromises = taskIds.map (taskId) -> + taskInitPromises = taskIds.map (taskId) -> resolvedPath = path.replace('$TASK_ID', taskId) fetchData(taskId, resolvedPath).done ({offset}) -> - dispatch(initTask(taskGroupId, taskId, offset, resolvedPath)) + dispatch(initTask(taskId, offset, resolvedPath, true)) + .error ({status}) -> + if status is 404 + app.caughtError() + dispatch(taskFileDoesNotExist(taskGroupId, taskId)) + Promise.resolve() + + taskStatusPromises = taskIds.map (taskId) -> + dispatch(updateTaskStatus(taskGroupId, taskId)) - Promise.all(taskPromises).then -> + Promise.all(taskInitPromises, taskStatusPromises).then -> dispatch(taskGroupFetchPrevious(taskGroupId)).then -> dispatch(taskGroupReady(taskGroupId)) @@ -54,27 +66,45 @@ addTaskGroup = (taskIds) -> type: 'LOG_ADD_TASK_GROUP' } -initTask = (taskGroupId, taskId, offset, path) -> +initTask = (taskId, offset, path, exists) -> { taskId - taskGroupId offset path + exists type: 'LOG_TASK_INIT' } +taskFileDoesNotExist = (taskGroupId, taskId) -> + { + taskId + taskGroupId + type: 'LOG_TASK_FILE_DOES_NOT_EXIST' + } + taskGroupReady = (taskGroupId) -> { taskGroupId type: 'LOG_TASK_GROUP_READY' } +taskHistory = (taskGroupId, taskId, taskHistory) -> + { + taskGroupId + taskId + taskHistory + type: 'LOG_TASK_HISTORY' + } + +getTasks = (taskGroup, tasks) -> + taskGroup.taskIds.map (taskId) -> tasks[taskId] + updateFilesizes = -> (dispatch, getState) -> - getState().taskGroups.map (taskGroup) -> - taskGroup.tasks.map ({taskId, path}) -> - fetchData(taskId, path).done ({offset}) -> - dispatch(taskFilesize(taskId, offset)) + { tasks } = getState() + for taskId of tasks + fetchData(taskId, tasks[taskId.path]).done ({offset}) -> + dispatch(taskFilesize(taskId, offset)) updateGroups = -> (dispatch, getState) -> @@ -85,16 +115,34 @@ updateGroups = -> if taskGroup.bottom dispatch(taskGroupFetchNext(taskGroupId)) +updateTaskStatuses = -> + (dispatch, getState) -> + {tasks, taskGroups} = getState() + taskGroups.map (taskGroup, taskGroupId) -> + getTasks(taskGroup, tasks).map ({taskId, terminated}) -> + if terminated + Promise.resolve() + else + dispatch(updateTaskStatus(taskGroupId, taskId)) + +updateTaskStatus = (taskGroupId, taskId) -> + (dispatch, getState) -> + fetchTaskHistory(taskId, ['taskUpdates']).done (data) -> + dispatch(taskHistory(taskGroupId, taskId, data)) + taskGroupFetchNext = (taskGroupId) -> (dispatch, getState) -> - {taskGroups, logRequestLength, maxLines} = getState() + {tasks, taskGroups, logRequestLength, maxLines} = getState() - promises = taskGroups[taskGroupId].tasks.map ({taskId, maxOffset, path, initialDataLoaded}) -> + taskGroup = taskGroups[taskGroupId] + + promises = getTasks(taskGroup, tasks).map ({taskId, maxOffset, path, initialDataLoaded, terminated}) -> + return Promise.resolve() if terminated if initialDataLoaded xhr = fetchData(taskId, path, maxOffset, logRequestLength) xhr.done ({data, offset, nextOffset}) -> if data.length > 0 - nextOffset = nextOffset || offset + data.length + nextOffset = offset + data.length dispatch(taskData(taskGroupId, taskId, data, offset, nextOffset, true, maxLines)) else Promise.resolve() # reject("initialDataLoaded is false for task #{taskId}") @@ -103,9 +151,12 @@ taskGroupFetchNext = (taskGroupId) -> taskGroupFetchPrevious = (taskGroupId) -> (dispatch, getState) -> - {taskGroups, logRequestLength, maxLines} = getState() + {tasks, taskGroups, logRequestLength, maxLines} = getState() + + taskGroup = taskGroups[taskGroupId] - promises = taskGroups[taskGroupId].tasks.map ({taskId, minOffset, path, initialDataLoaded}) -> + promises = getTasks(taskGroup, tasks).map (task) -> + {taskId, minOffset, path, initialDataLoaded} = task if minOffset > 0 and initialDataLoaded xhr = fetchData(taskId, path, Math.max(minOffset - logRequestLength, 0), Math.min(logRequestLength, minOffset)) xhr.done ({data, offset, nextOffset}) -> @@ -117,7 +168,7 @@ taskGroupFetchPrevious = (taskGroupId) -> Promise.all(promises) -taskData = (taskGroupId, taskId, data, offset, nextOffset, append, maxLines) -> +taskData = (taskGroupId, taskId, data, offset, nextOffset, append, maxLines, buffer) -> { taskGroupId taskId @@ -126,6 +177,7 @@ taskData = (taskGroupId, taskId, data, offset, nextOffset, append, maxLines) -> nextOffset append maxLines + buffer type: 'LOG_TASK_DATA' } @@ -174,7 +226,7 @@ switchViewMode = (newViewMode) -> dispatch({viewMode: newViewMode, type: 'LOG_SWITCH_VIEW_MODE'}) dispatch(initialize(activeRequest.requestId, path, search, taskIds)) -setCurrentSearch = (newSearch) -> +setCurrentSearch = (newSearch) -> # TODO: can we do something less heavyweight? (dispatch, getState) -> {activeRequest, path, taskGroups, currentSearch} = getState() if newSearch != currentSearch @@ -182,31 +234,56 @@ setCurrentSearch = (newSearch) -> toggleTaskLog = (taskId) -> (dispatch, getState) -> - {search, path, taskIds, viewMode} = getState() - if taskIds.length > 0 and taskId in taskIds + {search, path, tasks, viewMode} = getState() + if taskId of tasks and Object.keys(tasks).length > 1 dispatch({taskId, type: 'LOG_REMOVE_TASK'}) else if viewMode is 'split' - dispatch(addTaskGroup(path, [taskId])) - taskGroupId = getState().taskGroups.length - 1 - else - taskGroupId = 0 + dispatch(addTaskGroup([taskId])) + resolvedPath = path.replace('$TASK_ID', taskId) + fetchData(taskId, resolvedPath).done ({offset}) -> - dispatch(initTask(taskGroupId, taskId, offset, resolvedPath)) - dispatch(taskGroupFetchPrevious(taskGroupId)).then -> - dispatch(taskGroupReady(taskGroupId)) + dispatch(initTask(taskId, offset, resolvedPath, true)) + + getState().taskGroups.map (taskGroup, taskGroupId) -> + if taskId in taskGroup.taskIds + dispatch(updateTaskStatus(taskGroupId, taskId)) + dispatch(taskGroupFetchPrevious(taskGroupId)).then -> + dispatch(taskGroupReady(taskGroupId)) -scrollToTop = () -> +removeTaskGroup = (taskGroupId) -> (dispatch, getState) -> + { taskIds } = getState().taskGroups[taskGroupId] + dispatch({taskGroupId, taskIds, type: 'LOG_REMOVE_TASK_GROUP'}) + +expandTaskGroup = (taskGroupId) -> + (dispatch, getState) -> + { taskIds } = getState().taskGroups[taskGroupId] + dispatch({taskGroupId, taskIds, type: 'LOG_EXPAND_TASK_GROUP'}) + +scrollToTop = (taskGroupId) -> + (dispatch, getState) -> + { taskIds } = getState().taskGroups[taskGroupId] + dispatch({taskGroupId, taskIds, type: 'LOG_SCROLL_TO_TOP'}) + dispatch(taskGroupFetchNext(taskGroupId)) + +scrollAllToTop = () -> + (dispatch, getState) -> + dispatch({type: 'LOG_SCROLL_ALL_TO_TOP'}) getState().taskGroups.map (taskGroup, taskGroupId) -> - dispatch({taskGroupId, type: 'LOG_SCROLL_TO_TOP'}) dispatch(taskGroupFetchNext(taskGroupId)) -scrollToBottom = () -> +scrollToBottom = (taskGroupId) -> + (dispatch, getState) -> + { taskIds } = getState().taskGroups[taskGroupId] + dispatch({taskGroupId, taskIds, type: 'LOG_SCROLL_TO_BOTTOM'}) + dispatch(taskGroupFetchPrevious(taskGroupId)) + +scrollAllToBottom = () -> (dispatch, getState) -> + dispatch({type: 'LOG_SCROLL_ALL_TO_BOTTOM'}) getState().taskGroups.map (taskGroup, taskGroupId) -> - dispatch({taskGroupId, type: 'LOG_SCROLL_TO_BOTTOM'}) dispatch(taskGroupFetchPrevious(taskGroupId)) module.exports = { @@ -216,6 +293,7 @@ module.exports = { taskGroupFetchPrevious clickPermalink updateGroups + updateTaskStatuses updateFilesizes taskGroupTop taskGroupBottom @@ -224,5 +302,9 @@ module.exports = { setCurrentSearch toggleTaskLog scrollToTop + scrollAllToTop scrollToBottom + scrollAllToBottom + removeTaskGroup + expandTaskGroup } diff --git a/SingularityUI/app/components/logs/Header.cjsx b/SingularityUI/app/components/logs/Header.cjsx index 8d4ec50a82..bcf62fcbd0 100644 --- a/SingularityUI/app/components/logs/Header.cjsx +++ b/SingularityUI/app/components/logs/Header.cjsx @@ -4,18 +4,18 @@ SearchDropdown = require './SearchDropdown' TasksDropdown = require './TasksDropdown' { connect } = require 'react-redux' -{ switchViewMode, scrollToTop, scrollToBottom } = require '../../actions/log' +{ switchViewMode, scrollAllToTop, scrollAllToBottom } = require '../../actions/log' class Header extends React.Component @propTypes: requestId: React.PropTypes.string.isRequired path: React.PropTypes.string.isRequired - taskIdCount: React.PropTypes.number.isRequired + multipleTasks: React.PropTypes.bool.isRequired viewMode: React.PropTypes.string.isRequired switchViewMode: React.PropTypes.func.isRequired - scrollToBottom: React.PropTypes.func.isRequired - scrollToTop: React.PropTypes.func.isRequired + scrollAllToBottom: React.PropTypes.func.isRequired + scrollAllToTop: React.PropTypes.func.isRequired toggleHelp: -> # TODO @@ -28,21 +28,22 @@ class Header extends React.Component
  • {subpath}
  • renderViewButtons: -> - if @props.taskIdCount > 1 + if @props.multipleTasks
    renderAnchorButtons: -> - - - - - - - - + if @props.taskGroupCount > 1 + + + + + + + + render: ->
    @@ -73,11 +74,12 @@ class Header extends React.Component
    mapStateToProps = (state) -> - taskIdCount: state.taskIds.length + taskGroupCount: state.taskGroups.length + multipleTasks: state.taskGroups.length > 1 or (state.taskGroups.length > 0 and state.taskGroups[0].taskIds.length > 1) path: state.path viewMode: state.viewMode requestId: state.activeRequest.requestId -mapDispatchToProps = { switchViewMode, scrollToBottom, scrollToTop } +mapDispatchToProps = { switchViewMode, scrollAllToBottom, scrollAllToTop } module.exports = connect(mapStateToProps, mapDispatchToProps)(Header) diff --git a/SingularityUI/app/components/logs/LoadingSpinner.cjsx b/SingularityUI/app/components/logs/LoadingSpinner.cjsx new file mode 100644 index 0000000000..7a79223399 --- /dev/null +++ b/SingularityUI/app/components/logs/LoadingSpinner.cjsx @@ -0,0 +1,22 @@ +React = require 'react' +classNames = require 'classnames' + +class LoadingSpinner extends React.Component + @propTypes: + text: React.PropTypes.string + centered: React.PropTypes.bool + + render: -> + className = classNames + 'page-loader': true + centered: @props.centered + + if @props.children.length > 0 +
    +
    +

    { @props.children }

    +
    + else +
    + +module.exports = LoadingSpinner \ No newline at end of file diff --git a/SingularityUI/app/components/logs/LogContainer.cjsx b/SingularityUI/app/components/logs/LogContainer.cjsx index 14e1ae6866..9e00881b86 100644 --- a/SingularityUI/app/components/logs/LogContainer.cjsx +++ b/SingularityUI/app/components/logs/LogContainer.cjsx @@ -5,32 +5,50 @@ TaskGroupContainer = require './TaskGroupContainer' { connect } = require 'react-redux' -{ updateGroups } = require '../../actions/log' +{ updateGroups, updateTaskStatuses } = require '../../actions/log' class LogContainer extends React.Component @propTypes: - taskGroups: React.PropTypes.array.isRequired + taskGroupsCount: React.PropTypes.number.isRequired ready: React.PropTypes.bool.isRequired updateGroups: React.PropTypes.func.isRequired + updateTaskStatuses: React.PropTypes.func.isRequired renderTaskGroups: -> - @props.taskGroups.map (taskGroup, i) -> - + rows = [] + + row = [] + for i in [1..Math.min(@props.taskGroupsCount, 3)] + row.push + + rows.push row + + if @props.taskGroupsCount > 3 + row = [] + for i in [4..Math.min(@props.taskGroupsCount, 6)] + row.push + rows.push row + + rowClassName = 'row tail-row' + + if rows.length > 1 + rowClassName = 'row tail-row-half' + + rows.map (row, i) ->
    {row}
    render: ->
    +
    -
    - {@renderTaskGroups()} -
    + {@renderTaskGroups()}
    mapStateToProps = (state) -> - taskGroups: state.taskGroups + taskGroupsCount: state.taskGroups.length ready: _.all(_.pluck(state.taskGroups, 'ready')) -mapDispatchToProps = { updateGroups } +mapDispatchToProps = { updateGroups, updateTaskStatuses } module.exports = connect(mapStateToProps, mapDispatchToProps)(LogContainer) diff --git a/SingularityUI/app/components/logs/LogLine.cjsx b/SingularityUI/app/components/logs/LogLine.cjsx index 86e45b6cc3..755535361b 100644 --- a/SingularityUI/app/components/logs/LogLine.cjsx +++ b/SingularityUI/app/components/logs/LogLine.cjsx @@ -10,6 +10,7 @@ class LogLine extends React.Component isHighlighted: React.PropTypes.bool.isRequired content: React.PropTypes.string.isRequired taskId: React.PropTypes.string.isRequired + showDebugInfo: React.PropTypes.bool search: React.PropTypes.string clickPermalink: React.PropTypes.func.isRequired @@ -17,7 +18,10 @@ class LogLine extends React.Component highlightContent: (content) -> search = @props.search if not search or _.isEmpty(search) - return content + if @props.showDebugInfo + return "#{ @props.offset } | #{ @props.timestamp } | #{ content }" + else + return content regex = RegExp(search, 'g') matches = [] @@ -57,12 +61,13 @@ class LogLine extends React.Component
    - {@props.offset} | {@props.timestamp} | {@highlightContent(@props.content)} + {@highlightContent(@props.content)}
    mapStateToProps = (state, ownProps) -> search: state.search + showDebugInfo: state.showDebugInfo mapDispatchToProps = { clickPermalink } diff --git a/SingularityUI/app/components/logs/LogLines.cjsx b/SingularityUI/app/components/logs/LogLines.cjsx index d480fc7303..c711aadcb8 100644 --- a/SingularityUI/app/components/logs/LogLines.cjsx +++ b/SingularityUI/app/components/logs/LogLines.cjsx @@ -38,6 +38,8 @@ class LogLines extends React.Component if prevProps.updatedAt isnt @props.updatedAt if @props.prependedLineCount > 0 or @props.linesRemovedFromTop > 0 @refs.tailContents.scrollTop += 20 * (@props.prependedLineCount - @props.linesRemovedFromTop) + else + @handleScroll() renderLoadingPrevious: -> if @props.initialDataLoaded @@ -78,7 +80,7 @@ class LogLines extends React.Component render: ->
    -
    +
    {@renderLoadingPrevious()} {@renderLogLines()} {@renderLoadingMore()} @@ -87,6 +89,7 @@ class LogLines extends React.Component mapStateToProps = (state, ownProps) -> taskGroup = state.taskGroups[ownProps.taskGroupId] + tasks = taskGroup.taskIds.map (taskId) -> state.tasks[taskId] logLines: taskGroup.logLines updatedAt: taskGroup.updatedAt @@ -95,11 +98,11 @@ mapStateToProps = (state, ownProps) -> activeColor: state.activeColor top: taskGroup.top bottom: taskGroup.bottom - initialDataLoaded: _.all(_.pluck(taskGroup.tasks, 'initialDataLoaded')) - reachedStartOfFile: _.all(taskGroup.tasks.map ({minOffset}) -> minOffset is 0) - reachedEndOfFile: _.all(taskGroup.tasks.map ({maxOffset, filesize}) -> maxOffset >= filesize) - bytesRemainingBefore: sum(_.pluck(taskGroup.tasks, 'minOffset')) - bytesRemainingAfter: sum(taskGroup.tasks.map ({filesize, maxOffset}) -> Math.max(filesize - maxOffset, 0)) + initialDataLoaded: _.all(_.pluck(tasks, 'initialDataLoaded')) + reachedStartOfFile: _.all(tasks.map ({minOffset}) -> minOffset is 0) + reachedEndOfFile: _.all(tasks.map ({maxOffset, filesize}) -> maxOffset >= filesize) + bytesRemainingBefore: sum(_.pluck(tasks, 'minOffset')) + bytesRemainingAfter: sum(tasks.map ({filesize, maxOffset}) -> Math.max(filesize - maxOffset, 0)) mapDispatchToProps = { taskGroupTop, taskGroupBottom } diff --git a/SingularityUI/app/components/logs/TaskGroupContainer.cjsx b/SingularityUI/app/components/logs/TaskGroupContainer.cjsx index 34f0ef0bc4..8db891108d 100644 --- a/SingularityUI/app/components/logs/TaskGroupContainer.cjsx +++ b/SingularityUI/app/components/logs/TaskGroupContainer.cjsx @@ -1,6 +1,7 @@ React = require 'react' TaskGroupHeader = require './TaskGroupHeader' LogLines = require './LogLines' +LoadingSpinner = require './LoadingSpinner' classNames = require 'classnames' { connect } = require 'react-redux' @@ -8,17 +9,38 @@ classNames = require 'classnames' class TaskGroupContainer extends React.Component @propTypes: taskGroupId: React.PropTypes.number.isRequired - taskGroupCount: React.PropTypes.number.isRequired + taskGroupContainerCount: React.PropTypes.number.isRequired + + initialDataLoaded: React.PropTypes.bool.isRequired + fileExists: React.PropTypes.bool.isRequired + terminated: React.PropTypes.bool.isRequired + + getContainerWidth: -> + return (12 / @props.taskGroupContainerCount) + + renderLogLines: -> + if @props.initialDataLoaded + if @props.fileExists + return + else + return

    File does not exist

    + else + return Loading logs... render: -> - className = "col-md-#{ 12 / @props.taskGroupCount } tail-column" + className = "col-md-#{ @getContainerWidth() } tail-column"
    - + {@renderLogLines()}
    -mapStateToProps = (state) -> - taskGroupCount: state.taskGroups.length +mapStateToProps = (state, ownProps) -> + taskGroup = state.taskGroups[ownProps.taskGroupId] + tasks = taskGroup.taskIds.map (taskId) -> state.tasks[taskId] + + initialDataLoaded: _.all(_.pluck(tasks, 'logDataLoaded')) + fileExists: _.any(_.pluck(tasks, 'exists')) + terminated: _.all(_.pluck(tasks, 'terminated')) module.exports = connect(mapStateToProps)(TaskGroupContainer) \ No newline at end of file diff --git a/SingularityUI/app/components/logs/TaskGroupHeader.cjsx b/SingularityUI/app/components/logs/TaskGroupHeader.cjsx index 44ba828560..ff47ec1ec8 100644 --- a/SingularityUI/app/components/logs/TaskGroupHeader.cjsx +++ b/SingularityUI/app/components/logs/TaskGroupHeader.cjsx @@ -4,33 +4,61 @@ React = require 'react' { connect } = require 'react-redux' +{ removeTaskGroup, expandTaskGroup, scrollToTop, scrollToBottom } = require '../../actions/log' + class TaskGroupHeader extends React.Component @propTypes: taskGroupId: React.PropTypes.number.isRequired - taskIds: React.PropTypes.array.isRequired + tasks: React.PropTypes.array.isRequired toggleLegend: -> # TODO renderInstanceInfo: -> - if @props.taskIds.length > 1 - Viewing Instances {@props.taskIds.map(getInstanceNumberFromTaskId).join(', ')} + if @props.tasks.length > 1 + console.log @props.tasks + Viewing Instances {@props.tasks.map(({taskId}) -> getInstanceNumberFromTaskId(taskId)).join(', ')} else - Instance {getInstanceNumberFromTaskId(@props.taskIds[0])} + + +
    +
    + {@props.tasks[0].lastTaskStatus} +
    +
    renderTaskLegend: -> - if @props.taskIds.length > 1 + if @props.tasks.length > 1 + renderClose: -> + if @props.taskGroupsCount > 1 + @props.removeTaskGroup(@props.taskGroupId)} title="Close Task"> + + renderExpand: -> + if @props.taskGroupsCount > 1 + @props.expandTaskGroup(@props.taskGroupId)} title="Show only this Task"> + render: ->
    + {@renderClose()} + {@renderExpand()} {@renderInstanceInfo()} {@renderTaskLegend()} + + @props.scrollToBottom(@props.taskGroupId) } title="Scroll to Bottom"> + @props.scrollToTop(@props.taskGroupId) } title="Scroll to Top"> +
    mapStateToProps = (state, ownProps) -> - taskIds: _.pluck(state.taskGroups[ownProps.taskGroupId].tasks, 'taskId') + taskGroupsCount: state.taskGroups.length + tasks: state.taskGroups[ownProps.taskGroupId].taskIds.map (taskId) -> state.tasks[taskId] + +mapDispatchToProps = { scrollToTop, scrollToBottom, removeTaskGroup, expandTaskGroup } -module.exports = connect(mapStateToProps)(TaskGroupHeader) \ No newline at end of file +module.exports = connect(mapStateToProps, mapDispatchToProps)(TaskGroupHeader) \ No newline at end of file diff --git a/SingularityUI/app/components/logs/TasksDropdown.cjsx b/SingularityUI/app/components/logs/TasksDropdown.cjsx index 35139a38a5..7e009f47d6 100644 --- a/SingularityUI/app/components/logs/TasksDropdown.cjsx +++ b/SingularityUI/app/components/logs/TasksDropdown.cjsx @@ -35,7 +35,7 @@ class TasksDropdown extends React.Component mapStateToProps = (state) -> activeTasks: state.activeRequest.activeTasks - taskIds: state.taskIds + taskIds: _.flatten(_.pluck(state.taskGroups, 'taskIds')) mapDispatchToProps = { toggleTaskLog } diff --git a/SingularityUI/app/reducers/activeRequest.coffee b/SingularityUI/app/reducers/activeRequest.coffee new file mode 100644 index 0000000000..bebdf45379 --- /dev/null +++ b/SingularityUI/app/reducers/activeRequest.coffee @@ -0,0 +1,12 @@ +ACTIONS = { + LOG_INIT: (state, {requestId}) -> + return Object.assign({}, state, {requestId}) + REQUEST_ACTIVE_TASKS: (state, {tasks}) -> + return Object.assign({}, state, {activeTasks: tasks}) +} + +module.exports = (state={}, action) -> + if action.type of ACTIONS + return ACTIONS[action.type](state, action) + else + return state \ No newline at end of file diff --git a/SingularityUI/app/reducers/index.coffee b/SingularityUI/app/reducers/index.coffee index 7d8b9e9085..be7319fdcf 100644 --- a/SingularityUI/app/reducers/index.coffee +++ b/SingularityUI/app/reducers/index.coffee @@ -1,7 +1,8 @@ { combineReducers } = require 'redux' taskGroups = require './taskGroups' -taskIds = require './taskIds' +activeRequest = require './activeRequest' +tasks = require './tasks' path = (state='', action) -> if action.type is 'LOG_INIT' @@ -34,11 +35,12 @@ logRequestLength = (state=30000, action) -> maxLines = (state=1000, action) -> return state -activeRequest = (state={}, action) -> +showDebugInfo = (state=false, action) -> if action.type is 'LOG_INIT' - return Object.assign({}, state, {requestId: action.requestId}) - if action.type is 'REQUEST_ACTIVE_TASKS' - return Object.assign({}, state, {activeTasks: action.tasks}) + return Boolean(window.localStorage.showDebugInfo) || false + if action.type is 'LOG_DEBUG_INFO' + window.localStorage.showDebugInfo = action.value + return action.value return state -module.exports = combineReducers({taskGroups, taskIds, activeRequest, path, activeColor, colors, viewMode, search, logRequestLength, maxLines}) \ No newline at end of file +module.exports = combineReducers({showDebugInfo, taskGroups, tasks, activeRequest, path, activeColor, colors, viewMode, search, logRequestLength, maxLines}) \ No newline at end of file diff --git a/SingularityUI/app/reducers/taskGroups.coffee b/SingularityUI/app/reducers/taskGroups.coffee index 05c4dda14a..b81ba4cd06 100644 --- a/SingularityUI/app/reducers/taskGroups.coffee +++ b/SingularityUI/app/reducers/taskGroups.coffee @@ -4,19 +4,12 @@ moment = require 'moment' -buildTask = (taskId, offset=0) -> - { - taskId - minOffset: offset - maxOffset: offset - filesize: offset - initialDataLoaded: false - } - buildTaskGroup = (taskIds, search) -> - taskGroup = { + { + taskIds search logLines: [] + taskBuffer: {} prependedLineCount: 0 linesRemovedFromTop: 0 updatedAt: +new Date() @@ -24,22 +17,15 @@ buildTaskGroup = (taskIds, search) -> bottom: false ready: false pendingRequests: false - tasks: taskIds.map(buildTask) - taskIdLookup: buildTaskLookup(taskIds) + detectedTimestamp: false } -buildTaskLookup = (tasks) -> - lookup = {} - tasks.map (taskId, i) -> lookup[taskId] = i - return lookup - -updateTask = (state, taskId, update) -> - newTasks = Object.assign([], state.tasks) - index = state.taskIdLookup[taskId] - newTasks[index] = Object.assign({}, state.tasks[index], update) - newState = Object.assign({}, state) - newState.tasks = newTasks - return newState +resetTaskGroup = () -> { + logLines: [] + taskBuffer: {} + top: false + bottom: false +} updateTaskGroup = (state, taskGroupId, update) -> newState = Object.assign([], state) @@ -49,6 +35,14 @@ updateTaskGroup = (state, taskGroupId, update) -> filterLogLines = (lines, search) -> _.filter lines, ({data}) -> new RegExp(search).test(data) +parseLineTimestamp = (line) -> + match = line.match(/^\d{2}:\d{2}:\d{2}.\d{3}/) + if match + return moment(match, 'HH:mm:ss.SSS').valueOf() + else + return null + +buildEmptyBuffer = (taskId, offset) -> { offset, taskId, data: '' } ACTIONS = { # The logger is being initialized @@ -64,109 +58,125 @@ ACTIONS = { LOG_REMOVE_TASK: (state, {taskId}) -> newState = [] for taskGroup in state - if taskGroup.tasks[taskId] - if taskGroup.taskIdsLookup.length is 1 + if taskId in taskGroup.taskIds + if taskGroup.taskIds.length is 1 continue # remove task - newTasks = Object.assign({}, taskGroup.tasks) - delete newTasks[taskId] - - # update lookup map - newTaskLookup = buildTaskLookup(newTasks) + newTaskIds = _.without(taskGroup.taskIds, taskId) # remove task loglines newLogLines = taskGroup.logLines.filter (logLine) -> logLine.taskId isnt taskId - newTaskGroup = Object.assign({}, taskGroup) - newTaskGroup.tasks = newTasks - newTaskGroup.taskIdLookup = newTaskLookup - newTaskGroup.logLines = newLogLines - - newState.push(newTaskGroup) + newState.push(Object.assign({}, taskGroup, {tasksIds: newTasksIds, logLines: newLogLines})) else newState.push(taskGroup) return newState - # A task has been initialized - LOG_TASK_INIT: (state, {taskGroupId, taskId, path, offset}) -> - newTaskGroup = Object.assign({}, state[taskGroupId]) - newTaskGroup = updateTask(newTaskGroup, taskId, { - path - minOffset: offset - maxOffset: offset - filesize: offset - initialDataLoaded: true - }) - newState = Object.assign([], state) - newState[taskGroupId] = newTaskGroup - return newState - # The logger has either entered or exited the top LOG_TASK_GROUP_TOP: (state, {taskGroupId, visible}) -> return updateTaskGroup(state, taskGroupId, {top: visible}) # The logger has either entered or exited the bottom LOG_TASK_GROUP_BOTTOM: (state, {taskGroupId, visible}) -> - updateTaskGroup(state, taskGroupId, {bottom: visible}) + return updateTaskGroup(state, taskGroupId, {bottom: visible}) # An entire task group is ready LOG_TASK_GROUP_READY: (state, {taskGroupId}) -> return updateTaskGroup(state, taskGroupId, {ready: true}) + LOG_REMOVE_TASK_GROUP: (state, {taskGroupId}) -> + newState = [] + for i in [0..state.length-1] + unless i is taskGroupId + newState.push(state[i]) + return newState + + LOG_EXPAND_TASK_GROUP: (state, {taskGroupId}) -> + return [state[taskGroupId]] + # The logger has been asked to scroll to the top - LOG_SCROLL_TO_TOP: (state, {}) -> - return state.map (taskGroup) -> - Object.assign({}, taskGroup, { - tasks: taskGroup.tasks.map (task) -> Object.assign({}, task, {minOffset: 0, maxOffset: 0, prependedLineCount: 0}) - logLines: [] - top: false - bottom: false - }) - - # The logger has been asked to scroll to the bottom - LOG_SCROLL_TO_BOTTOM: (state, {}) -> + LOG_SCROLL_ALL_GROUPS_TO_TOP: (state) -> return state.map (taskGroup) -> - Object.assign({}, taskGroup, { - tasks: taskGroup.tasks.map (task) -> Object.assign({}, task, {minOffset: task.filesize, maxOffset: task.filesize, prependedLineCount: 0}) - logLines: [] - top: false - bottom: false - }) - - # We've received new filesize information for a task - LOG_TASK_FILESIZE: (state, {taskGroupId, taskId, filesize}) -> - newTaskGroup = Object.assign({}, state[taskGroupId]) - newTaskGroup.tasks = updateTask(state[taskGroupId], taskId, {filesize}) - return updateTaskGroup(state, taskGroupId, newTaskGroup) + Object.assign({}, taskGroup, resetTaskGroup()) + + LOG_SCROLL_TO_TOP: (state, {taskGroupId}) -> + newState = Object.assign([], state) + newState[taskGroupId] = Object.assign({}, state[taskGroupId], resetTaskGroup()) + return newState + + LOG_SCROLL_ALL_TO_TOP: (state) -> + state.map (taskGroup) -> Object.assign({}, taskGroup, resetTaskGroup()) + + LOG_SCROLL_TO_BOTTOM: (state, {taskGroupId}) -> + newState = Object.assign([], state) + newState[taskGroupId] = Object.assign({}, state[taskGroupId], resetTaskGroup()) + return newState + + LOG_SCROLL_ALL_TO_BOTTOM: (state) -> + state.map (taskGroup) -> Object.assign({}, taskGroup, resetTaskGroup()) # We've received logging data for a task LOG_TASK_DATA: (state, {taskGroupId, taskId, offset, nextOffset, maxLines, data, append}) -> + taskGroup = state[taskGroupId] + # bail early if no data - if data.length is 0 + if data.length is 0 and task.loadedData return state - taskGroup = state[taskGroupId] - task = taskGroup.tasks[taskGroup.taskIdLookup[taskId]] - - # split task data into separate lines + # split task data into separate lines, attempt to parse timestamp currentOffset = offset lines = _.initial(data.match /[^\n]*(\n|$)/g).map (data) -> currentOffset += data.length - parsedTimestamp = moment(data) - unless parsedTimestamp.isValid() - parsedTimestamp = moment(data, 'HH:mm:ss.SSS') - if parsedTimestamp.isValid() - timestamp = parsedTimestamp.valueOf() - else - timestamp = null + + timestamp = parseLineTimestamp(data) + + if timestamp + detectedTimestamp = true + {timestamp, data, offset: currentOffset - data.length, taskId} + # task buffers + taskBuffer = taskGroup.taskBuffer[taskId] || buildEmptyBuffer(taskId, 0) + + if append + if taskBuffer.offset + taskBuffer.data.length is offset + firstLine = _.first(lines) + lines = _.rest(lines) + taskBuffer = {offset: taskBuffer.offset, data: taskBuffer.data + firstLine.data, taskId} + if taskBuffer.data.endsWith('\n') + taskBuffer.timestamp = parseLineTimestamp(taskBuffer.data) + lines.unshift(taskBuffer) + taskBuffer = buildEmptyBuffer(taskId, nextOffset) + if lines.length > 0 + lastLine = _.last(lines) + if not lastLine.data.endsWith('\n') + taskBuffer = lastLine + lines = _.initial(lines) + else + if nextOffset is taskBuffer.offset + lastLine = _.last(lines) + lines = _.initial(lines) + taskBuffer = {offset: nextOffset - lastLine.data.length, data: lastLine.data + taskBuffer.data, taskId} + if lines.length > 0 + taskBuffer.timestamp = parseLineTimestamp(taskBuffer.data) + lines.push(taskBuffer) + taskBuffer = buildEmptyBuffer(taskId, offset) + if lines.length > 0 + firstLine = _.first(lines) + if firstLine.offset > 0 + taskBuffer = firstLine + lines = _.rest(lines) + + newTaskBuffer = Object.assign({}, taskGroup.taskBuffer) + newTaskBuffer[taskId] = taskBuffer + # backfill old timestamps if taskGroup.logLines.length > 0 lastTimestamp = _.last(taskGroup.logLines).timestamp else lastTimestamp = 0 + lines = lines.map (line) -> if line.timestamp lastTimestamp = line.timestamp @@ -174,34 +184,15 @@ ACTIONS = { line.timestamp = lastTimestamp return line - newLogLines = Object.assign([], taskGroup.logLines) - prependedLineCount = 0 updatedAt = +new Date() - # merge in tail - if offset > 0 and offset is task.maxOffset and not _.last(taskGroup.logLines).data.endsWith('\n') - newLastLine = Object.assign({}, _.last(taskGroup.logLines)) - - newLastLine.data = newLastLine.data + lines[0].data - - newLogLines = _.initial(newLogLines).concat(newLastLine) - lines = _.rest(lines) - - # merge in head - if offset + data.length is task.minOffset - newFirstLine = Object.assign({}, taskGroup.logLines[0]) - lastLine = _.last(lines) - unless lastLine.data.endsWith('\n') - newFirstLine.data = lastLine.data + newFirstLine.data - newFirstLine.offset = newFirstLine.offset - lastLine.data.length - lines = _.initial(lines) - - # TODO: find a better location + # search if taskGroup.search lines = filterLogLines(lines, taskGroup.search) # merge lines + newLogLines = Object.assign([], taskGroup.logLines) if append newLogLines = newLogLines.concat(lines) if newLogLines.length > maxLines @@ -212,17 +203,13 @@ ACTIONS = { if newLogLines.length > maxLines newLogLines = newLogLines.slice(0, maxLines) - newLogLines = _.sortBy(newLogLines, ({timestamp, offset}) -> [timestamp, offset]) + # sort lines by timestamp if unified view + if taskGroup.taskIds.length > 1 + newLogLines = _.sortBy(newLogLines, ({timestamp, offset}) -> [timestamp, offset]) # update state newState = Object.assign([], state) - newState[taskGroupId] = Object.assign({}, state[taskGroupId], {logLines: newLogLines, prependedLineCount, updatedAt}) - newState[taskGroupId] = updateTask(newState[taskGroupId], taskId, { - minOffset: _.min(newLogLines.map (line) -> line.offset), - maxOffset: _.max(newLogLines.map (line) -> line.offset + line.data.length), - filesize: Math.max(task.filesize, nextOffset) - }) - + newState[taskGroupId] = Object.assign({}, state[taskGroupId], {taskBuffer: newTaskBuffer, logLines: newLogLines, prependedLineCount, updatedAt}) return newState } diff --git a/SingularityUI/app/reducers/taskIds.coffee b/SingularityUI/app/reducers/taskIds.coffee deleted file mode 100644 index c9be502002..0000000000 --- a/SingularityUI/app/reducers/taskIds.coffee +++ /dev/null @@ -1,17 +0,0 @@ -ACTIONS = { - # The logger is being initialized - LOG_INIT: (state, {taskIdGroups}) -> - _.flatten(taskIdGroups) - # Add a group of tasks to the logger - LOG_ADD_TASK_GROUP: (state, {taskIds}) -> - state.concat(taskIds) - # Remove a task from the logger - LOG_REMOVE_TASK: (state, {taskId}) -> - _.without(state, taskId) -} - -module.exports = (state=[], action) -> - if action.type of ACTIONS - return ACTIONS[action.type](state, action) - else - return state \ No newline at end of file diff --git a/SingularityUI/app/reducers/tasks.coffee b/SingularityUI/app/reducers/tasks.coffee new file mode 100644 index 0000000000..3049fa382b --- /dev/null +++ b/SingularityUI/app/reducers/tasks.coffee @@ -0,0 +1,98 @@ +updateTask = (state, taskId, updates) -> + newState = Object.assign({}, state) + newState[taskId] = Object.assign({}, state[taskId], updates) + return newState + +buildTask = (taskId, offset=0) -> + { + taskId + minOffset: offset + maxOffset: offset + filesize: offset + initialDataLoaded: false + logDataLoaded: false + terminated: false + exists: false + } + +getLastTaskUpdate = (taskUpdates) -> + if taskUpdates.length > 0 + _.last(_.sortBy(taskUpdates, (taskUpdate) -> taskUpdate.timestamp)).taskState + else + return null + +isTerminalTaskState = (taskState) -> + taskState in ['TASK_FINISHED', 'TASK_KILLED', 'TASK_FAILED', 'TASK_LOST', 'TASK_ERROR'] + +ACTIONS = { + LOG_INIT: (state, {taskIdGroups}) -> + newState = {} + for taskIdGroup in taskIdGroups + for taskId in taskIdGroup + newState[taskId] = buildTask(taskId) + return newState + LOG_ADD_TASK_GROUP: (state, {taskIds}) -> + newState = Object.assign({}, state) + for taskId in taskIds + newState[taskId] = buildTask(taskId) + return newState + LOG_REMOVE_TASK: (state, {taskId}) -> + newState = Object.assign({}, state) + delete newState[taskId] + return newState + LOG_TASK_INIT: (state, {taskId, path, offset, exists}) -> + updateTask(state, taskId, { + path + exists + minOffset: offset + maxOffset: offset + filesize: offset + initialDataLoaded: true + }) + LOG_TASK_FILE_DOES_NOT_EXIST: (state, {taskId}) -> + updateTask(state, taskId, {exists: false}) + LOG_SCROLL_TO_TOP: (state, {taskIds}) -> + newState = Object.assign({}, state) + for taskId in taskIds + newState[taskId] = Object.assign({}, state[taskId], {minOffset: 0, maxOffset: 0, logDataLoaded: false}) + return newState + LOG_SCROLL_ALL_TO_TOP: (state) -> + newState = {} + for taskId of state + newState[taskId] = Object.assign({}, state[taskId], {minOffset: 0, maxOffset: 0, logDataLoaded: false}) + return newState + LOG_SCROLL_TO_BOTTOM: (state, {taskIds}) -> + newState = Object.assign({}, state) + for taskId in taskIds + newState[taskId] = Object.assign({}, state[taskId], {minOffset: state[taskId].filesize, maxOffset: state[taskId].filesize, logDataLoaded: false}) + return newState + LOG_SCROLL_ALL_TO_BOTTOM: (state) -> + newState = {} + for taskId of state + newState[taskId] = Object.assign({}, state[taskId], {minOffset: state[taskId].filesize, maxOffset: state[taskId].filesize, logDataLoaded: false}) + return newState + LOG_TASK_FILESIZE: (state, {taskId, filesize}) -> + updateTask(state, taskId, {filesize}) + LOG_TASK_DATA: (state, {taskId, offset, nextOffset}) -> + {minOffset, maxOffset, filesize} = state[taskId] + updateTask(state, taskId, {logDataLoaded: true, minOffset: Math.min(minOffset, offset), maxOffset: Math.max(maxOffset, nextOffset), filesize: Math.max(nextOffset, filesize)}) + LOG_TASK_HISTORY: (state, {taskId, taskHistory}) -> + lastTaskStatus = getLastTaskUpdate(taskHistory.taskUpdates) + updateTask(state, taskId, {lastTaskStatus, terminated: isTerminalTaskState(lastTaskStatus)}) + LOG_REMOVE_TASK_GROUP: (state, {taskIds}) -> + newState = Object.assign({}, state) + for taskId in taskIds + delete newState[taskId] + return newState + LOG_EXPAND_TASK_GROUP: (state, {taskIds}) -> + newState = {} + for taskId in taskIds + newState[taskId] = state[taskId] + return newState +} + +module.exports = (state={}, action) -> + if action.type of ACTIONS + return ACTIONS[action.type](state, action) + else + return state \ No newline at end of file From 2205730c1caf1b963592c2b995d71ea63f603f17 Mon Sep 17 00:00:00 2001 From: tpetr Date: Fri, 8 Apr 2016 16:45:22 -0400 Subject: [PATCH 21/44] fix --- .../app/components/logs/TaskGroupContainer.cjsx | 6 ++++++ SingularityUI/app/components/logs/TaskGroupHeader.cjsx | 9 ++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/SingularityUI/app/components/logs/TaskGroupContainer.cjsx b/SingularityUI/app/components/logs/TaskGroupContainer.cjsx index 8db891108d..43763e1c38 100644 --- a/SingularityUI/app/components/logs/TaskGroupContainer.cjsx +++ b/SingularityUI/app/components/logs/TaskGroupContainer.cjsx @@ -36,6 +36,12 @@ class TaskGroupContainer extends React.Component mapStateToProps = (state, ownProps) -> + unless ownProps.taskGroupId of state.taskGroups + return { + initialDataLoaded: false + fileExists: false + terminated: false + } taskGroup = state.taskGroups[ownProps.taskGroupId] tasks = taskGroup.taskIds.map (taskId) -> state.tasks[taskId] diff --git a/SingularityUI/app/components/logs/TaskGroupHeader.cjsx b/SingularityUI/app/components/logs/TaskGroupHeader.cjsx index ff47ec1ec8..cccd987c6e 100644 --- a/SingularityUI/app/components/logs/TaskGroupHeader.cjsx +++ b/SingularityUI/app/components/logs/TaskGroupHeader.cjsx @@ -18,7 +18,7 @@ class TaskGroupHeader extends React.Component if @props.tasks.length > 1 console.log @props.tasks Viewing Instances {@props.tasks.map(({taskId}) -> getInstanceNumberFromTaskId(taskId)).join(', ')} - else + else if @props.tasks.length > 0
    Instance {getInstanceNumberFromTaskId(@props.tasks[0].taskId)} @@ -28,6 +28,8 @@ class TaskGroupHeader extends React.Component {@props.tasks[0].lastTaskStatus}
    + else +
    renderTaskLegend: -> if @props.tasks.length > 1 @@ -56,6 +58,11 @@ class TaskGroupHeader extends React.Component
    mapStateToProps = (state, ownProps) -> + unless ownProps.taskGroupId of state.taskGroups + return { + taskGroupsCount: state.taskGroups.length + tasks: [] + } taskGroupsCount: state.taskGroups.length tasks: state.taskGroups[ownProps.taskGroupId].taskIds.map (taskId) -> state.tasks[taskId] From bd14778aad8154a11b33f986e7175331060738ac Mon Sep 17 00:00:00 2001 From: tpetr Date: Fri, 8 Apr 2016 17:05:20 -0400 Subject: [PATCH 22/44] add another timestamp regex --- SingularityUI/app/reducers/taskGroups.coffee | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/SingularityUI/app/reducers/taskGroups.coffee b/SingularityUI/app/reducers/taskGroups.coffee index b81ba4cd06..bd9f121270 100644 --- a/SingularityUI/app/reducers/taskGroups.coffee +++ b/SingularityUI/app/reducers/taskGroups.coffee @@ -36,10 +36,14 @@ filterLogLines = (lines, search) -> _.filter lines, ({data}) -> new RegExp(search).test(data) parseLineTimestamp = (line) -> - match = line.match(/^\d{2}:\d{2}:\d{2}.\d{3}/) + match = line.match(/^\d{2}:\d{2}:\d{2}\.\d{3}/) if match return moment(match, 'HH:mm:ss.SSS').valueOf() else + match = line.match(/^[A-Z ]+\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3})\]/) + if match + return moment(match[1], 'YYYY-MM-DD HH:mm:ss,SSS').valueOf() + return null buildEmptyBuffer = (taskId, offset) -> { offset, taskId, data: '' } From d1c813a4bbddcd0245552ccad6e6b98307ce1c39 Mon Sep 17 00:00:00 2001 From: tpetr Date: Fri, 8 Apr 2016 17:33:48 -0400 Subject: [PATCH 23/44] colors n shit --- SingularityUI/app/components/logs/LogLine.cjsx | 1 + .../app/components/logs/LogLines.cjsx | 11 ++++++++++- SingularityUI/app/reducers/taskGroups.coffee | 18 ++++++++++-------- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/SingularityUI/app/components/logs/LogLine.cjsx b/SingularityUI/app/components/logs/LogLine.cjsx index 755535361b..c63a286742 100644 --- a/SingularityUI/app/components/logs/LogLine.cjsx +++ b/SingularityUI/app/components/logs/LogLine.cjsx @@ -11,6 +11,7 @@ class LogLine extends React.Component content: React.PropTypes.string.isRequired taskId: React.PropTypes.string.isRequired showDebugInfo: React.PropTypes.bool + color: React.PropTypes.string search: React.PropTypes.string clickPermalink: React.PropTypes.func.isRequired diff --git a/SingularityUI/app/components/logs/LogLines.cjsx b/SingularityUI/app/components/logs/LogLines.cjsx index c711aadcb8..81eb229c17 100644 --- a/SingularityUI/app/components/logs/LogLines.cjsx +++ b/SingularityUI/app/components/logs/LogLines.cjsx @@ -56,7 +56,8 @@ class LogLines extends React.Component offset={offset} taskId={taskId} timestamp={timestamp} - isHighlighted={offset is @props.initialOffset} /> + isHighlighted={offset is @props.initialOffset} + color={@props.colorMap[taskId]} /> renderLoadingMore: -> if @props.initialDataLoaded @@ -91,6 +92,13 @@ mapStateToProps = (state, ownProps) -> taskGroup = state.taskGroups[ownProps.taskGroupId] tasks = taskGroup.taskIds.map (taskId) -> state.tasks[taskId] + colorMap = {} + if taskGroup.taskIds.length > 1 + i = 0 + for taskId in taskGroup.taskIds + colorMap[taskId] = "hsla(#{(360 / taskGroup.taskIds.length) * i}, 100%, 50%, 0.1)" + i++ + logLines: taskGroup.logLines updatedAt: taskGroup.updatedAt prependedLineCount: taskGroup.prependedLineCount @@ -103,6 +111,7 @@ mapStateToProps = (state, ownProps) -> reachedEndOfFile: _.all(tasks.map ({maxOffset, filesize}) -> maxOffset >= filesize) bytesRemainingBefore: sum(_.pluck(tasks, 'minOffset')) bytesRemainingAfter: sum(tasks.map ({filesize, maxOffset}) -> Math.max(filesize - maxOffset, 0)) + colorMap: colorMap mapDispatchToProps = { taskGroupTop, taskGroupBottom } diff --git a/SingularityUI/app/reducers/taskGroups.coffee b/SingularityUI/app/reducers/taskGroups.coffee index bd9f121270..86798ebef2 100644 --- a/SingularityUI/app/reducers/taskGroups.coffee +++ b/SingularityUI/app/reducers/taskGroups.coffee @@ -35,16 +35,18 @@ updateTaskGroup = (state, taskGroupId, update) -> filterLogLines = (lines, search) -> _.filter lines, ({data}) -> new RegExp(search).test(data) +TIMESTAMP_REGEX = [ + [/^(\d{2}:\d{2}:\d{2}\.\d{3})/, 'HH:mm:ss.SSS'] + [/^[A-Z ]+\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3})/, 'YYYY-MM-DD HH:mm:ss,SSS'] + [/^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3})/, 'YYYY-MM-DD HH:mm:ss,SSS'] +] + parseLineTimestamp = (line) -> - match = line.match(/^\d{2}:\d{2}:\d{2}\.\d{3}/) - if match - return moment(match, 'HH:mm:ss.SSS').valueOf() - else - match = line.match(/^[A-Z ]+\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3})\]/) + for group in TIMESTAMP_REGEX + match = line.match(group[0]) if match - return moment(match[1], 'YYYY-MM-DD HH:mm:ss,SSS').valueOf() - - return null + return moment(match, group[1]).valueOf() + return null buildEmptyBuffer = (taskId, offset) -> { offset, taskId, data: '' } From 1f1031d8d9ec04e4109ecbb451f4f519f088e5c8 Mon Sep 17 00:00:00 2001 From: tpetr Date: Fri, 8 Apr 2016 17:34:56 -0400 Subject: [PATCH 24/44] remove log --- SingularityUI/app/components/logs/TaskGroupHeader.cjsx | 1 - 1 file changed, 1 deletion(-) diff --git a/SingularityUI/app/components/logs/TaskGroupHeader.cjsx b/SingularityUI/app/components/logs/TaskGroupHeader.cjsx index cccd987c6e..c59dbabed1 100644 --- a/SingularityUI/app/components/logs/TaskGroupHeader.cjsx +++ b/SingularityUI/app/components/logs/TaskGroupHeader.cjsx @@ -16,7 +16,6 @@ class TaskGroupHeader extends React.Component renderInstanceInfo: -> if @props.tasks.length > 1 - console.log @props.tasks Viewing Instances {@props.tasks.map(({taskId}) -> getInstanceNumberFromTaskId(taskId)).join(', ')} else if @props.tasks.length > 0 From 4ad1f7ebfee5231023b87079d9e76add7d0322df Mon Sep 17 00:00:00 2001 From: tpetr Date: Fri, 8 Apr 2016 18:00:34 -0400 Subject: [PATCH 25/44] fix logline key + tweak timestamp regex --- SingularityUI/app/components/logs/LogLines.cjsx | 2 +- SingularityUI/app/reducers/taskGroups.coffee | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/SingularityUI/app/components/logs/LogLines.cjsx b/SingularityUI/app/components/logs/LogLines.cjsx index 81eb229c17..34f8375018 100644 --- a/SingularityUI/app/components/logs/LogLines.cjsx +++ b/SingularityUI/app/components/logs/LogLines.cjsx @@ -52,7 +52,7 @@ class LogLines extends React.Component @props.logLines.map ({data, offset, taskId, timestamp}) => TIMESTAMP_REGEX = [ [/^(\d{2}:\d{2}:\d{2}\.\d{3})/, 'HH:mm:ss.SSS'] - [/^[A-Z ]+\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3})/, 'YYYY-MM-DD HH:mm:ss,SSS'] + [/^[A-Z \[]+(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3})/, 'YYYY-MM-DD HH:mm:ss,SSS'] [/^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3})/, 'YYYY-MM-DD HH:mm:ss,SSS'] ] From c48f25c53f5c7fa1882f0256e511eab2290fb597 Mon Sep 17 00:00:00 2001 From: tpetr Date: Sun, 10 Apr 2016 18:07:48 -0400 Subject: [PATCH 26/44] fix bug --- SingularityUI/app/reducers/taskGroups.coffee | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/SingularityUI/app/reducers/taskGroups.coffee b/SingularityUI/app/reducers/taskGroups.coffee index c05f002c59..2a740b0905 100644 --- a/SingularityUI/app/reducers/taskGroups.coffee +++ b/SingularityUI/app/reducers/taskGroups.coffee @@ -191,6 +191,7 @@ ACTIONS = { return line prependedLineCount = 0 + linesRemovedFromTop = 0 updatedAt = +new Date() # search @@ -202,6 +203,7 @@ ACTIONS = { if append newLogLines = newLogLines.concat(lines) if newLogLines.length > maxLines + linesRemovedFromTop = newLogLines.length - maxLines newLogLines = newLogLines.slice(newLogLines.length - maxLines) else newLogLines = lines.concat(newLogLines) @@ -215,7 +217,7 @@ ACTIONS = { # update state newState = Object.assign([], state) - newState[taskGroupId] = Object.assign({}, state[taskGroupId], {taskBuffer: newTaskBuffer, logLines: newLogLines, prependedLineCount, updatedAt}) + newState[taskGroupId] = Object.assign({}, state[taskGroupId], {taskBuffer: newTaskBuffer, logLines: newLogLines, prependedLineCount, linesRemovedFromTop, updatedAt}) return newState } From f9f62ccb562ec3a08d4b122dc1ddb9472e2e6044 Mon Sep 17 00:00:00 2001 From: tpetr Date: Sun, 10 Apr 2016 18:44:21 -0400 Subject: [PATCH 27/44] tweaks --- SingularityUI/app/actions/log.coffee | 18 +++++++++++++++--- SingularityUI/app/controllers/LogViewer.cjsx | 2 +- SingularityUI/app/reducers/index.coffee | 2 +- SingularityUI/app/reducers/taskGroups.coffee | 12 +++++++++++- 4 files changed, 28 insertions(+), 6 deletions(-) diff --git a/SingularityUI/app/actions/log.coffee b/SingularityUI/app/actions/log.coffee index 3b37341b2a..2a2fe7e28e 100644 --- a/SingularityUI/app/actions/log.coffee +++ b/SingularityUI/app/actions/log.coffee @@ -136,6 +136,10 @@ taskGroupFetchNext = (taskGroupId) -> taskGroup = taskGroups[taskGroupId] + if taskGroup.pendingRequests + return Promise.resolve() + + dispatch({taskGroupId, type: 'LOG_REQUEST_START'}) promises = getTasks(taskGroup, tasks).map ({taskId, maxOffset, path, initialDataLoaded, terminated}) -> return Promise.resolve() if terminated if initialDataLoaded @@ -147,15 +151,23 @@ taskGroupFetchNext = (taskGroupId) -> else Promise.resolve() # reject("initialDataLoaded is false for task #{taskId}") - Promise.all(promises) + Promise.all(promises).then -> dispatch({taskGroupId, type: 'LOG_REQUEST_END'}) taskGroupFetchPrevious = (taskGroupId) -> (dispatch, getState) -> {tasks, taskGroups, logRequestLength, maxLines} = getState() taskGroup = taskGroups[taskGroupId] + tasks = getTasks(taskGroup, tasks) + + if _.all(tasks.map ({minOffset}) -> minOffset is 0) + return Promise.resolve() + + if taskGroup.pendingRequests + return Promise.resolve() - promises = getTasks(taskGroup, tasks).map (task) -> + dispatch({taskGroupId, type: 'LOG_REQUEST_START'}) + promises = tasks.map (task) -> {taskId, minOffset, path, initialDataLoaded} = task if minOffset > 0 and initialDataLoaded xhr = fetchData(taskId, path, Math.max(minOffset - logRequestLength, 0), Math.min(logRequestLength, minOffset)) @@ -166,7 +178,7 @@ taskGroupFetchPrevious = (taskGroupId) -> else Promise.resolve() # reject("initialDataLoaded is false for task #{taskId}") - Promise.all(promises) + Promise.all(promises).then -> dispatch({taskGroupId, type: 'LOG_REQUEST_END'}) taskData = (taskGroupId, taskId, data, offset, nextOffset, append, maxLines, buffer) -> { diff --git a/SingularityUI/app/controllers/LogViewer.cjsx b/SingularityUI/app/controllers/LogViewer.cjsx index c9ee1bb67b..0a633a9977 100644 --- a/SingularityUI/app/controllers/LogViewer.cjsx +++ b/SingularityUI/app/controllers/LogViewer.cjsx @@ -13,7 +13,6 @@ ActiveTasks = require '../actions/activeTasks' class LogViewer extends Controller initialize: ({@requestId, @path, @initialOffset, taskIds, viewMode, search}) -> - window.lv = @ @title 'Tail of ' + @path initialState = { @@ -40,5 +39,6 @@ class LogViewer extends Controller @setView @view # does nothing app.showView @view + window.getStateJSON = () => JSON.stringify(@store.getState()) module.exports = LogViewer diff --git a/SingularityUI/app/reducers/index.coffee b/SingularityUI/app/reducers/index.coffee index be7319fdcf..4bd42ae5df 100644 --- a/SingularityUI/app/reducers/index.coffee +++ b/SingularityUI/app/reducers/index.coffee @@ -32,7 +32,7 @@ search = (state='', action) -> logRequestLength = (state=30000, action) -> return state -maxLines = (state=1000, action) -> +maxLines = (state=100000, action) -> return state showDebugInfo = (state=false, action) -> diff --git a/SingularityUI/app/reducers/taskGroups.coffee b/SingularityUI/app/reducers/taskGroups.coffee index 2a740b0905..5ab1aa39b7 100644 --- a/SingularityUI/app/reducers/taskGroups.coffee +++ b/SingularityUI/app/reducers/taskGroups.coffee @@ -89,7 +89,7 @@ ACTIONS = { # An entire task group is ready LOG_TASK_GROUP_READY: (state, {taskGroupId}) -> - return updateTaskGroup(state, taskGroupId, {ready: true}) + return updateTaskGroup(state, taskGroupId, {ready: true, updatedAt: +new Date()}) LOG_REMOVE_TASK_GROUP: (state, {taskGroupId}) -> newState = [] @@ -122,6 +122,16 @@ ACTIONS = { LOG_SCROLL_ALL_TO_BOTTOM: (state) -> state.map (taskGroup) -> Object.assign({}, taskGroup, resetTaskGroup()) + LOG_REQUEST_START: (state, {taskGroupId}) -> + newState = Object.assign([], state) + newState[taskGroupId] = Object.assign({}, state[taskGroupId], {pendingRequests: true}) + return newState + + LOG_REQUEST_END: (state, {taskGroupId}) -> + newState = Object.assign([], state) + newState[taskGroupId] = Object.assign({}, state[taskGroupId], {pendingRequests: false}) + return newState + # We've received logging data for a task LOG_TASK_DATA: (state, {taskGroupId, taskId, offset, nextOffset, maxLines, data, append}) -> taskGroup = state[taskGroupId] From 17a355e2d7456aea2b022c109df5849283d0688e Mon Sep 17 00:00:00 2001 From: tpetr Date: Mon, 11 Apr 2016 11:26:51 -0400 Subject: [PATCH 28/44] fix logline permalink --- SingularityUI/app/components/logs/LogLine.cjsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/SingularityUI/app/components/logs/LogLine.cjsx b/SingularityUI/app/components/logs/LogLine.cjsx index c63a286742..c832cb6a1c 100644 --- a/SingularityUI/app/components/logs/LogLine.cjsx +++ b/SingularityUI/app/components/logs/LogLine.cjsx @@ -56,7 +56,7 @@ class LogLine extends React.Component highlightLine: @props.isHighlighted
    - @props.clickPermalink(@props.offset)}> + @props.clickPermalink(@props.offset)}>
    @@ -69,6 +69,7 @@ class LogLine extends React.Component mapStateToProps = (state, ownProps) -> search: state.search showDebugInfo: state.showDebugInfo + path: state.path mapDispatchToProps = { clickPermalink } From bbd374aabe2a9372a15decf16195691bb93e90bb Mon Sep 17 00:00:00 2001 From: tpetr Date: Mon, 11 Apr 2016 11:27:58 -0400 Subject: [PATCH 29/44] improve task status indicator --- .../app/components/logs/TaskGroupHeader.cjsx | 6 ++--- .../components/logs/TaskStatusIndicator.cjsx | 23 +++++++++++++++++++ 2 files changed, 25 insertions(+), 4 deletions(-) create mode 100644 SingularityUI/app/components/logs/TaskStatusIndicator.cjsx diff --git a/SingularityUI/app/components/logs/TaskGroupHeader.cjsx b/SingularityUI/app/components/logs/TaskGroupHeader.cjsx index c59dbabed1..502584ab9e 100644 --- a/SingularityUI/app/components/logs/TaskGroupHeader.cjsx +++ b/SingularityUI/app/components/logs/TaskGroupHeader.cjsx @@ -1,4 +1,5 @@ React = require 'react' +TaskStatusIndicator = require './TaskStatusIndicator' { getInstanceNumberFromTaskId } = require '../../utils' @@ -22,10 +23,7 @@ class TaskGroupHeader extends React.Component
    -
    -
    - {@props.tasks[0].lastTaskStatus} -
    + else
    diff --git a/SingularityUI/app/components/logs/TaskStatusIndicator.cjsx b/SingularityUI/app/components/logs/TaskStatusIndicator.cjsx new file mode 100644 index 0000000000..f3a2d16f80 --- /dev/null +++ b/SingularityUI/app/components/logs/TaskStatusIndicator.cjsx @@ -0,0 +1,23 @@ +React = require 'react' +Utils = require '../../utils' + +class TaskStatusIndicator extends React.Component + @propTypes: + status: React.PropTypes.string + + getClassName: -> + if @props.status in Utils.TERMINAL_TASK_STATES + 'bg-danger' + else + 'bg-info running' + + render: -> + if @props.status +
    +
    + {@props.status.toLowerCase().replace('_', ' ')} +
    + else +
    + +module.exports = TaskStatusIndicator From d5d5db0243748df5bf8f7ba8c66e963ecac2123d Mon Sep 17 00:00:00 2001 From: tpetr Date: Mon, 11 Apr 2016 11:28:18 -0400 Subject: [PATCH 30/44] remove waypoint dep, not used anymore --- SingularityUI/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/SingularityUI/package.json b/SingularityUI/package.json index 2ea86f7976..0ed95de7e4 100644 --- a/SingularityUI/package.json +++ b/SingularityUI/package.json @@ -37,7 +37,6 @@ "react-interval": "^1.2.1", "react-list": "~0.7.3", "react-redux": "^4.4.1", - "react-waypoint": "^1.3.0", "redux": "^3.3.1", "redux-logger": "^2.6.1", "redux-thunk": "^2.0.1", From f58e4abb8619352d7b7516177fd736cc83daab23 Mon Sep 17 00:00:00 2001 From: tpetr Date: Mon, 11 Apr 2016 15:22:53 -0400 Subject: [PATCH 31/44] more fixes --- SingularityUI/app/actions/log.coffee | 7 ++++++- .../app/components/logs/FileNotFound.cjsx | 11 +++++++++++ SingularityUI/app/components/logs/LogLines.cjsx | 6 ++++-- .../app/components/logs/TaskGroupContainer.cjsx | 15 +++++++++------ SingularityUI/app/controllers/LogViewer.cjsx | 7 ++++++- SingularityUI/app/reducers/taskGroups.coffee | 16 ++++++++++------ SingularityUI/app/reducers/tasks.coffee | 2 +- 7 files changed, 47 insertions(+), 17 deletions(-) create mode 100644 SingularityUI/app/components/logs/FileNotFound.cjsx diff --git a/SingularityUI/app/actions/log.coffee b/SingularityUI/app/actions/log.coffee index 2a2fe7e28e..1131b8f75d 100644 --- a/SingularityUI/app/actions/log.coffee +++ b/SingularityUI/app/actions/log.coffee @@ -33,14 +33,19 @@ initialize = (requestId, path, search, taskIds) -> groupPromises = taskIdGroups.map (taskIds, taskGroupId) -> taskInitPromises = taskIds.map (taskId) -> + taskInitDeferred = Q.defer() resolvedPath = path.replace('$TASK_ID', taskId) fetchData(taskId, resolvedPath).done ({offset}) -> dispatch(initTask(taskId, offset, resolvedPath, true)) + taskInitDeferred.resolve() .error ({status}) -> if status is 404 app.caughtError() dispatch(taskFileDoesNotExist(taskGroupId, taskId)) - Promise.resolve() + taskInitDeferred.resolve() + else + taskInitDeferred.reject() + return taskInitDeferred.promise taskStatusPromises = taskIds.map (taskId) -> dispatch(updateTaskStatus(taskGroupId, taskId)) diff --git a/SingularityUI/app/components/logs/FileNotFound.cjsx b/SingularityUI/app/components/logs/FileNotFound.cjsx new file mode 100644 index 0000000000..c4af678723 --- /dev/null +++ b/SingularityUI/app/components/logs/FileNotFound.cjsx @@ -0,0 +1,11 @@ +React = require 'react' + +class FileNotFound extends React.Component + render: -> +
    +
    +

    { @props.fileName } does not exist.

    +
    +
    + +module.exports = FileNotFound \ No newline at end of file diff --git a/SingularityUI/app/components/logs/LogLines.cjsx b/SingularityUI/app/components/logs/LogLines.cjsx index 34f8375018..8c721ebb4e 100644 --- a/SingularityUI/app/components/logs/LogLines.cjsx +++ b/SingularityUI/app/components/logs/LogLines.cjsx @@ -1,5 +1,4 @@ React = require 'react' -Waypoint = require 'react-waypoint' LogLine = require './LogLine' Humanize = require 'humanize-plus' LogLines = require '../../collections/LogLines' @@ -35,7 +34,9 @@ class LogLines extends React.Component window.removeEventListener 'resize', @handleScroll componentDidUpdate: (prevProps, prevState) -> - if prevProps.updatedAt isnt @props.updatedAt + if @props.tailing + @refs.tailContents.scrollTop = @refs.tailContents.scrollHeight + else if prevProps.updatedAt isnt @props.updatedAt if @props.prependedLineCount > 0 or @props.linesRemovedFromTop > 0 @refs.tailContents.scrollTop += 20 * (@props.prependedLineCount - @props.linesRemovedFromTop) else @@ -101,6 +102,7 @@ mapStateToProps = (state, ownProps) -> logLines: taskGroup.logLines updatedAt: taskGroup.updatedAt + tailing: taskGroup.tailing prependedLineCount: taskGroup.prependedLineCount linesRemovedFromTop: taskGroup.linesRemovedFromTop activeColor: state.activeColor diff --git a/SingularityUI/app/components/logs/TaskGroupContainer.cjsx b/SingularityUI/app/components/logs/TaskGroupContainer.cjsx index 43763e1c38..6f0aa46f12 100644 --- a/SingularityUI/app/components/logs/TaskGroupContainer.cjsx +++ b/SingularityUI/app/components/logs/TaskGroupContainer.cjsx @@ -2,6 +2,7 @@ React = require 'react' TaskGroupHeader = require './TaskGroupHeader' LogLines = require './LogLines' LoadingSpinner = require './LoadingSpinner' +FileNotFound = require './FileNotFound' classNames = require 'classnames' { connect } = require 'react-redux' @@ -19,11 +20,10 @@ class TaskGroupContainer extends React.Component return (12 / @props.taskGroupContainerCount) renderLogLines: -> - if @props.initialDataLoaded - if @props.fileExists - return - else - return

    File does not exist

    + if @props.logDataLoaded + return + else if @props.initialDataLoaded and not @props.fileExists + return
    else return Loading logs... @@ -40,13 +40,16 @@ mapStateToProps = (state, ownProps) -> return { initialDataLoaded: false fileExists: false + logDataLoaded: false terminated: false } taskGroup = state.taskGroups[ownProps.taskGroupId] tasks = taskGroup.taskIds.map (taskId) -> state.tasks[taskId] - initialDataLoaded: _.all(_.pluck(tasks, 'logDataLoaded')) + initialDataLoaded: _.all(_.pluck(tasks, 'initialDataLoaded')) + logDataLoaded: _.all(_.pluck(tasks, 'logDataLoaded')) fileExists: _.any(_.pluck(tasks, 'exists')) terminated: _.all(_.pluck(tasks, 'terminated')) + path: state.path module.exports = connect(mapStateToProps)(TaskGroupContainer) \ No newline at end of file diff --git a/SingularityUI/app/controllers/LogViewer.cjsx b/SingularityUI/app/controllers/LogViewer.cjsx index 0a633a9977..e6104226f8 100644 --- a/SingularityUI/app/controllers/LogViewer.cjsx +++ b/SingularityUI/app/controllers/LogViewer.cjsx @@ -24,7 +24,12 @@ class LogViewer extends Controller } } - @store = Redux.createStore(rootReducer, initialState, Redux.compose(Redux.applyMiddleware(thunk.default, logger()))) + middlewares = [thunk.default] + + if window.localStorage.enableReduxLogging + middlewares.push(logger()) + + @store = Redux.createStore(rootReducer, initialState, Redux.compose(Redux.applyMiddleware.apply(this, middlewares))) if taskIds.length > 0 initPromise = @store.dispatch(LogActions.initialize(@requestId, @path, search, taskIds)) diff --git a/SingularityUI/app/reducers/taskGroups.coffee b/SingularityUI/app/reducers/taskGroups.coffee index 5ab1aa39b7..d147d77b0a 100644 --- a/SingularityUI/app/reducers/taskGroups.coffee +++ b/SingularityUI/app/reducers/taskGroups.coffee @@ -15,16 +15,18 @@ buildTaskGroup = (taskIds, search) -> updatedAt: +new Date() top: false bottom: false + tailing: false ready: false pendingRequests: false detectedTimestamp: false } -resetTaskGroup = () -> { +resetTaskGroup = (tailing=false) -> { logLines: [] taskBuffer: {} top: false bottom: false + tailing } updateTaskGroup = (state, taskGroupId, update) -> @@ -81,11 +83,11 @@ ACTIONS = { # The logger has either entered or exited the top LOG_TASK_GROUP_TOP: (state, {taskGroupId, visible}) -> - return updateTaskGroup(state, taskGroupId, {top: visible}) + return updateTaskGroup(state, taskGroupId, {top: visible, tailing: false}) # The logger has either entered or exited the bottom LOG_TASK_GROUP_BOTTOM: (state, {taskGroupId, visible}) -> - return updateTaskGroup(state, taskGroupId, {bottom: visible}) + return updateTaskGroup(state, taskGroupId, {bottom: visible, tailing: false}) # An entire task group is ready LOG_TASK_GROUP_READY: (state, {taskGroupId}) -> @@ -116,11 +118,11 @@ ACTIONS = { LOG_SCROLL_TO_BOTTOM: (state, {taskGroupId}) -> newState = Object.assign([], state) - newState[taskGroupId] = Object.assign({}, state[taskGroupId], resetTaskGroup()) + newState[taskGroupId] = Object.assign({}, state[taskGroupId], resetTaskGroup(true)) return newState LOG_SCROLL_ALL_TO_BOTTOM: (state) -> - state.map (taskGroup) -> Object.assign({}, taskGroup, resetTaskGroup()) + state.map (taskGroup) -> Object.assign({}, taskGroup, resetTaskGroup(true)) LOG_REQUEST_START: (state, {taskGroupId}) -> newState = Object.assign([], state) @@ -138,7 +140,9 @@ ACTIONS = { # bail early if no data if data.length is 0 and task.loadedData - return state + newState = Object.assign([], state) + newState[taskGroupId] = Object.assign({}, taskGroup, {tailing: append and data.length is 0}) + return newState # split task data into separate lines, attempt to parse timestamp currentOffset = offset diff --git a/SingularityUI/app/reducers/tasks.coffee b/SingularityUI/app/reducers/tasks.coffee index 3049fa382b..a0503b05d3 100644 --- a/SingularityUI/app/reducers/tasks.coffee +++ b/SingularityUI/app/reducers/tasks.coffee @@ -50,7 +50,7 @@ ACTIONS = { initialDataLoaded: true }) LOG_TASK_FILE_DOES_NOT_EXIST: (state, {taskId}) -> - updateTask(state, taskId, {exists: false}) + updateTask(state, taskId, {exists: false, initialDataLoaded: true}) LOG_SCROLL_TO_TOP: (state, {taskIds}) -> newState = Object.assign({}, state) for taskId in taskIds From f91bace039b8e44708e3efec65d4cd8d42935b27 Mon Sep 17 00:00:00 2001 From: tpetr Date: Mon, 11 Apr 2016 16:55:14 -0400 Subject: [PATCH 32/44] more tweaks --- SingularityUI/app/actions/log.coffee | 12 +++++++----- SingularityUI/app/components/logs/LogLines.cjsx | 8 +++++--- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/SingularityUI/app/actions/log.coffee b/SingularityUI/app/actions/log.coffee index 1131b8f75d..31a072add7 100644 --- a/SingularityUI/app/actions/log.coffee +++ b/SingularityUI/app/actions/log.coffee @@ -140,13 +140,14 @@ taskGroupFetchNext = (taskGroupId) -> {tasks, taskGroups, logRequestLength, maxLines} = getState() taskGroup = taskGroups[taskGroupId] + tasks = getTasks(taskGroup, tasks) + # bail early if there's already a pending request if taskGroup.pendingRequests return Promise.resolve() dispatch({taskGroupId, type: 'LOG_REQUEST_START'}) - promises = getTasks(taskGroup, tasks).map ({taskId, maxOffset, path, initialDataLoaded, terminated}) -> - return Promise.resolve() if terminated + promises = tasks.map ({taskId, maxOffset, path, initialDataLoaded}) -> if initialDataLoaded xhr = fetchData(taskId, path, maxOffset, logRequestLength) xhr.done ({data, offset, nextOffset}) -> @@ -165,20 +166,21 @@ taskGroupFetchPrevious = (taskGroupId) -> taskGroup = taskGroups[taskGroupId] tasks = getTasks(taskGroup, tasks) + # bail early if all tasks are at the top if _.all(tasks.map ({minOffset}) -> minOffset is 0) return Promise.resolve() + # bail early if there's already a pending request if taskGroup.pendingRequests return Promise.resolve() dispatch({taskGroupId, type: 'LOG_REQUEST_START'}) - promises = tasks.map (task) -> - {taskId, minOffset, path, initialDataLoaded} = task + promises = tasks.map ({taskId, minOffset, path, initialDataLoaded}) -> if minOffset > 0 and initialDataLoaded xhr = fetchData(taskId, path, Math.max(minOffset - logRequestLength, 0), Math.min(logRequestLength, minOffset)) xhr.done ({data, offset, nextOffset}) -> if data.length > 0 - nextOffset = nextOffset || offset + data.length + nextOffset = offset + data.length dispatch(taskData(taskGroupId, taskId, data, offset, nextOffset, false, maxLines)) else Promise.resolve() # reject("initialDataLoaded is false for task #{taskId}") diff --git a/SingularityUI/app/components/logs/LogLines.cjsx b/SingularityUI/app/components/logs/LogLines.cjsx index 8c721ebb4e..b4f89db93f 100644 --- a/SingularityUI/app/components/logs/LogLines.cjsx +++ b/SingularityUI/app/components/logs/LogLines.cjsx @@ -44,9 +44,7 @@ class LogLines extends React.Component renderLoadingPrevious: -> if @props.initialDataLoaded - if @props.reachedStartOfFile -
    At beginning of file
    - else + if not @props.reachedStartOfFile
    Loading previous... ({Humanize.filesize(@props.bytesRemainingBefore)} remaining)
    renderLogLines: -> @@ -61,12 +59,15 @@ class LogLines extends React.Component color={@props.colorMap[taskId]} /> renderLoadingMore: -> + if @props.terminated + return null if @props.initialDataLoaded if @props.reachedEndOfFile
    Tailing...
    else
    Loading more... ({Humanize.filesize(@props.bytesRemainingAfter)} remaining)
    + handleScroll: => {scrollTop, scrollHeight, clientHeight} = @refs.tailContents @@ -109,6 +110,7 @@ mapStateToProps = (state, ownProps) -> top: taskGroup.top bottom: taskGroup.bottom initialDataLoaded: _.all(_.pluck(tasks, 'initialDataLoaded')) + terminated: _.all(_.pluck(tasks, 'terminated')) reachedStartOfFile: _.all(tasks.map ({minOffset}) -> minOffset is 0) reachedEndOfFile: _.all(tasks.map ({maxOffset, filesize}) -> maxOffset >= filesize) bytesRemainingBefore: sum(_.pluck(tasks, 'minOffset')) From 3db49ddcb75f6d3a3b3d08ef41202c49b8bcfc0c Mon Sep 17 00:00:00 2001 From: tpetr Date: Tue, 12 Apr 2016 13:33:49 -0400 Subject: [PATCH 33/44] delete some crap --- .../aggregateTail/AggregateTail.cjsx | 256 ------------------ .../components/aggregateTail/ColorLegend.cjsx | 18 -- .../components/aggregateTail/Contents.cjsx | 206 -------------- .../app/components/aggregateTail/Header.cjsx | 202 -------------- .../app/components/aggregateTail/Help.cjsx | 50 ---- .../aggregateTail/IndividualHeader.cjsx | 52 ---- .../aggregateTail/IndividualTail.cjsx | 149 ---------- .../aggregateTail/InterleavedHeader.cjsx | 27 -- .../aggregateTail/InterleavedTail.cjsx | 223 --------------- .../app/components/aggregateTail/Loader.cjsx | 13 - .../app/components/aggregateTail/LogLine.cjsx | 62 ----- .../aggregateTail/StatusIndicator.cjsx | 21 -- 12 files changed, 1279 deletions(-) delete mode 100644 SingularityUI/app/components/aggregateTail/AggregateTail.cjsx delete mode 100644 SingularityUI/app/components/aggregateTail/ColorLegend.cjsx delete mode 100644 SingularityUI/app/components/aggregateTail/Contents.cjsx delete mode 100644 SingularityUI/app/components/aggregateTail/Header.cjsx delete mode 100644 SingularityUI/app/components/aggregateTail/Help.cjsx delete mode 100644 SingularityUI/app/components/aggregateTail/IndividualHeader.cjsx delete mode 100644 SingularityUI/app/components/aggregateTail/IndividualTail.cjsx delete mode 100644 SingularityUI/app/components/aggregateTail/InterleavedHeader.cjsx delete mode 100644 SingularityUI/app/components/aggregateTail/InterleavedTail.cjsx delete mode 100644 SingularityUI/app/components/aggregateTail/Loader.cjsx delete mode 100644 SingularityUI/app/components/aggregateTail/LogLine.cjsx delete mode 100644 SingularityUI/app/components/aggregateTail/StatusIndicator.cjsx diff --git a/SingularityUI/app/components/aggregateTail/AggregateTail.cjsx b/SingularityUI/app/components/aggregateTail/AggregateTail.cjsx deleted file mode 100644 index cf8d512e67..0000000000 --- a/SingularityUI/app/components/aggregateTail/AggregateTail.cjsx +++ /dev/null @@ -1,256 +0,0 @@ -React = require 'react' -ReactDOM = require 'react-dom' -BackboneReactComponent = require 'backbone-react-component' -Header = require './Header' -IndividualTail = require './IndividualTail' -InterleavedTail = require './InterleavedTail' -Utils = require '../../utils' -Help = require './Help' -vex = require 'vex.dialog' - -AggregateTail = React.createClass - mixins: [Backbone.React.Component.mixin] - - # ============================================================================ - # Lifecycle Methods | - # ============================================================================ - - # Single Mode: Backwards compatability mode for the old URL format. Disables all task switching controls. - getInitialState: -> - params = Utils.getQueryParams() - viewingInstances: if @props.singleMode then [@props.singleModeTaskId] else (if params.taskIds then params.taskIds.split(',').slice(0, 6) else []) - color: @getActiveColor() - splitView: !(params.view is 'unified') - search: if params.grep then params.grep else '' - offset: @props.initialOffset - - componentWillMount: -> - # Automatically map backbone collections and models to the state of this component - Backbone.React.Component.mixin.on(@, { - collections: { - activeTasks: @props.activeTasks - } - }); - - $(window).on("blur", @onWindowBlur) - $(window).on("focus", @onWindowFocus) - - componentDidMount: -> - if @state.viewingInstances.length is 1 - document.title = "Tail of #{@props.path.replace('$TASK_ID', @state.viewingInstances[0])}" - else - document.title = "Tail of #{@props.path.replace('$TASK_ID', 'Task Directory')}" - - initialViewingInstancesOnUpdate: -> - runningTasks = (task for task in @state.activeTasks when task.lastTaskState == 'TASK_RUNNING') - if runningTasks.length > 0 - return runningTasks - else - return @state.activeTasks - - componentDidUpdate: (prevProps, prevState) -> - if prevState.activeTasks.length is 0 and @state.activeTasks.length > 0 and not Utils.getQueryParams()?.taskIds and !@props.singleMode - @setState - viewingInstances: _.pluck(@initialViewingInstancesOnUpdate(), 'id').slice(0, 6) - - componentWillUnmount: -> - Backbone.React.Component.mixin.off(@); - $(window).off("blur", @onWindowBlur) - $(window).off("focus", @onWindowFocus) - - # ============================================================================ - # Event Handlers | - # ============================================================================ - - handleOffsetLink: (offset) -> - this.setState {offset} - - onWindowBlur: -> - @blurTimer = _.delay( => - for k, tail of @refs - if tail.isTailing() - tail.stopTailing() - $(window).one("focus", => - tail.startTailing() - ) - , 900000) # 15 minutes - - onWindowFocus: -> - clearTimeout(@blurTimer) - - toggleViewingInstance: (taskId) -> - if @props.singleMode - return - - if taskId in @state.viewingInstances - viewing = _.without @state.viewingInstances, taskId - else - viewing = @state.viewingInstances.concat(taskId) - - if 0 < viewing.length <= 6 - @setState - viewingInstances: viewing - history.replaceState @state, '', location.href.replace(location.search, "?taskIds=#{viewing.join(',')}&view=#{@getViewString(@state.splitView)}&grep=#{@state.search}") - if viewing.length is 1 - document.title = "Tail of #{@props.path.replace('$TASK_ID', viewing[0])}" - else - document.title = "Tail of #{@props.path.replace('$TASK_ID', 'Task Directory')}" - - showOnlyInstance: (taskId) -> - if @props.singleMode - return - - @setState - viewingInstances: [taskId] - history.replaceState @state, '', location.href.replace(location.search, "?taskIds=#{taskId}&view=#{@getViewString(@state.splitView)}&grep=#{@state.search}") - - selectTasks: (selectFuncion) -> - if @props.singleMode - return - - viewing = _.pluck(selectFuncion(_.sortBy(@state.activeTasks, (task) => task.taskId.instanceNo)), 'id') - @setState - viewingInstances: viewing - history.replaceState @state, '', location.href.replace(location.search, "?taskIds=#{viewing.join(',')}&view=#{@getViewString(@state.splitView)}&grep=#{@state.search}") - - setSearch: (search) -> - @setState - search: search - history.replaceState @state, '', location.href.replace(location.search, "?taskIds=#{@state.viewingInstances.join(',')}&view=#{@getViewString(@state.splitView)}&grep=#{search}") - - scrollAllTop: -> - for tail of @refs - @refs[tail].scrollToTop() - - scrollAllBottom: -> - for tail of @refs - @refs[tail].scrollToBottom() - - getColumnWidth: -> - instances = @state.viewingInstances.length - if instances is 1 - return 12 - else if instances in [2, 4] - return 6 - else if instances in [3, 5, 6] - return 4 - else - return 1 - - setLogColor: (color) -> - localStorage.setItem('singularityLogColor', color) - @setState - color: color - - getActiveColor: -> - localStorage.getItem('singularityLogColor') - - toggleView: -> - splitView = !@state.splitView - @setState - splitView: splitView - viewString = @getViewString(splitView) - history.replaceState @state, '', location.href.replace(location.search, "?taskIds=#{@state.viewingInstances.join(',')}&view=#{viewString}&grep=#{@state.search}") - - getViewString: (splitView) -> - if splitView then 'split' else 'unified' - - toggleHelp: -> - vex.open - content: '
    ' - contentClassName: 'help-dialog' - ReactDOM.render(, $('#help-target').get()[0]) - - # ============================================================================ - # Rendering | - # ============================================================================ - - getRowType: -> - if !@state.splitView - return 'unified' - - if @state.viewingInstances.length > 3 then 'tail-row-half' else 'tail-row' - - getInstanceNumber: (taskId) -> - @state.activeTasks.filter((t) => - t.id is taskId - )[0]?.taskId.instanceNo - - renderTail: -> - if @state.splitView - return @renderIndividualTails() - else - return @renderInterleavedTail() - - renderIndividualTails: -> - @state.viewingInstances.sort((a, b) => - @getInstanceNumber(a) > @getInstanceNumber(b) - ).map((taskId, i) => - if @props.logLines[taskId] -
    - -
    - ) - - renderInterleavedTail: -> - logLines = @state.viewingInstances.map((taskId) => - @props.logLines[taskId] - ) - ajaxErrors = @state.viewingInstances.map((taskId) => - @props.ajaxError[taskId] - ) - if _.filter(logLines, (l) => l isnt undefined).length is @state.viewingInstances.length -
    - -
    - - render: -> -
    -
    -
    - {@renderTail()} -
    -
    - -module.exports = AggregateTail diff --git a/SingularityUI/app/components/aggregateTail/ColorLegend.cjsx b/SingularityUI/app/components/aggregateTail/ColorLegend.cjsx deleted file mode 100644 index f6c39f526b..0000000000 --- a/SingularityUI/app/components/aggregateTail/ColorLegend.cjsx +++ /dev/null @@ -1,18 +0,0 @@ -React = require 'react' - -ColorLegend = React.createClass - - renderColors: -> - _.keys(@props.colors).map (taskId) => -
  • -
    {taskId} -
  • - - render: -> -
    -
      - {@renderColors()} -
    -
    - -module.exports = ColorLegend diff --git a/SingularityUI/app/components/aggregateTail/Contents.cjsx b/SingularityUI/app/components/aggregateTail/Contents.cjsx deleted file mode 100644 index 55547d7da3..0000000000 --- a/SingularityUI/app/components/aggregateTail/Contents.cjsx +++ /dev/null @@ -1,206 +0,0 @@ -React = require 'react' -ReactDOM = require 'react-dom' -ReactList = require 'react-list' -LogLine = require './LogLine' -Loader = require './Loader' - -Utils = require '../../utils' - -Contents = React.createClass - - # ============================================================================ - # Lifecycle Methods | - # ============================================================================ - - getInitialState: -> - @state = - isLoading: false - loadingText: '' - linesToRender: [] - loadingFromTop: false - - componentDidMount: -> - @scrollNode = ReactDOM.findDOMNode(@refs.scrollContainer) - @currentOffset = parseInt @props.offset - if @props.taskState not in Utils.TERMINAL_TASK_STATES and not @props.ajaxError.present and not @props.offset - @startTailingPoll() - - componentDidUpdate: (prevProps, prevState) -> - if @tailingPoll and not @state.loadingFromTop - @scrollToBottom() - - # Stop tailing if the task dies - if @props.taskState in Utils.TERMINAL_TASK_STATES or @props.ajaxError.present - @stopTailingPoll() - - # Update our loglines components only if needed - if prevProps.logLines?.length isnt @props.logLines?.length - @setState - linesToRender: @renderLines(@props.offset) - - componentWillReceiveProps: (nextProps) -> - if nextProps.offset isnt @props.offset - @setState - linesToRender: @renderLines(nextProps.offset) - - componentWillUnmount: -> - @stopTailingPoll() - - # ============================================================================ - # Event Handlers | - # ============================================================================ - - handleScroll: (e) -> - node = @scrollNode - # Are we at the bottom? - if $(node).scrollTop() + $(node).innerHeight() >= node.scrollHeight - 20 - if @props.moreToFetch() - @props.fetchNext() - else if @props.taskState not in Utils.TERMINAL_TASK_STATES and @props.logLines.length > 0 and not @state.loadingFromTop - @startTailingPoll() - # Or the top? - else if $(node).scrollTop() is 0 - if not @tailingPoll and @props.logLines[0]?.offset > 0 - @setState - isLoading: true - loadingText: 'Fetching' - @props.fetchPrevious( => - @setState - isLoading: false - loadingText: '' - ) - else - @stopTailingPoll() - - handleKeyDown: (e) -> - if e.keyCode is 38 - @stopTailingPoll() - - startTailingPoll: -> - # Make sure there isn't one already running - @stopTailingPoll() - @setState - isLoading: true - loadingText: 'Tailing' - @tailingPoll = setInterval => - if @props.reachedEndOfFile() and not @props.reachedStartOfFile() and @scrollNode.scrollHeight <= $(@scrollNode).innerHeight() - @setState - isLoading: true - loadingText: 'Loading' - @props.fetchPrevious( => ) - else - @setState - isLoading: true - loadingText: 'Tailing' - @props.fetchNext() - , 2000 - - stopTailingPoll: -> - if @tailingPoll - clearInterval @tailingPoll - @tailingPoll = null - @setState - isLoading: false - loadingText: '' - loadingFromTop: false - - loadFromTop: -> - # Make sure there isn't one already running - @stopTailingPoll() - @setState - isLoading: true - loadingText: 'Loading' - loadingFromTop: true - @tailingPoll = setInterval => - if @state.linesToRender and @refs.lines.getVisibleRange()[1] < @state.linesToRender.length * 2 - @stopTailingPoll() - else - @props.fetchNext() - , 2000 - - # ============================================================================ - # Rendering | - # ============================================================================ - - renderError: -> - if @props.ajaxError.present -
    -
    -

    {@props.ajaxError.message}

    -
    -
    - - renderLines: (offset) -> - if @props.logLines and @props.logLines.length > 0 - if @props.colorMap - colors = @props.colorMap(@props.logLines) - else - colors = {} - colors[@props.logLines[0].taskId] = 'hsla(0, 0, 0, 0)' - @props.logLines.map((l, i) => - link = window.location.href.replace(window.location.search, '').replace(window.location.hash, '') - link += "?taskIds=#{@props.taskId}##{l.offset}" - isHighlighted = l.offset is offset - isFirstLine = i is 0 - isLastLine = i is @props.logLines.length - 1 - - ) - - lineRenderer: (index, key) -> - @state.linesToRender[index] - - getLineHeight: (index) -> - if index in [0, @state.linesToRender.length] - return 40 - else - return 20 - - render: -> -
    -
    - {@renderError()} - - -
    - -
    - - # ============================================================================ - # Utility Methods | - # ============================================================================ - - isTailing: -> - !(_.isNull(@tailingPoll) or _.isUndefined(@tailingPoll)) - - scrollToLine: (line) -> - @refs.lines.scrollTo(line) - - scrollToTop: -> - @refs.lines.scrollTo(0) - - scrollToBottom: -> - @refs.lines.scrollTo(@state.linesToRender?.length || 0) - -module.exports = Contents diff --git a/SingularityUI/app/components/aggregateTail/Header.cjsx b/SingularityUI/app/components/aggregateTail/Header.cjsx deleted file mode 100644 index 5f0b8b6ef4..0000000000 --- a/SingularityUI/app/components/aggregateTail/Header.cjsx +++ /dev/null @@ -1,202 +0,0 @@ -React = require 'react' -ReactDOM = require 'react-dom' - -Header = React.createClass - - getInitialState: -> - searchVal: @props.search - - handleSearchChange: (event) -> - @setState - searchVal: event.target.value - - setSearch: (val) -> - @props.setSearch(val) - - handleSearchKeyDown: (event) -> - if event.keyCode is 13 # Enter: commit search and close - @setSearch(@state.searchVal) - $("#searchDDToggle").dropdown("toggle") - else if event.keyCode is 27 # Escape: clear search and commit - @setState - searchVal: '' - @setSearch('') - $("#searchDDToggle").dropdown("toggle") - - handleSearchToggle: (event) -> - ReactDOM.findDOMNode(@refs.searchInput).focus() - - handleTasksKeyDown: (event) -> - if event.keyCode is 70 - @props.selectTasks((tasks) => - _.first(tasks, 6) - ) - else if event.keyCode is 76 - @props.selectTasks((tasks) => - _.last(tasks, 6) - ) - else if event.keyCode is 79 - @props.selectTasks((tasks) => - _.first(_.filter(tasks, (t) => - t.taskId.instanceNo % 2 is 1 - ), 6) - ) - else if event.keyCode is 69 - @props.selectTasks((tasks) => - if tasks.length <= 1 - return tasks - _.first(_.filter(tasks, (t) => - t.taskId.instanceNo % 2 is 0 - ), 6) - ) - else if 49 <= event.keyCode <= 57 - @props.selectTasks((tasks) => - _.filter(tasks, (t) => - t.taskId.instanceNo is parseInt(String.fromCharCode(event.keyCode)) - ) - ) - - renderBreadcrumbs: -> - path = @props.path - if @props.viewingInstances.length is 1 - path = path.replace('$TASK_ID', @props.viewingInstances[0]) - else - path = path.replace('$TASK_ID', 'Task Directory') - segments = path.split('/') - return segments.map (s, i) => - path = segments.slice(0, i + 1).join('/') - if i < segments.length - 1 - return ( -
  • - {s} -
  • - ) - else - return ( -
  • - - {s} - -
  • - ) - - renderTasksDropdown: -> - if @props.singleMode - return null - -
    - -
      - {@renderListItems()} -
    -
    - - renderListItems: -> - tasks = _.sortBy(@props.activeTasks, (t) => t.taskId.instanceNo).map (task, i) => - taskId = task.id -
  • - @props.toggleViewingInstance(taskId)}> - - Instance {task.taskId.instanceNo} - -
  • - if tasks.length > 0 - return tasks - else - return
  • No running instances
  • - - renderViewButtons: -> - if @props.viewingInstances.length > 1 -
    - - -
    - - renderColorList: -> - - - renderAnchorButtons: -> - - - - - - - - - - renderHelpButton: -> - - - - - renderSearch: -> -
    - -
      -
    • -
      - - - - -
      -
    • -
    -
    - - render: -> -
    -
    -
    - -
    -
    -
      - {@renderBreadcrumbs()} -
    -
    -
    - {@renderSearch()} - {@renderTasksDropdown()} - {@renderColorList()} - {@renderViewButtons()} - {@renderAnchorButtons()} - {@renderHelpButton()} -
    -
    -
    - -module.exports = Header diff --git a/SingularityUI/app/components/aggregateTail/Help.cjsx b/SingularityUI/app/components/aggregateTail/Help.cjsx deleted file mode 100644 index 2c750624d2..0000000000 --- a/SingularityUI/app/components/aggregateTail/Help.cjsx +++ /dev/null @@ -1,50 +0,0 @@ -React = require 'react' - -Help = React.createClass - - render: -> -
    -

    Log View Help

    -

    - The log view allows viewing and tailing of files inside the task sandbox. - The same file can be viewed across up to 6 tasks at time. -

    -

    Grep

    -

    - Enter a string here to only display lines in the file that contain a match. Regular expressions are supported. The button will turn blue to indicate a search string is being applied.
    - Shortcuts while the dropdown is open: -

    -
      -
    • return Commit the search
    • -
    • esc Clear the search
    • -
    -

    Select Instances

    -

    - This dropdown contains all running instances of the request. Select up to 6 of them to tail the file across.
    - Shortcuts while the dropdown is open: -

    -
      -
    • f Select (up to) the first 6 instances
    • -
    • l Select (up to) the last 6 instances
    • -
    • e Select (up to) the first 6 even-numbered instances
    • -
    • o Select (up to) the first 6 odd-numbered instances
    • -
    • [0-9] Select the corresponding numbered instance
    • -
    -

    Color Scheme

    -

    - Set the color scheme used by the file viewer. -

    -

    Unified/Split

    -

    - Toggle between Unified and Split views of the log lines.
    - Split View: Displays each task in a seperate pane.
    - Unified View: Interleaves the log lines, ordered based on timestamps contained in them. - This assumes Singularity is able to locate and parse timestamps in the file being viewed. -

    -

    Bottom/Top

    -

    - Jump to the bottom or top of the file(s) being viewed. -

    -
    - -module.exports = Help diff --git a/SingularityUI/app/components/aggregateTail/IndividualHeader.cjsx b/SingularityUI/app/components/aggregateTail/IndividualHeader.cjsx deleted file mode 100644 index 66ce53a5a4..0000000000 --- a/SingularityUI/app/components/aggregateTail/IndividualHeader.cjsx +++ /dev/null @@ -1,52 +0,0 @@ -React = require 'react' -ReactDOM = require 'react-dom' -StatusIndicator = require './StatusIndicator' - -IndividualHeader = React.createClass - - componentDidUpdate: (prevProps, prevState) -> - target = ReactDOM.findDOMNode(@refs.ttTarget) - if @props.task.task?.taskId? and target - $(target).tooltip(container: 'body', template: '') - - headerTarget = ReactDOM.findDOMNode(@refs.headerTarget) - if @props.taskState and @props.taskState != prevProps.taskState and headerTarget - if @props.taskState in ['TASK_KILLED', 'TASK_FAILED', 'TASK_LOST'] - $(headerTarget).addClass('status-changed-stopped') - else if @props.taskState is 'TASK_FINISHED' - $(headerTarget).addClass('status-changed-finished') - - componentWillUnmount: () -> - target = ReactDOM.findDOMNode(@refs.ttTarget) - if @props.task.task?.taskId? and target - $(target).tooltip('destroy') - - getTooltipText: -> - task = @props.task - "Deploy ID: #{task.task?.taskId?.deployId or ''}\nHost: #{task.task?.taskId?.host or ''}" - - renderClose: -> - if @props.onlyTask - return null - - - renderExpand: -> - if @props.onlyTask - return null - - - render: -> -
    - {@renderClose()} - {@renderExpand()} - - - - - - -
    - -module.exports = IndividualHeader diff --git a/SingularityUI/app/components/aggregateTail/IndividualTail.cjsx b/SingularityUI/app/components/aggregateTail/IndividualTail.cjsx deleted file mode 100644 index 4849829217..0000000000 --- a/SingularityUI/app/components/aggregateTail/IndividualTail.cjsx +++ /dev/null @@ -1,149 +0,0 @@ -React = require 'react' -BackboneReactComponent = require 'backbone-react-component' -IndividualHeader = require "./IndividualHeader" -Contents = require "./Contents" - -TaskHistory = require '../../models/TaskHistory' - -IndividualTail = React.createClass - mixins: [Backbone.React.Component.mixin] - - # ============================================================================ - # Lifecycle Methods | - # ============================================================================ - - componentWillMount: -> - # Get the task info - @task = new TaskHistory {taskId: @props.taskId} - @startTaskStatusPoll() - - @props.logLines.grep = @props.search - - # Automatically map backbone collections and models to the state of this component - Backbone.React.Component.mixin.on(@, { - collections: { - logLines: @props.logLines - }, - models: { - ajaxError: @props.ajaxError - task: @task - } - }); - - componentDidMount: -> - if @props.offset? - @props.logLines.fetchOffset(@props.offset) - else - @props.logLines.fetchInitialData() - - componentWillReceiveProps: (nextProps) -> - if nextProps.search isnt @props.search - @props.logLines.grep = nextProps.search - @props.logLines.reset() - @props.logLines.fetchInitialData().done _.delay(@refs.contents.scrollToBottom, 200) - - componentWillUnmount: -> - Backbone.React.Component.mixin.off(@) - @stopTaskStatusPoll() - - # ============================================================================ - # Event Handlers | - # ============================================================================ - - startTaskStatusPoll: -> - @task.fetch() - @taskPoll = setInterval => - @task.fetch() - , 5000 - - stopTaskStatusPoll: -> - clearInterval @taskPoll - - moreToFetch: -> - @props.logLines.state.get('moreToFetch') - - reachedStartOfFile: -> - @props.logLines.getMinOffset() is 0 - - reachedEndOfFile: -> - @props.logLines.state.get('reachedEndOfFile') - - fetchNext: -> - _.defer(@props.logLines.fetchNext) - - fetchPrevious: (callback) -> - @prevLineCount = @props.logLines.length - _.defer( => - xhr = @props.logLines.fetchPrevious() - if xhr - xhr.done => - newLines = @props.logLines.length - @prevLineCount - if newLines > 0 - @scrollToLine(newLines) - callback() - ) - - isTailing: -> - @refs.contents.isTailing() - - stopTailing: -> - @refs.contents.stopTailingPoll() - - startTailing: -> - @refs.contents.startTailingPoll() - - scrollToLine: (line) -> - @refs.contents.scrollToLine(line) - - scrollToTop: -> - @refs.contents.stopTailingPoll() - if @props.logLines.getMinOffset() is 0 - @refs.contents.scrollToTop() - else - @props.logLines.reset() - @props.logLines.fetchFromStart().done => - @refs.contents.scrollToTop() - @refs.contents.loadFromTop() - - scrollToBottom: -> - if @props.logLines.state.get('moreToFetch') is true - @props.logLines.reset() - @props.logLines.fetchInitialData().done _.delay(@refs.contents.scrollToBottom, 200) - else - @refs.contents.scrollToBottom() - - # ============================================================================ - # Rendering | - # ============================================================================ - - render: -> -
    - @props.closeTail(@props.taskId)} - expandTail={() => @props.expandTail(@props.taskId)} - taskState={_.last(@state.task.taskUpdates)?.taskState} - task={@state.task} - onlyTask={@props.onlyTask} /> - -
    - -module.exports = IndividualTail diff --git a/SingularityUI/app/components/aggregateTail/InterleavedHeader.cjsx b/SingularityUI/app/components/aggregateTail/InterleavedHeader.cjsx deleted file mode 100644 index 2b67b0ae41..0000000000 --- a/SingularityUI/app/components/aggregateTail/InterleavedHeader.cjsx +++ /dev/null @@ -1,27 +0,0 @@ -React = require 'react' -ColorLegend = require './ColorLegend' - -InterleavedHeader = React.createClass - - getInitialState: -> - @state = - showLegend: false - - toggleLegend: -> - @setState - showLegend: !@state.showLegend - - renderLegend: -> - if @state.showLegend - - - render: -> -
    - Viewing {@props.numTasks} Tasks - - - - {@renderLegend()} -
    - -module.exports = InterleavedHeader diff --git a/SingularityUI/app/components/aggregateTail/InterleavedTail.cjsx b/SingularityUI/app/components/aggregateTail/InterleavedTail.cjsx deleted file mode 100644 index 6442686f0c..0000000000 --- a/SingularityUI/app/components/aggregateTail/InterleavedTail.cjsx +++ /dev/null @@ -1,223 +0,0 @@ -React = require 'react' -BackboneReactComponent = require 'backbone-react-component' -InterleavedHeader = require "./InterleavedHeader" -Contents = require "./Contents" - -TaskHistory = require '../../models/TaskHistory' -LogLines = require '../../collections/LogLines' - -InterleavedTail = React.createClass - mixins: [Backbone.React.Component.mixin] - - # ============================================================================ - # Lifecycle Methods | - # ============================================================================ - - getInitialState: -> - @state = - mergedLines: [] - - componentWillMount: -> - # Get the task info - @task = new TaskHistory {taskId: @props.taskId} - - for logLines in @props.logLines - logLines.grep = @props.search - - models = {} - models.ajaxError = @props.ajaxErrors[0] - - Backbone.React.Component.mixin.on(@, { - models: models - }); - - componentDidMount: -> - for logLines in @props.logLines - logLines.reset() - - for logLines in @props.logLines - logLines.fetchInitialData(=> - @resetMergedLines() - @refs.contents.scrollToBottom() - ) - - componentWillReceiveProps: (nextProps) -> - if nextProps.logLines isnt @props.logLines - _.each(nextProps.logLines, (logLines) => logLines.reset()) - promises = [] - for logLines in nextProps.logLines - promises.push(logLines.fetchInitialData()) - Promise.all(promises).then => - @resetMergedLines() - if nextProps.search isnt @props.search - _.each(nextProps.logLines, (logLines) => - logLines.grep = nextProps.search - logLines.reset() - ) - for logLines in @props.logLines - logLines.fetchInitialData(=> - @resetMergedLines() - @refs.contents.scrollToBottom() - ) - - componentWillUnmount: -> - Backbone.React.Component.mixin.off(@) - - # ============================================================================ - # Event Handlers | - # ============================================================================ - - mergeLines: (lines, beginning = false) -> - if beginning - newLines = LogLines.merge(lines).concat(@state.mergedLines) - else - newLines = @state.mergedLines.concat(LogLines.merge lines) - @setState - mergedLines: newLines - - moreToFetch: -> - _.some(@props.logLines, (logLines) => - logLines.state.get('moreToFetch') - ) - - reachedStartOfFile: -> - _.some(@props.logLines, (logLines) -> logLines.getMinOffset() is 0) - - reachedEndOfFile: -> - _.some(@props.logLines, (logLines) -> logLines.state.get('reachedEndOfFile')) - - fetchNext: -> - promises = [] - oldLineCount = @props.logLines.map (logLines) => {taskId: logLines.taskId, length: logLines.length} - for logLines in @props.logLines - promises.push(logLines.fetchNext()) - - Promise.all(promises).then => _.delay => - newLineCount = @props.logLines.map (logLines) => {taskId: logLines.taskId, length: logLines.length} - deltas = newLineCount.map (count) => - taskId: count.taskId - delta: count.length - _.findWhere(oldLineCount, {taskId: count.taskId}).length - - newLines = [] - for delta in deltas - lines = _.findWhere(@props.logLines, {taskId: delta.taskId}).toJSON() - slice = lines.slice(lines.length - delta.delta, lines.length) - newLines.push(slice) - @mergeLines(newLines) - , 300 - - fetchPrevious: (callback) -> - promises = [] - oldLineCount = @props.logLines.map (logLines) => {taskId: logLines.taskId, length: logLines.length} - for logLines in @props.logLines - promises.push(logLines.fetchPrevious()) - - Promise.all(promises).then => _.delay => - newLineCount = @props.logLines.map (logLines) => {taskId: logLines.taskId, length: logLines.length} - deltas = newLineCount.map (count) => - taskId: count.taskId - delta: count.length - _.findWhere(oldLineCount, {taskId: count.taskId}).length - - newLines = [] - for delta in deltas - lines = _.findWhere(@props.logLines, {taskId: delta.taskId}).toJSON() - slice = lines.slice(0, delta.delta) - newLines.push(slice) - - @mergeLines(newLines, true) - totalNew = _.reduce(deltas, (memo, delta) => - memo + delta.delta - , 0) - if totalNew > 0 - @scrollToLine(totalNew) - callback() - , 300 - - isTailing: -> - @refs.contents.isTailing() - - stopTailing: -> - @refs.contents.stopTailingPoll() - - startTailing: -> - @refs.contents.startTailingPoll() - - scrollToLine: (line) -> - @refs.contents.scrollToLine(line) - - scrollToTop: -> - @refs.contents.stopTailingPoll() - if _.every(@props.logLines, (logLines) => logLines.getMinOffset() is 0) - @refs.contents.scrollToTop() - else - _.each(@props.logLines, (logLines) => logLines.reset()) - promises = [] - _.each(@props.logLines, (logLines) => promises.push(logLines.fetchFromStart())) - Promise.all(promises).then => - @resetMergedLines() - @refs.contents.scrollToTop() - @refs.contents.loadFromTop() - - scrollToBottom: -> - if _.every(@props.logLines, (logLines) => logLines.state.get('moreToFetch') is true) - _.each(@props.logLines, (logLines) => logLines.reset()) - for logLines in @props.logLines - logLines.fetchInitialData(=> - @resetMergedLines() - @refs.contents.scrollToBottom() - ) - else - @refs.contents.scrollToBottom() - - resetMergedLines: -> - @setState - mergedLines: [] - @mergeLines(@props.logLines.map((logLines) => logLines.toJSON())) - - # ============================================================================ - # Rendering | - # ============================================================================ - - taskIdToColorMap: (logLines) -> - if !logLines - return {} - - map = {} - taskIds = _.uniq(logLines.map((line) => - line.taskId - )).sort() - if taskIds.length is 1 - map[taskIds[0]] = 'hsla(0, 0, 0, 0)' - else - interval = 360 / taskIds.length - colors = taskIds.map (taskId, i) => - "hsla(#{interval * i}, 100%, 50%, 0.1)" - for taskId, i in taskIds - map[taskId] = colors[i] - map - - render: -> -
    - - -
    - -module.exports = InterleavedTail diff --git a/SingularityUI/app/components/aggregateTail/Loader.cjsx b/SingularityUI/app/components/aggregateTail/Loader.cjsx deleted file mode 100644 index 5eab694a75..0000000000 --- a/SingularityUI/app/components/aggregateTail/Loader.cjsx +++ /dev/null @@ -1,13 +0,0 @@ -React = require 'react' - -Loader = React.createClass - - render: -> - if @props.isVisable -
    -
    {@props.text}
    -
    - else -
    - -module.exports = Loader diff --git a/SingularityUI/app/components/aggregateTail/LogLine.cjsx b/SingularityUI/app/components/aggregateTail/LogLine.cjsx deleted file mode 100644 index 4da81615d1..0000000000 --- a/SingularityUI/app/components/aggregateTail/LogLine.cjsx +++ /dev/null @@ -1,62 +0,0 @@ -React = require 'react' - -LogLine = React.createClass - - shouldComponentUpdate: (nextProps) -> - (@props.offset isnt nextProps.offset) or (@props.isHighlighted isnt nextProps.isHighlighted) or (@props.content isnt nextProps.content) or (@props.isLastLine isnt nextProps.isLastLine) or (@props.isFirstLine isnt nextProps.isFirstLine) - - highlightContent: (content) -> - search = @props.search - if not search or _.isEmpty(search) - return content - - regex = RegExp(search, 'g') - matches = [] - - while m = regex.exec(content) - matches.push(m) - - sections = [] - lastEnd = 0 - for m in matches - last = - text: content.slice(lastEnd, m.index) - match: false - sect = - text: content.slice(m.index, m.index + m[0].length) - match: true - sections.push last, sect - lastEnd = m.index + m[0].length - sections.push - text: content.slice(lastEnd) - match: false - - sections.map (s, i) => - spanClass = classNames - 'search-match': s.match - {s.text} - - handleClick: (e) -> - e.preventDefault() - window.history.pushState({}, window.document.title, @props.offsetLink) # have to do it this janky way because of the hash - @props.handleOffsetLink(@props.offset) - - render: -> - divClass = classNames - line: true - highlightLine: @props.isHighlighted - 'first-line': @props.isFirstLine - 'last-line': @props.isLastLine - -
    - -
    - -
    -
    - - {@highlightContent(@props.content)} - -
    - -module.exports = LogLine diff --git a/SingularityUI/app/components/aggregateTail/StatusIndicator.cjsx b/SingularityUI/app/components/aggregateTail/StatusIndicator.cjsx deleted file mode 100644 index 8f850f3b42..0000000000 --- a/SingularityUI/app/components/aggregateTail/StatusIndicator.cjsx +++ /dev/null @@ -1,21 +0,0 @@ -React = require 'react' -Utils = require '../../utils' - -StatusIndicator = React.createClass - - getClassName: -> - if @props.status in Utils.TERMINAL_TASK_STATES - 'bg-danger' - else - 'bg-info running' - - render: -> - if @props.status -
    -
    - {@props.status.toLowerCase().replace('_', ' ')} -
    - else -
    - -module.exports = StatusIndicator From d74df2dcbbe40671a1e951783fb3286dd9724b17 Mon Sep 17 00:00:00 2001 From: tpetr Date: Tue, 12 Apr 2016 13:42:43 -0400 Subject: [PATCH 34/44] fix tail end of file --- SingularityUI/app/actions/log.coffee | 11 ++++++++--- SingularityUI/app/components/logs/LogLines.cjsx | 10 +++++----- SingularityUI/app/reducers/taskGroups.coffee | 11 ++++++----- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/SingularityUI/app/actions/log.coffee b/SingularityUI/app/actions/log.coffee index 31a072add7..6a98132e27 100644 --- a/SingularityUI/app/actions/log.coffee +++ b/SingularityUI/app/actions/log.coffee @@ -117,7 +117,7 @@ updateGroups = -> unless taskGroup.pendingRequests if taskGroup.top dispatch(taskGroupFetchPrevious(taskGroupId)) - if taskGroup.bottom + if taskGroup.bottom or taskGroup.tailing dispatch(taskGroupFetchNext(taskGroupId)) updateTaskStatuses = -> @@ -214,9 +214,14 @@ taskGroupTop = (taskGroupId, visible) -> if visible dispatch(taskGroupFetchPrevious(taskGroupId)) -taskGroupBottom = (taskGroupId, visible) -> +taskGroupBottom = (taskGroupId, visible, tailing=false) -> (dispatch, getState) -> - if getState().taskGroups[taskGroupId].bottom != visible + { taskGroups, tasks } = getState() + taskGroup = taskGroups[taskGroupId] + if taskGroup.tailing != tailing + if tailing is false or _.all(getTasks(taskGroup, tasks).map(({maxOffset, filesize}) -> maxOffset >= filesize)) + dispatch({taskGroupId, tailing, type: 'LOG_TASK_GROUP_TAILING'}) + if taskGroup.bottom != visible dispatch({taskGroupId, visible, type: 'LOG_TASK_GROUP_BOTTOM'}) if visible dispatch(taskGroupFetchNext(taskGroupId)) diff --git a/SingularityUI/app/components/logs/LogLines.cjsx b/SingularityUI/app/components/logs/LogLines.cjsx index b4f89db93f..299c9d3ab7 100644 --- a/SingularityUI/app/components/logs/LogLines.cjsx +++ b/SingularityUI/app/components/logs/LogLines.cjsx @@ -34,10 +34,10 @@ class LogLines extends React.Component window.removeEventListener 'resize', @handleScroll componentDidUpdate: (prevProps, prevState) -> - if @props.tailing - @refs.tailContents.scrollTop = @refs.tailContents.scrollHeight - else if prevProps.updatedAt isnt @props.updatedAt - if @props.prependedLineCount > 0 or @props.linesRemovedFromTop > 0 + if prevProps.updatedAt isnt @props.updatedAt + if @props.tailing + @refs.tailContents.scrollTop = @refs.tailContents.scrollHeight + else if @props.prependedLineCount > 0 or @props.linesRemovedFromTop > 0 @refs.tailContents.scrollTop += 20 * (@props.prependedLineCount - @props.linesRemovedFromTop) else @handleScroll() @@ -77,7 +77,7 @@ class LogLines extends React.Component @props.taskGroupTop(@props.taskGroupId, false) if scrollTop + clientHeight > scrollHeight - clientHeight - @props.taskGroupBottom(@props.taskGroupId, true) + @props.taskGroupBottom(@props.taskGroupId, true, (scrollTop + clientHeight > scrollHeight - 20)) else @props.taskGroupBottom(@props.taskGroupId, false) diff --git a/SingularityUI/app/reducers/taskGroups.coffee b/SingularityUI/app/reducers/taskGroups.coffee index d147d77b0a..68921c60b2 100644 --- a/SingularityUI/app/reducers/taskGroups.coffee +++ b/SingularityUI/app/reducers/taskGroups.coffee @@ -87,11 +87,14 @@ ACTIONS = { # The logger has either entered or exited the bottom LOG_TASK_GROUP_BOTTOM: (state, {taskGroupId, visible}) -> - return updateTaskGroup(state, taskGroupId, {bottom: visible, tailing: false}) + return updateTaskGroup(state, taskGroupId, {bottom: visible}) # An entire task group is ready LOG_TASK_GROUP_READY: (state, {taskGroupId}) -> - return updateTaskGroup(state, taskGroupId, {ready: true, updatedAt: +new Date()}) + return updateTaskGroup(state, taskGroupId, {ready: true, updatedAt: +new Date(), tailing: true}) + + LOG_TASK_GROUP_TAILING: (state, {taskGroupId, tailing}) -> + return updateTaskGroup(state, taskGroupId, {tailing}) LOG_REMOVE_TASK_GROUP: (state, {taskGroupId}) -> newState = [] @@ -140,9 +143,7 @@ ACTIONS = { # bail early if no data if data.length is 0 and task.loadedData - newState = Object.assign([], state) - newState[taskGroupId] = Object.assign({}, taskGroup, {tailing: append and data.length is 0}) - return newState + return state # split task data into separate lines, attempt to parse timestamp currentOffset = offset From d662035033937b2c284dcb47600b4deded5aa2ca Mon Sep 17 00:00:00 2001 From: tpetr Date: Tue, 12 Apr 2016 14:33:46 -0400 Subject: [PATCH 35/44] remove unnecessary variable --- SingularityUI/app/actions/log.coffee | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/SingularityUI/app/actions/log.coffee b/SingularityUI/app/actions/log.coffee index 6a98132e27..ebcbf67fdf 100644 --- a/SingularityUI/app/actions/log.coffee +++ b/SingularityUI/app/actions/log.coffee @@ -187,7 +187,7 @@ taskGroupFetchPrevious = (taskGroupId) -> Promise.all(promises).then -> dispatch({taskGroupId, type: 'LOG_REQUEST_END'}) -taskData = (taskGroupId, taskId, data, offset, nextOffset, append, maxLines, buffer) -> +taskData = (taskGroupId, taskId, data, offset, nextOffset, append, maxLines) -> { taskGroupId taskId @@ -196,7 +196,6 @@ taskData = (taskGroupId, taskId, data, offset, nextOffset, append, maxLines, buf nextOffset append maxLines - buffer type: 'LOG_TASK_DATA' } From 79cee5dfa9469cdc0a547d3a3005eabfd46dee41 Mon Sep 17 00:00:00 2001 From: tpetr Date: Tue, 12 Apr 2016 14:34:26 -0400 Subject: [PATCH 36/44] update updatedAt when task group is reset --- SingularityUI/app/reducers/taskGroups.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/SingularityUI/app/reducers/taskGroups.coffee b/SingularityUI/app/reducers/taskGroups.coffee index 68921c60b2..8fedad9b5f 100644 --- a/SingularityUI/app/reducers/taskGroups.coffee +++ b/SingularityUI/app/reducers/taskGroups.coffee @@ -26,6 +26,7 @@ resetTaskGroup = (tailing=false) -> { taskBuffer: {} top: false bottom: false + updatedAt: +new Date() tailing } From 3a7046f9992693375421c7e2cf203bc5f1b34a92 Mon Sep 17 00:00:00 2001 From: tpetr Date: Tue, 12 Apr 2016 16:09:12 -0400 Subject: [PATCH 37/44] fix adding group during search + improve top / bottom labels --- SingularityUI/app/actions/log.coffee | 5 +++-- SingularityUI/app/components/logs/LogLines.cjsx | 16 +++++++++++++--- SingularityUI/app/reducers/taskGroups.coffee | 8 ++++---- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/SingularityUI/app/actions/log.coffee b/SingularityUI/app/actions/log.coffee index ebcbf67fdf..8c0cf26351 100644 --- a/SingularityUI/app/actions/log.coffee +++ b/SingularityUI/app/actions/log.coffee @@ -65,9 +65,10 @@ init = (requestId, taskIdGroups, path, search) -> type: 'LOG_INIT' } -addTaskGroup = (taskIds) -> +addTaskGroup = (taskIds, search) -> { taskIds + search type: 'LOG_ADD_TASK_GROUP' } @@ -262,7 +263,7 @@ toggleTaskLog = (taskId) -> dispatch({taskId, type: 'LOG_REMOVE_TASK'}) else if viewMode is 'split' - dispatch(addTaskGroup([taskId])) + dispatch(addTaskGroup([taskId], search)) resolvedPath = path.replace('$TASK_ID', taskId) diff --git a/SingularityUI/app/components/logs/LogLines.cjsx b/SingularityUI/app/components/logs/LogLines.cjsx index 299c9d3ab7..94b2a07f6f 100644 --- a/SingularityUI/app/components/logs/LogLines.cjsx +++ b/SingularityUI/app/components/logs/LogLines.cjsx @@ -45,7 +45,10 @@ class LogLines extends React.Component renderLoadingPrevious: -> if @props.initialDataLoaded if not @props.reachedStartOfFile -
    Loading previous... ({Humanize.filesize(@props.bytesRemainingBefore)} remaining)
    + if @props.search +
    Searching for '{@props.search}'... ({Humanize.filesize(@props.bytesRemainingBefore)} remaining)
    + else +
    Loading previous... ({Humanize.filesize(@props.bytesRemainingBefore)} remaining)
    renderLogLines: -> @props.logLines.map ({data, offset, taskId, timestamp}) => @@ -63,9 +66,15 @@ class LogLines extends React.Component return null if @props.initialDataLoaded if @props.reachedEndOfFile -
    Tailing...
    + if @props.search +
    Tailing for '{@props.search}'...
    + else +
    Tailing...
    else -
    Loading more... ({Humanize.filesize(@props.bytesRemainingAfter)} remaining)
    + if @props.search +
    Searching for '{@props.search}'... ({Humanize.filesize(@props.bytesRemainingAfter)} remaining)
    + else +
    Loading more... ({Humanize.filesize(@props.bytesRemainingAfter)} remaining)
    handleScroll: => @@ -116,6 +125,7 @@ mapStateToProps = (state, ownProps) -> bytesRemainingBefore: sum(_.pluck(tasks, 'minOffset')) bytesRemainingAfter: sum(tasks.map ({filesize, maxOffset}) -> Math.max(filesize - maxOffset, 0)) colorMap: colorMap + search: state.search mapDispatchToProps = { taskGroupTop, taskGroupBottom } diff --git a/SingularityUI/app/reducers/taskGroups.coffee b/SingularityUI/app/reducers/taskGroups.coffee index 8fedad9b5f..698ba461f5 100644 --- a/SingularityUI/app/reducers/taskGroups.coffee +++ b/SingularityUI/app/reducers/taskGroups.coffee @@ -24,8 +24,8 @@ buildTaskGroup = (taskIds, search) -> resetTaskGroup = (tailing=false) -> { logLines: [] taskBuffer: {} - top: false - bottom: false + top: true + bottom: true updatedAt: +new Date() tailing } @@ -59,8 +59,8 @@ ACTIONS = { return taskIdGroups.map (taskIds) -> buildTaskGroup(taskIds, search) # Add a group of tasks to the logger - LOG_ADD_TASK_GROUP: (state, {taskIds}) -> - newState = state.concat(buildTaskGroup(taskIds, state.search)) + LOG_ADD_TASK_GROUP: (state, {taskIds, search}) -> + newState = state.concat(buildTaskGroup(taskIds, search)) return _.sortBy(newState, (taskGroup) -> getInstanceNumberFromTaskId(taskGroup.taskIds[0])) # Remove a task from the logger From ac27d1f74b09fbef23c6c648e0b7bbecc120cad1 Mon Sep 17 00:00:00 2001 From: tpetr Date: Tue, 12 Apr 2016 16:30:33 -0400 Subject: [PATCH 38/44] fix task toggle + misc cleanup --- SingularityUI/app/actions/log.coffee | 6 +++++- SingularityUI/app/components/logs/Header.cjsx | 3 --- SingularityUI/app/components/logs/TasksDropdown.cjsx | 5 +---- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/SingularityUI/app/actions/log.coffee b/SingularityUI/app/actions/log.coffee index 8c0cf26351..381817a1aa 100644 --- a/SingularityUI/app/actions/log.coffee +++ b/SingularityUI/app/actions/log.coffee @@ -259,8 +259,12 @@ setCurrentSearch = (newSearch) -> # TODO: can we do something less heavyweight? toggleTaskLog = (taskId) -> (dispatch, getState) -> {search, path, tasks, viewMode} = getState() - if taskId of tasks and Object.keys(tasks).length > 1 + if taskId of tasks + # only remove task if it's not the last one + if Object.keys(tasks).length > 1 dispatch({taskId, type: 'LOG_REMOVE_TASK'}) + else + return else if viewMode is 'split' dispatch(addTaskGroup([taskId], search)) diff --git a/SingularityUI/app/components/logs/Header.cjsx b/SingularityUI/app/components/logs/Header.cjsx index bcf62fcbd0..71ed1a98cf 100644 --- a/SingularityUI/app/components/logs/Header.cjsx +++ b/SingularityUI/app/components/logs/Header.cjsx @@ -17,9 +17,6 @@ class Header extends React.Component scrollAllToBottom: React.PropTypes.func.isRequired scrollAllToTop: React.PropTypes.func.isRequired - toggleHelp: -> - # TODO - renderBreadcrumbs: -> @props.path.split('/').map (subpath, i) -> if subpath is '$TASK_ID' diff --git a/SingularityUI/app/components/logs/TasksDropdown.cjsx b/SingularityUI/app/components/logs/TasksDropdown.cjsx index 7e009f47d6..5388106e66 100644 --- a/SingularityUI/app/components/logs/TasksDropdown.cjsx +++ b/SingularityUI/app/components/logs/TasksDropdown.cjsx @@ -4,9 +4,6 @@ React = require 'react' { connect } = require 'react-redux' class TasksDropdown extends React.Component - handleTasksKeyDown: -> - # TODO - renderListItems: -> if @props.activeTasks and @props.taskIds tasks = _.sortBy(@props.activeTasks, (t) => t.taskId.instanceNo).map (task, i) => @@ -25,7 +22,7 @@ class TasksDropdown extends React.Component render: ->
    -
      From 3d62da0bfcf64d84655a97dda51919c162d5c00c Mon Sep 17 00:00:00 2001 From: tpetr Date: Tue, 12 Apr 2016 18:09:57 -0400 Subject: [PATCH 39/44] force polling when a task group is ready --- SingularityUI/app/reducers/taskGroups.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SingularityUI/app/reducers/taskGroups.coffee b/SingularityUI/app/reducers/taskGroups.coffee index 698ba461f5..bc752c6e02 100644 --- a/SingularityUI/app/reducers/taskGroups.coffee +++ b/SingularityUI/app/reducers/taskGroups.coffee @@ -92,7 +92,7 @@ ACTIONS = { # An entire task group is ready LOG_TASK_GROUP_READY: (state, {taskGroupId}) -> - return updateTaskGroup(state, taskGroupId, {ready: true, updatedAt: +new Date(), tailing: true}) + return updateTaskGroup(state, taskGroupId, {ready: true, updatedAt: +new Date(), top: true, bottom: true, tailing: true}) LOG_TASK_GROUP_TAILING: (state, {taskGroupId, tailing}) -> return updateTaskGroup(state, taskGroupId, {tailing}) From 92369694cb321778b08acd5369fa987478926f8c Mon Sep 17 00:00:00 2001 From: tpetr Date: Tue, 12 Apr 2016 18:30:35 -0400 Subject: [PATCH 40/44] further cleanup --- SingularityUI/app/reducers/taskGroups.coffee | 29 ++++++-------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/SingularityUI/app/reducers/taskGroups.coffee b/SingularityUI/app/reducers/taskGroups.coffee index bc752c6e02..0e0d374fa5 100644 --- a/SingularityUI/app/reducers/taskGroups.coffee +++ b/SingularityUI/app/reducers/taskGroups.coffee @@ -60,14 +60,14 @@ ACTIONS = { # Add a group of tasks to the logger LOG_ADD_TASK_GROUP: (state, {taskIds, search}) -> - newState = state.concat(buildTaskGroup(taskIds, search)) - return _.sortBy(newState, (taskGroup) -> getInstanceNumberFromTaskId(taskGroup.taskIds[0])) + return _.sortBy(state.concat(buildTaskGroup(taskIds, search)), (taskGroup) -> getInstanceNumberFromTaskId(taskGroup.taskIds[0])) # Remove a task from the logger LOG_REMOVE_TASK: (state, {taskId}) -> newState = [] for taskGroup in state if taskId in taskGroup.taskIds + # remove task group if it only has one task if taskGroup.taskIds.length is 1 continue @@ -107,36 +107,23 @@ ACTIONS = { LOG_EXPAND_TASK_GROUP: (state, {taskGroupId}) -> return [state[taskGroupId]] - # The logger has been asked to scroll to the top - LOG_SCROLL_ALL_GROUPS_TO_TOP: (state) -> - return state.map (taskGroup) -> - Object.assign({}, taskGroup, resetTaskGroup()) - LOG_SCROLL_TO_TOP: (state, {taskGroupId}) -> - newState = Object.assign([], state) - newState[taskGroupId] = Object.assign({}, state[taskGroupId], resetTaskGroup()) - return newState + return updateTaskGroup(state, taskGroupId, resetTaskGroup()) LOG_SCROLL_ALL_TO_TOP: (state) -> - state.map (taskGroup) -> Object.assign({}, taskGroup, resetTaskGroup()) + return state.map (taskGroup) -> Object.assign({}, taskGroup, resetTaskGroup()) LOG_SCROLL_TO_BOTTOM: (state, {taskGroupId}) -> - newState = Object.assign([], state) - newState[taskGroupId] = Object.assign({}, state[taskGroupId], resetTaskGroup(true)) - return newState + return updateTaskGroup(state, taskGroupId, resetTaskGroup(true)) LOG_SCROLL_ALL_TO_BOTTOM: (state) -> - state.map (taskGroup) -> Object.assign({}, taskGroup, resetTaskGroup(true)) + return state.map (taskGroup) -> Object.assign({}, taskGroup, resetTaskGroup(true)) LOG_REQUEST_START: (state, {taskGroupId}) -> - newState = Object.assign([], state) - newState[taskGroupId] = Object.assign({}, state[taskGroupId], {pendingRequests: true}) - return newState + return updateTaskGroup(state, taskGroupId, {pendingRequests: true}) LOG_REQUEST_END: (state, {taskGroupId}) -> - newState = Object.assign([], state) - newState[taskGroupId] = Object.assign({}, state[taskGroupId], {pendingRequests: false}) - return newState + return updateTaskGroup(state, taskGroupId, {pendingRequests: false}) # We've received logging data for a task LOG_TASK_DATA: (state, {taskGroupId, taskId, offset, nextOffset, maxLines, data, append}) -> From fe873a40c96caea1f2eb0b9ec7d134dbcefaf83e Mon Sep 17 00:00:00 2001 From: tpetr Date: Fri, 15 Apr 2016 14:30:52 -0400 Subject: [PATCH 41/44] forgot one of the react-waypoints to remove --- SingularityUI/webpack.config.js | 1 - 1 file changed, 1 deletion(-) diff --git a/SingularityUI/webpack.config.js b/SingularityUI/webpack.config.js index c83f0b5d3c..d57654d2e0 100644 --- a/SingularityUI/webpack.config.js +++ b/SingularityUI/webpack.config.js @@ -18,7 +18,6 @@ module.exports = { 'bootstrap', 'classnames', 'react-interval', - 'react-waypoint', 'backbone-react-component', 'react-dom', 'fuzzy', From 08f1a078f7ae4899908b01522bd82949b8dab44c Mon Sep 17 00:00:00 2001 From: tpetr Date: Fri, 15 Apr 2016 16:10:32 -0400 Subject: [PATCH 42/44] set NODE_ENV to production --- SingularityUI/webpack.config.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/SingularityUI/webpack.config.js b/SingularityUI/webpack.config.js index d57654d2e0..2ec7e01750 100644 --- a/SingularityUI/webpack.config.js +++ b/SingularityUI/webpack.config.js @@ -67,6 +67,9 @@ module.exports = { warnings: false } }), + new webpack.DefinePlugin({ + 'process.env.NODE_ENV': '"production"' + }), new webpack.optimize.CommonsChunkPlugin('vendor', 'vendor.bundle.js'), ] }; From 926290f722785b5003b2d6ec5fd8ef09248814af Mon Sep 17 00:00:00 2001 From: tpetr Date: Tue, 19 Apr 2016 12:13:00 -0400 Subject: [PATCH 43/44] right-align dropdowns in tailer --- SingularityUI/app/components/logs/ColorDropdown.cjsx | 2 +- SingularityUI/app/components/logs/SearchDropdown.cjsx | 2 +- SingularityUI/app/components/logs/TasksDropdown.cjsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/SingularityUI/app/components/logs/ColorDropdown.cjsx b/SingularityUI/app/components/logs/ColorDropdown.cjsx index 5b0f6427b6..9a269fa327 100644 --- a/SingularityUI/app/components/logs/ColorDropdown.cjsx +++ b/SingularityUI/app/components/logs/ColorDropdown.cjsx @@ -23,7 +23,7 @@ class ColorDropdown extends React.Component -
        +
          {@renderColorChoices()}
    diff --git a/SingularityUI/app/components/logs/SearchDropdown.cjsx b/SingularityUI/app/components/logs/SearchDropdown.cjsx index 7ab0c82cbc..af581831fb 100644 --- a/SingularityUI/app/components/logs/SearchDropdown.cjsx +++ b/SingularityUI/app/components/logs/SearchDropdown.cjsx @@ -37,7 +37,7 @@ class SearchDropdown extends React.Component -
      +
      • @setState({searchValue: e.target.value})} /> diff --git a/SingularityUI/app/components/logs/TasksDropdown.cjsx b/SingularityUI/app/components/logs/TasksDropdown.cjsx index 5388106e66..2f8c414936 100644 --- a/SingularityUI/app/components/logs/TasksDropdown.cjsx +++ b/SingularityUI/app/components/logs/TasksDropdown.cjsx @@ -25,7 +25,7 @@ class TasksDropdown extends React.Component -
          +
            {@renderListItems()}
        From 763ed694fe636b988122ff1f5a043eb36efbd0fe Mon Sep 17 00:00:00 2001 From: tpetr Date: Mon, 25 Apr 2016 18:34:23 -0400 Subject: [PATCH 44/44] strip carriage returns --- SingularityUI/app/reducers/taskGroups.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/SingularityUI/app/reducers/taskGroups.coffee b/SingularityUI/app/reducers/taskGroups.coffee index 0e0d374fa5..e8f51b9530 100644 --- a/SingularityUI/app/reducers/taskGroups.coffee +++ b/SingularityUI/app/reducers/taskGroups.coffee @@ -138,6 +138,8 @@ ACTIONS = { lines = _.initial(data.match /[^\n]*(\n|$)/g).map (data) -> currentOffset += data.length + data = data.replace('\r', '') # carriage return screws stuff up + timestamp = parseLineTimestamp(data) if timestamp