mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-29 01:32:33 +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