mirror of
https://github.com/hashicorp/vault.git
synced 2026-05-05 12:26:34 +02:00
* fix code editor updates on @value change * fix test failures * remove setRunOptions * use component "restore example" instead of custom one" * add test coverage * Revert "use component "restore example" instead of custom one"" This reverts commit 8e685f871cefd3f8a8be72b094b5e1b5fb93c894. * fix formfield reset action * remove remaining unused @container args * add comment * use helpText * add test coverage Co-authored-by: claire bontempo <68122737+hellobontempo@users.noreply.github.com>
This commit is contained in:
parent
4617d3fd16
commit
f39bb43a4c
@ -16,7 +16,7 @@
|
||||
<PolicyForm @onSave={{this.onSave}} @model={{this.policy}} @onCancel={{@onCancel}} />
|
||||
</T.Panel>
|
||||
<T.Panel class="has-top-padding-m">
|
||||
<PolicyExample @policyType={{this.policy.policyType}} @container="#search-select-modal" />
|
||||
<PolicyExample @policyType={{this.policy.policyType}} />
|
||||
</T.Panel>
|
||||
</Hds::Tabs>
|
||||
{{else}}
|
||||
|
||||
@ -108,7 +108,7 @@
|
||||
Policy
|
||||
</M.Header>
|
||||
<M.Body>
|
||||
<PolicyExample @policyType={{@model.policyType}} @container="#policy-example-modal" />
|
||||
<PolicyExample @policyType={{@model.policyType}} />
|
||||
</M.Body>
|
||||
<M.Footer as |F|>
|
||||
<Hds::Button @text="Close" {{on "click" F.close}} data-test-modal-close-button />
|
||||
|
||||
@ -21,7 +21,6 @@
|
||||
@title="Unwrapped Data"
|
||||
@value={{stringify this.unwrapData}}
|
||||
@readOnly={{true}}
|
||||
@container=".toolbar-actions"
|
||||
/>
|
||||
</T.Panel>
|
||||
<T.Panel>
|
||||
|
||||
@ -51,7 +51,7 @@
|
||||
<JsonEditor
|
||||
class="has-top-margin-s"
|
||||
@title="Data to wrap"
|
||||
@subTitle="json-formatted"
|
||||
@helpText="json-formatted"
|
||||
@value={{this.stringifiedWrapData}}
|
||||
@valueUpdated={{this.editorUpdated}}
|
||||
/>
|
||||
|
||||
@ -490,14 +490,15 @@
|
||||
@helpText={{this.helpTextString}}
|
||||
@mode={{@attr.options.mode}}
|
||||
@example={{@attr.options.example}}
|
||||
@onSetup={{fn (mut this.codemirrorEditor)}}
|
||||
>
|
||||
{{#if @attr.options.allowReset}}
|
||||
<Hds::Button
|
||||
@text="Clear"
|
||||
@icon="reload"
|
||||
class="toolbar-button"
|
||||
@color="secondary"
|
||||
disabled={{not value}}
|
||||
{{on "click" this.setAndBroadcast}}
|
||||
{{on "click" (fn this.editorUpdated true "")}}
|
||||
data-test-json-clear-button
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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 @@
|
||||
</CE.Title>
|
||||
{{/if}}
|
||||
|
||||
{{#if @subTitle}}
|
||||
<CE.Description>
|
||||
{{@subTitle}}
|
||||
</CE.Description>
|
||||
{{/if}}
|
||||
|
||||
{{#if @helpText}}
|
||||
<CE.Description>
|
||||
{{@helpText}}
|
||||
@ -56,7 +50,7 @@
|
||||
|
||||
{{#if @example}}
|
||||
<Hds::Button
|
||||
class="toolbar-button"
|
||||
@color="secondary"
|
||||
@icon="reload"
|
||||
@text="Restore example"
|
||||
disabled={{not @value}}
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { action } from '@ember/object';
|
||||
import { assert } from '@ember/debug';
|
||||
|
||||
/**
|
||||
* @module JsonEditor
|
||||
@ -13,7 +14,6 @@ import { action } from '@ember/object';
|
||||
* <JsonEditor @title="Policy" @value={{hash foo="bar"}} @viewportMargin={{100}} />
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,14 +12,13 @@ import Component from '@glimmer/component';
|
||||
* (example below), otherwise the JsonEditor value won't render until it's focused.
|
||||
*
|
||||
* @example
|
||||
* <PolicyExample @policyType="acl" @container="#search-select-modal" />
|
||||
* <PolicyExample @policyType="acl" />
|
||||
* @example
|
||||
* <PolicyExample @policyType="rgp" />
|
||||
* @example
|
||||
* <PolicyExample @policyType="egp" />
|
||||
*
|
||||
* @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 {
|
||||
|
||||
@ -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)}}
|
||||
>
|
||||
<Hds::Button
|
||||
@icon="reload"
|
||||
@text="Restore example"
|
||||
@color="secondary"
|
||||
{{on "click" this.resetRoleRules}}
|
||||
data-test-restore-example
|
||||
/>
|
||||
</JsonEditor>
|
||||
@onRestoreExample={{this.resetRoleRules}}
|
||||
/>
|
||||
{{/let}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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`<JsonEditor
|
||||
@value={{"{}"}}
|
||||
@title={{"Test title"}}
|
||||
@showToolbar={{true}}
|
||||
/>`);
|
||||
});
|
||||
|
||||
test('onSetup returns instance of codemirror', async function (assert) {
|
||||
let codemirror;
|
||||
this.onSetup = (editor) => {
|
||||
codemirror = editor;
|
||||
};
|
||||
await render(hbs`
|
||||
<JsonEditor
|
||||
@value={{this.value}}
|
||||
@example={{this.example}}
|
||||
@mode="ruby"
|
||||
@valueUpdated={{fn (mut this.value)}}
|
||||
@onSetup={{this.onSetup}}
|
||||
/>
|
||||
`);
|
||||
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');
|
||||
|
||||
@ -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`<Page::Role::CreateAndEdit @model={{this.newModel}} @breadcrumbs={{this.breadcrumbs}} />`,
|
||||
{ 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:
|
||||
- <list of policies to authorize>
|
||||
`;
|
||||
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) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user