From 13d302d5094165c19a15f51d61b2b5f0752241db Mon Sep 17 00:00:00 2001 From: lane-wetmore Date: Mon, 24 Feb 2025 20:21:21 -0600 Subject: [PATCH] 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 --- changelog/29677.txt | 3 + ui/app/components/tools/wrap.hbs | 43 +++++-- ui/app/components/tools/wrap.js | 35 ++++- .../integration/components/tools/wrap-test.js | 121 ++++++++++++++++-- 4 files changed, 176 insertions(+), 26 deletions(-) create mode 100644 changelog/29677.txt diff --git a/changelog/29677.txt b/changelog/29677.txt new file mode 100644 index 0000000000..61103ff49f --- /dev/null +++ b/changelog/29677.txt @@ -0,0 +1,3 @@ +```release-note:improvement +ui: adds key value pair string inputs as optional form for wrap tool +``` diff --git a/ui/app/components/tools/wrap.hbs b/ui/app/components/tools/wrap.hbs index e294aba9ac..8887cdbbd6 100644 --- a/ui/app/components/tools/wrap.hbs +++ b/ui/app/components/tools/wrap.hbs @@ -40,16 +40,30 @@
-
-
- -
-
+ + + + JSON + + + + {{#if this.showJson}} + + {{else}} + + {{/if}} + {{#if this.hasLintingErrors}} + + {{/if}}
- +
diff --git a/ui/app/components/tools/wrap.js b/ui/app/components/tools/wrap.js index 281db49c9c..cf8fbf3e40 100644 --- a/ui/app/components/tools/wrap.js +++ b/ui/app/components/tools/wrap.js @@ -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 { diff --git a/ui/tests/integration/components/tools/wrap-test.js b/ui/tests/integration/components/tools/wrap-test.js index 2721bcdc93..c863a800ca 100644 --- a/ui/tests/integration/components/tools/wrap-test.js +++ b/ui/tests/integration/components/tools/wrap-test.js @@ -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(); }); });