http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/8c7424a1/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 new file mode 100644 index 0000000..21e022a --- /dev/null +++ b/zeppelin-web/src/app/notebook/paragraph/paragraph.controller.js @@ -0,0 +1,1594 @@ +/* global $:false, jQuery:false, ace:false, confirm:false, d3:false, nv:false*/ +/*jshint loopfunc: true, unused:false */ +/* + * 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. + */ +'use strict'; + +angular.module('zeppelinWebApp') + .controller('ParagraphCtrl', function($scope,$rootScope, $route, $window, $element, $routeParams, $location, + $timeout, $compile, websocketMsgSrv) { + + $scope.paragraph = null; + $scope.editor = null; + var editorMode = {scala: 'ace/mode/scala', sql: 'ace/mode/sql', markdown: 'ace/mode/markdown'}; + + // Controller init + $scope.init = function(newParagraph) { + $scope.paragraph = newParagraph; + $scope.chart = {}; + $scope.colWidthOption = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ]; + $scope.showTitleEditor = false; + $scope.paragraphFocused = false; + + if (!$scope.paragraph.config) { + $scope.paragraph.config = {}; + } + + initializeDefault(); + + if (!$scope.lastData) { + $scope.lastData = {}; + } + + if ($scope.getResultType() === 'TABLE') { + $scope.lastData.settings = angular.copy($scope.paragraph.settings); + $scope.lastData.config = angular.copy($scope.paragraph.config); + $scope.loadTableData($scope.paragraph.result); + $scope.setGraphMode($scope.getGraphMode(), false, false); + } else if ($scope.getResultType() === 'HTML') { + $scope.renderHtml(); + } else if ($scope.getResultType() === 'ANGULAR') { + $scope.renderAngular(); + } + }; + + $scope.renderHtml = function() { + var retryRenderer = function() { + if ($('#p'+$scope.paragraph.id+'_html').length) { + try { + $('#p'+$scope.paragraph.id+'_html').html($scope.paragraph.result.msg); + + $('#p'+$scope.paragraph.id+'_html').find('pre code').each(function(i, e) { hljs.highlightBlock(e); }); + } catch(err) { + console.log('HTML rendering error %o', err); + } + } else { + $timeout(retryRenderer,10); + } + }; + $timeout(retryRenderer); + + }; + + $scope.renderAngular = function() { + var retryRenderer = function() { + if (angular.element('#p'+$scope.paragraph.id+'_angular').length) { + try { + angular.element('#p'+$scope.paragraph.id+'_angular').html($scope.paragraph.result.msg); + + $compile(angular.element('#p'+$scope.paragraph.id+'_angular').contents())($rootScope.compiledScope); + } catch(err) { + console.log('ANGULAR rendering error %o', err); + } + } else { + $timeout(retryRenderer,10); + } + }; + $timeout(retryRenderer); + + }; + + + var initializeDefault = function() { + var config = $scope.paragraph.config; + + if (!config.colWidth) { + config.colWidth = 12; + } + + if (!config.graph) { + config.graph = {}; + } + + if (!config.graph.mode) { + config.graph.mode = 'table'; + } + + if (!config.graph.height) { + config.graph.height = 300; + } + + if (!config.graph.optionOpen) { + config.graph.optionOpen = false; + } + + if (!config.graph.keys) { + config.graph.keys = []; + } + + if (!config.graph.values) { + config.graph.values = []; + } + + if (!config.graph.groups) { + config.graph.groups = []; + } + + if (!config.graph.scatter) { + config.graph.scatter = {}; + } + }; + + $scope.getIframeDimensions = function () { + if ($scope.asIframe) { + var paragraphid = '#' + $routeParams.paragraphId + '_container'; + var height = $(paragraphid).height(); + return height; + } + return 0; + }; + + $scope.$watch($scope.getIframeDimensions, function (newValue, oldValue) { + if ($scope.asIframe && newValue) { + var message = {}; + message.height = newValue; + message.url = $location.$$absUrl; + $window.parent.postMessage(angular.toJson(message), '*'); + } + }); + + // TODO: this may have impact on performance when there are many paragraphs in a note. + $scope.$on('updateParagraph', function(event, data) { + if (data.paragraph.id === $scope.paragraph.id && + ( + data.paragraph.dateCreated !== $scope.paragraph.dateCreated || + data.paragraph.dateFinished !== $scope.paragraph.dateFinished || + data.paragraph.dateStarted !== $scope.paragraph.dateStarted || + data.paragraph.status !== $scope.paragraph.status || + data.paragraph.jobName !== $scope.paragraph.jobName || + data.paragraph.title !== $scope.paragraph.title || + data.paragraph.errorMessage !== $scope.paragraph.errorMessage || + !angular.equals(data.paragraph.settings, $scope.lastData.settings) || + !angular.equals(data.paragraph.config, $scope.lastData.config) + ) + ) { + // store original data for comparison + $scope.lastData.settings = angular.copy(data.paragraph.settings); + $scope.lastData.config = angular.copy(data.paragraph.config); + + var oldType = $scope.getResultType(); + var newType = $scope.getResultType(data.paragraph); + var oldGraphMode = $scope.getGraphMode(); + var newGraphMode = $scope.getGraphMode(data.paragraph); + var resultRefreshed = (data.paragraph.dateFinished !== $scope.paragraph.dateFinished); + + //console.log("updateParagraph oldData %o, newData %o. type %o -> %o, mode %o -> %o", $scope.paragraph, data, oldType, newType, oldGraphMode, newGraphMode); + + if ($scope.paragraph.text !== data.paragraph.text) { + if ($scope.dirtyText) { // check if editor has local update + if ($scope.dirtyText === data.paragraph.text ) { // when local update is the same from remote, clear local update + $scope.paragraph.text = data.paragraph.text; + $scope.dirtyText = undefined; + } else { // if there're local update, keep it. + $scope.paragraph.text = $scope.dirtyText; + } + } else { + $scope.paragraph.text = data.paragraph.text; + } + } + + /** push the rest */ + $scope.paragraph.aborted = data.paragraph.aborted; + $scope.paragraph.dateCreated = data.paragraph.dateCreated; + $scope.paragraph.dateFinished = data.paragraph.dateFinished; + $scope.paragraph.dateStarted = data.paragraph.dateStarted; + $scope.paragraph.errorMessage = data.paragraph.errorMessage; + $scope.paragraph.jobName = data.paragraph.jobName; + $scope.paragraph.title = data.paragraph.title; + $scope.paragraph.status = data.paragraph.status; + $scope.paragraph.result = data.paragraph.result; + $scope.paragraph.settings = data.paragraph.settings; + + if (!$scope.asIframe) { + $scope.paragraph.config = data.paragraph.config; + initializeDefault(); + } else { + data.paragraph.config.editorHide = true; + data.paragraph.config.tableHide = false; + $scope.paragraph.config = data.paragraph.config; + } + + if (newType === 'TABLE') { + $scope.loadTableData($scope.paragraph.result); + if (oldType !== 'TABLE' || resultRefreshed) { + clearUnknownColsFromGraphOption(); + selectDefaultColsForGraphOption(); + } + /** User changed the chart type? */ + if (oldGraphMode !== newGraphMode) { + $scope.setGraphMode(newGraphMode, false, false); + } else { + $scope.setGraphMode(newGraphMode, false, true); + } + } else if (newType === 'HTML') { + $scope.renderHtml(); + } else if (newType === 'ANGULAR') { + $scope.renderAngular(); + } + } + }); + + $scope.isRunning = function() { + if ($scope.paragraph.status === 'RUNNING' || $scope.paragraph.status === 'PENDING') { + return true; + } else { + return false; + } + }; + + $scope.cancelParagraph = function() { + console.log('Cancel %o', $scope.paragraph.id); + websocketMsgSrv.cancelParagraphRun($scope.paragraph.id); + }; + + + $scope.runParagraph = function(data) { + websocketMsgSrv.runParagraph($scope.paragraph.id, $scope.paragraph.title, + data, $scope.paragraph.config, $scope.paragraph.settings.params); + $scope.dirtyText = undefined; + }; + + $scope.moveUp = function() { + $scope.$emit('moveParagraphUp', $scope.paragraph.id); + }; + + $scope.moveDown = function() { + $scope.$emit('moveParagraphDown', $scope.paragraph.id); + }; + + $scope.insertNew = function() { + $scope.$emit('insertParagraph', $scope.paragraph.id); + }; + + $scope.removeParagraph = function() { + var result = confirm('Do you want to delete this paragraph?'); + if (result) { + console.log('Remove paragraph'); + websocketMsgSrv.removeParagraph($scope.paragraph.id); + } + }; + + $scope.toggleEditor = function() { + if ($scope.paragraph.config.editorHide) { + $scope.openEditor(); + } else { + $scope.closeEditor(); + } + }; + + $scope.closeEditor = function() { + console.log('close the note'); + + var newParams = angular.copy($scope.paragraph.settings.params); + var newConfig = angular.copy($scope.paragraph.config); + newConfig.editorHide = true; + + commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams); + }; + + $scope.openEditor = function() { + console.log('open the note'); + + var newParams = angular.copy($scope.paragraph.settings.params); + var newConfig = angular.copy($scope.paragraph.config); + newConfig.editorHide = false; + + commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams); + }; + + $scope.closeTable = function() { + console.log('close the output'); + + var newParams = angular.copy($scope.paragraph.settings.params); + var newConfig = angular.copy($scope.paragraph.config); + newConfig.tableHide = true; + + commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams); + }; + + $scope.openTable = function() { + console.log('open the output'); + + var newParams = angular.copy($scope.paragraph.settings.params); + var newConfig = angular.copy($scope.paragraph.config); + newConfig.tableHide = false; + + commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams); + }; + + $scope.showTitle = function() { + var newParams = angular.copy($scope.paragraph.settings.params); + var newConfig = angular.copy($scope.paragraph.config); + newConfig.title = true; + + commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams); + }; + + $scope.hideTitle = function() { + var newParams = angular.copy($scope.paragraph.settings.params); + var newConfig = angular.copy($scope.paragraph.config); + newConfig.title = false; + + commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams); + }; + + $scope.setTitle = function() { + var newParams = angular.copy($scope.paragraph.settings.params); + var newConfig = angular.copy($scope.paragraph.config); + commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams); + }; + + $scope.columnWidthClass = function(n) { + if ($scope.asIframe) { + return 'col-md-12'; + } else { + return 'col-md-' + n; + } + }; + + $scope.changeColWidth = function() { + + var newParams = angular.copy($scope.paragraph.settings.params); + var newConfig = angular.copy($scope.paragraph.config); + + commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams); + }; + + $scope.toggleGraphOption = function() { + var newConfig = angular.copy($scope.paragraph.config); + if (newConfig.graph.optionOpen) { + newConfig.graph.optionOpen = false; + } else { + newConfig.graph.optionOpen = true; + } + var newParams = angular.copy($scope.paragraph.settings.params); + + commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams); + }; + + $scope.toggleOutput = function() { + var newConfig = angular.copy($scope.paragraph.config); + newConfig.tableHide = !newConfig.tableHide; + var newParams = angular.copy($scope.paragraph.settings.params); + + commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams); + }; + + + $scope.loadForm = function(formulaire, params) { + var value = formulaire.defaultValue; + if (params[formulaire.name]) { + value = params[formulaire.name]; + } + + if (value === '') { + value = formulaire.options[0].value; + } + + $scope.paragraph.settings.params[formulaire.name] = value; + }; + + $scope.aceChanged = function() { + $scope.dirtyText = $scope.editor.getSession().getValue(); + }; + + $scope.aceLoaded = function(_editor) { + var langTools = ace.require('ace/ext/language_tools'); + var Range = ace.require('ace/range').Range; + + $scope.editor = _editor; + if (_editor.container.id !== '{{paragraph.id}}_editor') { + $scope.editor.renderer.setShowGutter(false); + $scope.editor.setHighlightActiveLine(false); + $scope.editor.setTheme('ace/theme/github'); + $scope.editor.focus(); + var hight = $scope.editor.getSession().getScreenLength() * $scope.editor.renderer.lineHeight + $scope.editor.renderer.scrollBar.getWidth(); + setEditorHeight(_editor.container.id, hight); + + $scope.editor.getSession().setUseWrapMode(true); + if (navigator.appVersion.indexOf('Mac') !== -1 ) { + $scope.editor.setKeyboardHandler('ace/keyboard/emacs'); + } else if (navigator.appVersion.indexOf('Win') !== -1 || + navigator.appVersion.indexOf('X11') !== -1 || + navigator.appVersion.indexOf('Linux') !== -1) { + // not applying emacs key binding while the binding override Ctrl-v. default behavior of paste text on windows. + } + + $scope.editor.setOptions({ + enableBasicAutocompletion: true, + enableSnippets: false, + enableLiveAutocompletion:false + }); + var remoteCompleter = { + getCompletions : function(editor, session, pos, prefix, callback) { + if (!$scope.editor.isFocused() ){ return;} + + var pos = session.getTextRange(new Range(0, 0, pos.row, pos.column)).length; + var buf = session.getValue(); + $rootScope.$emit('sendNewEvent', { + op : 'COMPLETION', + data : { + id : $scope.paragraph.id, + buf : buf, + cursor : pos + } + }); + + $scope.$on('completionList', function(event, data) { + if (data.completions) { + var completions = []; + for (var c in data.completions) { + var v = data.completions[c]; + completions.push({ + name:v, + value:v, + score:300 + }); + } + callback(null, completions); + } + }); + } + }; + langTools.addCompleter(remoteCompleter); + + + $scope.handleFocus = function(value) { + $scope.paragraphFocused = value; + // Protect against error in case digest is already running + $timeout(function() { + // Apply changes since they come from 3rd party library + $scope.$digest(); + }); + }; + + $scope.editor.on('focus', function() { + $scope.handleFocus(true); + }); + + $scope.editor.on('blur', function() { + $scope.handleFocus(false); + }); + + + $scope.editor.getSession().on('change', function(e, editSession) { + hight = editSession.getScreenLength() * $scope.editor.renderer.lineHeight + $scope.editor.renderer.scrollBar.getWidth(); + setEditorHeight(_editor.container.id, hight); + $scope.editor.resize(); + }); + + var code = $scope.editor.getSession().getValue(); + if ( String(code).startsWith('%sql')) { + $scope.editor.getSession().setMode(editorMode.sql); + } else if ( String(code).startsWith('%md')) { + $scope.editor.getSession().setMode(editorMode.markdown); + } else { + $scope.editor.getSession().setMode(editorMode.scala); + } + + $scope.editor.commands.addCommand({ + name: 'run', + bindKey: {win: 'Shift-Enter', mac: 'Shift-Enter'}, + exec: function(editor) { + var editorValue = editor.getValue(); + if (editorValue) { + $scope.runParagraph(editorValue); + } + }, + readOnly: false + }); + + // autocomplete on '.' + /* + $scope.editor.commands.on("afterExec", function(e, t) { + if (e.command.name == "insertstring" && e.args == "." ) { + var all = e.editor.completers; + //e.editor.completers = [remoteCompleter]; + e.editor.execCommand("startAutocomplete"); + //e.editor.completers = all; + } + }); + */ + + // autocomplete on 'ctrl+.' + $scope.editor.commands.bindKey('ctrl-.', 'startAutocomplete'); + $scope.editor.commands.bindKey('ctrl-space', null); + + // handle cursor moves + $scope.editor.keyBinding.origOnCommandKey = $scope.editor.keyBinding.onCommandKey; + $scope.editor.keyBinding.onCommandKey = function(e, hashId, keyCode) { + if ($scope.editor.completer && $scope.editor.completer.activated) { // if autocompleter is active + } else { + var numRows; + var currentRow; + if (keyCode === 38 || (keyCode === 80 && e.ctrlKey)) { // UP + numRows = $scope.editor.getSession().getLength(); + currentRow = $scope.editor.getCursorPosition().row; + if (currentRow === 0) { + // move focus to previous paragraph + $scope.$emit('moveFocusToPreviousParagraph', $scope.paragraph.id); + } + } else if (keyCode === 40 || (keyCode === 78 && e.ctrlKey)) { // DOWN + numRows = $scope.editor.getSession().getLength(); + currentRow = $scope.editor.getCursorPosition().row; + if (currentRow === numRows-1) { + // move focus to next paragraph + $scope.$emit('moveFocusToNextParagraph', $scope.paragraph.id); + } + } + } + this.origOnCommandKey(e, hashId, keyCode); + }; + } + }; + + var setEditorHeight = function(id, height) { + $('#' + id).height(height.toString() + 'px'); + }; + + $scope.getEditorValue = function() { + return $scope.editor.getValue(); + }; + + $scope.getProgress = function() { + return ($scope.currentProgress) ? $scope.currentProgress : 0; + }; + + $scope.getExecutionTime = function() { + var pdata = $scope.paragraph; + var timeMs = Date.parse(pdata.dateFinished) - Date.parse(pdata.dateStarted); + if (isNaN(timeMs) || timeMs < 0) { + return ' '; + } + return 'Took ' + (timeMs/1000) + ' seconds'; + }; + + $scope.$on('updateProgress', function(event, data) { + if (data.id === $scope.paragraph.id) { + $scope.currentProgress = data.progress; + } + }); + + $scope.$on('focusParagraph', function(event, paragraphId) { + if ($scope.paragraph.id === paragraphId) { + $scope.editor.focus(); + $('body').scrollTo('#'+paragraphId+'_editor', 300, {offset:-60}); + } + }); + + $scope.$on('runParagraph', function(event) { + $scope.runParagraph($scope.editor.getValue()); + }); + + $scope.$on('openEditor', function(event) { + $scope.openEditor(); + }); + + $scope.$on('closeEditor', function(event) { + $scope.closeEditor(); + }); + + $scope.$on('openTable', function(event) { + $scope.openTable(); + }); + + $scope.$on('closeTable', function(event) { + $scope.closeTable(); + }); + + + $scope.getResultType = function(paragraph) { + var pdata = (paragraph) ? paragraph : $scope.paragraph; + if (pdata.result && pdata.result.type) { + return pdata.result.type; + } else { + return 'TEXT'; + } + }; + + $scope.getBase64ImageSrc = function(base64Data) { + return 'data:image/png;base64,'+base64Data; + }; + + $scope.getGraphMode = function(paragraph) { + var pdata = (paragraph) ? paragraph : $scope.paragraph; + if (pdata.config.graph && pdata.config.graph.mode) { + return pdata.config.graph.mode; + } else { + return 'table'; + } + }; + + $scope.loadTableData = function(result) { + if (!result) { + return; + } + if (result.type === 'TABLE') { + var columnNames = []; + var rows = []; + var array = []; + var textRows = result.msg.split('\n'); + result.comment = ''; + var comment = false; + + for (var i = 0; i < textRows.length; i++) { + var textRow = textRows[i]; + if (comment) { + result.comment += textRow; + continue; + } + + if (textRow === '') { + if (rows.length>0) { + comment = true; + } + continue; + } + var textCols = textRow.split('\t'); + var cols = []; + var cols2 = []; + for (var j = 0; j < textCols.length; j++) { + var col = textCols[j]; + if (i === 0) { + columnNames.push({name:col, index:j, aggr:'sum'}); + } else { + cols.push(col); + cols2.push({key: (columnNames[i]) ? columnNames[i].name: undefined, value: col}); + } + } + if (i !== 0) { + rows.push(cols); + array.push(cols2); + } + } + result.msgTable = array; + result.columnNames = columnNames; + result.rows = rows; + } + }; + + $scope.setGraphMode = function(type, emit, refresh) { + if (emit) { + setNewMode(type); + } else { + clearUnknownColsFromGraphOption(); + // set graph height + var height = $scope.paragraph.config.graph.height; + $('#p'+$scope.paragraph.id+'_graph').height(height); + + if (!type || type === 'table') { + setTable($scope.paragraph.result, refresh); + } + else { + setD3Chart(type, $scope.paragraph.result, refresh); + } + } + }; + + var setNewMode = function(newMode) { + var newConfig = angular.copy($scope.paragraph.config); + var newParams = angular.copy($scope.paragraph.settings.params); + + // graph options + newConfig.graph.mode = newMode; + + commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams); + }; + + var commitParagraph = function(title, text, config, params) { + websocketMsgSrv.commitParagraph($scope.paragraph.id, title, text, config, params); + }; + + var setTable = function(type, data, refresh) { + var getTableContentFormat = function(d) { + if (isNaN(d)) { + if (d.length>'%html'.length && '%html ' === d.substring(0, '%html '.length)) { + return 'html'; + } else { + return ''; + } + } else { + return ''; + } + }; + + var formatTableContent = function(d) { + if (isNaN(d)) { + var f = getTableContentFormat(d); + if (f !== '') { + return d.substring(f.length+2); + } else { + return d; + } + } else { + var dStr = d.toString(); + var splitted = dStr.split('.'); + var formatted = splitted[0].replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,'); + if (splitted.length>1) { + formatted+= '.'+splitted[1]; + } + return formatted; + } + }; + + + var renderTable = function() { + var html = ''; + html += '<table class="table table-hover table-condensed">'; + html += ' <thead>'; + html += ' <tr style="background-color: #F6F6F6; font-weight: bold;">'; + for (var c in $scope.paragraph.result.columnNames) { + html += '<th>'+$scope.paragraph.result.columnNames[c].name+'</th>'; + } + html += ' </tr>'; + html += ' </thead>'; + + for (var r in $scope.paragraph.result.msgTable) { + var row = $scope.paragraph.result.msgTable[r]; + html += ' <tr>'; + for (var index in row) { + var v = row[index].value; + if (getTableContentFormat(v) !== 'html') { + v = v.replace(/[\u00A0-\u9999<>\&]/gim, function(i) { + return '&#'+i.charCodeAt(0)+';'; + }); + } + html += ' <td>'+formatTableContent(v)+'</td>'; + } + html += ' </tr>'; + } + + html += '</table>'; + + $('#p' + $scope.paragraph.id + '_table').html(html); + $('#p' + $scope.paragraph.id + '_table').perfectScrollbar(); + + // set table height + var height = $scope.paragraph.config.graph.height; + $('#p'+$scope.paragraph.id+'_table').height(height); + }; + + var retryRenderer = function() { + if ($('#p'+$scope.paragraph.id+'_table').length) { + try { + renderTable(); + } catch(err) { + console.log('Chart drawing error %o', err); + } + } else { + $timeout(retryRenderer,10); + } + }; + $timeout(retryRenderer); + + }; + + var setD3Chart = function(type, data, refresh) { + if (!$scope.chart[type]) { + var chart = nv.models[type](); + $scope.chart[type] = chart; + } + + var d3g = []; + + if (type === 'scatterChart') { + var scatterData = setScatterChart(data, refresh); + + var xLabels = scatterData.xLabels; + var yLabels = scatterData.yLabels; + d3g = scatterData.d3g; + + $scope.chart[type].xAxis.tickFormat(function(d) { + if (xLabels[d] && (isNaN(parseFloat(xLabels[d])) || !isFinite(xLabels[d]))) { + return xLabels[d]; + } else { + return d; + } + }); + + $scope.chart[type].yAxis.tickFormat(function(d) { + if (yLabels[d] && (isNaN(parseFloat(yLabels[d])) || !isFinite(yLabels[d]))) { + return yLabels[d]; + } else { + return d; + } + }); + + // configure how the tooltip looks. + $scope.chart[type].tooltipContent(function(key, x, y, data) { + var tooltipContent = '<h3>' + key + '</h3>'; + if ($scope.paragraph.config.graph.scatter.size && + $scope.isValidSizeOption($scope.paragraph.config.graph.scatter, $scope.paragraph.result.rows)) { + tooltipContent += '<p>' + data.point.size + '</p>'; + } + + return tooltipContent; + }); + + $scope.chart[type].showDistX(true) + .showDistY(true) + //handle the problem of tooltip not showing when muliple points have same value. + .scatter.useVoronoi(false); + } else { + var p = pivot(data); + if (type === 'pieChart') { + var d = pivotDataToD3ChartFormat(p, true).d3g; + + $scope.chart[type].x(function(d) { return d.label;}) + .y(function(d) { return d.value;}); + + if ( d.length > 0 ) { + for ( var i=0; i<d[0].values.length ; i++) { + var e = d[0].values[i]; + d3g.push({ + label : e.x, + value : e.y + }); + } + } + } else if (type === 'multiBarChart') { + d3g = pivotDataToD3ChartFormat(p, true, false, type).d3g; + $scope.chart[type].yAxis.axisLabelDistance(50); + } else if (type === 'lineChart' || type === 'stackedAreaChart') { + var pivotdata = pivotDataToD3ChartFormat(p, false, true); + var xLabels = pivotdata.xLabels; + d3g = pivotdata.d3g; + $scope.chart[type].xAxis.tickFormat(function(d) { + if (xLabels[d] && (isNaN(parseFloat(xLabels[d])) || !isFinite(xLabels[d]))) { // to handle string type xlabel + return xLabels[d]; + } else { + return d; + } + }); + $scope.chart[type].yAxis.axisLabelDistance(50); + $scope.chart[type].useInteractiveGuideline(true); // for better UX and performance issue. (https://github.com/novus/nvd3/issues/691) + $scope.chart[type].forceY([0]); // force y-axis minimum to 0 for line chart. + } + } + + var renderChart = function() { + if (!refresh) { + // TODO force destroy previous chart + } + + var height = $scope.paragraph.config.graph.height; + + var animationDuration = 300; + var numberOfDataThreshold = 150; + // turn off animation when dataset is too large. (for performance issue) + // still, since dataset is large, the chart content sequentially appears like animated. + try { + if (d3g[0].values.length > numberOfDataThreshold) { + animationDuration = 0; + } + } catch(ignoreErr) { + } + + var chartEl = d3.select('#p'+$scope.paragraph.id+'_'+type+' svg') + .attr('height', $scope.paragraph.config.graph.height) + .datum(d3g) + .transition() + .duration(animationDuration) + .call($scope.chart[type]); + d3.select('#p'+$scope.paragraph.id+'_'+type+' svg').style.height = height+'px'; + nv.utils.windowResize($scope.chart[type].update); + }; + + var retryRenderer = function() { + if ($('#p'+$scope.paragraph.id+'_'+type+' svg').length !== 0) { + try { + renderChart(); + } catch(err) { + console.log('Chart drawing error %o', err); + } + } else { + $timeout(retryRenderer,10); + } + }; + $timeout(retryRenderer); + }; + + $scope.isGraphMode = function(graphName) { + if ($scope.getResultType() === 'TABLE' && $scope.getGraphMode()===graphName) { + return true; + } else { + return false; + } + }; + + + $scope.onGraphOptionChange = function() { + clearUnknownColsFromGraphOption(); + $scope.setGraphMode($scope.paragraph.config.graph.mode, true, false); + }; + + $scope.removeGraphOptionKeys = function(idx) { + $scope.paragraph.config.graph.keys.splice(idx, 1); + clearUnknownColsFromGraphOption(); + $scope.setGraphMode($scope.paragraph.config.graph.mode, true, false); + }; + + $scope.removeGraphOptionValues = function(idx) { + $scope.paragraph.config.graph.values.splice(idx, 1); + clearUnknownColsFromGraphOption(); + $scope.setGraphMode($scope.paragraph.config.graph.mode, true, false); + }; + + $scope.removeGraphOptionGroups = function(idx) { + $scope.paragraph.config.graph.groups.splice(idx, 1); + clearUnknownColsFromGraphOption(); + $scope.setGraphMode($scope.paragraph.config.graph.mode, true, false); + }; + + $scope.setGraphOptionValueAggr = function(idx, aggr) { + $scope.paragraph.config.graph.values[idx].aggr = aggr; + clearUnknownColsFromGraphOption(); + $scope.setGraphMode($scope.paragraph.config.graph.mode, true, false); + }; + + $scope.removeScatterOptionXaxis = function(idx) { + $scope.paragraph.config.graph.scatter.xAxis = null; + clearUnknownColsFromGraphOption(); + $scope.setGraphMode($scope.paragraph.config.graph.mode, true, false); + }; + + $scope.removeScatterOptionYaxis = function(idx) { + $scope.paragraph.config.graph.scatter.yAxis = null; + clearUnknownColsFromGraphOption(); + $scope.setGraphMode($scope.paragraph.config.graph.mode, true, false); + }; + + $scope.removeScatterOptionGroup = function(idx) { + $scope.paragraph.config.graph.scatter.group = null; + clearUnknownColsFromGraphOption(); + $scope.setGraphMode($scope.paragraph.config.graph.mode, true, false); + }; + + $scope.removeScatterOptionSize = function(idx) { + $scope.paragraph.config.graph.scatter.size = null; + clearUnknownColsFromGraphOption(); + $scope.setGraphMode($scope.paragraph.config.graph.mode, true, false); + }; + + /* Clear unknown columns from graph option */ + var clearUnknownColsFromGraphOption = function() { + var unique = function(list) { + for (var i = 0; i<list.length; i++) { + for (var j=i+1; j<list.length; j++) { + if (angular.equals(list[i], list[j])) { + list.splice(j, 1); + } + } + } + }; + + var removeUnknown = function(list) { + for (var i = 0; i<list.length; i++) { + // remove non existing column + var found = false; + for (var j=0; j<$scope.paragraph.result.columnNames.length; j++) { + var a = list[i]; + var b = $scope.paragraph.result.columnNames[j]; + if (a.index === b.index && a.name === b.name) { + found = true; + break; + } + } + if (!found) { + list.splice(i, 1); + } + } + }; + + var removeUnknownFromScatterSetting = function(fields) { + for (var f in fields) { + if (fields[f]) { + var found = false; + for (var i = 0; i < $scope.paragraph.result.columnNames.length; i++) { + var a = fields[f]; + var b = $scope.paragraph.result.columnNames[i]; + if (a.index === b.index && a.name === b.name) { + found = true; + break; + } + } + if (!found) { + fields[f] = null; + } + } + } + }; + + unique($scope.paragraph.config.graph.keys); + removeUnknown($scope.paragraph.config.graph.keys); + + removeUnknown($scope.paragraph.config.graph.values); + + unique($scope.paragraph.config.graph.groups); + removeUnknown($scope.paragraph.config.graph.groups); + + removeUnknownFromScatterSetting($scope.paragraph.config.graph.scatter); + }; + + /* select default key and value if there're none selected */ + var selectDefaultColsForGraphOption = function() { + if ($scope.paragraph.config.graph.keys.length === 0 && $scope.paragraph.result.columnNames.length > 0) { + $scope.paragraph.config.graph.keys.push($scope.paragraph.result.columnNames[0]); + } + + if ($scope.paragraph.config.graph.values.length === 0 && $scope.paragraph.result.columnNames.length > 1) { + $scope.paragraph.config.graph.values.push($scope.paragraph.result.columnNames[1]); + } + + if (!$scope.paragraph.config.graph.scatter.xAxis && !$scope.paragraph.config.graph.scatter.yAxis) { + if ($scope.paragraph.result.columnNames.length > 1) { + $scope.paragraph.config.graph.scatter.xAxis = $scope.paragraph.result.columnNames[0]; + $scope.paragraph.config.graph.scatter.yAxis = $scope.paragraph.result.columnNames[1]; + } else if ($scope.paragraph.result.columnNames.length === 1) { + $scope.paragraph.config.graph.scatter.xAxis = $scope.paragraph.result.columnNames[0]; + } + } + }; + + var pivot = function(data) { + var keys = $scope.paragraph.config.graph.keys; + var groups = $scope.paragraph.config.graph.groups; + var values = $scope.paragraph.config.graph.values; + + var aggrFunc = { + sum : function(a,b) { + var varA = (a !== undefined) ? (isNaN(a) ? 1 : parseFloat(a)) : 0; + var varB = (b !== undefined) ? (isNaN(b) ? 1 : parseFloat(b)) : 0; + return varA+varB; + }, + count : function(a,b) { + var varA = (a !== undefined) ? parseInt(a) : 0; + var varB = (b !== undefined) ? 1 : 0; + return varA+varB; + }, + min : function(a,b) { + var varA = (a !== undefined) ? (isNaN(a) ? 1 : parseFloat(a)) : 0; + var varB = (b !== undefined) ? (isNaN(b) ? 1 : parseFloat(b)) : 0; + return Math.min(varA,varB); + }, + max : function(a,b) { + var varA = (a !== undefined) ? (isNaN(a) ? 1 : parseFloat(a)) : 0; + var varB = (b !== undefined) ? (isNaN(b) ? 1 : parseFloat(b)) : 0; + return Math.max(varA,varB); + }, + avg : function(a,b,c) { + var varA = (a !== undefined) ? (isNaN(a) ? 1 : parseFloat(a)) : 0; + var varB = (b !== undefined) ? (isNaN(b) ? 1 : parseFloat(b)) : 0; + return varA+varB; + } + }; + + var aggrFuncDiv = { + sum : false, + count : false, + min : false, + max : false, + avg : true + }; + + var schema = {}; + var rows = {}; + + for (var i=0; i < data.rows.length; i++) { + var row = data.rows[i]; + var newRow = {}; + var s = schema; + var p = rows; + + for (var k=0; k < keys.length; k++) { + var key = keys[k]; + + // add key to schema + if (!s[key.name]) { + s[key.name] = { + order : k, + index : key.index, + type : 'key', + children : {} + }; + } + s = s[key.name].children; + + // add key to row + var keyKey = row[key.index]; + if (!p[keyKey]) { + p[keyKey] = {}; + } + p = p[keyKey]; + } + + for (var g=0; g < groups.length; g++) { + var group = groups[g]; + var groupKey = row[group.index]; + + // add group to schema + if (!s[groupKey]) { + s[groupKey] = { + order : g, + index : group.index, + type : 'group', + children : {} + }; + } + s = s[groupKey].children; + + // add key to row + if (!p[groupKey]) { + p[groupKey] = {}; + } + p = p[groupKey]; + } + + for (var v=0; v < values.length; v++) { + var value = values[v]; + var valueKey = value.name+'('+value.aggr+')'; + + // add value to schema + if (!s[valueKey]) { + s[valueKey] = { + type : 'value', + order : v, + index : value.index + }; + } + + // add value to row + if (!p[valueKey]) { + p[valueKey] = { + value : (value.aggr !== 'count') ? row[value.index] : 1, + count: 1 + }; + } else { + p[valueKey] = { + value : aggrFunc[value.aggr](p[valueKey].value, row[value.index], p[valueKey].count+1), + count : (aggrFuncDiv[value.aggr]) ? p[valueKey].count+1 : p[valueKey].count + }; + } + } + } + + //console.log("schema=%o, rows=%o", schema, rows); + + return { + schema : schema, + rows : rows + }; + }; + + var pivotDataToD3ChartFormat = function(data, allowTextXAxis, fillMissingValues, chartType) { + // construct d3 data + var d3g = []; + + var schema = data.schema; + var rows = data.rows; + var values = $scope.paragraph.config.graph.values; + + var concat = function(o, n) { + if (!o) { + return n; + } else { + return o+'.'+n; + } + }; + + var getSchemaUnderKey = function(key, s) { + for (var c in key.children) { + s[c] = {}; + getSchemaUnderKey(key.children[c], s[c]); + } + }; + + var traverse = function(sKey, s, rKey, r, func, rowName, rowValue, colName) { + //console.log("TRAVERSE sKey=%o, s=%o, rKey=%o, r=%o, rowName=%o, rowValue=%o, colName=%o", sKey, s, rKey, r, rowName, rowValue, colName); + + if (s.type==='key') { + rowName = concat(rowName, sKey); + rowValue = concat(rowValue, rKey); + } else if (s.type==='group') { + colName = concat(colName, rKey); + } else if (s.type==='value' && sKey===rKey || valueOnly) { + colName = concat(colName, rKey); + func(rowName, rowValue, colName, r); + } + + for (var c in s.children) { + if (fillMissingValues && s.children[c].type === 'group' && r[c] === undefined) { + var cs = {}; + getSchemaUnderKey(s.children[c], cs); + traverse(c, s.children[c], c, cs, func, rowName, rowValue, colName); + continue; + } + + for (var j in r) { + if (s.children[c].type === 'key' || c === j) { + traverse(c, s.children[c], j, r[j], func, rowName, rowValue, colName); + } + } + } + }; + + var keys = $scope.paragraph.config.graph.keys; + var groups = $scope.paragraph.config.graph.groups; + var values = $scope.paragraph.config.graph.values; + var valueOnly = (keys.length === 0 && groups.length === 0 && values.length > 0); + var noKey = (keys.length === 0); + var isMultiBarChart = (chartType === 'multiBarChart'); + + var sKey = Object.keys(schema)[0]; + + var rowNameIndex = {}; + var rowIdx = 0; + var colNameIndex = {}; + var colIdx = 0; + var rowIndexValue = {}; + + for (var k in rows) { + traverse(sKey, schema[sKey], k, rows[k], function(rowName, rowValue, colName, value) { + //console.log("RowName=%o, row=%o, col=%o, value=%o", rowName, rowValue, colName, value); + if (rowNameIndex[rowValue] === undefined) { + rowIndexValue[rowIdx] = rowValue; + rowNameIndex[rowValue] = rowIdx++; + } + + if (colNameIndex[colName] === undefined) { + colNameIndex[colName] = colIdx++; + } + var i = colNameIndex[colName]; + if (noKey && isMultiBarChart) { + i = 0; + } + + if (!d3g[i]) { + d3g[i] = { + values : [], + key : (noKey && isMultiBarChart) ? 'values' : colName + }; + } + + var xVar = isNaN(rowValue) ? ((allowTextXAxis) ? rowValue : rowNameIndex[rowValue]) : parseFloat(rowValue); + var yVar = 0; + if (xVar === undefined) { xVar = colName; } + if (value !== undefined) { + yVar = isNaN(value.value) ? 0 : parseFloat(value.value) / parseFloat(value.count); + } + d3g[i].values.push({ + x : xVar, + y : yVar + }); + }); + } + + // clear aggregation name, if possible + var namesWithoutAggr = {}; + // TODO - This part could use som refactoring - Weird if/else with similar actions and variable names + for (var colName in colNameIndex) { + var withoutAggr = colName.substring(0, colName.lastIndexOf('(')); + if (!namesWithoutAggr[withoutAggr]) { + namesWithoutAggr[withoutAggr] = 1; + } else { + namesWithoutAggr[withoutAggr]++; + } + } + + if (valueOnly) { + for (var valueIndex = 0; valueIndex < d3g[0].values.length; valueIndex++) { + var colName = d3g[0].values[valueIndex].x; + if (!colName) { + continue; + } + + var withoutAggr = colName.substring(0, colName.lastIndexOf('(')); + if (namesWithoutAggr[withoutAggr] <= 1 ) { + d3g[0].values[valueIndex].x = withoutAggr; + } + } + } else { + for (var d3gIndex = 0; d3gIndex < d3g.length; d3gIndex++) { + var colName = d3g[d3gIndex].key; + var withoutAggr = colName.substring(0, colName.lastIndexOf('(')); + if (namesWithoutAggr[withoutAggr] <= 1 ) { + d3g[d3gIndex].key = withoutAggr; + } + } + + // use group name instead of group.value as a column name, if there're only one group and one value selected. + if (groups.length === 1 && values.length === 1) { + for (d3gIndex = 0; d3gIndex < d3g.length; d3gIndex++) { + var colName = d3g[d3gIndex].key; + colName = colName.split('.')[0]; + d3g[d3gIndex].key = colName; + } + } + + } + + return { + xLabels : rowIndexValue, + d3g : d3g + }; + }; + + + var setDiscreteScatterData = function(data) { + var xAxis = $scope.paragraph.config.graph.scatter.xAxis; + var yAxis = $scope.paragraph.config.graph.scatter.yAxis; + var group = $scope.paragraph.config.graph.scatter.group; + + var xValue; + var yValue; + var grp; + + var rows = {}; + + for (var i = 0; i < data.rows.length; i++) { + var row = data.rows[i]; + if (xAxis) { + xValue = row[xAxis.index]; + } + if (yAxis) { + yValue = row[yAxis.index]; + } + if (group) { + grp = row[group.index]; + } + + var key = xValue + ',' + yValue + ',' + grp; + + if(!rows[key]) { + rows[key] = { + x : xValue, + y : yValue, + group : grp, + size : 1 + }; + } else { + rows[key].size++; + } + } + + // change object into array + var newRows = []; + for(var r in rows){ + var newRow = []; + if (xAxis) { newRow[xAxis.index] = rows[r].x; } + if (yAxis) { newRow[yAxis.index] = rows[r].y; } + if (group) { newRow[group.index] = rows[r].group; } + newRow[data.rows[0].length] = rows[r].size; + newRows.push(newRow); + } + return newRows; + }; + + var setScatterChart = function(data, refresh) { + var xAxis = $scope.paragraph.config.graph.scatter.xAxis; + var yAxis = $scope.paragraph.config.graph.scatter.yAxis; + var group = $scope.paragraph.config.graph.scatter.group; + var size = $scope.paragraph.config.graph.scatter.size; + + var xValues = []; + var yValues = []; + var rows = {}; + var d3g = []; + + var rowNameIndex = {}; + var colNameIndex = {}; + var grpNameIndex = {}; + var rowIndexValue = {}; + var colIndexValue = {}; + var grpIndexValue = {}; + var rowIdx = 0; + var colIdx = 0; + var grpIdx = 0; + var grpName = ''; + + var xValue; + var yValue; + var row; + + if (!xAxis && !yAxis) { + return { + d3g : [] + }; + } + + for (var i = 0; i < data.rows.length; i++) { + row = data.rows[i]; + if (xAxis) { + xValue = row[xAxis.index]; + xValues[i] = xValue; + } + if (yAxis) { + yValue = row[yAxis.index]; + yValues[i] = yValue; + } + } + + var isAllDiscrete = ((xAxis && yAxis && isDiscrete(xValues) && isDiscrete(yValues)) || + (!xAxis && isDiscrete(yValues)) || + (!yAxis && isDiscrete(xValues))); + + if (isAllDiscrete) { + rows = setDiscreteScatterData(data); + } else { + rows = data.rows; + } + + if (!group && isAllDiscrete) { + grpName = 'count'; + } else if (!group && !size) { + if (xAxis && yAxis) { + grpName = '(' + xAxis.name + ', ' + yAxis.name + ')'; + } else if (xAxis && !yAxis) { + grpName = xAxis.name; + } else if (!xAxis && yAxis) { + grpName = yAxis.name; + } + } else if (!group && size) { + grpName = size.name; + } + + for (i = 0; i < rows.length; i++) { + row = rows[i]; + if (xAxis) { + xValue = row[xAxis.index]; + } + if (yAxis) { + yValue = row[yAxis.index]; + } + if (group) { + grpName = row[group.index]; + } + var sz = (isAllDiscrete) ? row[row.length-1] : ((size) ? row[size.index] : 1); + + if (grpNameIndex[grpName] === undefined) { + grpIndexValue[grpIdx] = grpName; + grpNameIndex[grpName] = grpIdx++; + } + + if (xAxis && rowNameIndex[xValue] === undefined) { + rowIndexValue[rowIdx] = xValue; + rowNameIndex[xValue] = rowIdx++; + } + + if (yAxis && colNameIndex[yValue] === undefined) { + colIndexValue[colIdx] = yValue; + colNameIndex[yValue] = colIdx++; + } + + if (!d3g[grpNameIndex[grpName]]) { + d3g[grpNameIndex[grpName]] = { + key : grpName, + values : [] + }; + } + + d3g[grpNameIndex[grpName]].values.push({ + x : xAxis ? (isNaN(xValue) ? rowNameIndex[xValue] : parseFloat(xValue)) : 0, + y : yAxis ? (isNaN(yValue) ? colNameIndex[yValue] : parseFloat(yValue)) : 0, + size : isNaN(parseFloat(sz))? 1 : parseFloat(sz) + }); + } + + return { + xLabels : rowIndexValue, + yLabels : colIndexValue, + d3g : d3g + }; + }; + + var isDiscrete = function(field) { + var getUnique = function(f) { + var uniqObj = {}; + var uniqArr = []; + var j = 0; + for (var i = 0; i < f.length; i++) { + var item = f[i]; + if(uniqObj[item] !== 1) { + uniqObj[item] = 1; + uniqArr[j++] = item; + } + } + return uniqArr; + }; + + for (var i = 0; i < field.length; i++) { + if(isNaN(parseFloat(field[i])) && + (typeof field[i] === 'string' || field[i] instanceof String)) { + return true; + } + } + + var threshold = 0.05; + var unique = getUnique(field); + if (unique.length/field.length < threshold) { + return true; + } else { + return false; + } + }; + + $scope.isValidSizeOption = function (options, rows) { + var xValues = []; + var yValues = []; + + for (var i = 0; i < rows.length; i++) { + var row = rows[i]; + var size = row[options.size.index]; + + //check if the field is numeric + if (isNaN(parseFloat(size)) || !isFinite(size)) { + return false; + } + + if (options.xAxis) { + var x = row[options.xAxis.index]; + xValues[i] = x; + } + if (options.yAxis) { + var y = row[options.yAxis.index]; + yValues[i] = y; + } + } + + //check if all existing fields are discrete + var isAllDiscrete = ((options.xAxis && options.yAxis && isDiscrete(xValues) && isDiscrete(yValues)) || + (!options.xAxis && isDiscrete(yValues)) || + (!options.yAxis && isDiscrete(xValues))); + + if (isAllDiscrete) { + return false; + } + + return true; + }; + + $scope.setGraphHeight = function() { + var height = $('#p'+$scope.paragraph.id+'_graph').height(); + + var newParams = angular.copy($scope.paragraph.settings.params); + var newConfig = angular.copy($scope.paragraph.config); + + newConfig.graph.height = height; + + commitParagraph($scope.paragraph.title, $scope.paragraph.text, newConfig, newParams); + }; + + /** Utility function */ + if (typeof String.prototype.startsWith !== 'function') { + String.prototype.startsWith = function(str) { + return this.slice(0, str.length) === str; + }; + } + + $scope.goToSingleParagraph = function () { + var noteId = $route.current.pathParams.noteId; + var redirectToUrl = location.protocol + '//' + location.host + '/#/notebook/' + noteId + '/paragraph/' + $scope.paragraph.id+'?asIframe'; + $window.open(redirectToUrl); + }; +});
http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/8c7424a1/zeppelin-web/src/app/notebook/paragraph/paragraph.html ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/notebook/paragraph/paragraph.html b/zeppelin-web/src/app/notebook/paragraph/paragraph.html new file mode 100644 index 0000000..bcd2483 --- /dev/null +++ b/zeppelin-web/src/app/notebook/paragraph/paragraph.html @@ -0,0 +1,449 @@ +<!-- +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 id="{{paragraph.id}}_container" + ng-class="{'paragraph outlineOnFocus': !asIframe, 'paragraphAsIframe': asIframe}"> + + <div ng-show="paragraph.config.title" + id="{{paragraph.id}}_title" + class="title"> + <input type="text" + placeholder="Edit title" + ng-model="paragraph.title" + ng-show="showTitleEditor" + ng-delete="showTitleEditor = false" + ng-enter="setTitle(); showTitleEditor = false"/> + <div ng-click="showTitleEditor = !asIframe && !viewOnly" + ng-show="!showTitleEditor" + ng-bind-html="paragraph.title || 'Untitled'"> + </div> + </div> + + <div> + <div ng-show="!paragraph.config.editorHide && !viewOnly"> + <div id="{{paragraph.id}}_editor" + style="opacity: 1;" + class="editor" + ui-ace="{ + onLoad : aceLoaded, + onChange: aceChanged, + require : ['ace/ext/language_tools'] + }" + ng-model="paragraph.text" + ng-class="{'disable': paragraph.status == 'RUNNING' || paragraph.status == 'PENDING' }"> + </div> + </div> + + <div id="{{paragraph.id}}_runControl" class="runControl"> + <div ng-if="(getProgress()<=0 || getProgress()>=100) && (paragraph.status=='RUNNING' )"> + <div id="{{paragraph.id}}_progress" + class="progress"> + <div class="progress-bar progress-bar-striped active" role="progressbar" style="width:100%;"></div> + <span class="sr-only"></span> + </div> + </div> + <div ng-if="getProgress()>0 && getProgress()<100 && paragraph.status=='RUNNING'"> + <div id="{{paragraph.id}}_progress" + class="progress"> + <div class="progress-bar" role="progressbar" style="width:{{getProgress()}}%;"></div> + <span class="sr-only">{{getProgress()}}%</span> + </div> + </div> + </div> + + <form id="{{paragraph.id}}_form" role="form" + ng-show="!paragraph.config.tableHide && !asIframe" + class=" paragraphForm form-horizontal row"> + <div class="form-group col-sm-6 col-md-6 col-lg-4" + ng-repeat="formulaire in paragraph.settings.forms" + ng-Init="loadForm(formulaire, paragraph.settings.params)"> + <label class="control-label input-sm" ng-class="{'disable': paragraph.status == 'RUNNING' || paragraph.status == 'PENDING' }">{{formulaire.name}}</label> + <div class=""> + <input class="form-control input-sm" + ng-if="!paragraph.settings.forms[formulaire.name].options" + ng-enter="runParagraph(getEditorValue())" + ng-model="paragraph.settings.params[formulaire.name]" + ng-class="{'disable': paragraph.status == 'RUNNING' || paragraph.status == 'PENDING' }" + name="{{formulaire.name}}"> + </input> + + <select class="form-control input-sm" + ng-if="paragraph.settings.forms[formulaire.name].options" + ng-change="runParagraph(getEditorValue())" + ng-model="paragraph.settings.params[formulaire.name]" + ng-class="{'disable': paragraph.status == 'RUNNING' || paragraph.status == 'PENDING' }" + name="{{formulaire.name}}" + ng-options="option.value as (option.displayName||option.value) for option in paragraph.settings.forms[formulaire.name].options" + > +<!-- + <option + ng-repeat="option in paragraph.settings.forms[formulaire.name].options" + value="{{option.value}}" + >{{option.displayName || option.value}} + </option> +--> + </select> + </div> + </div> + </form> + + <!-- Rendering --> + <div class='tableDisplay' ng-show="!paragraph.config.tableHide"> + <div id="{{paragraph.id}}_switch" + ng-if="paragraph.result.type == 'TABLE' && !asIframe && !viewOnly" + class="btn-group" + style='margin-bottom: 10px;'> + <button type="button" class="btn btn-default btn-sm" + ng-class="{'active': isGraphMode('table')}" + ng-click="setGraphMode('table', true)" ><i class="fa fa-table"></i> + </button> + <button type="button" class="btn btn-default btn-sm" + ng-class="{'active': isGraphMode('multiBarChart')}" + ng-click="setGraphMode('multiBarChart', true)"><i class="fa fa-bar-chart"></i> + </button> + <button type="button" class="btn btn-default btn-sm" + ng-class="{'active': isGraphMode('pieChart')}" + ng-click="setGraphMode('pieChart', true)"><i class="fa fa-pie-chart"></i> + </button> + <button type="button" class="btn btn-default btn-sm" + ng-class="{'active': isGraphMode('stackedAreaChart')}" + ng-click="setGraphMode('stackedAreaChart', true)"><i class="fa fa-area-chart"></i> + </button> + <button type="button" class="btn btn-default btn-sm" + ng-class="{'active': isGraphMode('lineChart')}" + ng-click="setGraphMode('lineChart', true)"><i class="fa fa-line-chart"></i> + </button> + <button type="button" class="btn btn-default btn-sm" + ng-class="{'active': isGraphMode('scatterChart')}" + ng-click="setGraphMode('scatterChart', true)"><i class="cf cf-scatter-chart"></i> + </button> + </div> + <span ng-if="getResultType()=='TABLE' && getGraphMode()!='table' && !asIframe && !viewOnly" + style="margin-left:10px; cursor:pointer; display: inline-block; vertical-align:top; position: relative; line-height:30px;"> + <a class="btnText" ng-if="paragraph.config.graph.optionOpen" + ng-click="toggleGraphOption()"> + settings <span class="fa fa-caret-up"></span> + </a> + <a class="btnText" ng-if="!paragraph.config.graph.optionOpen" + ng-click="toggleGraphOption()" > + settings <span class="fa fa-caret-down"></span> + </a> + </span> + + <div class="option lightBold" style="overflow: visible;" + ng-if="getResultType()=='TABLE' && getGraphMode()!='table' + && paragraph.config.graph.optionOpen && !asIframe && !viewOnly"> + All fields: + <div class="allFields row"> + <ul class="noDot"> + <li class="liVertical" ng-repeat="col in paragraph.result.columnNames"> + <div class="btn btn-default btn-xs" + data-drag="true" + data-jqyoui-options="{revert: 'invalid', helper: 'clone'}" + ng-model="paragraph.result.columnNames" + jqyoui-draggable="{index: {{$index}}, placeholder: 'keep'}"> + {{col.name | limitTo: 30}}{{col.name.length > 30 ? '...' : ''}} + </div> + </li> + </ul> + </div> + + <div class="row" ng-if="getGraphMode()!='scatterChart'"> + <div class="col-md-4"> + <span class="columns lightBold"> + Keys + <ul data-drop="true" + ng-model="paragraph.config.graph.keys" + jqyoui-droppable="{multiple:true, onDrop:'onGraphOptionChange()'}" + class="list-unstyled"> + <li ng-repeat="item in paragraph.config.graph.keys"> + <button class="btn btn-primary btn-xs"> + {{item.name}} <span class="fa fa-close" ng-click="removeGraphOptionKeys($index)"></span> + </button> + </li> + </ul> + </span> + </div> + <div class="col-md-4"> + <span class="columns lightBold"> + Groups + <ul data-drop="true" + ng-model="paragraph.config.graph.groups" + jqyoui-droppable="{multiple:true, onDrop:'onGraphOptionChange()'}" + class="list-unstyled"> + <li ng-repeat="item in paragraph.config.graph.groups"> + <button class="btn btn-success btn-xs"> + {{item.name}} <span class="fa fa-close" ng-click="removeGraphOptionGroups($index)"></span> + </button> + </li> + </ul> + </span> + </div> + <div class="col-md-4"> + <span class="columns lightBold"> + Values + <ul data-drop="true" + ng-model="paragraph.config.graph.values" + jqyoui-droppable="{multiple:true, onDrop:'onGraphOptionChange()'}" + class="list-unstyled"> + <li ng-repeat="item in paragraph.config.graph.values"> + <div class="btn-group"> + <div class="btn btn-info btn-xs dropdown-toggle" + type="button" + data-toggle="dropdown"> + {{item.name | limitTo: 30}}{{item.name.length > 30 ? '...' : ''}} + <font style="color:#EEEEEE;"><span class="lightBold" style="text-transform: uppercase;">{{item.aggr}}</span></font> + <span class="fa fa-close" ng-click="removeGraphOptionValues($index)"></span> + </div> + <ul class="dropdown-menu" role="menu"> + <li ng-click="setGraphOptionValueAggr($index, 'sum')"><a>sum</a></li> + <li ng-click="setGraphOptionValueAggr($index, 'count')"><a>count</a></li> + <li ng-click="setGraphOptionValueAggr($index, 'avg')"><a>avg</a></li> + <li ng-click="setGraphOptionValueAggr($index, 'min')"><a>min</a></li> + <li ng-click="setGraphOptionValueAggr($index, 'max')"><a>max</a></li> + </ul> + </div> + </li> + </ul> + </span> + </div> + </div> + + <div class="row" ng-if="getGraphMode()=='scatterChart'"> + <div class="col-md-3"> + <span class="columns lightBold"> + xAxis + <ul data-drop="true" + ng-model="paragraph.config.graph.scatter.xAxis" + jqyoui-droppable="{onDrop:'onGraphOptionChange()'}" + class="list-unstyled" + style="height:36px"> + <li ng-if="paragraph.config.graph.scatter.xAxis"> + <button class="btn btn-primary btn-xs"> + {{paragraph.config.graph.scatter.xAxis.name}} <span class="fa fa-close" ng-click="removeScatterOptionXaxis($index)"></span> + </button> + </li> + </ul> + </span> + </div> + <div class="col-md-3"> + <span class="columns lightBold"> + yAxis + <ul data-drop="true" + ng-model="paragraph.config.graph.scatter.yAxis" + jqyoui-droppable="{onDrop:'onGraphOptionChange()'}" + class="list-unstyled" + style="height:36px"> + <li ng-if="paragraph.config.graph.scatter.yAxis"> + <button class="btn btn-success btn-xs"> + {{paragraph.config.graph.scatter.yAxis.name}} <span class="fa fa-close" ng-click="removeScatterOptionYaxis($index)"></span> + </button> + </li> + </ul> + </span> + </div> + <div class="col-md-3"> + <span class="columns lightBold"> + group + <ul data-drop="true" + ng-model="paragraph.config.graph.scatter.group" + jqyoui-droppable="{onDrop:'onGraphOptionChange()'}" + class="list-unstyled" + style="height:36px"> + <li ng-if="paragraph.config.graph.scatter.group"> + <button class="btn btn-info btn-xs"> + {{paragraph.config.graph.scatter.group.name}} <span class="fa fa-close" ng-click="removeScatterOptionGroup($index)"></span> + </button> + </li> + </ul> + </span> + </div> + <div class="col-md-3"> + <span class="columns lightBold"> + size + <a tabindex="0" class="fa fa-info-circle" role="button" popover-placement="top" + popover-trigger="focus" + popover-html-unsafe="<li>Size option is valid only when you drop numeric field here.</li> + <li>When data in each axis are discrete, 'number of values in corresponding coordinate' will be used as size.</li> + <li>Zeppelin consider values as discrete when the values contain string value or the number of distinct values are bigger than 5% of total number of values.</li> + <li>Size field button turns to grey when the option you chose is not valid.</li>"></a> + <ul data-drop="true" + ng-model="paragraph.config.graph.scatter.size" + jqyoui-droppable="{onDrop:'onGraphOptionChange()'}" + class="list-unstyled" + style="height:36px"> + <li ng-if="paragraph.config.graph.scatter.size"> + <button class="btn btn-xs" style="color:white" ng-class="{'btn-warning': isValidSizeOption(paragraph.config.graph.scatter, paragraph.result.rows)}"> + {{paragraph.config.graph.scatter.size.name}} <span class="fa fa-close" ng-click="removeScatterOptionSize($index)"></span> + </button> + </li> + </ul> + </span> + </div> + </div> + + </div> + + + <div id="p{{paragraph.id}}_graph" + class="graphContainer" + ng-class="{'noOverflow': getGraphMode()=='table'}" + ng-if="getResultType()=='TABLE'" + allowresize="{{!asIframe && !viewOnly}}" + resizable on-resize="setGraphHeight();"> + + <div ng-if="getGraphMode()=='table'" + id="p{{paragraph.id}}_table" + class="table"> + </div> + + <div ng-if="getGraphMode()=='multiBarChart'" + id="p{{paragraph.id}}_multiBarChart"> + <svg></svg> + </div> + + <div ng-if="getGraphMode()=='pieChart'" + id="p{{paragraph.id}}_pieChart"> + <svg></svg> + </div> + + <div ng-if="getGraphMode()=='stackedAreaChart'" + id="p{{paragraph.id}}_stackedAreaChart"> + <svg></svg> + </div> + + <div ng-if="getGraphMode()=='lineChart'" + id="p{{paragraph.id}}_lineChart"> + <svg></svg> + </div> + + <div ng-if="getGraphMode()=='scatterChart'" + id="p{{paragraph.id}}_scatterChart"> + <svg></svg> + </div> + </div> + + <div id="{{paragraph.id}}_comment" + class="text" + ng-if="getResultType()=='TABLE' && paragraph.result.comment" + ng-Init="loadResultType(paragraph.result)" + ng-bind-html="paragraph.result.comment"> + </div> + + <div id="{{paragraph.id}}_text" + class="text" + ng-if="paragraph.result.type == 'TEXT'" + ng-Init="loadResultType(paragraph.result)" + ng-bind="paragraph.result.msg"> + </div> + + <div id="p{{paragraph.id}}_html" + ng-if="paragraph.result.type == 'HTML'" + ng-Init="loadResultType(paragraph.result)"> + </div> + + <div id="p{{paragraph.id}}_angular" + ng-if="paragraph.result.type == 'ANGULAR'" + ng-Init="loadResultType(paragraph.result)"> + </div> + + <img id="{{paragraph.id}}_img" + ng-if="paragraph.result.type == 'IMG'" + ng-Init="loadResultType(paragraph.result)" + ng-src="{{getBase64ImageSrc(paragraph.result.msg)}}"> + </img> + + <div id="{{paragraph.id}}_error" + class="error" + ng-if="paragraph.status == 'ERROR'" + ng-bind="paragraph.errorMessage"> + </div> + + <div id="{{paragraph.id}}_executionTime" class="executionTime" ng-bind-html="getExecutionTime()"> + </div> + </div> + </div> + <div id="{{paragraph.id}}_control" class="control" ng-show="!asIframe"> + + <span> + {{paragraph.status}} + </span> + + <span ng-if="paragraph.status=='RUNNING'"> + {{getProgress()}}% + </span> + + <!-- Run / Cancel button --> + <span class="icon-control-play" style="cursor:pointer;color:#3071A9" tooltip-placement="top" tooltip="Run this paragraph" + ng-click="runParagraph(getEditorValue())" + ng-show="paragraph.status!='RUNNING' && paragraph.status!='PENDING'"></span> + <span class="icon-control-pause" style="cursor:pointer;color:#CD5C5C" tooltip-placement="top" tooltip="Cancel" + ng-click="cancelParagraph()" + ng-show="paragraph.status=='RUNNING' || paragraph.status=='PENDING'"></span> + + <span class="{{paragraph.config.editorHide ? 'icon-size-fullscreen' : 'icon-size-actual'}}" style="cursor:pointer;" tooltip-placement="top" tooltip="{{(paragraph.config.editorHide ? 'Show' : 'Hide') + ' editor'}}" + ng-click="toggleEditor()"></span> + <span class="{{paragraph.config.tableHide ? 'icon-notebook' : 'icon-book-open'}}" style="cursor:pointer;" tooltip-placement="top" tooltip="{{(paragraph.config.tableHide ? 'Show' : 'Hide') + ' output'}}" + ng-click="toggleOutput()"></span> + + <span class="dropdown navbar-right"> + <span class="icon-settings" style="cursor:pointer" + data-toggle="dropdown" + type="button"> + </span> + <ul class="dropdown-menu" role="menu" style="width:200px;"> + <li> + <a class="fa fa-arrows-h dropdown"> Width + <form style="display:inline; margin-left:5px;"> + <select ng-model="paragraph.config.colWidth" + class="selectpicker" + ng-change="changeColWidth()" + ng-options="col for col in colWidthOption"></select> + </form> + </a> + </li> + <li> + <a class="icon-arrow-up" style="cursor:pointer" + ng-click="moveUp()"> Move Up</a> + </li> + <li> + <a class="icon-arrow-down" style="cursor:pointer" + ng-click="moveDown()"> Move Down</a> + </li> + <li> + <a class="icon-plus" style="cursor:pointer" + ng-click="insertNew()"> Insert New</a> + </li> + <li> + <!-- paragraph handler --> + <a class="fa fa-font" style="cursor:pointer" + ng-click="hideTitle()" + ng-show="paragraph.config.title"> Hide title</a> + <a class="fa fa-font" style="cursor:pointer" + ng-click="showTitle()" + ng-show="!paragraph.config.title"> Show title</a> + </li> + + <li><a class="icon-share-alt" style="cursor:pointer" + ng-click="goToSingleParagraph()"> Link this paragraph</a> + </li> + <li> + <!-- remove paragraph --> + <a class="fa fa-times" style="cursor:pointer" + ng-click="removeParagraph()"> Remove</a> + </li> + </ul> + </span> + </div> +</div> http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/8c7424a1/zeppelin-web/src/assets/images/zepLogo.png ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/assets/images/zepLogo.png b/zeppelin-web/src/assets/images/zepLogo.png new file mode 100644 index 0000000..f11f022 Binary files /dev/null and b/zeppelin-web/src/assets/images/zepLogo.png differ http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/8c7424a1/zeppelin-web/src/assets/images/zepLogoW.png ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/assets/images/zepLogoW.png b/zeppelin-web/src/assets/images/zepLogoW.png new file mode 100644 index 0000000..0b45fa4 Binary files /dev/null and b/zeppelin-web/src/assets/images/zepLogoW.png differ http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/8c7424a1/zeppelin-web/src/assets/styles/looknfeel/default.css ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/assets/styles/looknfeel/default.css b/zeppelin-web/src/assets/styles/looknfeel/default.css new file mode 100644 index 0000000..8aeee95 --- /dev/null +++ b/zeppelin-web/src/assets/styles/looknfeel/default.css @@ -0,0 +1,67 @@ +/* + * 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. + */ + +body { + background: #ecf0f1; +} + +/** + * Box and well + */ +.box{ + border-style: solid; + min-height: 20px; + padding: 19px; + margin-bottom: 20px; +} + +.box, +.well { + background-color: #ffffff; + border-color: #e5e5e5; + border-width: 1px 1px 2px; + border-radius: 3px; + -webkit-box-shadow: none; + box-shadow: none; +} + +.paragraph { + min-height: 32px; +} + +.noteAction { + background-color: white; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15); + color: #2c3e50; + border-bottom: 1px solid #E5E5E5; +} + +.control span { + margin-left: 4px; +} + +.control { + padding: 4px; +} + +.paragraph-space { + margin-bottom: 5px; + padding: 10px !important; +} + +.editor, +.executionTime, +.nv-controlsWrap { + display:block; +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/8c7424a1/zeppelin-web/src/assets/styles/looknfeel/report.css ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/assets/styles/looknfeel/report.css b/zeppelin-web/src/assets/styles/looknfeel/report.css new file mode 100644 index 0000000..065b92c --- /dev/null +++ b/zeppelin-web/src/assets/styles/looknfeel/report.css @@ -0,0 +1,68 @@ +/* + * 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. + */ + +body { + background: white; +} + + +/** + * Box and well + */ +.box{ + border: 0px; + min-height: 0px; +} + +.box:hover, +.well:hover { + background-color: white; + border: 0px; +} + +.paragraph-col{ + border: 0px solid white; +} + +.paragraph { + min-height: 32px; +} + +.paragraph-space { + margin-bottom: 5px; + padding: 10px !important; +} + +.paragraph .control { + visibility : hidden; + right:15px; + top: 6px; +} + +.paragraph:hover .control { + visibility : hidden; +} + +.noteAction span, .noteAction button, .noteAction form { + visibility : hidden; +} + +.noteAction:hover span, .noteAction:hover button, .noteAction:hover form { + visibility : visible; +} + +.executionTime, +.nv-controlsWrap { + display:none; +} http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/8c7424a1/zeppelin-web/src/assets/styles/looknfeel/simple.css ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/assets/styles/looknfeel/simple.css b/zeppelin-web/src/assets/styles/looknfeel/simple.css new file mode 100644 index 0000000..9edb95e --- /dev/null +++ b/zeppelin-web/src/assets/styles/looknfeel/simple.css @@ -0,0 +1,73 @@ +/* + * 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. + */ + +body { + background: white; +} + + +/** + * Box and well + */ +.box{ + border: 1px solid white; + min-height: 20px; +} + +.box:hover, +.well:hover { + background-color: #ffffff; + border: 1px solid #DDDDDD; +} + +.paragraph-col .focused{ + box-shadow: 1px 2px 7px rgba(0, 0, 0, 0.3); +} + +.paragraph-col{ + border: 2px solid white; +} + +.paragraph { + min-height: 32px; +} + +.paragraph-space { + padding: 0px 2px 0px 2px !important; +} + + +.paragraph .control { + visibility : hidden; + right:15px; + top: 6px; +} + +.paragraph:hover .control { + visibility : visible; +} + +.noteAction span, .noteAction button, .noteAction form { + visibility : hidden; +} + +.noteAction:hover span, .noteAction:hover button, .noteAction:hover form { + visibility : visible; +} + +.editor, +.executionTime, +.nv-controlsWrap { + display:block; +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/8c7424a1/zeppelin-web/src/assets/styles/printMode.css ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/assets/styles/printMode.css b/zeppelin-web/src/assets/styles/printMode.css new file mode 100644 index 0000000..953b3a6 --- /dev/null +++ b/zeppelin-web/src/assets/styles/printMode.css @@ -0,0 +1,61 @@ +/* + * 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. + */ + + +body { + background: white; +} + +.labelBtn { + display: none; +} + +.new_h3 { + margin-left: 220px; + top: 0px; + position: fixed; +} + +.noteAction { + border: 1px solid #3071a9; + box-shadow: none; +} + +.control { + display: none; +} + +.editor { + display: none; +} + +.form-horizontal { + display: none; +} + +.btn-group { + display: none; +} + +.box { + border: none; +} + +svg { + margin-left: -20px; +} + +.btn-link { + display: none; +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/8c7424a1/zeppelin-web/src/components/baseUrl/baseUrl.service.js ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/components/baseUrl/baseUrl.service.js b/zeppelin-web/src/components/baseUrl/baseUrl.service.js new file mode 100644 index 0000000..662d88f --- /dev/null +++ b/zeppelin-web/src/components/baseUrl/baseUrl.service.js @@ -0,0 +1,66 @@ +/* + * 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. + */ +'use strict'; + +angular.module('zeppelinWebApp').service('baseUrlSrv', function() { + + /** Get the current port of the websocket + * + * When running Zeppelin, the body of this function will be dynamically + * overridden with the AppScriptServlet from zeppelin-site.xml config value. + * + * If the config value is not defined, it defaults to the HTTP port + 1 + * + * In the case of running "grunt serve", this function will appear + * as is. + */ + + /* @preserve AppScriptServlet - getPort */ + this.getPort = function() { + var port = Number(location.port); + if (location.protocol !== 'https:' && !port) { + port = 80; + } else if (location.protocol === 'https:' && !port) { + port = 443; + } else if (port === 3333 || port === 9000) { + port = 8080; + } + return port + 1; + }; + /* @preserve AppScriptServlet - close */ + + this.getWebsocketProtocol = function() { + return location.protocol === 'https:' ? 'wss' : 'ws'; + }; + + this.getRestApiBase = function() { + var port = Number(location.port); + if (!port) { + port = 80; + if (location.protocol === 'https:') { + port = 443; + } + } + + if (port === 3333 || port === 9000) { + port = 8080; + } + return location.protocol + '//' + location.hostname + ':' + port + skipTrailingSlash(location.pathname) + '/api'; + }; + + var skipTrailingSlash = function(path) { + return path.replace(/\/$/, ''); + }; + +}); \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/8c7424a1/zeppelin-web/src/components/dropdowninput/dropdowninput.directive.js ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/components/dropdowninput/dropdowninput.directive.js b/zeppelin-web/src/components/dropdowninput/dropdowninput.directive.js new file mode 100644 index 0000000..65dd5d3 --- /dev/null +++ b/zeppelin-web/src/components/dropdowninput/dropdowninput.directive.js @@ -0,0 +1,25 @@ +/* + * 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. + */ +'use strict'; + +angular.module('zeppelinWebApp').directive('dropdownInput', function () { + return { + restrict: 'A', + link: function (scope, element) { + element.bind('click', function (event) { + event.stopPropagation(); + }); + } + }; +}); http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/8c7424a1/zeppelin-web/src/components/modal-shortcut/modal-shortcut.html ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/components/modal-shortcut/modal-shortcut.html b/zeppelin-web/src/components/modal-shortcut/modal-shortcut.html new file mode 100644 index 0000000..1fdf9ea --- /dev/null +++ b/zeppelin-web/src/components/modal-shortcut/modal-shortcut.html @@ -0,0 +1,130 @@ +<!-- +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="modal fade" id="shortcutModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">×</span><span class="sr-only">Close</span></button> + <h4 class="modal-title" id="myModalLabel">Keyboard shortcuts</h4> + </div> + <div class="modal-body"> + + <h4>Control in Note</h4> + + <div class="row"> + <div class="col-md-4"> + <div class="keys"> + <kbd class="kbd-dark">Shift</kbd> + <kbd class="kbd-dark">Enter</kbd> + </div> + </div> + <div class="col-md-8"> + Run the note + </div> + </div> + + <div class="row"> + <div class="col-md-4"> + <div class="keys"> + <kbd class="kbd-dark">Ctrl</kbd> + <kbd class="kbd-dark">p</kbd> + </div> + </div> + <div class="col-md-8"> + Move cursor Up + </div> + </div> + + <div class="row"> + <div class="col-md-4"> + <div class="keys"> + <kbd class="kbd-dark">Ctrl</kbd> + <kbd class="kbd-dark">n</kbd> + </div> + </div> + <div class="col-md-8"> + Move cursor Down + </div> + </div> + + + <h4>Control in Note Editor</h4> + + <div class="row"> + <div class="col-md-4"> + <div class="keys"> + <kbd class="kbd-dark">Ctrl</kbd> + <kbd class="kbd-dark">k</kbd> + </div> + </div> + <div class="col-md-8"> + Cut the line + </div> + </div> + + <div class="row"> + <div class="col-md-4"> + <div class="keys"> + <kbd class="kbd-dark">Ctrl</kbd> + <kbd class="kbd-dark">y</kbd> + </div> + </div> + <div class="col-md-8"> + Paste the line + </div> + </div> + + <div class="row"> + <div class="col-md-4"> + <div class="keys"> + <kbd class="kbd-dark">Ctrl</kbd> + <kbd class="kbd-dark">s</kbd> + </div> + </div> + <div class="col-md-8"> + Search inside the code + </div> + </div> + + <div class="row"> + <div class="col-md-4"> + <div class="keys"> + <kbd class="kbd-dark">Ctrl</kbd> + <kbd class="kbd-dark">a</kbd> + </div> + </div> + <div class="col-md-8"> + Move cursor to the beginning + </div> + </div> + + <div class="row"> + <div class="col-md-4"> + <div class="keys"> + <kbd class="kbd-dark">Ctrl</kbd> + <kbd class="kbd-dark">e</kbd> + </div> + </div> + <div class="col-md-8"> + Move cursor at the end + </div> + </div> + + <div class="row"> + <div class="col-md-4"> + <div class="keys"> + <kbd class="kbd-dark">Ctrl</kbd> + <kbd class="kbd-dark">N</kbd> + </div> + </div> + <div class="col-md-8"> + Move cursor Down + </div> + </div> + </div> + </div> + </div> +</div>
