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:
1 parent
c3d9fd8fc5
commit
c62eb751e4
12 files changed
+380
-97
No files matched your search
@@ -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>
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user