mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-30 18:17:55 +00:00 
			
		
		
		
	Create sections for Secrets sync destination fields for create/edit view (#27538)
* initial shuffling of credentials and advanced configuration options * update all destination models * wip changelog * Update 27538.txt * remove custom_tags from gh * missed vercel and remove custom_tags from base * refactor conditional logic on templace * things * test coverage and dynamic subText * add assert to not see enableInput on create * clean up * remove extra parens * test clean up to clarify what the header subtext vs breadcrumb transition are testing
This commit is contained in:
		
							
								
								
									
										3
									
								
								changelog/27538.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								changelog/27538.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| ```release-note:improvement | ||||
| ui: Creates separate section for updating sensitive creds for Secrets sync create/edit view. | ||||
| ``` | ||||
| @@ -7,6 +7,7 @@ import SyncDestinationModel from '../destination'; | ||||
| import { attr } from '@ember-data/model'; | ||||
| import { withFormFields } from 'vault/decorators/model-form-fields'; | ||||
|  | ||||
| // displayFields are used on the destination details view | ||||
| const displayFields = [ | ||||
|   // connection details | ||||
|   'name', | ||||
| @@ -20,11 +21,13 @@ const displayFields = [ | ||||
|   'secretNameTemplate', | ||||
|   'customTags', | ||||
| ]; | ||||
| // formFieldGroups are used on the create-edit destination view | ||||
| const formFieldGroups = [ | ||||
|   { | ||||
|     default: ['name', 'region', 'roleArn', 'externalId', 'granularity', 'secretNameTemplate', 'customTags'], | ||||
|     default: ['name', 'region', 'roleArn', 'externalId'], | ||||
|   }, | ||||
|   { Credentials: ['accessKeyId', 'secretAccessKey'] }, | ||||
|   { 'Advanced configuration': ['granularity', 'secretNameTemplate', 'customTags'] }, | ||||
| ]; | ||||
| @withFormFields(displayFields, formFieldGroups) | ||||
| export default class SyncDestinationsAwsSecretsManagerModel extends SyncDestinationModel { | ||||
|   | ||||
| @@ -6,7 +6,7 @@ | ||||
| import SyncDestinationModel from '../destination'; | ||||
| import { attr } from '@ember-data/model'; | ||||
| import { withFormFields } from 'vault/decorators/model-form-fields'; | ||||
|  | ||||
| // displayFields are used on the destination details view | ||||
| const displayFields = [ | ||||
|   // connection details | ||||
|   'name', | ||||
| @@ -20,20 +20,13 @@ const displayFields = [ | ||||
|   'secretNameTemplate', | ||||
|   'customTags', | ||||
| ]; | ||||
| // formFieldGroups are used on the create-edit destination view | ||||
| const formFieldGroups = [ | ||||
|   { | ||||
|     default: [ | ||||
|       'name', | ||||
|       'keyVaultUri', | ||||
|       'tenantId', | ||||
|       'cloud', | ||||
|       'clientId', | ||||
|       'granularity', | ||||
|       'secretNameTemplate', | ||||
|       'customTags', | ||||
|     ], | ||||
|     default: ['name', 'keyVaultUri', 'tenantId', 'cloud', 'clientId'], | ||||
|   }, | ||||
|   { Credentials: ['clientSecret'] }, | ||||
|   { 'Advanced configuration': ['granularity', 'secretNameTemplate', 'customTags'] }, | ||||
| ]; | ||||
| @withFormFields(displayFields, formFieldGroups) | ||||
| export default class SyncDestinationsAzureKeyVaultModel extends SyncDestinationModel { | ||||
|   | ||||
| @@ -6,7 +6,7 @@ | ||||
| import SyncDestinationModel from '../destination'; | ||||
| import { attr } from '@ember-data/model'; | ||||
| import { withFormFields } from 'vault/decorators/model-form-fields'; | ||||
|  | ||||
| // displayFields are used on the destination details view | ||||
| const displayFields = [ | ||||
|   // connection details | ||||
|   'name', | ||||
| @@ -17,9 +17,11 @@ const displayFields = [ | ||||
|   'secretNameTemplate', | ||||
|   'customTags', | ||||
| ]; | ||||
| // formFieldGroups are used on the create-edit destination view | ||||
| const formFieldGroups = [ | ||||
|   { default: ['name', 'projectId', 'granularity', 'secretNameTemplate', 'customTags'] }, | ||||
|   { default: ['name', 'projectId'] }, | ||||
|   { Credentials: ['credentials'] }, | ||||
|   { 'Advanced configuration': ['granularity', 'secretNameTemplate', 'customTags'] }, | ||||
| ]; | ||||
| @withFormFields(displayFields, formFieldGroups) | ||||
| export default class SyncDestinationsGoogleCloudSecretManagerModel extends SyncDestinationModel { | ||||
|   | ||||
| @@ -6,7 +6,7 @@ | ||||
| import SyncDestinationModel from '../destination'; | ||||
| import { attr } from '@ember-data/model'; | ||||
| import { withFormFields } from 'vault/decorators/model-form-fields'; | ||||
|  | ||||
| // displayFields are used on the destination details view | ||||
| const displayFields = [ | ||||
|   // connection details | ||||
|   'name', | ||||
| @@ -17,9 +17,12 @@ const displayFields = [ | ||||
|   'granularity', | ||||
|   'secretNameTemplate', | ||||
| ]; | ||||
|  | ||||
| // formFieldGroups are used on the create-edit destination view | ||||
| const formFieldGroups = [ | ||||
|   { default: ['name', 'repositoryOwner', 'repositoryName', 'granularity', 'secretNameTemplate'] }, | ||||
|   { default: ['name', 'repositoryOwner', 'repositoryName'] }, | ||||
|   { Credentials: ['accessToken'] }, | ||||
|   { 'Advanced configuration': ['granularity', 'secretNameTemplate'] }, | ||||
| ]; | ||||
|  | ||||
| @withFormFields(displayFields, formFieldGroups) | ||||
|   | ||||
| @@ -21,7 +21,7 @@ const validations = { | ||||
|   // getter/setter for the deploymentEnvironments model attribute | ||||
|   deploymentEnvironmentsArray: [{ type: 'presence', message: 'At least one environment is required.' }], | ||||
| }; | ||||
|  | ||||
| // displayFields are used on the destination details view | ||||
| const displayFields = [ | ||||
|   // connection details | ||||
|   'name', | ||||
| @@ -33,9 +33,11 @@ const displayFields = [ | ||||
|   'granularity', | ||||
|   'secretNameTemplate', | ||||
| ]; | ||||
| // formFieldGroups are used on the create-edit destination view | ||||
| const formFieldGroups = [ | ||||
|   { default: ['name', 'projectId', 'teamId', 'deploymentEnvironments', 'granularity', 'secretNameTemplate'] }, | ||||
|   { default: ['name', 'projectId', 'teamId', 'deploymentEnvironments'] }, | ||||
|   { Credentials: ['accessToken'] }, | ||||
|   { 'Advanced configuration': ['granularity', 'secretNameTemplate'] }, | ||||
| ]; | ||||
| @withModelValidations(validations) | ||||
| @withFormFields(displayFields, formFieldGroups) | ||||
|   | ||||
| @@ -12,27 +12,38 @@ | ||||
|  | ||||
|   {{#each @destination.formFieldGroups as |fieldGroup|}} | ||||
|     {{#each-in fieldGroup as |group fields|}} | ||||
|       {{#if (and (eq group "Credentials") (not @destination.isNew))}} | ||||
|       {{#if (not-eq group "default")}} | ||||
|         <hr class="has-top-margin-xl has-bottom-margin-l has-background-gray-200" /> | ||||
|         <Hds::Text::Display @tag="h2" @size="400" @weight="bold">Credentials</Hds::Text::Display> | ||||
|         <Hds::Text::Body @tag="p" @size="100" @color="faint" class="has-bottom-margin-m"> | ||||
|           Connection credentials are sensitive information and the value cannot be read. Enable the input to update. | ||||
|         <Hds::Text::Display | ||||
|           @tag="h2" | ||||
|           @size="400" | ||||
|           @weight="bold" | ||||
|           data-test-destination-header={{group}} | ||||
|         >{{group}}</Hds::Text::Display> | ||||
|         <Hds::Text::Body | ||||
|           @tag="p" | ||||
|           @size="100" | ||||
|           @color="faint" | ||||
|           class="has-bottom-margin-m" | ||||
|           data-test-destination-subText={{group}} | ||||
|         > | ||||
|           {{this.groupSubtext group @destination.isNew}} | ||||
|         </Hds::Text::Body> | ||||
|       {{/if}} | ||||
|       {{#each fields as |attr|}} | ||||
|         {{#if (and (eq group "Credentials") (not @destination.isNew))}} | ||||
|           <EnableInput data-test-enable-field={{attr.name}} class="field" @attr={{attr}}> | ||||
|             <FormField @attr={{attr}} @model={{@destination}} @modelValidations={{this.modelValidations}} /> | ||||
|           </EnableInput> | ||||
|         {{/each}} | ||||
|         {{else}} | ||||
|         {{#each fields as |attr|}} | ||||
|           <FormField | ||||
|             @attr={{attr}} | ||||
|             @model={{@destination}} | ||||
|             @modelValidations={{this.modelValidations}} | ||||
|             @onKeyUp={{this.updateWarningValidation}} | ||||
|           /> | ||||
|         {{/each}} | ||||
|         {{/if}} | ||||
|       {{/each}} | ||||
|     {{/each-in}} | ||||
|   {{/each}} | ||||
|  | ||||
|   | ||||
| @@ -56,6 +56,20 @@ export default class DestinationsCreateForm extends Component<Args> { | ||||
|         }; | ||||
|   } | ||||
|  | ||||
|   groupSubtext(group: string, isNew: boolean) { | ||||
|     const dynamicText = isNew | ||||
|       ? 'used to authenticate with the destination' | ||||
|       : 'and the value cannot be read. Enable the input to update'; | ||||
|     switch (group) { | ||||
|       case 'Advanced configuration': | ||||
|         return 'Configuration options for the destination.'; | ||||
|       case 'Credentials': | ||||
|         return `Connection credentials are sensitive information ${dynamicText}.`; | ||||
|       default: | ||||
|         return ''; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @task | ||||
|   @waitFor | ||||
|   *save(event: Event) { | ||||
|   | ||||
| @@ -86,6 +86,8 @@ export const PAGE = { | ||||
|   toolbar: (btnText) => `[data-test-toolbar="${btnText}"]`, | ||||
|   form: { | ||||
|     enableInput: (attr) => `[data-test-enable-field="${attr}"] [data-test-icon="edit"]`, | ||||
|     fieldGroupHeader: (group) => `[data-test-destination-header="${group}"]`, | ||||
|     fieldGroupSubtext: (group) => `[data-test-destination-subText="${group}"]`, | ||||
|     fillInByAttr: async (attr, value) => { | ||||
|       // for handling more complex form input elements by attr name | ||||
|       switch (attr) { | ||||
|   | ||||
| @@ -44,7 +44,7 @@ module('Integration | Component | sync | Secrets::Page::Destinations::CreateAndE | ||||
|     }; | ||||
|   }); | ||||
|  | ||||
|   test('create: it renders and navigates back to create on cancel', async function (assert) { | ||||
|   test('create: it renders breadcrumbs and navigates back to create on cancel', async function (assert) { | ||||
|     assert.expect(2); | ||||
|     const { type } = SYNC_DESTINATIONS[0]; | ||||
|     this.model = this.store.createRecord(`sync/destinations/${type}`, { type }); | ||||
| @@ -56,23 +56,59 @@ module('Integration | Component | sync | Secrets::Page::Destinations::CreateAndE | ||||
|     assert.true(transition, 'transitions to vault.cluster.sync.secrets.destinations.create on cancel'); | ||||
|   }); | ||||
|  | ||||
|   test('edit: it renders and navigates back to details on cancel', async function (assert) { | ||||
|   test('create: it renders headers and fieldGroups subtext', async function (assert) { | ||||
|     assert.expect(4); | ||||
|     const { type } = SYNC_DESTINATIONS[0]; | ||||
|     this.model = this.store.createRecord(`sync/destinations/${type}`, { type }); | ||||
|  | ||||
|     await this.renderFormComponent(); | ||||
|     assert | ||||
|       .dom(PAGE.form.fieldGroupHeader('Credentials')) | ||||
|       .hasText('Credentials', 'renders credentials section on create'); | ||||
|     assert | ||||
|       .dom(PAGE.form.fieldGroupHeader('Advanced configuration')) | ||||
|       .hasText('Advanced configuration', 'renders advanced configuration section on create'); | ||||
|     assert | ||||
|       .dom(PAGE.form.fieldGroupSubtext('Credentials')) | ||||
|       .hasText('Connection credentials are sensitive information used to authenticate with the destination.'); | ||||
|     assert | ||||
|       .dom(PAGE.form.fieldGroupSubtext('Advanced configuration')) | ||||
|       .hasText('Configuration options for the destination.'); | ||||
|   }); | ||||
|  | ||||
|   test('edit: it renders breadcrumbs and navigates back to details on cancel', async function (assert) { | ||||
|     assert.expect(2); | ||||
|     this.model = this.generateModel(); | ||||
|  | ||||
|     await this.renderFormComponent(); | ||||
|     assert.dom(PAGE.breadcrumbs).hasText('Secrets Sync Destinations Destination Edit Destination'); | ||||
|     assert.dom('h2').hasText('Credentials', 'renders credentials section on edit'); | ||||
|     assert | ||||
|       .dom('p.hds-foreground-faint') | ||||
|       .hasText( | ||||
|         'Connection credentials are sensitive information and the value cannot be read. Enable the input to update.' | ||||
|       ); | ||||
|  | ||||
|     await click(PAGE.cancelButton); | ||||
|     const transition = this.transitionStub.calledWith('vault.cluster.sync.secrets.destinations.destination'); | ||||
|     assert.true(transition, 'transitions to vault.cluster.sync.secrets.destinations.destination on cancel'); | ||||
|   }); | ||||
|  | ||||
|   test('edit: it renders headers and fieldGroup subtext', async function (assert) { | ||||
|     assert.expect(4); | ||||
|     this.model = this.generateModel(); | ||||
|  | ||||
|     await this.renderFormComponent(); | ||||
|     assert | ||||
|       .dom(PAGE.form.fieldGroupHeader('Credentials')) | ||||
|       .hasText('Credentials', 'renders credentials section on edit'); | ||||
|     assert | ||||
|       .dom(PAGE.form.fieldGroupHeader('Advanced configuration')) | ||||
|       .hasText('Advanced configuration', 'renders advanced configuration section on edit'); | ||||
|     assert | ||||
|       .dom(PAGE.form.fieldGroupSubtext('Credentials')) | ||||
|       .hasText( | ||||
|         'Connection credentials are sensitive information and the value cannot be read. Enable the input to update.' | ||||
|       ); | ||||
|     assert | ||||
|       .dom(PAGE.form.fieldGroupSubtext('Advanced configuration')) | ||||
|       .hasText('Configuration options for the destination.'); | ||||
|   }); | ||||
|  | ||||
|   test('edit: it PATCH updates custom_tags', async function (assert) { | ||||
|     assert.expect(1); | ||||
|     this.model = this.generateModel(); | ||||
| @@ -236,7 +272,7 @@ module('Integration | Component | sync | Secrets::Page::Destinations::CreateAndE | ||||
|         const filteredObfuscatedFields = this.model.formFields.filter((field) => | ||||
|           obfuscatedFields.includes(field.name) | ||||
|         ); | ||||
|         assert.expect(filteredObfuscatedFields.length); | ||||
|         assert.expect(filteredObfuscatedFields.length * 2); | ||||
|         await this.renderFormComponent(); | ||||
|         // iterate over the form fields and filter for those that are obfuscated | ||||
|         // fill those in and assert that they are masked | ||||
| @@ -246,6 +282,9 @@ module('Integration | Component | sync | Secrets::Page::Destinations::CreateAndE | ||||
|           assert | ||||
|             .dom(PAGE.maskedInput(field.name)) | ||||
|             .hasClass('masked-font', `it renders ${field.name} for ${destination} with masked font`); | ||||
|           assert | ||||
|             .dom(PAGE.form.enableInput(field.name)) | ||||
|             .doesNotExist(`it does not render enable input for ${field.name}`); | ||||
|         }); | ||||
|       }); | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Angel Garbarino
					Angel Garbarino