mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-30 02:02:43 +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"> | ||||
|       <NamespaceReminder @mode="perform" @noun="wrap" /> | ||||
|       <MessageError @errorMessage={{this.errorMessage}} /> | ||||
|       <div class="field"> | ||||
|         <div class="control"> | ||||
|       <Toolbar> | ||||
|         <ToolbarFilters> | ||||
|           <Toggle @name="json" @checked={{this.showJson}} @onChange={{this.handleToggle}}> | ||||
|             <span class="has-text-grey">JSON</span> | ||||
|           </Toggle> | ||||
|         </ToolbarFilters> | ||||
|       </Toolbar> | ||||
|       {{#if this.showJson}} | ||||
|         <JsonEditor | ||||
|           class="has-top-margin-s" | ||||
|           @title="Data to wrap" | ||||
|           @subTitle="json-formatted" | ||||
|             @value={{this.wrapData}} | ||||
|           @value={{this.stringifiedWrapData}} | ||||
|           @valueUpdated={{this.codemirrorUpdated}} | ||||
|         /> | ||||
|         </div> | ||||
|       </div> | ||||
|       {{else}} | ||||
|         <KvObjectEditor | ||||
|           class="has-top-margin-l" | ||||
|           @label="Data to wrap" | ||||
|           @value={{this.wrapData}} | ||||
|           @onChange={{fn (mut this.wrapData)}} | ||||
|           @warnNonStringValues={{true}} | ||||
|         /> | ||||
|       {{/if}} | ||||
|       <TtlPicker | ||||
|         @label="Wrap TTL" | ||||
|         @initialValue="30m" | ||||
| @@ -58,10 +72,17 @@ | ||||
|         @helperTextEnabled="Wrap will expire after" | ||||
|         @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 class="field is-grouped box is-fullwidth is-bottomless"> | ||||
|       <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> | ||||
|   </form> | ||||
|   | ||||
| @@ -7,6 +7,7 @@ import Component from '@glimmer/component'; | ||||
| import { service } from '@ember/service'; | ||||
| import { action } from '@ember/object'; | ||||
| import { tracked } from '@glimmer/tracking'; | ||||
| import { stringify } from 'core/helpers/stringify'; | ||||
| import errorMessage from 'vault/utils/error-message'; | ||||
|  | ||||
| /** | ||||
| @@ -21,18 +22,38 @@ export default class ToolsWrap extends Component { | ||||
|   @service store; | ||||
|   @service flashMessages; | ||||
|  | ||||
|   @tracked buttonDisabled = false; | ||||
|   @tracked hasLintingErrors = false; | ||||
|   @tracked token = ''; | ||||
|   @tracked wrapTTL = null; | ||||
|   @tracked wrapData = '{\n}'; | ||||
|   @tracked wrapData = null; | ||||
|   @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 | ||||
|   reset(clearData = true) { | ||||
|     this.token = ''; | ||||
|     this.errorMessage = ''; | ||||
|     this.wrapTTL = null; | ||||
|     if (clearData) this.wrapData = '{\n}'; | ||||
|     this.hasLintingErrors = false; | ||||
|     if (clearData) this.wrapData = null; | ||||
|   } | ||||
|  | ||||
|   @action | ||||
| @@ -44,15 +65,15 @@ export default class ToolsWrap extends Component { | ||||
|   @action | ||||
|   codemirrorUpdated(val, codemirror) { | ||||
|     codemirror.performLint(); | ||||
|     const hasErrors = codemirror?.state.lint.marked?.length > 0; | ||||
|     this.buttonDisabled = hasErrors; | ||||
|     if (!hasErrors) this.wrapData = val; | ||||
|     this.hasLintingErrors = codemirror?.state.lint.marked?.length > 0; | ||||
|     if (!this.hasLintingErrors) this.wrapData = JSON.parse(val); | ||||
|   } | ||||
|  | ||||
|   @action | ||||
|   async handleSubmit(evt) { | ||||
|     evt.preventDefault(); | ||||
|     const data = JSON.parse(this.wrapData); | ||||
|  | ||||
|     const data = this.wrapData; | ||||
|     const wrapTTL = this.wrapTTL || null; | ||||
|  | ||||
|     try { | ||||
|   | ||||
| @@ -37,8 +37,13 @@ module('Integration | Component | tools/wrap', function (hooks) { | ||||
|     await this.renderComponent(); | ||||
|  | ||||
|     assert.dom('h1').hasText('Wrap Data', 'Title renders'); | ||||
|     assert.dom('label').hasText('Data to wrap (json-formatted)'); | ||||
|     assert.strictEqual(codemirror().getValue(' '), '{ }', 'json editor initializes with empty object'); | ||||
|     assert.dom('[data-test-toggle-label="json"]').hasText('JSON'); | ||||
|     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(TS.submit).isEnabled(); | ||||
|     assert.dom(TS.toolsInput('wrapping-token')).doesNotExist(); | ||||
| @@ -104,6 +109,67 @@ module('Integration | Component | tools/wrap', function (hooks) { | ||||
|     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) { | ||||
|     await this.renderComponent(); | ||||
|     await codemirror().setValue(this.wrapData); | ||||
| @@ -113,7 +179,11 @@ module('Integration | Component | tools/wrap', function (hooks) { | ||||
|  | ||||
|     await waitUntil(() => find(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'); | ||||
|     await click(TTL.toggleByLabel('Wrap TTL')); | ||||
|     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 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'); | ||||
|   }); | ||||
|  | ||||
|   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 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); | ||||
|     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