Updated code mirror component for consistency (#11500)

* Updated code mirror component for consistency

- Hide gutters, line number and selection while read only
- Show toolbar with copy functionality for all instances

* Moved toolbar and actions to json editor component

* Updated form-field-from-model template

* Added test for toolbar
This commit is contained in:
Arnav Palnitkar 2021-05-06 09:59:15 -07:00 committed by GitHub
parent 0926e302c5
commit 1d26f056bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 253 additions and 153 deletions

3
changelog/11500.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
ui: Updated ivy code mirror component for consistency
```

View File

@ -1,5 +1,5 @@
import { assign } from '@ember/polyfills';
import IvyCodemirrorComponent from './ivy-codemirror';
import Component from '@ember/component';
const JSON_EDITOR_DEFAULTS = {
// IMPORTANT: `gutters` must come before `lint` since the presence of
// `gutters` is cached internally when `lint` is toggled
@ -13,19 +13,41 @@ const JSON_EDITOR_DEFAULTS = {
showCursorWhenSelecting: true,
};
export default IvyCodemirrorComponent.extend({
'data-test-component': 'json-editor',
updateCodeMirrorOptions() {
const options = assign({}, JSON_EDITOR_DEFAULTS, this.options);
if (options.autoHeight) {
options.viewportMargin = Infinity;
delete options.autoHeight;
}
export default Component.extend({
showToolbar: true,
title: null,
subTitle: null,
helpText: null,
value: null,
options: null,
valueUpdated: null,
onFocusOut: null,
readOnly: false,
if (options) {
Object.keys(options).forEach(function(option) {
this.updateCodeMirrorOption(option, options[option]);
}, this);
init() {
this._super(...arguments);
this.options = { ...JSON_EDITOR_DEFAULTS, ...this.options };
if (this.options.autoHeight) {
this.options.viewportMargin = Infinity;
delete this.options.autoHeight;
}
if (this.options.readOnly) {
this.options.readOnly = 'nocursor';
this.options.lineNumbers = false;
delete this.options.gutters;
}
},
actions: {
updateValue(...args) {
if (this.valueUpdated) {
this.valueUpdated(...args);
}
},
onFocus(...args) {
if (this.onFocusOut) {
this.onFocusOut(...args);
}
},
},
});

View File

@ -49,6 +49,8 @@ export default Model.extend({
}),
policyDocument: attr('string', {
editType: 'json',
helpText:
'A policy is an object in AWS that, when associated with an identity or resource, defines their permissions.',
}),
fields: computed('credentialType', function() {
let credentialType = this.credentialType;

View File

@ -1,5 +1,5 @@
<div class="console-ui-output has-copy-button">
<JsonEditor @value={{stringify content}} @options={{hash
<JsonEditor @showToolbar={{false}} @value={{stringify content}} @options={{hash
readOnly=true
lineNumbers=false
autoHeight=true

View File

@ -12,6 +12,7 @@
<div class="has-copy-button">
<JsonEditor
data-test-json-viewer
@showToolbar={{false}}
@value={{ stringify unwrapData }}
@options={{hash
readOnly=true

View File

@ -0,0 +1,32 @@
{{#if showToolbar }}
<div data-test-component="json-editor-toolbar">
<Toolbar>
<label class="is-label" data-test-component="json-editor-title">
{{title}}
{{#if subTitle }}
<span class="is-size-9 is-lowercase has-text-grey">({{ subTitle }})</span>
{{/if}}
</label>
<ToolbarActions>
{{yield}}
<div class="toolbar-separator"></div>
<CopyButton class="button is-transparent" @clipboardText={{value}}
@buttonType="button" @success={{action (set-flash-message 'Data copied!')}}>
<Icon @glyph="copy-action" aria-label="Copy" />
</CopyButton>
</ToolbarActions>
</Toolbar>
</div>
{{/if}}
{{ivy-codemirror
data-test-component="json-editor"
value=value
options=options
valueUpdated=(action "updateValue")
onFocusOut=(action "onFocus")
}}
{{#if helpText }}
<div class="box is-shadowless is-fullwidth has-short-padding">
<p class="sub-text">{{ helpText }}</p>
</div>
{{/if}}

View File

@ -36,18 +36,12 @@
{{#if @showAdvancedMode}}
<div class="form-section">
<label class="title is-5">
{{#if isV2}}
Version data
{{else}}
Secret data
{{/if}}
</label>
<JsonEditor
<JsonEditor
@title={{if isV2 "Version Data" "Secret Data"}}
@value={{@codemirrorString}}
@valueUpdated={{@editActions.codemirrorUpdated}}
@onFocusOut={{@editActions.formatJSON}}
/>
@valueUpdated={{action @editActions.codemirrorUpdated}}
@onFocusOut={{action @editActions.formatJSON}}>
</JsonEditor>
</div>
{{else}}
<div class="form-section">

View File

@ -4,14 +4,14 @@
<p>You can decrypt ciphertext using <code>{{key.name}}</code> as the encryption key.</p>
</div>
<div class="field">
<label for="ciphertext" class="is-label">Ciphertext</label>
<div id="ciphertext-control" class="control">
<IvyCodemirror @valueUpdated={{action (mut ciphertext)}} @options={{hash
lineNumbers=true
tabSize=2
mode='ruby'
theme='hashi'
}} @data-test-transit-input="ciphertext" />
<JsonEditor
@title="Ciphertext"
@valueUpdated={{action (mut ciphertext)}}
@options={{hash
mode='ruby'
}}
@data-test-transit-input="ciphertext" />
</div>
</div>
{{#if key.derived}}

View File

@ -6,16 +6,14 @@
</div>
<KeyVersionSelect @key={{key}} @onVersionChange={{action (mut key_version)}} @key_version={{key_version}} />
<div class="field">
<label for="plaintext" class="is-label">
Plaintext
</label>
<div id="plaintext-control" class="control is-relative">
<IvyCodemirror @value={{plaintext}} @valueUpdated={{action (mut plaintext)}} @options={{hash
lineNumbers=true
tabSize=2
mode='ruby'
theme='hashi'
}} @data-test-transit-input="plaintext" />
<JsonEditor
@title="Plaintext"
@value={{plaintext}} @valueUpdated={{action (mut plaintext)}}
@options={{hash
mode='ruby'
}}
@data-test-transit-input="plaintext" />
</div>
</div>
<div class="field">

View File

@ -6,16 +6,14 @@
</div>
<KeyVersionSelect @key={{key}} @onVersionChange={{action (mut key_version)}} @key_version={{key_version}} />
<div class="field">
<label for="input" class="is-label">
Input
</label>
<div id="input-control" class="control is-relative">
<IvyCodemirror @valueUpdated={{action (mut input)}} @options={{hash
lineNumbers=true
tabSize=2
mode='ruby'
theme='hashi'
}} @data-test-transit-input="input" />
<JsonEditor
@title="Input"
@valueUpdated={{action (mut input)}}
@options={{hash
mode='ruby'
}}
@data-test-transit-input="input" />
</div>
</div>
<div class="field">

View File

@ -6,14 +6,14 @@
</div>
<KeyVersionSelect @key={{key}} @onVersionChange={{action (mut key_version)}} @key_version={{key_version}} />
<div class="field">
<label for="ciphertext" class="is-label">Ciphertext</label>
<div class="control is-expanded">
<IvyCodemirror @valueUpdated={{action (mut ciphertext)}} @options={{hash
lineNumbers=true
tabSize=2
mode='ruby'
theme='hashi'
}} />
<JsonEditor
@title="Ciphertext"
@valueUpdated={{action (mut ciphertext)}}
@options={{hash
mode='ruby'
}}
/>
</div>
</div>
{{#if key.derived}}

View File

@ -6,16 +6,15 @@
</div>
<KeyVersionSelect @key={{key}} @onVersionChange={{action (mut key_version)}} @key_version={{key_version}} />
<div class="field">
<label for="input" class="is-label">
Input
</label>
<div class="control is-relative">
<IvyCodemirror @value={{input}} @valueUpdated={{action (mut input)}} @options={{hash
lineNumbers=true
tabSize=2
<JsonEditor
@title="Input"
@value={{input}}
@valueUpdated={{action (mut input)}}
@options={{hash
mode='ruby'
theme='hashi'
}} @data-test-transit-input="input" />
}}
@data-test-transit-input="input" />
</div>
</div>
<div class="field">

View File

@ -4,16 +4,14 @@
<p>Check whether the provided signature is valid for the given data.</p>
</div>
<div class="field">
<label for="input" class="is-label">
Input
</label>
<div class="control is-relative">
<IvyCodemirror @id="input" @value={{input}} @valueUpdated={{action (mut input)}} @options={{hash
lineNumbers=true
tabSize=2
mode='ruby'
theme='hashi'
}} @data-test-transit-input="input" />
<JsonEditor
@title="Input"
@value={{input}} @valueUpdated={{action (mut input)}}
@options={{hash
mode='ruby'
}}
@data-test-transit-input="input" />
</div>
</div>
<div class="field">
@ -115,25 +113,26 @@
<div class="column is-two-thirds is-flex-column">
{{#if (or (and verification (eq verification 'HMAC')) hmac)}}
<div class="field is-flex-column is-flex-1">
<label for="hmac" class="is-label">HMAC</label>
<div class="control is-flex-column is-flex-1">
<IvyCodemirror @id="hmac" @value={{hmac}} @valueUpdated={{action (mut hmac)}} @options={{hash
lineNumbers=true
tabSize=2
mode='ruby'
theme='hashi'
}} />
<JsonEditor
@title="HMAC"
@value={{hmac}}
@valueUpdated={{action (mut hmac)}}
@options={{hash
mode='ruby'
}}
/>
</div>
</div>
{{else}}
<div class="field is-flex-column is-flex-1">
<label for="signature" class="is-label">Signature</label>
<div class="control is-flex-column is-flex-1">
<IvyCodemirror @id="signature" @value={{signature}} @valueUpdated={{action (mut signature)}} @options={{hash
lineNumbers=true
tabSize=2
<JsonEditor
@title="Signature"
@value={{signature}}
@valueUpdated={{action (mut signature)}}
@options={{hash
mode='ruby'
theme='hashi'
}} />
</div>
</div>
@ -142,14 +141,15 @@
</div>
{{else}}
<div class="field">
<label for="hmac" class="is-label">HMAC</label>
<div class="control">
<IvyCodemirror @id="hmac" @value={{hmac}} @valueUpdated={{action (mut hmac)}} @options={{hash
lineNumbers=true
tabSize=2
mode='ruby'
theme='hashi'
}} />
<JsonEditor
@title="HMAC"
@value={{hmac}}
@valueUpdated={{action (mut hmac)}}
@options={{hash
mode='ruby'
}}
/>
</div>
</div>
<div class="field">

View File

@ -17,6 +17,7 @@
(eq attr.type "boolean")
)
}}
{{#unless (eq attr.type "object")}}
<label for="{{attr.name}}" class="is-label">
{{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
{{#if attr.options.helpText}}
@ -24,6 +25,7 @@
{{/if}}
</label>
{{/unless}}
{{/unless}}
{{#if attr.options.possibleValues}}
<div class="control is-expanded">
<div class="select is-fullwidth">
@ -73,6 +75,10 @@
</label>
</div>
{{else if (eq attr.type "object")}}
<JsonEditor @value={{if (get model attr.name) (stringify (get model attr.name)) emptyData}} @valueUpdated={{action "codemirrorUpdated" attr.name}} />
<JsonEditor
@title={{capitalize (or attr.options.label (humanize (dasherize attr.name)))}}
@helpText={{attr.options.helpText}}
@value={{if (get model attr.name) (stringify (get model attr.name)) emptyData}}
@valueUpdated={{action "codemirrorUpdated" attr.name}} />
{{/if}}
</div>

View File

@ -29,9 +29,15 @@
</EmptyState>
{{else}}
{{#if showAdvancedMode}}
<JsonEditor @value={{modelForData.dataAsJSONString}} @options={{hash
readOnly=true
}} />
<div class="has-top-margin-s">
<JsonEditor
@title={{if isV2 "Version Data" "Secret Data"}}
@value={{modelForData.dataAsJSONString}}
@options={{hash
readOnly=true
}}
/>
</div>
{{else}}
<div class="table info-table-row-header">
<div class="info-table-row thead">

View File

@ -21,7 +21,7 @@
{{#if (eq unwrapActiveTab "data")}}
<div class="field">
<div class="control">
<JsonEditor @value={{stringify unwrap_data}} @options={{hash
<JsonEditor @title="Unwrapped Data" @value={{stringify unwrap_data}} @options={{hash
readOnly=true
}} />
</div>

View File

@ -32,9 +32,11 @@
<NamespaceReminder @mode="perform" @noun={{selectedAction}} />
<MessageError @errors={{errors}} />
<div class="field">
<label for="data" class="is-label">Data to wrap <span class="is-size-9 is-lowercase has-text-grey">(json-formatted)</span></label>
<div class="control">
<JsonEditor @value={{data}} @valueUpdated={{action "codemirrorUpdated"}} />
<JsonEditor @title="Data to wrap"
@subTitle="json-formatted"
@value={{data}}
@valueUpdated={{action "codemirrorUpdated"}} />
</div>
</div>
<TtlPicker2

View File

@ -32,36 +32,32 @@
</div>
</div>
<div class="field">
<div class="level is-mobile">
<div class="level-left">
<label for="policy" class="is-label">Policy</label>
</div>
<div class="level-right">
<div class="field is-horizontal is-flex-end is-single-line">
<Toolbar>
<label class="is-label">Policy</label>
<ToolbarActions>
<div class="toolbar-separator"></div>
<div class="control is-flex">
<Input @id="fileUploadToggle" @type="checkbox" @name="fileUploadToggle" class="switch is-rounded is-success is-small" @checked={{showFileUpload}} @change={{toggle-action "showFileUpload" this}} data-test-policy-edit-toggle={{true}} />
<label for="fileUploadToggle">Upload file</label>
<Input @id="fileUploadToggle" @type="checkbox" @name="fileUploadToggle" class="switch is-rounded is-success is-small" @checked={{showFileUpload}} @change={{toggle-action "showFileUpload" this}} data-test-policy-edit-toggle={{true}} />
<label for="fileUploadToggle">Upload file</label>
</div>
</div>
</div>
</div>
</ToolbarActions>
</Toolbar>
{{#if showFileUpload}}
<TextFile @inputOnly={{true}} @file={{file}} @onChange={{action "setPolicyFromFile"}} />
{{else}}
<IvyCodemirror @value={{model.policy}} @id="policy" @valueUpdated={{action (mut model.policy)}} @options={{hash
lineNumbers=true
tabSize=2
<JsonEditor
@title="Policy"
@helpText="You can use Alt+Tab (Option+Tab on MacOS) in the code editor to skip to the next field"
@showToolbar={{false}}
@value={{model.policy}}
@valueUpdated={{action (mut model.policy)}}
@options={{hash
mode='ruby'
theme='hashi'
extraKeys=(hash
Shift-Enter=(action "savePolicy" model)
)
}} />
<div class="box is-shadowless is-fullwidth has-short-padding">
<p class="help-text has-text-grey-dark is-size-7">
You can use Alt+Tab (Option+Tab on MacOS) in the code editor to skip to the next field
</p>
</div>
}}>
</JsonEditor>
{{/if}}
</div>
{{#each model.additionalAttrs as |attr|}}

View File

@ -47,13 +47,13 @@
<div class="box is-bottomless is-fullwidth is-marginless">
<MessageError @model={{model}} />
<NamespaceReminder @mode="edit" @noun="policy" />
<label for="policy" class="is-label">Policy</label>
<div class="field">
<IvyCodemirror @value={{model.policy}} @valueUpdated={{action (mut model.policy)}} @options={{hash
lineNumbers=true
tabSize=2
<JsonEditor
@title="Policy"
@value={{model.policy}}
@valueUpdated={{action (mut model.policy)}}
@options={{hash
mode='ruby'
theme='hashi'
extraKeys=(hash
Shift-Enter=(action "savePolicy" model)
)

View File

@ -40,20 +40,13 @@
</Toolbar>
<div class="box is-bottomless is-fullwidth is-marginless">
<div class="field">
<label for="policy" class="is-label">
Policy
{{#if (eq policyType "acl")}}
<span class="tag is-white is-size-9 has-text-grey" data-test-acl-format>
({{uppercase model.format}} format)
</span>
{{/if}}
</label>
<IvyCodemirror @value={{model.policy}} @options={{hash
<JsonEditor
@title="Policy"
@subTitle={{if (eq policyType "acl") (concat uppercase model.format " format")}}
@value={{model.policy}}
@options={{hash
readOnly=true
lineNumbers=true
tabSize=2
mode='ruby'
theme='hashi'
}} />
</div>
{{#if model.paths}}

View File

@ -17,7 +17,8 @@
)
)
}}
<label for="{{attr.name}}" class="is-label">
{{#unless (eq attr.type "object")}}
<label for="{{attr.name}}" class="is-label">
{{labelString}}
{{#if attr.options.helpText}}
{{#info-tooltip}}
@ -27,9 +28,10 @@
{{/info-tooltip}}
{{/if}}
</label>
{{#if attr.options.subText}}
<p class="sub-text">{{attr.options.subText}} {{#if attr.options.docLink}}<a href="{{attr.options.docLink}}" target="_blank" rel="noopener noreferrer">See our documentation</a> for help.{{/if}}</p>
{{/if}}
{{#if attr.options.subText}}
<p class="sub-text">{{attr.options.subText}} {{#if attr.options.docLink}}<a href="{{attr.options.docLink}}" target="_blank" rel="noopener noreferrer">See our documentation</a> for help.{{/if}}</p>
{{/if}}
{{/unless}}
{{/unless}}
{{#if attr.options.possibleValues}}
<div class="control is-expanded">
@ -198,21 +200,12 @@
@spellcheck="false" />
{{else if (eq attr.options.editType "json")}}
{{!-- JSON Editor --}}
<label for="{{attr.name}}" class="is-label">
{{labelString}}
{{#if attr.options.helpText}}
{{#info-tooltip}}
<span data-test-help-text>
{{attr.options.helpText}}
</span>
{{/info-tooltip}}
{{/if}}
</label>
{{#if attr.options.subText}}
<p class="sub-text">{{attr.options.subText}} {{#if attr.options.docLink}}<a href="{{attr.options.docLink}}" target="_blank" rel="noopener noreferrer">See our documentation</a> for help.{{/if}}</p>
{{/if}}
<JsonEditor
data-test-input={{attr.name}}
@title={{labelString}}
@value={{if
(get model valuePath)
(stringify (jsonify (get model valuePath)))
@ -222,6 +215,7 @@
@options={{hash
theme=(or attr.options.theme 'hashi')
}}
@helpText={{attr.options.helpText}}
/>
{{else}}
{{!-- Regular Text Input --}}
@ -259,7 +253,9 @@
</div>
{{else if (eq attr.type "object")}}
{{json-editor
title=labelString
value=(if (get model valuePath) (stringify (get model valuePath)) emptyData)
valueUpdated=(action "codemirrorUpdated" attr.name false)
helpText=attr.options.helpText
}}
{{/if}}

View File

@ -0,0 +1,44 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { create } from 'ember-cli-page-object';
import { render } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
import jsonEditor from '../../pages/components/json-editor';
const component = create(jsonEditor);
module('Integration | Component | json-editor', function(hooks) {
setupRenderingTest(hooks);
const setup = async function(context, title, value, options, showToolbar = true) {
context.set('value', JSON.stringify(value));
context.set('options', options);
context.set('title', title);
context.set('showToolbar', showToolbar);
await render(hbs`{{json-editor title=title value=value options=options showToolbar=showToolbar}}`);
};
test('it renders', async function(assert) {
let value = '';
await setup(this, 'Test title', value, null);
assert.equal(component.title, 'Test title', 'renders the provided title');
assert.equal(component.hasToolbar, true, 'renders the toolbar');
assert.equal(component.hasJSONEditor, true, 'renders the ivy code mirror component');
assert.equal(component.canEdit, true, 'json editor is in read only mode');
});
test('it renders in read only mode', async function(assert) {
let value = '';
let options = {
readOnly: true,
};
await setup(this, 'Test title', value, options);
assert.equal(component.canEdit, false, 'editor is in read only mode');
});
test('it renders the editor without toolbar', async function(assert) {
let value = '';
await setup(this, 'Test title', value, null, false);
assert.equal(component.hasToolbar, false, 'toolbar is not rendered');
});
});

View File

@ -0,0 +1,8 @@
import { isPresent, isVisible, text } from 'ember-cli-page-object';
export default {
title: text('[data-test-component=json-editor-title]'),
hasToolbar: isPresent('[data-test-component=json-editor-toolbar]'),
hasJSONEditor: isPresent('[data-test-component=json-editor]'),
canEdit: isVisible('div.CodeMirror-gutters'),
};