diff --git a/ui/app/components/modal-form/policy-template.hbs b/ui/app/components/modal-form/policy-template.hbs
index 0b6f0ec5f7..f191f14724 100644
--- a/ui/app/components/modal-form/policy-template.hbs
+++ b/ui/app/components/modal-form/policy-template.hbs
@@ -16,7 +16,7 @@
-
+
{{else}}
diff --git a/ui/app/components/policy-form.hbs b/ui/app/components/policy-form.hbs
index 10d6b1785e..46db130cb6 100644
--- a/ui/app/components/policy-form.hbs
+++ b/ui/app/components/policy-form.hbs
@@ -108,7 +108,7 @@
Policy
-
+
diff --git a/ui/app/components/tools/unwrap.hbs b/ui/app/components/tools/unwrap.hbs
index b83469d093..967d50d7f0 100644
--- a/ui/app/components/tools/unwrap.hbs
+++ b/ui/app/components/tools/unwrap.hbs
@@ -21,7 +21,6 @@
@title="Unwrapped Data"
@value={{stringify this.unwrapData}}
@readOnly={{true}}
- @container=".toolbar-actions"
/>
diff --git a/ui/app/components/tools/wrap.hbs b/ui/app/components/tools/wrap.hbs
index 21c60899f3..f0993a6787 100644
--- a/ui/app/components/tools/wrap.hbs
+++ b/ui/app/components/tools/wrap.hbs
@@ -51,7 +51,7 @@
diff --git a/ui/lib/core/addon/components/form-field.hbs b/ui/lib/core/addon/components/form-field.hbs
index 8573a2742c..c1f3809381 100644
--- a/ui/lib/core/addon/components/form-field.hbs
+++ b/ui/lib/core/addon/components/form-field.hbs
@@ -490,14 +490,15 @@
@helpText={{this.helpTextString}}
@mode={{@attr.options.mode}}
@example={{@attr.options.example}}
+ @onSetup={{fn (mut this.codemirrorEditor)}}
>
{{#if @attr.options.allowReset}}
{{/if}}
diff --git a/ui/lib/core/addon/components/form-field.js b/ui/lib/core/addon/components/form-field.js
index 70c699c2b6..de766070a0 100644
--- a/ui/lib/core/addon/components/form-field.js
+++ b/ui/lib/core/addon/components/form-field.js
@@ -67,6 +67,7 @@ export default class FormFieldComponent extends Component {
'ttl',
'toggleButton',
];
+ @tracked codemirrorEditor;
@tracked showToggleTextInput = false;
@tracked toggleInputEnabled = false;
@@ -250,10 +251,16 @@ export default class FormFieldComponent extends Component {
editorUpdated(isString, value) {
try {
const valToSet = isString ? value : JSON.parse(value);
-
this.args.model.set(this.valuePath, valToSet);
-
this.onChange(this.valuePath, valToSet);
+
+ // Clicking "Clear" passes an empty string as the value and in that case we must manually reset the editor.
+ // At this time `allowReset` is only passed when `isString` is true.
+ if (value === '' && this.args.attr.options.allowReset && isString) {
+ this.codemirrorEditor.dispatch({
+ changes: [{ from: 0, to: this.codemirrorEditor.state.doc.length, insert: '' }],
+ });
+ }
} catch {
// if the value is not valid JSON, we don't want to set it on the model
}
diff --git a/ui/lib/core/addon/components/json-editor.hbs b/ui/lib/core/addon/components/json-editor.hbs
index 175b7241c2..07b6f00ef0 100644
--- a/ui/lib/core/addon/components/json-editor.hbs
+++ b/ui/lib/core/addon/components/json-editor.hbs
@@ -27,7 +27,7 @@
@isLintingEnabled={{eq this.mode "json"}}
@value={{or @value @example}}
@onBlur={{@onBlur}}
- @onInput={{this.onUpdate}}
+ @onInput={{@valueUpdated}}
@onLint={{@onLint}}
@onSetup={{this.onSetup}}
as |CE|
@@ -39,12 +39,6 @@
{{/if}}
- {{#if @subTitle}}
-
- {{@subTitle}}
-
- {{/if}}
-
{{#if @helpText}}
{{@helpText}}
@@ -56,7 +50,7 @@
{{#if @example}}
*
* @param {string} [title] - Name above codemirror view
- * @param {boolean} [showToolbar=true] - If false, toolbar and title are hidden
* @param {string} [value] - a specific string the comes from codemirror. It's the value inside the codemirror display
* @param {Function} [valueUpdated] - action to preform when you edit the codemirror value.
* @param {Function} [onBlur] - action to preform when you focus out of codemirror.
@@ -23,14 +23,21 @@ 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} [value] - Value within the display. Generally, a json string.
* @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} [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
* @param {Function} [onSetup] - action to preform when the codemirror editor is setup.
+ * @param {Function} [onRestoreExample] - override callback to customize "Restore example" behavior. Default behavior is to reset @value to `null`
*
*/
export default class JsonEditorComponent extends Component {
_codemirrorEditor = null;
+ constructor() {
+ super(...arguments);
+
+ const hasValueUpdated = !this.args.readOnly ? !!this.args.valueUpdated : true;
+ assert('@valueUpdated callback is required when component is not @readOnly', hasValueUpdated);
+ }
+
get mode() {
return this.args.mode ?? 'json';
}
@@ -51,24 +58,23 @@ export default class JsonEditorComponent extends Component {
}
@action
- onUpdate(...args) {
- if (!this.args.readOnly) {
- // catching a situation in which the user is not readOnly and has not provided a valueUpdated function to the instance
- this.args.valueUpdated(...args);
+ restoreExample() {
+ if (this.args.onRestoreExample) {
+ // Override to reset the @value of the code editor to something other than `null`
+ this.args.onRestoreExample();
+ } else {
+ // Display @example in the editor but reset @value to `null` because
+ // sometimes @example is not valid to set and submit as the actual input value.
+ this._codemirrorEditor.dispatch({
+ changes: [
+ {
+ from: 0,
+ to: this._codemirrorEditor.state.doc.length,
+ insert: this.args.example,
+ },
+ ],
+ });
+ this.args.valueUpdated(null);
}
}
-
- @action
- restoreExample() {
- this._codemirrorEditor.dispatch({
- changes: [
- {
- from: 0,
- to: this._codemirrorEditor.state.doc.length,
- insert: this.args.example,
- },
- ],
- });
- this.args.valueUpdated(null, this._codemirrorEditor);
- }
}
diff --git a/ui/lib/core/addon/components/policy-example.js b/ui/lib/core/addon/components/policy-example.js
index 159ddf89e3..05fd541c40 100644
--- a/ui/lib/core/addon/components/policy-example.js
+++ b/ui/lib/core/addon/components/policy-example.js
@@ -12,14 +12,13 @@ import Component from '@glimmer/component';
* (example below), otherwise the JsonEditor value won't render until it's focused.
*
* @example
- *
+ *
* @example
*
* @example
*
*
* @param {string} policyType - policy type to decide which template to render; can either be "acl" or "rgp"
- * @param {string} container - selector for the container the example renders inside, passed to the copy button in JsonEditor
*/
export default class PolicyExampleComponent extends Component {
diff --git a/ui/lib/kubernetes/addon/components/page/role/create-and-edit.hbs b/ui/lib/kubernetes/addon/components/page/role/create-and-edit.hbs
index 7965302776..fb4ade83e5 100644
--- a/ui/lib/kubernetes/addon/components/page/role/create-and-edit.hbs
+++ b/ui/lib/kubernetes/addon/components/page/role/create-and-edit.hbs
@@ -112,19 +112,13 @@
data-test-rules
@title="Role rules"
@value={{template.rules}}
+ @example={{template.rules}}
@mode="ruby"
@valueUpdated={{fn (mut template.rules)}}
@helpText={{sanitized-html this.roleRulesHelpText}}
@onSetup={{fn (mut this.codemirrorEditor)}}
- >
-
-
+ @onRestoreExample={{this.resetRoleRules}}
+ />
{{/let}}
{{/if}}
diff --git a/ui/lib/kubernetes/addon/components/page/role/create-and-edit.js b/ui/lib/kubernetes/addon/components/page/role/create-and-edit.js
index 4fe70b5f21..c193b6ecc5 100644
--- a/ui/lib/kubernetes/addon/components/page/role/create-and-edit.js
+++ b/ui/lib/kubernetes/addon/components/page/role/create-and-edit.js
@@ -112,14 +112,21 @@ export default class CreateAndEditRolePageComponent extends Component {
@action
resetRoleRules() {
+ // Reset tracked rule templates to initial values
this.roleRulesTemplates = getRules();
+ // Make sure editor renders the reset template
+ this.updateCodeMirror();
+ }
+ @action
+ updateCodeMirror() {
+ const template = this.roleRulesTemplates.find((t) => t.id === this.selectedTemplateId);
this.codemirrorEditor.dispatch({
changes: [
{
from: 0,
to: this.codemirrorEditor.state.doc.length,
- insert: this.args.value,
+ insert: template.rules,
},
],
});
@@ -128,6 +135,8 @@ export default class CreateAndEditRolePageComponent extends Component {
@action
selectTemplate(event) {
this.selectedTemplateId = event.target.value;
+ // Dispatch the event to codemirror so the code editor updates when a template is selected
+ this.updateCodeMirror();
}
@action
diff --git a/ui/tests/integration/components/form-field-test.js b/ui/tests/integration/components/form-field-test.js
index d758194639..4339510e6d 100644
--- a/ui/tests/integration/components/form-field-test.js
+++ b/ui/tests/integration/components/form-field-test.js
@@ -6,12 +6,13 @@
import EmberObject from '@ember/object';
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
-import { render, click, fillIn, findAll, setupOnerror } from '@ember/test-helpers';
+import { render, click, fillIn, findAll, setupOnerror, waitFor } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
import { create } from 'ember-cli-page-object';
import sinon from 'sinon';
import formFields from '../../pages/components/form-field';
import { format, startOfDay } from 'date-fns';
+import codemirror, { getCodeEditorValue, setCodeEditorValue } from 'vault/tests/helpers/codemirror';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
import AuthMethodForm from 'vault/forms/auth/method';
@@ -114,11 +115,16 @@ module('Integration | Component | form field', function (hooks) {
assert.ok(component.hasJSONEditor, 'renders the json editor');
});
- test('it renders: string as json with clear button', async function (assert) {
+ test('it renders: string as json with clear button and resets input', async function (assert) {
await setup.call(this, createAttr('foo', 'string', { editType: 'json', allowReset: true }));
assert.dom('[data-test-component="json-editor-title"]').hasText('Foo', 'renders a label');
assert.ok(component.hasJSONEditor, 'renders the json editor');
assert.ok(component.hasJSONClearButton, 'renders button that will clear the JSON value');
+ await waitFor('.cm-editor');
+ const editor = codemirror();
+ setCodeEditorValue(editor, 'some input');
+ await click('[data-test-json-clear-button]');
+ assert.strictEqual(getCodeEditorValue(editor), '', 'it clears the editor');
});
test('it renders: toggleButton', async function (assert) {
diff --git a/ui/tests/integration/components/json-editor-test.js b/ui/tests/integration/components/json-editor-test.js
index 17040dc106..a6df51258f 100644
--- a/ui/tests/integration/components/json-editor-test.js
+++ b/ui/tests/integration/components/json-editor-test.js
@@ -5,11 +5,10 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
-import { render, click, triggerKeyEvent, waitFor } from '@ember/test-helpers';
+import { render, click, triggerKeyEvent, waitFor, setupOnerror } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
import { SELECTORS } 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';
import codemirror, { getCodeEditorValue, setCodeEditorValue } from 'vault/tests/helpers/codemirror';
@@ -30,16 +29,6 @@ module('Integration | Component | json-editor', function (hooks) {
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: {
- // CodeMirror has a secret textarea without a label that causes this problem
- label: { enabled: false },
- // TODO: investigate and fix Codemirror styling
- 'color-contrast': { enabled: false },
- // failing on .CodeMirror-scroll
- 'scrollable-region-focusable': { enabled: false },
- },
- });
});
test('it renders', async function (assert) {
@@ -56,6 +45,41 @@ module('Integration | Component | json-editor', function (hooks) {
assert.dom(SELECTORS.codeBlock).exists('renders the code block');
});
+ test('it requires @valueUpdated arg when component is not @readOnly', async function (assert) {
+ assert.expect(1);
+ // catch error so qunit test doesn't fail
+ setupOnerror((error) => {
+ assert.strictEqual(
+ error.message,
+ 'Assertion Failed: @valueUpdated callback is required when component is not @readOnly',
+ 'it throws error when component is editable (not @readOnly) and @valueUpdated is not provided'
+ );
+ });
+ await render(hbs``);
+ });
+
+ test('onSetup returns instance of codemirror', async function (assert) {
+ let codemirror;
+ this.onSetup = (editor) => {
+ codemirror = editor;
+ };
+ await render(hbs`
+
+ `);
+ await waitFor('.cm-editor');
+ assert.true(codemirror.viewState.inView, 'onSetup returns instance of codemirror');
+ });
+
test('it should render example and restore it', async function (assert) {
this.value = null;
this.example = 'this is a test example';
@@ -69,8 +93,8 @@ module('Integration | Component | json-editor', function (hooks) {
/>
`);
- let view = codemirror();
await waitFor('.cm-editor');
+ let view = codemirror();
let editorValue = getCodeEditorValue(view);
assert.strictEqual(editorValue, this.example, 'Example renders when there is no value');
diff --git a/ui/tests/integration/components/kubernetes/page/role/create-and-edit-test.js b/ui/tests/integration/components/kubernetes/page/role/create-and-edit-test.js
index 273053455a..f01a55eff6 100644
--- a/ui/tests/integration/components/kubernetes/page/role/create-and-edit-test.js
+++ b/ui/tests/integration/components/kubernetes/page/role/create-and-edit-test.js
@@ -12,7 +12,7 @@ import hbs from 'htmlbars-inline-precompile';
import sinon from 'sinon';
import { setRunOptions } from 'ember-a11y-testing/test-support';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
-import codemirror, { setCodeEditorValue } from 'vault/tests/helpers/codemirror';
+import codemirror, { getCodeEditorValue, setCodeEditorValue } from 'vault/tests/helpers/codemirror';
module('Integration | Component | kubernetes | Page::Role::CreateAndEdit', function (hooks) {
setupRenderingTest(hooks);
@@ -151,6 +151,49 @@ module('Integration | Component | kubernetes | Page::Role::CreateAndEdit', funct
);
});
+ test('it should update code editor when template selection changes', async function (assert) {
+ await render(
+ hbs``,
+ { owner: this.engine }
+ );
+
+ await click('[data-test-radio-card="full"]');
+ await waitFor('.cm-editor');
+ const editor = codemirror();
+ const expectedInitialValue = `# The below is an example that you can use as a starting point.
+#
+# rules:
+# - apiGroups: [""]
+# resources: ["serviceaccounts", "serviceaccounts/token"]
+# verbs: ["create", "update", "delete"]
+# - apiGroups: ["rbac.authorization.k8s.io"]
+# resources: ["rolebindings", "clusterrolebindings"]
+# verbs: ["create", "update", "delete"]
+# - apiGroups: ["rbac.authorization.k8s.io"]
+# resources: ["roles", "clusterroles"]
+# verbs: ["bind", "escalate", "create", "update", "delete"]
+`;
+ assert.strictEqual(
+ getCodeEditorValue(editor),
+ expectedInitialValue,
+ 'editor initially renders rules from example template'
+ );
+ // Select a different template
+ await fillIn('[data-test-select-template]', '6');
+ const expectedRule = `rules:
+- apiGroups: ['policy']
+ resources: ['podsecuritypolicies']
+ verbs: ['use']
+ resourceNames:
+ -
+`;
+ assert.strictEqual(
+ getCodeEditorValue(editor),
+ expectedRule,
+ 'code editor updates and renders rules from selected template'
+ );
+ });
+
test('it should create new role', async function (assert) {
assert.expect(3);
@@ -257,8 +300,16 @@ module('Integration | Component | kubernetes | Page::Role::CreateAndEdit', funct
const editor = codemirror();
setCodeEditorValue(editor, addedText);
await settled();
+ assert.strictEqual(getCodeEditorValue(editor), addedText, 'code editor contains addedText');
await click('[data-test-restore-example]');
- assert.dom('.cm-content').doesNotContainText(addedText, 'Role rules example restored');
+ const expectedValue = `rules:
+- apiGroups: [""]
+ resources: ["secrets", "services"]
+ verbs: ["get", "watch", "list", "create", "delete", "deletecollection", "patch", "update"]
+`;
+ assert.strictEqual(getCodeEditorValue(editor), expectedValue, 'code editor is reset to initial value');
+ assert.strictEqual(this.role.generatedRoleRules, expectedValue, 'model value matches code editor');
+ assert.dom('.cm-content').doesNotContainText(addedText, 'editor does not contain added text');
});
test('it should set generatedRoleRoles model prop on save', async function (assert) {