Skip to content

Commit 7311f4d

Browse files
lane-wetmoredrivera258
authored andcommitted
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
1 parent 4135414 commit 7311f4d

File tree

4 files changed

+176
-26
lines changed

4 files changed

+176
-26
lines changed

changelog/29677.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:improvement
2+
ui: adds key value pair string inputs as optional form for wrap tool
3+
```

ui/app/components/tools/wrap.hbs

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,30 @@
4040
<div class="box is-sideless is-fullwidth is-marginless">
4141
<NamespaceReminder @mode="perform" @noun="wrap" />
4242
<MessageError @errorMessage={{this.errorMessage}} />
43-
<div class="field">
44-
<div class="control">
45-
<JsonEditor
46-
@title="Data to wrap"
47-
@subTitle="json-formatted"
48-
@value={{this.wrapData}}
49-
@valueUpdated={{this.codemirrorUpdated}}
50-
/>
51-
</div>
52-
</div>
43+
<Toolbar>
44+
<ToolbarFilters>
45+
<Toggle @name="json" @checked={{this.showJson}} @onChange={{this.handleToggle}}>
46+
<span class="has-text-grey">JSON</span>
47+
</Toggle>
48+
</ToolbarFilters>
49+
</Toolbar>
50+
{{#if this.showJson}}
51+
<JsonEditor
52+
class="has-top-margin-s"
53+
@title="Data to wrap"
54+
@subTitle="json-formatted"
55+
@value={{this.stringifiedWrapData}}
56+
@valueUpdated={{this.codemirrorUpdated}}
57+
/>
58+
{{else}}
59+
<KvObjectEditor
60+
class="has-top-margin-l"
61+
@label="Data to wrap"
62+
@value={{this.wrapData}}
63+
@onChange={{fn (mut this.wrapData)}}
64+
@warnNonStringValues={{true}}
65+
/>
66+
{{/if}}
5367
<TtlPicker
5468
@label="Wrap TTL"
5569
@initialValue="30m"
@@ -58,10 +72,17 @@
5872
@helperTextEnabled="Wrap will expire after"
5973
@changeOnInit={{true}}
6074
/>
75+
{{#if this.hasLintingErrors}}
76+
<AlertInline
77+
@color="warning"
78+
class="has-top-padding-s"
79+
@message="JSON is unparsable. Fix linting errors to avoid data discrepancies."
80+
/>
81+
{{/if}}
6182
</div>
6283
<div class="field is-grouped box is-fullwidth is-bottomless">
6384
<div class="control">
64-
<Hds::Button @text="Wrap data" type="submit" disabled={{this.buttonDisabled}} data-test-tools-submit />
85+
<Hds::Button @text="Wrap data" type="submit" data-test-tools-submit />
6586
</div>
6687
</div>
6788
</form>

ui/app/components/tools/wrap.js

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import Component from '@glimmer/component';
77
import { service } from '@ember/service';
88
import { action } from '@ember/object';
99
import { tracked } from '@glimmer/tracking';
10+
import { stringify } from 'core/helpers/stringify';
1011
import errorMessage from 'vault/utils/error-message';
1112

1213
/**
@@ -21,18 +22,38 @@ export default class ToolsWrap extends Component {
2122
@service store;
2223
@service flashMessages;
2324

24-
@tracked buttonDisabled = false;
25+
@tracked hasLintingErrors = false;
2526
@tracked token = '';
2627
@tracked wrapTTL = null;
27-
@tracked wrapData = '{\n}';
28+
@tracked wrapData = null;
2829
@tracked errorMessage = '';
30+
@tracked showJson = true;
31+
32+
get startingValue() {
33+
// must pass the third param called "space" in JSON.stringify to structure object with whitespace
34+
// otherwise the following codemirror modifier check will pass `this._editor.getValue() !== namedArgs.content` and _setValue will be called.
35+
// the method _setValue moves the cursor to the beginning of the text field.
36+
// the effect is that the cursor jumps after the first key input.
37+
return JSON.stringify({ '': '' }, null, 2);
38+
}
39+
40+
get stringifiedWrapData() {
41+
return this?.wrapData ? stringify([this.wrapData], {}) : this.startingValue;
42+
}
43+
44+
@action
45+
handleToggle() {
46+
this.showJson = !this.showJson;
47+
this.hasLintingErrors = false;
48+
}
2949

3050
@action
3151
reset(clearData = true) {
3252
this.token = '';
3353
this.errorMessage = '';
3454
this.wrapTTL = null;
35-
if (clearData) this.wrapData = '{\n}';
55+
this.hasLintingErrors = false;
56+
if (clearData) this.wrapData = null;
3657
}
3758

3859
@action
@@ -44,15 +65,15 @@ export default class ToolsWrap extends Component {
4465
@action
4566
codemirrorUpdated(val, codemirror) {
4667
codemirror.performLint();
47-
const hasErrors = codemirror?.state.lint.marked?.length > 0;
48-
this.buttonDisabled = hasErrors;
49-
if (!hasErrors) this.wrapData = val;
68+
this.hasLintingErrors = codemirror?.state.lint.marked?.length > 0;
69+
if (!this.hasLintingErrors) this.wrapData = JSON.parse(val);
5070
}
5171

5272
@action
5373
async handleSubmit(evt) {
5474
evt.preventDefault();
55-
const data = JSON.parse(this.wrapData);
75+
76+
const data = this.wrapData;
5677
const wrapTTL = this.wrapTTL || null;
5778

5879
try {

ui/tests/integration/components/tools/wrap-test.js

Lines changed: 113 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,13 @@ module('Integration | Component | tools/wrap', function (hooks) {
3737
await this.renderComponent();
3838

3939
assert.dom('h1').hasText('Wrap Data', 'Title renders');
40-
assert.dom('label').hasText('Data to wrap (json-formatted)');
41-
assert.strictEqual(codemirror().getValue(' '), '{ }', 'json editor initializes with empty object');
40+
assert.dom('[data-test-toggle-label="json"]').hasText('JSON');
41+
assert.dom('[data-test-component="json-editor-title"]').hasText('Data to wrap (json-formatted)');
42+
assert.strictEqual(
43+
codemirror().getValue(' '),
44+
`{ \"\": \"\" }`, // eslint-disable-line no-useless-escape
45+
'json editor initializes with empty object that includes whitespace'
46+
);
4247
assert.dom(TTL.toggleByLabel('Wrap TTL')).isNotChecked('Wrap TTL defaults to unchecked');
4348
assert.dom(TS.submit).isEnabled();
4449
assert.dom(TS.toolsInput('wrapping-token')).doesNotExist();
@@ -104,6 +109,67 @@ module('Integration | Component | tools/wrap', function (hooks) {
104109
await click(TS.submit);
105110
});
106111

