Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog/29677.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
ui: adds key value pair string inputs as optional form for wrap tool
```
43 changes: 32 additions & 11 deletions ui/app/components/tools/wrap.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -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">
<JsonEditor
@title="Data to wrap"
@subTitle="json-formatted"
@value={{this.wrapData}}
@valueUpdated={{this.codemirrorUpdated}}
/>
</div>
</div>
<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.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
@label="Wrap TTL"
@initialValue="30m"
Expand All @@ -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>
Expand Down
35 changes: 28 additions & 7 deletions ui/app/components/tools/wrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand All @@ -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
Expand All @@ -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 {
Expand Down
121 changes: 113 additions & 8 deletions ui/tests/integration/components/tools/wrap-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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({
Copy link
Copy Markdown
Contributor

@hellobontempo hellobontempo Feb 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason to stringify? We can just compare the two objects below, right? (Instead of need to call JSON.parse() on 145)

...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);
Expand All @@ -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');
Expand All @@ -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();
});
});
Loading