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:
lane-wetmore
2025-02-24 20:21:21 -06:00
committed by GitHub
parent 5d1a971c44
commit 13d302d509
4 changed files with 176 additions and 26 deletions

3
changelog/29677.txt Normal file
View File

@@ -0,0 +1,3 @@
```release-note:improvement
ui: adds key value pair string inputs as optional form for wrap tool
```

View File

@@ -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"
@@ -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>

View File

@@ -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 {

View File

@@ -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();
});
});