You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@aurora.apache.org by dm...@apache.org on 2017/10/23 19:48:30 UTC
aurora git commit: Add sorting and filtering controls for TaskList
Repository: aurora
Updated Branches:
refs/heads/master ec640117c -> 5b91150fd
Add sorting and filtering controls for TaskList
Reviewed at https://reviews.apache.org/r/63188/
Project: http://git-wip-us.apache.org/repos/asf/aurora/repo
Commit: http://git-wip-us.apache.org/repos/asf/aurora/commit/5b91150f
Tree: http://git-wip-us.apache.org/repos/asf/aurora/tree/5b91150f
Diff: http://git-wip-us.apache.org/repos/asf/aurora/diff/5b91150f
Branch: refs/heads/master
Commit: 5b91150fd0668c23b178d80516427763764ac2d3
Parents: ec64011
Author: David McLaughlin <da...@dmclaughlin.com>
Authored: Mon Oct 23 12:48:20 2017 -0700
Committer: David McLaughlin <da...@dmclaughlin.com>
Committed: Mon Oct 23 12:48:20 2017 -0700
----------------------------------------------------------------------
ui/src/main/js/components/JobHistory.js | 2 +-
ui/src/main/js/components/TaskList.js | 132 +++++++++++++++++--
.../js/components/__tests__/JobHistory-test.js | 2 +-
.../js/components/__tests__/TaskList-test.js | 82 +++++++++++-
ui/src/main/js/utils/Common.js | 8 ++
ui/src/main/js/utils/__tests__/Common-test.js | 19 +++
ui/src/main/sass/components/_task-list.scss | 28 ++++
7 files changed, 259 insertions(+), 14 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/aurora/blob/5b91150f/ui/src/main/js/components/JobHistory.js
----------------------------------------------------------------------
diff --git a/ui/src/main/js/components/JobHistory.js b/ui/src/main/js/components/JobHistory.js
index 9f00a7b..74f6fcb 100644
--- a/ui/src/main/js/components/JobHistory.js
+++ b/ui/src/main/js/components/JobHistory.js
@@ -10,6 +10,6 @@ import { getLastEventTime, isActive } from 'utils/Task';
export default function ({ tasks }) {
const terminalTasks = sort(tasks.filter((t) => !isActive(t)), (t) => getLastEventTime(t), true);
return (<Tab id='history' name={`Job History (${terminalTasks.length})`}>
- <PanelGroup><TaskList tasks={terminalTasks} /></PanelGroup>
+ <PanelGroup><TaskList sortBy='latest' tasks={terminalTasks} /></PanelGroup>
</Tab>);
}
http://git-wip-us.apache.org/repos/asf/aurora/blob/5b91150f/ui/src/main/js/components/TaskList.js
----------------------------------------------------------------------
diff --git a/ui/src/main/js/components/TaskList.js b/ui/src/main/js/components/TaskList.js
index dd34c62..4a4b8d3 100644
--- a/ui/src/main/js/components/TaskList.js
+++ b/ui/src/main/js/components/TaskList.js
@@ -1,10 +1,12 @@
import React from 'react';
import { Link } from 'react-router-dom';
+import Icon from 'components/Icon';
import Pagination from 'components/Pagination';
import { RelativeTime } from 'components/Time';
import TaskStateMachine from 'components/TaskStateMachine';
+import { pluralize } from 'utils/Common';
import { getClassForScheduleStatus, getDuration, getLastEventTime, isActive } from 'utils/Task';
import { SCHEDULE_STATUS } from 'utils/Thrift';
@@ -64,15 +66,125 @@ export class TaskListItem extends React.Component {
}
}
-export default function TaskList({ tasks }) {
- return (<div className='task-list'>
- <table className='psuedo-table'>
- <Pagination
- data={tasks}
- hideIfSinglePage
- isTable
- numberPerPage={25}
- renderer={(t) => <TaskListItem key={t.assignedTask.taskId} task={t} />} />
- </table>
+// VisibleForTesting
+export function searchTask(task, userQuery) {
+ const query = userQuery.toLowerCase();
+ return (task.assignedTask.instanceId.toString().startsWith(query) ||
+ (task.assignedTask.slaveHost && task.assignedTask.slaveHost.toLowerCase().includes(query)) ||
+ SCHEDULE_STATUS[task.status].toLowerCase().startsWith(query));
+}
+
+export function TaskListFilter({ numberPerPage, onChange, tasks }) {
+ if (tasks.length > numberPerPage) {
+ return (<div className='table-input-wrapper'>
+ <Icon name='search' />
+ <input
+ autoFocus
+ onChange={(e) => onChange(e)}
+ placeholder='Search tasks by instance-id, host or current status'
+ type='text' />
+ </div>);
+ }
+ return null;
+}
+
+export function TaskListStatus({ status }) {
+ return [
+ <span className={`img-circle ${getClassForScheduleStatus(ScheduleStatus[status])}`} />,
+ <span>{status}</span>
+ ];
+}
+
+export function TaskListStatusFilter({ onClick, tasks }) {
+ const statuses = Object.keys(tasks.reduce((seen, task) => {
+ seen[SCHEDULE_STATUS[task.status]] = true;
+ return seen;
+ }, {}));
+
+ if (statuses.length <= 1) {
+ return (<div>
+ {pluralize(tasks, 'One task is ', `All ${tasks.length} tasks are `)}
+ <TaskListStatus status={statuses[0]} />
+ </div>);
+ }
+
+ return (<ul className='task-list-status-filter'>
+ <li>Filter by:</li>
+ <li onClick={(e) => onClick(null)}>all</li>
+ {statuses.map((status) => (<li key={status} onClick={(e) => onClick(status)}>
+ <TaskListStatus status={status} />
+ </li>))}
+ </ul>);
+}
+
+export function TaskListControls({ currentSort, onFilter, onSort, tasks }) {
+ return (<div className='task-list-controls'>
+ <ul className='task-list-main-sort'>
+ <li>Sort by:</li>
+ <li className={currentSort === 'default' ? 'active' : ''} onClick={(e) => onSort('default')}>
+ instance
+ </li>
+ <li className={currentSort === 'latest' ? 'active' : ''} onClick={(e) => onSort('latest')}>
+ updated
+ </li>
+ </ul>
+ <TaskListStatusFilter onClick={onFilter} tasks={tasks} />
</div>);
}
+
+export default class TaskList extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ filter: props.filter,
+ reverseSort: props.reverse || false,
+ sortBy: props.sortBy || 'default'
+ };
+ }
+
+ setFilter(filter) {
+ this.setState({filter});
+ }
+
+ setSort(sortBy) {
+ if (sortBy === this.state.sortBy) {
+ this.setState({reverseSort: !this.state.reverseSort});
+ } else {
+ this.setState({sortBy});
+ }
+ }
+
+ render() {
+ const that = this;
+ const tasksPerPage = 25;
+ const filterFn = (t) => that.state.filter ? searchTask(t, that.state.filter) : true;
+ const sortFn = this.state.sortBy === 'latest'
+ ? (t) => getLastEventTime(t) * -1
+ : (t) => t.assignedTask.instanceId;
+
+ return (<div>
+ <TaskListFilter
+ numberPerPage={tasksPerPage}
+ onChange={(e) => that.setFilter(e.target.value)}
+ tasks={this.props.tasks} />
+ <TaskListControls
+ currentSort={this.state.sortBy}
+ onFilter={(query) => that.setFilter(query)}
+ onSort={(key) => that.setSort(key)}
+ tasks={this.props.tasks} />
+ <div className='task-list'>
+ <table className='psuedo-table'>
+ <Pagination
+ data={this.props.tasks}
+ filter={filterFn}
+ hideIfSinglePage
+ isTable
+ numberPerPage={tasksPerPage}
+ renderer={(t) => <TaskListItem key={t.assignedTask.taskId} task={t} />}
+ reverseSort={this.state.reverseSort}
+ sortBy={sortFn} />
+ </table>
+ </div>
+ </div>);
+ }
+}
http://git-wip-us.apache.org/repos/asf/aurora/blob/5b91150f/ui/src/main/js/components/__tests__/JobHistory-test.js
----------------------------------------------------------------------
diff --git a/ui/src/main/js/components/__tests__/JobHistory-test.js b/ui/src/main/js/components/__tests__/JobHistory-test.js
index 13f7ecc..7a916d1 100644
--- a/ui/src/main/js/components/__tests__/JobHistory-test.js
+++ b/ui/src/main/js/components/__tests__/JobHistory-test.js
@@ -13,6 +13,6 @@ describe('JobHistory', () => {
ScheduledTaskBuilder.status(ScheduleStatus.FINISHED).build()
];
const el = shallow(JobHistory({tasks}));
- expect(el.contains(<TaskList tasks={[tasks[1]]} />)).toBe(true);
+ expect(el.contains(<TaskList sortBy='latest' tasks={[tasks[1]]} />)).toBe(true);
});
});
http://git-wip-us.apache.org/repos/asf/aurora/blob/5b91150f/ui/src/main/js/components/__tests__/TaskList-test.js
----------------------------------------------------------------------
diff --git a/ui/src/main/js/components/__tests__/TaskList-test.js b/ui/src/main/js/components/__tests__/TaskList-test.js
index ae74ff4..c222b61 100644
--- a/ui/src/main/js/components/__tests__/TaskList-test.js
+++ b/ui/src/main/js/components/__tests__/TaskList-test.js
@@ -1,10 +1,16 @@
import React from 'react';
import { shallow } from 'enzyme';
-import { TaskListItem } from '../TaskList';
+import {
+ TaskListControls,
+ TaskListStatusFilter,
+ TaskListItem,
+ TaskListStatus,
+ searchTask
+} from '../TaskList';
import TaskStateMachine from '../TaskStateMachine';
-import { ScheduledTaskBuilder } from 'test-utils/TaskBuilders';
+import { AssignedTaskBuilder, ScheduledTaskBuilder } from 'test-utils/TaskBuilders';
describe('TaskListItem', () => {
it('Should not show any state machine element by default', () => {
@@ -20,3 +26,75 @@ describe('TaskListItem', () => {
expect(el.find('tr.expanded').length).toBe(1);
});
});
+
+describe('TaskListControls', () => {
+ it('Should attach active to default list element', () => {
+ const el = shallow(<TaskListControls
+ currentSort='default'
+ onFilter={() => {}}
+ onSort={() => {}}
+ tasks={[ScheduledTaskBuilder.build()]} />);
+
+ expect(el.find('li.active').text()).toContain('instance');
+ });
+
+ it('Should attach active to latest list element', () => {
+ const el = shallow(<TaskListControls
+ currentSort='latest'
+ onFilter={() => {}}
+ onSort={() => {}}
+ tasks={[ScheduledTaskBuilder.build()]} />);
+
+ expect(el.find('li.active').text()).toContain('updated');
+ });
+});
+
+describe('TaskListStatus', () => {
+ it('Should not show filters for one status', () => {
+ const el = shallow(<TaskListStatusFilter
+ onClick={() => {}}
+ tasks={[
+ ScheduledTaskBuilder.status(ScheduleStatus.PENDING).build(),
+ ScheduledTaskBuilder.status(ScheduleStatus.PENDING).build()]} />);
+ expect(
+ el.contains(<div>All {2} tasks are <TaskListStatus status='PENDING' /></div>)).toBe(true);
+ });
+
+ it('Should show filters for multiple status', () => {
+ const el = shallow(<TaskListStatusFilter
+ onClick={() => {}}
+ tasks={[
+ ScheduledTaskBuilder.status(ScheduleStatus.PENDING).build(),
+ ScheduledTaskBuilder.status(ScheduleStatus.RUNNING).build()]} />);
+ expect(el.find(TaskListStatus).length).toBe(2);
+ });
+});
+
+describe('searchTask', () => {
+ it('Should match task by status', () => {
+ const el = ScheduledTaskBuilder.status(ScheduleStatus.PENDING).build();
+ expect(searchTask(el, 'RUNNING')).toBe(false);
+ expect(searchTask(el, 'PENDING')).toBe(true);
+ expect(searchTask(el, 'pend')).toBe(true);
+ });
+
+ it('Should match task by instanceId', () => {
+ const el = ScheduledTaskBuilder.assignedTask(
+ AssignedTaskBuilder.instanceId(539).build()
+ ).build();
+ expect(searchTask(el, '1')).toBe(false);
+ expect(searchTask(el, '5')).toBe(true);
+ expect(searchTask(el, '53')).toBe(true);
+ expect(searchTask(el, '539')).toBe(true);
+ });
+
+ it('Should match task by slaveHost', () => {
+ const el = ScheduledTaskBuilder.assignedTask(
+ AssignedTaskBuilder.slaveHost('aaa-zzz-123').build()
+ ).build();
+ expect(searchTask(el, 'y')).toBe(false);
+ expect(searchTask(el, 'aAa')).toBe(true);
+ expect(searchTask(el, 'zZz')).toBe(true);
+ expect(searchTask(el, '123')).toBe(true);
+ });
+});
http://git-wip-us.apache.org/repos/asf/aurora/blob/5b91150f/ui/src/main/js/utils/Common.js
----------------------------------------------------------------------
diff --git a/ui/src/main/js/utils/Common.js b/ui/src/main/js/utils/Common.js
index 603a11b..d731394 100644
--- a/ui/src/main/js/utils/Common.js
+++ b/ui/src/main/js/utils/Common.js
@@ -37,6 +37,14 @@ export function sort(arr, prop, reverse = false) {
});
}
+export function pluralize(elements, singular, plural) {
+ if (elements.length === 1) {
+ return singular;
+ }
+
+ return (isNully(plural)) ? `${singular}s` : plural;
+}
+
export function range(start, end) {
return [...Array(1 + end - start).keys()].map((i) => start + i);
}
http://git-wip-us.apache.org/repos/asf/aurora/blob/5b91150f/ui/src/main/js/utils/__tests__/Common-test.js
----------------------------------------------------------------------
diff --git a/ui/src/main/js/utils/__tests__/Common-test.js b/ui/src/main/js/utils/__tests__/Common-test.js
new file mode 100644
index 0000000..23f97ec
--- /dev/null
+++ b/ui/src/main/js/utils/__tests__/Common-test.js
@@ -0,0 +1,19 @@
+import { pluralize } from '../Common';
+
+describe('pluralize', () => {
+ it('Should treat empty lists as plural (e.g. zero tasks are...)', () => {
+ expect(pluralize([], 'task')).toBe('tasks');
+ });
+
+ it('Should treat lists with multiple as plural', () => {
+ expect(pluralize([1, 2, 3], 'task')).toBe('tasks');
+ });
+
+ it('Should treat single element lists as singular', () => {
+ expect(pluralize([1], 'task')).toBe('task');
+ });
+
+ it('Should allow you to set your own plural form', () => {
+ expect(pluralize([1, 2], 'task', 'beetlejuice')).toBe('beetlejuice');
+ });
+});
http://git-wip-us.apache.org/repos/asf/aurora/blob/5b91150f/ui/src/main/sass/components/_task-list.scss
----------------------------------------------------------------------
diff --git a/ui/src/main/sass/components/_task-list.scss b/ui/src/main/sass/components/_task-list.scss
index 42b9cac..3c72219 100644
--- a/ui/src/main/sass/components/_task-list.scss
+++ b/ui/src/main/sass/components/_task-list.scss
@@ -88,4 +88,32 @@
overflow: hidden;
}
}
+}
+
+.task-list-controls {
+ display: flex;
+ justify-content: space-between;
+ text-transform: lowercase;
+ margin-bottom: 10px;
+
+ ul {
+ list-style-type: none;
+ padding: 0;
+ margin: 0;
+
+ li {
+ float: left;
+ padding: 0px 5px;
+ }
+
+ li.active {
+ font-weight: 600;
+ }
+ }
+
+ .img-circle {
+ margin-right: 3px;
+ width: 7px;
+ height: 7px;
+ }
}
\ No newline at end of file