diff --git a/changelog/28808.txt b/changelog/28808.txt
new file mode 100644
index 000000000000..20d4d1ce4e36
--- /dev/null
+++ b/changelog/28808.txt
@@ -0,0 +1,6 @@
+```release-note:improvement
+ui: Replace KVv2 json secret details view with Hds::CodeBlock component allowing users to search the full secret height.
+```
+```release-note:bug
+ui: Allow users to search the full json object within the json code-editor edit/create view.
+```
diff --git a/ui/app/templates/components/console/log-json.hbs b/ui/app/templates/components/console/log-json.hbs
index 6a1044791aca..f1c64321bbea 100644
--- a/ui/app/templates/components/console/log-json.hbs
+++ b/ui/app/templates/components/console/log-json.hbs
@@ -8,7 +8,8 @@
@showToolbar={{false}}
@value={{stringify this.content}}
@readOnly={{true}}
- @viewportMargin="Infinity"
+ {{! ideally we calculate the "height" of the json data, but 100 should cover most cases }}
+ @viewportMargin="100"
@gutters={{false}}
@theme="hashi auto-height"
/>
diff --git a/ui/app/templates/components/control-group-success.hbs b/ui/app/templates/components/control-group-success.hbs
index fd79e5ec2d17..b5a913b7a2ee 100644
--- a/ui/app/templates/components/control-group-success.hbs
+++ b/ui/app/templates/components/control-group-success.hbs
@@ -21,7 +21,8 @@
@showToolbar={{false}}
@value={{stringify this.unwrapData}}
@readOnly={{true}}
- @viewportMargin="Infinity"
+ {{! ideally we calculate the "height" of the json data, but 100 should cover most cases }}
+ @viewportMargin="100"
@gutters={{false}}
@theme="hashi-read-only auto-height"
/>
diff --git a/ui/lib/core/addon/components/json-editor.hbs b/ui/lib/core/addon/components/json-editor.hbs
index 28b784a9e439..a7587a0cdffd 100644
--- a/ui/lib/core/addon/components/json-editor.hbs
+++ b/ui/lib/core/addon/components/json-editor.hbs
@@ -51,7 +51,7 @@
mode=@mode
readOnly=@readOnly
theme=@theme
- viewportMarg=@viewportMargin
+ viewportMargin=@viewportMargin
onSetup=this.onSetup
onUpdate=this.onUpdate
onFocus=this.onFocus
diff --git a/ui/lib/core/addon/components/json-editor.js b/ui/lib/core/addon/components/json-editor.js
index 66507a6181cd..88b36ef8ff9b 100644
--- a/ui/lib/core/addon/components/json-editor.js
+++ b/ui/lib/core/addon/components/json-editor.js
@@ -24,7 +24,7 @@ import { action } from '@ember/object';
* @param {Boolean} [readOnly] - Sets the view to readOnly, allowing for copying but no editing. It also hides the cursor. Defaults to false.
* @param {String} [theme] - Specify or customize the look via a named "theme" class in scss.
* @param {String} [value] - Value within the display. Generally, a json string.
- * @param {String} [viewportMargin] - Size of viewport. Often set to "Infinity" to load/show all text regardless of length.
+ * @param {String} [viewportMargin] - Specifies the amount of lines rendered on the DOM (this is not the editor display height). The codemirror default is 10 which we set explicity in the code-mirror modifier per the recommendations from the codemirror docs.
* @param {string} [example] - Example to show when value is null -- when example is provided a restore action will render in the toolbar to clear the current value and show the example after input
* @param {string} [screenReaderLabel] - This label is read by the screen readers when CodeMirror text area is focused. This is helpful for accessibility.
* @param {string} [container] - **REQUIRED if rendering within a modal** Selector string or element object of containing element, set the focused element as the container value. This is for the Hds::Copy::Button and to set `autoRefresh=true` so content renders https://hds-website-hashicorp.vercel.app/components/copy/button?tab=code
diff --git a/ui/lib/core/addon/modifiers/code-mirror.js b/ui/lib/core/addon/modifiers/code-mirror.js
index b9b46900dcc8..8b6c73f66783 100644
--- a/ui/lib/core/addon/modifiers/code-mirror.js
+++ b/ui/lib/core/addon/modifiers/code-mirror.js
@@ -76,7 +76,7 @@ export default class CodeMirrorModifier extends Modifier {
readOnly: namedArgs.readOnly || false,
theme: namedArgs.theme || 'hashi',
value: namedArgs.content || '',
- viewportMargin: namedArgs.viewportMargin || '',
+ viewportMargin: namedArgs.viewportMargin || 10,
autoRefresh: namedArgs.autoRefresh,
});
diff --git a/ui/lib/kv/addon/components/kv-data-fields.hbs b/ui/lib/kv/addon/components/kv-data-fields.hbs
index e6af5effb985..1b75d56762bc 100644
--- a/ui/lib/kv/addon/components/kv-data-fields.hbs
+++ b/ui/lib/kv/addon/components/kv-data-fields.hbs
@@ -13,12 +13,27 @@
{{#if @showJson}}
-
+ {{#if (eq @type "details")}}
+
+
+ Version data
+
+
+ {{else}}
+
+ {{/if}}
{{#if (or @modelValidations.secretData.errors this.lintingErrors)}}
Reveal subkeys in JSON
{{#if this.showSubkeys}}
-
+
{{/if}}
\ No newline at end of file
diff --git a/ui/lib/kv/addon/components/kv-paths-card.hbs b/ui/lib/kv/addon/components/kv-paths-card.hbs
index d7976d92f27c..8e1edceb5a4e 100644
--- a/ui/lib/kv/addon/components/kv-paths-card.hbs
+++ b/ui/lib/kv/addon/components/kv-paths-card.hbs
@@ -53,7 +53,7 @@
for other CLI commands.
`[data-test-sidebar-nav-link="${label}"]`,
cancelButton: '[data-test-cancel]',
saveButton: '[data-test-save]',
+ backButton: '[data-test-back-button]',
+ codeBlock: (label: string) => `[data-test-code-block="${label}"]`,
codemirror: `[data-test-component="code-mirror-modifier"]`,
codemirrorTextarea: `[data-test-component="code-mirror-modifier"] textarea`,
};
diff --git a/ui/tests/helpers/kv/kv-selectors.js b/ui/tests/helpers/kv/kv-selectors.js
index 3c0b4af42c46..67b766fdf9f9 100644
--- a/ui/tests/helpers/kv/kv-selectors.js
+++ b/ui/tests/helpers/kv/kv-selectors.js
@@ -90,8 +90,8 @@ export const PAGE = {
},
paths: {
copyButton: (label) => `${PAGE.infoRowValue(label)} button`,
- codeSnippet: (section) => `[data-test-commands="${section}"] code`,
- snippetCopy: (section) => `[data-test-commands="${section}"] button`,
+ codeSnippet: (section) => `[data-test-code-block="${section}"] code`,
+ snippetCopy: (section) => `[data-test-code-block="${section}"] button`,
},
};
diff --git a/ui/tests/helpers/secret-engine/secret-engine-helpers.js b/ui/tests/helpers/secret-engine/secret-engine-helpers.js
index 2a9c88601c4d..e970bd0afa23 100644
--- a/ui/tests/helpers/secret-engine/secret-engine-helpers.js
+++ b/ui/tests/helpers/secret-engine/secret-engine-helpers.js
@@ -206,3 +206,49 @@ export const fillInAwsConfig = async (situation = 'withAccess') => {
await fillIn(GENERAL.ttl.input('Identity token TTL'), '7200');
}
};
+
+// Example usage
+// createLongJson (2, 3) will create a json object with 2 original keys, each with 3 nested keys
+// {
+// "key-0": {
+// "nested-key-0": {
+// "nested-key-1": {
+// "nested-key-2": "nested-value"
+// }
+// }
+// },
+// "key-1": {
+// "nested-key-0": {
+// "nested-key-1": {
+// "nested-key-2": "nested-value"
+// }
+// }
+// }
+// }
+
+export function createLongJson(lines = 10, nestLevel = 3) {
+ const keys = Array.from({ length: nestLevel }, (_, i) => `nested-key-${i}`);
+ const jsonObject = {};
+
+ for (let i = 0; i < lines; i++) {
+ nestLevel > 0
+ ? (jsonObject[`key-${i}`] = createNestedObject({}, keys, 'nested-value'))
+ : (jsonObject[`key-${i}`] = 'non-nested-value');
+ }
+ return jsonObject;
+}
+
+function createNestedObject(obj = {}, keys, value) {
+ let current = obj;
+
+ for (let i = 0; i < keys.length - 1; i++) {
+ const key = keys[i];
+ if (!current[key]) {
+ current[key] = {};
+ }
+ current = current[key];
+ }
+
+ current[keys[keys.length - 1]] = value;
+ return obj;
+}
diff --git a/ui/tests/integration/components/json-editor-test.js b/ui/tests/integration/components/json-editor-test.js
index 100b7d8ef9b8..56f4a674f8d9 100644
--- a/ui/tests/integration/components/json-editor-test.js
+++ b/ui/tests/integration/components/json-editor-test.js
@@ -11,6 +11,7 @@ import hbs from 'htmlbars-inline-precompile';
import jsonEditor from '../../pages/components/json-editor';
import sinon from 'sinon';
import { setRunOptions } from 'ember-a11y-testing/test-support';
+import { createLongJson } from 'vault/tests/helpers/secret-engine/secret-engine-helpers';
const component = create(jsonEditor);
@@ -29,6 +30,7 @@ module('Integration | Component | json-editor', function (hooks) {
this.set('onFocusOut', sinon.spy());
this.set('json_blob', JSON_BLOB);
this.set('bad_json_blob', BAD_JSON_BLOB);
+ this.set('long_json', JSON.stringify(createLongJson(), null, `\t`));
this.set('hashi-read-only-theme', 'hashi-read-only auto-height');
setRunOptions({
rules: {
@@ -36,6 +38,8 @@ module('Integration | Component | json-editor', function (hooks) {
label: { enabled: false },
// TODO: investigate and fix Codemirror styling
'color-contrast': { enabled: false },
+ // failing on .CodeMirror-scroll
+ 'scrollable-region-focusable': { enabled: false },
},
});
});
@@ -129,4 +133,31 @@ module('Integration | Component | json-editor', function (hooks) {
'even after hitting enter the value is still set correctly'
);
});
+
+ test('no viewportMargin renders only default 10 lines of data on the DOM', async function (assert) {
+ await render(hbs`
+
+ `);
+ assert
+ .dom('.CodeMirror-code')
+ .doesNotIncludeText('key-9', 'Without viewportMargin, user cannot search for key-9');
+ });
+
+ test('when viewportMargin is set user is able to search a long secret', async function (assert) {
+ await render(hbs`
+
+ `);
+ assert
+ .dom('.CodeMirror-code')
+ .containsText('key-9', 'With viewportMargin set, user can search for key-9');
+ });
});
diff --git a/ui/tests/integration/components/kv/kv-data-fields-test.js b/ui/tests/integration/components/kv/kv-data-fields-test.js
index e9e18d99f368..ac3d5ed8b606 100644
--- a/ui/tests/integration/components/kv/kv-data-fields-test.js
+++ b/ui/tests/integration/components/kv/kv-data-fields-test.js
@@ -11,6 +11,9 @@ import { hbs } from 'ember-cli-htmlbars';
import { fillIn, render, click } from '@ember/test-helpers';
import codemirror from 'vault/tests/helpers/codemirror';
import { PAGE, FORM } from 'vault/tests/helpers/kv/kv-selectors';
+import { GENERAL } from 'vault/tests/helpers/general-selectors';
+import { setRunOptions } from 'ember-a11y-testing/test-support';
+import { createLongJson } from 'vault/tests/helpers/secret-engine/secret-engine-helpers';
module('Integration | Component | kv-v2 | KvDataFields', function (hooks) {
setupRenderingTest(hooks);
@@ -22,6 +25,12 @@ module('Integration | Component | kv-v2 | KvDataFields', function (hooks) {
this.backend = 'my-kv-engine';
this.path = 'my-secret';
this.secret = this.store.createRecord('kv/data', { backend: this.backend });
+ setRunOptions({
+ rules: {
+ // failing on .CodeMirror-scroll
+ 'scrollable-region-focusable': { enabled: false },
+ },
+ });
});
test('it updates the secret model', async function (assert) {
@@ -88,7 +97,7 @@ module('Integration | Component | kv-v2 | KvDataFields', function (hooks) {
assert.dom(PAGE.infoRowValue('foo')).hasText('bar', 'secret value shows after toggle');
});
- test('it shows readonly json editor when viewing secret details of complex secret', async function (assert) {
+ test('it shows hds codeblock when viewing secret details of complex secret', async function (assert) {
this.secret.secretData = {
foo: {
bar: 'baz',
@@ -100,7 +109,24 @@ module('Integration | Component | kv-v2 | KvDataFields', function (hooks) {
owner: this.engine,
});
assert.dom(PAGE.infoRowValue('foo')).doesNotExist('does not render rows of secret data');
- assert.dom('[data-test-component="code-mirror-modifier"]').hasClass('readonly-codemirror');
- assert.dom('[data-test-component="code-mirror-modifier"]').includesText(`{ "foo": { "bar": "baz" }}`);
+ assert.dom(GENERAL.codeBlock('secret-data')).exists('hds codeBlock exists');
+ assert
+ .dom(GENERAL.codeBlock('secret-data'))
+ .hasText(`Version data { "foo": { "bar": "baz" } } `, 'Json data is displayed');
+ });
+
+ test('it defaults to a viewportMargin 10 when there is no secret data', async function (assert) {
+ await render(hbs``, { owner: this.engine });
+ assert.strictEqual(codemirror().options.viewportMargin, 10, 'viewportMargin defaults to 10');
+ });
+
+ test('it calculates viewportMargin based on secret size', async function (assert) {
+ this.secret.secretData = createLongJson(100);
+ await render(hbs``, { owner: this.engine });
+ assert.strictEqual(
+ codemirror().options.viewportMargin,
+ 100,
+ 'viewportMargin is set to 100 matching the height of the json'
+ );
});
});
diff --git a/ui/tests/integration/components/kv/kv-patch/editor/form-test.js b/ui/tests/integration/components/kv/kv-patch/editor/form-test.js
index 4096feb945be..3cc72291067a 100644
--- a/ui/tests/integration/components/kv/kv-patch/editor/form-test.js
+++ b/ui/tests/integration/components/kv/kv-patch/editor/form-test.js
@@ -96,14 +96,14 @@ module('Integration | Component | kv | kv-patch/editor/form', function (hooks) {
await this.renderComponent();
assert.dom(GENERAL.toggleInput('Reveal subkeys')).isNotChecked('toggle is initially unchecked');
- assert.dom('[data-test-subkeys]').doesNotExist();
+ assert.dom(GENERAL.codeBlock('subkeys')).doesNotExist();
await click(GENERAL.toggleInput('Reveal subkeys'));
assert.dom(GENERAL.toggleInput('Reveal subkeys')).isChecked();
- assert.dom('[data-test-subkeys]').hasText(JSON.stringify(this.subkeys, null, 2));
+ assert.dom(GENERAL.codeBlock('subkeys')).hasText(JSON.stringify(this.subkeys, null, 2));
await click(GENERAL.toggleInput('Reveal subkeys'));
assert.dom(GENERAL.toggleInput('Reveal subkeys')).isNotChecked();
- assert.dom('[data-test-subkeys]').doesNotExist('unchecking re-hides subkeys');
+ assert.dom(GENERAL.codeBlock('subkeys')).doesNotExist('unchecking re-hides subkeys');
});
test('it enables and disables inputs', async function (assert) {
diff --git a/ui/tests/integration/components/kv/kv-patch/json-form-test.js b/ui/tests/integration/components/kv/kv-patch/json-form-test.js
index 166ea8d2fa7a..37703e2bd985 100644
--- a/ui/tests/integration/components/kv/kv-patch/json-form-test.js
+++ b/ui/tests/integration/components/kv/kv-patch/json-form-test.js
@@ -59,14 +59,14 @@ module('Integration | Component | kv | kv-patch/editor/json-form', function (hoo
await this.renderComponent();
assert.dom(GENERAL.toggleInput('Reveal subkeys')).isNotChecked('toggle is initially unchecked');
- assert.dom('[data-test-subkeys]').doesNotExist();
+ assert.dom(GENERAL.codeBlock('subkeys')).doesNotExist();
await click(GENERAL.toggleInput('Reveal subkeys'));
assert.dom(GENERAL.toggleInput('Reveal subkeys')).isChecked();
- assert.dom('[data-test-subkeys]').hasText(JSON.stringify(this.subkeys, null, 2));
+ assert.dom(GENERAL.codeBlock('subkeys')).hasText(JSON.stringify(this.subkeys, null, 2));
await click(GENERAL.toggleInput('Reveal subkeys'));
assert.dom(GENERAL.toggleInput('Reveal subkeys')).isNotChecked();
- assert.dom('[data-test-subkeys]').doesNotExist('unchecking re-hides subkeys');
+ assert.dom(GENERAL.codeBlock('subkeys')).doesNotExist('unchecking re-hides subkeys');
});
test('it renders linting errors', async function (assert) {
diff --git a/ui/tests/integration/components/kv/page/kv-page-secret-details-test.js b/ui/tests/integration/components/kv/page/kv-page-secret-details-test.js
index 430ff3765f08..298d1fd330a3 100644
--- a/ui/tests/integration/components/kv/page/kv-page-secret-details-test.js
+++ b/ui/tests/integration/components/kv/page/kv-page-secret-details-test.js
@@ -7,13 +7,14 @@ import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { setupEngine } from 'ember-engines/test-support';
import { setupMirage } from 'ember-cli-mirage/test-support';
-import { click, find, render } from '@ember/test-helpers';
+import { click, render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { kvDataPath } from 'vault/utils/kv-path';
-import { FORM, PAGE, parseJsonEditor } from 'vault/tests/helpers/kv/kv-selectors';
+import { FORM, PAGE } from 'vault/tests/helpers/kv/kv-selectors';
import { syncStatusResponse } from 'vault/mirage/handlers/sync';
import { encodePath } from 'vault/utils/path-encoding-helpers';
import { baseSetup } from 'vault/tests/helpers/kv/kv-run-commands';
+import { GENERAL } from 'vault/tests/helpers/general-selectors';
module('Integration | Component | kv-v2 | Page::Secret::Details', function (hooks) {
setupRenderingTest(hooks);
@@ -126,19 +127,24 @@ module('Integration | Component | kv-v2 | Page::Secret::Details', function (hook
await click(FORM.toggleMasked);
assert.dom(PAGE.infoRowValue('foo')).hasText('bar', 'renders secret value');
await click(FORM.toggleJson);
- assert.propEqual(parseJsonEditor(find), this.secretData, 'json editor renders secret data');
+ assert.dom(GENERAL.codeBlock('secret-data')).hasText(
+ `Version data {
+ "foo": "bar"
+}`,
+ 'json editor renders secret data'
+ );
assert
.dom(PAGE.detail.versionTimestamp)
.includesText(`Version ${this.version} created`, 'renders version and time created');
});
- test('it renders json view when secret is complex', async function (assert) {
+ test('it renders hds codeblock view when secret is complex', async function (assert) {
assert.expect(4);
await this.renderComponent(this.modelComplex);
assert.dom(PAGE.infoRowValue('foo')).doesNotExist('does not render rows of secret data');
assert.dom(FORM.toggleJson).isChecked();
assert.dom(FORM.toggleJson).isNotDisabled();
- assert.dom('[data-test-component="code-mirror-modifier"]').exists('shows json editor');
+ assert.dom(GENERAL.codeBlock('secret-data')).exists('hds codeBlock exists');
});
test('it renders deleted empty state', async function (assert) {