From 0793c109ff978b78bf50be055f0fcc6b4dc4e309 Mon Sep 17 00:00:00 2001 From: Prabhjyot Singh Date: Wed, 27 Jul 2016 19:10:24 +0530 Subject: [PATCH 1/2] Auto-suggestion of notebook permissions should list group as well --- .../org/apache/zeppelin/rest/GetUserList.java | 19 ++ .../apache/zeppelin/rest/SecurityRestApi.java | 24 +- .../integration/AuthenticationIT.java | 12 +- zeppelin-web/bower.json | 3 +- zeppelin-web/src/app/app.js | 7 + .../src/app/notebook/notebook.controller.js | 266 ++++-------------- zeppelin-web/src/app/notebook/notebook.css | 50 +--- zeppelin-web/src/app/notebook/notebook.html | 55 +--- zeppelin-web/src/index.html | 2 + zeppelin-web/test/karma.conf.js | 1 + 10 files changed, 131 insertions(+), 308 deletions(-) diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/GetUserList.java b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/GetUserList.java index 2727fb4a411..f1a895c8bcf 100644 --- a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/GetUserList.java +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/GetUserList.java @@ -67,6 +67,25 @@ public List getUserList(IniRealm r) { return userList; } + + /*** + * Get user roles from shiro.ini + * @param r + * @return + */ + public List getRolesList(IniRealm r) { + List roleList = new ArrayList<>(); + Map getIniRoles = r.getIni().get("roles"); + if (getIniRoles != null) { + Iterator it = getIniRoles.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry pair = (Map.Entry) it.next(); + roleList.add(pair.getKey().toString().trim()); + } + } + return roleList; + } + /** * function to extract users from LDAP */ diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/SecurityRestApi.java b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/SecurityRestApi.java index a079a4460c9..d0b2c52e367 100644 --- a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/SecurityRestApi.java +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/SecurityRestApi.java @@ -18,6 +18,7 @@ package org.apache.zeppelin.rest; +import org.apache.commons.lang3.StringUtils; import org.apache.shiro.realm.Realm; import org.apache.shiro.realm.jdbc.JdbcRealm; import org.apache.shiro.realm.ldap.AbstractLdapRealm; @@ -29,6 +30,7 @@ import org.apache.zeppelin.server.JsonResponse; import org.apache.zeppelin.ticket.TicketContainer; import org.apache.zeppelin.utils.SecurityUtils; +import org.eclipse.jetty.util.StringUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -98,6 +100,7 @@ public Response ticket() { public Response getUserList(@PathParam("searchText") final String searchText) { List usersList = new ArrayList<>(); + List rolesList = new ArrayList<>(); try { GetUserList getUserListObj = new GetUserList(); Collection realmsList = SecurityUtils.getRealmsList(); @@ -107,6 +110,7 @@ public Response getUserList(@PathParam("searchText") final String searchText) { String name = realm.getName(); if (name.equals("iniRealm")) { usersList.addAll(getUserListObj.getUserList((IniRealm) realm)); + rolesList.addAll(getUserListObj.getRolesList((IniRealm) realm)); } else if (name.equals("ldapRealm")) { usersList.addAll(getUserListObj.getUserList((JndiLdapRealm) realm, searchText)); } else if (name.equals("activeDirectoryRealm")) { @@ -120,8 +124,10 @@ public Response getUserList(@PathParam("searchText") final String searchText) { } catch (Exception e) { LOG.error("Exception in retrieving Users from realms ", e); } - List autoSuggestList = new ArrayList<>(); + List autoSuggestUserList = new ArrayList<>(); + List autoSuggestRoleList = new ArrayList<>(); Collections.sort(usersList); + Collections.sort(rolesList); Collections.sort(usersList, new Comparator() { @Override public int compare(String o1, String o2) { @@ -139,13 +145,25 @@ public int compare(String o1, String o2) { String searchTextLowerCase = searchText.toLowerCase(); if (userLowerCase.indexOf(searchTextLowerCase) != -1) { maxLength++; - autoSuggestList.add(usersList.get(i)); + autoSuggestUserList.add(usersList.get(i)); } if (maxLength == 5) { break; } } - return new JsonResponse<>(Response.Status.OK, "", autoSuggestList).build(); + + for (String role : rolesList) { + if (StringUtils.startsWithIgnoreCase(role, searchText)) { + autoSuggestRoleList.add(role); + } + } + + Map returnListMap = new HashMap<>(); + returnListMap.put("users", autoSuggestUserList); + returnListMap.put("roles", autoSuggestRoleList); + + + return new JsonResponse<>(Response.Status.OK, "", returnListMap).build(); } } diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/integration/AuthenticationIT.java b/zeppelin-server/src/test/java/org/apache/zeppelin/integration/AuthenticationIT.java index 671b213b471..ea810cbd05e 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/integration/AuthenticationIT.java +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/integration/AuthenticationIT.java @@ -161,12 +161,12 @@ public void testGroupPermission() throws Exception { pollingWait(By.xpath("//span[@tooltip='Note permissions']"), MAX_BROWSER_TIMEOUT_SEC).click(); - pollingWait(By.xpath("//input[@ng-model='permissions.owners']"), MAX_BROWSER_TIMEOUT_SEC) - .sendKeys("finance"); - pollingWait(By.xpath("//input[@ng-model='permissions.readers']"), MAX_BROWSER_TIMEOUT_SEC) - .sendKeys("finance"); - pollingWait(By.xpath("//input[@ng-model='permissions.writers']"), MAX_BROWSER_TIMEOUT_SEC) - .sendKeys("finance"); + pollingWait(By.xpath(".//*[@id='selectOwners']/following::span//input"), + MAX_BROWSER_TIMEOUT_SEC).sendKeys("finance "); + pollingWait(By.xpath(".//*[@id='selectReaders']/following::span//input"), + MAX_BROWSER_TIMEOUT_SEC).sendKeys("finance "); + pollingWait(By.xpath(".//*[@id='selectWriters']/following::span//input"), + MAX_BROWSER_TIMEOUT_SEC).sendKeys("finance "); pollingWait(By.xpath("//button[@ng-click='savePermissions()']"), MAX_BROWSER_TIMEOUT_SEC) .sendKeys(Keys.ENTER); diff --git a/zeppelin-web/bower.json b/zeppelin-web/bower.json index 5d849b3490d..ae29f70cca9 100644 --- a/zeppelin-web/bower.json +++ b/zeppelin-web/bower.json @@ -31,7 +31,8 @@ "ng-focus-if": "~1.0.2", "bootstrap3-dialog": "bootstrap-dialog#~1.34.7", "handsontable": "~0.24.2", - "moment-duration-format": "^1.3.0" + "moment-duration-format": "^1.3.0", + "select2": "^4.0.3" }, "devDependencies": { "angular-mocks": "1.5.0" diff --git a/zeppelin-web/src/app/app.js b/zeppelin-web/src/app/app.js index 98d6b879f09..20ccfb11f9e 100644 --- a/zeppelin-web/src/app/app.js +++ b/zeppelin-web/src/app/app.js @@ -98,6 +98,13 @@ var baseUrlSrv = angular.injector(['zeppelinWebApp']).get('baseUrlSrv'); // withCredentials when running locally via grunt $http.defaults.withCredentials = true; + jQuery.ajaxSetup({ + dataType: 'json', + xhrFields: { + withCredentials: true + }, + crossDomain: true + }); return $http.get(baseUrlSrv.getRestApiBase() + '/security/ticket').then(function(response) { zeppelinWebApp.run(function($rootScope) { $rootScope.ticket = angular.fromJson(response.data).body; diff --git a/zeppelin-web/src/app/notebook/notebook.controller.js b/zeppelin-web/src/app/notebook/notebook.controller.js index dc59f50ae71..00946dd3b64 100644 --- a/zeppelin-web/src/app/notebook/notebook.controller.js +++ b/zeppelin-web/src/app/notebook/notebook.controller.js @@ -43,16 +43,6 @@ angular.module('zeppelinWebApp').controller('NotebookCtrl', function($scope, $ro var connectedOnce = false; // user auto complete related - $scope.suggestions = []; - $scope.selectIndex = -1; - var selectedUser = ''; - var selectedUserIndex = 0; - var previousSelectedList = []; - var previousSelectedListOwners = []; - var previousSelectedListReaders = []; - var previousSelectedListWriters = []; - var searchText = []; - $scope.role = ''; $scope.noteRevisions = []; $scope.$on('setConnectedStatus', function(event, param) { @@ -556,6 +546,59 @@ angular.module('zeppelinWebApp').controller('NotebookCtrl', function($scope, $ro success(function(data, status, headers, config) { $scope.permissions = data.body; $scope.permissionsOrig = angular.copy($scope.permissions); // to check dirty + + var selectJson = { + tokenSeparators: [',', ' '], + ajax: { + url: function(params) { + return baseUrlSrv.getRestApiBase() + '/security/userlist/' + params.term; + }, + delay: 250, + processResults: function(data, params) { + var results = []; + + if (data.body.users.length !== 0) { + var users = []; + for (var len = 0; len < data.body.users.length; len++) { + users.push({ + 'id': data.body.users[len], + 'text': data.body.users[len] + }); + } + results.push({ + 'text': 'Users :', + 'children': users + }); + } + if (data.body.roles.length !== 0) { + var roles = []; + for (var len = 0; len < data.body.roles.length; len++) { + roles.push({ + 'id': data.body.roles[len], + 'text': data.body.roles[len] + }); + } + results.push({ + 'text': 'Roles :', + 'children': roles + }); + } + return { + results: results, + pagination: { + more: false + } + }; + }, + cache: false + }, + width: ' ', + tags: true + }; + + angular.element('#selectOwners').select2(selectJson); + angular.element('#selectReaders').select2(selectJson); + angular.element('#selectWriters').select2(selectJson); if (callback) { callback(); } @@ -592,15 +635,9 @@ angular.module('zeppelinWebApp').controller('NotebookCtrl', function($scope, $ro }; function convertPermissionsToArray() { - if (!angular.isArray($scope.permissions.owners)) { - $scope.permissions.owners = $scope.permissions.owners.split(','); - } - if (!angular.isArray($scope.permissions.readers)) { - $scope.permissions.readers = $scope.permissions.readers.split(','); - } - if (!angular.isArray($scope.permissions.writers)) { - $scope.permissions.writers = $scope.permissions.writers.split(','); - } + $scope.permissions.owners = angular.element('#selectOwners').val(); + $scope.permissions.readers = angular.element('#selectReaders').val(); + $scope.permissions.writers = angular.element('#selectWriters').val(); } $scope.savePermissions = function() { @@ -652,6 +689,9 @@ angular.module('zeppelinWebApp').controller('NotebookCtrl', function($scope, $ro $scope.togglePermissions = function() { if ($scope.showPermissions) { $scope.closePermissions(); + angular.element('#selectOwners').select2({}); + angular.element('#selectReaders').select2({}); + angular.element('#selectWriters').select2({}); } else { $scope.openPermissions(); $scope.closeSetting(); @@ -674,195 +714,7 @@ angular.module('zeppelinWebApp').controller('NotebookCtrl', function($scope, $ro } }; - function checkPreviousRole(role) { - var i = 0; - if (role !== $scope.role) { - if ($scope.role === 'owners') { - previousSelectedListOwners = []; - for (i = 0; i < previousSelectedList.length; i++) { - previousSelectedListOwners[i] = previousSelectedList[i]; - } - } - if ($scope.role === 'readers') { - previousSelectedListReaders = []; - for (i = 0; i < previousSelectedList.length; i++) { - previousSelectedListReaders[i] = previousSelectedList[i]; - } - } - if ($scope.role === 'writers') { - previousSelectedListWriters = []; - for (i = 0; i < previousSelectedList.length; i++) { - previousSelectedListWriters[i] = previousSelectedList[i]; - } - } - - $scope.role = role; - previousSelectedList = []; - if (role === 'owners') { - for (i = 0; i < previousSelectedListOwners.length; i++) { - previousSelectedList[i] = previousSelectedListOwners[i]; - } - } - if (role === 'readers') { - for (i = 0; i < previousSelectedListReaders.length; i++) { - previousSelectedList[i] = previousSelectedListReaders[i]; - } - } - if (role === 'writers') { - for (i = 0; i < previousSelectedListWriters.length; i++) { - previousSelectedList[i] = previousSelectedListWriters[i]; - } - } - } - } - - function convertToArray(role) { - if (!$scope.permissions) { - return; - } else if (role === 'owners' && typeof $scope.permissions.owners === 'string') { - searchText = $scope.permissions.owners.split(','); - } else if (role === 'readers' && typeof $scope.permissions.readers === 'string') { - searchText = $scope.permissions.readers.split(','); - } else if (role === 'writers' && typeof $scope.permissions.writers === 'string') { - searchText = $scope.permissions.writers.split(','); - } - - for (var i = 0; i < searchText.length; i++) { - searchText[i] = searchText[i].trim(); - } - } - - function convertToString(role) { - if (role === 'owners') { - $scope.permissions.owners = searchText.join(); - } else if (role === 'readers') { - $scope.permissions.readers = searchText.join(); - } else if (role === 'writers') { - $scope.permissions.writers = searchText.join(); - } - } - - function getSuggestions(searchQuery) { - $scope.suggestions = []; - $http.get(baseUrlSrv.getRestApiBase() + '/security/userlist/' + searchQuery).then(function - (response) { - var userlist = angular.fromJson(response.data).body; - for (var k in userlist) { - $scope.suggestions.push(userlist[k]); - } - }); - } - - function updatePreviousList() { - for (var i = 0; i < searchText.length; i++) { - previousSelectedList[i] = searchText[i]; - } - } - - var getChangedIndex = function() { - if (previousSelectedList.length === 0) { - selectedUserIndex = searchText.length - 1; - } else { - for (var i = 0; i < searchText.length; i++) { - if (previousSelectedList[i] !== searchText[i]) { - selectedUserIndex = i; - previousSelectedList = []; - break; - } - } - } - updatePreviousList(); - }; - - $scope.$watch('permissions.owners', _.debounce(function(readers) { - $scope.$apply(function() { - $scope.search('owners'); - }); - }, 350)); - - $scope.$watch('permissions.readers', _.debounce(function(readers) { - $scope.$apply(function() { - $scope.search('readers'); - }); - }, 350)); - - $scope.$watch('permissions.writers', _.debounce(function(readers) { - $scope.$apply(function() { - $scope.search('writers'); - }); - }, 350)); - - // function to find suggestion list on change - $scope.search = function(role) { - angular.element('.userlist').show(); - convertToArray(role); - checkPreviousRole(role); - getChangedIndex(); - $scope.selectIndex = -1; - $scope.suggestions = []; - selectedUser = searchText[selectedUserIndex]; - if (selectedUser !== '') { - getSuggestions(selectedUser); - } else { - $scope.suggestions = []; - } - }; - - var checkIfSelected = function() { - if (($scope.suggestions.length === 0) && - ($scope.selectIndex < 0 || $scope.selectIndex >= $scope.suggestions.length) || - ($scope.suggestions.length !== 0 && ($scope.selectIndex < 0 || $scope.selectIndex >= $scope.suggestions.length)) - ) { - searchText[selectedUserIndex] = selectedUser; - $scope.suggestions = []; - return true; - } else { - return false; - } - }; - - $scope.checkKeyDown = function(event, role) { - if (event.keyCode === 40) { - event.preventDefault(); - if ($scope.selectIndex + 1 !== $scope.suggestions.length) { - $scope.selectIndex++; - } - } else if (event.keyCode === 38) { - event.preventDefault(); - - if ($scope.selectIndex - 1 !== -1) { - $scope.selectIndex--; - - } - } else if (event.keyCode === 13) { - event.preventDefault(); - if (!checkIfSelected()) { - selectedUser = $scope.suggestions[$scope.selectIndex]; - searchText[selectedUserIndex] = $scope.suggestions[$scope.selectIndex]; - updatePreviousList(); - convertToString(role); - $scope.suggestions = []; - } - } - }; - - $scope.checkKeyUp = function(event) { - if (event.keyCode !== 8 || event.keyCode !== 46) { - if (searchText[selectedUserIndex] === '') { - $scope.suggestions = []; - } - } - }; - - $scope.assignValueAndHide = function(index, role) { - searchText[selectedUserIndex] = $scope.suggestions[index]; - updatePreviousList(); - convertToString(role); - $scope.suggestions = []; - }; - angular.element(document).click(function() { - angular.element('.userlist').hide(); angular.element('.ace_autocomplete').hide(); }); diff --git a/zeppelin-web/src/app/notebook/notebook.css b/zeppelin-web/src/app/notebook/notebook.css index ed45c67bc70..c11544fd0d2 100644 --- a/zeppelin-web/src/app/notebook/notebook.css +++ b/zeppelin-web/src/app/notebook/notebook.css @@ -308,51 +308,7 @@ cursor: default; } -.userlist { - width: 230px; - font-family: Georgia, Times, serif; - font-size: 15px; - position: absolute; - z-index: 9999; -} - -.userlist ul { - list-style: none; -} - -.userlist ul li { - box-shadow: 3px 3px 5px #888888; - display: list-item; - text-decoration: none; - color: #000000; - background-color: #FFFFFF; - line-height: 30px; - border-bottom-style: none; - border-bottom-width: 1px; - border-bottom:1px #CCCCCC solid; - padding-left: 10px; - cursor: pointer; -} - -.userlist ul li:first-child { - border-top-right-radius: 5px; - border-top-left-radius: 5px; -} - -.userlist ul li:last-child { - border-bottom-right-radius: 5px; - border-bottom-left-radius: 5px; -} - -.userlist ul li strong { - margin-right: 10px; -} - -.userlist li:hover { - background-color: #E0E0E0; -} - -.userlist li:active, -.userlist li.active { - background-color: #428BCA; +.select2-container--default{ + min-width: 150px; + max-width: 50%; } diff --git a/zeppelin-web/src/app/notebook/notebook.html b/zeppelin-web/src/app/notebook/notebook.html index fd329ac18e8..9ad71662305 100644 --- a/zeppelin-web/src/app/notebook/notebook.html +++ b/zeppelin-web/src/app/notebook/notebook.html @@ -70,57 +70,24 @@

Note Permissions (Only note owners can change)

-

Owners - - Owners can change permissions,read and write the note. +

Owners + + Owners can change permissions,read and write the note.

-
-
    -
  • - {{suggestion}} -
  • -
-

Readers - + Readers can only read the note.

-
-
    -
  • - {{suggestion}} -
  • -
-

Writers - + Writers can read and write the note.

-
-
    -
  • - {{suggestion}} -
  • -
-

diff --git a/zeppelin-web/src/index.html b/zeppelin-web/src/index.html index 9fe9489501c..ff1fa91feeb 100644 --- a/zeppelin-web/src/index.html +++ b/zeppelin-web/src/index.html @@ -48,6 +48,7 @@ + @@ -144,6 +145,7 @@ + diff --git a/zeppelin-web/test/karma.conf.js b/zeppelin-web/test/karma.conf.js index 778f0f151fd..f9f03a413fa 100644 --- a/zeppelin-web/test/karma.conf.js +++ b/zeppelin-web/test/karma.conf.js @@ -64,6 +64,7 @@ module.exports = function(config) { 'bower_components/pikaday/pikaday.js', 'bower_components/handsontable/dist/handsontable.js', 'bower_components/moment-duration-format/lib/moment-duration-format.js', + 'bower_components/select2/dist/js/select2.js', 'bower_components/angular-mocks/angular-mocks.js', // endbower 'src/app/app.js', From 17e17a9a47c5779a2e083e5a54be1cdd8ce74079 Mon Sep 17 00:00:00 2001 From: Prabhjyot Singh Date: Thu, 28 Jul 2016 00:25:31 +0530 Subject: [PATCH 2/2] implement @r-kamath feedback --- .../org/apache/zeppelin/rest/SecurityRestApi.java | 12 ++++-------- .../apache/zeppelin/rest/SecurityRestApiTest.java | 4 ++-- zeppelin-web/src/app/notebook/notebook.controller.js | 6 +++++- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/SecurityRestApi.java b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/SecurityRestApi.java index d0b2c52e367..7af52c8c8ba 100644 --- a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/SecurityRestApi.java +++ b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/SecurityRestApi.java @@ -21,7 +21,6 @@ import org.apache.commons.lang3.StringUtils; import org.apache.shiro.realm.Realm; import org.apache.shiro.realm.jdbc.JdbcRealm; -import org.apache.shiro.realm.ldap.AbstractLdapRealm; import org.apache.shiro.realm.ldap.JndiLdapRealm; import org.apache.shiro.realm.text.IniRealm; import org.apache.zeppelin.annotation.ZeppelinApi; @@ -30,7 +29,6 @@ import org.apache.zeppelin.server.JsonResponse; import org.apache.zeppelin.ticket.TicketContainer; import org.apache.zeppelin.utils.SecurityUtils; -import org.eclipse.jetty.util.StringUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -140,12 +138,10 @@ public int compare(String o1, String o2) { } }); int maxLength = 0; - for (int i = 0; i < usersList.size(); i++) { - String userLowerCase = usersList.get(i).toLowerCase(); - String searchTextLowerCase = searchText.toLowerCase(); - if (userLowerCase.indexOf(searchTextLowerCase) != -1) { + for (String user : usersList) { + if (StringUtils.containsIgnoreCase(user, searchText)) { + autoSuggestUserList.add(user); maxLength++; - autoSuggestUserList.add(usersList.get(i)); } if (maxLength == 5) { break; @@ -153,7 +149,7 @@ public int compare(String o1, String o2) { } for (String role : rolesList) { - if (StringUtils.startsWithIgnoreCase(role, searchText)) { + if (StringUtils.containsIgnoreCase(role, searchText)) { autoSuggestRoleList.add(role); } } diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/SecurityRestApiTest.java b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/SecurityRestApiTest.java index 54c31c1fd6a..b4ecd97e375 100644 --- a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/SecurityRestApiTest.java +++ b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/SecurityRestApiTest.java @@ -69,7 +69,7 @@ public void testGetUserList() throws IOException { get.addRequestHeader("Origin", "http://localhost"); Map resp = gson.fromJson(get.getResponseBodyAsString(), new TypeToken>(){}.getType()); - List userList = (List) resp.get("body"); + List userList = (List) ((Map) resp.get("body")).get("users"); collector.checkThat("Search result size", userList.size(), CoreMatchers.equalTo(1)); collector.checkThat("Search result contains admin", userList.contains("admin"), @@ -80,7 +80,7 @@ public void testGetUserList() throws IOException { notUser.addRequestHeader("Origin", "http://localhost"); Map notUserResp = gson.fromJson(notUser.getResponseBodyAsString(), new TypeToken>(){}.getType()); - List emptyUserList = (List) notUserResp.get("body"); + List emptyUserList = (List) ((Map) notUserResp.get("body")).get("users"); collector.checkThat("Search result size", emptyUserList.size(), CoreMatchers.equalTo(0)); diff --git a/zeppelin-web/src/app/notebook/notebook.controller.js b/zeppelin-web/src/app/notebook/notebook.controller.js index 00946dd3b64..bf92fb71f3f 100644 --- a/zeppelin-web/src/app/notebook/notebook.controller.js +++ b/zeppelin-web/src/app/notebook/notebook.controller.js @@ -551,6 +551,9 @@ angular.module('zeppelinWebApp').controller('NotebookCtrl', function($scope, $ro tokenSeparators: [',', ' '], ajax: { url: function(params) { + if (!params.term) { + return false; + } return baseUrlSrv.getRestApiBase() + '/security/userlist/' + params.term; }, delay: 250, @@ -593,7 +596,8 @@ angular.module('zeppelinWebApp').controller('NotebookCtrl', function($scope, $ro cache: false }, width: ' ', - tags: true + tags: true, + minimumInputLength: 3 }; angular.element('#selectOwners').select2(selectJson);