Repository: zeppelin Updated Branches: refs/heads/master d343348bd -> d87f2e5df
[ZEPPELIN-1054] Improve "Credentials" UI ### What is this PR for? Currently, users can add new their credential info for data source authentication in Zeppelin "Credentials" menu. Even though it was saved successfully, they can't see the whole list of credentials in Zeppelin UI. This PR enables to `get` all credential list, `edit` and `remove` via UI. *NOTE : Since this patch was implemented based on #1030 API, should be tested after #1030 merged.* ### What type of PR is it? Improvement & Documentation ### Todos * [x] - rename `interpreter_authorization.md` -> `datasource_authorization.md` * [x] - remove `Interpreter Authorization` section (since we don't have this feature yet : [ZEPPELIN-945](https://issues.apache.org/jira/browse/ZEPPELIN-945)) * [x] - rebase after #1030 & #1064 merged * [ ] - address reviews ### What is the Jira issue? [ZEPPELIN-1054](https://issues.apache.org/jira/browse/ZEPPELIN-1054) ### How should this be tested? 1. Apply this patch and build `zeppelin-web` as described in [here](https://github.com/apache/zeppelin/tree/master/zeppelin-web#configured-environment). 2. Go to `Credentials` menu. 3. Add new credentials -> you can see the credential info in the credential list table. 4. You can edit & delete them. -> Compare with `conf/credentials.json` ### Screenshots (if appropriate) - Before <img width="952" alt="screen shot 2016-06-28 at 12 37 10 am" src="https://cloud.githubusercontent.com/assets/10060731/16407604/69b0c4d8-3cc9-11e6-8284-9abe2969cdc1.png"> - After  If there is no credential <img width="957" alt="screen shot 2016-06-28 at 12 19 46 am" src="https://cloud.githubusercontent.com/assets/10060731/16407620/7838995e-3cc9-11e6-90ba-1bd0173a1b49.png"> - `datasource_authorization.md` <img width="845" alt="screen shot 2016-06-28 at 7 58 24 pm" src="https://cloud.githubusercontent.com/assets/10060731/16439169/d4026034-3d6a-11e6-930f-86de12e5fc49.png"> <img width="851" alt="screen shot 2016-06-28 at 7 58 44 pm" src="https://cloud.githubusercontent.com/assets/10060731/16439170/d62f2842-3d6a-11e6-9d3f-ecc5cda29c77.png"> <img width="846" alt="screen shot 2016-06-28 at 8 00 20 pm" src="https://cloud.githubusercontent.com/assets/10060731/16439200/fed58390-3d6a-11e6-9aa2-8cff5a1b7b66.png"> ### Questions: * Does the licenses files need update? no * Is there breaking changes for older versions? no * Does this needs documentation? yes Author: AhyoungRyu <[email protected]> Closes #1100 from AhyoungRyu/ZEPPELIN-1054 and squashes the following commits: 7c38c90 [AhyoungRyu] Fix checkstyle error with jscs rule ab9814c [AhyoungRyu] Remove cancelCredentialInfoUpdate() 899bb15 [AhyoungRyu] Fix a bug reported by @Leemoonsoo 57cb280 [AhyoungRyu] Make focusing to text inputbox after update cancel cea8c93 [AhyoungRyu] Fix typos in datasource_authorization.md cc72ae8 [AhyoungRyu] update xeditable license version c100a64 [AhyoungRyu] Delete interpreter_authorization.md 304e684 [AhyoungRyu] Add datasource_authorization.md docs 5768604 [AhyoungRyu] Add datasource_authorization.md to index & navi menu 64bf6fe [AhyoungRyu] Update angular-xeditable version 573c3d1 [AhyoungRyu] Enable credential info to get list, edit and remove via UI Project: http://git-wip-us.apache.org/repos/asf/zeppelin/repo Commit: http://git-wip-us.apache.org/repos/asf/zeppelin/commit/d87f2e5d Tree: http://git-wip-us.apache.org/repos/asf/zeppelin/tree/d87f2e5d Diff: http://git-wip-us.apache.org/repos/asf/zeppelin/diff/d87f2e5d Branch: refs/heads/master Commit: d87f2e5dfb3ec99c75eace7bdaa3d0211fd14c4a Parents: d343348 Author: AhyoungRyu <[email protected]> Authored: Sat Jul 9 22:48:16 2016 +0900 Committer: Damien CORNEAU <[email protected]> Committed: Mon Jul 11 18:18:42 2016 +0900 ---------------------------------------------------------------------- docs/_includes/themes/zeppelin/_navigation.html | 2 +- .../zeppelin/img/docs-img/add_credential.png | Bin 0 -> 167834 bytes .../zeppelin/img/docs-img/credential_tab.png | Bin 0 -> 30166 bytes docs/index.md | 2 +- docs/security/datasource_authorization.md | 59 +++++++ docs/security/interpreter_authorization.md | 38 ----- zeppelin-distribution/src/bin_license/LICENSE | 2 +- zeppelin-web/bower.json | 2 +- .../src/app/credential/credential.controller.js | 136 +++++++++++++--- zeppelin-web/src/app/credential/credential.html | 155 ++++++++++++++----- 10 files changed, 295 insertions(+), 101 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/zeppelin/blob/d87f2e5d/docs/_includes/themes/zeppelin/_navigation.html ---------------------------------------------------------------------- diff --git a/docs/_includes/themes/zeppelin/_navigation.html b/docs/_includes/themes/zeppelin/_navigation.html index c0f32b6..9feda62 100644 --- a/docs/_includes/themes/zeppelin/_navigation.html +++ b/docs/_includes/themes/zeppelin/_navigation.html @@ -100,7 +100,7 @@ <li><a href="{{BASE_PATH}}/security/authentication.html">Authentication for NGINX</a></li> <li><a href="{{BASE_PATH}}/security/shiroauthentication.html">Shiro Authentication</a></li> <li><a href="{{BASE_PATH}}/security/notebook_authorization.html">Notebook Authorization</a></li> - <li><a href="{{BASE_PATH}}/security/interpreter_authorization.html">Interpreter & Data Resource Authorization</a></li> + <li><a href="{{BASE_PATH}}/security/datasource_authorization.html">Data Source Authorization</a></li> <li role="separator" class="divider"></li> <li class="title"><span><b>Contibute</b><span></li> <li><a href="{{BASE_PATH}}/development/writingzeppelininterpreter.html">Writing Zeppelin Interpreter</a></li> http://git-wip-us.apache.org/repos/asf/zeppelin/blob/d87f2e5d/docs/assets/themes/zeppelin/img/docs-img/add_credential.png ---------------------------------------------------------------------- diff --git a/docs/assets/themes/zeppelin/img/docs-img/add_credential.png b/docs/assets/themes/zeppelin/img/docs-img/add_credential.png new file mode 100644 index 0000000..dcf4460 Binary files /dev/null and b/docs/assets/themes/zeppelin/img/docs-img/add_credential.png differ http://git-wip-us.apache.org/repos/asf/zeppelin/blob/d87f2e5d/docs/assets/themes/zeppelin/img/docs-img/credential_tab.png ---------------------------------------------------------------------- diff --git a/docs/assets/themes/zeppelin/img/docs-img/credential_tab.png b/docs/assets/themes/zeppelin/img/docs-img/credential_tab.png new file mode 100644 index 0000000..66a1fbe Binary files /dev/null and b/docs/assets/themes/zeppelin/img/docs-img/credential_tab.png differ http://git-wip-us.apache.org/repos/asf/zeppelin/blob/d87f2e5d/docs/index.md ---------------------------------------------------------------------- diff --git a/docs/index.md b/docs/index.md index e120e7a..141e7f6 100644 --- a/docs/index.md +++ b/docs/index.md @@ -167,7 +167,7 @@ Join to our [Mailing list](https://zeppelin.apache.org/community.html) and repor * [Authentication for NGINX](./security/authentication.html) * [Shiro Authentication](./security/shiroauthentication.html) * [Notebook Authorization](./security/notebook_authorization.html) - * [Interpreter & Data Resource Authorization](./security/interpreter_authorization.html) + * [Data Source Authorization](./security/datasource_authorization.html) * Contribute * [Writing Zeppelin Interpreter](./development/writingzeppelininterpreter.html) * [Writing Zeppelin Application (Experimental)](./development/writingzeppelinapplication.html) http://git-wip-us.apache.org/repos/asf/zeppelin/blob/d87f2e5d/docs/security/datasource_authorization.md ---------------------------------------------------------------------- diff --git a/docs/security/datasource_authorization.md b/docs/security/datasource_authorization.md new file mode 100644 index 0000000..3f86b8a --- /dev/null +++ b/docs/security/datasource_authorization.md @@ -0,0 +1,59 @@ +--- +layout: page +title: "Data Source Authorization" +description: "Data Source Authorization" +group: security +--- +<!-- +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. +--> +# Data Source Authorization in Apache Zeppelin + +<div id="toc"></div> + +## Overview + +Data source authorization involves authenticating to the data source like a Mysql database and letting it determine user permissions. +Apache Zeppelin allows users to use their own credentials to authenticate with **Data Sources**. + +For example, let's assume you have an account in the Vertica databases with credentials. +You might want to use this account to create a JDBC connection instead of a shared account with all users who are defined in `conf/shiro.ini`. +In this case, you can add your credential information to Apache Zeppelin and use them with below simple steps. + +## How to save the credential information? +You can add new credentials in the dropdown menu for your data source which can be passed to interpreters. + +<img class="img-responsive" src="../assets/themes/zeppelin/img/docs-img/credential_tab.png" width="180px"/> + +**Entity** can be the key that distinguishes each credential sets. Type **Username & Password** for your own credentials. ex) user & password of Mysql + +<img class="img-responsive" src="../assets/themes/zeppelin/img/docs-img/add_credential.png" /> + +The credentials saved as per users defined in `conf/shiro.ini`. +If you didn't activate [shiro authentication in Apache Zeppelin](./shiroauthentication.html), your credential information will be saved as `anonymous`. +All credential information also can be found in `conf/credentials.json`. + +#### JDBC interpreter +You need to maintain per-user connection pools. +The interpret method takes the user string as a parameter and executes the jdbc call using a connection in the user's connection pool. + +#### Presto +You don't need a password if the Presto DB server runs backend code using HDFS authorization for the user. + +#### Vertica and Mysql +You have to store the password information for users. + +## Please note +As a first step of data source authentication feature, [ZEPPELIN-828](https://issues.apache.org/jira/browse/ZEPPELIN-828) was proposed and implemented in Pull Request [#860](https://github.com/apache/zeppelin/pull/860). +Currently, only customized 3rd party interpreters can use this feature. We are planning to apply this mechanism to [the community interpreters](../manual/interpreterinstallation.md#available-community-managed-interpreters) in the near future. +Please keep track [ZEPPELIN-1070](https://issues.apache.org/jira/browse/ZEPPELIN-1070). http://git-wip-us.apache.org/repos/asf/zeppelin/blob/d87f2e5d/docs/security/interpreter_authorization.md ---------------------------------------------------------------------- diff --git a/docs/security/interpreter_authorization.md b/docs/security/interpreter_authorization.md deleted file mode 100644 index 6e59e07..0000000 --- a/docs/security/interpreter_authorization.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -layout: page -title: "Notebook Authorization" -description: "Notebook Authorization" -group: security ---- -<!-- -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. ---> -# Interpreter and Data Source Authorization - -<div id="toc"></div> - -## Interpreter Authorization - -Interpreter authorization involves permissions like creating an interpreter and execution queries using it. - -## Data Source Authorization - -Data source authorization involves authenticating to the data source like a Mysql database and letting it determine user permissions. - -For the JDBC interpreter, we need to maintain per-user connection pools. -The interpret method takes the user string as parameter and executes the jdbc call using a connection in the user's connection pool. - -In case of Presto, we don't need password if the Presto DB server runs backend code using HDFS authorization for the user. -For databases like Vertica and Mysql we have to store password information for users. - -The Credentials tab in the navbar allows users to save credentials for data sources which are passed to interpreters. http://git-wip-us.apache.org/repos/asf/zeppelin/blob/d87f2e5d/zeppelin-distribution/src/bin_license/LICENSE ---------------------------------------------------------------------- diff --git a/zeppelin-distribution/src/bin_license/LICENSE b/zeppelin-distribution/src/bin_license/LICENSE index 292b8d3..d7cc1cc 100644 --- a/zeppelin-distribution/src/bin_license/LICENSE +++ b/zeppelin-distribution/src/bin_license/LICENSE @@ -124,7 +124,7 @@ The text of each license is also included at licenses/LICENSE-[project]-[version (The MIT License) angular-elastic v2.4.2 (https://github.com/monospaced/angular-elastic) - https://github.com/monospaced/angular-elastic/blob/v2.4.2/LICENCE.txt (The MIT License) angular-elastic-input v2.2.0 (https://github.com/jacek-pulit/angular-elastic-input) - https://github.com/jacek-pulit/angular-elastic-input/blob/v2.2.0/LICENSE (The MIT License) ng-focus-if v1.0.2 (https://github.com/hiebj/ng-focus-if) - https://github.com/hiebj/ng-focus-if/blob/v1.0.2/LICENSE - (The MIT License) angular-xeditable v0.1.8 (http://vitalets.github.io/angular-xeditable/) - https://github.com/vitalets/angular-xeditable/tree/0.1.8 + (The MIT License) angular-xeditable v0.1.12 (http://vitalets.github.io/angular-xeditable/) - https://github.com/vitalets/angular-xeditable/tree/0.1.12 (The MIT License) lodash v3.9.3 (https://lodash.com/) - https://github.com/lodash/lodash/blob/3.9.3/LICENSE.txt (The MIT License) angular-filter v0.5.4 (https://github.com/a8m/angular-filter) - https://github.com/a8m/angular-filter/blob/v0.5.4/license.md (The MIT License) ngToast v2.0.0 (http://tamerayd.in/ngToast/) - http://tameraydin.mit-license.org/ http://git-wip-us.apache.org/repos/asf/zeppelin/blob/d87f2e5d/zeppelin-web/bower.json ---------------------------------------------------------------------- diff --git a/zeppelin-web/bower.json b/zeppelin-web/bower.json index a414653..94d228a 100644 --- a/zeppelin-web/bower.json +++ b/zeppelin-web/bower.json @@ -23,7 +23,7 @@ "ng-sortable": "~1.3.3", "angular-elastic": "~2.4.2", "angular-elastic-input": "~2.2.0", - "angular-xeditable": "0.1.8", + "angular-xeditable": "0.1.12", "highlightjs": "^9.2.0", "lodash": "~3.9.3", "angular-filter": "~0.5.4", http://git-wip-us.apache.org/repos/asf/zeppelin/blob/d87f2e5d/zeppelin-web/src/app/credential/credential.controller.js ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/credential/credential.controller.js b/zeppelin-web/src/app/credential/credential.controller.js index df23163..621e499 100644 --- a/zeppelin-web/src/app/credential/credential.controller.js +++ b/zeppelin-web/src/app/credential/credential.controller.js @@ -15,42 +15,134 @@ 'use strict'; angular.module('zeppelinWebApp').controller('CredentialCtrl', function($scope, $route, $routeParams, $location, - $rootScope, $http, baseUrlSrv) { + $rootScope, $http, baseUrlSrv, ngToast) { $scope._ = _; - $scope.credentialEntity = ''; - $scope.credentialUsername = ''; - $scope.credentialPassword = ''; + $scope.credentialInfo = []; + $scope.showAddNewCredentialInfo = false; - $scope.updateCredentials = function() { - if (_.isEmpty($scope.credentialEntity.trim()) || - _.isEmpty($scope.credentialUsername.trim())) { - BootstrapDialog.alert({ - closable: true, - message: 'Username \\ Entity can not be empty.' + var getCredentialInfo = function() { + $http.get(baseUrlSrv.getRestApiBase() + '/credential'). + success(function(data, status, headers, config) { + $scope.credentialInfo = _.map(data.body.userCredentials, function(value, prop) { + return {entity: prop, password: value.password, username: value.username}; + }); + console.log('Success %o %o', status, $scope.credentialInfo); + }). + error(function(data, status, headers, config) { + console.log('Error %o %o', status, data.message); + }); + }; + + $scope.addNewCredentialInfo = function() { + if ($scope.entity && _.isEmpty($scope.entity.trim()) && + $scope.username && _.isEmpty($scope.username.trim())) { + ngToast.danger({ + content: 'Username \\ Entity can not be empty.', + verticalPosition: 'bottom', + timeout: '3000' }); return; } - $http.put(baseUrlSrv.getRestApiBase() + '/credential', - {'entity': $scope.credentialEntity, - 'username': $scope.credentialUsername, - 'password': $scope.credentialPassword - }). + var newCredential = { + 'entity': $scope.entity, + 'username': $scope.username, + 'password': $scope.password + }; + + $http.put(baseUrlSrv.getRestApiBase() + '/credential', newCredential). success(function(data, status, headers, config) { - BootstrapDialog.alert({ - closable: true, - message: 'Successfully saved credentials.' + ngToast.success({ + content: 'Successfully saved credentials.', + verticalPosition: 'bottom', + timeout: '3000' }); - $scope.credentialEntity = ''; - $scope.credentialUsername = ''; - $scope.credentialPassword = ''; + $scope.credentialInfo.push(newCredential); + resetCredentialInfo(); + $scope.showAddNewCredentialInfo = false; console.log('Success %o %o', status, data.message); }). error(function(data, status, headers, config) { - alert('Error saving credentials'); + ngToast.danger({ + content: 'Error saving credentials', + verticalPosition: 'bottom', + timeout: '3000' + }); console.log('Error %o %o', status, data.message); }); }; + $scope.cancelCredentialInfo = function() { + $scope.showAddNewCredentialInfo = false; + resetCredentialInfo(); + }; + + var resetCredentialInfo = function() { + $scope.entity = ''; + $scope.username = ''; + $scope.password = ''; + }; + + $scope.copyOriginCredentialsInfo = function() { + ngToast.info({ + content: 'Since entity is a unique key, you can edit only username & password', + verticalPosition: 'bottom', + timeout: '3000' + }); + }; + + $scope.updateCredentialInfo = function(form, data, entity) { + var request = { + entity: entity, + username: data.username, + password: data.password + }; + + $http.put(baseUrlSrv.getRestApiBase() + '/credential/', request). + success(function(data, status, headers, config) { + var index = _.findIndex($scope.credentialInfo, {'entity': entity}); + $scope.credentialInfo[index] = request; + return true; + }). + error(function(data, status, headers, config) { + console.log('Error %o %o', status, data.message); + ngToast.danger({ + content: 'We couldn\'t save the credential', + verticalPosition: 'bottom', + timeout: '3000' + }); + form.$show(); + }); + return false; + }; + + $scope.removeCredentialInfo = function(entity) { + BootstrapDialog.confirm({ + closable: false, + closeByBackdrop: false, + closeByKeyboard: false, + title: '', + message: 'Do you want to delete this credential information?', + callback: function(result) { + if (result) { + $http.delete(baseUrlSrv.getRestApiBase() + '/credential/' + entity). + success(function(data, status, headers, config) { + var index = _.findIndex($scope.credentialInfo, {'entity': entity}); + $scope.credentialInfo.splice(index, 1); + console.log('Success %o %o', status, data.message); + }). + error(function(data, status, headers, config) { + console.log('Error %o %o', status, data.message); + }); + } + } + }); + }; + + var init = function() { + getCredentialInfo(); + }; + + init(); }); http://git-wip-us.apache.org/repos/asf/zeppelin/blob/d87f2e5d/zeppelin-web/src/app/credential/credential.html ---------------------------------------------------------------------- diff --git a/zeppelin-web/src/app/credential/credential.html b/zeppelin-web/src/app/credential/credential.html index 46d83d9..d4131b1 100644 --- a/zeppelin-web/src/app/credential/credential.html +++ b/zeppelin-web/src/app/credential/credential.html @@ -18,53 +18,134 @@ limitations under the License. <h3 class="new_h3"> Credentials </h3> + <div class="pull-right" style="margin-top:10px;"> + <a style="cursor:pointer;margin-right:10px;text-decoration:none;" + target="_blank" + ng-href="http://zeppelin.apache.org/docs/{{zeppelinVersion}}/security/datasource_authorization.html" + tooltip-placement="bottom" tooltip="Learn more"> + <i class="icon-question" ng-style="{color: showRepositoryInfo ? '#3071A9' : 'black' }"></i> + </a> + <button class="btn btn-default btn-sm" + ng-click="showAddNewCredentialInfo = !showAddNewCredentialInfo"> + <i class="fa fa-plus"></i> + Add + </button> + </div> </div> </div> <div class="row"> <div class="col-md-12"> - Add credentials for entities one at a time.<br/> + Manage your credentials. You can add new credential information. + </div> + </div> + </div> + + <!--Credential addition form--> + <div class="row interpreter"> + <div class="col-md-12" ng-show="showAddNewCredentialInfo"> + <hr /> + <div class="interpreterSettingAdd"> + <h4>Add new credential</h4> + <div> + <div class="row interpreter"> + <div class="col-md-12"> + <table class="table table-striped"> + <thead> + <tr> + <th style="width:30%">Entity</th> + <th>Username</th> + <th>Password</th> + </tr> + </thead> + <tr> + <td> + <textarea msd-elastic ng-model="entity"></textarea> + </td> + <td> + <textarea msd-elastic ng-model="username"></textarea> + </td> + <td> + <input type="password" ng-model="password"/> + </td> + </tr> + </table> + <span class="btn btn-primary" ng-click="addNewCredentialInfo()"> + Save + </span> + <span class="btn btn-default" ng-click="cancelCredentialInfo()"> + Cancel + </span> + </div> + </div> + </div> </div> </div> </div> </div> <div class="box width-full"> - <div> - <div class="row interpreter"> - <div class="col-md-12"> - <table class="table table-striped"> - <thead> - <tr> - <th style="width:30%">Entity</th> - <th>Username</th> - <th>Password</th> - <th ng-if="valueform.$visible">action</th> - </tr> - </thead> - <tr> - <td> - <textarea msd-elastic ng-model="credentialEntity"></textarea> - </td> - <td> - <textarea msd-elastic ng-model="credentialUsername"></textarea> - </td> - <td> - <input type="password" ng-model="credentialPassword"/> - </td> - </tr> - </table> - <form editable-form name="valueform" onaftersave="updateCredentials()"> - <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()"> - Cancel - </button> - </form> - </div> + <div class="row interpreter"> + <div ng-show="_.isEmpty(credentialInfo) || valueform.$hidden" + class="col-md-12 gray40-message"> + <em>Currently there is no credential information</em> + </div> + <div class="col-md-12" ng-show="!_.isEmpty(credentialInfo) || valueform.$visible"> + <table class="table table-striped"> + <thead> + <tr> + <th style="width:30%">Entity</th> + <th>Username</th> + <th>Password</th> + <th></th> + </tr> + </thead> + <tr ng-repeat="credential in credentialInfo"> + <td> + <span> + {{credential.entity}} + </span> + </td> + <td> + <span editable-textarea="credential.username" e-name="username" e-form="valueform" + e-msd-elastic focus-if="credential.username.length == 0"> + {{credential.username}} + </span> + </td> + <td> + <span editable-password="credential.password" e-name="password" e-form="valueform" + e-msd-elastic focus-if="credential.password.length == 0"> + ********** + </span> + </td> + <td> + <!-- Edit credential info --> + <span style="float:right" ng-show="!valueform.$visible"> + <button class="btn btn-default btn-xs" + ng-click="valueform.$show(); + copyOriginCredentialsInfo();"> + <span class="fa fa-pencil"></span> edit</button> + <button class="btn btn-default btn-xs" + ng-click="removeCredentialInfo(credential.entity)"> + <span class="fa fa-trash"></span> remove</button> + + </span> + <span style="float:right" ng-show="valueform.$visible"> + <form editable-form name="valueform" + onbeforesave="updateCredentialInfo(valueform, $data, credential.entity)" + ng-show="valueform.$visible"> + <button type="submit" class="btn btn-primary btn-xs"> + <span class="fa fa-check"></span> save + </button> + <button type="button" class="btn btn-default btn-xs" + ng-disabled="valueform.$waiting" + ng-click="valueform.$cancel();"> + <span class="fa fa-remove"></span> cancel + </button> + </form> + </span> + </td> + </tr> + </table> </div> </div> </div>
