diff --git a/SingularityUI/app/actions/api/history.es6 b/SingularityUI/app/actions/api/history.es6 index b8e4baa771..7b62fdb50a 100644 --- a/SingularityUI/app/actions/api/history.es6 +++ b/SingularityUI/app/actions/api/history.es6 @@ -1,4 +1,5 @@ import { buildApiAction } from './base'; +import Utils from '../../utils'; export const FetchTaskHistory = buildApiAction( 'FETCH_TASK_HISTORY', @@ -37,6 +38,23 @@ export const FetchDeployForRequest = buildApiAction( }) ); +export const FetchTaskSearchParams = buildApiAction( + 'FETCH_TASK_HISTORY', + ({requestId = null, deployId = null, host = null, lastTaskStatus = null, startedAfter = null, startedBefore = null, orderDirection = null, count, page}) => { + const args = { + requestId, + deployId, + host, + lastTaskStatus, + startedAfter, + startedBefore, + orderDirection + }; + return { + url: `/history/tasks?count=${count}&page=${page}&${Utils.queryParams(args)}` + }; +}); + export const FetchRequestRunHistory = buildApiAction( 'FETCH_REQUEST_RUN_HISTORY', (requestId, runId) => ({ diff --git a/SingularityUI/app/components/common/atomicDisplayItems/TaskStateLabel.jsx b/SingularityUI/app/components/common/atomicDisplayItems/TaskStateLabel.jsx index dfbac7ca1a..b61e2ab070 100644 --- a/SingularityUI/app/components/common/atomicDisplayItems/TaskStateLabel.jsx +++ b/SingularityUI/app/components/common/atomicDisplayItems/TaskStateLabel.jsx @@ -18,4 +18,3 @@ let TaskStateLabel = React.createClass({ }); export default TaskStateLabel; - diff --git a/SingularityUI/app/components/common/formItems/DateEntry.jsx b/SingularityUI/app/components/common/formItems/DateEntry.jsx deleted file mode 100644 index 44bf422ec1..0000000000 --- a/SingularityUI/app/components/common/formItems/DateEntry.jsx +++ /dev/null @@ -1,55 +0,0 @@ -import React from 'react'; -import moment from 'moment'; -import FormField from './FormField'; -import Glyphicon from '../atomicDisplayItems/Glyphicon'; -import datetimepicker from 'eonasdan-bootstrap-datetimepicker'; - -let DateEntry = React.createClass({ - - componentWillReceiveProps(nextProps) { - let id = `#${ this.props.id }`; - //datetimepicker = $(id).data('datetimepicker'); - if (datetimepicker) { - if (nextProps.prop.value !== this.props.prop.value) { - if (!nextProps.prop.value) { - return datetimepicker.setDate(null); - } - } - } - }, - - initializeDateTimePicker() { - let id = `#${ this.props.id }`; - let changeFn = this.props.prop.updateFn; - return $(() => $(id).datetimepicker({ - sideBySide: true, - format: window.config.timestampFormat - }).on('dp.change', changeFn)); // value will be in event.date - }, - - getValue() { - if (!this.props.prop.value) { - return; - } - let time = moment(this.props.prop.value); - return time.format(window.config.timestampFormat); - }, - - // MUST pass in UNIQUE id in props. - // Otherwise the datetime picker will break in ways that aren't even very interesting - render() { - return
; - } -}); - -export default DateEntry; - diff --git a/SingularityUI/app/components/common/formItems/ReduxSelect.jsx b/SingularityUI/app/components/common/formItems/ReduxSelect.jsx new file mode 100644 index 0000000000..5e769b65f7 --- /dev/null +++ b/SingularityUI/app/components/common/formItems/ReduxSelect.jsx @@ -0,0 +1,13 @@ +import React from 'react'; +import Select from 'react-select'; + +// Wrapper for react-select for use with redux form. Needs to override onBlur of react-select +// More info: https://github.com/erikras/redux-form/issues/82 +export default (props) => { + return ( + + +
+ + +
+
+ + +
+ +
+
+ +
+
+ +
+
+ +
+
+ {startedAfter.error || startedBefore.error} +
+ + +
+
+ +
+
+ + + + + ); + } +} + +const validate = values => { + const errors = {}; + if (values.dateStart && !moment(parseInt(values.dateStart, 10)).isValid()) { + errors.dateStart = 'Please enter a valid date'; + } + if (values.dateEnd && !moment(parseInt(values.dateEnd, 10)).isValid()) { + errors.dateEnd = 'Please enter a valid date'; + } + if (values.dateStart && values.dateEnd && parseInt(values.dateEnd, 10) < parseInt(values.dateStart, 10)) { + errors.dateEnd = 'End date must be after start'; + } + + return errors; +}; + +function mapStateToProps(state, ownProps) { + return { + initialValues: { + requestId: ownProps.requestId || '', + dateStart: null, + dateEnd: null + } + }; +} + +export default reduxForm({ + form: 'taskSearch', + fields: ['requestId', 'deployId', 'host', 'startedAfter', 'startedBefore', 'lastTaskStatus'], + validate +}, mapStateToProps)(TaskSearchFilters); diff --git a/SingularityUI/app/components/taskSearch/TaskSearchForm.jsx b/SingularityUI/app/components/taskSearch/TaskSearchForm.jsx deleted file mode 100644 index af432689df..0000000000 --- a/SingularityUI/app/components/taskSearch/TaskSearchForm.jsx +++ /dev/null @@ -1,93 +0,0 @@ -import React from 'react'; -import Utils from '../../utils'; - -import FormField from '../common/formItems/FormField'; -import DropDown from '../common/formItems/DropDown'; -import DateEntry from '../common/formItems/DateEntry'; -import LinkedFormItem from '../common/formItems/LinkedFormItem'; -import Enums from './Enums'; - -let TaskSearchForm = React.createClass({ - - getRequestIdTitle() { - return
Request ID{this.props.requestIdCurrentSearch ? {this.props.requestIdCurrentSearch} : undefined}
; - }, - - getDeployIdTitle() { - return
Deploy ID{this.props.deployIdCurrentSearch ? {this.props.deployIdCurrentSearch} : undefined}
; - }, - - getHostTitle() { - return
Host{this.props.hostCurrentSearch ? {this.props.hostCurrentSearch} : undefined}
; - }, - - getStartedBetweenTitle() { - return
TODO: fix
- }, - /* return
- Started Between - {if (this.props.startedAfterCurrentSearch && this.props.startedBeforeCurrentSearch) { - return {@props.startedAfterCurrentSearch} - {@props.startedBeforeCurrentSearch} - } else if (this.props.startedAfterCurrentSearch) { - return After {@props.startedAfterCurrentSearch} - } else if (this.props.startedBeforeCurrentSearch) { - Before {@props.startedBeforeCurrentSearch} - }} -
- },*/ - - getLastTaskStatusTitle() { - return
Last Task Status{this.props.lastTaskStatusCurrentSearch ? {this.props.lastTaskStatusCurrentSearch} : undefined}
; - }, - - render() { - ({ render() {} }); - return
; - } -}); - -export default TaskSearchForm; - diff --git a/SingularityUI/app/components/taskSearch/TasksTable.jsx b/SingularityUI/app/components/taskSearch/TasksTable.jsx new file mode 100644 index 0000000000..815d1dd464 --- /dev/null +++ b/SingularityUI/app/components/taskSearch/TasksTable.jsx @@ -0,0 +1,69 @@ +import React from 'react'; + +import { Table, Pagination } from 'react-bootstrap'; + +export default class TasksTable extends React.Component { + + renderHeaders() { + let row = this.props.headers.map((h, i) => { + return {h}; + }); + return {row}; + } + + renderTableRows() { + const rows = this.props.data.map((e, i) => { + return this.props.renderTableRow(e, i); + }); + return rows; + } + + renderPagination() { + if (this.props.paginate) { + return ( +
+ this.props.onPage(selectedEvent.eventKey)} /> +
+ ); + } + } + + renderTable() { + return ( +
+ + + {this.renderHeaders()} + + + {this.renderTableRows()} + +
+
+ ); + } + + render() { + if (this.props.data.length) { + return ( +
+ {this.renderTable()} + {this.renderPagination()} +
+ ); + } else { + return ( +
+ {this.props.emptyMessage} +
+ ); + } + } +} diff --git a/SingularityUI/app/controllers/TaskSearch.coffee b/SingularityUI/app/controllers/TaskSearch.coffee deleted file mode 100644 index ca7b6296bb..0000000000 --- a/SingularityUI/app/controllers/TaskSearch.coffee +++ /dev/null @@ -1,28 +0,0 @@ -Controller = require './Controller' - -TaskSearchView = require('../views/taskSearch').default - -Utils = require '../utils' - -class TaskSearchController extends Controller - - - initialize: ({@requestId}) -> - @formSubmitted = false - @title 'Task Search' - @params = {} - if @requestId - @global = false - else - @global = true - @view = new TaskSearchView - requestId : @requestId - global : @global - @setView @view - - @view.render() - app.showView @view - - - -module.exports = TaskSearchController diff --git a/SingularityUI/app/controllers/TaskSearch.es6 b/SingularityUI/app/controllers/TaskSearch.es6 new file mode 100644 index 0000000000..1d39bd8ba0 --- /dev/null +++ b/SingularityUI/app/controllers/TaskSearch.es6 @@ -0,0 +1,28 @@ +import Controller from './Controller'; +import TaskSearchView from '../views/taskSearch'; +import { FetchRequest } from '../actions/api/requests'; +import { FetchTaskSearchParams } from '../actions/api/history'; +import TaskSearch from '../components/taskSearch/TaskSearch'; + +class TaskSearchController extends Controller { + + initialize({store, requestId}) { + app.showPageLoader(); + this.title('Task Search'); + this.store = store; + this.requestId = requestId; + + const promises = []; + if (this.requestId) { + promises.push(this.store.dispatch(FetchRequest.trigger(this.requestId))); + } + promises.push(this.store.dispatch(FetchTaskSearchParams.trigger({requestId: this.requestId, page: 1, count: TaskSearch.TASKS_PER_PAGE}))); + + Promise.all(promises).then(() => { + this.setView(new TaskSearchView(store, this.requestId)); + app.showView(this.view); + }); + } +} + +export default TaskSearchController; diff --git a/SingularityUI/app/reducers/api/index.es6 b/SingularityUI/app/reducers/api/index.es6 index 849dd67ebe..388b403e36 100644 --- a/SingularityUI/app/reducers/api/index.es6 +++ b/SingularityUI/app/reducers/api/index.es6 @@ -13,7 +13,8 @@ import { FetchActiveTasksForRequest, FetchActiveTasksForDeploy, FetchTaskHistoryForDeploy, - FetchDeployForRequest + FetchDeployForRequest, + FetchTaskSearchParams } from '../../actions/api/history'; import { FetchTaskS3Logs } from '../../actions/api/logs'; @@ -60,7 +61,7 @@ const user = buildApiActionReducer(FetchUser); const webhooks = buildApiActionReducer(FetchWebhooks, []); const slaves = buildApiActionReducer(FetchSlaves, []); const racks = buildApiActionReducer(FetchRacks, []); -const request = buildKeyedApiActionReducer(FetchRequest); +const request = buildApiActionReducer(FetchRequest); const saveRequest = buildApiActionReducer(SaveRequest); const requests = buildApiActionReducer(FetchRequests, []); const requestsInState = buildApiActionReducer(FetchRequestsInState, []); @@ -77,6 +78,7 @@ const taskResourceUsage = buildApiActionReducer(FetchTaskStatistics); const taskS3Logs = buildApiActionReducer(FetchTaskS3Logs, []); const taskShellCommandResponse = buildApiActionReducer(RunCommandOnTask); const task = buildKeyedApiActionReducer(FetchTaskHistory); +const taskHistory = buildApiActionReducer(FetchTaskSearchParams, []); const tasks = buildApiActionReducer(FetchTasksInState, []); export default combineReducers({ @@ -101,5 +103,6 @@ export default combineReducers({ taskResourceUsage, taskS3Logs, deploys, - taskShellCommandResponse + taskShellCommandResponse, + taskHistory }); diff --git a/SingularityUI/app/reducers/index.es6 b/SingularityUI/app/reducers/index.es6 index 4cd4ec9fb9..eda269e0c9 100644 --- a/SingularityUI/app/reducers/index.es6 +++ b/SingularityUI/app/reducers/index.es6 @@ -3,9 +3,9 @@ import { combineReducers } from 'redux'; import taskGroups from './taskGroups'; import activeRequest from './activeRequest'; import tasks from './tasks'; - import api from './api'; import ui from './ui'; +import {reducer as formReducer} from 'redux-form'; const path = (state='', action) => { if (action.type === 'LOG_INIT') { @@ -68,5 +68,6 @@ export default combineReducers({ viewMode, search, logRequestLength, - maxLines + maxLines, + form: formReducer }); diff --git a/SingularityUI/app/router.es6 b/SingularityUI/app/router.es6 index 586074e59f..cd479bf7a9 100644 --- a/SingularityUI/app/router.es6 +++ b/SingularityUI/app/router.es6 @@ -1,5 +1,4 @@ let RequestDetailController; -let TaskSearchController; const hasProp = {}.hasOwnProperty; @@ -29,7 +28,7 @@ import DeployDetailController from 'controllers/DeployDetail'; import LogViewerController from 'controllers/LogViewer'; -TaskSearchController = require('controllers/TaskSearch'); +import TaskSearchController from 'controllers/TaskSearch'; import WebhooksController from 'controllers/Webhooks'; @@ -89,6 +88,7 @@ class Router extends Backbone.Router { taskSearch(requestId) { return this.app.bootstrapController(new TaskSearchController({ + store: this.app.store, requestId })); } @@ -112,28 +112,28 @@ class Router extends Backbone.Router { } return this.app.bootstrapController(new TasksTableController({ store: this.app.store, - state: state, - requestsSubFilter: requestsSubFilter, - searchFilter: searchFilter + state, + requestsSubFilter, + searchFilter })); } taskDetail(taskId) { return this.app.bootstrapController(new TaskDetailController({ store: this.app.store, - taskId: taskId, + taskId, filePath: taskId })); } taskFileBrowser(taskId, filePath) { if (filePath == null) { - filePath = ""; + filePath = ''; } return this.app.bootstrapController(new TaskDetailController({ store: this.app.store, - taskId: taskId, - filePath: filePath + taskId, + filePath })); } @@ -176,8 +176,8 @@ class Router extends Backbone.Router { deployDetail(requestId, deployId) { return this.app.bootstrapController(new DeployDetailController({ store: this.app.store, - requestId: requestId, - deployId: deployId + requestId, + deployId })); } diff --git a/SingularityUI/app/styles/detailHeader.styl b/SingularityUI/app/styles/detailHeader.styl index f4427f4fbd..433d0bbb8b 100644 --- a/SingularityUI/app/styles/detailHeader.styl +++ b/SingularityUI/app/styles/detailHeader.styl @@ -149,3 +149,6 @@ div.task-detail margin-top -20px + +.inline-header + display inline-block diff --git a/SingularityUI/app/styles/reactComponents.styl b/SingularityUI/app/styles/reactComponents.styl index a9c19c3a43..2087b84046 100644 --- a/SingularityUI/app/styles/reactComponents.styl +++ b/SingularityUI/app/styles/reactComponents.styl @@ -47,3 +47,7 @@ 100% -webkit-transform rotate(359deg) transform rotate(359deg) + +.task-filters + & button.pull-right + margin-left 10px diff --git a/SingularityUI/app/styles/table.styl b/SingularityUI/app/styles/table.styl index 91c7429959..99a6206373 100644 --- a/SingularityUI/app/styles/table.styl +++ b/SingularityUI/app/styles/table.styl @@ -117,3 +117,11 @@ th #schedule #empty-table margin-bottom 20px + +.count-options + a + margin-left 0.5em + a.inactive + pointer-events none + cursor default + color $base-text diff --git a/SingularityUI/app/utils.es6 b/SingularityUI/app/utils.es6 index 89cbac0852..713e0b6cad 100644 --- a/SingularityUI/app/utils.es6 +++ b/SingularityUI/app/utils.es6 @@ -491,6 +491,16 @@ const Utils = { ? (expiringBounce.startMillis + expiringBounce.expiringAPIRequestObject.durationMillis) > new Date().getTime() : false; } + }, + + queryParams(source) { + const array = []; + for(var key in source) { + if (source[key]) { + array.push(`${encodeURIComponent(key)}=${encodeURIComponent(source[key])}`); + } + } + return array.join("&"); } }; diff --git a/SingularityUI/app/views/taskSearch.jsx b/SingularityUI/app/views/taskSearch.jsx index 41f9f6a5ed..56fd594f33 100644 --- a/SingularityUI/app/views/taskSearch.jsx +++ b/SingularityUI/app/views/taskSearch.jsx @@ -1,48 +1,20 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; - import ReactView from './reactView'; - import TaskSearch from '../components/taskSearch/TaskSearch'; -class TaskSearchView extends ReactView { - - constructor(...args) { - super(...args); - this.viewJson = this.viewJson.bind(this); - } - - events() { - return _.extend(super.events(), - {'click [data-action="viewJSON"]': 'viewJson'}); - } +import React from 'react'; +import ReactDOM from 'react-dom'; - viewJson(e) { - let $target = $(e.currentTarget).parents('tr'); - let id = $target.data('id'); - let collectionName = $target.data('collection'); +import { Provider } from 'react-redux'; - // Need to reach into subviews to get the necessary data - let { collection } = this.subviews[collectionName]; - return utils.viewJSON(collection.get(id)); - } +export default class TaskSearchView extends ReactView { - initialize({requestId, global}, opts) { - this.requestId = requestId; - this.global = global; - this.opts = opts; - } + constructor(store, requestId) { + super(); + this.store = store; + this.requestId = requestId; + } - render() { - $(this.el).addClass("task-search-root"); - ReactDOM.render( - , - this.el); + render() { + ReactDOM.render(, this.el); } } - -export default TaskSearchView; diff --git a/SingularityUI/package.json b/SingularityUI/package.json index 3b301d3a9f..67d40acdc2 100644 --- a/SingularityUI/package.json +++ b/SingularityUI/package.json @@ -41,6 +41,7 @@ "q": "^1.4.1", "react": "^15.1.0", "react-bootstrap": "^0.29.5", + "react-bootstrap-datetimepicker": "0.0.22", "react-dom": "^15.1.0", "react-interval": "^1.2.1", "react-json-tree": "^0.8.0", @@ -51,6 +52,7 @@ "react-typeahead": "git://github.com/HubSpot/react-typeahead.git#hubspot-3", "react-waypoint": "^2.0.3", "redux": "^3.5.2", + "redux-form": "^5.3.0", "redux-logger": "^2.6.1", "redux-thunk": "^2.0.1", "reselect": "^2.5.1",