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 (
+
+ );
+};
diff --git a/SingularityUI/app/components/deployDetail/DeployDetail.jsx b/SingularityUI/app/components/deployDetail/DeployDetail.jsx
index 1006cc5d13..f7e5e4c6d9 100644
--- a/SingularityUI/app/components/deployDetail/DeployDetail.jsx
+++ b/SingularityUI/app/components/deployDetail/DeployDetail.jsx
@@ -241,7 +241,7 @@ class DeployDetail extends React.Component {
function mapStateToProps(state) {
let latestHealthchecks = _.mapObject(state.api.task, (val, key) => {
- if (val.data && val.data.healthcheckResults.length > 0) {
+ if (val.data && val.data.healthcheckResults && val.data.healthcheckResults.length > 0) {
return _.max(val.data.healthcheckResults, (hc) => {
return hc.timestamp;
});
diff --git a/SingularityUI/app/components/taskSearch/DisplayResults.jsx b/SingularityUI/app/components/taskSearch/DisplayResults.jsx
deleted file mode 100644
index 303f8a8e2d..0000000000
--- a/SingularityUI/app/components/taskSearch/DisplayResults.jsx
+++ /dev/null
@@ -1,112 +0,0 @@
-import React from 'react';
-import HistoricalTasks from '../../collections/HistoricalTasks';
-
-import QueryParameters from '../common/QueryParameters';
-import FormField from '../common/formItems/FormField';
-import DropDown from '../common/formItems/DropDown';
-import TaskTable from '../common/TaskTable';
-
-import Enums from './Enums';
-
-let DisplayResults = React.createClass({
-
- // Used to detect if any query params have changed
- didQueryParamsChange(nextProps) {
- if (nextProps.requestId !== this.props.requestId) {
- return true;
- }
- if (nextProps.global !== this.props.global) {
- return true;
- }
- if (nextProps.deployId !== this.props.deployId) {
- return true;
- }
- if (nextProps.host !== this.props.host) {
- return true;
- }
- if (nextProps.lastTaskStatus !== this.props.lastTaskStatus) {
- return true;
- }
- if (nextProps.startedBefore !== this.props.startedBefore) {
- return true;
- }
- if (nextProps.startedAfter !== this.props.startedAfter) {
- return true;
- }
- if (nextProps.sortDirection !== this.props.sortDirection) {
- return true;
- }
- if (nextProps.page !== this.props.page) {
- return true;
- }
- if (nextProps.count !== this.props.count) {
- return true;
- }
- return false;
- },
-
- getInitialState() {
- this.willFetch = false;
- return {
- loading: true
- };
- },
-
- getEmptyTableMessage() {
- if (this.props.holdOffOnSearching) {
- return 'Enter parameters above to view tasks.';
- } else if (this.state.loading) {
- return 'Loading Tasks...';
- } else {
- return 'No Tasks Found';
- }
- },
-
- fetchCollection() {
- if (!this.props.holdOffOnSearching) {
- this.willFetch = false;
- }
- this.collection = new HistoricalTasks([], {
- params: {
- requestId: this.props.requestId,
- deployId: this.props.deployId,
- host: this.props.host,
- lastTaskStatus: this.props.lastTaskStatus,
- startedBefore: this.props.startedBefore ? this.props.startedBefore.valueOf() : undefined,
- startedAfter: this.props.startedAfter ? this.props.startedAfter.valueOf() : undefined,
- orderDirection: this.props.sortDirection,
- count: this.props.count,
- page: this.props.page
- }
- });
- if (!this.props.holdOffOnSearching) {
- return this.collection.fetch({ success: () => this.setState({ loading: false }) });
- }
- },
-
- componentWillMount() {
- return this.fetchCollection();
- },
-
- componentWillReceiveProps(nextProps) {
- // Note that if adding another query param you MUST update @didQueryParamsChange
- if (this.didQueryParamsChange(nextProps) || this.props.holdOffOnSearching && !nextProps.holdOffOnSearching) {
- this.willFetch = true;
- return this.setState({ loading: true });
- }
- },
-
- renderPageNavBar() {
- return ;
- },
-
- render() {
- if (this.willFetch) {
- this.fetchCollection();
- }
- return
;
- }
-});
-
-export default DisplayResults;
-
diff --git a/SingularityUI/app/components/taskSearch/Enums.jsx b/SingularityUI/app/components/taskSearch/Enums.jsx
deleted file mode 100644
index 9875053078..0000000000
--- a/SingularityUI/app/components/taskSearch/Enums.jsx
+++ /dev/null
@@ -1,13 +0,0 @@
-class Enums {
-
- static sortDirections() {
- return [{ user: 'Ascending', value: 'ASC' }, { user: 'Descending', value: 'DESC' }];
- }
-
- static extendedTaskState() {
- return [{ user: 'Error', value: 'TASK_ERROR' }, { user: 'Failed', value: 'TASK_FAILED' }, { user: 'Finished', value: 'TASK_FINISHED' }, { user: 'Killed', value: 'TASK_KILLED' }, { user: 'Lost', value: 'TASK_LOST' }, { user: 'Lost While Down', value: 'TASK_LOST_WHILE_DOWN' }];
- }
-}
-
-export default Enums;
-
diff --git a/SingularityUI/app/components/taskSearch/Header.jsx b/SingularityUI/app/components/taskSearch/Header.jsx
deleted file mode 100644
index 63df196771..0000000000
--- a/SingularityUI/app/components/taskSearch/Header.jsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import React from 'react';
-import Glyphicon from '../common/atomicDisplayItems/Glyphicon';
-
-let Header = React.createClass({
-
- render() {
- return ;
- }
-});
-
-export default Header;
-
diff --git a/SingularityUI/app/components/taskSearch/TaskSearch.jsx b/SingularityUI/app/components/taskSearch/TaskSearch.jsx
index 7f4a2d09ee..f611feab43 100644
--- a/SingularityUI/app/components/taskSearch/TaskSearch.jsx
+++ b/SingularityUI/app/components/taskSearch/TaskSearch.jsx
@@ -1,166 +1,181 @@
import React from 'react';
+import { connect } from 'react-redux';
+import classNames from 'classnames';
+import { FetchTaskSearchParams } from '../../actions/api/history';
+
+import Breadcrumbs from '../common/Breadcrumbs';
+import TasksTable from './TasksTable';
+import TaskSearchFilters from './TaskSearchFilters';
+import Glyphicon from '../common/atomicDisplayItems/Glyphicon';
+import JSONButton from '../common/JSONButton';
import Utils from '../../utils';
-import Enums from './Enums';
-import TaskSearchForm from './TaskSearchForm';
-import DisplayResults from './DisplayResults';
-import Header from './Header';
-
-let TaskSearch = React.createClass({
-
- countChoices: [5, 10, 25],
-
- defaultCount: 10,
-
- defaultSortDirection: 'DESC',
-
- getInitialState() {
- return {
- form: {
- requestId: this.props.initialRequestId || '',
- deployId: this.props.initialDeployId || '',
- host: this.props.initialHost || '',
- lastTaskStatus: this.props.initialTaskStatus || '',
- startedBefore: this.props.initialStartedBefore || '',
- startedAfter: this.props.initialStartedAfter || ''
- },
- sortDirection: this.props.initialSortDirection || this.defaultSortDirection,
- queryParams: {
- requestId: this.props.initialRequestId || '',
- deployId: this.props.initialDeployId || '',
- host: this.props.initialHost || '',
- lastTaskStatus: this.props.initialTaskStatus || '',
- startedBefore: this.props.initialStartedBefore || '',
- startedAfter: this.props.initialStartedAfter || ''
- },
- pageNumber: 1,
- count: this.props.initialCount || this.defaultCount,
- hasDoneAnySearch: false
- };
- },
-
- handleSubmit(event) {
- event.preventDefault();
- return this.setState({
- queryParams: this.state.form,
- pageNumber: 1, // If you narrow down your search you most likely want to go back to page 1
- hasDoneAnySearch: true
+
+class TaskSearch extends React.Component {
+
+ static propTypes = {
+ requestId: React.PropTypes.string,
+ fetchTaskHistory: React.PropTypes.func.isRequired,
+ request: React.PropTypes.object,
+ taskHistory: React.PropTypes.array
+ }
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ filter: {
+ requestId: props.requestId,
+ page: 1,
+ count: TaskSearch.TASKS_PER_PAGE
+ },
+ disableNext: false,
+ loading: false
+ };
+ }
+
+ static TASKS_PER_PAGE = 10;
+
+ setCount(count) {
+ TaskSearch.TASKS_PER_PAGE = count;
+ const newFilter = _.extend({}, this.state.filter, {count, page: 1});
+ this.setState({
+ filter: newFilter
+ });
+ this.props.fetchTaskHistory(newFilter);
+ }
+
+ handleSearch(filter) {
+ const newFilter = _.extend({}, this.state.filter, filter, {page: 1});
+ this.setState({
+ filter: newFilter,
+ disableNext: false
+ });
+ this.props.fetchTaskHistory(newFilter);
+ }
+
+ handlePage(page) {
+ const newFilter = _.extend({}, this.state.filter, {page});
+ this.setState({
+ loading: true
+ });
+
+ this.props.fetchTaskHistory(newFilter).then((resp) => {
+ if (!resp.data.length) {
+ this.props.fetchTaskHistory(_.extend({}, newFilter, {page: page - 1})).then(() => {
+ this.setState({
+ disableNext: true,
+ loading: false
+ });
+ });
+ } else if (resp.data.length < TaskSearch.TASKS_PER_PAGE) {
+ this.setState({
+ filter: newFilter,
+ loading: false,
+ disableNext: true
+ });
+ } else {
+ this.setState({
+ filter: newFilter,
+ loading: false,
+ disableNext: false
});
- },
-
- isAnyQueryParams() {
- return this.state.queryParams.requestId || this.state.queryParams.deployId || this.state.queryParams.host || this.state.queryParams.lastTaskStatus || this.state.queryParams.startedBefore || this.state.queryParams.startedAfter;
- },
-
- // Annoying that we need a new function for each property.
- // Unfortuantely using a curried function doesn't seem to work.
- updateReqeustId(event) {
- if (this.props.global) {
- let form = $.extend({}, this.state.form);
- form.requestId = event.target.value;
- return this.setState({ form });
- }
- },
-
- updateDeployId(event) {
- let form = $.extend({}, this.state.form);
- form.deployId = event.target.value;
- return this.setState({ form });
- },
-
- updateHost(event) {
- let form = $.extend({}, this.state.form);
- form.host = event.target.value;
- return this.setState({ form });
- },
-
- updateLastTaskStatus(event) {
- let form = $.extend({}, this.state.form);
- form.lastTaskStatus = event.target.value;
- return this.setState({ form });
- },
-
- updateStartedBefore(event) {
- let form = $.extend({}, this.state.form);
- form.startedBefore = event.date;
- return this.setState({ form });
- },
-
- updateStartedAfter(event) {
- let form = $.extend({}, this.state.form);
- form.startedAfter = event.date;
- return this.setState({ form });
- },
-
- resetForm() {
- return this.setState(this.getInitialState());
- },
-
- updateSortDirection(event) {
- if (this.state.sortDirection === Enums.sortDirections()[0].value) {
- return this.setState({ sortDirection: Enums.sortDirections()[1].value });
- } else {
- return this.setState({ sortDirection: Enums.sortDirections()[0].value });
- }
- },
-
- updatePageNumber(event) {
- return this.setState({ pageNumber: event.target.value });
- },
-
- increasePageNumber(event) {
- return this.setState({ pageNumber: this.state.pageNumber + 1 });
- },
-
- setPageNumber(pageNumber) {
- if (pageNumber > 0) {
- return this.setState({ pageNumber });
- }
- },
-
- decreasePageNumber(event) {
- if (this.state.pageNumber > 1) {
- return this.setState({ pageNumber: this.state.pageNumber - 1 });
- }
- },
-
- updateCount(newCount) {
- return this.setState({ count: newCount });
- },
-
- clearRequestId(event) {
- if (this.props.global) {
- return this.setState({ requestId: '' });
- }
- },
-
- clearDeployId(event) {
- return this.setState({ deployId: '' });
- },
-
- clearHost(event) {
- return this.setState({ host: '' });
- },
-
- clearSortDirection(event) {
- return this.setState({ sortDirection: '' });
- },
-
- clearLastTaskStatus(event) {
- return this.setState({ lastTaskStatus: '' });
- },
-
- clearStartedBefore(event) {
- return this.setState({ startedBefore: '' });
- },
-
- clearStartedAfter(event) {
- return this.setState({ startedAfter: '' });
- },
-
- render() {
- return Search Parameters
Tasks
;
+ }
+ });
+ }
+
+ renderTableRow(data, i) {
+ return (
+
+ |
+ {data.taskId.requestId} |
+ {data.taskId.deployId} |
+ {data.taskId.host} |
+
+
+ {Utils.humanizeText(data.lastTaskState)}
+
+ |
+ {Utils.timeStampFromNow(data.taskId.startedAt)} |
+ {Utils.timeStampFromNow(data.updatedAt)} |
+
+ ···
+ {'{ }'}
+ |
+
+ );
+ }
+
+ renderBreadcrumbs() {
+ if (this.props.requestId) {
+ return (
+
+ );
}
-});
-
-export default TaskSearch;
-
+ return null;
+ }
+
+ renderPageOptions() {
+ if (this.props.taskHistory.length) {
+ return (
+
+ Results per page:
+ this.setCount(5)}>5
+ this.setCount(10)}>10
+ this.setCount(25)}>25
+
+ );
+ }
+ return null;
+ }
+
+ render() {
+ return (
+
+ {this.renderBreadcrumbs()}
+
{!this.props.requestId && 'Global '}Historical Tasks
+ {this.props.requestId &&
for {this.props.requestId}
}
+
Search Parameters
+
this.handleSearch(filter)} />
+ {this.renderPageOptions()}
+
+
+ this.handlePage(page)}
+ renderTableRow={(...args) => this.renderTableRow(...args)}
+ loading={this.state.loading}
+ />
+
+
+
+ );
+ }
+}
+
+function mapStateToProps(state) {
+ return {
+ request: state.api.request.data,
+ taskHistory: state.api.taskHistory.data
+ };
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ fetchTaskHistory: (...args) => dispatch(FetchTaskSearchParams.trigger(...args))
+ };
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(TaskSearch);
diff --git a/SingularityUI/app/components/taskSearch/TaskSearchFilters.jsx b/SingularityUI/app/components/taskSearch/TaskSearchFilters.jsx
new file mode 100644
index 0000000000..39992ca55a
--- /dev/null
+++ b/SingularityUI/app/components/taskSearch/TaskSearchFilters.jsx
@@ -0,0 +1,122 @@
+import React from 'react';
+import {reduxForm} from 'redux-form';
+import classNames from 'classnames';
+import { Panel, Button } from 'react-bootstrap';
+import DateTimeField from 'react-bootstrap-datetimepicker';
+import moment from 'moment';
+
+import ReduxSelect from '../common/formItems/ReduxSelect';
+import Utils from '../../utils';
+
+class TaskSearchFilters extends React.Component {
+
+ static propTypes = {
+ onSearch: React.PropTypes.func.isRequired,
+ requestId: React.PropTypes.string,
+ valid: React.PropTypes.bool,
+ fields: React.PropTypes.object,
+ resetForm: React.PropTypes.func
+ }
+
+ handleSubmit(e) {
+ e.preventDefault();
+ if (this.props.valid) {
+ const result = _.mapObject(this.props.fields, (v) => v.value);
+ this.props.onSearch(result);
+ }
+ }
+
+ renderStatusOptions(opt) {
+ return (
+
+ {opt.label}
+
+ );
+ }
+
+ render() {
+ const {fields: {requestId, deployId, host, startedAfter, startedBefore, lastTaskStatus}} = this.props;
+ const statusOptions = [
+ { value: 'TASK_ERROR', label: 'Error' },
+ { value: 'TASK_FAILED', label: 'Failed' },
+ { value: 'TASK_FINISHED', label: 'Finished' },
+ { value: 'TASK_KILLED', label: 'Killed' },
+ { value: 'TASK_LOST', label: 'Lost' },
+ { value: 'TASK_LOST_WHILE_DOWN', label: 'Lost while down' },
+ ];
+
+ return (
+
+
+
+ );
+ }
+}
+
+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",