Repository: zeppelin Updated Branches: refs/heads/master c7c9aa1cc -> bed82eb4b
[ZEPPELIN-2411] Improve Table ### What is this PR for? **Improve Table** - column filter - persist column state: type, order, hide/show, sorting, pinning - pagination - `setting` menu to configure table UI - group by + aggregation And **all these things are persisted and synchronized among web socket clients** See the screenshot section for more detail. ### What type of PR is it? [Improvement] ### Todos * [x] - Remove handsontable dependencies * [x] - Use npm packaged moment* packages. * [x] - Apply ui-grid * [x] - Add setting menu * [x] - Fix some issues * [x] - Persist column type ### What is the Jira issue? [ZEPPELIN-2411](https://issues.apache.org/jira/browse/ZEPPELIN-2411) ### How should this be tested? 1. Build: `mvn clean package -DskipTests; ./bin/zeppelin-daemon.sh restart` 2. Open a note and create tables. If you don't have proper paragraphs, use this snippet. ```scala %spark import org.apache.commons.io.IOUtils import java.net.URL import java.nio.charset.Charset val bankText = sc.parallelize( IOUtils.toString( new URL("https://s3.amazonaws.com/apache-zeppelin/tutorial/bank/bank.csv"), Charset.forName("utf8")).split("\n")) case class Bank( age: Integer, job: String, marital: String, education: String, balance: Integer, housing: Boolean, loan: Boolean, contact: String, day: Int, month: String, duration: Int, y: Boolean ) val bank = bankText.map(s => s.split(";")).filter(s => s(0) != "\"age\"").map( s => Bank(s(0).toInt, s(1).replaceAll("\"", ""), s(2).replaceAll("\"", ""), s(3).replaceAll("\"", ""), s(5).replaceAll("\"", "").toInt, if (s(6).replaceAll("\"", "") == "yes") true else false, if (s(7).replaceAll("\"", "") == "yes") true else false, s(8).replaceAll("\"", ""), s(9).replaceAll("\"", "").toInt, s(10).replaceAll("\"", ""), s(11).replaceAll("\"", "").toInt, if (s(16).replaceAll("\"", "") == "yes") true else false ) ).toDF() bank.registerTempTable("bank") ``` ```sql select age, education, job, balance from bank limit 1000 ``` ### Screenshots (if appropriate) #### Before  #### After: Filter  #### After: Column related features  #### After: Pagination  #### After: Group, Aggregation  #### After: Synchronized  ### Questions: * Does the licenses files need update? - YES, updated * Is there breaking changes for older versions? - NO * Does this needs documentation? - NO Author: 1ambda <[email protected]> Closes #2323 from 1ambda/ZEPPELIN-2411/prettify-table and squashes the following commits: c56edca [1ambda] fix: Change table cell color to white 2047c5f [1ambda] feat: Render HTML display in cell 2a79915 [1ambda] fix: Add tooltip and change icon for restore 39d64c1 [1ambda] fixup: tooltip 2b40978 [1ambda] fix: Remove save btn in table setting panel bd2b4c8 [1ambda] fix: Add tooltip for description 3a87718 [1ambda] fix: DON'T display type of table options 1885172 [1ambda] fix: Remove pagination related options from table spec 4878614 [1ambda] fix: order for table options e4d2c37 [1ambda] fix: Disable selection 06e7841 [1ambda] feat: Add option for selection a9d313d [1ambda] fix: Remove desc in opt specs cf597bf [1ambda] fix: Reset tableColumnTypeState 399116b [1ambda] fix: SparkParagraphIT for table 84c04ac [1ambda] fix: Remove duplicated console fdcfe7f [1ambda] chore: Remove unused license fc2f6d0 [1ambda] fix: css loader for karma test 4742a28 [1ambda] fix: RAT issue 8ea8ae5 [1ambda] fix: DON'T debounce for emit a1ae980 [1ambda] feat: Persist type d928290 [1ambda] fix: Use isRestoring flag to avoid triggering event when initializing a262b79 [1ambda] fix: Persist tableOption immediately f83c070 [1ambda] fix: Commit graph config when closing 6db38ae [1ambda] feat: Add missing change events 911c0e7 [1ambda] fix: Prevent recursive emitting d47ccc8 [1ambda] feat: Persist grid state b433d7f [1ambda] refactor: Add getGrid* funcs 1480841 [1ambda] fix: Remove refresh in menu actions 35d99e9 [1ambda] fix: enable scroll in col menus 25a72fe [1ambda] feat: Add types to menu acc38cf [1ambda] feat: Add paginiation table opts 8793d8f [1ambda] fix: set valid pagination opts 744dc66 [1ambda] docs: Update desc for table option 8bd8256 [1ambda] fix: persist initial config 78cec42 [1ambda] feat: resetTableOption 475bc31 [1ambda] feat: Add table option fc0abd4 [1ambda] feat: Use tabledata 85cdd8e [1ambda] feat: render setting for table 5ee6a2e [1ambda] fix: Remove handsonhelper while using moment form npm 2444855 [1ambda] refactor: variable name ed17862 [1ambda] fix: Remove unused css 1f61260 [1ambda] refactor: extract table related css to display-table.css Project: http://git-wip-us.apache.org/repos/asf/zeppelin/repo Commit: http://git-wip-us.apache.org/repos/asf/zeppelin/commit/bed82eb4 Tree: http://git-wip-us.apache.org/repos/asf/zeppelin/tree/bed82eb4 Diff: http://git-wip-us.apache.org/repos/asf/zeppelin/diff/bed82eb4 Branch: refs/heads/master Commit: bed82eb4bdf20db5c894444ccaed8a8631846997 Parents: c7c9aa1 Author: 1ambda <[email protected]> Authored: Fri May 19 00:43:23 2017 +0900 Committer: Lee moon soo <[email protected]> Committed: Sat May 20 15:10:20 2017 -0400 ---------------------------------------------------------------------- zeppelin-distribution/src/bin_license/LICENSE | 6 +- .../zeppelin/integration/SparkParagraphIT.java | 14 +- zeppelin-web/bower.json | 2 - zeppelin-web/karma.conf.js | 5 - zeppelin-web/package.json | 7 +- zeppelin-web/src/app/app.js | 13 + .../src/app/handsontable/handsonHelper.js | 201 ---------- .../src/app/jobmanager/jobs/job.controller.js | 2 + .../src/app/notebook/notebook-actionBar.html | 2 +- .../src/app/notebook/notebook.controller.js | 9 +- .../notebook/paragraph/paragraph.controller.js | 20 +- .../src/app/notebook/paragraph/paragraph.css | 103 ----- .../notebook/paragraph/result/display-table.css | 49 +++ .../paragraph/result/result-chart-selector.html | 2 +- .../paragraph/result/result.controller.js | 21 +- .../app/notebook/paragraph/result/result.html | 4 +- .../visualization-table-grid-filter.html | 27 ++ .../builtins/visualization-table-setting.html | 84 ++++ .../builtins/visualization-table.js | 385 +++++++++++++++++-- .../builtins/visualization-util.js | 172 +++++++++ .../components/editor/codeEditor.directive.js | 2 +- .../websocketEvents/websocketEvents.factory.js | 3 +- zeppelin-web/src/index.html | 8 +- zeppelin-web/src/index.js | 1 - zeppelin-web/webpack.config.js | 1 - 25 files changed, 774 insertions(+), 369 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/zeppelin/blob/bed82eb4/zeppelin-distribution/src/bin_license/LICENSE ---------------------------------------------------------------------- diff --git a/zeppelin-distribution/src/bin_license/LICENSE b/zeppelin-distribution/src/bin_license/LICENSE index 60fe5c0..7dd1fb3 100644 --- a/zeppelin-distribution/src/bin_license/LICENSE +++ b/zeppelin-distribution/src/bin_license/LICENSE @@ -244,9 +244,9 @@ The text of each license is also included at licenses/LICENSE-[project]-[version (The MIT License) lodash v3.9.3 (https://lodash.com/) - https://github.com/lodash/lodash/blob/3.9.3/LICENSE.txt (The MIT License) angular-filter v0.5.4 (https://github.com/a8m/angular-filter) - https://github.com/a8m/angular-filter/blob/v0.5.4/license.md (The MIT License) ngToast v2.0.0 (http://tamerayd.in/ngToast/) - http://tameraydin.mit-license.org/ - (The MIT License) Handsontable v0.24.2 (https://github.com/handsontable/handsontable) - https://github.com/handsontable/handsontable/blob/master/LICENSE - (The MIT License) Zeroclipboard v2.2.0 (https://github.com/zeroclipboard/zeroclipboard) - https://github.com/zeroclipboard/zeroclipboard/blob/v2.2.0/LICENSE - (The MIT License) Moment v2.9.0 (https://github.com/moment/moment) - https://github.com/moment/moment/blob/2.9.0/LICENSE + (The MIT License) moment v2.18.1 (https://github.com/moment/moment) - https://github.com/moment/moment/blob/2.18.1/LICENSE + (The MIT License) moment-duration-format v1.3.0 (https://github.com/jsmreese/moment-duration-format) - https://github.com/jsmreese/moment-duration-format/blob/1.3.0/LICENSE + (The MIT License) angular-ui-grid v4.0.4 (https://github.com/angular-ui/ui-grid) - https://github.com/angular-ui/ui-grid/blob/v4.0.4/LICENSE.md (The MIT License) Pikaday v1.3.2 (https://github.com/dbushell/Pikaday) - https://github.com/dbushell/Pikaday/blob/1.3.2/LICENSE (The MIT License) slf4j v1.7.10 (org.slf4j:slf4j-api:jar:1.7.10 - http://www.slf4j.org) - http://www.slf4j.org/license.html (The MIT License) slf4j v1.7.21 (org.slf4j:slf4j-simple:1.7.21 - http://www.slf4j.org) - http://www.slf4j.org/license.html http://git-wip-us.apache.org/repos/asf/zeppelin/blob/bed82eb4/zeppelin-server/src/test/java/org/apache/zeppelin/integration/SparkParagraphIT.java ---------------------------------------------------------------------- diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/integration/SparkParagraphIT.java b/zeppelin-server/src/test/java/org/apache/zeppelin/integration/SparkParagraphIT.java index 2cc6a6c..0aa0354 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/integration/SparkParagraphIT.java +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/integration/SparkParagraphIT.java @@ -32,6 +32,8 @@ import org.openqa.selenium.WebElement; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.List; + public class SparkParagraphIT extends AbstractZeppelinIT { private static final Logger LOG = LoggerFactory.getLogger(SparkParagraphIT.class); @@ -182,10 +184,16 @@ public class SparkParagraphIT extends AbstractZeppelinIT { ); } - WebElement paragraph1Result = driver.findElement(By.xpath( - getParagraphXPath(1) + "//div[contains(@id,\"_graph\")]/div/div/div/div/div[1]")); + // Age, Job, Marital, Education, Balance + List<WebElement> tableHeaders = driver.findElements(By.cssSelector("span.ui-grid-header-cell-label")); + String headerNames = ""; + + for(WebElement header : tableHeaders) { + headerNames += header.getText().toString() + "|"; + } + collector.checkThat("Paragraph from SparkParagraphIT of testSqlSpark result: ", - paragraph1Result.getText().toString(), CoreMatchers.equalTo("age\nâ¼\njob\nâ¼\nmarital\nâ¼\neducation\nâ¼\nbalance\nâ¼\n30 unemployed married primary 1787")); + headerNames, CoreMatchers.equalTo("Age|Job|Marital|Education|Balance|")); } catch (Exception e) { handleException("Exception in SparkParagraphIT while testSqlSpark", e); } http://git-wip-us.apache.org/repos/asf/zeppelin/blob/bed82eb4/zeppelin-web/bower.json ---------------------------------------------------------------------- diff --git a/zeppelin-web/bower.json b/zeppelin-web/bower.json index 30fc5f0..8e9a1e5 100644 --- a/zeppelin-web/bower.json +++ b/zeppelin-web/bower.json @@ -30,8 +30,6 @@ "ngtoast": "~2.0.0", "ng-focus-if": "~1.0.2", "bootstrap3-dialog": "bootstrap-dialog#~1.34.7", - "handsontable": "~0.24.2", - "moment-duration-format": "^1.3.0", "select2": "^4.0.3", "MathJax": "2.7.0", "ngclipboard": "^1.1.1" http://git-wip-us.apache.org/repos/asf/zeppelin/blob/bed82eb4/zeppelin-web/karma.conf.js ---------------------------------------------------------------------- diff --git a/zeppelin-web/karma.conf.js b/zeppelin-web/karma.conf.js index f47741a..b163b56 100644 --- a/zeppelin-web/karma.conf.js +++ b/zeppelin-web/karma.conf.js @@ -81,11 +81,6 @@ module.exports = function(config) { 'bower_components/ngtoast/dist/ngToast.js', 'bower_components/ng-focus-if/focusIf.js', 'bower_components/bootstrap3-dialog/dist/js/bootstrap-dialog.min.js', - 'bower_components/zeroclipboard/dist/ZeroClipboard.js', - 'bower_components/moment/moment.js', - 'bower_components/pikaday/pikaday.js', - 'bower_components/handsontable/dist/handsontable.js', - 'bower_components/moment-duration-format/lib/moment-duration-format.js', 'bower_components/select2/dist/js/select2.js', 'bower_components/MathJax/MathJax.js', 'bower_components/clipboard/dist/clipboard.js', http://git-wip-us.apache.org/repos/asf/zeppelin/blob/bed82eb4/zeppelin-web/package.json ---------------------------------------------------------------------- diff --git a/zeppelin-web/package.json b/zeppelin-web/package.json index a992a0f..39b230a 100644 --- a/zeppelin-web/package.json +++ b/zeppelin-web/package.json @@ -18,14 +18,16 @@ "dev:helium": "HELIUM_BUNDLE_DEV=true webpack-dev-server --hot", "dev:watch": "grunt watch-webpack-dev", "dev": "npm-run-all --parallel dev:server lint:watch dev:watch", - "pretest": "npm install karma-phantomjs-launcher babel-polyfill", "test": "karma start karma.conf.js" }, "dependencies": { "github-markdown-css": "2.6.0", + "angular-ui-grid": "^4.0.4", "grunt-angular-templates": "^0.5.7", "grunt-dom-munger": "^3.4.0", - "headroom.js": "^0.9.3" + "headroom.js": "^0.9.3", + "moment": "^2.18.1", + "moment-duration-format": "^1.3.0" }, "devDependencies": { "autoprefixer": "^6.5.4", @@ -73,6 +75,7 @@ "karma-coverage": "^1.1.1", "karma-jasmine": "~1.0.2", "karma-sourcemap-loader": "^0.3.7", + "karma-phantomjs-launcher": "^1.0.4", "karma-webpack": "^1.8.1", "load-grunt-tasks": "^0.4.0", "ng-annotate-loader": "^0.2.0", http://git-wip-us.apache.org/repos/asf/zeppelin/blob/bed82eb4/zeppelin-web/src/app/app.js ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/app.js b/zeppelin-web/src/app/app.js index fc5ffbd..427d340 100644 --- a/zeppelin-web/src/app/app.js +++ b/zeppelin-web/src/app/app.js @@ -18,6 +18,9 @@ import 'headroom.js' import 'headroom.js/dist/angular.headroom' +import 'angular-ui-grid/ui-grid.css' +import 'angular-ui-grid' + const requiredModules = [ 'ngCookies', 'ngAnimate', @@ -37,6 +40,16 @@ const requiredModules = [ 'focus-if', 'ngResource', 'ngclipboard', + 'ui.grid', + 'ui.grid.exporter', + 'ui.grid.edit', 'ui.grid.rowEdit', + 'ui.grid.selection', + 'ui.grid.cellNav', 'ui.grid.pinning', + 'ui.grid.grouping', + 'ui.grid.emptyBaseLayer', + 'ui.grid.resizeColumns', 'ui.grid.moveColumns', + 'ui.grid.pagination', + 'ui.grid.saveState', ] // headroom should not be used for CI, since we have to execute some integration tests. http://git-wip-us.apache.org/repos/asf/zeppelin/blob/bed82eb4/zeppelin-web/src/app/handsontable/handsonHelper.js ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/handsontable/handsonHelper.js b/zeppelin-web/src/app/handsontable/handsonHelper.js deleted file mode 100644 index 8d724c0..0000000 --- a/zeppelin-web/src/app/handsontable/handsonHelper.js +++ /dev/null @@ -1,201 +0,0 @@ -/* - * Licensed 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. - */ - -/** - * HandsonHelper class - */ -export default class HandsonHelper { - constructor (columns, rows, comment) { - this.columns = columns || [] - this.rows = rows || [] - this.comment = comment || '' - this._numericValidator = this._numericValidator.bind(this) - } - - getHandsonTableConfig (columns, columnNames, resultRows) { - let self = this - return { - colHeaders: columnNames, - data: resultRows, - rowHeaders: false, - stretchH: 'all', - sortIndicator: true, - columns: columns, - columnSorting: true, - contextMenu: false, - manualColumnResize: true, - manualRowResize: true, - readOnly: true, - readOnlyCellClassName: '', - fillHandle: false, - fragmentSelection: true, - disableVisualSelection: true, - cells: function (ro, co, pro) { - let cellProperties = {} - let colType = columns[co].type - cellProperties.renderer = function (instance, td, row, col, prop, value, cellProperties) { - self._cellRenderer(instance, td, row, col, prop, value, cellProperties, colType) - } - return cellProperties - }, - afterGetColHeader: function (col, TH) { - let instance = this - let menu = self._buildDropDownMenu(columns[col].type) - let button = self._buildTypeSwitchButton() - - self._addButtonMenuEvent(button, menu) - - Handsontable.Dom.addEvent(menu, 'click', function (event) { - if (event.target.nodeName === 'LI') { - self._setColumnType(columns, event.target.data.colType, instance, col) - } - }) - if (TH.firstChild.lastChild.nodeName === 'BUTTON') { - TH.firstChild.removeChild(TH.firstChild.lastChild) - } - TH.firstChild.appendChild(button) - TH.style['white-space'] = 'normal' - } - } - } - - /* - ** Private Service Functions - */ - - _addButtonMenuEvent (button, menu) { - Handsontable.Dom.addEvent(button, 'click', function (event) { - let changeTypeMenu - let position - let removeMenu - - document.body.appendChild(menu) - - event.preventDefault() - event.stopImmediatePropagation() - - changeTypeMenu = document.querySelectorAll('.changeTypeMenu') - - for (let i = 0, len = changeTypeMenu.length; i < len; i++) { - changeTypeMenu[i].style.display = 'none' - } - menu.style.display = 'block' - position = button.getBoundingClientRect() - - menu.style.top = (position.top + (window.scrollY || window.pageYOffset)) + 2 + 'px' - menu.style.left = (position.left) + 'px' - - removeMenu = function (event) { - if (menu.parentNode) { - menu.parentNode.removeChild(menu) - } - } - Handsontable.Dom.removeEvent(document, 'click', removeMenu) - Handsontable.Dom.addEvent(document, 'click', removeMenu) - }) - } - - _buildDropDownMenu (activeCellType) { - let menu = document.createElement('UL') - let types = ['text', 'numeric', 'date'] - let item - - menu.className = 'changeTypeMenu' - - for (let i = 0, len = types.length; i < len; i++) { - item = document.createElement('LI') - if ('innerText' in item) { - item.innerText = types[i] - } else { - item.textContent = types[i] - } - - item.data = {'colType': types[i]} - - if (activeCellType === types[i]) { - item.className = 'active' - } - menu.appendChild(item) - } - - return menu - } - - _buildTypeSwitchButton () { - let button = document.createElement('BUTTON') - - button.innerHTML = '\u25BC' - button.className = 'changeType' - - return button - } - - _isNumeric (value) { - if (!isNaN(value)) { - if (value.length !== 0) { - if (Number(value) <= Number.MAX_SAFE_INTEGER && Number(value) >= Number.MIN_SAFE_INTEGER) { - return true - } - } - } - return false - } - - _cellRenderer (instance, td, row, col, prop, value, cellProperties, colType) { - if (colType === 'numeric' && this._isNumeric(value)) { - cellProperties.format = '0,0.[00000]' - td.style.textAlign = 'left' - // eslint-disable-next-line prefer-rest-params - Handsontable.renderers.NumericRenderer.apply(this, arguments) - } else if (value.length > '%html'.length && value.substring(0, '%html '.length) === '%html ') { - td.innerHTML = value.substring('%html'.length) - } else { - // eslint-disable-next-line prefer-rest-params - Handsontable.renderers.TextRenderer.apply(this, arguments) - } - } - - _dateValidator (value, callback) { - let d = moment(value) - return callback(d.isValid()) - } - - _numericValidator (value, callback) { - return callback(this._isNumeric(value)) - } - - _setColumnType (columns, type, instance, col) { - columns[col].type = type - this._setColumnValidator(columns, col) - instance.updateSettings({columns: columns}) - instance.validateCells(null) - if (this._isColumnSorted(instance, col)) { - instance.sort(col, instance.sortOrder) - } - } - - _isColumnSorted (instance, col) { - return instance.sortingEnabled && instance.sortColumn === col - } - - _setColumnValidator (columns, col) { - if (columns[col].type === 'numeric') { - columns[col].validator = this._numericValidator - } else if (columns[col].type === 'date') { - columns[col].validator = this._dateValidator - } else { - columns[col].validator = null - } - } -} http://git-wip-us.apache.org/repos/asf/zeppelin/blob/bed82eb4/zeppelin-web/src/app/jobmanager/jobs/job.controller.js ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/jobmanager/jobs/job.controller.js b/zeppelin-web/src/app/jobmanager/jobs/job.controller.js index e1ce02d..e811f7b 100644 --- a/zeppelin-web/src/app/jobmanager/jobs/job.controller.js +++ b/zeppelin-web/src/app/jobmanager/jobs/job.controller.js @@ -12,6 +12,8 @@ * limitations under the License. */ +import moment from 'moment' + import { ParagraphStatus, } from '../../notebook/paragraph/paragraph.status' angular.module('zeppelinWebApp').controller('JobCtrl', JobCtrl) http://git-wip-us.apache.org/repos/asf/zeppelin/blob/bed82eb4/zeppelin-web/src/app/notebook/notebook-actionBar.html ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/notebook/notebook-actionBar.html b/zeppelin-web/src/app/notebook/notebook-actionBar.html index e83e05e..5b600f2 100644 --- a/zeppelin-web/src/app/notebook/notebook-actionBar.html +++ b/zeppelin-web/src/app/notebook/notebook-actionBar.html @@ -156,7 +156,7 @@ limitations under the License. <strong>{{revision.message}}</strong> </span> <span class="revisionDate"> - <em>{{moment.unix(revision.time).format('MMMM Do YYYY, h:mm a')}}</em> + <em>{{formatRevisionDate(revision.time)}}</em> </span> </a> </li> http://git-wip-us.apache.org/repos/asf/zeppelin/blob/bed82eb4/zeppelin-web/src/app/notebook/notebook.controller.js ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/notebook/notebook.controller.js b/zeppelin-web/src/app/notebook/notebook.controller.js index 4b02f1a..3944326 100644 --- a/zeppelin-web/src/app/notebook/notebook.controller.js +++ b/zeppelin-web/src/app/notebook/notebook.controller.js @@ -12,6 +12,8 @@ * limitations under the License. */ +import moment from 'moment' + import { isParagraphRunning, } from './paragraph/paragraph.status' angular.module('zeppelinWebApp').controller('NotebookCtrl', NotebookCtrl) @@ -25,7 +27,6 @@ function NotebookCtrl ($scope, $route, $routeParams, $location, $rootScope, ngToast.dismiss() $scope.note = null - $scope.moment = moment $scope.editorToggled = false $scope.tableToggled = false $scope.viewOnly = false @@ -42,6 +43,10 @@ function NotebookCtrl ($scope, $route, $routeParams, $location, $rootScope, {name: '1d', value: '0 0 0 * * ?'} ] + $scope.formatRevisionDate = function(date) { + return moment.unix(date).format('MMMM Do YYYY, h:mm a') + } + $scope.interpreterSettings = [] $scope.interpreterBindings = [] $scope.isNoteDirty = null @@ -223,7 +228,7 @@ function NotebookCtrl ($scope, $route, $routeParams, $location, $rootScope, } $scope.$on('listRevisionHistory', function (event, data) { - console.log('received list of revisions %o', data) + console.debug('received list of revisions %o', data) $scope.noteRevisions = data.revisionList $scope.noteRevisions.splice(0, 0, { id: 'Head', http://git-wip-us.apache.org/repos/asf/zeppelin/blob/bed82eb4/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js index a6564d4..3d75676 100644 --- a/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js +++ b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js @@ -17,6 +17,9 @@ import { ParagraphStatus, isParagraphRunning, } from './paragraph.status' +import moment from 'moment' +require('moment-duration-format') + const ParagraphExecutor = { SPELL: 'SPELL', INTERPRETER: 'INTERPRETER', @@ -983,19 +986,24 @@ function ParagraphCtrl ($scope, $rootScope, $route, $window, $routeParams, $loca } $scope.getExecutionTime = function (pdata) { - let timeMs = Date.parse(pdata.dateFinished) - Date.parse(pdata.dateStarted) + const end = pdata.dateFinished + const start = pdata.dateStarted + let timeMs = Date.parse(end) - Date.parse(start) if (isNaN(timeMs) || timeMs < 0) { if ($scope.isResultOutdated(pdata)) { return 'outdated' } return '' } + + const durationFormat = moment.duration((timeMs / 1000), 'seconds').format('h [hrs] m [min] s [sec]') + const endFormat = moment(pdata.dateFinished).format('MMMM DD YYYY, h:mm:ss A') + let user = (pdata.user === undefined || pdata.user === null) ? 'anonymous' : pdata.user - let desc = 'Took ' + moment.duration((timeMs / 1000), 'seconds').format('h [hrs] m [min] s [sec]') + - '. Last updated by ' + user + ' at ' + moment(pdata.dateFinished).format('MMMM DD YYYY, h:mm:ss A') + '.' - if ($scope.isResultOutdated(pdata)) { - desc += ' (outdated)' - } + let desc = `Took ${durationFormat}. Last updated by ${user} at ${endFormat}.` + + if ($scope.isResultOutdated(pdata)) { desc += ' (outdated)' } + return desc } http://git-wip-us.apache.org/repos/asf/zeppelin/blob/bed82eb4/zeppelin-web/src/app/notebook/paragraph/paragraph.css ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph.css b/zeppelin-web/src/app/notebook/paragraph/paragraph.css index e517be4..b17acf7 100644 --- a/zeppelin-web/src/app/notebook/paragraph/paragraph.css +++ b/zeppelin-web/src/app/notebook/paragraph/paragraph.css @@ -52,21 +52,6 @@ width: 100%; } -table.dataTable { - margin-top: 0px !important; - margin-bottom: 6px !important; -} - -table.dataTable.table-condensed > thead > tr > th { - padding-right: 28px; -} - -table.dataTable.table-condensed .sorting:after, -table.dataTable.table-condensed .sorting_asc:after, -table.dataTable.table-condensed .sorting_desc:after { - right: 12px; -} - .plainTextContainer { font-family: "Monaco","Menlo","Ubuntu Mono","Consolas","source-code-pro",monospace; font-size: 12px !important; @@ -488,46 +473,6 @@ table.table-shortcut { } /* - Handsontable -*/ - -.handsontable th { - font-weight: bold; -} - -.handsontable th, .handsontable td { - border-right: 0px; - border-left: 0px !important; - padding: 4px; -} - -.handsontable tr:first-child th { - text-align: left; - border-top: 0px; - padding: 4px 0px 0px 0px; - border-left: 0px; - border-right: 0px; - border-bottom: 2px solid #CCC; -} - -.handsontable .columnSorting.ascending::after { - content: '\f160'; - margin-left: 3px; - font-size: 12px; - font-family: FontAwesome; - line-height: 2; -} - -.handsontable .columnSorting.descending::after { - content: '\f161'; - margin-left: 3px; - font-size: 12px; - margin-left: 3px; - font-family: FontAwesome; - line-height: 2; -} - -/* Pivot CSS */ @@ -583,54 +528,6 @@ table.table-striped { width: 20px; } - -.changeType { - border: 1px solid #bbb; - color: #bbb; - background: #eee; - border-radius: 2px; - padding: 2px; - font-size: 9px; - float: right; - line-height: 9px; - margin: 3px 3px 0 0; -} -.changeType:hover { - border: 1px solid #777; - color: #777; - cursor: pointer; -} -.changeType.pressed { - background-color: #999; -} -.changeTypeMenu { - position: absolute; - border: 1px solid #ccc; - margin-top: 22px; - box-shadow: 0 1px 3px -1px #323232; - background: white; - padding: 0; - font-size: 13px; - display: none; - z-index: 10; -} -.changeTypeMenu li { - text-align: left; - list-style: none; - padding: 2px 20px; - cursor: pointer; - margin-bottom: 0; -} -.changeTypeMenu li.active:before { - font-size: 12px; - content: "\2714"; - margin-left: -15px; - margin-right: 3px; -} -.changeTypeMenu li:hover { - background: #eee; -} - /* Overwrite github-markdown-css for Markdown interpreter */ http://git-wip-us.apache.org/repos/asf/zeppelin/blob/bed82eb4/zeppelin-web/src/app/notebook/paragraph/result/display-table.css ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/notebook/paragraph/result/display-table.css b/zeppelin-web/src/app/notebook/paragraph/result/display-table.css new file mode 100644 index 0000000..9462109 --- /dev/null +++ b/zeppelin-web/src/app/notebook/paragraph/result/display-table.css @@ -0,0 +1,49 @@ +/* + * Licensed 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. + */ + +.ui-grid-pager-panel { + vertical-align: middle; + background-color: #f3f3f3; +} + +.ui-grid-footer-info { + padding: 5px; + background-color: #f3f3f3; +} + +.ui-grid-menu { + overflow: auto; /** enable scrollbar in column menu */ +} + +/** disable alternative color for even lows in a table */ +.ui-grid-row:nth-child(even) .ui-grid-cell { + background-color: white !important; +} + +/** support `height: auto` for cells */ +.ui-grid-viewport .ui-grid-cell-contents { + word-wrap: break-word; + white-space: normal !important; + border-bottom: 1px solid #e8e8e8; +} + +.ui-grid-row, .ui-grid-cell { + height: auto !important; +} + +.ui-grid-row div[role=row] { + display: flex ; + align-content: stretch; +} + http://git-wip-us.apache.org/repos/asf/zeppelin/blob/bed82eb4/zeppelin-web/src/app/notebook/paragraph/result/result-chart-selector.html ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/notebook/paragraph/result/result-chart-selector.html b/zeppelin-web/src/app/notebook/paragraph/result/result-chart-selector.html index d8a316b..4becdc6 100644 --- a/zeppelin-web/src/app/notebook/paragraph/result/result-chart-selector.html +++ b/zeppelin-web/src/app/notebook/paragraph/result/result-chart-selector.html @@ -93,7 +93,7 @@ limitations under the License. </div> <span - ng-if="type=='TABLE' && !config.helium.activeApp && graphMode!='table' && !asIframe && !viewOnly" + ng-if="type=='TABLE' && !config.helium.activeApp && !asIframe && !viewOnly" style="margin-left:10px; cursor:pointer; display: inline-block; vertical-align:top; position: relative; line-height:30px;"> <a class="btnText" ng-click="toggleGraphSetting()"> settings <span ng-class="config.graph.optionOpen ? 'fa fa-caret-up' : 'fa fa-caret-down'"></span> http://git-wip-us.apache.org/repos/asf/zeppelin/blob/bed82eb4/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js b/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js index 4c9ad84..8237c78 100644 --- a/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js +++ b/zeppelin-web/src/app/notebook/paragraph/result/result.controller.js @@ -12,6 +12,8 @@ * limitations under the License. */ +import moment from 'moment' + import TableData from '../../../tabledata/tabledata' import TableVisualization from '../../../visualization/builtins/visualization-table' import BarchartVisualization from '../../../visualization/builtins/visualization-barchart' @@ -25,11 +27,14 @@ import { } from '../../../spell' import { ParagraphStatus, } from '../paragraph.status' +const TableGridFilterTemplate = require('../../../visualization/builtins/visualization-table-grid-filter.html') + angular.module('zeppelinWebApp').controller('ResultCtrl', ResultCtrl) function ResultCtrl ($scope, $rootScope, $route, $window, $routeParams, $location, - $timeout, $compile, $http, $q, $templateRequest, $sce, websocketMsgSrv, - baseUrlSrv, ngToast, saveAsService, noteVarShareService, heliumService) { + $timeout, $compile, $http, $q, $templateCache, $templateRequest, $sce, websocketMsgSrv, + baseUrlSrv, ngToast, saveAsService, noteVarShareService, heliumService, + uiGridConstants) { 'ngInject' /** @@ -527,6 +532,12 @@ function ResultCtrl ($scope, $rootScope, $route, $window, $routeParams, $locatio } builtInViz.instance._emitter = emitter builtInViz.instance._compile = $compile + + // ui-grid related + $templateCache.put('ui-grid/ui-grid-filter', TableGridFilterTemplate) + builtInViz.instance._uiGridConstants = uiGridConstants + builtInViz.instance._timeout = $timeout + builtInViz.instance._createNewScope = createNewScope builtInViz.instance._templateRequest = $templateRequest const transformation = builtInViz.instance.getTransformation() @@ -616,8 +627,8 @@ function ResultCtrl ($scope, $rootScope, $route, $window, $routeParams, $locatio } else { newConfig.graph.optionOpen = true } - let newParams = angular.copy(paragraph.settings.params) + let newParams = angular.copy(paragraph.settings.params) commitParagraphResult(paragraph.title, paragraph.text, newConfig, newParams) } @@ -646,7 +657,7 @@ function ResultCtrl ($scope, $rootScope, $route, $window, $routeParams, $locatio } } } - console.log('getVizConfig', config) + console.debug('getVizConfig', config) return config } @@ -675,7 +686,7 @@ function ResultCtrl ($scope, $rootScope, $route, $window, $routeParams, $locatio newConfig.graph.values = newConfig.graph.commonSetting.pivot.values delete newConfig.graph.commonSetting.pivot } - console.log('committVizConfig', newConfig) + console.debug('committVizConfig', newConfig) let newParams = angular.copy(paragraph.settings.params) commitParagraphResult(paragraph.title, paragraph.text, newConfig, newParams) } http://git-wip-us.apache.org/repos/asf/zeppelin/blob/bed82eb4/zeppelin-web/src/app/notebook/paragraph/result/result.html ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/notebook/paragraph/result/result.html b/zeppelin-web/src/app/notebook/paragraph/result/result.html index 5a523de..faac9cd 100644 --- a/zeppelin-web/src/app/notebook/paragraph/result/result.html +++ b/zeppelin-web/src/app/notebook/paragraph/result/result.html @@ -25,9 +25,9 @@ limitations under the License. <div ng-if="type=='TABLE'" ng-style="getPointerEvent()"> - <!-- graph setting --> + <!-- setting --> <div class="option lightBold" style="overflow: visible;" - ng-show="graphMode!='table' && config.graph.optionOpen && !asIframe && !viewOnly"> + ng-show="config.graph.optionOpen && !asIframe && !viewOnly"> <div ng-repeat="viz in builtInTableDataVisualizationList track by $index" id="trsetting{{id}}_{{viz.id}}" ng-show="graphMode == viz.id"></div> http://git-wip-us.apache.org/repos/asf/zeppelin/blob/bed82eb4/zeppelin-web/src/app/visualization/builtins/visualization-table-grid-filter.html ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/visualization/builtins/visualization-table-grid-filter.html b/zeppelin-web/src/app/visualization/builtins/visualization-table-grid-filter.html new file mode 100644 index 0000000..d4b7e98 --- /dev/null +++ b/zeppelin-web/src/app/visualization/builtins/visualization-table-grid-filter.html @@ -0,0 +1,27 @@ +<!-- +Licensed 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 class="ui-grid-filter-container" + ng-repeat="colFilter in col.filters" + ng-class="{'ui-grid-filter-cancel-button-hidden' : colFilter.disableCancelFilterButton === true }"> + <div ng-if="colFilter.type !== 'select'"> + <input type="text" class="input-sm form-control" + style="font-size: 14px; font-weight: 500" + + ng-model="colFilter.term" + ng-model-options="{ debounce : { 'default' : 300, 'blur' : 0 }}" + ng-attr-placeholder="{{colFilter.placeholder || ''}}" + aria-label="{{colFilter.ariaLabel || aria.defaultFilterLabel}}" /> + </div> +</div> http://git-wip-us.apache.org/repos/asf/zeppelin/blob/bed82eb4/zeppelin-web/src/app/visualization/builtins/visualization-table-setting.html ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/visualization/builtins/visualization-table-setting.html b/zeppelin-web/src/app/visualization/builtins/visualization-table-setting.html new file mode 100644 index 0000000..d01fd1b --- /dev/null +++ b/zeppelin-web/src/app/visualization/builtins/visualization-table-setting.html @@ -0,0 +1,84 @@ +<!-- +Licensed 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 class="panel panel-default" style="margin-top: 10px; margin-bottom: 11px;"> + <div class="panel-heading" style="padding: 6px 12px 6px 12px; font-size: 13px;"> + <span style="vertical-align: middle; display: inline-block; margin-top: 3px;">Table Options</span> + <span style="float: right;"> + <div class="btn-group" role="group" aria-label="..."> + <div type="button" ng-click="resetTableOption()" + uib-tooltip="Restore the default setting" tooltip-placement="left" + class="btn btn-default" style="font-size: 11px; padding: 2px 5px 2px 5px;"> + <i class="fa fa-undo" aria-hidden="true"></i> + </div> + </div> + </span> + <div style="clear: both;"></div> <!-- to fix previous span which has float: right --> + </div> + + <div class="panel-body" style="padding: 8px 12px; margin-top: 3px;"> + <table class="table table-striped"> + <tr> + <th style="font-size: 12px; font-style: italic">Name</th> + <th style="font-size: 12px; font-style: italic">Value</th> + </tr> + <tr> + </tr> + + <tr data-ng-repeat="optSpec in tableOptionSpecs"> + <td style="font-weight: 400; vertical-align: middle;"> + <span uib-tooltip="{{optSpec.description}}" tooltip-placement="right"> + {{optSpec.name}} + <i class="fa fa-info-circle" style="margin-top: 2px; margin-left: 3px; color: #7b7bbd;" aria-hidden="true"></i> + </span> + </td> + <td> + <div ng-if="isInputWidget(optSpec)" + class="input-group"> + <input type="text" class="form-control input-sm" + style="font-weight: 400; font-size: 12px; vertical-align:middle; border-radius: 5px;" + ng-keydown="tableWidgetOnKeyDown($event, optSpec)" + data-ng-model="config.tableOptionValue[optSpec.name]" /> + </div> + <div class="btn-group" + ng-if="isOptionWidget(optSpec)"> + <select class="form-control input-sm" + ng-change="tableOptionValueChanged(optSpec)" + data-ng-model="config.tableOptionValue[optSpec.name]" + data-ng-options="optionValue for optionValue in optSpec.optionValues" + style="font-weight: 400; font-size: 12px;"> + </select> + </div> + + <div ng-if="isCheckboxWidget(optSpec)"> + <input type="checkbox" + ng-keydown="parameterOnKeyDown($event, optSpec)" + ng-click="tableOptionValueChanged(optSpec)" + data-ng-model="config.tableOptionValue[optSpec.name]" + data-ng-checked="config.tableOptionValue[optSpec.name]" /> + </div> + + <div ng-if="isTextareaWidget(optSpec)"> + <textarea class="form-control" rows="3" + ng-keydown="tableWidgetOnKeyDown($event, optSpec)" + data-ng-model="config.tableOptionValue[optSpec.name]" + style="font-weight: 400; font-size: 12px;"> + </textarea> + </div> + + </td> + </tr> + </table> + </div> +</div> http://git-wip-us.apache.org/repos/asf/zeppelin/blob/bed82eb4/zeppelin-web/src/app/visualization/builtins/visualization-table.js ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/visualization/builtins/visualization-table.js b/zeppelin-web/src/app/visualization/builtins/visualization-table.js index 3192ee6..f8c280a 100644 --- a/zeppelin-web/src/app/visualization/builtins/visualization-table.js +++ b/zeppelin-web/src/app/visualization/builtins/visualization-table.js @@ -14,7 +14,41 @@ import Visualization from '../visualization' import PassthroughTransformation from '../../tabledata/passthrough' -import HandsonHelper from '../../handsontable/handsonHelper' + +import { + Widget, ValueType, + isInputWidget, isOptionWidget, isCheckboxWidget, + isTextareaWidget, isBtnGroupWidget, + initializeTableConfig, resetTableOptionConfig, + DefaultTableColumnType, TableColumnType, updateColumnTypeState, + parseTableOption, +} from './visualization-util' + +const SETTING_TEMPLATE = require('./visualization-table-setting.html') + +const TABLE_OPTION_SPECS = [ + { + name: 'useFilter', + valueType: ValueType.BOOLEAN, + defaultValue: false, + widget: Widget.CHECKBOX, + description: 'Enable filter for columns', + }, + { + name: 'showPagination', + valueType: ValueType.BOOLEAN, + defaultValue: false, + widget: Widget.CHECKBOX, + description: 'Enable pagination for better navigation', + }, + { + name: 'showAggregationFooter', + valueType: ValueType.BOOLEAN, + defaultValue: false, + widget: Widget.CHECKBOX, + description: 'Enable a footer for displaying aggregated values', + }, +] /** * Visualize data in table format @@ -22,42 +56,349 @@ import HandsonHelper from '../../handsontable/handsonHelper' export default class TableVisualization extends Visualization { constructor (targetEl, config) { super(targetEl, config) - console.log('Init table viz') - targetEl.addClass('table') this.passthrough = new PassthroughTransformation(config) + this.emitTimeout = null + this.isRestoring = false + + initializeTableConfig(config, TABLE_OPTION_SPECS) + } + + createGridOptions(tableData, onRegisterApiCallback, config) { + const rows = tableData.rows + const columnNames = tableData.columns.map(c => c.name) + + const gridData = rows.map(r => { + return columnNames.reduce((acc, colName, index) => { + acc[colName] = r[index] + return acc + }, {}) + }) + + const gridOptions = { + data: gridData, + enableGridMenu: true, + modifierKeysToMultiSelectCells: true, + exporterMenuCsv: true, + exporterMenuPdf: false, + flatEntityAccess: true, + fastWatch: false, + treeRowHeaderAlwaysVisible: false, + + columnDefs: columnNames.map(colName => { + return { + name: colName, + type: DefaultTableColumnType, + cellTemplate: ` + <div ng-if="!grid.getCellValue(row, col).startsWith('%html')" + class="ui-grid-cell-contents"> + {{grid.getCellValue(row, col)}} + </div> + <div ng-if="grid.getCellValue(row, col).startsWith('%html')" + ng-bind-html="grid.getCellValue(row, col).split('%html')[1]" + class="ui-grid-cell-contents"> + </div> + `, + } + }), + rowEditWaitInterval: -1, /** disable saveRow event */ + enableRowHashing: true, + saveFocus: false, + saveScroll: false, + saveSort: true, + savePinning: true, + saveGrouping: true, + saveGroupingExpandedStates: true, + saveOrder: true, // column order + saveVisible: true, // column visibility + saveTreeView: true, + saveFilter: true, + saveSelection: false, + } + + return gridOptions } - refresh () { - this.hot.render() + getGridElemId() { + // angular doesn't allow `-` in scope variable name + const gridElemId = `${this.targetEl[0].id}_grid`.replace('-', '_') + return gridElemId } - render (tableData) { - let height = this.targetEl.height() - let container = this.targetEl.css('height', height).get(0) - let resultRows = tableData.rows - let columnNames = _.pluck(tableData.columns, 'name') - // eslint-disable-next-line prefer-spread - let columns = Array.apply(null, Array(tableData.columns.length)).map(function () { - return {type: 'text'} + getGridApiId() { + // angular doesn't allow `-` in scope variable name + const gridApiId = `${this.targetEl[0].id}_gridApi`.replace('-', '_') + return gridApiId + } + + refresh() { + const gridElemId = this.getGridElemId() + const gridElem = angular.element(`#${gridElemId}`) + + if (gridElem) { + gridElem.css('height', this.targetEl.height() - 10) + } + } + + refreshGrid() { + const gridElemId = this.getGridElemId() + const gridElem = angular.element(`#${gridElemId}`) + + if (gridElem) { + const scope = this.getScope() + const gridApiId = this.getGridApiId() + scope[gridApiId].core.notifyDataChange(this._uiGridConstants.dataChange.ALL) + } + } + + updateColDefType(colDef, type) { + if (type === colDef.type) { return } + + colDef.type = type + const colName = colDef.name + const config = this.config + if (config.tableColumnTypeState.names && config.tableColumnTypeState.names[colName]) { + config.tableColumnTypeState.names[colName] = type + this.persistConfigWithGridState(this.config) + } + } + + addColumnMenus(gridOptions) { + if (!gridOptions || !gridOptions.columnDefs) { return } + + const self = this // for closure + + // SHOULD use `function() { ... }` syntax for each action to get `this` + gridOptions.columnDefs.map(colDef => { + colDef.menuItems = [ + { + title: 'Type: String', + action: function() { + self.updateColDefType(this.context.col.colDef, TableColumnType.STRING) + }, + active: function() { + return this.context.col.colDef.type === TableColumnType.STRING + }, + }, + { + title: 'Type: Number', + action: function() { + self.updateColDefType(this.context.col.colDef, TableColumnType.NUMBER) + }, + active: function() { + return this.context.col.colDef.type === TableColumnType.NUMBER + }, + }, + { + title: 'Type: Date', + action: function() { + self.updateColDefType(this.context.col.colDef, TableColumnType.DATE) + }, + active: function() { + return this.context.col.colDef.type === TableColumnType.DATE + }, + }, + ] }) + } + + setDynamicGridOptions(gridOptions, config) { + // parse based on their type definitions + const parsed = parseTableOption(TABLE_OPTION_SPECS, config.tableOptionValue) + + const { showAggregationFooter, useFilter, showPagination, } = parsed - if (this.hot) { - this.hot.destroy() + gridOptions.showGridFooter = false + gridOptions.showColumnFooter = showAggregationFooter + gridOptions.enableFiltering = useFilter + + gridOptions.enablePagination = showPagination + gridOptions.enablePaginationControls = showPagination + + if (showPagination) { + gridOptions.paginationPageSize = 50 + gridOptions.paginationPageSizes = [25, 50, 100, 250, 1000] } - let handsonHelper = new HandsonHelper() - this.hot = new Handsontable(container, handsonHelper.getHandsonTableConfig( - columns, columnNames, resultRows)) - this.hot.validateCells(null) + // selection can't be rendered dynamically in ui-grid 4.0.4 + gridOptions.enableRowSelection = false + gridOptions.enableRowHeaderSelection = false + gridOptions.enableFullRowSelection = false + gridOptions.enableSelectAll = false + gridOptions.enableGroupHeaderSelection = false + gridOptions.enableSelectionBatchEvent = false } - destroy () { - if (this.hot) { - this.hot.destroy() + render (tableData) { + const gridElemId = this.getGridElemId() + let gridElem = document.getElementById(gridElemId) + + const config = this.config + const self = this // for closure + + if (!gridElem) { + // create, compile and append grid elem + gridElem = angular.element( + `<div id="${gridElemId}" ui-grid="${gridElemId}" + ui-grid-edit ui-grid-row-edit + ui-grid-pagination + ui-grid-selection + ui-grid-cellNav ui-grid-pinning + ui-grid-empty-base-layer + ui-grid-resize-columns ui-grid-move-columns + ui-grid-grouping + ui-grid-save-state + ui-grid-exporter></div>`) + + gridElem.css('height', this.targetEl.height() - 10) + const scope = this.getScope() + gridElem = this._compile(gridElem)(scope) + this.targetEl.append(gridElem) + + // set gridOptions for this elem + const gridOptions = this.createGridOptions(tableData, onRegisterApiCallback, config) + this.setDynamicGridOptions(gridOptions, config) + this.addColumnMenus(gridOptions) + scope[gridElemId] = gridOptions + + // set gridApi for this elem + const gridApiId = this.getGridApiId() + const onRegisterApiCallback = (gridApi) => { + scope[gridApiId] = gridApi + // should restore state before registering APIs + + // register callbacks for change evens + // should persist `self.config` instead `config` (closure issue) + gridApi.core.on.columnVisibilityChanged(scope, () => { self.persistConfigWithGridState(self.config) }) + gridApi.colMovable.on.columnPositionChanged(scope, () => { self.persistConfigWithGridState(self.config) }) + gridApi.core.on.sortChanged(scope, () => { self.persistConfigWithGridState(self.config) }) + gridApi.core.on.filterChanged(scope, () => { self.persistConfigWithGridState(self.config) }) + gridApi.grouping.on.aggregationChanged(scope, () => { self.persistConfigWithGridState(self.config) }) + gridApi.grouping.on.groupingChanged(scope, () => { self.persistConfigWithGridState(self.config) }) + gridApi.treeBase.on.rowCollapsed(scope, () => { self.persistConfigWithGridState(self.config) }) + gridApi.treeBase.on.rowExpanded(scope, () => { self.persistConfigWithGridState(self.config) }) + + // pagination doesn't follow usual life-cycle in ui-grid v4.0.4 + // gridApi.pagination.on.paginationChanged(scope, () => { self.persistConfigWithGridState(self.config) }) + // TBD: do we need to propagate row selection? + // gridApi.selection.on.rowSelectionChanged(scope, () => { self.persistConfigWithGridState(self.config) }) + // gridApi.selection.on.rowSelectionChangedBatch(scope, () => { self.persistConfigWithGridState(self.config) }) + } + gridOptions.onRegisterApi = onRegisterApiCallback + } else { + // don't need to update gridOptions.data since it's synchronized by paragraph execution + const gridOptions = this.getGridOptions() + this.setDynamicGridOptions(gridOptions, config) + this.refreshGrid() + } + + const columnDefs = this.getGridOptions().columnDefs + updateColumnTypeState(tableData.columns, config, columnDefs) + // SHOULD restore grid state after columnDefs are updated + this.restoreGridState(config.tableGridState) + } + + restoreGridState(gridState) { + if (!gridState) { return } + + // should set isRestoring to avoid that changed* events are triggered while restoring + this.isRestoring = true + const gridApi = this.getGridApi() + + // restore grid state when gridApi is available + if (!gridApi) { + setTimeout(() => this.restoreGridState(gridState), 100) + } else { + gridApi.saveState.restore(this.getScope(), gridState) + this.isRestoring = false } } + destroy () { + } + getTransformation () { return this.passthrough } + + getScope() { + const scope = this.targetEl.scope() + return scope + } + + getGridOptions() { + const scope = this.getScope() + const gridElemId = this.getGridElemId() + return scope[gridElemId] + } + + getGridApi() { + const scope = this.targetEl.scope() + const gridApiId = this.getGridApiId() + return scope[gridApiId] + } + + persistConfigImmediatelyWithGridState(config) { + this.persistConfigWithGridState(config) + } + + persistConfigWithGridState(config) { + if (this.isRestoring) { return } + + const gridApi = this.getGridApi() + config.tableGridState = gridApi.saveState.save() + this.emitConfig(config) + } + + persistConfig(config) { + this.emitConfig(config) + } + + getSetting (chart) { + const self = this // for closure in scope + const configObj = self.config + + // emit config if it's updated in `render` + if (configObj.initialized) { + configObj.initialized = false + this.persistConfig(configObj) // should persist w/o state + } else if (configObj.tableColumnTypeState && + configObj.tableColumnTypeState.updated) { + configObj.tableColumnTypeState.updated = false + this.persistConfig(configObj) // should persist w/o state + } + + return { + template: SETTING_TEMPLATE, + scope: { + config: configObj, + tableOptionSpecs: TABLE_OPTION_SPECS, + isInputWidget: isInputWidget, + isOptionWidget: isOptionWidget, + isCheckboxWidget: isCheckboxWidget, + isTextareaWidget: isTextareaWidget, + isBtnGroupWidget: isBtnGroupWidget, + tableOptionValueChanged: () => { + self.persistConfigWithGridState(configObj) + }, + saveTableOption: () => { + self.persistConfigWithGridState(configObj) + }, + resetTableOption: () => { + resetTableOptionConfig(configObj) + initializeTableConfig(configObj, TABLE_OPTION_SPECS) + self.persistConfigWithGridState(configObj) + }, + tableWidgetOnKeyDown: (event, optSpec) => { + const code = event.keyCode || event.which + if (code === 13 && isInputWidget(optSpec)) { + self.persistConfigWithGridState(configObj) + } else if (code === 13 && event.shiftKey && isTextareaWidget(optSpec)) { + self.persistConfigWithGridState(configObj) + } + + event.stopPropagation() /** avoid to conflict with paragraph shortcuts */ + } + } + } + } } http://git-wip-us.apache.org/repos/asf/zeppelin/blob/bed82eb4/zeppelin-web/src/app/visualization/builtins/visualization-util.js ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/visualization/builtins/visualization-util.js b/zeppelin-web/src/app/visualization/builtins/visualization-util.js new file mode 100644 index 0000000..cd9cd48 --- /dev/null +++ b/zeppelin-web/src/app/visualization/builtins/visualization-util.js @@ -0,0 +1,172 @@ +/* + * Licensed 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. + */ + +export const Widget = { + CHECKBOX: 'checkbox', + INPUT: 'input', + TEXTAREA: 'textarea', + OPTION: 'option', + BTN_GROUP: 'btn-group', +} + +export const ValueType = { + INT: 'int', + FLOAT: 'float', + BOOLEAN: 'boolean', + STRING: 'string', + JSON: 'JSON', +} + +export const TableColumnType = { + STRING: 'string', + BOOLEAN: 'boolean', + NUMBER: 'number', + DATE: 'date', + OBJECT: 'object', + NUMBER_STR: 'numberStr', +} + +export const DefaultTableColumnType = TableColumnType.STRING + +export function isInputWidget (spec) { return spec.widget === Widget.INPUT } +export function isOptionWidget (spec) { return spec.widget === Widget.OPTION } +export function isCheckboxWidget (spec) { return spec.widget === Widget.CHECKBOX } +export function isTextareaWidget (spec) { return spec.widget === Widget.TEXTAREA } +export function isBtnGroupWidget (spec) { return spec.widget === Widget.BTN_GROUP } + +export function resetTableOptionConfig(config) { + delete config.tableOptionSpecHash + config.tableOptionSpecHash = {} + delete config.tableOptionValue + config.tableOptionValue = {} + delete config.tableColumnTypeState.names + config.tableColumnTypeState.names = {} + config.updated = false + return config +} + +export function initializeTableConfig(config, tableOptionSpecs) { + if (typeof config.tableOptionValue === 'undefined') { config.tableOptionValue = {} } + if (typeof config.tableGridState === 'undefined') { config.tableGridState = {} } + if (typeof config.tableColumnTypeState === 'undefined') { config.tableColumnTypeState = {} } + + // should remove `$$hashKey` using angular.toJson + const newSpecHash = JSON.stringify(JSON.parse(angular.toJson(tableOptionSpecs))) + const previousSpecHash = config.tableOptionSpecHash + + // check whether spec is updated or not + if (typeof previousSpecHash === 'undefined' || (previousSpecHash !== newSpecHash)) { + resetTableOptionConfig(config) + + config.tableOptionSpecHash = newSpecHash + config.initialized = true + + // reset all persisted option values if spec is updated + for (let i = 0; i < tableOptionSpecs.length; i++) { + const option = tableOptionSpecs[i] + config.tableOptionValue[option.name] = option.defaultValue + } + } + + return config +} + +export function parseTableOption(specs, persistedTableOption) { + /** copy original params */ + const parsed = JSON.parse(JSON.stringify(persistedTableOption)) + + for (let i = 0; i < specs.length; i++) { + const s = specs[i] + const name = s.name + + if (s.valueType === ValueType.INT && + typeof parsed[name] !== 'number') { + try { parsed[name] = parseInt(parsed[name]) } catch (error) { parsed[name] = s.defaultValue } + } else if (s.valueType === ValueType.FLOAT && + typeof parsed[name] !== 'number') { + try { parsed[name] = parseFloat(parsed[name]) } catch (error) { parsed[name] = s.defaultValue } + } else if (s.valueType === ValueType.BOOLEAN) { + if (parsed[name] === 'false') { + parsed[name] = false + } else if (parsed[name] === 'true') { + parsed[name] = true + } else if (typeof parsed[name] !== 'boolean') { + parsed[name] = s.defaultValue + } + } else if (s.valueType === ValueType.JSON) { + if (parsed[name] !== null && typeof parsed[name] !== 'object') { + try { parsed[name] = JSON.parse(parsed[name]) } catch (error) { parsed[name] = s.defaultValue } + } else if (parsed[name] === null) { + parsed[name] = s.defaultValue + } + } + } + + return parsed +} + +export function isColumnNameUpdated(prevColumnNames, newColumnNames) { + if (typeof prevColumnNames === 'undefined') { return true } + + let columnNameUpdated = false + + for (let prevColName in prevColumnNames) { + if (!newColumnNames[prevColName]) { + return true + } + } + + if (!columnNameUpdated) { + for (let newColName in newColumnNames) { + if (!prevColumnNames[newColName]) { + return true + } + } + } + + return false +} + +export function updateColumnTypeState(columns, config, columnDefs) { + const columnTypeState = config.tableColumnTypeState + + if (!columnTypeState) { return } + + // compare objects because order might be changed + const prevColumnNames = columnTypeState.names || {} + const newColumnNames = columns.reduce((acc, c) => { + const prevColumnType = prevColumnNames[c.name] + + // use previous column type if exists + if (prevColumnType) { + acc[c.name] = prevColumnType + } else { + acc[c.name] = DefaultTableColumnType + } + return acc + }, {}) + + let columnNameUpdated = isColumnNameUpdated(prevColumnNames, newColumnNames) + + if (columnNameUpdated) { + columnTypeState.names = newColumnNames + columnTypeState.updated = true + } + + // update `columnDefs[n].type` + for (let i = 0; i < columnDefs.length; i++) { + const colName = columnDefs[i].name + columnDefs[i].type = columnTypeState.names[colName] + } +} http://git-wip-us.apache.org/repos/asf/zeppelin/blob/bed82eb4/zeppelin-web/src/components/editor/codeEditor.directive.js ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/components/editor/codeEditor.directive.js b/zeppelin-web/src/components/editor/codeEditor.directive.js index b8e1b6a..d8cb73f 100644 --- a/zeppelin-web/src/components/editor/codeEditor.directive.js +++ b/zeppelin-web/src/components/editor/codeEditor.directive.js @@ -31,7 +31,7 @@ function codeEditor ($templateRequest, $compile) { editor.attr('id', scope.paragraphId + '_editor') element.append(editor) $compile(editor)(scope) - console.log('codeEditor directive revision view is ' + scope.revisionView) + console.debug('codeEditor directive revision view is ' + scope.revisionView) }) } } http://git-wip-us.apache.org/repos/asf/zeppelin/blob/bed82eb4/zeppelin-web/src/components/websocketEvents/websocketEvents.factory.js ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/components/websocketEvents/websocketEvents.factory.js b/zeppelin-web/src/components/websocketEvents/websocketEvents.factory.js index 08eb16b..16b3f7a 100644 --- a/zeppelin-web/src/components/websocketEvents/websocketEvents.factory.js +++ b/zeppelin-web/src/components/websocketEvents/websocketEvents.factory.js @@ -54,8 +54,9 @@ function websocketEvents ($rootScope, $websocket, $location, baseUrlSrv) { if (event.data) { payload = angular.fromJson(event.data) } - console.log('Receive Json << %o', event.data) + console.log('Receive << %o, %o', payload.op, payload) + let op = payload.op let data = payload.data if (op === 'NOTE') { http://git-wip-us.apache.org/repos/asf/zeppelin/blob/bed82eb4/zeppelin-web/src/index.html ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/index.html b/zeppelin-web/src/index.html index fc7a21f..7d48584 100644 --- a/zeppelin-web/src/index.html +++ b/zeppelin-web/src/index.html @@ -44,8 +44,6 @@ limitations under the License. <link rel="stylesheet" href="bower_components/highlightjs/styles/github.css" /> <link rel="stylesheet" href="bower_components/ngtoast/dist/ngToast.css" /> <link rel="stylesheet" href="bower_components/bootstrap3-dialog/dist/css/bootstrap-dialog.min.css" /> - <link rel="stylesheet" href="bower_components/pikaday/css/pikaday.css" /> - <link rel="stylesheet" href="bower_components/handsontable/dist/handsontable.css" /> <!-- endbower --> <link rel="stylesheet" href="bower_components/jquery-ui/themes/base/jquery-ui.css" /> <link rel="stylesheet" href="bower_components/select2/dist/css/select2.css" /> @@ -57,6 +55,7 @@ limitations under the License. <link rel="stylesheet" href="app/notebook/notebook.css" /> <link rel="stylesheet" href="app/notebook/paragraph/paragraph.css" /> <link rel="stylesheet" href="app/notebook/paragraph/result/result.css" /> + <link rel="stylesheet" href="app/notebook/paragraph/result/display-table.css" /> <link rel="stylesheet" href="app/jobmanager/jobmanager.css" /> <link rel="stylesheet" href="app/jobmanager/jobs/job.css" /> <link rel="stylesheet" href="app/interpreter/interpreter.css" /> @@ -163,11 +162,6 @@ limitations under the License. <script src="bower_components/ngtoast/dist/ngToast.js"></script> <script src="bower_components/ng-focus-if/focusIf.js"></script> <script src="bower_components/bootstrap3-dialog/dist/js/bootstrap-dialog.min.js"></script> - <script src="bower_components/zeroclipboard/dist/ZeroClipboard.js"></script> - <script src="bower_components/moment/moment.js"></script> - <script src="bower_components/pikaday/pikaday.js"></script> - <script src="bower_components/handsontable/dist/handsontable.js"></script> - <script src="bower_components/moment-duration-format/lib/moment-duration-format.js"></script> <script src="bower_components/select2/dist/js/select2.js"></script> <script src="bower_components/MathJax/MathJax.js"></script> <script src="bower_components/clipboard/dist/clipboard.js"></script> http://git-wip-us.apache.org/repos/asf/zeppelin/blob/bed82eb4/zeppelin-web/src/index.js ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/index.js b/zeppelin-web/src/index.js index fc2d65b..d94714a 100644 --- a/zeppelin-web/src/index.js +++ b/zeppelin-web/src/index.js @@ -18,7 +18,6 @@ import 'github-markdown-css/github-markdown.css' import './app/app.js' import './app/app.controller.js' import './app/home/home.controller.js' -import './app/handsontable/handsonHelper.js' import './app/notebook/notebook.controller.js' import './app/tabledata/tabledata.js' http://git-wip-us.apache.org/repos/asf/zeppelin/blob/bed82eb4/zeppelin-web/webpack.config.js ---------------------------------------------------------------------- diff --git a/zeppelin-web/webpack.config.js b/zeppelin-web/webpack.config.js index 201649c..88c7acc 100644 --- a/zeppelin-web/webpack.config.js +++ b/zeppelin-web/webpack.config.js @@ -230,7 +230,6 @@ module.exports = function makeWebpackConfig () { template: './src/index.html', inject: 'body' }), - // Reference: https://webpack.github.io/docs/list-of-plugins.html#defineplugin new webpack.DefinePlugin({ 'process.env': {
