TEZ-2273. Tez UI: Support client side searching & sorting for dag tasks page (Sreenath Somarajapuram via pramachandran)
Project: http://git-wip-us.apache.org/repos/asf/tez/repo Commit: http://git-wip-us.apache.org/repos/asf/tez/commit/8ade35c8 Tree: http://git-wip-us.apache.org/repos/asf/tez/tree/8ade35c8 Diff: http://git-wip-us.apache.org/repos/asf/tez/diff/8ade35c8 Branch: refs/heads/TEZ-2003 Commit: 8ade35c8d81753b9fdcf714628aef23d2ebf8d07 Parents: 47f3e83 Author: Prakash Ramachandran <[email protected]> Authored: Wed Apr 8 21:14:11 2015 +0530 Committer: Prakash Ramachandran <[email protected]> Committed: Wed Apr 8 21:14:11 2015 +0530 ---------------------------------------------------------------------- CHANGES.txt | 1 + .../basic-table/basic-table-component.js | 173 +++++++++++++++++-- .../components/basic-table/column-definition.js | 1 + .../components/basic-table/header-cell-view.js | 15 ++ .../components/basic-table/search-view.js | 48 +++++ .../webapp/app/scripts/controllers/dag_tasks.js | 24 +++ .../controllers/table-page-controller.js | 7 +- tez-ui/src/main/webapp/app/styles/main.less | 27 +++ .../main/webapp/app/templates/common/table.hbs | 7 + .../app/templates/components/basic-table.hbs | 9 + .../components/basic-table/header-cell.hbs | 3 + .../components/basic-table/search-view.hbs | 32 ++++ .../webapp/app/templates/partials/table.hbs | 2 +- 13 files changed, 337 insertions(+), 12 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/tez/blob/8ade35c8/CHANGES.txt ---------------------------------------------------------------------- diff --git a/CHANGES.txt b/CHANGES.txt index db1f9a8..2f2a790 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -9,6 +9,7 @@ INCOMPATIBLE CHANGES TEZ-1993. Implement a pluggable InputSizeEstimator for grouping fairly ALL CHANGES: + TEZ-2273. Tez UI: Support client side searching & sorting for dag tasks page TEZ-1909. Remove need to copy over all events from attempt 1 to attempt 2 dir TEZ-2223. TestMockDAGAppMaster fails due to TEZ-2210 on mac. TEZ-2236. Tez UI: Support loading of all tasks in the dag tasks page http://git-wip-us.apache.org/repos/asf/tez/blob/8ade35c8/tez-ui/src/main/webapp/app/scripts/components/basic-table/basic-table-component.js ---------------------------------------------------------------------- diff --git a/tez-ui/src/main/webapp/app/scripts/components/basic-table/basic-table-component.js b/tez-ui/src/main/webapp/app/scripts/components/basic-table/basic-table-component.js index 5a47c78..0c0c83c 100644 --- a/tez-ui/src/main/webapp/app/scripts/components/basic-table/basic-table-component.js +++ b/tez-ui/src/main/webapp/app/scripts/components/basic-table/basic-table-component.js @@ -19,35 +19,93 @@ App.BasicTableComponent = Em.Component.extend({ layoutName: 'components/basic-table', + sortColumnId: '', + sortOrder: '', + + searchText: '', + searchRegEx: null, + searchColumnNames: null, + statusMessage: null, - internalStatusMessage: null, + + isSorting: false, + isSearching: false, pageNum: 1, rowCount: 10, rowCountOptions: [5, 10, 25, 50, 100], pageNavOnFooterAt: 25, + init: function () { + if(this.get('sortColumnId')) { + this._sortObserver(); + } + if(this.get('searchText')) { + this._searchObserver(); + } + this._super(); + }, + totalPages: function () { - return Math.ceil(this.get('rows.length') / this.get('rowCount')); - }.property('rows.length', 'rowCount'), + return Math.ceil(this.get('_searchedRows.length') / this.get('rowCount')); + }.property('_searchedRows.length', 'rowCount'), hasPageNavOnFooter: function () { return this.get('enablePagination') && this.get('_rows.length') >= this.get('pageNavOnFooterAt'); }.property('enablePagination', '_rows.length', 'pageNavOnFooterAt'), _showHeader: function () { - return this.get('enablePagination') || + return this.get('enableSearch') || + this.get('enablePagination') || this.get('extraHeaderItem') || this.get('_statusMessage'); - }.property('enablePagination', 'extraHeaderItem', '_statusMessage'), + }.property('enableSearch', 'enablePagination', 'extraHeaderItem', '_statusMessage'), _statusMessage: function() { - return this.get('internalStatusMessage') || this.get('statusMessage'); - }.property('internalStatusMessage', 'statusMessage'), + if(this.get('isSorting')) { + return "Sorting..."; + } + else if(this.get('isSearching')) { + return "Searching..."; + } + return this.get('statusMessage'); + }.property('isSearching', 'isSorting', 'statusMessage'), _pageNumResetObserver: function () { this.set('pageNum', 1); - }.observes('rowCount'), + }.observes('searchRegEx', 'rowCount'), + + _searchedRows: function () { + var regex = this.get('searchRegEx'), + rows = this.get('rows') || [], + searchColumnNames, + columns; + + function checkRow(column) { + var value; + if(!column.get('searchAndSortable')) { + return false; + } + value = column.getSearchValue(this); + return (typeof value == 'string') ? value.match(regex) : false; + } + + this.set('isSearching', false); + + if(!regex) { + return rows; + } + else { + searchColumnNames = this.get('searchColumnNames'), + columns = searchColumnNames ? this.get('columns').filter(function (column) { + return searchColumnNames.indexOf(column.get('headerCellName')) != -1; + }) : this.get('columns'); + + return rows.filter(function (row) { + return columns.some(checkRow, row); + }); + } + }.property('columns.@each', 'rows.@each', 'searchRegEx'), _columns: function () { var columns = this.get('columns'), @@ -78,10 +136,105 @@ App.BasicTableComponent = Em.Component.extend({ _rows: function () { var startIndex = (this.get('pageNum') - 1) * this.get('rowCount'); - return this.get('rows').slice(startIndex, startIndex + this.get('rowCount')); - }.property('rows.@each', 'rowCount', 'pageNum'), + return this.get('_searchedRows').slice(startIndex, startIndex + this.get('rowCount')); + }.property('_searchedRows.@each', 'rowCount', 'pageNum'), + + _searchObserver: function () { + var searchText = this.get('searchText'), + columnNames = [], + delimIndex, + rowsCount, + that = this; + + if(searchText) { + delimIndex = searchText.indexOf(':'); + if(delimIndex != -1) { + columnNames = searchText.substr(0, delimIndex). + split(","). + reduce(function (arr, columnName) { + columnName = columnName.trim(); + if(columnName) { + arr.push(columnName); + } + return arr; + }, []); + searchText = searchText.substr(delimIndex + 1); + } + searchText = searchText.trim(); + } + + rowsCount = this.get('rows.length'); + + if(rowsCount) { + this.set('isSearching', true); + + Ember.run.later(function () { + that.setProperties({ + searchRegEx: searchText ? new RegExp(searchText, 'im') : null, + searchColumnNames: columnNames.length ? columnNames : null + }); + }, 400); + } + }.observes('searchText'), + + _sortObserver: function () { + var rows = this.get('rows'), + column = this.get('columns').findBy('id', this.get('sortColumnId')), + ascending = this.get('sortOrder') == 'asc', + that = this; + + if(rows.length > 0 && column) { + this.set('isSorting', true); + + Ember.run.later(function () { + /* + * Creating sortArray as calling getSortValue form inside the + * sort function would be more costly. + */ + var sortArray = rows.map(function (row) { + return { + value: column.getSortValue(row), + row: row + }; + }); + + sortArray.sort(function (a, b){ + if(ascending ^ (a.value > b.value)) { + return -1; + } + else if(ascending ^ (a.value < b.value)) { + return 1; + } + return 0; + }); + + that.setProperties({ + rows: sortArray.map(function (record) { + return record.row; + }), + isSorting: false + }); + + }, 400); + } + }.observes('sortColumnId', 'sortOrder'), actions: { + search: function (searchText) { + this.set('searchText', searchText); + }, + sort: function (columnId) { + if(this.get('sortColumnId') != columnId) { + this.setProperties({ + sortColumnId: columnId, + sortOrder: 'asc' + }); + } + else { + this.set('sortOrder', this.get('sortOrder') == 'asc' ? 'desc' : 'asc'); + } + }, + changePage: function (pageNum) { this.set('pageNum', pageNum); } http://git-wip-us.apache.org/repos/asf/tez/blob/8ade35c8/tez-ui/src/main/webapp/app/scripts/components/basic-table/column-definition.js ---------------------------------------------------------------------- diff --git a/tez-ui/src/main/webapp/app/scripts/components/basic-table/column-definition.js b/tez-ui/src/main/webapp/app/scripts/components/basic-table/column-definition.js index fa0a47d..3e29a45 100644 --- a/tez-ui/src/main/webapp/app/scripts/components/basic-table/column-definition.js +++ b/tez-ui/src/main/webapp/app/scripts/components/basic-table/column-definition.js @@ -31,6 +31,7 @@ App.BasicTableComponent.ColumnDefinition = (function () { return Em.Object.extend({ contentPath: null, headerCellName: "Not Available!", + searchAndSortable: true, width: "", http://git-wip-us.apache.org/repos/asf/tez/blob/8ade35c8/tez-ui/src/main/webapp/app/scripts/components/basic-table/header-cell-view.js ---------------------------------------------------------------------- diff --git a/tez-ui/src/main/webapp/app/scripts/components/basic-table/header-cell-view.js b/tez-ui/src/main/webapp/app/scripts/components/basic-table/header-cell-view.js index d6773d6..b4f1165 100644 --- a/tez-ui/src/main/webapp/app/scripts/components/basic-table/header-cell-view.js +++ b/tez-ui/src/main/webapp/app/scripts/components/basic-table/header-cell-view.js @@ -19,6 +19,18 @@ App.BasicTableComponent.HeaderCellView = Ember.View.extend({ templateName: 'components/basic-table/header-cell', + sortIconCSS: function () { + var css = 'sort-icon '; + if(this.get('column.searchAndSortable') == false) { + css = 'no-display'; + } + else if(this.get('parentView.sortColumnId') == this.get('column.id')) { + css += this.get('parentView.sortOrder'); + } + + return css; + }.property('parentView.sortOrder', 'parentView.sortColumnId', 'column.searchAndSortable'), + _onColResize: function (event) { var data = event.data; @@ -39,6 +51,9 @@ App.BasicTableComponent.HeaderCellView = Ember.View.extend({ }, actions: { + sort: function () { + this.get('parentView').send('sort', this.get('column.id')); + }, startColResize: function () { var mouseTracker = { thisHeader: this, http://git-wip-us.apache.org/repos/asf/tez/blob/8ade35c8/tez-ui/src/main/webapp/app/scripts/components/basic-table/search-view.js ---------------------------------------------------------------------- diff --git a/tez-ui/src/main/webapp/app/scripts/components/basic-table/search-view.js b/tez-ui/src/main/webapp/app/scripts/components/basic-table/search-view.js new file mode 100644 index 0000000..4dd41a1 --- /dev/null +++ b/tez-ui/src/main/webapp/app/scripts/components/basic-table/search-view.js @@ -0,0 +1,48 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +App.BasicTableComponent.SearchView = Ember.View.extend({ + templateName: 'components/basic-table/search-view', + + classNames: ['search-view'], + + text: '', + _boundText: function () { + return this.get('text'); + }.property(), + + _validRegEx: function () { + var regExText = this.get('_boundText'); + regExText = regExText.substr(regExText.indexOf(':') + 1); + try { + new RegExp(regExText, 'im'); + } + catch(e) { + return false; + } + return true; + }.property('_boundText'), + + actions: { + search: function () { + if(this.get('_validRegEx')) { + this.get('parentView').send('search', this.get('_boundText')); + } + } + } +}); \ No newline at end of file http://git-wip-us.apache.org/repos/asf/tez/blob/8ade35c8/tez-ui/src/main/webapp/app/scripts/controllers/dag_tasks.js ---------------------------------------------------------------------- diff --git a/tez-ui/src/main/webapp/app/scripts/controllers/dag_tasks.js b/tez-ui/src/main/webapp/app/scripts/controllers/dag_tasks.js index 267eac5..761e0b6 100644 --- a/tez-ui/src/main/webapp/app/scripts/controllers/dag_tasks.js +++ b/tez-ui/src/main/webapp/app/scripts/controllers/dag_tasks.js @@ -104,6 +104,11 @@ App.DagTasksController = App.TablePageController.extend({ displayText: id.indexOf(idPrefix) == 0 ? id.substr(idPrefix.length) : id }; }, + getSearchValue: function (row) { + var id = row.get('id'), + idPrefix = 'task_%@_'.fmt(row.get('dagID').substr(4)); + return id.indexOf(idPrefix) == 0 ? id.substr(idPrefix.length) : id; + } }, { id: 'vertexName', @@ -113,6 +118,14 @@ App.DagTasksController = App.TablePageController.extend({ var vertexId = row.get('vertexID'); return vertexIdToNameMap[vertexId] || vertexId; }, + getSearchValue: function(row) { + var vertexId = row.get('vertexID'); + return vertexIdToNameMap[vertexId] || vertexId; + }, + getSortValue: function(row) { + var vertexId = row.get('vertexID'); + return vertexIdToNameMap[vertexId] || vertexId; + } }, { id: 'startTime', @@ -121,6 +134,9 @@ App.DagTasksController = App.TablePageController.extend({ getCellContent: function(row) { return App.Helpers.date.dateFormat(row.get('startTime')); }, + getSearchValue: function(row) { + return App.Helpers.date.dateFormat(row.get('startTime')); + } }, { id: 'endTime', @@ -129,6 +145,9 @@ App.DagTasksController = App.TablePageController.extend({ getCellContent: function(row) { return App.Helpers.date.dateFormat(row.get('endTime')); }, + getSearchValue: function(row) { + return App.Helpers.date.dateFormat(row.get('endTime')); + }, }, { id: 'duration', @@ -137,6 +156,9 @@ App.DagTasksController = App.TablePageController.extend({ getCellContent: function(row) { return App.Helpers.date.timingFormat(row.get('duration'), 1); }, + getSearchValue: function(row) { + return App.Helpers.date.timingFormat(row.get('duration'), 1); + }, }, { id: 'status', @@ -156,11 +178,13 @@ App.DagTasksController = App.TablePageController.extend({ headerCellName: 'Actions', templateName: 'components/basic-table/task-actions-cell', contentPath: 'id', + searchAndSortable: false }, { id: 'logs', headerCellName: 'Logs', templateName: 'components/basic-table/logs-cell', + searchAndSortable: false, getCellContent: function(row) { var taskAttemptId = row.get('successfulAttemptId') || row.get('attempts.lastObject'), store = that.get('store'); http://git-wip-us.apache.org/repos/asf/tez/blob/8ade35c8/tez-ui/src/main/webapp/app/scripts/controllers/table-page-controller.js ---------------------------------------------------------------------- diff --git a/tez-ui/src/main/webapp/app/scripts/controllers/table-page-controller.js b/tez-ui/src/main/webapp/app/scripts/controllers/table-page-controller.js index 002c6b5..7e17c0e 100644 --- a/tez-ui/src/main/webapp/app/scripts/controllers/table-page-controller.js +++ b/tez-ui/src/main/webapp/app/scripts/controllers/table-page-controller.js @@ -19,11 +19,16 @@ App.TablePageController = Em.ObjectController.extend( App.DataArrayLoaderMixin, App.ColumnSelectorMixin, { - queryParams: ['pageNum', 'rowCount'], + queryParams: ['pageNum', 'rowCount', 'searchText', 'sortColumnId', 'sortOrder'], + + sortColumnId: '', + sortOrder: '', pageNum: 1, rowCount: 25, + searchText: '', + isRefreshable: true, statusMessage: function () { http://git-wip-us.apache.org/repos/asf/tez/blob/8ade35c8/tez-ui/src/main/webapp/app/styles/main.less ---------------------------------------------------------------------- diff --git a/tez-ui/src/main/webapp/app/styles/main.less b/tez-ui/src/main/webapp/app/styles/main.less index 870e2ec..4687ac4 100644 --- a/tez-ui/src/main/webapp/app/styles/main.less +++ b/tez-ui/src/main/webapp/app/styles/main.less @@ -559,6 +559,12 @@ div.indent { white-space: nowrap; } + .search-view { + .inline-block; + .align-top; + width: 70%; + } + .extra-table-buttons { .inline-block; @@ -685,6 +691,27 @@ div.indent { height: 18px; } + .sort-icon { + .fa; + cursor: pointer; + position: absolute; + right: 8px; + top: 9px; + + opacity: .5; + .fa-icon(sort); + + &.asc { + opacity: 1; + top: 12px; + .fa-icon(sort-asc); + } + &.desc { + opacity: 1; + top: 6px; + .fa-icon(sort-desc); + } + } .resize-column { .fa; .fa-icon(ellipsis-v); http://git-wip-us.apache.org/repos/asf/tez/blob/8ade35c8/tez-ui/src/main/webapp/app/templates/common/table.hbs ---------------------------------------------------------------------- diff --git a/tez-ui/src/main/webapp/app/templates/common/table.hbs b/tez-ui/src/main/webapp/app/templates/common/table.hbs index 2c54903..631d08d 100644 --- a/tez-ui/src/main/webapp/app/templates/common/table.hbs +++ b/tez-ui/src/main/webapp/app/templates/common/table.hbs @@ -31,10 +31,17 @@ extraHeaderItem=App.ExtraTableButtonsView statusMessage=statusMessage + enableSearch=true enablePagination=true + enableSort=true pageNumBinding='pageNum' rowCountBinding='rowCount' + + searchTextBinding='searchText' + + sortColumnIdBinding='sortColumnId' + sortOrderBinding='sortOrder' }} {{else}} <h1>No records available!</n1> http://git-wip-us.apache.org/repos/asf/tez/blob/8ade35c8/tez-ui/src/main/webapp/app/templates/components/basic-table.hbs ---------------------------------------------------------------------- diff --git a/tez-ui/src/main/webapp/app/templates/components/basic-table.hbs b/tez-ui/src/main/webapp/app/templates/components/basic-table.hbs index ac4a466..175a3b9 100644 --- a/tez-ui/src/main/webapp/app/templates/components/basic-table.hbs +++ b/tez-ui/src/main/webapp/app/templates/components/basic-table.hbs @@ -20,6 +20,15 @@ {{#if _showHeader}} <div class='table-header'> <div class="horizontal-half align-top"> + {{#if enableSearch}} + {{view App.BasicTableComponent.SearchView text=searchText}} + {{/if}} + <div class="table-message"> + {{#if _statusMessage}} + <i class="waiting"></i> + {{_statusMessage}} + {{/if}} + </div> </div><div class="horizontal-half align-top align-children-right"> {{#if enablePagination}} {{view App.BasicTableComponent.PaginationView pageNum=pageNum totalPages=totalPages}} http://git-wip-us.apache.org/repos/asf/tez/blob/8ade35c8/tez-ui/src/main/webapp/app/templates/components/basic-table/header-cell.hbs ---------------------------------------------------------------------- diff --git a/tez-ui/src/main/webapp/app/templates/components/basic-table/header-cell.hbs b/tez-ui/src/main/webapp/app/templates/components/basic-table/header-cell.hbs index 1159524..c975d32 100644 --- a/tez-ui/src/main/webapp/app/templates/components/basic-table/header-cell.hbs +++ b/tez-ui/src/main/webapp/app/templates/components/basic-table/header-cell.hbs @@ -20,5 +20,8 @@ <div class='cell-content'> {{unbound column.headerCellName}} </div> + {{#if enableSort}} + <i {{bind-attr class=view.sortIconCSS}} {{action 'sort' target='view'}}></i> + {{/if}} <i class="resize-column" {{action 'startColResize' on=mouseDown target='view'}} ></i> </div> http://git-wip-us.apache.org/repos/asf/tez/blob/8ade35c8/tez-ui/src/main/webapp/app/templates/components/basic-table/search-view.hbs ---------------------------------------------------------------------- diff --git a/tez-ui/src/main/webapp/app/templates/components/basic-table/search-view.hbs b/tez-ui/src/main/webapp/app/templates/components/basic-table/search-view.hbs new file mode 100644 index 0000000..949fc24 --- /dev/null +++ b/tez-ui/src/main/webapp/app/templates/components/basic-table/search-view.hbs @@ -0,0 +1,32 @@ +{{! +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +}} + +<div {{bind-attr class=":input-group view._validRegEx::has-error"}}> + {{input + class="form-control" + placeholder="RegEx or Column1, Column2... :RegEx" + action="search" + value=view._boundText + targetObject=view + }} + <span class="input-group-btn"> + <button class="btn btn-default" type="button" {{action "search" target='view'}}> + Search + </button> + </span> +</div> http://git-wip-us.apache.org/repos/asf/tez/blob/8ade35c8/tez-ui/src/main/webapp/app/templates/partials/table.hbs ---------------------------------------------------------------------- diff --git a/tez-ui/src/main/webapp/app/templates/partials/table.hbs b/tez-ui/src/main/webapp/app/templates/partials/table.hbs index 79c2d1d..019735f 100644 --- a/tez-ui/src/main/webapp/app/templates/partials/table.hbs +++ b/tez-ui/src/main/webapp/app/templates/partials/table.hbs @@ -16,7 +16,7 @@ * limitations under the License. }} -<div class='table-container'> +<div class='ember-table-container'> {{extended-table-component hasFooter=false enableContentSelection=true
