Icu 6680 UI code editor for secrets (#1499)

* feat: 🎸 initial secret editor

* feat: 🎸 Create Secret Editor for JSON Credentials

✅ Closes: ICU-6680

* style: 💄 Fix sass lint errors

* style: 💄 Appease sass-lint rule for using --size in stylesheet

* refactor: 💡 Set Icon size via component attribute

* refactor: 💡 remove computed decorators for Secret Editor

* refactor: 💡 update editor styles to using sizing variables

* refactor: 💡 update styles and add comments

* refactor: 💡 clean up styling for secret editor

* refactor: 💡 update class names to be more verbose

* chore: 🤖 move jsonlint import to rose index.js

* refactor: 💡 remove duplicate margin style for skeleton body

* refactor: 💡 add comments to SecretEditor component class

* refactor: 💡 large updates to secret json editor

* refactor: 💡 Move JSON Secret to form/field/ directory

* style: 💄 fix padding for Secret Editor line numbers

* chore: 🤖 disable property-sort-oder in .sasslintrc

* refactor: 💡 reorder app.scss styles

* test: 💍 Add integration tests for JSON Secret and Skeleton
This commit is contained in:
Cameron Perera authored and GitHub committed 2022-12-02 13:58:04 -06:00
1 parent c3d9fd8fc5
commit c62eb751e4
12 files changed
+380 -97

No files matched your search

+1
View File
@@ -5,3 +5,4 @@ rules:
mixins-before-declarations: 0
nesting-depth: 0
placeholder-in-extend: 0
property-sort-order: 0
@@ -0,0 +1,31 @@
<div class='secret-editor'>
{{#if this.isDisabled}}
<Form::Field::JsonSecret::Skeleton>
<Rose::Icon @name='flight-icons/svg/eye-off-16' @size='medium' />
{{t 'form.secret-editor.details'}}
</Form::Field::JsonSecret::Skeleton>
{{else if this.isActionable}}
<Form::Field::JsonSecret::Skeleton>
<Rose::Button
@iconLeft='flight-icons/svg/pencil-tool-16'
@style='secret-editor-skeleton-button'
{{on 'click' this.enableEditing}}
>
{{t 'form.secret-editor.edit'}}
</Rose::Button>
</Form::Field::JsonSecret::Skeleton>
{{else}}
<Rose::CodeEditor as |c|>
<c.fieldEditor
class='secret-editor-json'
@onInput={{@onInput}}
@value={{@value}}
@options={{hash
mode='application/json'
gutters=(array 'CodeMirror-lint-markers')
lint='true'
}}
/>
</Rose::CodeEditor>
{{/if}}
</div>
@@ -0,0 +1,44 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
export default class FormFieldJsonSecretComponent extends Component {
// =properties
/**
* Tracks when editor should be displayed
* @type {boolean}
*/
@tracked editing = false;
/**
* True if viewing the resource after it has been created but not editing it
* and displays as a read only state. Since this is a secret editor the secret
* value is not displayed and a skeleton view is shown.
* @type {boolean}
*/
get isDisabled() {
return this.args.disabled;
}
/**
* True if editing the resource after it has been created but not editing
* the secret value in the Secret Editor. This will present the Secret
* Editor as a read only state even when the form is editing the resource.
* @type {boolean}
*/
get isActionable() {
return this.args.showEditButton && !this.editing;
}
// =actions
/**
* Sets editing to true when Secret Editor
* button is clicked in Actionable view
*/
@action
enableEditing() {
if (!this.args.disabled) this.editing = true;
}
}
@@ -0,0 +1,18 @@
<div class='secret-editor-skeleton-wrapper'>
<div class='secret-editor-skeleton-gutter'></div>
<div class='secret-editor-skeleton-body'>
<div class='xl'></div>
<div class='m'></div>
<div class='l'></div>
<div class='s'></div>
<div class='xl'></div>
<div class='l'></div>
<div class='m'></div>
<div class='xl'></div>
<div class='m'></div>
<div class='s'></div>
</div>
<div class='secret-editor-skeleton-message'>
{{yield}}
</div>
</div>
+110 -3
View File
@@ -1,3 +1,4 @@
// sass-lint:disable no-color-literals class-name-format
@use 'rose/variables/sizing';
@use 'rose/variables/media';
@use 'rose/utilities/type';
@@ -466,17 +467,17 @@
}
.hds-form-helper-text {
grid-area: helper-text;
font-size: sizing.rems(m);
color: var(--token-form-label-color);
font-size: sizing.rems(m);
grid-area: helper-text;
}
.hds-form-text-input {
background-color: transparent;
border: 0;
grid-area: text-input;
margin-top: sizing.rems(xs);
line-height: sizing.rems(m) + sizing.rems(xxs);
margin-top: sizing.rems(xs);
padding: 0;
}
}
@@ -493,3 +494,109 @@
}
}
}
// json secret editor
.secret-editor {
// Secret Editor body and gutter colors are based on the Monokai color template
// and are used for both the Editor and the Skeleton views
--button-shadow: #000004;
--secret-editor-body: #272822; // body and button background
--secret-editor-gutter: #31332b; // gutter background
--secret-editor-height: 210px;
background: var(--secret-editor-body);
border-radius: sizing.rems(xxs);
height: var(--secret-editor-height);
margin-bottom: sizing.rems(l);
.secret-editor-json {
.CodeMirror {
border-radius: sizing.rems(xxs);
height: var(--secret-editor-height);
}
.CodeMirror-gutters {
background: var(--secret-editor-gutter);
}
// override CodeMirror padding to add padding to left of line numbers
.CodeMirror-linenumber {
padding: 0;
}
}
.secret-editor-skeleton-wrapper {
// using ui-gray directly so it does not change when
// user is in dark mode since the secret editor
// does not change theme colors between light and dark
color: #{color(ui-gray)};
display: flex;
height: 100%;
position: relative;
}
.secret-editor-skeleton-gutter {
background: var(--secret-editor-gutter);
border-radius: sizing.rems(xxs) 0 0 sizing.rems(xxs);
width: sizing.rems(xxl) + sizing.rems(xxs); // 52
}
.secret-editor-skeleton-body {
width: 100%;
// skeleton text rows for detail and edit views
// applies general styles to child div of skeleton-body
// then adds dynamic width to each child
// depending on the class (e.g., s, m, l)
> div {
background: var(--secret-editor-gutter);
border-radius: sizing.rems(m);
height: sizing.rems(xs);
margin: sizing.rems(s) - sizing.rems(xxxxs); // 11
&:first-child {
margin-top: sizing.rems(m);
}
&.s {
width: 5%;
}
&.m {
width: 10%;
}
&.l {
width: 15%;
}
&.xl {
width: 20%;
}
}
}
.secret-editor-skeleton-message {
font-size: sizing.rems(s) + sizing.rems(xxxs); // 14
left: 50%;
position: absolute;
top: 50%;
// centers message with editor body and gutter
transform: translate(-50%, -50%);
z-index: 1;
}
.rose-button-secret-editor-skeleton-button {
background: var(--secret-editor-body);
border: sizing.rems(xxxxs) solid var(--ui-gray);
border-radius: sizing.rems(xxxs);
box-shadow: 0 sizing.rems(xxs) sizing.rems(xxs) var(--button-shadow);
// using ui-gray directly so it does not change when
// user is in dark mode since the secret editor
// does not change theme colors between light and dark
color: #{color(ui-gray, 050)};
font-size: sizing.rems(s) + sizing.rems(xxxs); // 14
height: sizing.rems(xl);
padding: 0 sizing.rems(l);
}
}
@@ -0,0 +1,20 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
module('Integration | Component | form/field/json-secret/skeleton', function (hooks) {
setupRenderingTest(hooks);
test('it renders', async function (assert) {
assert.expect(2);
await render(hbs`
<Form::Field::JsonSecret::Skeleton>
Skeleton Message
</Form::Field::JsonSecret::Skeleton>
`);
assert.dom('.secret-editor-skeleton-wrapper').isVisible();
assert.dom('.secret-editor-skeleton-message').hasText('Skeleton Message');
});
});
@@ -0,0 +1,82 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, click } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { setupIntl } from 'ember-intl/test-support';
module('Integration | Component | form/field/json-secret', function (hooks) {
setupRenderingTest(hooks);
setupIntl(hooks);
hooks.beforeEach(function () {
this.model = {
json_object: '{}',
};
});
test('it renders the active editor', async function (assert) {
assert.expect(3);
await render(hbs`
<Form::Field::JsonSecret
@value={{this.model.json_object}}
@onInput={{fn (mut this.model.json_object)}}
@showEditButton={{false}}
/>
`);
assert.dom('.secret-editor').isVisible();
assert.dom('.secret-editor-json').isVisible();
assert.dom('.secret-editor-json .CodeMirror-line').hasText(this.model.json_object);
});
test('it renders the disabled editor', async function (assert) {
assert.expect(3);
await render(hbs`
<Form::Field::JsonSecret
@disabled={{true}}
@value={{this.model.json_object}}
@onInput={{fn (mut this.model.json_object)}}
@showEditButton={{true}}
/>
`);
assert.dom('.secret-editor').isVisible();
assert.dom('.secret-editor-json').doesNotExist();
assert.dom('.secret-editor-skeleton-wrapper').hasText('This secret is saved but won\'t be displayed');
});
test('it renders the actionable editor', async function (assert) {
assert.expect(3);
await render(hbs`
<Form::Field::JsonSecret
@disabled={{false}}
@value={{this.model.json_object}}
@onInput={{fn (mut this.model.json_object)}}
@showEditButton={{true}}
/>
`);
assert.dom('.secret-editor').isVisible();
assert.dom('.secret-editor-json').doesNotExist();
assert.dom('.secret-editor-skeleton-wrapper').hasText('Click to replace this secret');
});
test('it renders the active editor from the actionable view', async function (assert) {
assert.expect(4);
await render(hbs`
<Form::Field::JsonSecret
@disabled={{false}}
@value={{this.model.json_object}}
@onInput={{fn (mut this.model.json_object)}}
@showEditButton={{true}}
/>
`);
assert.dom('.secret-editor').isVisible();
assert.dom('.secret-editor-json').doesNotExist();
assert.dom('.secret-editor-skeleton-wrapper').hasText('Click to replace this secret');
await click('button')
assert.dom('.secret-editor-json').isVisible();
});
});