112+
test('it toggles between views and preserves input data', async function (assert) {
113+
assert.expect(6);
114+
await this.renderComponent();
115+
await codemirror().setValue(this.wrapData);
116+
assert.dom('[data-test-component="json-editor-title"]').hasText('Data to wrap (json-formatted)');
117+
await click('[data-test-toggle-input="json"]');
118+
assert.dom('[data-test-component="json-editor-title"]').doesNotExist();
119+
assert.dom('[data-test-kv-key="0"]').hasValue('foo');
120+
assert.dom('[data-test-kv-value="0"]').hasValue('bar');
121+
await click('[data-test-toggle-input="json"]');
122+
assert.dom('[data-test-component="json-editor-title"]').exists();
123+
assert.strictEqual(
124+
codemirror().getValue(' '),
125+
`{ \"foo": \"bar" }`, // eslint-disable-line no-useless-escape
126+
'json editor has original data'
127+
);
128+
});
129+
130+
test('it submits from kv view', async function (assert) {
131+
assert.expect(6);
132+
133+
const multilineData = `this is a multi-line secret
134+
that contains
135+
some seriously important config`;
136+
const flashSpy = sinon.spy(this.owner.lookup('service:flash-messages'), 'success');
137+
const updatedWrapData = JSON.stringify({
138+
...JSON.parse(this.wrapData),
139+
foo: 'bar',
140+
foo2: multilineData,
141+
});
142+
143+
this.server.post('sys/wrapping/wrap', (schema, { requestBody, requestHeaders }) => {
144+
const payload = JSON.parse(requestBody);
145+
assert.propEqual(payload, JSON.parse(updatedWrapData), `payload contains data: ${requestBody}`);
146+
assert.strictEqual(requestHeaders['X-Vault-Wrap-TTL'], '30m', 'request header has default wrap ttl');
147+
return {
148+
wrap_info: {
149+
token: this.token,
150+
accessor: '5yjKx6Om9NmBx1mjiN1aIrnm',
151+
ttl: 1800,
152+
creation_time: '2024-06-07T12:02:22.096254-07:00',
153+
creation_path: 'sys/wrapping/wrap',
154+
},
155+
};
156+
});
157+
158+
await this.renderComponent();
159+
await click('[data-test-toggle-input="json"]');
160+
await fillIn('[data-test-kv-key="0"]', 'foo');
161+
await fillIn('[data-test-kv-value="0"]', 'bar');
162+
await click('[data-test-kv-add-row="0"]');
163+
await fillIn('[data-test-kv-key="1"]', 'foo2');
164+
await fillIn('[data-test-kv-value="1"]', multilineData);
165+
await click(TS.submit);
166+
await waitUntil(() => find(TS.toolsInput('wrapping-token')));
167+
assert.true(flashSpy.calledWith('Wrap was successful.'), 'it renders success flash');
168+
assert.dom(TS.toolsInput('wrapping-token')).hasText(this.token);
169+
assert.dom('label').hasText('Wrapped token');
170+
assert.dom('.CodeMirror').doesNotExist();
171+
});
172+
107173
test('it resets on done', async function (assert) {
108174
await this.renderComponent();
109175
await codemirror().setValue(this.wrapData);
@@ -113,7 +179,11 @@ module('Integration | Component | tools/wrap', function (hooks) {
113179

114180
await waitUntil(() => find(TS.button('Done')));
115181
await click(TS.button('Done'));
116-
assert.strictEqual(codemirror().getValue(' '), '{ }', 'json editor resets to empty object');
182+
assert.strictEqual(
183+
codemirror().getValue(' '),
184+
`{ \"\": \"\" }`, // eslint-disable-line no-useless-escape
185+
'json editor initializes with empty object that includes whitespace'
186+
);
117187
assert.dom(TTL.toggleByLabel('Wrap TTL')).isNotChecked('Wrap TTL resets to unchecked');
118188
await click(TTL.toggleByLabel('Wrap TTL'));
119189
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) {
126196

127197
await waitUntil(() => find(TS.button('Back')));
128198
await click(TS.button('Back'));
129-
assert.strictEqual(codemirror().getValue(' '), `{"foo": "bar"}`, 'json editor has original data');
199+
assert.strictEqual(
200+
codemirror().getValue(' '),
201+
`{ \"foo": \"bar" }`, // eslint-disable-line no-useless-escape
202+
'json editor has original data'
203+
);
130204
assert.dom(TTL.toggleByLabel('Wrap TTL')).isNotChecked('Wrap TTL defaults to unchecked');
131205
});
132206

133-
test('it disables/enables submit based on json linting', async function (assert) {
207+
test('it renders/hides warning based on json linting', async function (assert) {
134208
await this.renderComponent();
135209
await codemirror().setValue(`{bad json}`);
136-
assert.dom(TS.submit).isDisabled('submit disables if json editor has linting errors');
137-
210+
assert
211+
.dom('[data-test-inline-alert]')
212+
.hasText(
213+
'JSON is unparsable. Fix linting errors to avoid data discrepancies.',
214+
'Linting error message is shown for json view'
215+
);
138216
await codemirror().setValue(this.wrapData);
139-
assert.dom(TS.submit).isEnabled('submit reenables if json editor has no linting errors');
217+
assert.dom('[data-test-inline-alert]').doesNotExist();
218+
});
219+
220+
test('it hides json warning on back and on done', async function (assert) {
221+
await this.renderComponent();
222+
await codemirror().setValue(`{bad json}`);
223+
assert
224+
.dom('[data-test-inline-alert]')
225+
.hasText(
226+
'JSON is unparsable. Fix linting errors to avoid data discrepancies.',
227+
'Linting error message is shown for json view'
228+
);
229+
await click(TS.submit);
230+
await waitUntil(() => find(TS.button('Done')));
231+
await click(TS.button('Done'));
232+
assert.dom('[data-test-inline-alert]').doesNotExist();
233+
234+
await codemirror().setValue(`{bad json}`);
235+
assert
236+
.dom('[data-test-inline-alert]')
237+
.hasText(
238+
'JSON is unparsable. Fix linting errors to avoid data discrepancies.',
239+
'Linting error message is shown for json view'
240+
);
241+
await click(TS.submit);
242+
await waitUntil(() => find(TS.button('Back')));
243+
await click(TS.button('Back'));
244+
assert.dom('[data-test-inline-alert]').doesNotExist();
140245
});
141246
});

0 commit comments

Comments
 (0)