mirror of
https://github.com/kemko/nomad.git
synced 2026-01-05 09:55:44 +03:00
Edit Secure Variables as JSON (#13461)
* Toying with insert and update helpers before translation func * Working prototype that lets you switch between json and tabular * No longer add the bonus items row in json mode * Trimmed the ivy from the codemirror (#13503) * Trimmed the ivy from the codemirror * editedJSONItems removal * De-debugger * Replaced other instances of IvyCodeMirror throughout the app (#13528) * Replaced other instances of IvyCodeMirror throughout the app * PR requests for codemirror modifier * Screen reader setting as param * Trying a simpler codemirror test helper * Lint removal * Screen Reader Label added for a11y * JSONViewer cleanup * JSON editor added to /new and all variables stringified before save or translate * Give users a foothold when editing an empty item in JSON mode * Copy the empty KV * No duplicate keys in KV * Better handling of cursor snapping in json edit field * Catch formatting errors on the fly * Basic tests for JSON to Table and Table to JSON in form
This commit is contained in:
@@ -1,16 +1,8 @@
|
||||
import Component from '@ember/component';
|
||||
import { computed } from '@ember/object';
|
||||
import { classNames, classNameBindings } from '@ember-decorators/component';
|
||||
import classic from 'ember-classic-decorator';
|
||||
|
||||
@classic
|
||||
@classNames('json-viewer')
|
||||
@classNameBindings('fluidHeight:has-fluid-height')
|
||||
export default class JsonViewer extends Component {
|
||||
json = null;
|
||||
|
||||
@computed('json')
|
||||
get jsonStr() {
|
||||
return JSON.stringify(this.json, null, 2);
|
||||
}
|
||||
}
|
||||
export default class JsonViewer extends Component {}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<form class="new-secure-variables" autocomplete="off" {{on "submit" this.save}}
|
||||
{{did-insert this.appendItemIfEditing}}
|
||||
>
|
||||
{{!-- TODO: {{if this.parseError 'is-danger'}} on inputs --}}
|
||||
{{did-update this.onViewChange @view}}
|
||||
{{did-insert this.establishKeyValues}}
|
||||
<form class="new-secure-variables" autocomplete="off" {{on "submit" this.save}}>
|
||||
<div>
|
||||
<label>
|
||||
<span>
|
||||
@@ -34,46 +33,64 @@
|
||||
</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{#each this.keyValues as |entry iter|}}
|
||||
<div class="key-value">
|
||||
<label>
|
||||
<span>
|
||||
Key
|
||||
</span>
|
||||
<Input
|
||||
@type="text"
|
||||
@value={{entry.key}}
|
||||
class="input"
|
||||
{{autofocus ignore=(eq iter 0)}}
|
||||
{{on "input" (fn this.validateKey entry)}}
|
||||
/>
|
||||
</label>
|
||||
<SecureVariableForm::InputGroup @entry={{entry}} />
|
||||
{{#if (eq entry this.keyValues.lastObject)}}
|
||||
<button
|
||||
class="add-more button is-info is-inverted"
|
||||
type="button"
|
||||
disabled={{not (and entry.key entry.value)}}
|
||||
{{on "click" this.appendRow}}
|
||||
>
|
||||
Add More
|
||||
</button>
|
||||
{{else}}
|
||||
<button
|
||||
class="delete-row button is-danger is-inverted"
|
||||
type="button"
|
||||
{{on "click" (action this.deleteRow entry)}}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
{{#if (eq this.view "json")}}
|
||||
<div class="editor-wrapper boxed-section-body is-full-bleed {{if this.JSONError "error"}}">
|
||||
<div
|
||||
data-test-json-editor
|
||||
{{code-mirror
|
||||
content=this.JSONItems
|
||||
onUpdate=this.updateCode
|
||||
extraKeys=(hash Cmd-Enter=(action "save"))
|
||||
}}
|
||||
/>
|
||||
{{#if this.JSONError}}
|
||||
<p class="help is-danger">
|
||||
{{this.JSONError}}
|
||||
</p>
|
||||
{{/if}}
|
||||
{{#each-in entry.warnings as |k v|}}
|
||||
<span class="key-value-error help is-danger">
|
||||
{{v}}
|
||||
</span>
|
||||
{{/each-in}}
|
||||
</div>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
{{#each this.keyValues as |entry iter|}}
|
||||
<div class="key-value">
|
||||
<label>
|
||||
<span>
|
||||
Key
|
||||
</span>
|
||||
<Input
|
||||
@type="text"
|
||||
@value={{entry.key}}
|
||||
class="input"
|
||||
{{autofocus ignore=(eq iter 0)}}
|
||||
{{on "input" (fn this.validateKey entry)}}
|
||||
/>
|
||||
</label>
|
||||
<SecureVariableForm::InputGroup @entry={{entry}} />
|
||||
{{#if (eq entry this.keyValues.lastObject)}}
|
||||
<button
|
||||
class="add-more button is-info is-inverted"
|
||||
type="button"
|
||||
disabled={{not (and entry.key entry.value)}}
|
||||
{{on "click" this.appendRow}}
|
||||
>
|
||||
Add More
|
||||
</button>
|
||||
{{else}}
|
||||
<button
|
||||
class="delete-row button is-danger is-inverted"
|
||||
type="button"
|
||||
{{on "click" (action this.deleteRow entry)}}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
{{/if}}
|
||||
{{#each-in entry.warnings as |k v|}}
|
||||
<span class="key-value-error help is-danger">
|
||||
{{v}}
|
||||
</span>
|
||||
{{/each-in}}
|
||||
</div>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
<footer>
|
||||
<button
|
||||
disabled={{this.shouldDisableSave}}
|
||||
|
||||
@@ -6,7 +6,17 @@ import { tracked } from '@glimmer/tracking';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { trimPath } from '../helpers/trim-path';
|
||||
import { copy } from 'ember-copy';
|
||||
import EmberObject from '@ember/object';
|
||||
import EmberObject, { set } from '@ember/object';
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import MutableArray from '@ember/array/mutable';
|
||||
import { A } from '@ember/array';
|
||||
import { stringifyObject } from 'nomad-ui/helpers/stringify-object';
|
||||
|
||||
const EMPTY_KV = {
|
||||
key: '',
|
||||
value: '',
|
||||
warnings: EmberObject.create(),
|
||||
};
|
||||
|
||||
export default class SecureVariableFormComponent extends Component {
|
||||
@service router;
|
||||
@@ -23,16 +33,45 @@ export default class SecureVariableFormComponent extends Component {
|
||||
@tracked duplicatePathWarning = null;
|
||||
|
||||
get shouldDisableSave() {
|
||||
return !this.args.model?.path;
|
||||
return !!this.JSONError || !this.args.model?.path;
|
||||
}
|
||||
|
||||
@tracked keyValues = copy(this.args.model?.keyValues || [])?.map((kv) => {
|
||||
return {
|
||||
key: kv.key,
|
||||
value: kv.value,
|
||||
warnings: EmberObject.create(),
|
||||
};
|
||||
});
|
||||
/**
|
||||
* @type {MutableArray<{key: string, value: string, warnings: EmberObject}>}
|
||||
*/
|
||||
keyValues = A([]);
|
||||
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
JSONItems = '{}';
|
||||
|
||||
@action
|
||||
establishKeyValues() {
|
||||
const keyValues = copy(this.args.model?.keyValues || [])?.map((kv) => {
|
||||
return {
|
||||
key: kv.key,
|
||||
value: kv.value,
|
||||
warnings: EmberObject.create(),
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Appends a row to the end of the Items list if you're editing an existing variable.
|
||||
* This will allow it to auto-focus and make all other rows deletable
|
||||
*/
|
||||
if (!this.args.model?.isNew) {
|
||||
keyValues.pushObject(copy(EMPTY_KV));
|
||||
}
|
||||
this.keyValues = keyValues;
|
||||
|
||||
this.JSONItems = stringifyObject([
|
||||
this.keyValues.reduce((acc, { key, value }) => {
|
||||
acc[key] = value;
|
||||
return acc;
|
||||
}, {}),
|
||||
]);
|
||||
}
|
||||
|
||||
@action
|
||||
validatePath(e) {
|
||||
@@ -53,20 +92,26 @@ export default class SecureVariableFormComponent extends Component {
|
||||
@action
|
||||
validateKey(entry, e) {
|
||||
const value = e.target.value;
|
||||
// No dots in key names
|
||||
if (value.includes('.')) {
|
||||
entry.warnings.set('dottedKeyError', 'Key should not contain a period.');
|
||||
} else {
|
||||
delete entry.warnings.dottedKeyError;
|
||||
entry.warnings.notifyPropertyChange('dottedKeyError');
|
||||
}
|
||||
|
||||
// no duplicate keys
|
||||
const existingKeys = this.keyValues.map((kv) => kv.key);
|
||||
if (existingKeys.includes(value)) {
|
||||
entry.warnings.set('duplicateKeyError', 'Key already exists.');
|
||||
} else {
|
||||
delete entry.warnings.duplicateKeyError;
|
||||
entry.warnings.notifyPropertyChange('duplicateKeyError');
|
||||
}
|
||||
}
|
||||
|
||||
@action appendRow() {
|
||||
this.keyValues.pushObject({
|
||||
key: '',
|
||||
value: '',
|
||||
warnings: EmberObject.create(),
|
||||
});
|
||||
this.keyValues.pushObject(copy(EMPTY_KV));
|
||||
}
|
||||
|
||||
@action deleteRow(row) {
|
||||
@@ -75,10 +120,16 @@ export default class SecureVariableFormComponent extends Component {
|
||||
|
||||
@action
|
||||
async save(e) {
|
||||
e.preventDefault();
|
||||
if (e.type === 'submit') {
|
||||
e.preventDefault();
|
||||
}
|
||||
// TODO: temp, hacky way to force translation to tabular keyValues
|
||||
if (this.view === 'json') {
|
||||
this.translateAndValidateItems('table');
|
||||
}
|
||||
try {
|
||||
const nonEmptyItems = this.keyValues.filter(
|
||||
(item) => item.key.trim() && item.value
|
||||
const nonEmptyItems = A(
|
||||
this.keyValues.filter((item) => item.key.trim() && item.value)
|
||||
);
|
||||
if (!nonEmptyItems.length) {
|
||||
throw new Error('Please provide at least one key/value pair.');
|
||||
@@ -96,7 +147,6 @@ export default class SecureVariableFormComponent extends Component {
|
||||
type: 'success',
|
||||
destroyOnClick: false,
|
||||
timeout: 5000,
|
||||
showProgress: true,
|
||||
});
|
||||
this.router.transitionTo('variables.variable', this.args.model.path);
|
||||
} catch (error) {
|
||||
@@ -110,13 +160,93 @@ export default class SecureVariableFormComponent extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a row to the end of the Items list if you're editing an existing variable.
|
||||
* This will allow it to auto-focus and make all other rows deletable
|
||||
*/
|
||||
@action appendItemIfEditing() {
|
||||
if (!this.args.model?.isNew) {
|
||||
this.appendRow();
|
||||
//#region JSON Editing
|
||||
|
||||
view = this.args.view;
|
||||
// Prevent duplicate onUpdate events when @view is set to its already-existing value,
|
||||
// which happens because parent's queryParams and toggle button both resolve independently.
|
||||
@action onViewChange([view]) {
|
||||
if (view !== this.view) {
|
||||
set(this, 'view', view);
|
||||
this.translateAndValidateItems(view);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
translateAndValidateItems(view) {
|
||||
// TODO: move the translation functions in serializers/variable.js to generic importable functions.
|
||||
if (view === 'json') {
|
||||
// Translate table to JSON
|
||||
set(
|
||||
this,
|
||||
'JSONItems',
|
||||
stringifyObject([
|
||||
this.keyValues
|
||||
.filter((item) => item.key.trim() && item.value) // remove empty items when translating to JSON
|
||||
.reduce((acc, { key, value }) => {
|
||||
acc[key] = value;
|
||||
return acc;
|
||||
}, {}),
|
||||
])
|
||||
);
|
||||
|
||||
// Give the user a foothold if they're transitioning an empty K/V form into JSON
|
||||
if (!Object.keys(this.JSONItems).length) {
|
||||
set(this, 'JSONItems', stringifyObject([{ '': '' }]));
|
||||
}
|
||||
} else if (view === 'table') {
|
||||
// Translate JSON to table
|
||||
set(
|
||||
this,
|
||||
'keyValues',
|
||||
A(
|
||||
Object.entries(JSON.parse(this.JSONItems)).map(([key, value]) => {
|
||||
return {
|
||||
key,
|
||||
value: typeof value === 'string' ? value : JSON.stringify(value),
|
||||
warnings: EmberObject.create(),
|
||||
};
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Reset any error state, since the errorring json will not persist
|
||||
set(this, 'JSONError', null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
@tracked JSONError = null;
|
||||
/**
|
||||
*
|
||||
* @param {string} value
|
||||
*/
|
||||
@action updateCode(value, codemirror) {
|
||||
codemirror.performLint();
|
||||
try {
|
||||
const hasLintErrors = codemirror?.state.lint.marked?.length > 0;
|
||||
if (hasLintErrors || !JSON.parse(value)) {
|
||||
throw new Error('Invalid JSON');
|
||||
}
|
||||
|
||||
// "myString" is valid JSON, but it's not a valid Secure Variable.
|
||||
// Ditto for an array of objects. We expect a single object to be a Secure Variable.
|
||||
const hasFormatErrors =
|
||||
JSON.parse(value) instanceof Array ||
|
||||
typeof JSON.parse(value) !== 'object';
|
||||
if (hasFormatErrors) {
|
||||
throw new Error(
|
||||
'A Secure Variable must be formatted as a single JSON object'
|
||||
);
|
||||
}
|
||||
|
||||
set(this, 'JSONError', null);
|
||||
set(this, 'JSONItems', value);
|
||||
} catch (error) {
|
||||
set(this, 'JSONError', error);
|
||||
}
|
||||
}
|
||||
//#endregion JSON Editing
|
||||
}
|
||||
|
||||
@@ -1,9 +1,29 @@
|
||||
// @ts-check
|
||||
|
||||
import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
export default class VariablesNewController extends Controller {
|
||||
@service store;
|
||||
queryParams = ['path'];
|
||||
queryParams = ['path', 'view'];
|
||||
get existingVariables() {
|
||||
return this.store.peekAll('variable');
|
||||
}
|
||||
|
||||
//#region Code View
|
||||
/**
|
||||
* @type {"table" | "json"}
|
||||
*/
|
||||
@tracked
|
||||
view = 'table';
|
||||
|
||||
toggleView() {
|
||||
if (this.view === 'table') {
|
||||
this.view = 'json';
|
||||
} else {
|
||||
this.view = 'table';
|
||||
}
|
||||
}
|
||||
//#endregion Code View
|
||||
}
|
||||
|
||||
@@ -1,9 +1,30 @@
|
||||
// @ts-check
|
||||
|
||||
import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
export default class VariablesVariableEditController extends Controller {
|
||||
@service store;
|
||||
queryParams = ['path'];
|
||||
queryParams = ['path', 'view'];
|
||||
get existingVariables() {
|
||||
return this.store.peekAll('variable');
|
||||
}
|
||||
|
||||
//#region Code View
|
||||
/**
|
||||
* @type {"table" | "json"}
|
||||
*/
|
||||
@tracked
|
||||
view = 'table';
|
||||
|
||||
toggleView() {
|
||||
if (this.view === 'table') {
|
||||
this.view = 'json';
|
||||
} else {
|
||||
this.view = 'table';
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion Code View
|
||||
}
|
||||
|
||||
57
ui/app/modifiers/code-mirror.js
Normal file
57
ui/app/modifiers/code-mirror.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import { action } from '@ember/object';
|
||||
import { bind } from '@ember/runloop';
|
||||
import codemirror from 'codemirror';
|
||||
import Modifier from 'ember-modifier';
|
||||
|
||||
import 'codemirror/addon/edit/matchbrackets';
|
||||
import 'codemirror/addon/selection/active-line';
|
||||
import 'codemirror/addon/lint/lint.js';
|
||||
import 'codemirror/addon/lint/json-lint.js';
|
||||
import 'codemirror/mode/javascript/javascript';
|
||||
|
||||
export default class CodeMirrorModifier extends Modifier {
|
||||
didInstall() {
|
||||
this._setup();
|
||||
}
|
||||
|
||||
didUpdateArguments() {
|
||||
this._editor.setOption('readOnly', this.args.named.readOnly);
|
||||
if (!this.args.named.content) {
|
||||
return;
|
||||
}
|
||||
if (this._editor.getValue() !== this.args.named.content) {
|
||||
this._editor.setValue(this.args.named.content);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
_onChange(editor) {
|
||||
this.args.named.onUpdate(editor.getValue(), this._editor);
|
||||
}
|
||||
|
||||
_setup() {
|
||||
if (this.element) {
|
||||
const editor = codemirror(this.element, {
|
||||
gutters: this.args.named.gutters || ['CodeMirror-lint-markers'],
|
||||
matchBrackets: true,
|
||||
lint: { lintOnChange: true },
|
||||
showCursorWhenSelecting: true,
|
||||
styleActiveLine: true,
|
||||
tabSize: 2,
|
||||
// all values we can pass into the modifier
|
||||
extraKeys: this.args.named.extraKeys || '',
|
||||
lineNumbers: this.args.named.lineNumbers || true,
|
||||
mode: this.args.named.mode || 'application/json',
|
||||
readOnly: this.args.named.readOnly || false,
|
||||
theme: this.args.named.theme || 'hashi',
|
||||
value: this.args.named.content || '',
|
||||
viewportMargin: this.args.named.viewportMargin || '',
|
||||
screenReaderLabel: this.args.named.screenReaderLabel || '',
|
||||
});
|
||||
|
||||
editor.on('change', bind(this, this._onChange));
|
||||
|
||||
this._editor = editor;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,16 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import withForbiddenState from 'nomad-ui/mixins/with-forbidden-state';
|
||||
import WithModelErrorHandling from 'nomad-ui/mixins/with-model-error-handling';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default class VariablesVariableRoute extends Route.extend(
|
||||
withForbiddenState,
|
||||
WithModelErrorHandling
|
||||
) {
|
||||
@service store;
|
||||
model(params) {
|
||||
return this.store.findRecord('variable', decodeURIComponent(params.path));
|
||||
return this.store.findRecord('variable', decodeURIComponent(params.path), {
|
||||
reload: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +78,18 @@
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.editor-wrapper {
|
||||
&.error {
|
||||
.CodeMirror {
|
||||
box-shadow: 0 0 0 3px $red;
|
||||
}
|
||||
.help {
|
||||
padding: 1rem 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
table.path-tree {
|
||||
|
||||
@@ -48,17 +48,15 @@
|
||||
</div>
|
||||
{{#if this.hasPayload}}
|
||||
<div class="boxed-section-body is-full-bleed">
|
||||
<IvyCodemirror
|
||||
<div
|
||||
data-test-payload-editor
|
||||
aria-label="Payload definition"
|
||||
@valueUpdated={{action (mut this.payload)}}
|
||||
@options={{hash
|
||||
mode="javascript"
|
||||
{{code-mirror
|
||||
theme="hashi"
|
||||
screenReaderLabel="Payload definition editor"
|
||||
tabSize=2
|
||||
lineNumbers=true
|
||||
}} />
|
||||
onUpdate=(action (mut this.payload))
|
||||
mode="javascript"
|
||||
screenReaderLabel="Payload definition"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="boxed-section-body">
|
||||
|
||||
@@ -51,17 +51,14 @@
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="boxed-section-body is-full-bleed">
|
||||
<IvyCodemirror
|
||||
<div
|
||||
data-test-editor
|
||||
aria-label="Job definition"
|
||||
@value={{or this.job._newDefinition this.jobSpec}}
|
||||
@valueUpdated={{this.updateCode}}
|
||||
@options={{hash
|
||||
mode="javascript"
|
||||
{{code-mirror
|
||||
screenReaderLabel="Job definition"
|
||||
content=this.job._newDefinition
|
||||
theme="hashi"
|
||||
screenReaderLabel="Job definition editor"
|
||||
tabSize=2
|
||||
lineNumbers=true
|
||||
onUpdate=this.updateCode
|
||||
mode="javascript"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
<IvyCodemirror
|
||||
<div
|
||||
data-test-json-viewer
|
||||
@value={{this.jsonStr}}
|
||||
@options={{hash
|
||||
mode="javascript"
|
||||
{{code-mirror
|
||||
content=(stringify-object @json)
|
||||
theme="hashi-read-only"
|
||||
screenReaderLabel="Job definition"
|
||||
tabSize=2
|
||||
lineNumbers=true
|
||||
readOnly=true
|
||||
}} />
|
||||
screenReaderLabel="JSON Viewer"
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
{{page-title "New Secure Variable"}}
|
||||
<Breadcrumb @crumb={{hash label="New" args=(array "variables.new")}} />
|
||||
|
||||
<section class="section">
|
||||
<h1 class="title variable-title">
|
||||
Create a Secure Variable
|
||||
<Toggle
|
||||
data-test-memory-toggle
|
||||
@isActive={{eq this.view "json"}}
|
||||
@onToggle={{action this.toggleView}}
|
||||
title="JSON"
|
||||
>JSON</Toggle>
|
||||
</h1>
|
||||
|
||||
<SecureVariableForm
|
||||
@model={{this.model}}
|
||||
@path={{this.path}}
|
||||
@existingVariables={{this.existingVariables}}
|
||||
@view={{this.view}}
|
||||
/>
|
||||
</section>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{{page-title "Edit Secure Variable"}}
|
||||
|
||||
<h1 class="title">
|
||||
<h1 class="title variable-title">
|
||||
<LinkTo class="back-link" @route="variables.variable.index">
|
||||
<FlightIcon
|
||||
@name="chevron-left"
|
||||
@@ -10,6 +10,17 @@
|
||||
</LinkTo>
|
||||
Edit
|
||||
{{this.model.path}}
|
||||
<Toggle
|
||||
data-test-memory-toggle
|
||||
@isActive={{eq this.view "json"}}
|
||||
@onToggle={{action this.toggleView}}
|
||||
title="JSON"
|
||||
>JSON</Toggle>
|
||||
|
||||
</h1>
|
||||
|
||||
<SecureVariableForm @model={{this.model}} @existingVariables={{this.existingVariables}} />
|
||||
<SecureVariableForm
|
||||
@model={{this.model}}
|
||||
@existingVariables={{this.existingVariables}}
|
||||
@view={{this.view}}
|
||||
/>
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
class="button is-info is-inverted is-small"
|
||||
@model={{this.model}}
|
||||
@route="variables.variable.edit"
|
||||
@query={{hash view=this.view}}
|
||||
>
|
||||
Edit
|
||||
</LinkTo>
|
||||
|
||||
@@ -47,6 +47,9 @@ module.exports = function (defaults) {
|
||||
// along with the exports of each module as its value.
|
||||
|
||||
app.import('node_modules/xterm/css/xterm.css');
|
||||
app.import('node_modules/jsonlint/lib/jsonlint.js');
|
||||
app.import('node_modules/codemirror/addon/lint/lint.css');
|
||||
app.import('node_modules/codemirror/lib/codemirror.css');
|
||||
|
||||
return app.toTree();
|
||||
};
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
"base64-js": "^1.3.1",
|
||||
"broccoli-asset-rev": "^3.0.0",
|
||||
"bulma": "0.9.3",
|
||||
"codemirror": "^5.58.2",
|
||||
"core-js": "3.19.1",
|
||||
"d3-array": "^3.1.1",
|
||||
"d3-axis": "^3.0.0",
|
||||
@@ -128,7 +129,7 @@
|
||||
"http-proxy": "^1.1.6",
|
||||
"husky": "^4.2.5",
|
||||
"is-ip": "^3.1.0",
|
||||
"ivy-codemirror": "IvyApp/ivy-codemirror#c3b7f49f8e6492878619f8055695581240cce21a",
|
||||
"jsonlint": "^1.6.3",
|
||||
"lint-staged": "^11.2.6",
|
||||
"loader.js": "^4.7.0",
|
||||
"lodash.intersection": "^4.4.0",
|
||||
@@ -174,7 +175,6 @@
|
||||
"@hashicorp/ember-flight-icons": "^2.0.5",
|
||||
"@percy/cli": "^1.1.0",
|
||||
"@percy/ember": "^3.0.0",
|
||||
"codemirror": "^5.56.0",
|
||||
"curved-arrows": "^0.1.0",
|
||||
"d3": "^7.3.0",
|
||||
"lru_map": "^0.4.1",
|
||||
@@ -182,7 +182,6 @@
|
||||
"title-case": "^3.0.3"
|
||||
},
|
||||
"resolutions": {
|
||||
"ivy-codemirror/codemirror": "^5.56.0",
|
||||
"ember-auto-import": "^2.4.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,6 @@
|
||||
const invariant = (truthy, error) => {
|
||||
if (!truthy) throw new Error(error);
|
||||
};
|
||||
|
||||
export function getCodeMirrorInstance(container) {
|
||||
return function (selector) {
|
||||
const cmService = container.lookup('service:code-mirror');
|
||||
|
||||
const element = document.querySelector(selector);
|
||||
invariant(element, `Selector ${selector} matched no elements`);
|
||||
|
||||
const cm = cmService.instanceFor(element.id);
|
||||
invariant(cm, `No registered CodeMirror instance for ${selector}`);
|
||||
|
||||
return cm;
|
||||
export function getCodeMirrorInstance() {
|
||||
return function () {
|
||||
return document.querySelector('.CodeMirror').CodeMirror;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,15 @@ import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'ember-qunit';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { componentA11yAudit } from 'nomad-ui/tests/helpers/a11y-audit';
|
||||
import { click, typeIn, findAll, render } from '@ember/test-helpers';
|
||||
import { click, typeIn, find, findAll, render } from '@ember/test-helpers';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import setupCodeMirror from 'nomad-ui/tests/helpers/codemirror';
|
||||
import { codeFillable, code } from 'nomad-ui/tests/pages/helpers/codemirror';
|
||||
|
||||
module('Integration | Component | secure-variable-form', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupMirage(hooks);
|
||||
setupCodeMirror(hooks);
|
||||
|
||||
test('passes an accessibility audit', async function (assert) {
|
||||
assert.expect(1);
|
||||
@@ -244,12 +247,150 @@ module('Integration | Component | secure-variable-form', function (hooks) {
|
||||
);
|
||||
|
||||
await render(hbs`<SecureVariableForm @model={{this.mockedModel}} />`);
|
||||
await typeIn('.key-value label:nth-child(1) input', 'foo');
|
||||
|
||||
await typeIn('.key-value label:nth-child(1) input', 'superSecret');
|
||||
assert.dom('.key-value-error').doesNotExist();
|
||||
|
||||
find('.key-value:nth-child(2) label:nth-child(1) input').value = '';
|
||||
|
||||
await typeIn('.key-value label:nth-child(1) input', 'super.secret');
|
||||
assert.dom('.key-value-error').exists();
|
||||
});
|
||||
|
||||
test('warns you when you create a duplicate key', async function (assert) {
|
||||
this.set(
|
||||
'mockedModel',
|
||||
server.create('variable', {
|
||||
keyValues: [{ key: 'myKey', value: 'myVal' }],
|
||||
})
|
||||
);
|
||||
|
||||
await render(hbs`<SecureVariableForm @model={{this.mockedModel}} />`);
|
||||
|
||||
await click('.key-value button.add-more');
|
||||
|
||||
await typeIn(
|
||||
'.key-value:nth-child(3) label:nth-child(1) input',
|
||||
'myWonderfulKey'
|
||||
);
|
||||
assert.dom('.key-value-error').doesNotExist();
|
||||
|
||||
find('.key-value:nth-child(3) label:nth-child(1) input').value = '';
|
||||
|
||||
await typeIn('.key-value:nth-child(3) label:nth-child(1) input', 'myKey');
|
||||
assert.dom('.key-value-error').exists();
|
||||
});
|
||||
});
|
||||
|
||||
module('Views', function () {
|
||||
test('Allows you to swap between JSON and Key/Value Views', async function (assert) {
|
||||
this.set(
|
||||
'mockedModel',
|
||||
server.create('variable', {
|
||||
path: '',
|
||||
keyValues: [{ key: '', value: '' }],
|
||||
})
|
||||
);
|
||||
|
||||
this.set(
|
||||
'existingVariables',
|
||||
server.createList('variable', 1, {
|
||||
path: 'baz/bat',
|
||||
})
|
||||
);
|
||||
|
||||
this.set('view', 'table');
|
||||
|
||||
await render(
|
||||
hbs`<SecureVariableForm @model={{this.mockedModel}} @existingVariables={{this.existingVariables}} @view={{this.view}} />`
|
||||
);
|
||||
assert.dom('.key-value').exists();
|
||||
assert.dom('.CodeMirror').doesNotExist();
|
||||
|
||||
this.set('view', 'json');
|
||||
assert.dom('.key-value').doesNotExist();
|
||||
assert.dom('.CodeMirror').exists();
|
||||
});
|
||||
|
||||
test('Persists Key/Values table data to JSON', async function (assert) {
|
||||
const keyValues = [
|
||||
{ key: 'foo', value: '123' },
|
||||
{ key: 'bar', value: '456' },
|
||||
];
|
||||
this.set(
|
||||
'mockedModel',
|
||||
server.create('variable', {
|
||||
path: '',
|
||||
keyValues,
|
||||
})
|
||||
);
|
||||
|
||||
this.set('view', 'json');
|
||||
|
||||
await render(
|
||||
hbs`<SecureVariableForm @model={{this.mockedModel}} @view={{this.view}} />`
|
||||
);
|
||||
|
||||
const keyValuesAsJSON = keyValues.reduce((acc, { key, value }) => {
|
||||
acc[key] = value;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
assert.equal(
|
||||
code('.editor-wrapper').get(),
|
||||
JSON.stringify(keyValuesAsJSON, null, 2),
|
||||
'JSON editor contains the key values, stringified, by default'
|
||||
);
|
||||
|
||||
this.set('view', 'table');
|
||||
|
||||
await click('.key-value button.add-more');
|
||||
|
||||
await typeIn('.key-value:last-of-type label:nth-child(1) input', 'howdy');
|
||||
await typeIn(
|
||||
'.key-value:last-of-type label:nth-child(2) input',
|
||||
'partner'
|
||||
);
|
||||
|
||||
this.set('view', 'json');
|
||||
|
||||
assert.ok(
|
||||
code('[data-test-json-editor]').get().includes('"howdy": "partner"'),
|
||||
'JSON editor contains the new key value'
|
||||
);
|
||||
});
|
||||
|
||||
test('Persists JSON data to Key/Values table', async function (assert) {
|
||||
const keyValues = [{ key: '', value: '' }];
|
||||
this.set(
|
||||
'mockedModel',
|
||||
server.create('variable', {
|
||||
path: '',
|
||||
keyValues,
|
||||
})
|
||||
);
|
||||
|
||||
this.set('view', 'json');
|
||||
|
||||
await render(
|
||||
hbs`<SecureVariableForm @model={{this.mockedModel}} @view={{this.view}} />`
|
||||
);
|
||||
|
||||
codeFillable('[data-test-json-editor]').get()(
|
||||
JSON.stringify({ golden: 'gate' }, null, 2)
|
||||
);
|
||||
this.set('view', 'table');
|
||||
assert.equal(
|
||||
find(`.key-value:last-of-type label:nth-child(1) input`).value,
|
||||
'golden',
|
||||
'Key persists from JSON to Table'
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find(`.key-value:last-of-type label:nth-child(2) input`).value,
|
||||
'gate',
|
||||
'Value persists from JSON to Table'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
114
ui/yarn.lock
114
ui/yarn.lock
@@ -4704,6 +4704,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d"
|
||||
integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==
|
||||
|
||||
JSV@^4.0.x:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/JSV/-/JSV-4.0.2.tgz#d077f6825571f82132f9dffaed587b4029feff57"
|
||||
integrity sha512-ZJ6wx9xaKJ3yFUhq5/sk82PJMuUyLk277I8mQeyDgCTjGdjWJIvPfaU5LIXaMuaN2UO1X3kZH4+lgphublZUHw==
|
||||
|
||||
abab@^2.0.3:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.5.tgz#c0b678fb32d60fc1219c784d6a826fe385aeb79a"
|
||||
@@ -4982,6 +4987,11 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0:
|
||||
dependencies:
|
||||
color-convert "^2.0.1"
|
||||
|
||||
ansi-styles@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.0.0.tgz#cb102df1c56f5123eab8b67cd7b98027a0279178"
|
||||
integrity sha512-3iF4FIKdxaVYT3JqQuY3Wat/T2t7TRbbQ94Fu50ZUCbLy4TFbTzr90NOHQodQkNqmeEGCw8WbeP78WNi6SKYUA==
|
||||
|
||||
ansi-to-html@^0.6.11:
|
||||
version "0.6.14"
|
||||
resolved "https://registry.yarnpkg.com/ansi-to-html/-/ansi-to-html-0.6.14.tgz#65fe6d08bba5dd9db33f44a20aec331e0010dad8"
|
||||
@@ -6585,7 +6595,7 @@ broccoli-funnel@2.0.1:
|
||||
symlink-or-copy "^1.0.0"
|
||||
walk-sync "^0.3.1"
|
||||
|
||||
broccoli-funnel@^1.0.1, broccoli-funnel@^1.1.0:
|
||||
broccoli-funnel@^1.1.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/broccoli-funnel/-/broccoli-funnel-1.2.0.tgz#cddc3afc5ff1685a8023488fff74ce6fb5a51296"
|
||||
integrity sha1-zdw6/F/xaFqAI0iP/3TOb7WlEpY=
|
||||
@@ -6669,20 +6679,6 @@ broccoli-kitchen-sink-helpers@^0.3.1:
|
||||
glob "^5.0.10"
|
||||
mkdirp "^0.5.1"
|
||||
|
||||
broccoli-merge-trees@^1.1.1:
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/broccoli-merge-trees/-/broccoli-merge-trees-1.2.4.tgz#a001519bb5067f06589d91afa2942445a2d0fdb5"
|
||||
integrity sha1-oAFRm7UGfwZYnZGvopQkRaLQ/bU=
|
||||
dependencies:
|
||||
broccoli-plugin "^1.3.0"
|
||||
can-symlink "^1.0.0"
|
||||
fast-ordered-set "^1.0.2"
|
||||
fs-tree-diff "^0.5.4"
|
||||
heimdalljs "^0.2.1"
|
||||
heimdalljs-logger "^0.1.7"
|
||||
rimraf "^2.4.3"
|
||||
symlink-or-copy "^1.0.0"
|
||||
|
||||
broccoli-merge-trees@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/broccoli-merge-trees/-/broccoli-merge-trees-2.0.1.tgz#14d4b7fc1a90318c12b16f843e6ba2693808100c"
|
||||
@@ -7460,6 +7456,15 @@ chalk@^4.1.0, chalk@^4.1.2:
|
||||
ansi-styles "^4.1.0"
|
||||
supports-color "^7.1.0"
|
||||
|
||||
chalk@~0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.4.0.tgz#5199a3ddcd0c1efe23bc08c1b027b06176e0c64f"
|
||||
integrity sha512-sQfYDlfv2DGVtjdoQqxS0cEZDroyG8h6TamA6rvxwlrU5BaSLDx9xhatBYl2pxZ7gmpNaPFVwBtdGdu5rQ+tYQ==
|
||||
dependencies:
|
||||
ansi-styles "~1.0.0"
|
||||
has-color "~0.1.0"
|
||||
strip-ansi "~0.1.0"
|
||||
|
||||
character-entities-legacy@^1.0.0:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz#94bc1845dce70a5bb9d2ecc748725661293d8fc1"
|
||||
@@ -7774,10 +7779,10 @@ code-point-at@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
|
||||
integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
|
||||
|
||||
codemirror@^5.47.0, codemirror@^5.56.0:
|
||||
version "5.63.3"
|
||||
resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.63.3.tgz#97042a242027fe0c87c09b36bc01931d37b76527"
|
||||
integrity sha512-1C+LELr+5grgJYqwZKqxrcbPsHFHapVaVAloBsFBASbpLnQqLw1U8yXJ3gT5D+rhxIiSpo+kTqN+hQ+9ialIXw==
|
||||
codemirror@^5.58.2:
|
||||
version "5.65.6"
|
||||
resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.65.6.tgz#fc313797331cbeb3bcab0652d1ec9d0f40c23ab5"
|
||||
integrity sha512-zNihMSMoDxK9Gqv9oEyDT8oM51rcRrQ+IEo2zyS48gJByBq5Fj8XuNEguMra+MuIOuh6lkpnLUJeL70DoTt6yw==
|
||||
|
||||
collapse-white-space@^1.0.2:
|
||||
version "1.0.6"
|
||||
@@ -9607,18 +9612,6 @@ ember-cli-moment-shim@^3.8.0:
|
||||
moment "^2.19.3"
|
||||
moment-timezone "^0.5.13"
|
||||
|
||||
ember-cli-node-assets@^0.2.2:
|
||||
version "0.2.2"
|
||||
resolved "https://registry.yarnpkg.com/ember-cli-node-assets/-/ember-cli-node-assets-0.2.2.tgz#d2d55626e7cc6619f882d7fe55751f9266022708"
|
||||
integrity sha1-0tVWJufMZhn4gtf+VXUfkmYCJwg=
|
||||
dependencies:
|
||||
broccoli-funnel "^1.0.1"
|
||||
broccoli-merge-trees "^1.1.1"
|
||||
broccoli-source "^1.1.0"
|
||||
debug "^2.2.0"
|
||||
lodash "^4.5.1"
|
||||
resolve "^1.1.7"
|
||||
|
||||
ember-cli-normalize-entity-name@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ember-cli-normalize-entity-name/-/ember-cli-normalize-entity-name-1.0.0.tgz#0b14f7bcbc599aa117b5fddc81e4fd03c4bad5b7"
|
||||
@@ -11277,7 +11270,7 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6:
|
||||
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
|
||||
integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
|
||||
|
||||
fast-ordered-set@^1.0.0, fast-ordered-set@^1.0.2:
|
||||
fast-ordered-set@^1.0.0:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/fast-ordered-set/-/fast-ordered-set-1.0.3.tgz#3fbb36634f7be79e4f7edbdb4a357dee25d184eb"
|
||||
integrity sha1-P7s2Y097555PftvbSjV97iXRhOs=
|
||||
@@ -12359,6 +12352,11 @@ has-bigints@^1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113"
|
||||
integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==
|
||||
|
||||
has-color@~0.1.0:
|
||||
version "0.1.7"
|
||||
resolved "https://registry.yarnpkg.com/has-color/-/has-color-0.1.7.tgz#67144a5260c34fc3cca677d041daf52fe7b78b2f"
|
||||
integrity sha512-kaNz5OTAYYmt646Hkqw50/qyxP2vFnTVu5AQ1Zmk22Kk5+4Qx6BpO8+u7IKsML5fOsFk0ZT0AcCJNYwcvaLBvw==
|
||||
|
||||
has-flag@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
|
||||
@@ -13534,14 +13532,6 @@ iterate-value@^1.0.2:
|
||||
es-get-iterator "^1.0.2"
|
||||
iterate-iterator "^1.0.1"
|
||||
|
||||
ivy-codemirror@IvyApp/ivy-codemirror#c3b7f49f8e6492878619f8055695581240cce21a:
|
||||
version "2.1.0"
|
||||
resolved "https://codeload.github.com/IvyApp/ivy-codemirror/tar.gz/c3b7f49f8e6492878619f8055695581240cce21a"
|
||||
dependencies:
|
||||
codemirror "^5.47.0"
|
||||
ember-cli-babel "^7.7.3"
|
||||
ember-cli-node-assets "^0.2.2"
|
||||
|
||||
jest-worker@^26.5.0:
|
||||
version "26.6.2"
|
||||
resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed"
|
||||
@@ -13746,6 +13736,14 @@ jsonify@~0.0.0:
|
||||
resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
|
||||
integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=
|
||||
|
||||
jsonlint@^1.6.3:
|
||||
version "1.6.3"
|
||||
resolved "https://registry.yarnpkg.com/jsonlint/-/jsonlint-1.6.3.tgz#cb5e31efc0b78291d0d862fbef05900adf212988"
|
||||
integrity sha512-jMVTMzP+7gU/IyC6hvKyWpUU8tmTkK5b3BPNuMI9U8Sit+YAWLlZwB6Y6YrdCxfg2kNz05p3XY3Bmm4m26Nv3A==
|
||||
dependencies:
|
||||
JSV "^4.0.x"
|
||||
nomnom "^1.5.x"
|
||||
|
||||
jsprim@^1.2.2:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
|
||||
@@ -14292,7 +14290,7 @@ lodash.values@^4.3.0:
|
||||
resolved "https://registry.yarnpkg.com/lodash.values/-/lodash.values-4.3.0.tgz#a3a6c2b0ebecc5c2cba1c17e6e620fe81b53d347"
|
||||
integrity sha1-o6bCsOvsxcLLocF+bmIP6BtT00c=
|
||||
|
||||
lodash@^4.17.10, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.5.1:
|
||||
lodash@^4.17.10, lodash@^4.17.12, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4:
|
||||
version "4.17.21"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||
@@ -15167,6 +15165,14 @@ node-watch@0.7.3:
|
||||
resolved "https://registry.yarnpkg.com/node-watch/-/node-watch-0.7.3.tgz#6d4db88e39c8d09d3ea61d6568d80e5975abc7ab"
|
||||
integrity sha512-3l4E8uMPY1HdMMryPRUAl+oIHtXtyiTlIiESNSVSNxcPfzAFzeTbXFQkZfAwBbo0B1qMSG8nUABx+Gd+YrbKrQ==
|
||||
|
||||
nomnom@^1.5.x:
|
||||
version "1.8.1"
|
||||
resolved "https://registry.yarnpkg.com/nomnom/-/nomnom-1.8.1.tgz#2151f722472ba79e50a76fc125bb8c8f2e4dc2a7"
|
||||
integrity sha512-5s0JxqhDx9/rksG2BTMVN1enjWSvPidpoSgViZU4ZXULyTe+7jxcCRLB6f42Z0l1xYJpleCBtSyY6Lwg3uu5CQ==
|
||||
dependencies:
|
||||
chalk "~0.4.0"
|
||||
underscore "~1.6.0"
|
||||
|
||||
nopt@^3.0.6:
|
||||
version "3.0.6"
|
||||
resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9"
|
||||
@@ -17197,14 +17203,6 @@ resolve-url@^0.2.1:
|
||||
resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
|
||||
integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
|
||||
|
||||
resolve@^1.1.7, resolve@^1.10.1, resolve@^1.11.1, resolve@^1.12.0, resolve@^1.19.0, resolve@^1.3.3, resolve@^1.8.1:
|
||||
version "1.19.0"
|
||||
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.19.0.tgz#1af5bf630409734a067cae29318aac7fa29a267c"
|
||||
integrity sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==
|
||||
dependencies:
|
||||
is-core-module "^2.1.0"
|
||||
path-parse "^1.0.6"
|
||||
|
||||
resolve@^1.10.0, resolve@^1.13.1, resolve@^1.17.0, resolve@^1.20.0, resolve@^1.4.0, resolve@^1.5.0:
|
||||
version "1.22.0"
|
||||
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198"
|
||||
@@ -17214,6 +17212,14 @@ resolve@^1.10.0, resolve@^1.13.1, resolve@^1.17.0, resolve@^1.20.0, resolve@^1.4
|
||||
path-parse "^1.0.7"
|
||||
supports-preserve-symlinks-flag "^1.0.0"
|
||||
|
||||
resolve@^1.10.1, resolve@^1.11.1, resolve@^1.12.0, resolve@^1.19.0, resolve@^1.3.3, resolve@^1.8.1:
|
||||
version "1.19.0"
|
||||
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.19.0.tgz#1af5bf630409734a067cae29318aac7fa29a267c"
|
||||
integrity sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==
|
||||
dependencies:
|
||||
is-core-module "^2.1.0"
|
||||
path-parse "^1.0.6"
|
||||
|
||||
resolve@^1.14.2, resolve@^1.3.2:
|
||||
version "1.20.0"
|
||||
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
|
||||
@@ -18322,6 +18328,11 @@ strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-ansi@~0.1.0:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-0.1.1.tgz#39e8a98d044d150660abe4a6808acf70bb7bc991"
|
||||
integrity sha512-behete+3uqxecWlDAm5lmskaSaISA+ThQ4oNNBDTBJt0x2ppR6IPqfZNuj6BLaLJ/Sji4TPZlcRyOis8wXQTLg==
|
||||
|
||||
strip-bom@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
|
||||
@@ -19101,6 +19112,11 @@ underscore@>=1.8.3:
|
||||
resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.1.tgz#0c1c6bd2df54b6b69f2314066d65b6cde6fcf9d1"
|
||||
integrity sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g==
|
||||
|
||||
underscore@~1.6.0:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.6.0.tgz#8b38b10cacdef63337b8b24e4ff86d45aea529a8"
|
||||
integrity sha512-z4o1fvKUojIWh9XuaVLUDdf86RQiq13AC1dmHbTpoyuu+bquHms76v16CjycCbec87J7z0k//SiQVk0sMdFmpQ==
|
||||
|
||||
unfetch@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-4.2.0.tgz#7e21b0ef7d363d8d9af0fb929a5555f6ef97a3be"
|
||||
|
||||
Reference in New Issue
Block a user