http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/8c7424a1/zeppelin-web/src/WEB-INF/web.xml ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/WEB-INF/web.xml b/zeppelin-web/src/WEB-INF/web.xml new file mode 100644 index 0000000..f34da18 --- /dev/null +++ b/zeppelin-web/src/WEB-INF/web.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> +<!-- + ~ Licensed to the Apache Software Foundation (ASF) under one or more + ~ contributor license agreements. See the NOTICE file distributed with + ~ this work for additional information regarding copyright ownership. + ~ The ASF licenses this file to You under the Apache License, Version 2.0 + ~ (the "License"); you may not use this file except in compliance with + ~ the License. You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" + version="2.5"> + + <display-name>zeppelin-web</display-name> + <servlet> + <servlet-name>default</servlet-name> + <servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class> + <init-param> + <param-name>com.sun.jersey.config.property.packages</param-name> + <param-value>org.apache.zeppelin.rest;com.wordnik.swagger.jersey.listing</param-value> + </init-param> + <load-on-startup>1</load-on-startup> + </servlet> + + <!-- This route is for swagger, must be different than root --> + <servlet-mapping> + <servlet-name>default</servlet-name> + <url-pattern>/rest/*</url-pattern> + </servlet-mapping> + + <context-param> + <param-name>configuration</param-name> + <param-value>deployment</param-value> + </context-param> +</web-app>
http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/8c7424a1/zeppelin-web/src/app/app.controller.js ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/app.controller.js b/zeppelin-web/src/app/app.controller.js new file mode 100644 index 0000000..f2bf8ab --- /dev/null +++ b/zeppelin-web/src/app/app.controller.js @@ -0,0 +1,45 @@ +/* + * 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('MainCtrl', function($scope, $rootScope, $window) { + $rootScope.compiledScope = $scope.$new(true, $rootScope); + $scope.looknfeel = 'default'; + + var init = function() { + $scope.asIframe = (($window.location.href.indexOf('asIframe') > -1) ? true : false); + }; + + init(); + + $rootScope.$on('setIframe', function(event, data) { + if (!event.defaultPrevented) { + $scope.asIframe = data; + event.preventDefault(); + } + }); + + $rootScope.$on('setLookAndFeel', function(event, data) { + if (!event.defaultPrevented && data && data !== '' && data != $scope.looknfeel) { + $scope.looknfeel = data; + event.preventDefault(); + } + }); + + // Set The lookAndFeel to default on every page + $rootScope.$on('$routeChangeStart', function(event, next, current) { + $rootScope.$broadcast('setLookAndFeel', 'default'); + }); + +}); http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/8c7424a1/zeppelin-web/src/app/app.js ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/app.js b/zeppelin-web/src/app/app.js new file mode 100644 index 0000000..0d163d9 --- /dev/null +++ b/zeppelin-web/src/app/app.js @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +'use strict'; + +angular.module('zeppelinWebApp', [ + 'ngAnimate', + 'ngCookies', + 'ngRoute', + 'ngSanitize', + 'angular-websocket', + 'ui.ace', + 'ui.bootstrap', + 'ui.sortable', + 'ngTouch', + 'ngDragDrop', + 'monospaced.elastic', + 'puElasticInput', + 'xeditable' + ]) + .filter('breakFilter', function() { + return function (text) { + if (!!text) { + return text.replace(/\n/g, '<br />'); + } + }; + }) + .config(function ($routeProvider) { + $routeProvider + .when('/', { + templateUrl: 'app/home/home.html', + controller: 'HomeCtrl' + }) + .when('/notebook/:noteId', { + templateUrl: 'app/notebook/notebook.html', + controller: 'NotebookCtrl' + }) + .when('/notebook/:noteId/paragraph/:paragraphId?', { + templateUrl: 'app/notebook/notebook.html', + controller: 'NotebookCtrl' + }) + .when('/interpreter', { + templateUrl: 'app/interpreter/interpreter.html', + controller: 'InterpreterCtrl' + }) + .otherwise({ + redirectTo: '/' + }); + }); http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/8c7424a1/zeppelin-web/src/app/home/home.controller.js ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/home/home.controller.js b/zeppelin-web/src/app/home/home.controller.js new file mode 100644 index 0000000..e66201c --- /dev/null +++ b/zeppelin-web/src/app/home/home.controller.js @@ -0,0 +1,22 @@ +/* + * 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('HomeCtrl', function($scope, notebookListDataFactory, websocketMsgSrv) { + + var vm = this; + vm.notes = notebookListDataFactory; + vm.websocketMsgSrv = websocketMsgSrv; + +}); http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/8c7424a1/zeppelin-web/src/app/home/home.css ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/home/home.css b/zeppelin-web/src/app/home/home.css new file mode 100644 index 0000000..5058cb8 --- /dev/null +++ b/zeppelin-web/src/app/home/home.css @@ -0,0 +1,288 @@ +/* + * 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. + */ +@import url(//fonts.googleapis.com/css?family=Patua+One); +@import url(//fonts.googleapis.com/css?family=Roboto); +@import url(//fonts.googleapis.com/css?family=Source+Code+Pro); + +body { + padding-top: 60px; + color: #212121; +} + +html,body { height: 100%;} + +.bodyAsIframe { + background: white; +} + +.displayNavBar { + display: inline !important; +} + + +body.asIframe { + padding-top: 0px; +} +body .navbar { + margin-bottom: 10px; +} + +.container { + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; + width: auto; +} + +.navbar-inverse { + background-color: #3071a9; + color: #fff; + border-color: #3071a9; + font-size: 18px; +} + +.navbar-inverse .navbar-nav > .open > a, .navbar-inverse .navbar-nav > .open > a:hover, .navbar-inverse .navbar-nav > .open > a:focus { + color: #fff; + background-color: #3071a9; +} + +.navbar-inverse .navbar-toggle:hover, .navbar-inverse .navbar-toggle:focus { + background-color: #3071a9; +} + +.navbar-inverse .navbar-nav > li > a:hover, .navbar-inverse .navbar-nav > li > a:focus { + color: #fff; + background-color: rgba(0, 0, 0, 0.2); + +} + +.navbar-inverse .navbar-toggle { + border-color: #FFFFFF; +} + +.navbar-inverse .navbar-nav > .active > a, .navbar-inverse .navbar-nav > .active > a:hover, .navbar-inverse .navbar-nav > .active > a:focus { + color: #fff; + background-color: #080808; +} + +.navbar-inverse .navbar-nav > li > a { + color: #FFFFFF; +} + +.navbar-inverse .navbar-brand { + color: #fff; + text-decoration: none; + font-family: 'Patua One', cursive; + font-size: 32px; +} + +a.navbar-brand:hover { + color: #fff !important; +} + +/* bootstrap customization for scrollable dropdown menu */ +.dropdown-menu > .scrollbar-container > li > a { + display: block; + padding: 3px 20px; + clear: both; + font-weight: normal; + line-height: 1.42857143; + color: #333; + white-space: nowrap; +} +.dropdown-menu > .scrollbar-container > li > a:hover, +.dropdown-menu > .scrollbar-container > li > a:focus { + color: #262626; + text-decoration: none; + background-color: #f5f5f5; +} + +.dropdown-menu > .scrollbar-container > .active > a, +.dropdown-menu > .scrollbar-container > .active > a:hover, +.dropdown-menu > .scrollbar-container > .active > a:focus { + color: #fff; + text-decoration: none; + background-color: #428bca; + outline: 0; +} + +@media (max-width: 767px) { + .navbar-nav .open .dropdown-menu > .scrollbar-container > li > a { + padding: 5px 15px 5px 25px; + line-height: 20px; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > .scrollbar-container > li > a { + color: #777; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > .scrollbar-container > li > a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu > .scrollbar-container > li > a:focus { + color: #fff; + background-color: transparent; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > .scrollbar-container > .active > a, + .navbar-inverse .navbar-nav .open .dropdown-menu > .scrollbar-container > .active > a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu > .scrollbar-container > .active > a:focus { + color: #fff; + background-color: #080808; + } +} + + +#main { + padding-left: 10px; + padding-right: 10px; + padding-bottom: 10px; + height: 100%; +} + +#notebook-list { + position: relative; + overflow: hidden; +} + +@media (min-width: 768px) { + #notebook-list { + max-height: 500px; + } +} + +.server-status { + font-size:12px; + margin-top: 6px; +} + +.server-connected { + color: #00CC00; +} + +.server-disconnected { + color: rgba(240, 48, 0, 1); + font-size: 12px !important; + font-weight: bold !important; +} + + +/** + * Box and well + */ +.box{ + border-style: solid; + min-height: 20px; + padding: 10px; + 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; +} + +.box-heading{ + position:relative; + max-width: 100%; + /*font-size: 20px;*/ + font-weight:300; + white-space: nowrap; + /*overflow:hidden;*/ + text-overflow: ellipsis; + vertical-align: middle; + margin: 0 0 15px; + padding-bottom: 2px; +} +/** +h1.box-heading, +h2.box-heading, +h3.box-heading, +h4.box-heading, +h5.box-heading, +h6.box-heading{ + font-size: 20px; +}*/ +.box-heading > .btn-group, +.box-heading > .btn{ + margin-top: -3px; +} + + +.icheck-label{ + position: relative; + top: 0; +} + + +.zeppelin { + background-image: url('/assets/images/zepLogo.png'); + background-repeat: no-repeat; + background-position: right; + height: 380px; + opacity: 0.2; +} + +.zeppelin2 { + background-image: url('/assets/images/zepLogo.png'); + background-repeat: no-repeat; + background-position: right; + background-position-y: 12px; + height: 380px; + opacity: 0.2; +} + +.keys { + padding-right: 10px; + color: #999; + text-align: right; + white-space: nowrap; +} + +.kbd-dark { + color: #eee; + background-color: #222; + background-image: none; + border: 0; +} + +kbd { + background-color: #e7e7e7; + background-image: -webkit-linear-gradient(#fefefe, #e7e7e7); + background-image: linear-gradient(#fefefe, #e7e7e7); + background-repeat: repeat-x; + display: inline-block; + padding: 4px 5px; + font: 11px Consolas, "Liberation Mono", Menlo, Courier, monospace; + line-height: 10px; + color: #000; + border: 1px solid #cfcfcf; + border-radius: 2px; +} + +.home { + min-height: 400px; +} + + +/* +temporary fix for bootstrap issue (https://github.com/twbs/bootstrap/issues/5865) +This part should be removed when new version of bootstrap handles this issue. +*/ +.btn-group > .tooltip + .btn, +.btn-group > .popover + .btn { + margin-left:-1px; +} + http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/8c7424a1/zeppelin-web/src/app/home/home.html ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/home/home.html b/zeppelin-web/src/app/home/home.html new file mode 100644 index 0000000..9e7963c --- /dev/null +++ b/zeppelin-web/src/app/home/home.html @@ -0,0 +1,53 @@ +<!-- +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="box width-full home" ng-controller="HomeCtrl as home"> + <div class="zeppelin"> + <div class="zeppelin2"></div> + </div> + <div style="margin-top: -380px;"> + <h1 class="box-heading" id="welcome"> + Welcome to Zeppelin! + </h1> + Zeppelin is web-based notebook that enables interactive data analytics.<br>You can make beautiful data-driven, interactive, collaborative document with SQL, code and even more!<br> + + <div class="row"> + <div class="col-md-4"> + <h4>Notebook</h4> + + <div> + <h5><a href="javascript:void(0);" ng-click="home.websocketMsgSrv.createNotebook()" style="text-decoration: none;"> + <i style="font-size: 15px;" class="icon-notebook"></i> Create new note</a></h5> + <ul style="list-style-type: none;"> + <li ng-repeat="note in home.notes.list track by $index"><i style="font-size: 10px;" class="icon-doc"></i> + <a style="text-decoration: none;" href="#/notebook/{{note.id}}">{{note.name || 'Note ' + note.id}}</a> + </li> + </ul> + </div> + </div> + <div class="col-md-6"> + <h4>Help</h4> + Get started with <a style="text-decoration: none;" href="http://zeppelin.incubator.apache.org/docs/index.html" target="_blank">Zeppelin documentation</a><br> + + <h4>Community</h4> + Please feel free to help us to improve Zeppelin, <br> + Any contribution are welcome!<br><br> + <a style="text-decoration: none;" href="http://zeppelin.incubator.apache.org/community.html" target="_blank"><i style="font-size: 15px;" class="fa fa-users"></i> Mailing list</a><br> + <a style="text-decoration: none;" href="https://issues.apache.org/jira/browse/ZEPPELIN" target="_blank"><i style="font-size: 15px;" class="fa fa-bug"></i> Issues tracking</a><br> + <a style="text-decoration: none;" href="https://github.com/apache/incubator-zeppelin" target="_blank"><i style="font-size: 20px;" class="fa fa-github"></i> Github</a> + </div> + </div> + </div> + <br/><br/><br/> +</div> http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/8c7424a1/zeppelin-web/src/app/interpreter/interpreter-create/interpreter-create.html ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/interpreter/interpreter-create/interpreter-create.html b/zeppelin-web/src/app/interpreter/interpreter-create/interpreter-create.html new file mode 100644 index 0000000..e7096f6 --- /dev/null +++ b/zeppelin-web/src/app/interpreter/interpreter-create/interpreter-create.html @@ -0,0 +1,77 @@ +<!-- +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> + <div class="row"> + <div class="col-md-12"> + <div class="interpreterSettingAdd" ng-show="showAddNewSetting"> + <hr /> + <h4>Create new interpreter</h4> + + <div class="form-group" style="width:200px"> + <b>Name</b> + <input id="newInterpreterSettingName" input pu-elastic-input + pu-elastic-input-minwidth="180px" ng-model="newInterpreterSetting.name" /> + </div> + + <b>Interpreter</b> + <div class="form-group" + style="width:180px"> + <select class="form-control input-sm" ng-model="newInterpreterSetting.group" + ng-change="newInterpreterGroupChange()"> + <option ng-repeat="(groupName, interpreterGroup) in availableInterpreters" value="{{groupName}}">{{groupName}}</option> + </select> + </div> + + <b>Properties</b> + <table class="table table-striped properties"> + <tr> + <th>name</th> + <th>value</th> + <th>description</th> + <th>action</th> + </tr> + <tr ng-repeat="(key, value) in newInterpreterSetting.properties"> + <td>{{key}}</td> + <td><textarea msd-elastic ng-model="value.value" ng-init="value.value = value.defaultValue"></textarea></td> + <td>{{value.description}}</td> + <td> + <div class="btn btn-default btn-sm fa fa-remove" ng-click="removeInterpreterProperty(key)"> + </div> + </td> + </tr> + + <tr> + <td> + <input pu-elastic-input pu-elastic-input-minwidth="180px" + ng-model="newInterpreterSetting.propertyKey" /> + </td> + <td><textarea msd-elastic ng-model="newInterpreterSetting.propertyValue"></textarea></td> + <td></td> + <td> + <div class="btn btn-default btn-sm fa fa-plus" ng-click="addNewInterpreterProperty()"> + </div> + </td> + </tr> + </table> + + <span class="btn btn-primary" ng-click="addNewInterpreterSetting()"> + Save + </span> + <span class="btn btn-default" ng-click="showAddNewSetting=false"> + Cancel + </span> + </div> + </div> + </div> +</div> \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/8c7424a1/zeppelin-web/src/app/interpreter/interpreter.controller.js ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/interpreter/interpreter.controller.js b/zeppelin-web/src/app/interpreter/interpreter.controller.js new file mode 100644 index 0000000..499c270 --- /dev/null +++ b/zeppelin-web/src/app/interpreter/interpreter.controller.js @@ -0,0 +1,202 @@ +/* global confirm:false, alert:false, _:false */ +/* jshint loopfunc: true */ +/* + * 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('InterpreterCtrl', function($scope, $route, $routeParams, $location, $rootScope, + $http, baseUrlSrv) { + var interpreterSettingsTmp = []; + $scope.interpreterSettings = []; + $scope.availableInterpreters = {}; + $scope.showAddNewSetting = false; + + var getInterpreterSettings = function() { + $http.get(baseUrlSrv.getRestApiBase()+'/interpreter/setting'). + success(function(data, status, headers, config) { + $scope.interpreterSettings = data.body; + }). + error(function(data, status, headers, config) { + console.log('Error %o %o', status, data.message); + }); + }; + + var getAvailableInterpreters = function() { + $http.get(baseUrlSrv.getRestApiBase()+'/interpreter'). + success(function(data, status, headers, config) { + $scope.availableInterpreters = data.body; + }). + error(function(data, status, headers, config) { + console.log('Error %o %o', status, data.message); + }); + }; + + var emptyNewProperty = function(object) { + angular.extend(object, {propertyValue: '', propertyKey: ''}); + }; + + var removeTMPSettings = function(index) { + interpreterSettingsTmp.splice(index, 1); + }; + + $scope.copyOriginInterpreterSettingProperties = function(settingId) { + var index = _.findIndex($scope.interpreterSettings, { 'id': settingId }); + interpreterSettingsTmp[index] = angular.copy($scope.interpreterSettings[index]); + }; + + $scope.updateInterpreterSetting = function(settingId) { + var result = confirm('Do you want to update this interpreter and restart with new settings?'); + if (!result) { + return; + } + + var index = _.findIndex($scope.interpreterSettings, { 'id': settingId }); + + var request = { + option : angular.copy($scope.interpreterSettings[index].option), + properties : angular.copy($scope.interpreterSettings[index].properties), + }; + + + $http.put(baseUrlSrv.getRestApiBase() + '/interpreter/setting/' + settingId, request). + success(function(data, status, headers, config) { + $scope.interpreterSettings[index] = data.body; + removeTMPSettings(index); + }). + error(function(data, status, headers, config) { + console.log('Error %o %o', status, data.message); + }); + }; + + $scope.resetInterpreterSetting = function(settingId){ + var index = _.findIndex($scope.interpreterSettings, { 'id': settingId }); + + // Set the old settings back + $scope.interpreterSettings[index] = angular.copy(interpreterSettingsTmp[index]); + removeTMPSettings(index); + }; + + $scope.removeInterpreterSetting = function(settingId) { + var result = confirm('Do you want to delete this interpreter setting?'); + if (!result) { + return; + } + + $http.delete(baseUrlSrv.getRestApiBase() + '/interpreter/setting/' + settingId). + success(function(data, status, headers, config) { + + var index = _.findIndex($scope.interpreterSettings, { 'id': settingId }); + $scope.interpreterSettings.splice(index, 1); + }). + error(function(data, status, headers, config) { + console.log('Error %o %o', status, data.message); + }); + }; + + $scope.newInterpreterGroupChange = function() { + $scope.newInterpreterSetting.properties = $scope.availableInterpreters[$scope.newInterpreterSetting.group].properties; + }; + + $scope.restartInterpreterSetting = function(settingId) { + var result = confirm('Do you want to restart this interpreter?'); + if (!result) { + return; + } + + $http.put(baseUrlSrv.getRestApiBase() + '/interpreter/setting/restart/' + settingId). + success(function(data, status, headers, config) { + var index = _.findIndex($scope.interpreterSettings, { 'id': settingId }); + $scope.interpreterSettings[index] = data.body; + }). + error(function(data, status, headers, config) { + console.log('Error %o %o', status, data.message); + }); + }; + + $scope.addNewInterpreterSetting = function() { + if (!$scope.newInterpreterSetting.name || !$scope.newInterpreterSetting.group) { + alert('Please determine name and interpreter'); + return; + } + + if (_.findIndex($scope.interpreterSettings, { 'name': $scope.newInterpreterSetting.name }) >= 0) { + alert('Name ' + $scope.newInterpreterSetting.name + ' already exists'); + return; + } + + var newSetting = angular.copy($scope.newInterpreterSetting); + + for (var p in $scope.newInterpreterSetting.properties) { + newSetting.properties[p] = $scope.newInterpreterSetting.properties[p].value; + } + + $http.post(baseUrlSrv.getRestApiBase()+'/interpreter/setting', newSetting). + success(function(data, status, headers, config) { + $scope.resetNewInterpreterSetting(); + getInterpreterSettings(); + $scope.showAddNewSetting = false; + }). + error(function(data, status, headers, config) { + console.log('Error %o %o', status, data.message); + }); + }; + + + $scope.resetNewInterpreterSetting = function() { + $scope.newInterpreterSetting = { + name : undefined, + group : undefined, + option : { remote : true }, + properties : {} + }; + emptyNewProperty($scope.newInterpreterSetting); + }; + + $scope.removeInterpreterProperty = function(key, settingId) { + if (settingId === undefined) { + delete $scope.newInterpreterSetting.properties[key]; + } + else { + var index = _.findIndex($scope.interpreterSettings, { 'id': settingId }); + delete $scope.interpreterSettings[index].properties[key]; + } + }; + + $scope.addNewInterpreterProperty = function(settingId) { + if(settingId === undefined) { + // Add new property from create form + if (!$scope.newInterpreterSetting.propertyKey || $scope.newInterpreterSetting.propertyKey === '') { + return; + } + $scope.newInterpreterSetting.properties[$scope.newInterpreterSetting.propertyKey] = $scope.newInterpreterSetting.propertyValue; + emptyNewProperty($scope.newInterpreterSetting); + } + else { + // Add new property from create form + var index = _.findIndex($scope.interpreterSettings, { 'id': settingId }); + var setting = $scope.interpreterSettings[index]; + + setting.properties[setting.propertyKey] = setting.propertyValue; + emptyNewProperty(setting); + } + }; + + var init = function() { + $scope.resetNewInterpreterSetting(); + getInterpreterSettings(); + getAvailableInterpreters(); + }; + + init(); +}); http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/8c7424a1/zeppelin-web/src/app/interpreter/interpreter.css ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/interpreter/interpreter.css b/zeppelin-web/src/app/interpreter/interpreter.css new file mode 100644 index 0000000..1dcc52b --- /dev/null +++ b/zeppelin-web/src/app/interpreter/interpreter.css @@ -0,0 +1,79 @@ +/* + * 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. + */ + +.interpreterHead { + margin-left: -10px; + margin-right: -10px; + margin-top: -10px; + margin-bottom: 20px; + padding: 10px 15px 15px 15px; + background-color: white; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15); + border-bottom: 1px solid #E5E5E5; +} + +.interpreterHead .header { + font-family: 'Roboto', sans-serif; +} + +.interpreterHead textarea, .interpreter textarea { + width: 100%; + display: block; + height: 20px; + resize: none; + border: 1px solid #CCCCCC; + font-size: 12px; +} + +.interpreter .interpreter-title { + font-size:20px; + font-weight:bold; + color:#3071a9; + float:left; + margin-top:0px; +} + +.interpreter ul { + margin: 0px 0px 0px 0px; + padding: 0px 0px 0px 0px; +} + +.interpreter .interpreterInfo { + list-style-type: none; +} + + +.interpreter table tr .interpreterPropertyKey { + padding : 5px 5px 5px 5px; +} + +.interpreter table tr .interpreterPropertyValue { + padding : 5px 5px 5px 5px; + display: block; + max-height: 100px; + overflow-y: auto; +} + +.interpreter table tr { + height : 45px; +} + +.interpreterSettingAdd { + margin : 5px 5px 5px 5px; + padding : 10px 10px 10px 10px; +} + +.editable-wrap { + width : 100%; +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/8c7424a1/zeppelin-web/src/app/interpreter/interpreter.html ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/interpreter/interpreter.html b/zeppelin-web/src/app/interpreter/interpreter.html new file mode 100644 index 0000000..69b4766 --- /dev/null +++ b/zeppelin-web/src/app/interpreter/interpreter.html @@ -0,0 +1,119 @@ +<!-- +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="interpreterHead"> + <div class="header"> + <div class="row"> + <div class="col-md-12"> + <h3 class="new_h3" style="float:left"> + Interpreters + </h3> + <span class="btn btn-default fa fa-plus" + ng-click="showAddNewSetting = !showAddNewSetting" + style="float:right;margin-top:10px;"> + Create + </span> + </div> + </div> + <div class="row"> + <div class="col-md-12"> + Manage interpreters settings. You can create create / remove settings. Note can bind/unbind these interpreter settings. + </div> + </div> + </div> + <div ng-include src="'app/interpreter/interpreter-create/interpreter-create.html'"></div> +</div> + +<div class="box width-full home" + ng-repeat="setting in interpreterSettings"> + <div> + <div class="row interpreter"> + <div class="col-md-12"> + <h3 class="interpreter-title">{{setting.name}} + <small> + <span ng-repeat="interpreter in setting.interpreterGroup" + title="{{interpreter.class}}"> + <span ng-show="!$first">, </span> + %{{interpreter.name}} + </span> + </small> + </h3> + <span style="float:right"> + <button class="btn btn-default btn-xs" + ng-click="valueform.$show(); + copyOriginInterpreterSettingProperties(setting.id)"> + <span class="fa fa-pencil"></span> edit</button> + <button class="btn btn-default btn-xs" + ng-click="restartInterpreterSetting(setting.id)"> + <span class="fa fa-refresh"></span> restart</button> + <button class="btn btn-default btn-xs" + ng-click="removeInterpreterSetting(setting.id)"> + <span class="fa fa-remove"></span> remove</button> + </span> + </div> + </div> + <br /> + <div class="row interpreter"> + <div class="col-md-12"> + <b>Properties</b> + <table class="table table-striped"> + <tr> + <th style="width:30%">name</th> + <th>value</th> + <th ng-if="valueform.$visible">action</th> + </tr> + <tr ng-repeat="(key, value) in setting.properties"> + <td>{{key}}</td> + <td> + <span editable-textarea="setting.properties[key]" e-form="valueform" e-msd-elastic> + {{value | breakFilter}} + </span> + </td> + <td ng-if="valueform.$visible"> + <div class="btn btn-default btn-sm fa fa-remove" + ng-click="removeInterpreterProperty(key, setting.id)"> + </div> + </td> + </tr> + <tr ng-if="valueform.$visible"> + <td> + <input ng-model="setting.propertyKey" + pu-elastic-input + pu-elastic-input-minwidth="180px"> + </input> + </td> + <td> + <textarea msd-elastic ng-model="setting.propertyValue"></textarea> + </td> + <td> + <div class="btn btn-default btn-sm fa fa-plus" + ng-click="addNewInterpreterProperty(setting.id)"> + </div> + </td> + </tr> + </table> + <form editable-form name="valueform" onaftersave="updateInterpreterSetting(setting.id)" ng-show="valueform.$visible"> + <button type="submit" class="btn btn-primary" + ng-disabled="valueform.$waiting"> + Save + </button> + <button type="button" class="btn btn-default" + ng-disabled="valueform.$waiting" + ng-click="valueform.$cancel(); resetInterpreterSetting(setting.id)"> + Cancel + </button> + </form> + </div> + </div> + </div> +</div> http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/8c7424a1/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 new file mode 100644 index 0000000..0d01c37 --- /dev/null +++ b/zeppelin-web/src/app/notebook/notebook.controller.js @@ -0,0 +1,473 @@ +/* global confirm:false, alert:false */ +/* jshint loopfunc: true */ +/* + * 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('NotebookCtrl', function($scope, $route, $routeParams, $location, $rootScope, $http, websocketMsgSrv, baseUrlSrv) { + $scope.note = null; + $scope.showEditor = false; + $scope.editorToggled = false; + $scope.tableToggled = false; + $scope.viewOnly = false; + $scope.looknfeelOption = [ 'default', 'simple', 'report']; + $scope.cronOption = [ + {name: 'None', value : undefined}, + {name: '1m', value: '0 0/1 * * * ?'}, + {name: '5m', value: '0 0/5 * * * ?'}, + {name: '1h', value: '0 0 0/1 * * ?'}, + {name: '3h', value: '0 0 0/3 * * ?'}, + {name: '6h', value: '0 0 0/6 * * ?'}, + {name: '12h', value: '0 0 0/12 * * ?'}, + {name: '1d', value: '0 0 0 * * ?'} + ]; + + $scope.interpreterSettings = []; + $scope.interpreterBindings = []; + + var angularObjectRegistry = {}; + + $scope.getCronOptionNameFromValue = function(value) { + if (!value) { + return ''; + } + + for (var o in $scope.cronOption) { + if ($scope.cronOption[o].value===value) { + return $scope.cronOption[o].name; + } + } + return value; + }; + + /** Init the new controller */ + var initNotebook = function() { + websocketMsgSrv.getNotebook($routeParams.noteId); + }; + + initNotebook(); + + /** Remove the note and go back tot he main page */ + /** TODO(anthony): In the nearly future, go back to the main page and telle to the dude that the note have been remove */ + $scope.removeNote = function(noteId) { + var result = confirm('Do you want to delete this notebook?'); + if (result) { + websocketMsgSrv.deleteNotebook(noteId); + $location.path('/#'); + } + }; + + $scope.runNote = function() { + var result = confirm('Run all paragraphs?'); + if (result) { + $scope.$broadcast('runParagraph'); + } + }; + + $scope.toggleAllEditor = function() { + if ($scope.editorToggled) { + $scope.$broadcast('closeEditor'); + } else { + $scope.$broadcast('openEditor'); + } + $scope.editorToggled = !$scope.editorToggled; + }; + + $scope.showAllEditor = function() { + $scope.$broadcast('openEditor'); + }; + + $scope.hideAllEditor = function() { + $scope.$broadcast('closeEditor'); + }; + + $scope.toggleAllTable = function() { + if ($scope.tableToggled) { + $scope.$broadcast('closeTable'); + } else { + $scope.$broadcast('openTable'); + } + $scope.tableToggled = !$scope.tableToggled; + }; + + $scope.showAllTable = function() { + $scope.$broadcast('openTable'); + }; + + $scope.hideAllTable = function() { + $scope.$broadcast('closeTable'); + }; + + $scope.isNoteRunning = function() { + var running = false; + if(!$scope.note){ return false; } + for (var i=0; i<$scope.note.paragraphs.length; i++) { + if ( $scope.note.paragraphs[i].status === 'PENDING' || $scope.note.paragraphs[i].status === 'RUNNING') { + running = true; + break; + } + } + return running; + }; + + $scope.setLookAndFeel = function(looknfeel) { + $scope.note.config.looknfeel = looknfeel; + $scope.setConfig(); + }; + + /** Set cron expression for this note **/ + $scope.setCronScheduler = function(cronExpr) { + $scope.note.config.cron = cronExpr; + $scope.setConfig(); + }; + + /** Update note config **/ + $scope.setConfig = function(config) { + if(config) { + $scope.note.config = config; + } + websocketMsgSrv.updateNotebook($scope.note.id, $scope.note.name, $scope.note.config); + }; + + /** Update the note name */ + $scope.sendNewName = function() { + $scope.showEditor = false; + if ($scope.note.name) { + websocketMsgSrv.updateNotebook($scope.note.id, $scope.note.name, $scope.note.config); + } + }; + + /** update the current note */ + $scope.$on('setNoteContent', function(event, note) { + $scope.paragraphUrl = $routeParams.paragraphId; + $scope.asIframe = $routeParams.asIframe; + if ($scope.paragraphUrl) { + note = cleanParagraphExcept($scope.paragraphUrl, note); + $rootScope.$broadcast('setIframe', $scope.asIframe); + } + + if ($scope.note === null) { + $scope.note = note; + } else { + updateNote(note); + } + initializeLookAndFeel(); + //open interpreter binding setting when there're none selected + getInterpreterBindings(getInterpreterBindingsCallBack); + }); + + + var initializeLookAndFeel = function() { + if (!$scope.note.config.looknfeel) { + $scope.note.config.looknfeel = 'default'; + } else { + $scope.viewOnly = $scope.note.config.looknfeel === 'report' ? true : false; + } + $rootScope.$broadcast('setLookAndFeel', $scope.note.config.looknfeel); + }; + + var cleanParagraphExcept = function(paragraphId, note) { + var noteCopy = {}; + noteCopy.id = note.id; + noteCopy.name = note.name; + noteCopy.config = note.config; + noteCopy.info = note.info; + noteCopy.paragraphs = []; + for (var i=0; i<note.paragraphs.length; i++) { + if (note.paragraphs[i].id === paragraphId) { + noteCopy.paragraphs[0] = note.paragraphs[i]; + if (!noteCopy.paragraphs[0].config) { + noteCopy.paragraphs[0].config = {}; + } + noteCopy.paragraphs[0].config.editorHide = true; + noteCopy.paragraphs[0].config.tableHide = false; + break; + } + } + return noteCopy; + }; + + $scope.$on('moveParagraphUp', function(event, paragraphId) { + var newIndex = -1; + for (var i=0; i<$scope.note.paragraphs.length; i++) { + if ($scope.note.paragraphs[i].id === paragraphId) { + newIndex = i-1; + break; + } + } + + if (newIndex<0 || newIndex>=$scope.note.paragraphs.length) { + return; + } + websocketMsgSrv.moveParagraph(paragraphId, newIndex); + }); + + // create new paragraph on current position + $scope.$on('insertParagraph', function(event, paragraphId) { + var newIndex = -1; + for (var i=0; i<$scope.note.paragraphs.length; i++) { + if ($scope.note.paragraphs[i].id === paragraphId) { + newIndex = i+1; + break; + } + } + + if (newIndex === $scope.note.paragraphs.length) { + alert('Cannot insert after the last paragraph.'); + return; + } + if (newIndex < 0 || newIndex > $scope.note.paragraphs.length) { + return; + } + websocketMsgSrv.insertParagraph(newIndex); + }); + + $scope.$on('moveParagraphDown', function(event, paragraphId) { + var newIndex = -1; + for (var i=0; i<$scope.note.paragraphs.length; i++) { + if ($scope.note.paragraphs[i].id === paragraphId) { + newIndex = i+1; + break; + } + } + + if (newIndex<0 || newIndex>=$scope.note.paragraphs.length) { + return; + } + websocketMsgSrv.moveParagraph(paragraphId, newIndex); + }); + + $scope.$on('moveFocusToPreviousParagraph', function(event, currentParagraphId){ + var focus = false; + for (var i=$scope.note.paragraphs.length-1; i>=0; i--) { + if (focus === false ) { + if ($scope.note.paragraphs[i].id === currentParagraphId) { + focus = true; + continue; + } + } else { + var p = $scope.note.paragraphs[i]; + if (!p.config.hide && !p.config.editorHide && !p.config.tableHide) { + $scope.$broadcast('focusParagraph', $scope.note.paragraphs[i].id); + break; + } + } + } + }); + + $scope.$on('moveFocusToNextParagraph', function(event, currentParagraphId){ + var focus = false; + for (var i=0; i<$scope.note.paragraphs.length; i++) { + if (focus === false ) { + if ($scope.note.paragraphs[i].id === currentParagraphId) { + focus = true; + continue; + } + } else { + var p = $scope.note.paragraphs[i]; + if (!p.config.hide && !p.config.editorHide && !p.config.tableHide) { + $scope.$broadcast('focusParagraph', $scope.note.paragraphs[i].id); + break; + } + } + } + }); + + var updateNote = function(note) { + /** update Note name */ + if (note.name !== $scope.note.name) { + console.log('change note name: %o to %o', $scope.note.name, note.name); + $scope.note.name = note.name; + } + + $scope.note.config = note.config; + $scope.note.info = note.info; + + var newParagraphIds = note.paragraphs.map(function(x) {return x.id;}); + var oldParagraphIds = $scope.note.paragraphs.map(function(x) {return x.id;}); + + var numNewParagraphs = newParagraphIds.length; + var numOldParagraphs = oldParagraphIds.length; + + /** add a new paragraph */ + if (numNewParagraphs > numOldParagraphs) { + for (var index in newParagraphIds) { + if (oldParagraphIds[index] !== newParagraphIds[index]) { + $scope.note.paragraphs.splice(index, 0, note.paragraphs[index]); + break; + } + } + } + + /** update or move paragraph */ + if (numNewParagraphs === numOldParagraphs) { + for (var idx in newParagraphIds) { + var newEntry = note.paragraphs[idx]; + if (oldParagraphIds[idx] === newParagraphIds[idx]) { + $scope.$broadcast('updateParagraph', {paragraph: newEntry}); + } else { + // move paragraph + var oldIdx = oldParagraphIds.indexOf(newParagraphIds[idx]); + $scope.note.paragraphs.splice(oldIdx, 1); + $scope.note.paragraphs.splice(idx, 0, newEntry); + // rebuild id list since paragraph has moved. + oldParagraphIds = $scope.note.paragraphs.map(function(x) {return x.id;}); + } + } + } + + /** remove paragraph */ + if (numNewParagraphs < numOldParagraphs) { + for (var oldidx in oldParagraphIds) { + if(oldParagraphIds[oldidx] !== newParagraphIds[oldidx]) { + $scope.note.paragraphs.splice(oldidx, 1); + break; + } + } + } + }; + + var getInterpreterBindings = function(callback) { + $http.get(baseUrlSrv.getRestApiBase()+ '/notebook/interpreter/bind/' +$scope.note.id). + success(function(data, status, headers, config) { + $scope.interpreterBindings = data.body; + $scope.interpreterBindingsOrig = jQuery.extend(true, [], $scope.interpreterBindings); // to check dirty + if (callback) { + callback(); + } + }). + error(function(data, status, headers, config) { + console.log('Error %o %o', status, data.message); + }); + }; + + var getInterpreterBindingsCallBack = function() { + var selected = false; + for (var i in $scope.interpreterBindings) { + var setting = $scope.interpreterBindings[i]; + if (setting.selected) { + selected = true; + break; + } + } + + if (!selected) { + // make default selection + var selectedIntp = {}; + for (var i in $scope.interpreterBindings) { + var setting = $scope.interpreterBindings[i]; + if (!selectedIntp[setting.group]) { + setting.selected = true; + selectedIntp[setting.group] = true; + } + } + $scope.showSetting = true; + } + }; + + $scope.interpreterSelectionListeners = { + accept : function(sourceItemHandleScope, destSortableScope) {return true;}, + itemMoved: function (event) {}, + orderChanged: function(event) {} + }; + + $scope.openSetting = function() { + $scope.showSetting = true; + getInterpreterBindings(); + }; + + $scope.closeSetting = function() { + if (isSettingDirty()) { + var result = confirm('Changes will be discarded'); + if (!result) { + return; + } + } + $scope.showSetting = false; + }; + + $scope.saveSetting = function() { + var selectedSettingIds = []; + for (var no in $scope.interpreterBindings) { + var setting = $scope.interpreterBindings[no]; + if (setting.selected) { + selectedSettingIds.push(setting.id); + } + } + + $http.put(baseUrlSrv.getRestApiBase() + '/notebook/interpreter/bind/' + $scope.note.id, + selectedSettingIds). + success(function(data, status, headers, config) { + console.log('Interpreter binding %o saved', selectedSettingIds); + $scope.showSetting = false; + }). + error(function(data, status, headers, config) { + console.log('Error %o %o', status, data.message); + }); + }; + + $scope.toggleSetting = function() { + if ($scope.showSetting) { + $scope.closeSetting(); + } else { + $scope.openSetting(); + } + }; + + var isSettingDirty = function() { + if (angular.equals($scope.interpreterBindings, $scope.interpreterBindingsOrig)) { + return false; + } else { + return true; + } + }; + + $scope.$on('angularObjectUpdate', function(event, data) { + if (data.noteId === $scope.note.id) { + var scope = $rootScope.compiledScope; + var varName = data.angularObject.name; + + if (angular.equals(data.angularObject.object, scope[varName])) { + // return when update has no change + return; + } + + if (!angularObjectRegistry[varName]) { + angularObjectRegistry[varName] = { + interpreterGroupId : data.interpreterGroupId, + }; + } + + angularObjectRegistry[varName].skipEmit = true; + + if (!angularObjectRegistry[varName].clearWatcher) { + angularObjectRegistry[varName].clearWatcher = scope.$watch(varName, function(newValue, oldValue) { + if (angularObjectRegistry[varName].skipEmit) { + angularObjectRegistry[varName].skipEmit = false; + return; + } + websocketMsgSrv.updateAngularObject($routeParams.noteId, varName, newValue, angularObjectRegistry[varName].interpreterGroupId); + }); + } + scope[varName] = data.angularObject.object; + } + + }); + + var isFunction = function(functionToCheck) { + var getType = {}; + return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]'; + }; + +}); http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/8c7424a1/zeppelin-web/src/app/notebook/notebook.css ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/notebook/notebook.css b/zeppelin-web/src/app/notebook/notebook.css new file mode 100644 index 0000000..477e7a1 --- /dev/null +++ b/zeppelin-web/src/app/notebook/notebook.css @@ -0,0 +1,517 @@ +/* + * 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. + */ + +.paragraph-col { + margin: 0 0 0 0px; + padding: 0 0 0 0px; +} + +.paragraph { + padding: 2px 8px 4px 8px; + min-height: 32px; +} + +.paragraphForm.form-horizontal .form-group { + margin-right: 0px; + margin-left: 0px; +} + +.paragraphForm.form-horizontal .form-group label { + padding-left: 0; +} + +.paragraph .tableDisplay .hljs { + background: none; +} + +.paragraph .ace_print-margin { + background: none !important; +} + +.paragraphAsIframe{ + padding: 0px 0px 0px 0px; + margin-top: -79px; + margin-left: -10px; + margin-right: -10px; +} + +.paragraphAsIframe .control { + background-color: rgba(255,255,255,0.9); + border-top: 1px solid #EFEFEF; + display: none; + float: right; + color: #999; + margin-top: -9px; + margin-right:0px; + position:absolute; + clear:both; + right:25px; + /*z-index:10;*/ +} + +.paragraphAsIframe table { + margin-bottom: 0px; +} + +.paragraphAsIframe .editor { + width: 100%; + border-left: 4px solid #EEEEEE; + background: rgba(255, 255, 255, 0.9); +} + +.paragraphAsIframe .text { + white-space: pre; + display: block; + unicode-bidi: embed; + display: block !important; + margin: 0 0 10px!important; + font-size: 12px!important; + line-height: 1.42857143!important; + word-break: break-all!important; + word-wrap: break-word!important; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace; +} + +.ace_marker-layer .ace_selection { + z-index: 0 !important; +} + +.ace_marker-layer .ace_selected-word { + z-index: 0 !important; +} + +.labelBtn { + padding: .2em .6em .3em; + font-size: 75%; + font-weight: bold; + line-height: 1; + color: #fff; + text-align: center; + white-space: nowrap; + border-radius: .25em; +} + +.note-jump { + margin-top: 20px; +} + +.noteBtnfa { + margin-left: 3px; +} + +.control span { + margin-left: 4px; +} + +.control { + padding: 4px; +} + +.paragraph-space { + margin-bottom: 5px; + padding: 10px !important; +} + +.paragraph .control { + background-color: rgba(255,255,255,0.85); + /*display: none;*/ + float: right; + color: #999; + margin-top: 1px; + margin-right: 5px; + position:absolute; + clear:both; + right:15px; + top: 16px; + text-align:right; + font-size:12px; +} + +.paragraph .control li{ + font-size:12px; + margin-bottom:4px; + color: #333333; +} + +.paragraph table { + margin-bottom: 0px; +} + +.paragraph .title { + margin: 3px 0px 0px 0px; + height: 20px; + font-size: 12px; +} + +.paragraph .title div { + width: 80%; + font-weight: bold; + font-family: 'Roboto', sans-serif; + font-size: 17px !important; + text-transform: capitalize; +} + +.paragraph .title input { + width: 80%; + line-height: 1.42857143; + color: #555; + background-color: #fff; + background-image: none; + border: 1px solid #ccc; + border-radius: 0px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + -webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s; + -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; + transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; + text-transform: capitalize; + font-family: 'Roboto', sans-serif; + font-size: 14px!important; +} + +.paragraph .editor { + width: 100%; + border-left: 4px solid #DDDDDD; + background: rgba(255, 255, 255, 0.0); + margin: 7px 0 2px 0px; +} + +.paragraph .text { + white-space: pre; + display: block; + unicode-bidi: embed; + display: block !important; + margin: 0 0 0px !important; + line-height: 1.42857143 !important; + word-break: break-all !important; + word-wrap: break-word !important; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace; + font-size: 12px !important; + margin-bottom: 5px !important; +} + +.paragraph p { + margin : 0 0 0 0px; +} + +.paragraph div svg { + width : 100%; +} + +.ace-tm { + background-color: #FFFFFF; + color: black; +} +.ace_editor { + position: relative; + overflow: hidden; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace; + font-size: 12px; + line-height: normal; + direction: ltr; +} + +.ace_hidden-cursors {opacity:0} + +/** Remove z-index to ace */ +.ace_text-input, .ace_gutter, .ace_layer, +.emacs-mode, .ace_text-layer, .ace_cursor-layer, +.ace_cursor, .ace_scrollbar { + z-index:auto !important; +} + +/** Force opacity:0 to textarea in writing texts **/ +.ace_text-input.ace_composition { + opacity: 0 !important; +} + +#main .emacs-mode .ace_cursor { + background-color:#C0C0C0!important; + border: none !important; +} + +.paragraph .status { + font-size: 10px; + color: #AAAAAA; + text-align: right; + visibility: hidden; +} + +.paragraph .runControl { + font-size: 1px; + color: #AAAAAA; + height:4px; + margin: 1px 0px 0px 0px; +} + +.paragraph .runControl .progress { + position: relative; + width:100%; + height:4px; + z-index:100; + border-radius: 0px; +} + +.paragraph .runControl .progress .progress-bar { + z-index:100; +} + +.paragraph .executionTime { + color: #999; + font-size: 10px; + font-family: 'Roboto', sans-serif; +} + + +.disable { + opacity:0.6!important; + pointer-events: none; +} + +.noteAction { + margin-left: -10px; + margin-right: -10px; + margin-top: -10px; + font-family: 'Roboto', sans-serif; +} + +.noteAction li{ + font-size:12px; + margin-bottom:4px; + color: #333333; +} + +.new_h3 { + margin-top: 1px; + padding-top: 7px; +} + +.form-control2 { + width: 100%; + margin-left: 15px; + font-size: 29px; + line-height: 1.42857143; + color: #555; + background-color: #fff; + background-image: none; + border: 1px solid #ccc; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + -webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s; + -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; + transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; +} + +.form-control-static2 { + padding-top: 7px; + font-size: 29px; + margin-left: 15px; + padding-bottom: 7px; + margin-bottom: 0; + display: inline-block; +} + +/* panel default */ +.panel-default { + /* border: none; */ + border-color: #DDDDDD; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15); +} + +.panel-group .panel-default { + /*border: solid #e5e5e5; + border-width: 1px 1px 2px;*/ +} + +.panel-default > .panel-heading { + color: #34495e; + background-color: #ffffff; + border-color: #e5e5e5; +} + +.panel-default > .panel-heading + .panel-collapse .panel-body { + border-top-color: #e5e5e5; +} + +.panel-default > .panel-heading > .dropdown .caret { + border-color: #ecf0f1 transparent; +} + +.panel-default > .panel-footer + .panel-collapse .panel-body { + border-bottom-color: #e5e5e5; +} + +.panel-body-heading { + position: relative; + display: block; + padding-left: 10px; + padding-top: 15px; + padding-bottom: 15px; +} + +.paragraph-margin { + margin-right: 2px; + margin-left: 2px; +} + +.tableDisplay img { + display: block; + max-width: 100%; + height: auto; +} + +.tableDisplay .btn-group span { + margin: 10px 0px 0px 10px; + font-size:12px; +} +.tableDisplay .btn-group span a { + cursor:pointer; +} + +.tableDisplay .option { + padding: 5px 5px 5px 5px; + font-size:12px; + height:auto; + overflow : auto; + /*min-height: 200px;*/ + border-top: 1px solid #ecf0f1; + +} + +.tableDisplay .option .columns { + height: 100%; +} + +.tableDisplay .option .columns ul { + + background-color: white; + /*min-width: 100px;*/ + width:auto; + padding: 3px 3px 3px 3px; + height : 150px; + border: 1px solid #CCC; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + -webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s; + -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; + transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; + +} + +.tableDisplay .option .columns ul li { + margin: 3px 3px 3px 3px; +} + +.tableDisplay .option .columns ul li span { + cursor: pointer; + margin-left: 3px; + margin-top: 3px; +} + +.tableDisplay .option .columns ul li div ul { /* aggregation menu */ + width:auto; + height:auto; +} + +.tableDisplay .option .columns ul li div ul li a { + padding: 0px; + margin: 0px; + cursor: pointer; +} + +.tableDisplay .option .columns a:focus, +.tableDisplay .option .columns a:hover { + text-decoration: none; + outline: 0; + outline-offset: 0px; +} + +.graphContainer { + position:relative; + margin-bottom: 5px; + overflow: visible; +} + +.noOverflow { + overflow: hidden !important; +} + +.graphContainer .table { + overflow: hidden; + margin-bottom: 5px !important; +} + +.allFields { + margin-bottom: 10px; +} +.noDot { + list-style-type: none; + padding-left:5px; +} + +.liVertical { + display:block; + float:left; + padding: 5px; +} +.row { + margin-right: 0px !important +} + +.lightBold { + font-weight: 500; +} + +.resizable-helper { + border: 3px solid #DDDDDD; +} + +/* note setting panel */ +.setting { + background-color: white; + padding: 10px 15px 15px 15px; + margin-left: -10px; + margin-right: -10px; + font-family: 'Roboto', sans-serif; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15); + border-bottom: 1px solid #E5E5E5; +} + +.setting .interpreterSettings { + list-style-type: none; + background-color: #EFEFEF; + padding: 10px 10px 10px 10px; + box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.15); + border: 1px solid #E5E5E5; +} + +.setting .interpreterSettings div div { + margin: 2px 0px 2px 0px; +} + +.setting .interpreterSettings div div { + cursor: pointer; +} + +.setting .modal-header { + border: 0px; +} + +.setting .modal-body { + border: 0px; +} + +.setting .modal-footer { + border: 0px; +} http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/8c7424a1/zeppelin-web/src/app/notebook/notebook.html ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/notebook/notebook.html b/zeppelin-web/src/app/notebook/notebook.html new file mode 100644 index 0000000..3a3f751 --- /dev/null +++ b/zeppelin-web/src/app/notebook/notebook.html @@ -0,0 +1,170 @@ +<!-- +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. +--> +<!-- Here the controller <NotebookCtrl> is not needed because explicitly set in the app.js (route) --> +<div> + <div class="noteAction" ng-show="note.id && !paragraphUrl"> + <h3 class="new_h3"> + <input type="text" class="form-control2" placeholder="{{note.name || 'Note ' + note.id}}" style="width:200px;" + ng-show="showEditor" ng-model="note.name" ng-enter="sendNewName()" ng-delete="showEditor = false" autofocus/> + <p class="form-control-static2" ng-click="showEditor = true" ng-show="!showEditor">{{note.name || 'Note ' + note.id}}</p> + <span class="labelBtn btn-group"> + <button type="button" + class="btn btn-default btn-xs" + ng-click="runNote()" + ng-if="!isNoteRunning()" + tooltip-placement="top" tooltip="Run all the note"> + <i class="icon-control-play"></i> + </button> + + <button type="button" + class="btn btn-default btn-xs" + ng-click="toggleAllEditor()" + ng-hide="viewOnly" + tooltip-placement="top" tooltip="Show/hide the code"> + <i ng-class="editorToggled ? 'fa icon-size-actual' : 'fa icon-size-fullscreen'"></i></button> + + <button type="button" + class="btn btn-default btn-xs" + ng-click="toggleAllTable()" + ng-hide="viewOnly" + tooltip-placement="top" tooltip="Show/hide the output"> + <i ng-class="tableToggled ? 'fa icon-notebook' : 'fa icon-book-open'"></i></button> + + <button type="button" + class="btn btn-default btn-xs" + ng-click="removeNote(note.id)" + ng-hide="viewOnly" + tooltip-placement="top" tooltip="Remove the notebook"> + <i class="icon-trash"></i></button> + </span> + + <span ng-hide="viewOnly"> + <div class="labelBtn btn-group"> + <div class="btn btn-default btn-xs dropdown-toggle" + type="button" + data-toggle="dropdown" + ng-class="{ 'btn-info' : note.config.cron, 'btn-danger' : note.info.cron, 'btn-default' : !note.config.cron}"> + <span class="fa fa-clock-o"></span> {{getCronOptionNameFromValue(note.config.cron)}} + </div> + <ul class="dropdown-menu" role="menu" style="width:300px"> + <li> + <div style="padding:10px 20px 0 20px;font-weight:normal;word-wrap:break-word"> + Run note with cron scheduler. + Either choose from<br/>preset or write your own <a href="http://www.quartz-scheduler.org/documentation/quartz-1.x/tutorials/crontrigger" target=_blank>cron expression</a>. + <br/><br/> + <span>- Preset</span> + <a ng-repeat="cr in cronOption" + type="button" + ng-click="setCronScheduler(cr.value)" + style="cursor:pointer" + dropdown-input>{{cr.name}}</a> + <br/><br/> + <span>- Cron expression</span> + <input type="text" + ng-model="note.config.cron" + ng-change="setCronScheduler(note.config.cron)" + dropdown-input> + </input> + <p ng-show="note.info.cron" + style="color:red"> + {{note.info.cron}} + </p> + </div> + </li> + </ul> + </div> + </span> + + <div class="pull-right" + style="margin-top:15px; margin-right:15px; font-size:15px;"> + <span style="position:relative; top:3px; margin-right:4px; cursor:pointer" + data-toggle="modal" + data-target="#shortcutModal" + tooltip-placement="top" tooltip="List of shortcut"> + <i class="icon-question"></i> + </span> + <span style="position:relative; top:2px; margin-right:4px; cursor:pointer;" + ng-click="toggleSetting()" + tooltip-placement="top" tooltip="Interpreter binding"> + <i class="fa fa-cog" + ng-style="{color: showSetting ? '#3071A9' : 'black' }"></i> + </span> + + <span class="btn-group"> + <button type="button" + class="btn btn-default btn-xs dropdown-toggle" + data-toggle="dropdown"> + {{note.config.looknfeel}} <span class="caret"></span> + </button> + <ul class="dropdown-menu pull-right" role="menu"> + <li ng-repeat="looknfeel in looknfeelOption"> + <a style="cursor:pointer" + ng-click="setLookAndFeel(looknfeel)">{{looknfeel}}</a> + </li> + </ul> + </span> + </div> + </h3> + </div> + + <!-- settings --> + <div ng-show="showSetting" + class="setting"> + <div> + <h4>Settings</h4> + </div> + <hr /> + <div> + <h5>Interpreter binding</h5> + <p> + Bind interpreter for this note. + Click to Bind/Unbind interpreter. + Drag and drop to reorder interpreters. <br /> + The first interpreter on the list becomes default. To create/remove interpreters, go to <a href="/#/interpreter">Interpreter</a> menu. + </p> + + <div class="interpreterSettings" + as-sortable="interpreterSelectionListeners" data-ng-model="interpreterBindings"> + <div data-ng-repeat="item in interpreterBindings" as-sortable-item> + <div as-sortable-item-handle + ng-click="item.selected = !item.selected" + class="btn" + ng-class="{'btn-info': item.selected, 'btn-default': !item.selected}"><font style="font-size:16px">{{item.name}}</font> <small><span ng-repeat="intp in item.interpreters"><span ng-show="!$first">, </span>%{{intp.name}}</span></small></div> + </div> + </div> + </div> + <br /> + <div> + <button class="btn btn-primary" ng-click="saveSetting()">Save</button> + <button class="btn btn-default" ng-click="closeSetting()">Cancel</button> + </div> + </div> + + <div class="note-jump"></div> + + <!-- Include the paragraphs according to the note --> + <div id="{{currentParagraph.id}}_paragraphColumn_main" + ng-repeat="currentParagraph in note.paragraphs" + ng-controller="ParagraphCtrl" + ng-Init="init(currentParagraph)" + ng-class="columnWidthClass(currentParagraph.config.colWidth)" + class="paragraph-col"> + <div id="{{currentParagraph.id}}_paragraphColumn" + ng-include src="'app/notebook/paragraph/paragraph.html'" + ng-class="{'paragraph-space box paragraph-margin': !asIframe, 'focused': paragraphFocused}" + ng-hide="currentParagraph.config.tableHide && viewOnly"> + </div> + </div> + <div style="clear:both;height:10px"></div> +</div>
