mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-29 17:52:32 +00:00 
			
		
		
		
	UI: Add KV view for wrap tool (#29677)
* add kv view for wrap tool * add changelog entry * update toggle and tests * update changelog, style updates, fix linting error bug * update tests * update test to include multiline input * clean up * test improvements and clean up * shift away from disabling button on error * update test for json lint warning * add check after back * move assertions to a better test for them
This commit is contained in:
		
							
								
								
									
										3
									
								
								changelog/29677.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								changelog/29677.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | ```release-note:improvement | ||||||
|  | ui: adds key value pair string inputs as optional form for wrap tool | ||||||
|  | ``` | ||||||
| @@ -40,16 +40,30 @@ | |||||||
|     <div class="box is-sideless is-fullwidth is-marginless"> |     <div class="box is-sideless is-fullwidth is-marginless"> | ||||||
|       <NamespaceReminder @mode="perform" @noun="wrap" /> |       <NamespaceReminder @mode="perform" @noun="wrap" /> | ||||||
|       <MessageError @errorMessage={{this.errorMessage}} /> |       <MessageError @errorMessage={{this.errorMessage}} /> | ||||||
|       <div class="field"> |       <Toolbar> | ||||||
|         <div class="control"> |         <ToolbarFilters> | ||||||
|           <JsonEditor |           <Toggle @name="json" @checked={{this.showJson}} @onChange={{this.handleToggle}}> | ||||||
|             @title="Data to wrap" |             <span class="has-text-grey">JSON</span> | ||||||
|             @subTitle="json-formatted" |           </Toggle> | ||||||
|             @value={{this.wrapData}} |         </ToolbarFilters> | ||||||
|             @valueUpdated={{this.codemirrorUpdated}} |       </Toolbar> | ||||||
|           /> |       {{#if this.showJson}} | ||||||
|         </div> |         <JsonEditor | ||||||
|       </div> |           class="has-top-margin-s" | ||||||
|  |           @title="Data to wrap" | ||||||
|  |           @subTitle="json-formatted" | ||||||
|  |           @value={{this.stringifiedWrapData}} | ||||||
|  |           @valueUpdated={{this.codemirrorUpdated}} | ||||||
|  |         /> | ||||||
|  |       {{else}} | ||||||
|  |         <KvObjectEditor | ||||||
|  |           class="has-top-margin-l" | ||||||
|  |           @label="Data to wrap" | ||||||
|  |           @value={{this.wrapData}} | ||||||
|  |           @onChange={{fn (mut this.wrapData)}} | ||||||
|  |           @warnNonStringValues={{true}} | ||||||
|  |         /> | ||||||
|  |       {{/if}} | ||||||
|       <TtlPicker |       <TtlPicker | ||||||
|         @label="Wrap TTL" |         @label="Wrap TTL" | ||||||
|         @initialValue="30m" |         @initialValue="30m" | ||||||
| @@ -58,10 +72,17 @@ | |||||||
|         @helperTextEnabled="Wrap will expire after" |         @helperTextEnabled="Wrap will expire after" | ||||||
|         @changeOnInit={{true}} |         @changeOnInit={{true}} | ||||||
|       /> |       /> | ||||||
|  |       {{#if this.hasLintingErrors}} | ||||||
|  |         <AlertInline | ||||||
|  |           @color="warning" | ||||||
|  |           class="has-top-padding-s" | ||||||
|  |           @message="JSON is unparsable. Fix linting errors to avoid data discrepancies." | ||||||
|  |         /> | ||||||
|  |       {{/if}} | ||||||
|     </div> |     </div> | ||||||
|     <div class="field is-grouped box is-fullwidth is-bottomless"> |     <div class="field is-grouped box is-fullwidth is-bottomless"> | ||||||
|       <div class="control"> |       <div class="control"> | ||||||
|         <Hds::Button @text="Wrap data" type="submit" disabled={{this.buttonDisabled}} data-test-tools-submit /> |         <Hds::Button @text="Wrap data" type="submit" data-test-tools-submit /> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|   </form> |   </form> | ||||||
|   | |||||||
| @@ -7,6 +7,7 @@ import Component from '@glimmer/component'; | |||||||
| import { service } from '@ember/service'; | import { service } from '@ember/service'; | ||||||
| import { action } from '@ember/object'; | import { action } from '@ember/object'; | ||||||
| import { tracked } from '@glimmer/tracking'; | import { tracked } from '@glimmer/tracking'; | ||||||
|  | import { stringify } from 'core/helpers/stringify'; | ||||||
| import errorMessage from 'vault/utils/error-message'; | import errorMessage from 'vault/utils/error-message'; | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -21,18 +22,38 @@ export default class ToolsWrap extends Component { | |||||||
|   @service store; |   @service store; | ||||||
|   @service flashMessages; |   @service flashMessages; | ||||||
|  |  | ||||||
|   @tracked buttonDisabled = false; |   @tracked hasLintingErrors = false; | ||||||
|   @tracked token = ''; |   @tracked token = ''; | ||||||
|   @tracked wrapTTL = null; |   @tracked wrapTTL = null; | ||||||
|   @tracked wrapData = '{\n}'; |   @tracked wrapData = null; | ||||||
|   @tracked errorMessage = ''; |   @tracked errorMessage = ''; | ||||||
|  |   @tracked showJson = true; | ||||||
|  |  | ||||||
|  |   get startingValue() { | ||||||
|  |     // must pass the third param called "space" in JSON.stringify to structure object with whitespace | ||||||
|  |     // otherwise the following codemirror modifier check will pass `this._editor.getValue() !== namedArgs.content` and _setValue will be called. | ||||||
|  |     // the method _setValue moves the cursor to the beginning of the text field. | ||||||
|  |     // the effect is that the cursor jumps after the first key input. | ||||||
|  |     return JSON.stringify({ '': '' }, null, 2); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   get stringifiedWrapData() { | ||||||
|  |     return this?.wrapData ? stringify([this.wrapData], {}) : this.startingValue; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   @action | ||||||
|  |   handleToggle() { | ||||||
|  |     this.showJson = !this.showJson; | ||||||
|  |     this.hasLintingErrors = false; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   @action |   @action | ||||||
|   reset(clearData = true) { |   reset(clearData = true) { | ||||||
|     this.token = ''; |     this.token = ''; | ||||||
|     this.errorMessage = ''; |     this.errorMessage = ''; | ||||||
|     this.wrapTTL = null; |     this.wrapTTL = null; | ||||||
|     if (clearData) this.wrapData = '{\n}'; |     this.hasLintingErrors = false; | ||||||
|  |     if (clearData) this.wrapData = null; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @action |   @action | ||||||
| @@ -44,15 +65,15 @@ export default class ToolsWrap extends Component { | |||||||
|   @action |   @action | ||||||
|   codemirrorUpdated(val, codemirror) { |   codemirrorUpdated(val, codemirror) { | ||||||
|     codemirror.performLint(); |     codemirror.performLint(); | ||||||
|     const hasErrors = codemirror?.state.lint.marked?.length > 0; |     this.hasLintingErrors = codemirror?.state.lint.marked?.length > 0; | ||||||
|     this.buttonDisabled = hasErrors; |     if (!this.hasLintingErrors) this.wrapData = JSON.parse(val); | ||||||
|     if (!hasErrors) this.wrapData = val; |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   @action |   @action | ||||||
|   async handleSubmit(evt) { |   async handleSubmit(evt) { | ||||||
|     evt.preventDefault(); |     evt.preventDefault(); | ||||||
|     const data = JSON.parse(this.wrapData); |  | ||||||
|  |     const data = this.wrapData; | ||||||
|     const wrapTTL = this.wrapTTL || null; |     const wrapTTL = this.wrapTTL || null; | ||||||
|  |  | ||||||
|     try { |     try { | ||||||
|   | |||||||
| @@ -37,8 +37,13 @@ module('Integration | Component | tools/wrap', function (hooks) { | |||||||
|     await this.renderComponent(); |     await this.renderComponent(); | ||||||
|  |  | ||||||
|     assert.dom('h1').hasText('Wrap Data', 'Title renders'); |     assert.dom('h1').hasText('Wrap Data', 'Title renders'); | ||||||
|     assert.dom('label').hasText('Data to wrap (json-formatted)'); |     assert.dom('[data-test-toggle-label="json"]').hasText('JSON'); | ||||||
|     assert.strictEqual(codemirror().getValue(' '), '{ }', 'json editor initializes with empty object'); |     assert.dom('[data-test-component="json-editor-title"]').hasText('Data to wrap (json-formatted)'); | ||||||
|  |     assert.strictEqual( | ||||||
|  |       codemirror().getValue(' '), | ||||||
|  |       `{   \"\": \"\" }`, // eslint-disable-line no-useless-escape | ||||||
|  |       'json editor initializes with empty object that includes whitespace' | ||||||
|  |     ); | ||||||
|     assert.dom(TTL.toggleByLabel('Wrap TTL')).isNotChecked('Wrap TTL defaults to unchecked'); |     assert.dom(TTL.toggleByLabel('Wrap TTL')).isNotChecked('Wrap TTL defaults to unchecked'); | ||||||
|     assert.dom(TS.submit).isEnabled(); |     assert.dom(TS.submit).isEnabled(); | ||||||
|     assert.dom(TS.toolsInput('wrapping-token')).doesNotExist(); |     assert.dom(TS.toolsInput('wrapping-token')).doesNotExist(); | ||||||
| @@ -104,6 +109,67 @@ module('Integration | Component | tools/wrap', function (hooks) { | |||||||
|     await click(TS.submit); |     await click(TS.submit); | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|  |   test('it toggles between views and preserves input data', async function (assert) { | ||||||
|  |     assert.expect(6); | ||||||
|  |     await this.renderComponent(); | ||||||
|  |     await codemirror().setValue(this.wrapData); | ||||||
|  |     assert.dom('[data-test-component="json-editor-title"]').hasText('Data to wrap (json-formatted)'); | ||||||
|  |     await click('[data-test-toggle-input="json"]'); | ||||||
|  |     assert.dom('[data-test-component="json-editor-title"]').doesNotExist(); | ||||||
|  |     assert.dom('[data-test-kv-key="0"]').hasValue('foo'); | ||||||
|  |     assert.dom('[data-test-kv-value="0"]').hasValue('bar'); | ||||||
|  |     await click('[data-test-toggle-input="json"]'); | ||||||
|  |     assert.dom('[data-test-component="json-editor-title"]').exists(); | ||||||
|  |     assert.strictEqual( | ||||||
|  |       codemirror().getValue(' '), | ||||||
|  |       `{   \"foo": \"bar" }`, // eslint-disable-line no-useless-escape | ||||||
|  |       'json editor has original data' | ||||||
|  |     ); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   test('it submits from kv view', async function (assert) { | ||||||
|  |     assert.expect(6); | ||||||
|  |  | ||||||
|  |     const multilineData = `this is a multi-line secret | ||||||
|  |       that contains | ||||||
|  |       some seriously important config`; | ||||||
|  |     const flashSpy = sinon.spy(this.owner.lookup('service:flash-messages'), 'success'); | ||||||
|  |     const updatedWrapData = JSON.stringify({ | ||||||
|  |       ...JSON.parse(this.wrapData), | ||||||
|  |       foo: 'bar', | ||||||
|  |       foo2: multilineData, | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     this.server.post('sys/wrapping/wrap', (schema, { requestBody, requestHeaders }) => { | ||||||
|  |       const payload = JSON.parse(requestBody); | ||||||
|  |       assert.propEqual(payload, JSON.parse(updatedWrapData), `payload contains data: ${requestBody}`); | ||||||
|  |       assert.strictEqual(requestHeaders['X-Vault-Wrap-TTL'], '30m', 'request header has default wrap ttl'); | ||||||
|  |       return { | ||||||
|  |         wrap_info: { | ||||||
|  |           token: this.token, | ||||||
|  |           accessor: '5yjKx6Om9NmBx1mjiN1aIrnm', | ||||||
|  |           ttl: 1800, | ||||||
|  |           creation_time: '2024-06-07T12:02:22.096254-07:00', | ||||||
|  |           creation_path: 'sys/wrapping/wrap', | ||||||
|  |         }, | ||||||
|  |       }; | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     await this.renderComponent(); | ||||||
|  |     await click('[data-test-toggle-input="json"]'); | ||||||
|  |     await fillIn('[data-test-kv-key="0"]', 'foo'); | ||||||
|  |     await fillIn('[data-test-kv-value="0"]', 'bar'); | ||||||
|  |     await click('[data-test-kv-add-row="0"]'); | ||||||
|  |     await fillIn('[data-test-kv-key="1"]', 'foo2'); | ||||||
|  |     await fillIn('[data-test-kv-value="1"]', multilineData); | ||||||
|  |     await click(TS.submit); | ||||||
|  |     await waitUntil(() => find(TS.toolsInput('wrapping-token'))); | ||||||
|  |     assert.true(flashSpy.calledWith('Wrap was successful.'), 'it renders success flash'); | ||||||
|  |     assert.dom(TS.toolsInput('wrapping-token')).hasText(this.token); | ||||||
|  |     assert.dom('label').hasText('Wrapped token'); | ||||||
|  |     assert.dom('.CodeMirror').doesNotExist(); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|   test('it resets on done', async function (assert) { |   test('it resets on done', async function (assert) { | ||||||
|     await this.renderComponent(); |     await this.renderComponent(); | ||||||
|     await codemirror().setValue(this.wrapData); |     await codemirror().setValue(this.wrapData); | ||||||
| @@ -113,7 +179,11 @@ module('Integration | Component | tools/wrap', function (hooks) { | |||||||
|  |  | ||||||
|     await waitUntil(() => find(TS.button('Done'))); |     await waitUntil(() => find(TS.button('Done'))); | ||||||
|     await click(TS.button('Done')); |     await click(TS.button('Done')); | ||||||
|     assert.strictEqual(codemirror().getValue(' '), '{ }', 'json editor resets to empty object'); |     assert.strictEqual( | ||||||
|  |       codemirror().getValue(' '), | ||||||
|  |       `{   \"\": \"\" }`, // eslint-disable-line no-useless-escape | ||||||
|  |       'json editor initializes with empty object that includes whitespace' | ||||||
|  |     ); | ||||||
|     assert.dom(TTL.toggleByLabel('Wrap TTL')).isNotChecked('Wrap TTL resets to unchecked'); |     assert.dom(TTL.toggleByLabel('Wrap TTL')).isNotChecked('Wrap TTL resets to unchecked'); | ||||||
|     await click(TTL.toggleByLabel('Wrap TTL')); |     await click(TTL.toggleByLabel('Wrap TTL')); | ||||||
|     assert.dom(TTL.valueInputByLabel('Wrap TTL')).hasValue('30', 'ttl resets to default when toggled'); |     assert.dom(TTL.valueInputByLabel('Wrap TTL')).hasValue('30', 'ttl resets to default when toggled'); | ||||||
| @@ -126,16 +196,51 @@ module('Integration | Component | tools/wrap', function (hooks) { | |||||||
|  |  | ||||||
|     await waitUntil(() => find(TS.button('Back'))); |     await waitUntil(() => find(TS.button('Back'))); | ||||||
|     await click(TS.button('Back')); |     await click(TS.button('Back')); | ||||||
|     assert.strictEqual(codemirror().getValue(' '), `{"foo": "bar"}`, 'json editor has original data'); |     assert.strictEqual( | ||||||
|  |       codemirror().getValue(' '), | ||||||
|  |       `{   \"foo": \"bar" }`, // eslint-disable-line no-useless-escape | ||||||
|  |       'json editor has original data' | ||||||
|  |     ); | ||||||
|     assert.dom(TTL.toggleByLabel('Wrap TTL')).isNotChecked('Wrap TTL defaults to unchecked'); |     assert.dom(TTL.toggleByLabel('Wrap TTL')).isNotChecked('Wrap TTL defaults to unchecked'); | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   test('it disables/enables submit based on json linting', async function (assert) { |   test('it renders/hides warning based on json linting', async function (assert) { | ||||||
|     await this.renderComponent(); |     await this.renderComponent(); | ||||||
|     await codemirror().setValue(`{bad json}`); |     await codemirror().setValue(`{bad json}`); | ||||||
|     assert.dom(TS.submit).isDisabled('submit disables if json editor has linting errors'); |     assert | ||||||
|  |       .dom('[data-test-inline-alert]') | ||||||
|  |       .hasText( | ||||||
|  |         'JSON is unparsable. Fix linting errors to avoid data discrepancies.', | ||||||
|  |         'Linting error message is shown for json view' | ||||||
|  |       ); | ||||||
|     await codemirror().setValue(this.wrapData); |     await codemirror().setValue(this.wrapData); | ||||||
|     assert.dom(TS.submit).isEnabled('submit reenables if json editor has no linting errors'); |     assert.dom('[data-test-inline-alert]').doesNotExist(); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   test('it hides json warning on back and on done', async function (assert) { | ||||||
|  |     await this.renderComponent(); | ||||||
|  |     await codemirror().setValue(`{bad json}`); | ||||||
|  |     assert | ||||||
|  |       .dom('[data-test-inline-alert]') | ||||||
|  |       .hasText( | ||||||
|  |         'JSON is unparsable. Fix linting errors to avoid data discrepancies.', | ||||||
|  |         'Linting error message is shown for json view' | ||||||
|  |       ); | ||||||
|  |     await click(TS.submit); | ||||||
|  |     await waitUntil(() => find(TS.button('Done'))); | ||||||
|  |     await click(TS.button('Done')); | ||||||
|  |     assert.dom('[data-test-inline-alert]').doesNotExist(); | ||||||
|  |  | ||||||
|  |     await codemirror().setValue(`{bad json}`); | ||||||
|  |     assert | ||||||
|  |       .dom('[data-test-inline-alert]') | ||||||
|  |       .hasText( | ||||||
|  |         'JSON is unparsable. Fix linting errors to avoid data discrepancies.', | ||||||
|  |         'Linting error message is shown for json view' | ||||||
|  |       ); | ||||||
|  |     await click(TS.submit); | ||||||
|  |     await waitUntil(() => find(TS.button('Back'))); | ||||||
|  |     await click(TS.button('Back')); | ||||||
|  |     assert.dom('[data-test-inline-alert]').doesNotExist(); | ||||||
|   }); |   }); | ||||||
| }); | }); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 lane-wetmore
					lane-wetmore