mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-31 10:37:56 +00:00 
			
		
		
		
	UI - identity details (#4502)
* add popups * add ability to disable entity and banner when entity is disabled * re-add alias-popup template * add accpetance tests for creating entities * add more entity creation acceptance tests * add delete to edit-form * add more identity tests and associated selectors * add onSuccess hook and use UnloadModel route mixins * add ability to toggle entity disabling from the popover * fix store list cache because unloadAll isn't synchronous * fill out tests for identity items and aliases * add ability to enable entity from the detail page * toArray on the peekAll * fix other tests/behavior that relied on a RecordArray * adjust layout for disabled entity and label for disabling an entity on the edit form * add item-details integration tests * move disable field on the entity form * use ghost buttons for delete in identity and policy edit forms * adding computed macros for lazy capability fetching and using them in the identity models
This commit is contained in:
		| @@ -16,6 +16,7 @@ export default Ember.Component.extend({ | ||||
|         class={{buttonClasses}} | ||||
|         type="button" | ||||
|         disabled={{disabled}} | ||||
|         data-test-confirm-action-trigger=true | ||||
|         {{action 'toggleConfirm'}} | ||||
|       > | ||||
|         {{yield}} | ||||
|   | ||||
							
								
								
									
										40
									
								
								ui/app/components/identity/_popup-base.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								ui/app/components/identity/_popup-base.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| import Ember from 'ember'; | ||||
| const { assert, inject, Component } = Ember; | ||||
|  | ||||
| export default Component.extend({ | ||||
|   tagName: '', | ||||
|   flashMessages: inject.service(), | ||||
|   params: null, | ||||
|   successMessage() { | ||||
|     return 'Save was successful'; | ||||
|   }, | ||||
|   errorMessage() { | ||||
|     return 'There was an error saving'; | ||||
|   }, | ||||
|   onError(model) { | ||||
|     if (model && model.rollbackAttributes) { | ||||
|       model.rollbackAttributes(); | ||||
|     } | ||||
|   }, | ||||
|   onSuccess(){}, | ||||
|   // override and return a promise | ||||
|   transaction() { | ||||
|     assert('override transaction call in an extension of popup-base', false); | ||||
|   }, | ||||
|  | ||||
|   actions: { | ||||
|     performTransaction() { | ||||
|       let args = [...arguments]; | ||||
|       let messageArgs = this.messageArgs(...args); | ||||
|       return this.transaction(...args) | ||||
|         .then(() => { | ||||
|           this.get('onSuccess')(); | ||||
|           this.get('flashMessages').success(this.successMessage(...messageArgs)); | ||||
|         }) | ||||
|         .catch(e => { | ||||
|           this.onError(...messageArgs); | ||||
|           this.get('flashMessages').success(this.errorMessage(e, ...messageArgs)); | ||||
|         }); | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
| @@ -2,20 +2,23 @@ import Ember from 'ember'; | ||||
| import { task } from 'ember-concurrency'; | ||||
| import { humanize } from 'vault/helpers/humanize'; | ||||
|  | ||||
| const { computed } = Ember; | ||||
| const { computed, inject } = Ember; | ||||
| export default Ember.Component.extend({ | ||||
|   flashMessages: inject.service(), | ||||
|   'data-test-component': 'identity-edit-form', | ||||
|   model: null, | ||||
|  | ||||
|   // 'create', 'edit', 'merge' | ||||
|   mode: 'create', | ||||
|   /* | ||||
|    * @param Function | ||||
|    * @public | ||||
|    * | ||||
|    * Optional param to call a function upon successfully mounting a backend | ||||
|    * | ||||
|    * Optional param to call a function upon successfully saving an entity | ||||
|    */ | ||||
|   onSave: () => {}, | ||||
|  | ||||
|   cancelLink: computed('mode', 'model', function() { | ||||
|   cancelLink: computed('mode', 'model.identityType', function() { | ||||
|     let { model, mode } = this.getProperties('model', 'mode'); | ||||
|     let key = `${mode}-${model.get('identityType')}`; | ||||
|     let routes = { | ||||
| @@ -33,16 +36,17 @@ export default Ember.Component.extend({ | ||||
|     return routes[key]; | ||||
|   }), | ||||
|  | ||||
|   getMessage(model) { | ||||
|   getMessage(model, isDelete = false) { | ||||
|     let mode = this.get('mode'); | ||||
|     let typeDisplay = humanize([model.get('identityType')]); | ||||
|     let action = isDelete ? 'deleted' : 'saved'; | ||||
|     if (mode === 'merge') { | ||||
|       return 'Successfully merged entities'; | ||||
|     } | ||||
|     if (model.get('id')) { | ||||
|       return `Successfully saved ${typeDisplay} ${model.id}.`; | ||||
|       return `Successfully ${action} ${typeDisplay} ${model.id}.`; | ||||
|     } | ||||
|     return `Successfully saved ${typeDisplay}.`; | ||||
|     return `Successfully ${action} ${typeDisplay}.`; | ||||
|   }, | ||||
|  | ||||
|   save: task(function*() { | ||||
| @@ -56,13 +60,26 @@ export default Ember.Component.extend({ | ||||
|       return; | ||||
|     } | ||||
|     this.get('flashMessages').success(message); | ||||
|     yield this.get('onSave')(model); | ||||
|     yield this.get('onSave')({saveType: 'save', model}); | ||||
|   }).drop(), | ||||
|  | ||||
|   willDestroy() { | ||||
|     let model = this.get('model'); | ||||
|     if (!model.isDestroyed || !model.isDestroying) { | ||||
|     if ((model.get('isDirty') && !model.isDestroyed) || !model.isDestroying) { | ||||
|       model.rollbackAttributes(); | ||||
|     } | ||||
|   }, | ||||
|  | ||||
|   actions: { | ||||
|     deleteItem(model) { | ||||
|       let message = this.getMessage(model, true); | ||||
|       let flash = this.get('flashMessages'); | ||||
|       model | ||||
|         .destroyRecord() | ||||
|         .then(() => { | ||||
|           flash.success(message); | ||||
|           return this.get('onSave')({saveType: 'delete', model}); | ||||
|         }); | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
|   | ||||
							
								
								
									
										23
									
								
								ui/app/components/identity/item-details.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								ui/app/components/identity/item-details.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| import Ember from 'ember'; | ||||
|  | ||||
| const { inject } = Ember; | ||||
|  | ||||
| export default Ember.Component.extend({ | ||||
|   flashMessages: inject.service(), | ||||
|  | ||||
|   actions: { | ||||
|     enable(model) { | ||||
|       model.set('disabled', false); | ||||
|  | ||||
|       model.save(). | ||||
|         then(() => { | ||||
|           this.get('flashMessages').success(`Successfully enabled entity: ${model.id}`); | ||||
|         }) | ||||
|         .catch(e => { | ||||
|           this.get('flashMessages').success( | ||||
|             `There was a problem enabling the entity: ${model.id} - ${e.error.join(' ') || e.message}` | ||||
|           ); | ||||
|         }); | ||||
|     } | ||||
|   } | ||||
| }); | ||||
							
								
								
									
										22
									
								
								ui/app/components/identity/popup-alias.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								ui/app/components/identity/popup-alias.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| import Base from './_popup-base'; | ||||
|  | ||||
| export default Base.extend({ | ||||
|   messageArgs(model) { | ||||
|     let type = model.get('identityType'); | ||||
|     let id = model.id; | ||||
|     return [type, id]; | ||||
|   }, | ||||
|  | ||||
|   successMessage(type, id) { | ||||
|     return `Successfully deleted ${type}: ${id}`; | ||||
|   }, | ||||
|  | ||||
|   errorMessage(e, type, id) { | ||||
|     let error = e.errors ? e.errors.join(' ') : e.message; | ||||
|     return `There was a problem deleting ${type}: ${id} - ${error}`; | ||||
|   }, | ||||
|  | ||||
|   transaction(model) { | ||||
|     return model.destroyRecord(); | ||||
|   }, | ||||
| }); | ||||
							
								
								
									
										34
									
								
								ui/app/components/identity/popup-members.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								ui/app/components/identity/popup-members.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| import Base from './_popup-base'; | ||||
| import Ember from 'ember'; | ||||
| const { computed } = Ember; | ||||
|  | ||||
| export default Base.extend({ | ||||
|   model: computed.alias('params.firstObject'), | ||||
|  | ||||
|   groupArray: computed('params', function() { | ||||
|     return this.get('params').objectAt(1); | ||||
|   }), | ||||
|  | ||||
|   memberId: computed('params', function() { | ||||
|     return this.get('params').objectAt(2); | ||||
|   }), | ||||
|  | ||||
|   messageArgs(/*model, groupArray, memberId*/) { | ||||
|     return [...arguments]; | ||||
|   }, | ||||
|  | ||||
|   successMessage(model, groupArray, memberId) { | ||||
|     return `Successfully removed '${memberId}' from the group`; | ||||
|   }, | ||||
|  | ||||
|   errorMessage(e, model, groupArray, memberId) { | ||||
|     let error = e.errors ? e.errors.join(' ') : e.message; | ||||
|     return `There was a problem removing '${memberId}' from the group - ${error}`; | ||||
|   }, | ||||
|  | ||||
|   transaction(model, groupArray, memberId) { | ||||
|     let members = model.get(groupArray); | ||||
|     model.set(groupArray, members.without(memberId)); | ||||
|     return model.save(); | ||||
|   }, | ||||
| }); | ||||
							
								
								
									
										29
									
								
								ui/app/components/identity/popup-metadata.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								ui/app/components/identity/popup-metadata.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| import Base from './_popup-base'; | ||||
| import Ember from 'ember'; | ||||
| const { computed } = Ember; | ||||
|  | ||||
| export default Base.extend({ | ||||
|   model: computed.alias('params.firstObject'), | ||||
|   key: computed('params', function() { | ||||
|     return this.get('params').objectAt(1); | ||||
|   }), | ||||
|  | ||||
|   messageArgs(model, key) { | ||||
|     return [model, key]; | ||||
|   }, | ||||
|  | ||||
|   successMessage(model, key) { | ||||
|     return `Successfully removed '${key}' from metadata`; | ||||
|   }, | ||||
|   errorMessage(e, model, key) { | ||||
|     let error = e.errors ? e.errors.join(' ') : e.message; | ||||
|     return `There was a problem removing '${key}' from the metadata - ${error}`; | ||||
|   }, | ||||
|  | ||||
|   transaction(model, key) { | ||||
|     let metadata = model.get('metadata'); | ||||
|     delete metadata[key]; | ||||
|     model.set('metadata', { ...metadata }); | ||||
|     return model.save(); | ||||
|   }, | ||||
| }); | ||||
							
								
								
									
										29
									
								
								ui/app/components/identity/popup-policy.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								ui/app/components/identity/popup-policy.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| import Base from './_popup-base'; | ||||
| import Ember from 'ember'; | ||||
| const { computed } = Ember; | ||||
|  | ||||
| export default Base.extend({ | ||||
|   model: computed.alias('params.firstObject'), | ||||
|   policyName: computed('params', function() { | ||||
|     return this.get('params').objectAt(1); | ||||
|   }), | ||||
|  | ||||
|   messageArgs(model, policyName) { | ||||
|     return [model, policyName]; | ||||
|   }, | ||||
|  | ||||
|   successMessage(model, policyName) { | ||||
|     return `Successfully removed '${policyName}' policy from ${model.id} `; | ||||
|   }, | ||||
|  | ||||
|   errorMessage(e, model, policyName) { | ||||
|     let error = e.errors ? e.errors.join(' ') : e.message; | ||||
|     return `There was a problem removing '${policyName}' policy - ${error}`; | ||||
|   }, | ||||
|  | ||||
|   transaction(model, policyName) { | ||||
|     let policies = model.get('policies'); | ||||
|     model.set('policies', policies.without(policyName)); | ||||
|     return model.save(); | ||||
|   }, | ||||
| }); | ||||
| @@ -1,6 +1,7 @@ | ||||
| import Ember from 'ember'; | ||||
|  | ||||
| export default Ember.Component.extend({ | ||||
|   'data-test-component': 'info-table-row', | ||||
|   classNames: ['info-table-row'], | ||||
|   isVisible: Ember.computed.or('alwaysRender', 'value'), | ||||
|  | ||||
|   | ||||
| @@ -6,6 +6,8 @@ const { computed } = Ember; | ||||
| export default Ember.Component.extend({ | ||||
|   type: null, | ||||
|  | ||||
|   yieldWithoutColumn: false, | ||||
|  | ||||
|   classNameBindings: ['containerClass'], | ||||
|  | ||||
|   containerClass: computed('type', function() { | ||||
|   | ||||
| @@ -9,6 +9,4 @@ export default Ember.Component.extend({ | ||||
|   baseKey: null, | ||||
|   backendCrumb: null, | ||||
|   model: null, | ||||
|  | ||||
|  | ||||
| }); | ||||
|   | ||||
| @@ -71,7 +71,7 @@ export default Ember.Component.extend(DEFAULTS, { | ||||
|  | ||||
|   handleSuccess(resp, action) { | ||||
|     let props = {}; | ||||
|     let secret = resp && resp.data || resp.auth; | ||||
|     let secret = (resp && resp.data) || resp.auth; | ||||
|     if (secret && action === 'unwrap') { | ||||
|       props = Ember.assign({}, props, { unwrap_data: secret }); | ||||
|     } | ||||
|   | ||||
| @@ -1,4 +1,10 @@ | ||||
| import Ember from 'ember'; | ||||
| import ListController from 'vault/mixins/list-controller'; | ||||
|  | ||||
| export default Ember.Controller.extend(ListController); | ||||
| export default Ember.Controller.extend(ListController, { | ||||
|   actions: { | ||||
|     onDelete() { | ||||
|       this.send('reload'); | ||||
|     } | ||||
|   } | ||||
| }); | ||||
|   | ||||
| @@ -4,7 +4,26 @@ import { task } from 'ember-concurrency'; | ||||
| export default Ember.Controller.extend({ | ||||
|   showRoute: 'vault.cluster.access.identity.show', | ||||
|   showTab: 'details', | ||||
|   navToShow: task(function*(model) { | ||||
|     yield this.transitionToRoute(this.get('showRoute'), model.id, this.get('showTab')); | ||||
|   navAfterSave: task(function*({saveType, model}) { | ||||
|     let isDelete = saveType === 'delete'; | ||||
|     let type = model.get('identityType'); | ||||
|     let listRoutes= { | ||||
|       'entity-alias': 'vault.cluster.access.identity.aliases.index', | ||||
|       'group-alias': 'vault.cluster.access.identity.aliases.index', | ||||
|       'group': 'vault.cluster.access.identity.index', | ||||
|       'entity': 'vault.cluster.access.identity.index', | ||||
|     }; | ||||
|     let routeName = listRoutes[type] | ||||
|     if (!isDelete) { | ||||
|       yield this.transitionToRoute( | ||||
|         this.get('showRoute'), | ||||
|         model.id, | ||||
|         this.get('showTab') | ||||
|       ); | ||||
|       return; | ||||
|     } | ||||
|     yield this.transitionToRoute( | ||||
|       routeName | ||||
|     ); | ||||
|   }), | ||||
| }); | ||||
|   | ||||
| @@ -1,4 +1,46 @@ | ||||
| import Ember from 'ember'; | ||||
| import ListController from 'vault/mixins/list-controller'; | ||||
|  | ||||
| export default Ember.Controller.extend(ListController); | ||||
| const { inject } = Ember; | ||||
|  | ||||
| export default Ember.Controller.extend(ListController, { | ||||
|   flashMessages: inject.service(), | ||||
|  | ||||
|   actions: { | ||||
|     delete(model) { | ||||
|       let type = model.get('identityType'); | ||||
|       let id = model.id; | ||||
|       return model | ||||
|         .destroyRecord() | ||||
|         .then(() => { | ||||
|           this.send('reload'); | ||||
|           this.get('flashMessages').success(`Successfully deleted ${type}: ${id}`); | ||||
|         }) | ||||
|         .catch(e => { | ||||
|           this.get('flashMessages').success( | ||||
|             `There was a problem deleting ${type}: ${id} - ${e.error.join(' ') || e.message}` | ||||
|           ); | ||||
|         }); | ||||
|     }, | ||||
|  | ||||
|     toggleDisabled(model) { | ||||
|       let action = model.get('disabled') ? ['enabled', 'enabling'] : ['disabled', 'disabling']; | ||||
|       let type = model.get('identityType'); | ||||
|       let id = model.id; | ||||
|       model.toggleProperty('disabled'); | ||||
|  | ||||
|       model.save(). | ||||
|         then(() => { | ||||
|           this.get('flashMessages').success(`Successfully ${action[0]} ${type}: ${id}`); | ||||
|         }) | ||||
|         .catch(e => { | ||||
|           this.get('flashMessages').success( | ||||
|             `There was a problem ${action[1]} ${type}: ${id} - ${e.error.join(' ') || e.message}` | ||||
|           ); | ||||
|         }); | ||||
|     }, | ||||
|     reloadRecord(model) { | ||||
|       model.reload(); | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
|   | ||||
| @@ -1,9 +1,11 @@ | ||||
| import Ember from 'ember'; | ||||
| import utils from 'vault/lib/key-utils'; | ||||
|  | ||||
| export default Ember.Controller.extend({ | ||||
|   flashMessages: Ember.inject.service(), | ||||
|   clusterController: Ember.inject.controller('vault.cluster'), | ||||
| const { inject, computed, Controller } = Ember; | ||||
| export default Controller.extend({ | ||||
|   flashMessages: inject.service(), | ||||
|   store: inject.service(), | ||||
|   clusterController: inject.controller('vault.cluster'), | ||||
|   queryParams: { | ||||
|     page: 'page', | ||||
|     pageFilter: 'pageFilter', | ||||
| @@ -13,7 +15,7 @@ export default Ember.Controller.extend({ | ||||
|   pageFilter: null, | ||||
|   filter: null, | ||||
|  | ||||
|   backendCrumb: Ember.computed(function() { | ||||
|   backendCrumb: computed(function() { | ||||
|     return { | ||||
|       label: 'leases', | ||||
|       text: 'leases', | ||||
| @@ -24,13 +26,13 @@ export default Ember.Controller.extend({ | ||||
|  | ||||
|   isLoading: false, | ||||
|  | ||||
|   filterMatchesKey: Ember.computed('filter', 'model', 'model.[]', function() { | ||||
|   filterMatchesKey: computed('filter', 'model', 'model.[]', function() { | ||||
|     var filter = this.get('filter'); | ||||
|     var content = this.get('model'); | ||||
|     return !!(content.length && content.findBy('id', filter)); | ||||
|   }), | ||||
|  | ||||
|   firstPartialMatch: Ember.computed('filter', 'model', 'model.[]', 'filterMatchesKey', function() { | ||||
|   firstPartialMatch: computed('filter', 'model', 'model.[]', 'filterMatchesKey', function() { | ||||
|     var filter = this.get('filter'); | ||||
|     var content = this.get('model'); | ||||
|     var filterMatchesKey = this.get('filterMatchesKey'); | ||||
| @@ -42,7 +44,7 @@ export default Ember.Controller.extend({ | ||||
|         }); | ||||
|   }), | ||||
|  | ||||
|   filterIsFolder: Ember.computed('filter', function() { | ||||
|   filterIsFolder: computed('filter', function() { | ||||
|     return !!utils.keyIsFolder(this.get('filter')); | ||||
|   }), | ||||
|  | ||||
| @@ -56,7 +58,7 @@ export default Ember.Controller.extend({ | ||||
|     }, | ||||
|  | ||||
|     revokePrefix(prefix, isForce) { | ||||
|       const adapter = this.model.store.adapterFor('lease'); | ||||
|       const adapter = this.get('store').adapterFor('lease'); | ||||
|       const method = isForce ? 'forceRevokePrefix' : 'revokePrefix'; | ||||
|       const fn = adapter[method]; | ||||
|       fn | ||||
|   | ||||
| @@ -66,6 +66,7 @@ export default Ember.Controller.extend(BackendCrumbMixin, { | ||||
|     delete(item) { | ||||
|       const name = item.id; | ||||
|       item.destroyRecord().then(() => { | ||||
|         this.send('reload'); | ||||
|         this.get('flashMessages').success(`${name} was successfully deleted.`); | ||||
|       }); | ||||
|     }, | ||||
|   | ||||
							
								
								
									
										5
									
								
								ui/app/macros/identity-capabilities.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								ui/app/macros/identity-capabilities.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities'; | ||||
|  | ||||
| export default function() { | ||||
|   return lazyCapabilities(apiPath`identity/${'identityType'}/id/${'id'}`, 'id', 'identityType'); | ||||
| } | ||||
							
								
								
									
										25
									
								
								ui/app/macros/lazy-capabilities.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								ui/app/macros/lazy-capabilities.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| import { queryRecord } from 'ember-computed-query'; | ||||
|  | ||||
| export function apiPath(strings, ...keys) { | ||||
|   return function(data) { | ||||
|     let dict = data || {}; | ||||
|     let result = [strings[0]]; | ||||
|     keys.forEach((key, i) => { | ||||
|       result.push(dict[key], strings[i + 1]); | ||||
|     }); | ||||
|     return result.join(''); | ||||
|   }; | ||||
| } | ||||
|  | ||||
| export default function() { | ||||
|   let [templateFn, ...keys] = arguments; | ||||
|   return queryRecord( | ||||
|     'capabilities', | ||||
|     context => { | ||||
|       return { | ||||
|         id: templateFn(context.getProperties(...keys)), | ||||
|       }; | ||||
|     }, | ||||
|     ...keys | ||||
|   ); | ||||
| } | ||||
| @@ -1,8 +1,12 @@ | ||||
| import IdentityModel from './_base'; | ||||
| import DS from 'ember-data'; | ||||
| import Ember from 'ember'; | ||||
| import identityCapabilities from 'vault/macros/identity-capabilities'; | ||||
| const { attr, belongsTo } = DS; | ||||
| const { computed } = Ember; | ||||
|  | ||||
| export default IdentityModel.extend({ | ||||
|   parentType: 'entity', | ||||
|   formFields: ['name', 'mountAccessor', 'metadata'], | ||||
|   entity: belongsTo('identity/entity', { readOnly: true, async: false }), | ||||
|  | ||||
| @@ -12,7 +16,7 @@ export default IdentityModel.extend({ | ||||
|     label: 'Auth Backend', | ||||
|     editType: 'mountAccessor', | ||||
|   }), | ||||
|   metadata: attr('object', { | ||||
|   metadata: attr({ | ||||
|     editType: 'kv', | ||||
|   }), | ||||
|   mountPath: attr('string', { | ||||
| @@ -28,4 +32,8 @@ export default IdentityModel.extend({ | ||||
|     readOnly: true, | ||||
|   }), | ||||
|   mergedFromCanonicalIds: attr(), | ||||
|  | ||||
|   updatePath: identityCapabilities(), | ||||
|   canDelete: computed.alias('updatePath.canDelete'), | ||||
|   canEdit: computed.alias('updatePath.canUpdate'), | ||||
| }); | ||||
|   | ||||
| @@ -1,12 +1,23 @@ | ||||
| import Ember from 'ember'; | ||||
| import IdentityModel from './_base'; | ||||
| import DS from 'ember-data'; | ||||
| import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities'; | ||||
| import identityCapabilities from 'vault/macros/identity-capabilities'; | ||||
|  | ||||
| const { computed } = Ember; | ||||
|  | ||||
| const { attr, hasMany } = DS; | ||||
|  | ||||
| export default IdentityModel.extend({ | ||||
|   formFields: ['name', 'policies', 'metadata'], | ||||
|   formFields: ['name', 'disabled', 'policies', 'metadata'], | ||||
|   name: attr('string'), | ||||
|   disabled: attr('boolean', { | ||||
|     defaultValue: false, | ||||
|     label: 'Disable entity', | ||||
|     helpText: 'All associated tokens cannot be used, but are not revoked.', | ||||
|   }), | ||||
|   mergedEntityIds: attr(), | ||||
|   metadata: attr('object', { | ||||
|   metadata: attr({ | ||||
|     editType: 'kv', | ||||
|   }), | ||||
|   policies: attr({ | ||||
| @@ -28,4 +39,11 @@ export default IdentityModel.extend({ | ||||
|   inheritedGroupIds: attr({ | ||||
|     readOnly: true, | ||||
|   }), | ||||
|  | ||||
|   updatePath: identityCapabilities(), | ||||
|   canDelete: computed.alias('updatePath.canDelete'), | ||||
|   canEdit: computed.alias('updatePath.canUpdate'), | ||||
|  | ||||
|   aliasPath: lazyCapabilities(apiPath`identity/entity-alias`), | ||||
|   canAddAlias: computed.alias('aliasPath.canCreate'), | ||||
| }); | ||||
|   | ||||
| @@ -1,8 +1,13 @@ | ||||
| import IdentityModel from './_base'; | ||||
| import DS from 'ember-data'; | ||||
| import Ember from 'ember'; | ||||
| import identityCapabilities from 'vault/macros/identity-capabilities'; | ||||
|  | ||||
| const { attr, belongsTo } = DS; | ||||
| const { computed } = Ember; | ||||
|  | ||||
| export default IdentityModel.extend({ | ||||
|   parentType: 'group', | ||||
|   formFields: ['name', 'mountAccessor'], | ||||
|   group: belongsTo('identity/group', { readOnly: true, async: false }), | ||||
|  | ||||
| @@ -26,4 +31,9 @@ export default IdentityModel.extend({ | ||||
|   lastUpdateTime: attr('string', { | ||||
|     readOnly: true, | ||||
|   }), | ||||
|  | ||||
|   updatePath: identityCapabilities(), | ||||
|   canDelete: computed.alias('updatePath.canDelete'), | ||||
|   canEdit: computed.alias('updatePath.canUpdate'), | ||||
|  | ||||
| }); | ||||
|   | ||||
| @@ -1,6 +1,8 @@ | ||||
| import Ember from 'ember'; | ||||
| import IdentityModel from './_base'; | ||||
| import DS from 'ember-data'; | ||||
| import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities'; | ||||
| import identityCapabilities from 'vault/macros/identity-capabilities'; | ||||
|  | ||||
| const { computed } = Ember; | ||||
| const { attr, belongsTo } = DS; | ||||
| @@ -52,4 +54,18 @@ export default IdentityModel.extend({ | ||||
|   ), | ||||
|  | ||||
|   alias: belongsTo('identity/group-alias', { async: false, readOnly: true }), | ||||
|   updatePath: identityCapabilities(), | ||||
|   canDelete: computed.alias('updatePath.canDelete'), | ||||
|   canEdit: computed.alias('updatePath.canUpdate'), | ||||
|  | ||||
|   aliasPath: lazyCapabilities(apiPath`identity/group-alias`), | ||||
|   canAddAlias: computed('aliasPath.canCreate', 'type', 'alias', function() { | ||||
|     let type = this.get('type'); | ||||
|     let alias = this.get('alias'); | ||||
|     // internal groups can't have aliases, and external groups can only have one | ||||
|     if (type === 'internal' || alias) { | ||||
|       return false; | ||||
|     } | ||||
|     return this.get('aliasPath.canCreate'); | ||||
|   }), | ||||
| }); | ||||
|   | ||||
| @@ -1,6 +1,8 @@ | ||||
| import Ember from 'ember'; | ||||
| import UnloadModelRoute from 'vault/mixins/unload-model-route'; | ||||
| import UnsavedModelRoute from 'vault/mixins/unsaved-model-route'; | ||||
|  | ||||
| export default Ember.Route.extend({ | ||||
| export default Ember.Route.extend(UnloadModelRoute, UnsavedModelRoute, { | ||||
|   model(params) { | ||||
|     let itemType = this.modelFor('vault.cluster.access.identity'); | ||||
|     let modelType = `identity/${itemType}-alias`; | ||||
|   | ||||
| @@ -1,6 +1,8 @@ | ||||
| import Ember from 'ember'; | ||||
| import UnloadModelRoute from 'vault/mixins/unload-model-route'; | ||||
| import UnsavedModelRoute from 'vault/mixins/unsaved-model-route'; | ||||
|  | ||||
| export default Ember.Route.extend({ | ||||
| export default Ember.Route.extend(UnloadModelRoute, UnsavedModelRoute, { | ||||
|   model(params) { | ||||
|     let itemType = this.modelFor('vault.cluster.access.identity'); | ||||
|     let modelType = `identity/${itemType}-alias`; | ||||
|   | ||||
| @@ -27,10 +27,14 @@ export default Ember.Route.extend(ListRoute, { | ||||
|   actions: { | ||||
|     willTransition(transition) { | ||||
|       window.scrollTo(0, 0); | ||||
|       if (transition.targetName !== this.routeName) { | ||||
|       if (!transition || transition.targetName !== this.routeName) { | ||||
|         this.store.clearAllDatasets(); | ||||
|       } | ||||
|       return true; | ||||
|     }, | ||||
|     reload() { | ||||
|       this.store.clearAllDatasets(); | ||||
|       this.refresh(); | ||||
|     } | ||||
|   }, | ||||
| }); | ||||
|   | ||||
| @@ -1,6 +1,8 @@ | ||||
| import Ember from 'ember'; | ||||
| import UnloadModelRoute from 'vault/mixins/unload-model-route'; | ||||
| import UnsavedModelRoute from 'vault/mixins/unsaved-model-route'; | ||||
|  | ||||
| export default Ember.Route.extend({ | ||||
| export default Ember.Route.extend(UnloadModelRoute, UnsavedModelRoute, { | ||||
|   model() { | ||||
|     let itemType = this.modelFor('vault.cluster.access.identity'); | ||||
|     let modelType = `identity/${itemType}`; | ||||
|   | ||||
| @@ -1,6 +1,8 @@ | ||||
| import Ember from 'ember'; | ||||
| import UnloadModelRoute from 'vault/mixins/unload-model-route'; | ||||
| import UnsavedModelRoute from 'vault/mixins/unsaved-model-route'; | ||||
|  | ||||
| export default Ember.Route.extend({ | ||||
| export default Ember.Route.extend(UnloadModelRoute, UnsavedModelRoute, { | ||||
|   model(params) { | ||||
|     let itemType = this.modelFor('vault.cluster.access.identity'); | ||||
|     let modelType = `identity/${itemType}`; | ||||
|   | ||||
| @@ -34,5 +34,9 @@ export default Ember.Route.extend(ListRoute, { | ||||
|       } | ||||
|       return true; | ||||
|     }, | ||||
|     reload() { | ||||
|       this.store.clearAllDatasets(); | ||||
|       this.refresh(); | ||||
|     } | ||||
|   }, | ||||
| }); | ||||
|   | ||||
| @@ -13,13 +13,36 @@ export default Ember.Route.extend({ | ||||
|       Ember.set(error, 'httpStatus', 404); | ||||
|       throw error; | ||||
|     } | ||||
|     // TODO peekRecord here to see if we have the record already | ||||
|  | ||||
|     // if the record is in the store use that | ||||
|     let model = this.store.peekRecord(modelType, params.item_id); | ||||
|  | ||||
|     // if we don't have creationTime, we only have a partial model so reload | ||||
|     if (model && !model.get('creationTime')) { | ||||
|       model = model.reload(); | ||||
|     } | ||||
|  | ||||
|     // if there's no model, we need to fetch it | ||||
|     if (!model) { | ||||
|       model = this.store.findRecord(modelType, params.item_id); | ||||
|     } | ||||
|  | ||||
|     return Ember.RSVP.hash({ | ||||
|       model: this.store.findRecord(modelType, params.item_id), | ||||
|       model, | ||||
|       section, | ||||
|     }); | ||||
|   }, | ||||
|  | ||||
|   activate() { | ||||
|     // if we're just entering the route, and it's not a hard reload | ||||
|     // reload to make sure we have the newest info | ||||
|     if (this.currentModel) { | ||||
|       Ember.run.next(() => { | ||||
|         this.controller.get('model').reload(); | ||||
|       }); | ||||
|     } | ||||
|   }, | ||||
|  | ||||
|   afterModel(resolvedModel) { | ||||
|     let { section, model } = resolvedModel; | ||||
|     if (model.get('identityType') === 'group' && model.get('type') === 'internal' && section === 'aliases') { | ||||
|   | ||||
| @@ -159,5 +159,9 @@ export default Ember.Route.extend({ | ||||
|       } | ||||
|       return true; | ||||
|     }, | ||||
|     reload() { | ||||
|       this.refresh(); | ||||
|       this.store.clearAllDatasets(); | ||||
|     } | ||||
|   }, | ||||
| }); | ||||
|   | ||||
| @@ -131,13 +131,25 @@ export default DS.Store.extend({ | ||||
|   // pushes records into the store and returns the result | ||||
|   fetchPage(modelName, query) { | ||||
|     const response = this.constructResponse(modelName, query); | ||||
|     this.unloadAll(modelName); | ||||
|     this.peekAll(modelName).forEach(record => { | ||||
|       record.unloadRecord(); | ||||
|     }); | ||||
|     return new Ember.RSVP.Promise(resolve => { | ||||
|       Ember.run.schedule('destroy', () => { | ||||
|         this.push( | ||||
|       this.serializerFor(modelName).normalizeResponse(this, this.modelFor(modelName), response, null, 'query') | ||||
|           this.serializerFor(modelName).normalizeResponse( | ||||
|             this, | ||||
|             this.modelFor(modelName), | ||||
|             response, | ||||
|             null, | ||||
|             'query' | ||||
|           ) | ||||
|         ); | ||||
|     const model = this.peekAll(modelName); | ||||
|         let model = this.peekAll(modelName).toArray(); | ||||
|         model.set('meta', response.meta); | ||||
|     return model; | ||||
|         resolve(model); | ||||
|       }); | ||||
|     }); | ||||
|   }, | ||||
|  | ||||
|   // get cached data | ||||
|   | ||||
| @@ -49,6 +49,7 @@ | ||||
|     height: auto; | ||||
|     width: 100%; | ||||
|     text-align: left; | ||||
|     text-decoration: none; | ||||
|  | ||||
|     &:hover { | ||||
|       background-color: $menu-item-hover-background-color; | ||||
|   | ||||
| @@ -133,6 +133,17 @@ $button-box-shadow-standard: 0 3px 1px 0 rgba($black, 0.12); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   &.is-orange { | ||||
|     background-color: $orange; | ||||
|     border-color: $orange; | ||||
|     color: $white; | ||||
|  | ||||
|     &:hover, | ||||
|     &.is-hovered { | ||||
|       background-color: darken($orange, 5%); | ||||
|       border-color: darken($orange, 5%); | ||||
|     } | ||||
|   } | ||||
|   &.is-compact { | ||||
|     height: 2rem; | ||||
|     padding: $size-11 $size-8; | ||||
|   | ||||
| @@ -10,7 +10,9 @@ | ||||
|       {{form-field data-test-field attr=attr model=model}} | ||||
|     {{/each}} | ||||
|   </div> | ||||
|   <div class="field is-grouped box is-fullwidth is-bottomless"> | ||||
|  | ||||
|   <div class="field is-grouped is-grouped-split is-fullwidth box is-bottomless"> | ||||
|     <div class="field is-grouped"> | ||||
|       <div class="control"> | ||||
|         <button type="submit" data-test-identity-submit=true class="button is-primary {{if save.isRunning 'loading'}}" disabled={{save.isRunning}}> | ||||
|           {{#if (eq mode "create")}} | ||||
| @@ -20,14 +22,27 @@ | ||||
|           {{/if}} | ||||
|         </button> | ||||
|         {{#if (or (eq mode "merge") (eq mode "create" ))}} | ||||
|         <a href={{href-to cancelLink}} class="button"> | ||||
|           <a href={{href-to cancelLink}} class="button" data-test-cancel-link> | ||||
|             Cancel | ||||
|           </a> | ||||
|         {{else}} | ||||
|         <a href={{href-to cancelLink model.id "details"}} class="button"> | ||||
|           <a href={{href-to cancelLink model.id "details"}} class="button" data-test-cancel-link> | ||||
|             Cancel | ||||
|           </a> | ||||
|         {{/if}} | ||||
|       </div> | ||||
|     </div> | ||||
|  | ||||
|       {{#if (and (eq mode "edit") model.canDelete)}} | ||||
|         {{#confirm-action | ||||
|           buttonClasses="button is-ghost" | ||||
|           onConfirmAction=(action "deleteItem" model) | ||||
|           confirmMessage=(concat "Are you sure you want to delete " model.id "?") | ||||
|           data-test-entity-item-delete=true | ||||
|         }} | ||||
|             Delete | ||||
|         {{/confirm-action}} | ||||
|       {{/if}} | ||||
|      | ||||
|   </div> | ||||
| </form> | ||||
|   | ||||
| @@ -7,12 +7,12 @@ | ||||
|     </div> | ||||
|     <div class="level-right"> | ||||
|       {{#if (eq identityType "entity")}} | ||||
|         <a href="{{href-to 'vault.cluster.access.identity.merge'}}" class="button has-icon-right is-ghost is-compact" data-test-entity-merge-link=true> | ||||
|         <a href="{{href-to 'vault.cluster.access.identity.merge' (pluralize identityType)}}" class="button has-icon-right is-ghost is-compact" data-test-entity-merge-link=true> | ||||
|           Merge {{pluralize identityType}} | ||||
|           {{i-con glyph="chevron-right" size=11}} | ||||
|         </a> | ||||
|       {{/if}} | ||||
|       <a href="{{href-to 'vault.cluster.access.identity.create'}}" class="button has-icon-right is-ghost is-compact" data-test-entity-create-link=true> | ||||
|       <a href="{{href-to 'vault.cluster.access.identity.create' (pluralize identityType)}}" class="button has-icon-right is-ghost is-compact" data-test-entity-create-link=true> | ||||
|         Create {{identityType}} | ||||
|         {{i-con glyph="chevron-right" size=11}} | ||||
|       </a> | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| {{info-table-row label="Name" value=model.name }} | ||||
| {{info-table-row label="Name" value=model.name data-test-alias-name=true}} | ||||
| {{info-table-row label="ID" value=model.id }} | ||||
| {{#info-table-row label=(if (eq model.identityType "entity-alias") "Entity ID" "Group ID") value=model.canonicalId}} | ||||
|   <a href={{href-to 'vault.cluster.access.identity.show' (if (eq model.identityType "entity-alias") "entities" "groups") model.canonicalId "details"}} | ||||
|   | ||||
| @@ -8,6 +8,9 @@ | ||||
|         {{value}} | ||||
|       </div> | ||||
|       <div class="column has-text-right"> | ||||
|         {{#if model.canEdit}} | ||||
|           {{identity/popup-metadata params=(array model key)}} | ||||
|         {{/if}} | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
|   | ||||
| @@ -18,6 +18,7 @@ | ||||
|         <code class="has-text-grey is-size-8">{{item.mountAccessor}}</code> | ||||
|       </div> | ||||
|       <div class="column has-text-right"> | ||||
|         {{identity/popup-alias params=(array item)}} | ||||
|       </div> | ||||
|     </div> | ||||
|   {{/linked-block}} | ||||
|   | ||||
| @@ -1,4 +1,20 @@ | ||||
| {{info-table-row label="Name" value=model.name }} | ||||
| {{#if model.disabled}} | ||||
|   <div class="box is-shadowless is-marginless"> | ||||
|     {{#message-in-page type="warning" yieldWithoutColumn=true messageClass="message-body is-marginless" data-test-disabled-warning=true}} | ||||
|       <div class="column"> | ||||
|         <strong>Attention</strong> This {{model.identityType}} is disabled. All associated tokens cannot be used, but are not revoked. | ||||
|       </div> | ||||
|       {{#if model.canEdit}} | ||||
|         <div class="column is-flex-v-centered is-narrow"> | ||||
|           <button type="button" class="button is-orange box" {{action "enable" model}} data-test-enable=true> | ||||
|             Enable | ||||
|           </button> | ||||
|         </div> | ||||
|       {{/if}} | ||||
|     {{/message-in-page}} | ||||
|   </div> | ||||
| {{/if}} | ||||
| {{info-table-row label="Name" value=model.name data-test-identity-item-name=true}} | ||||
| {{info-table-row label="Type" value=model.type }} | ||||
| {{info-table-row label="ID" value=model.id }} | ||||
| {{#info-table-row label="Merged Ids" value=model.mergedEntityIds }} | ||||
|   | ||||
| @@ -1,21 +1,56 @@ | ||||
| {{#if model.hasMembers}} | ||||
|   {{#each model.memberGroupIds as |gid|}} | ||||
|     <a href={{href-to "vault.cluster.access.identity.show" "groups" gid "details" }} | ||||
|  | ||||
|   {{#linked-block | ||||
|     "vault.cluster.access.identity.show" | ||||
|     "groups" | ||||
|     gid | ||||
|     details | ||||
|     class="box is-sideless is-marginless" | ||||
|   }} | ||||
|     <div class="columns is-mobile"> | ||||
|       <div class="column is-10"> | ||||
|         <a href={{href-to "vault.cluster.access.identity.show" "groups" gid "details" }} | ||||
|           class="is-block has-text-black has-text-weight-semibold" | ||||
|           >{{i-con | ||||
|             glyph='folder' | ||||
|             size=14 | ||||
|             class="has-text-grey-light" | ||||
|             }}{{gid}}</a> | ||||
|       </div> | ||||
|       <div class="column has-text-right"> | ||||
|         {{#if model.canEdit}} | ||||
|           {{identity/popup-members params=(array model "memberGroupIds" gid)}} | ||||
|         {{/if}} | ||||
|       </div> | ||||
|     </div> | ||||
|     {{/linked-block}} | ||||
|   {{/each}} | ||||
|   {{#each model.memberEntityIds as |gid|}} | ||||
|     <a href={{href-to "vault.cluster.access.identity.show" "entities" gid "details" }} | ||||
|     {{#linked-block | ||||
|       "vault.cluster.access.identity.show" | ||||
|       "groups" | ||||
|       gid | ||||
|       details | ||||
|       class="box is-sideless is-marginless" | ||||
|     }} | ||||
|       <div class="columns"> | ||||
|         <div class="column is-10"> | ||||
|           <a href={{href-to "vault.cluster.access.identity.show" "entities" gid "details" }} | ||||
|             class="is-block has-text-black has-text-weight-semibold" | ||||
|             >{{i-con | ||||
|               glyph='role' | ||||
|               size=14 | ||||
|               class="has-text-grey-light" | ||||
|               }}{{gid}}</a> | ||||
|         </div> | ||||
|         <div class="column has-text-right"> | ||||
|           {{#if model.canEdit}} | ||||
|             {{identity/popup-members params=(array model "memberEntityIds" gid)}} | ||||
|           {{/if}} | ||||
|         </div> | ||||
|       </div> | ||||
|     {{/linked-block}} | ||||
|   {{/each}} | ||||
| {{else}} | ||||
|   <div class="box is-bottomless has-background-white-bis"> | ||||
|   | ||||
| @@ -8,6 +8,9 @@ | ||||
|         {{value}} | ||||
|       </div> | ||||
|       <div class="column has-text-right"> | ||||
|         {{#if model.canEdit}} | ||||
|           {{identity/popup-metadata params=(array model key)}} | ||||
|         {{/if}} | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| {{#each model.policies as |item|}} | ||||
| {{#each model.policies as |policyName|}} | ||||
|   {{#linked-block | ||||
|     "vault.cluster.policy.show" | ||||
|     "acl" | ||||
| @@ -7,12 +7,15 @@ | ||||
|   }} | ||||
|     <div class="columns is-mobile"> | ||||
|       <div class="column is-10"> | ||||
|         <a href={{href-to "vault.cluster.policy.show" "acl" item}} | ||||
|           class="has-text-black has-text-weight-semibold" | ||||
|           ><span class="is-underline">{{item}}</span> | ||||
|         <a href={{href-to "vault.cluster.policy.show" "acl" policyName}} | ||||
|           class="is-block has-text-black has-text-weight-semibold" | ||||
|           ><span class="is-underline">{{policyName}}</span> | ||||
|         </a> | ||||
|       </div> | ||||
|       <div class="column has-text-right"> | ||||
|         {{#if model.canEdit}} | ||||
|           {{identity/popup-policy params=(array model policyName)}} | ||||
|         {{/if}} | ||||
|       </div> | ||||
|     </div> | ||||
|   {{/linked-block}} | ||||
|   | ||||
							
								
								
									
										45
									
								
								ui/app/templates/components/identity/popup-alias.hbs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								ui/app/templates/components/identity/popup-alias.hbs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| {{#popup-menu name="alias-menu"}} | ||||
|   {{#with params.firstObject as |item|}} | ||||
|   <nav class="menu"> | ||||
|     <ul class="menu-list"> | ||||
|       <li class="action"> | ||||
|         <a href={{href-to "vault.cluster.access.identity.aliases.show" (pluralize item.parentType) item.id "details" }}> | ||||
|           Details | ||||
|         </a> | ||||
|       </li> | ||||
|       {{#if item.updatePath.isPending}} | ||||
|         <li class="action"> | ||||
|           <button disabled=true type="button" class="link button is-loading is-transparent"> | ||||
|             loading | ||||
|           </button> | ||||
|         </li> | ||||
|       {{else}} | ||||
|         {{#if item.canEdit}} | ||||
|           <li class="action"> | ||||
|             <a href={{href-to "vault.cluster.access.identity.aliases.edit" (pluralize item.parentType) item.id}}> | ||||
|               Edit | ||||
|             </a> | ||||
|           </li> | ||||
|         {{/if}} | ||||
|         {{#if item.canDelete}} | ||||
|           <li class="action"> | ||||
|             {{#confirm-action | ||||
|               data-test-item-delete=true | ||||
|               confirmButtonClasses="button is-primary" | ||||
|               buttonClasses="link" | ||||
|               onConfirmAction=(action "performTransaction" item) | ||||
|               confirmMessage=(concat "Are you sure you want to delete " item.id "?") | ||||
|               showConfirm=(get this (concat "shouldDelete-" item.id)) | ||||
|               class=(if (get this (concat "shouldDelete-" item.id)) "message is-block is-warning is-outline") | ||||
|               containerClasses="message-body is-block" | ||||
|               messageClasses="is-block" | ||||
|             }} | ||||
|               Delete | ||||
|             {{/confirm-action}} | ||||
|           </li> | ||||
|         {{/if}} | ||||
|       {{/if}} | ||||
|     </ul> | ||||
|   </nav> | ||||
| {{/with}} | ||||
| {{/popup-menu}} | ||||
							
								
								
									
										21
									
								
								ui/app/templates/components/identity/popup-members.hbs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								ui/app/templates/components/identity/popup-members.hbs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| {{#popup-menu name="member-edit-menu"}} | ||||
|   <nav class="menu"> | ||||
|     <ul class="menu-list"> | ||||
|       <li class="action"> | ||||
|         {{#confirm-action | ||||
|           confirmButtonClasses="button is-primary" | ||||
|           confirmButtonText="Remove" | ||||
|           buttonClasses="link" | ||||
|           onConfirmAction=(action "performTransaction" model groupArray memberId) | ||||
|           confirmMessage=(concat "Are you sure you want to remove " memberId "?") | ||||
|           showConfirm=(get this (concat "shouldDelete-" memberId)) | ||||
|           class=(if (get this (concat "shouldDelete-" memberId)) "message is-block is-warning is-outline") | ||||
|           containerClasses="message-body is-block" | ||||
|           messageClasses="is-block" | ||||
|         }} | ||||
|           Remove | ||||
|         {{/confirm-action}} | ||||
|       </li> | ||||
|     </ul> | ||||
|   </nav> | ||||
| {{/popup-menu}} | ||||
							
								
								
									
										21
									
								
								ui/app/templates/components/identity/popup-metadata.hbs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								ui/app/templates/components/identity/popup-metadata.hbs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| {{#popup-menu name="metadata-edit-menu"}} | ||||
|   <nav class="menu"> | ||||
|     <ul class="menu-list"> | ||||
|       <li class="action"> | ||||
|         {{#confirm-action | ||||
|           confirmButtonClasses="button is-primary" | ||||
|           confirmButtonText="Remove" | ||||
|           buttonClasses="link" | ||||
|           onConfirmAction=(action "performTransaction" model key) | ||||
|           confirmMessage=(concat "Are you sure you want to remove " key "?") | ||||
|           showConfirm=(get this (concat "shouldDelete-" key)) | ||||
|           class=(if (get this (concat "shouldDelete-" key)) "message is-block is-warning is-outline") | ||||
|           containerClasses="message-body is-block" | ||||
|           messageClasses="is-block" | ||||
|         }} | ||||
|           Remove | ||||
|         {{/confirm-action}} | ||||
|       </li> | ||||
|     </ul> | ||||
|   </nav> | ||||
| {{/popup-menu}} | ||||
							
								
								
									
										31
									
								
								ui/app/templates/components/identity/popup-policy.hbs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								ui/app/templates/components/identity/popup-policy.hbs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| {{#popup-menu name="policy-menu"}} | ||||
|   <nav class="menu"> | ||||
|     <ul class="menu-list"> | ||||
|       <li class="action"> | ||||
|         <a href={{href-to "vault.cluster.policy.show" "acl" policyName }}> | ||||
|           View Policy | ||||
|         </a> | ||||
|       </li> | ||||
|       <li class="action"> | ||||
|         <a href={{href-to "vault.cluster.policy.edit" "acl" policyName }}> | ||||
|           Edit Policy | ||||
|         </a> | ||||
|       </li> | ||||
|       <li class="action"> | ||||
|         {{#confirm-action | ||||
|           confirmButtonClasses="button is-primary" | ||||
|           confirmButtonText="Remove" | ||||
|           buttonClasses="link" | ||||
|           onConfirmAction=(action "performTransaction" model policyName) | ||||
|           confirmMessage=(concat "Are you sure you want to remove " policyName "?") | ||||
|           showConfirm=(get this (concat "shouldDelete-" policyName)) | ||||
|           class=(if (get this (concat "shouldDelete-" policyName)) "message is-block is-warning is-outline") | ||||
|           containerClasses="message-body is-block" | ||||
|           messageClasses="is-block" | ||||
|         }} | ||||
|           Remove from {{model.identityType}} | ||||
|         {{/confirm-action}} | ||||
|       </li> | ||||
|     </ul> | ||||
|   </nav> | ||||
| {{/popup-menu}} | ||||
| @@ -8,11 +8,15 @@ | ||||
|         excludeIconClass=true | ||||
|       }} | ||||
|     </div> | ||||
|     {{#if yieldWithoutColumn}} | ||||
|       {{yield}} | ||||
|     {{else}} | ||||
|       <div class="column"> | ||||
|         <p> | ||||
|         <strong>{{alertType.text}}</strong> | ||||
|         {{yield}} | ||||
|         </p> | ||||
|       </div> | ||||
|     {{/if}} | ||||
|   </div> | ||||
| </div> | ||||
|   | ||||
| @@ -8,4 +8,4 @@ | ||||
|   </div> | ||||
| </header> | ||||
|  | ||||
| {{identity/edit-form model=model onSave=(perform navToShow)}} | ||||
| {{identity/edit-form model=model onSave=(perform navAfterSave)}} | ||||
|   | ||||
| @@ -8,4 +8,4 @@ | ||||
|   </div> | ||||
| </header> | ||||
|  | ||||
| {{identity/edit-form mode="edit" model=model onSave=(perform navToShow)}} | ||||
| {{identity/edit-form mode="edit" model=model onSave=(perform navAfterSave)}} | ||||
|   | ||||
| @@ -1,23 +1,33 @@ | ||||
| {{identity/entity-nav identityType=identityType}} | ||||
| {{#if model.meta.total}} | ||||
|   {{#each model as |item|}} | ||||
|     {{#linked-block | ||||
|       "vault.cluster.access.identity.aliases.show" | ||||
|       item.id | ||||
|       "details" | ||||
|       class="box is-sideless is-marginless" | ||||
|       data-test-identity-row=true | ||||
|       }} | ||||
|       <div class="columns is-mobile"> | ||||
|         <div class="column is-10"> | ||||
|           <a href={{href-to | ||||
|             "vault.cluster.access.identity.aliases.show" | ||||
|             item.id | ||||
|              "details" | ||||
|             }} | ||||
|       class="is-flex box is-sideless is-marginless" | ||||
|       data-test-lease-link={{item.id}} | ||||
|     > | ||||
|       {{i-con | ||||
|             class="is-block has-text-black has-text-weight-semibold" | ||||
|             data-test-identity-link={{item.id}} | ||||
|           >{{i-con | ||||
|               glyph="role" | ||||
|               size=14 | ||||
|               class="has-text-grey-light" | ||||
|       }} | ||||
|       <span class="has-text-weight-semibold"> | ||||
|         {{item.id}} | ||||
|       </span> | ||||
|     </a> | ||||
|             }}<span class="has-text-weight-semibold">{{item.id}}</span></a> | ||||
|         </div> | ||||
|         <div class="column has-text-right"> | ||||
|           {{identity/popup-alias params=(array item) onSuccess=(action "onDelete")}} | ||||
|         </div> | ||||
|       </div> | ||||
|     {{/linked-block}} | ||||
|   {{/each}} | ||||
| {{else}} | ||||
|   <div class="box is-bottomless has-background-white-bis"> | ||||
|   | ||||
| @@ -16,7 +16,7 @@ | ||||
|       </h1> | ||||
|     </div> | ||||
|     <div class="level-right"> | ||||
|       <a href="{{href-to 'vault.cluster.access.identity.aliases.edit' model.id}}" class="button has-icon-right is-ghost is-compact" data-test-entity-create-link=true> | ||||
|       <a href="{{href-to 'vault.cluster.access.identity.aliases.edit' model.id}}" class="button has-icon-right is-ghost is-compact" data-test-alias-edit-link=true> | ||||
|         Edit {{lowercase (humanize model.identityType)}} | ||||
|         {{i-con glyph="chevron-right" size=11}} | ||||
|       </a> | ||||
|   | ||||
| @@ -8,4 +8,4 @@ | ||||
|   </div> | ||||
| </header> | ||||
|  | ||||
| {{identity/edit-form model=model onSave=(perform navToShow)}} | ||||
| {{identity/edit-form model=model onSave=(perform navAfterSave)}} | ||||
|   | ||||
| @@ -8,4 +8,4 @@ | ||||
|   </div> | ||||
| </header> | ||||
|  | ||||
| {{identity/edit-form mode="edit" model=model onSave=(perform navToShow)}} | ||||
| {{identity/edit-form mode="edit" model=model onSave=(perform navAfterSave)}} | ||||
|   | ||||
| @@ -1,23 +1,103 @@ | ||||
| {{identity/entity-nav identityType=identityType}} | ||||
| {{#if model.meta.total}} | ||||
|   {{#each model as |item|}} | ||||
|     {{#linked-block | ||||
|       "vault.cluster.access.identity.show" | ||||
|       item.id | ||||
|       "details" | ||||
|       class="box is-sideless is-marginless" | ||||
|       data-test-identity-row=true | ||||
|       }} | ||||
|       <div class="columns is-mobile"> | ||||
|         <div class="column is-10"> | ||||
|           <a href={{href-to | ||||
|             "vault.cluster.access.identity.show" | ||||
|             item.id | ||||
|             "details" | ||||
|             }} | ||||
|       class="is-flex box is-sideless is-marginless" | ||||
|       data-test-lease-link={{item.id}} | ||||
|     > | ||||
|       {{i-con | ||||
|             class="is-block has-text-black has-text-weight-semibold" | ||||
|             data-test-identity-link=true | ||||
|           >{{i-con | ||||
|               glyph="role" | ||||
|               size=14 | ||||
|               class="has-text-grey-light" | ||||
|       }} | ||||
|       <span class="has-text-weight-semibold"> | ||||
|         {{item.id}} | ||||
|       </span> | ||||
|             }}<span class="has-text-weight-semibold">{{item.id}}</span></a> | ||||
|         </div> | ||||
|         <div class="column has-text-right"> | ||||
|           {{#popup-menu name="identity-item" onOpen=(action "reloadRecord" item)}} | ||||
|             <nav class="menu"> | ||||
|               <ul class="menu-list"> | ||||
|                 <li class="action"> | ||||
|                   <a href={{href-to "vault.cluster.access.identity.show" item.id "details" }}> | ||||
|                     Details | ||||
|                   </a> | ||||
|                 </li> | ||||
|                 {{#if (or item.isReloading item.updatePath.isPending item.aliasPath.isPending)}} | ||||
|                   <li class="action"> | ||||
|                     <button disabled=true type="button" class="link button is-loading is-transparent"> | ||||
|                       loading | ||||
|                     </button> | ||||
|                   </li> | ||||
|                 {{else}} | ||||
|                   {{#if item.canEdit}} | ||||
|                     <li class="action"> | ||||
|                       <a href={{href-to 'vault.cluster.access.identity.edit' item.id}}> | ||||
|                         Edit | ||||
|                       </a> | ||||
|                     </li> | ||||
|                     <li class="action"> | ||||
|                       {{#if item.disabled}} | ||||
|                         <button type="button" {{action "toggleDisabled" item}} class="link"> | ||||
|                           Enable | ||||
|                         </button> | ||||
|                       {{else}} | ||||
|                         {{#confirm-action | ||||
|                           confirmButtonClasses="button is-primary" | ||||
|                           confirmButtonText="Disable" | ||||
|                           buttonClasses="link" | ||||
|                           onConfirmAction=(action "toggleDisabled" item) | ||||
|                           confirmMessage=(concat "Are you sure you want to disable " item.id "?") | ||||
|                           showConfirm=(get this (concat "shouldDisable-" item.id)) | ||||
|                           class=(if (get this (concat "shouldDisable-" item.id)) "message is-block is-warning is-outline") | ||||
|                           containerClasses="message-body is-block" | ||||
|                           messageClasses="is-block" | ||||
|                         }} | ||||
|                           Disable | ||||
|                         {{/confirm-action}} | ||||
|                       {{/if}} | ||||
|                     </li> | ||||
|                   {{/if}} | ||||
|                   {{#if item.canAddAlias}} | ||||
|                     <li class="action"> | ||||
|                       <a href={{href-to 'vault.cluster.access.identity.aliases.add' (pluralize identityType) item.id}}> | ||||
|                         Add alias | ||||
|                       </a> | ||||
|                     </li> | ||||
|                   {{/if}} | ||||
|                   {{#if item.canDelete}} | ||||
|                     <li class="action"> | ||||
|                       {{#confirm-action | ||||
|                         data-test-item-delete=true | ||||
|                         confirmButtonClasses="button is-primary" | ||||
|                         buttonClasses="link" | ||||
|                         onConfirmAction=(action "delete" item) | ||||
|                         confirmMessage=(concat "Are you sure you want to delete " item.id "?") | ||||
|                         showConfirm=(get this (concat "shouldDelete-" item.id)) | ||||
|                         class=(if (get this (concat "shouldDelete-" item.id)) "message is-block is-warning is-outline") | ||||
|                         containerClasses="message-body is-block" | ||||
|                         messageClasses="is-block" | ||||
|                       }} | ||||
|                         Delete | ||||
|                       {{/confirm-action}} | ||||
|                     </li> | ||||
|                   {{/if}} | ||||
|                 {{/if}} | ||||
|               </ul> | ||||
|             </nav> | ||||
|           {{/popup-menu}} | ||||
|         </div> | ||||
|       </div> | ||||
|     {{/linked-block}} | ||||
|   {{/each}} | ||||
| {{else}} | ||||
|   <div class="box is-bottomless has-background-white-bis"> | ||||
|   | ||||
| @@ -8,4 +8,4 @@ | ||||
|   </div> | ||||
| </header> | ||||
|  | ||||
| {{identity/edit-form mode="merge" model=model onSave=(perform navToShow)}} | ||||
| {{identity/edit-form mode="merge" model=model onSave=(perform navAfterSave)}} | ||||
|   | ||||
| @@ -17,12 +17,12 @@ | ||||
|     </div> | ||||
|     <div class="level-right"> | ||||
|       {{#unless (or (and (eq model.identityType "group") (eq model.type "internal")) model.alias)}} | ||||
|         <a href="{{href-to 'vault.cluster.access.identity.aliases.add' model.id}}" class="button has-icon-right is-ghost is-compact" data-test-entity-create-link=true> | ||||
|         <a href="{{href-to 'vault.cluster.access.identity.aliases.add' (pluralize model.identityType) model.id}}" class="button has-icon-right is-ghost is-compact" data-test-entity-create-link=true> | ||||
|           Add alias | ||||
|           {{i-con glyph="chevron-right" size=11}} | ||||
|         </a> | ||||
|       {{/unless}} | ||||
|       <a href="{{href-to 'vault.cluster.access.identity.edit' model.id}}" class="button has-icon-right is-ghost is-compact" data-test-entity-create-link=true> | ||||
|       <a href="{{href-to 'vault.cluster.access.identity.edit' (pluralize model.identityType) model.id}}" class="button has-icon-right is-ghost is-compact" data-test-entity-edit-link=true> | ||||
|         Edit {{model.identityType}} | ||||
|         {{i-con glyph="chevron-right" size=11}} | ||||
|       </a> | ||||
| @@ -34,7 +34,7 @@ | ||||
|     <ul> | ||||
|       {{#each (tabs-for-identity-show model.identityType model.type) as |tab|}} | ||||
|         {{#link-to "vault.cluster.access.identity.show" model.id tab tagName="li"}} | ||||
|           <a href={{href-to "vault.cluster.access.identity.show" model.id tab }}> | ||||
|           <a href={{href-to "vault.cluster.access.identity.show" (pluralize model.identityType) model.id tab }}> | ||||
|             {{capitalize tab}} | ||||
|           </a> | ||||
|         {{/link-to}} | ||||
|   | ||||
| @@ -92,7 +92,7 @@ | ||||
|       </div> | ||||
|       {{#if (and (not-eq model.id "default") capabilities.canDelete)}} | ||||
|         {{#confirm-action | ||||
|           buttonClasses="button is-link is-outlined is-inverted" | ||||
|           buttonClasses="button is-ghost" | ||||
|           onConfirmAction=(action "deletePolicy" model) | ||||
|           confirmMessage=(concat "Are you sure you want to delete " model.id "?") | ||||
|           data-test-policy-delete=true | ||||
|   | ||||
							
								
								
									
										77
									
								
								ui/tests/acceptance/access/identity/_shared-alias-tests.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								ui/tests/acceptance/access/identity/_shared-alias-tests.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,77 @@ | ||||
| import page from 'vault/tests/pages/access/identity/aliases/add'; | ||||
| import aliasIndexPage from 'vault/tests/pages/access/identity/aliases/index'; | ||||
| import aliasShowPage from 'vault/tests/pages/access/identity/aliases/show'; | ||||
| import createItemPage from 'vault/tests/pages/access/identity/create'; | ||||
| import showItemPage from 'vault/tests/pages/access/identity/show'; | ||||
|  | ||||
| export const testAliasCRUD = (name, itemType, assert) => { | ||||
|   let itemID; | ||||
|   let aliasID; | ||||
|   if (itemType === 'groups') { | ||||
|     createItemPage.createItem(itemType, 'external'); | ||||
|   } else { | ||||
|     createItemPage.createItem(itemType); | ||||
|   } | ||||
|   andThen(() => { | ||||
|     let idRow = showItemPage.rows.filterBy('hasLabel').filterBy('rowLabel', 'ID')[0]; | ||||
|     itemID = idRow.rowValue; | ||||
|     page.visit({ item_type: itemType, id: itemID }); | ||||
|   }); | ||||
|   page.editForm.name(name).submit(); | ||||
|   andThen(() => { | ||||
|     let idRow = aliasShowPage.rows.filterBy('hasLabel').filterBy('rowLabel', 'ID')[0]; | ||||
|     aliasID = idRow.rowValue; | ||||
|     assert.equal( | ||||
|       currentRouteName(), | ||||
|       'vault.cluster.access.identity.aliases.show', | ||||
|       'navigates to the correct route' | ||||
|     ); | ||||
|     assert.ok( | ||||
|       aliasShowPage.flashMessage.latestMessage.startsWith('Successfully saved', `${itemType}: shows a flash message`) | ||||
|     ); | ||||
|     assert.ok(aliasShowPage.nameContains(name), `${itemType}: renders the name on the show page`); | ||||
|   }); | ||||
|  | ||||
|   aliasIndexPage.visit({ item_type: itemType }); | ||||
|   andThen(() => { | ||||
|     assert.equal(aliasIndexPage.items.filterBy('id', aliasID).length, 1, `${itemType}: lists the entity in the entity list`); | ||||
|     aliasIndexPage.items.filterBy('id', aliasID)[0].menu(); | ||||
|   }); | ||||
|   aliasIndexPage.delete().confirmDelete(); | ||||
|  | ||||
|   andThen(() => { | ||||
|     assert.equal(aliasIndexPage.items.filterBy('id', aliasID).length, 0, `${itemType}: the row is deleted`); | ||||
|     aliasIndexPage.flashMessage.latestMessage.startsWith('Successfully deleted', `${itemType}: shows flash message`); | ||||
|   }); | ||||
| }; | ||||
|  | ||||
| export const testAliasDeleteFromForm = (name, itemType, assert) => { | ||||
|   let itemID; | ||||
|   let aliasID; | ||||
|   if (itemType === 'groups') { | ||||
|     createItemPage.createItem(itemType, 'external'); | ||||
|   } else { | ||||
|     createItemPage.createItem(itemType); | ||||
|   } | ||||
|   andThen(() => { | ||||
|     let idRow = showItemPage.rows.filterBy('hasLabel').filterBy('rowLabel', 'ID')[0]; | ||||
|     itemID = idRow.rowValue; | ||||
|     page.visit({ item_type: itemType, id: itemID }); | ||||
|   }); | ||||
|   page.editForm.name(name).submit(); | ||||
|   andThen(() => { | ||||
|     let idRow = aliasShowPage.rows.filterBy('hasLabel').filterBy('rowLabel', 'ID')[0]; | ||||
|     aliasID = idRow.rowValue; | ||||
|   }); | ||||
|   aliasShowPage.edit(); | ||||
|  | ||||
|   andThen(() => { | ||||
|     assert.equal(currentRouteName(), 'vault.cluster.access.identity.aliases.edit', `${itemType}: navigates to edit on create`); | ||||
|   }); | ||||
|   page.editForm.delete().confirmDelete(); | ||||
|   andThen(() => { | ||||
|     assert.equal(currentRouteName(), 'vault.cluster.access.identity.aliases.index', `${itemType}: navigates to list page on delete`); | ||||
|     assert.equal(aliasIndexPage.items.filterBy('id', aliasID).length, 0, `${itemType}: the row does not show in the list`); | ||||
|     aliasIndexPage.flashMessage.latestMessage.startsWith('Successfully deleted', `${itemType}: shows flash message`); | ||||
|   }); | ||||
| }; | ||||
							
								
								
									
										51
									
								
								ui/tests/acceptance/access/identity/_shared-tests.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								ui/tests/acceptance/access/identity/_shared-tests.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | ||||
| import page from 'vault/tests/pages/access/identity/create'; | ||||
| import showPage from 'vault/tests/pages/access/identity/show'; | ||||
| import indexPage from 'vault/tests/pages/access/identity/index'; | ||||
|  | ||||
| export const testCRUD = (name, itemType, assert) => { | ||||
|   let id; | ||||
|   page.visit({ item_type: itemType }); | ||||
|   page.editForm.name(name).submit(); | ||||
|   andThen(() => { | ||||
|     let idRow = showPage.rows.filterBy('hasLabel').filterBy('rowLabel', 'ID')[0]; | ||||
|     id = idRow.rowValue; | ||||
|     assert.equal(currentRouteName(), 'vault.cluster.access.identity.show', `${itemType}: navigates to show on create`); | ||||
|     assert.ok( | ||||
|       showPage.flashMessage.latestMessage.startsWith('Successfully saved', `${itemType}: shows a flash message`) | ||||
|     ); | ||||
|     assert.ok(showPage.nameContains(name), `${itemType}: renders the name on the show page`); | ||||
|   }); | ||||
|  | ||||
|   indexPage.visit({ item_type: itemType }); | ||||
|   andThen(() => { | ||||
|     assert.equal(indexPage.items.filterBy('id', id).length, 1, `${itemType}: lists the entity in the entity list`); | ||||
|     indexPage.items.filterBy('id', id)[0].menu(); | ||||
|   }); | ||||
|   indexPage.delete().confirmDelete(); | ||||
|  | ||||
|   andThen(() => { | ||||
|     assert.equal(indexPage.items.filterBy('id', id).length, 0, `${itemType}: the row is deleted`); | ||||
|     indexPage.flashMessage.latestMessage.startsWith('Successfully deleted', `${itemType}: shows flash message`); | ||||
|   }); | ||||
| }; | ||||
|  | ||||
|  | ||||
| export const testDeleteFromForm = (name, itemType, assert) => { | ||||
|   let id; | ||||
|   page.visit({ item_type: itemType }); | ||||
|   page.editForm.name(name).submit(); | ||||
|   andThen(() => { | ||||
|     id = showPage.rows.filterBy('hasLabel').filterBy('rowLabel', 'ID')[0].rowValue | ||||
|   }); | ||||
|   showPage.edit(); | ||||
|   andThen(() => { | ||||
|     assert.equal(currentRouteName(), 'vault.cluster.access.identity.edit', `${itemType}: navigates to edit on create`); | ||||
|   }); | ||||
|   page.editForm.delete().confirmDelete(); | ||||
|   andThen(() => { | ||||
|     assert.equal(currentRouteName(), 'vault.cluster.access.identity.index', `${itemType}: navigates to list page on delete`); | ||||
|     assert.equal(indexPage.items.filterBy('id', id).length, 0, `${itemType}: the row does not show in the list`); | ||||
|     indexPage.flashMessage.latestMessage.startsWith('Successfully deleted', `${itemType}: shows flash message`); | ||||
|   }); | ||||
| }; | ||||
|  | ||||
| @@ -0,0 +1,21 @@ | ||||
| import { test } from 'qunit'; | ||||
| import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance'; | ||||
| import { testAliasCRUD, testAliasDeleteFromForm } from '../../_shared-alias-tests'; | ||||
|  | ||||
| moduleForAcceptance('Acceptance | /access/identity/entities/aliases/add', { | ||||
|   beforeEach() { | ||||
|     return authLogin(); | ||||
|   }, | ||||
| }); | ||||
|  | ||||
|  | ||||
| test('it allows create, list, delete of an entity alias', function(assert) { | ||||
|   let name = `alias-${Date.now()}`; | ||||
|   testAliasCRUD(name, 'entities', assert); | ||||
| }); | ||||
|  | ||||
| test('it allows delete from the edit form', function(assert) { | ||||
|   let name = `alias-${Date.now()}`; | ||||
|   testAliasDeleteFromForm(name, 'entities', assert); | ||||
| }); | ||||
|  | ||||
							
								
								
									
										32
									
								
								ui/tests/acceptance/access/identity/entities/create-test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								ui/tests/acceptance/access/identity/entities/create-test.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| import { test } from 'qunit'; | ||||
| import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance'; | ||||
| import page from 'vault/tests/pages/access/identity/create'; | ||||
| import { testCRUD, testDeleteFromForm } from '../_shared-tests'; | ||||
|  | ||||
| moduleForAcceptance('Acceptance | /access/identity/entities/create', { | ||||
|   beforeEach() { | ||||
|     return authLogin(); | ||||
|   }, | ||||
| }); | ||||
|  | ||||
| test('it visits the correct page', function(assert) { | ||||
|   page.visit({ item_type: 'entities' }); | ||||
|   andThen(() => { | ||||
|     assert.equal( | ||||
|       currentRouteName(), | ||||
|       'vault.cluster.access.identity.create', | ||||
|       'navigates to the correct route' | ||||
|     ); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| test('it allows create, list, delete of an entity', function(assert) { | ||||
|   let name = `entity-${Date.now()}`; | ||||
|   testCRUD(name, 'entities', assert); | ||||
| }); | ||||
|  | ||||
| test('it can be deleted from the edit form', function(assert) { | ||||
|   let name = `entity-${Date.now()}`; | ||||
|   testDeleteFromForm(name, 'entities', assert); | ||||
| }); | ||||
|  | ||||
							
								
								
									
										23
									
								
								ui/tests/acceptance/access/identity/entities/index-test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								ui/tests/acceptance/access/identity/entities/index-test.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| import { test } from 'qunit'; | ||||
| import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance'; | ||||
| import page from 'vault/tests/pages/access/identity/index'; | ||||
|  | ||||
| moduleForAcceptance('Acceptance | /access/identity/entities', { | ||||
|   beforeEach() { | ||||
|     return authLogin(); | ||||
|   }, | ||||
| }); | ||||
|  | ||||
| test('it renders the entities page', function(assert) { | ||||
|   page.visit({ item_type: 'entities' }); | ||||
|   andThen(() => { | ||||
|     assert.equal(currentRouteName(), 'vault.cluster.access.identity.index', 'navigates to the correct route'); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| test('it renders the groups page', function(assert) { | ||||
|   page.visit({ item_type: 'groups' }); | ||||
|   andThen(() => { | ||||
|     assert.equal(currentRouteName(), 'vault.cluster.access.identity.index', 'navigates to the correct route'); | ||||
|   }); | ||||
| }); | ||||
| @@ -0,0 +1,21 @@ | ||||
| import { test } from 'qunit'; | ||||
| import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance'; | ||||
| import { testAliasCRUD, testAliasDeleteFromForm } from '../../_shared-alias-tests'; | ||||
|  | ||||
| moduleForAcceptance('Acceptance | /access/identity/groups/aliases/add', { | ||||
|   beforeEach() { | ||||
|     return authLogin(); | ||||
|   }, | ||||
| }); | ||||
|  | ||||
|  | ||||
| test('it allows create, list, delete of an entity alias', function(assert) { | ||||
|   let name = `alias-${Date.now()}`; | ||||
|   testAliasCRUD(name, 'groups', assert); | ||||
| }); | ||||
|  | ||||
| test('it allows delete from the edit form', function(assert) { | ||||
|   let name = `alias-${Date.now()}`; | ||||
|   testAliasDeleteFromForm(name, 'groups', assert); | ||||
| }); | ||||
|  | ||||
							
								
								
									
										31
									
								
								ui/tests/acceptance/access/identity/groups/create-test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								ui/tests/acceptance/access/identity/groups/create-test.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| import { test } from 'qunit'; | ||||
| import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance'; | ||||
| import page from 'vault/tests/pages/access/identity/create'; | ||||
| import { testCRUD, testDeleteFromForm } from '../_shared-tests'; | ||||
|  | ||||
| moduleForAcceptance('Acceptance | /access/identity/groups/create', { | ||||
|   beforeEach() { | ||||
|     return authLogin(); | ||||
|   }, | ||||
| }); | ||||
|  | ||||
| test('it visits the correct page', function(assert) { | ||||
|   page.visit({ item_type: 'groups' }); | ||||
|   andThen(() => { | ||||
|     assert.equal( | ||||
|       currentRouteName(), | ||||
|       'vault.cluster.access.identity.create', | ||||
|       'navigates to the correct route' | ||||
|     ); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| test('it allows create, list, delete of an group', function(assert) { | ||||
|   let name = `group-${Date.now()}`; | ||||
|   testCRUD(name, 'groups', assert); | ||||
| }); | ||||
|  | ||||
| test('it can be deleted from the group edit form', function(assert) { | ||||
|   let name = `group-${Date.now()}`; | ||||
|   testDeleteFromForm(name, 'groups', assert); | ||||
| }); | ||||
| @@ -1,16 +0,0 @@ | ||||
| import { test } from 'qunit'; | ||||
| import moduleForAcceptance from 'vault/tests/helpers/module-for-acceptance'; | ||||
| import page from 'vault/tests/pages/access/identity/index'; | ||||
|  | ||||
| moduleForAcceptance('Acceptance | /access/identity/entities', { | ||||
|   beforeEach() { | ||||
|     return authLogin(); | ||||
|   }, | ||||
| }); | ||||
|  | ||||
| test('it renders the page', function(assert) { | ||||
|   page.visit({ item_type: 'entities' }); | ||||
|   andThen(() => { | ||||
|     assert.ok(currentRouteName(), 'vault.cluster.access.identity.index', 'navigates to the correct route'); | ||||
|   }); | ||||
| }); | ||||
| @@ -87,7 +87,9 @@ test('replication', function(assert) { | ||||
|       find('[data-test-mount-config-mode]').text().trim().toLowerCase().includes(mode), | ||||
|       'show page renders the correct mode' | ||||
|     ); | ||||
|     assert.dom('[data-test-mount-config-paths]').hasText(mountPath, 'show page renders the correct mount path'); | ||||
|     assert | ||||
|       .dom('[data-test-mount-config-paths]') | ||||
|       .hasText(mountPath, 'show page renders the correct mount path'); | ||||
|   }); | ||||
|   // click edit | ||||
|  | ||||
| @@ -101,7 +103,9 @@ test('replication', function(assert) { | ||||
|       `/vault/replication/performance/secondaries`, | ||||
|       'redirects to the secondaries page' | ||||
|     ); | ||||
|     assert.dom('[data-test-flash-message-body]:contains(The performance mount filter)').hasText( | ||||
|     assert | ||||
|       .dom('[data-test-flash-message-body]:contains(The performance mount filter)') | ||||
|       .hasText( | ||||
|         `The performance mount filter config for the secondary ${secondaryName} was successfully deleted.`, | ||||
|         'renders success flash upon deletion' | ||||
|       ); | ||||
| @@ -149,10 +153,9 @@ test('replication', function(assert) { | ||||
|   }); | ||||
|   click('[data-test-replication-link="secondaries"]'); | ||||
|   andThen(() => { | ||||
|     assert.dom('[data-test-secondary-name]').hasText( | ||||
|       secondaryName, | ||||
|       'it displays the secondary in the list of known secondaries' | ||||
|     ); | ||||
|     assert | ||||
|       .dom('[data-test-secondary-name]') | ||||
|       .hasText(secondaryName, 'it displays the secondary in the list of known secondaries'); | ||||
|   }); | ||||
|  | ||||
|   // disable dr replication | ||||
|   | ||||
| @@ -51,7 +51,9 @@ test('it renders the show page', function(assert) { | ||||
|       'vault.cluster.access.leases.show', | ||||
|       'a lease for the secret is in the list' | ||||
|     ); | ||||
|     assert.dom('[data-test-lease-renew-picker]').doesNotExist('non-renewable lease does not render a renew picker'); | ||||
|     assert | ||||
|       .dom('[data-test-lease-renew-picker]') | ||||
|       .doesNotExist('non-renewable lease does not render a renew picker'); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| @@ -65,7 +67,9 @@ skip('it renders the show page with a picker', function(assert) { | ||||
|       'vault.cluster.access.leases.show', | ||||
|       'a lease for the secret is in the list' | ||||
|     ); | ||||
|     assert.dom('[data-test-lease-renew-picker]').exists({ count: 1 }, 'renewable lease renders a renew picker'); | ||||
|     assert | ||||
|       .dom('[data-test-lease-renew-picker]') | ||||
|       .exists({ count: 1 }, 'renewable lease renders a renew picker'); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| @@ -84,7 +88,9 @@ test('it removes leases upon revocation', function(assert) { | ||||
|   click(`[data-test-lease-link="${this.enginePath}/"]`); | ||||
|   click('[data-test-lease-link="data/"]'); | ||||
|   andThen(() => { | ||||
|     assert.dom(`[data-test-lease-link="${this.enginePath}/data/${this.name}/"]`).doesNotExist('link to the lease was removed with revocation'); | ||||
|     assert | ||||
|       .dom(`[data-test-lease-link="${this.enginePath}/data/${this.name}/"]`) | ||||
|       .doesNotExist('link to the lease was removed with revocation'); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| @@ -99,16 +105,17 @@ test('it removes branches when a prefix is revoked', function(assert) { | ||||
|       'vault.cluster.access.leases.list-root', | ||||
|       'it navigates back to the leases root on revocation' | ||||
|     ); | ||||
|     assert.dom(`[data-test-lease-link="${this.enginePath}/"]`).doesNotExist('link to the prefix was removed with revocation'); | ||||
|     assert | ||||
|       .dom(`[data-test-lease-link="${this.enginePath}/"]`) | ||||
|       .doesNotExist('link to the prefix was removed with revocation'); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| test('lease not found', function(assert) { | ||||
|   visit('/vault/access/leases/show/not-found'); | ||||
|   andThen(() => { | ||||
|     assert.dom('[data-test-lease-error]').hasText( | ||||
|       'not-found is not a valid lease ID', | ||||
|       'it shows an error when the lease is not found' | ||||
|     ); | ||||
|     assert | ||||
|       .dom('[data-test-lease-error]') | ||||
|       .hasText('not-found is not a valid lease ID', 'it shows an error when the lease is not found'); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -46,7 +46,9 @@ test('policies', function(assert) { | ||||
|   }); | ||||
|   click('[data-test-policy-list-link]'); | ||||
|   andThen(function() { | ||||
|     assert.dom(`[data-test-policy-link="${policyLower}"]`).exists({ count: 1 }, 'new policy shown in the list'); | ||||
|     assert | ||||
|       .dom(`[data-test-policy-link="${policyLower}"]`) | ||||
|       .exists({ count: 1 }, 'new policy shown in the list'); | ||||
|   }); | ||||
|  | ||||
|   // policy deletion | ||||
| @@ -56,7 +58,9 @@ test('policies', function(assert) { | ||||
|   click('[data-test-confirm-button]'); | ||||
|   andThen(function() { | ||||
|     assert.equal(currentURL(), `/vault/policies/acl`, 'navigates to policy list on successful deletion'); | ||||
|     assert.dom(`[data-test-policy-item="${policyLower}"]`).doesNotExist('deleted policy is not shown in the list'); | ||||
|     assert | ||||
|       .dom(`[data-test-policy-item="${policyLower}"]`) | ||||
|       .doesNotExist('deleted policy is not shown in the list'); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
|   | ||||
| @@ -46,10 +46,6 @@ test('settings', function(assert) { | ||||
|   }); | ||||
|  | ||||
|   andThen(() => { | ||||
|     assert.ok( | ||||
|       currentURL(), | ||||
|       '/vault/secrets/${path}/configuration', | ||||
|       'navigates to the config page' | ||||
|     ); | ||||
|     assert.ok(currentURL(), '/vault/secrets/${path}/configuration', 'navigates to the config page'); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -143,7 +143,9 @@ test('ssh backend', function(assert) { | ||||
|     click(`[data-test-confirm-button]`); | ||||
|  | ||||
|     andThen(() => { | ||||
|       assert.dom(`[data-test-secret-link="${role.name}"]`).doesNotExist(`${role.type}: role is no longer in the list`); | ||||
|       assert | ||||
|         .dom(`[data-test-secret-link="${role.name}"]`) | ||||
|         .doesNotExist(`${role.type}: role is no longer in the list`); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -118,42 +118,38 @@ test('tools functionality', function(assert) { | ||||
|   click('[data-test-tools-b64-toggle="input"]'); | ||||
|   click('[data-test-tools-submit]'); | ||||
|   andThen(() => { | ||||
|     assert.dom('[data-test-tools-input="sum"]').hasValue( | ||||
|       'LCa0a2j/xo/5m0U8HTBBNBNCLXBkg7+g+YpeiGJm564=', | ||||
|       'hashes the data, encodes input' | ||||
|     ); | ||||
|     assert | ||||
|       .dom('[data-test-tools-input="sum"]') | ||||
|       .hasValue('LCa0a2j/xo/5m0U8HTBBNBNCLXBkg7+g+YpeiGJm564=', 'hashes the data, encodes input'); | ||||
|   }); | ||||
|   click('[data-test-tools-back]'); | ||||
|   fillIn('[data-test-tools-input="hash-input"]', 'e2RhdGE6ImZvbyJ9'); | ||||
|  | ||||
|   click('[data-test-tools-submit]'); | ||||
|   andThen(() => { | ||||
|     assert.dom('[data-test-tools-input="sum"]').hasValue( | ||||
|       'JmSi2Hhbgu2WYOrcOyTqqMdym7KT3sohCwAwaMonVrc=', | ||||
|       'hashes the data, passes b64 input through' | ||||
|     ); | ||||
|     assert | ||||
|       .dom('[data-test-tools-input="sum"]') | ||||
|       .hasValue('JmSi2Hhbgu2WYOrcOyTqqMdym7KT3sohCwAwaMonVrc=', 'hashes the data, passes b64 input through'); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| const AUTH_RESPONSE = { | ||||
|   "request_id": "39802bc4-235c-2f0b-87f3-ccf38503ac3e", | ||||
|   "lease_id": "", | ||||
|   "renewable": false, | ||||
|   "lease_duration": 0, | ||||
|   "data": null, | ||||
|   "wrap_info": null, | ||||
|   "warnings": null, | ||||
|   "auth": { | ||||
|     "client_token": "ecfc2758-588e-981d-50f4-a25883bbf03c", | ||||
|     "accessor": "6299780b-f2b2-1a3f-7b83-9d3d67629249", | ||||
|     "policies": [ | ||||
|       "root" | ||||
|     ], | ||||
|     "metadata": null, | ||||
|     "lease_duration": 0, | ||||
|     "renewable": false, | ||||
|     "entity_id": "" | ||||
|   } | ||||
|   request_id: '39802bc4-235c-2f0b-87f3-ccf38503ac3e', | ||||
|   lease_id: '', | ||||
|   renewable: false, | ||||
|   lease_duration: 0, | ||||
|   data: null, | ||||
|   wrap_info: null, | ||||
|   warnings: null, | ||||
|   auth: { | ||||
|     client_token: 'ecfc2758-588e-981d-50f4-a25883bbf03c', | ||||
|     accessor: '6299780b-f2b2-1a3f-7b83-9d3d67629249', | ||||
|     policies: ['root'], | ||||
|     metadata: null, | ||||
|     lease_duration: 0, | ||||
|     renewable: false, | ||||
|     entity_id: '', | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| test('ensure unwrap with auth block works properly', function(assert) { | ||||
|   | ||||
| @@ -101,14 +101,18 @@ const testEncryption = (assert, keyName) => { | ||||
|         ); | ||||
|       }, | ||||
|       assertBeforeDecrypt: key => { | ||||
|         assert.dom('[data-test-transit-input="context"]').hasValue( | ||||
|         assert | ||||
|           .dom('[data-test-transit-input="context"]') | ||||
|           .hasValue( | ||||
|             'nqR8LiVgNh/lwO2rArJJE9F9DMhh0lKo4JX9DAAkCDw=', | ||||
|             `${key}: the ui shows the base64-encoded context` | ||||
|           ); | ||||
|       }, | ||||
|  | ||||
|       assertAfterDecrypt: key => { | ||||
|         assert.dom('[data-test-transit-input="plaintext"]').hasValue( | ||||
|         assert | ||||
|           .dom('[data-test-transit-input="plaintext"]') | ||||
|           .hasValue( | ||||
|             'NaXud2QW7KjyK6Me9ggh+zmnCeBGdG93LQED49PtoOI=', | ||||
|             `${key}: the ui shows the base64-encoded plaintext` | ||||
|           ); | ||||
| @@ -128,10 +132,14 @@ const testEncryption = (assert, keyName) => { | ||||
|         ); | ||||
|       }, | ||||
|       assertBeforeDecrypt: key => { | ||||
|         assert.dom('[data-test-transit-input="context"]').hasValue(encodeString('context'), `${key}: the ui shows the input context`); | ||||
|         assert | ||||
|           .dom('[data-test-transit-input="context"]') | ||||
|           .hasValue(encodeString('context'), `${key}: the ui shows the input context`); | ||||
|       }, | ||||
|       assertAfterDecrypt: key => { | ||||
|         assert.dom('[data-test-transit-input="plaintext"]').hasValue( | ||||
|         assert | ||||
|           .dom('[data-test-transit-input="plaintext"]') | ||||
|           .hasValue( | ||||
|             'NaXud2QW7KjyK6Me9ggh+zmnCeBGdG93LQED49PtoOI=', | ||||
|             `${key}: the ui shows the base64-encoded plaintext` | ||||
|           ); | ||||
| @@ -151,10 +159,14 @@ const testEncryption = (assert, keyName) => { | ||||
|         ); | ||||
|       }, | ||||
|       assertBeforeDecrypt: key => { | ||||
|         assert.dom('[data-test-transit-input="context"]').hasValue(encodeString('context'), `${key}: the ui shows the input context`); | ||||
|         assert | ||||
|           .dom('[data-test-transit-input="context"]') | ||||
|           .hasValue(encodeString('context'), `${key}: the ui shows the input context`); | ||||
|       }, | ||||
|       assertAfterDecrypt: key => { | ||||
|         assert.dom('[data-test-transit-input="plaintext"]').hasValue('This is the secret', `${key}: the ui decodes plaintext`); | ||||
|         assert | ||||
|           .dom('[data-test-transit-input="plaintext"]') | ||||
|           .hasValue('This is the secret', `${key}: the ui decodes plaintext`); | ||||
|       }, | ||||
|     }, | ||||
|  | ||||
| @@ -173,11 +185,15 @@ const testEncryption = (assert, keyName) => { | ||||
|         ); | ||||
|       }, | ||||
|       assertBeforeDecrypt: key => { | ||||
|         assert.dom('[data-test-transit-input="context"]').hasValue(encodeString('secret 2'), `${key}: the ui shows the encoded context`); | ||||
|         assert | ||||
|           .dom('[data-test-transit-input="context"]') | ||||
|           .hasValue(encodeString('secret 2'), `${key}: the ui shows the encoded context`); | ||||
|       }, | ||||
|       assertAfterDecrypt: key => { | ||||
|         assert.ok(findWithAssert('[data-test-transit-input="plaintext"]'), `${key}: plaintext box shows`); | ||||
|         assert.dom('[data-test-transit-input="plaintext"]').hasValue('There are many secrets 🤐', `${key}: the ui decodes plaintext`); | ||||
|         assert | ||||
|           .dom('[data-test-transit-input="plaintext"]') | ||||
|           .hasValue('There are many secrets 🤐', `${key}: the ui decodes plaintext`); | ||||
|       }, | ||||
|     }, | ||||
|   ]; | ||||
| @@ -229,12 +245,16 @@ test('transit backend', function(assert) { | ||||
|     if (index === 0) { | ||||
|       click('[data-test-transit-link="versions"]'); | ||||
|       andThen(() => { | ||||
|         assert.dom('[data-test-transit-key-version-row]').exists({ count: 1 }, `${key.name}: only one key version`); | ||||
|         assert | ||||
|           .dom('[data-test-transit-key-version-row]') | ||||
|           .exists({ count: 1 }, `${key.name}: only one key version`); | ||||
|       }); | ||||
|       click('[data-test-transit-key-rotate] button'); | ||||
|       click('[data-test-confirm-button]'); | ||||
|       andThen(() => { | ||||
|         assert.dom('[data-test-transit-key-version-row]').exists({ count: 2 }, `${key.name}: two key versions after rotate`); | ||||
|         assert | ||||
|           .dom('[data-test-transit-key-version-row]') | ||||
|           .exists({ count: 2 }, `${key.name}: two key versions after rotate`); | ||||
|       }); | ||||
|     } | ||||
|     click('[data-test-transit-key-actions-link]'); | ||||
| @@ -256,7 +276,9 @@ test('transit backend', function(assert) { | ||||
|           `${key.name}: exportable key has a link to export action` | ||||
|         ); | ||||
|       } else { | ||||
|         assert.dom('[data-test-transit-action-link="export"]').doesNotExist(`${key.name}: non-exportable key does not link to export action`); | ||||
|         assert | ||||
|           .dom('[data-test-transit-action-link="export"]') | ||||
|           .doesNotExist(`${key.name}: non-exportable key does not link to export action`); | ||||
|       } | ||||
|       if (key.convergent && key.supportsEncryption) { | ||||
|         testEncryption(assert, key.name); | ||||
|   | ||||
| @@ -0,0 +1,56 @@ | ||||
| import { moduleForComponent, test } from 'ember-qunit'; | ||||
| import hbs from 'htmlbars-inline-precompile'; | ||||
| import sinon from 'sinon'; | ||||
| import { create } from 'ember-cli-page-object'; | ||||
| import itemDetails from 'vault/tests/pages/components/identity/item-details'; | ||||
| import Ember from 'ember'; | ||||
|  | ||||
| const component = create(itemDetails); | ||||
| const { getOwner } = Ember; | ||||
|  | ||||
| moduleForComponent('identity/item-details', 'Integration | Component | identity/item details', { | ||||
|   integration: true, | ||||
|   beforeEach() { | ||||
|     component.setContext(this); | ||||
|     getOwner(this).lookup('service:flash-messages').registerTypes(['success']); | ||||
|   }, | ||||
|   afterEach() { | ||||
|     component.removeContext(); | ||||
|   } | ||||
| }); | ||||
|  | ||||
| test('it renders the disabled warning', function(assert) { | ||||
|   let model = Ember.Object.create({ | ||||
|     save() { | ||||
|       return Ember.RSVP.resolve(); | ||||
|     }, | ||||
|     disabled: true, | ||||
|     canEdit: true | ||||
|   }); | ||||
|   sinon.spy(model, 'save'); | ||||
|   this.set('model', model); | ||||
|   this.render(hbs`{{identity/item-details model=model}}`); | ||||
|   assert.dom('[data-test-disabled-warning]').exists(); | ||||
|   component.enable(); | ||||
|  | ||||
|   assert.ok(model.save.calledOnce, 'clicking enable calls model save'); | ||||
| }); | ||||
|  | ||||
| test('it does not render the button if canEdit is false', function(assert) { | ||||
|   let model = Ember.Object.create({ | ||||
|     disabled: true | ||||
|   }); | ||||
|  | ||||
|   this.set('model', model); | ||||
|   this.render(hbs`{{identity/item-details model=model}}`); | ||||
|   assert.dom('[data-test-disabled-warning]').exists('shows the warning banner'); | ||||
|   assert.dom('[data-test-enable]').doesNotExist('does not show the enable button'); | ||||
| }); | ||||
|  | ||||
| test('it does not render the banner when item is enabled', function(assert) { | ||||
|   let model = Ember.Object.create(); | ||||
|   this.set('model', model); | ||||
|  | ||||
|   this.render(hbs`{{identity/item-details model=model}}`); | ||||
|   assert.dom('[data-test-disabled-warning]').doesNotExist('does not show the warning banner'); | ||||
| }); | ||||
							
								
								
									
										7
									
								
								ui/tests/pages/access/identity/aliases/add.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								ui/tests/pages/access/identity/aliases/add.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| import { create, visitable } from 'ember-cli-page-object'; | ||||
| import editForm from 'vault/tests/pages/components/identity/edit-form'; | ||||
|  | ||||
| export default create({ | ||||
|   visit: visitable('/vault/access/identity/:item_type/aliases/add/:id'), | ||||
|   editForm, | ||||
| }); | ||||
							
								
								
									
										13
									
								
								ui/tests/pages/access/identity/aliases/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								ui/tests/pages/access/identity/aliases/index.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| import { create, clickable, text, visitable, collection } from 'ember-cli-page-object'; | ||||
| import flashMessage from 'vault/tests/pages/components/flash-message'; | ||||
|  | ||||
| export default create({ | ||||
|   visit: visitable('/vault/access/identity/:item_type/aliases'), | ||||
|   flashMessage, | ||||
|   items: collection('[data-test-identity-row]', { | ||||
|     menu: clickable('[data-test-popup-menu-trigger]'), | ||||
|     id: text('[data-test-identity-link]'), | ||||
|   }), | ||||
|   delete: clickable('[data-test-item-delete] [data-test-confirm-action-trigger]'), | ||||
|   confirmDelete: clickable('[data-test-item-delete] [data-test-confirm-button]'), | ||||
| }); | ||||
							
								
								
									
										11
									
								
								ui/tests/pages/access/identity/aliases/show.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								ui/tests/pages/access/identity/aliases/show.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| import { create, clickable, collection, contains, visitable } from 'ember-cli-page-object'; | ||||
| import flashMessage from 'vault/tests/pages/components/flash-message'; | ||||
| import infoTableRow from 'vault/tests/pages/components/info-table-row'; | ||||
|  | ||||
| export default create({ | ||||
|   visit: visitable('/vault/access/identity/:item_type/aliases/:alias_id'), | ||||
|   flashMessage, | ||||
|   nameContains: contains('[data-test-alias-name]'), | ||||
|   rows: collection('[data-test-component="info-table-row"]', infoTableRow), | ||||
|   edit: clickable('[data-test-alias-edit-link]') | ||||
| }); | ||||
							
								
								
									
										13
									
								
								ui/tests/pages/access/identity/create.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								ui/tests/pages/access/identity/create.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| import { create, visitable } from 'ember-cli-page-object'; | ||||
| import editForm from 'vault/tests/pages/components/identity/edit-form'; | ||||
|  | ||||
| export default create({ | ||||
|   visit: visitable('/vault/access/identity/:item_type/create'), | ||||
|   editForm, | ||||
|   createItem(item_type, type) { | ||||
|     if (type) { | ||||
|       return this.visit({item_type}).editForm.type(type).submit(); | ||||
|     } | ||||
|     return this.visit({item_type}).editForm.submit(); | ||||
|   } | ||||
| }); | ||||
| @@ -1,4 +1,13 @@ | ||||
| import { create, visitable } from 'ember-cli-page-object'; | ||||
| import { create, clickable, text, visitable, collection } from 'ember-cli-page-object'; | ||||
| import flashMessage from 'vault/tests/pages/components/flash-message'; | ||||
|  | ||||
| export default create({ | ||||
|   visit: visitable('/vault/access/identity/:item_type'), | ||||
|   flashMessage, | ||||
|   items: collection('[data-test-identity-row]', { | ||||
|     menu: clickable('[data-test-popup-menu-trigger]'), | ||||
|     id: text('[data-test-identity-link]'), | ||||
|   }), | ||||
|   delete: clickable('[data-test-item-delete] [data-test-confirm-action-trigger]'), | ||||
|   confirmDelete: clickable('[data-test-item-delete] [data-test-confirm-button]'), | ||||
| }); | ||||
|   | ||||
							
								
								
									
										11
									
								
								ui/tests/pages/access/identity/show.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								ui/tests/pages/access/identity/show.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| import { create, clickable, collection, contains, visitable } from 'ember-cli-page-object'; | ||||
| import flashMessage from 'vault/tests/pages/components/flash-message'; | ||||
| import infoTableRow from 'vault/tests/pages/components/info-table-row'; | ||||
|  | ||||
| export default create({ | ||||
|   visit: visitable('/vault/access/identity/:item_type/:item_id'), | ||||
|   flashMessage, | ||||
|   nameContains: contains('[data-test-identity-item-name]'), | ||||
|   rows: collection('[data-test-component="info-table-row"]', infoTableRow), | ||||
|   edit: clickable('[data-test-entity-edit-link]') | ||||
| }); | ||||
							
								
								
									
										14
									
								
								ui/tests/pages/components/identity/edit-form.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								ui/tests/pages/components/identity/edit-form.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| import { clickable, fillable, attribute } from 'ember-cli-page-object'; | ||||
| import fields from '../form-field'; | ||||
|  | ||||
| export default { | ||||
|   ...fields, | ||||
|   cancelLinkHref: attribute('href', '[data-test-cancel-link]'), | ||||
|   cancelLink: clickable('[data-test-cancel-link]'), | ||||
|   name: fillable('[data-test-input="name"]'), | ||||
|   disabled: clickable('[data-test-input="disabled"]'), | ||||
|   type: fillable('[data-test-input="type"]'), | ||||
|   submit: clickable('[data-test-identity-submit]'), | ||||
|   delete: clickable('[data-test-confirm-action-trigger]'), | ||||
|   confirmDelete: clickable('[data-test-confirm-button]'), | ||||
| }; | ||||
							
								
								
									
										5
									
								
								ui/tests/pages/components/identity/item-details.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								ui/tests/pages/components/identity/item-details.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| import { clickable } from 'ember-cli-page-object'; | ||||
|  | ||||
| export default { | ||||
|   enable: clickable('[data-test-enable]'), | ||||
| }; | ||||
							
								
								
									
										7
									
								
								ui/tests/pages/components/info-table-row.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								ui/tests/pages/components/info-table-row.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| import { text, isPresent } from 'ember-cli-page-object'; | ||||
|  | ||||
| export default { | ||||
|   hasLabel: isPresent('[data-test-row-label]'), | ||||
|   rowLabel: text('[data-test-row-label]'), | ||||
|   rowValue: text('[data-test-row-value]'), | ||||
| }; | ||||
							
								
								
									
										73
									
								
								ui/tests/unit/components/identity/edit-form-test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								ui/tests/unit/components/identity/edit-form-test.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,73 @@ | ||||
| import { moduleForComponent, test } from 'ember-qunit'; | ||||
| import sinon from 'sinon'; | ||||
| import Ember from 'ember'; | ||||
|  | ||||
| moduleForComponent('identity/edit-form', 'Unit | Component | identity/edit-form', { | ||||
|   unit: true, | ||||
|   needs: ['service:auth', 'service:flash-messages'], | ||||
| }); | ||||
|  | ||||
| let testCases = [ | ||||
|   { | ||||
|     identityType: 'entity', | ||||
|     mode: 'create', | ||||
|     expected: 'vault.cluster.access.identity', | ||||
|   }, | ||||
|   { | ||||
|     identityType: 'entity', | ||||
|     mode: 'edit', | ||||
|     expected: 'vault.cluster.access.identity.show', | ||||
|   }, | ||||
|   { | ||||
|     identityType: 'entity-merge', | ||||
|     mode: 'merge', | ||||
|     expected: 'vault.cluster.access.identity', | ||||
|   }, | ||||
|   { | ||||
|     identityType: 'entity-alias', | ||||
|     mode: 'create', | ||||
|     expected: 'vault.cluster.access.identity.aliases', | ||||
|   }, | ||||
|   { | ||||
|     identityType: 'entity-alias', | ||||
|     mode: 'edit', | ||||
|     expected: 'vault.cluster.access.identity.aliases.show', | ||||
|   }, | ||||
|   { | ||||
|     identityType: 'group', | ||||
|     mode: 'create', | ||||
|     expected: 'vault.cluster.access.identity', | ||||
|   }, | ||||
|   { | ||||
|     identityType: 'group', | ||||
|     mode: 'edit', | ||||
|     expected: 'vault.cluster.access.identity.show', | ||||
|   }, | ||||
|   { | ||||
|     identityType: 'group-alias', | ||||
|     mode: 'create', | ||||
|     expected: 'vault.cluster.access.identity.aliases', | ||||
|   }, | ||||
|   { | ||||
|     identityType: 'group-alias', | ||||
|     mode: 'edit', | ||||
|     expected: 'vault.cluster.access.identity.aliases.show', | ||||
|   }, | ||||
| ]; | ||||
| testCases.forEach(function(testCase) { | ||||
|   let model = Ember.Object.create({ | ||||
|     identityType: testCase.identityType, | ||||
|     rollbackAttributes: sinon.spy(), | ||||
|   }); | ||||
|   test(`it computes cancelLink properly: ${testCase.identityType} ${testCase.mode}`, function(assert) { | ||||
|     let component = this.subject(); | ||||
|  | ||||
|     component.set('mode', testCase.mode); | ||||
|     component.set('model', model); | ||||
|     assert.equal( | ||||
|       component.get('cancelLink'), | ||||
|       testCase.expected, | ||||
|       'cancel link is correct' | ||||
|     ); | ||||
|   }); | ||||
| }); | ||||
| @@ -89,6 +89,7 @@ test('store.constructResponse', function(assert) { | ||||
| }); | ||||
|  | ||||
| test('store.fetchPage', function(assert) { | ||||
|   let done = assert.async(4); | ||||
|   const keys = ['zero', 'one', 'two', 'three', 'four', 'five', 'six']; | ||||
|   const data = { | ||||
|     data: { | ||||
| @@ -106,11 +107,14 @@ test('store.fetchPage', function(assert) { | ||||
|  | ||||
|   let result; | ||||
|   Ember.run(() => { | ||||
|     result = store.fetchPage('transit-key', query); | ||||
|     store.fetchPage('transit-key', query).then(r => { | ||||
|       result = r; | ||||
|       done(); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   assert.ok(result.get('length'), pageSize, 'returns the correct number of items'); | ||||
|   assert.deepEqual(result.toArray().mapBy('id'), keys.slice(0, pageSize), 'returns the first page of items'); | ||||
|   assert.deepEqual(result.mapBy('id'), keys.slice(0, pageSize), 'returns the first page of items'); | ||||
|   assert.deepEqual( | ||||
|     result.get('meta'), | ||||
|     { | ||||
| @@ -125,44 +129,54 @@ test('store.fetchPage', function(assert) { | ||||
|   ); | ||||
|  | ||||
|   Ember.run(() => { | ||||
|     result = store.fetchPage('transit-key', { | ||||
|     store.fetchPage('transit-key', { | ||||
|       size: pageSize, | ||||
|       page: 3, | ||||
|       responsePath: 'data.keys', | ||||
|     }).then(r => { | ||||
|       result = r; | ||||
|       done() | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   const pageThreeEnd = 3 * pageSize; | ||||
|   const pageThreeStart = pageThreeEnd - pageSize; | ||||
|   assert.deepEqual( | ||||
|     result.toArray().mapBy('id'), | ||||
|     result.mapBy('id'), | ||||
|     keys.slice(pageThreeStart, pageThreeEnd), | ||||
|     'returns the third page of items' | ||||
|   ); | ||||
|  | ||||
|   Ember.run(() => { | ||||
|     result = store.fetchPage('transit-key', { | ||||
|     store.fetchPage('transit-key', { | ||||
|       size: pageSize, | ||||
|       page: 99, | ||||
|       responsePath: 'data.keys', | ||||
|     }).then(r => { | ||||
|  | ||||
|       result = r; | ||||
|       done(); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   assert.deepEqual( | ||||
|     result.toArray().mapBy('id'), | ||||
|     result.mapBy('id'), | ||||
|     keys.slice(keys.length - 1), | ||||
|     'returns the last page when the page value is beyond the of bounds' | ||||
|   ); | ||||
|  | ||||
|   Ember.run(() => { | ||||
|     result = store.fetchPage('transit-key', { | ||||
|     store.fetchPage('transit-key', { | ||||
|       size: pageSize, | ||||
|       page: 0, | ||||
|       responsePath: 'data.keys', | ||||
|     }).then(r => { | ||||
|       result = r; | ||||
|       done(); | ||||
|     }); | ||||
|   }); | ||||
|   assert.deepEqual( | ||||
|     result.toArray().mapBy('id'), | ||||
|     result.mapBy('id'), | ||||
|     keys.slice(0, pageSize), | ||||
|     'returns the first page when page value is under the bounds' | ||||
|   ); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Matthew Irish
					Matthew Irish