mirror of https://github.com/status-im/consul.git
ui: Centralized Config Intention Permission CRUD (#8762)
This commit is contained in:
parent
5261bb0d0d
commit
ed9826bbbe
|
@ -1,4 +1,5 @@
|
|||
import Adapter, { DATACENTER_QUERY_PARAM as API_DATACENTER_KEY } from './application';
|
||||
import { get } from '@ember/object';
|
||||
import { FOREIGN_KEY as DATACENTER_KEY } from 'consul-ui/models/dc';
|
||||
// Intentions use SourceNS and DestinationNS properties for namespacing
|
||||
// so we don't need to add the `?ns=` anywhere here
|
||||
|
@ -25,55 +26,66 @@ export default Adapter.extend({
|
|||
if (typeof id === 'undefined') {
|
||||
throw new Error('You must specify an id');
|
||||
}
|
||||
|
||||
// get the information we need from the id, which has been previously encoded
|
||||
const [SourceNS, SourceName, DestinationNS, DestinationName] = id
|
||||
.split(':')
|
||||
.map(decodeURIComponent);
|
||||
|
||||
return request`
|
||||
GET /v1/connect/intentions/exact?source=${SourceNS +
|
||||
'/' +
|
||||
SourceName}&destination=${DestinationNS + '/' + DestinationName}&${{ dc }}
|
||||
GET /v1/connect/intentions/exact?${{
|
||||
source: `${SourceNS}/${SourceName}`,
|
||||
destination: `${DestinationNS}/${DestinationName}`,
|
||||
dc: dc,
|
||||
}}
|
||||
Cache-Control: no-store
|
||||
|
||||
${{ index }}
|
||||
`;
|
||||
},
|
||||
requestForCreateRecord: function(request, serialized, data) {
|
||||
// TODO: need to make sure we remove dc
|
||||
return request`
|
||||
POST /v1/connect/intentions?${{ [API_DATACENTER_KEY]: data[DATACENTER_KEY] }}
|
||||
const body = {
|
||||
SourceNS: serialized.SourceNS,
|
||||
DestinationNS: serialized.DestinationNS,
|
||||
SourceName: serialized.SourceName,
|
||||
DestinationName: serialized.DestinationName,
|
||||
SourceType: serialized.SourceType,
|
||||
Meta: serialized.Meta,
|
||||
Description: serialized.Description,
|
||||
};
|
||||
|
||||
${{
|
||||
SourceNS: serialized.SourceNS,
|
||||
DestinationNS: serialized.DestinationNS,
|
||||
SourceName: serialized.SourceName,
|
||||
DestinationName: serialized.DestinationName,
|
||||
SourceType: serialized.SourceType,
|
||||
Action: serialized.Action,
|
||||
Description: serialized.Description,
|
||||
// only send the Action if we have one
|
||||
if (get(serialized, 'Action.length')) {
|
||||
body.Action = serialized.Action;
|
||||
} else {
|
||||
// otherwise only send Permissions if we have them
|
||||
if (serialized.Permissions) {
|
||||
body.Permissions = serialized.Permissions;
|
||||
}
|
||||
}
|
||||
return request`
|
||||
PUT /v1/connect/intentions/exact?${{
|
||||
source: `${data.SourceNS}/${data.SourceName}`,
|
||||
destination: `${data.DestinationNS}/${data.DestinationName}`,
|
||||
[API_DATACENTER_KEY]: data[DATACENTER_KEY],
|
||||
}}
|
||||
|
||||
${body}
|
||||
`;
|
||||
},
|
||||
requestForUpdateRecord: function(request, serialized, data) {
|
||||
return request`
|
||||
PUT /v1/connect/intentions/${data.LegacyID}?${{ [API_DATACENTER_KEY]: data[DATACENTER_KEY] }}
|
||||
|
||||
${{
|
||||
SourceNS: serialized.SourceNS,
|
||||
DestinationNS: serialized.DestinationNS,
|
||||
SourceName: serialized.SourceName,
|
||||
DestinationName: serialized.DestinationName,
|
||||
SourceType: serialized.SourceType,
|
||||
Action: serialized.Action,
|
||||
Meta: serialized.Meta,
|
||||
Description: serialized.Description,
|
||||
}}
|
||||
`;
|
||||
// you can no longer save Destinations
|
||||
delete serialized.DestinationNS;
|
||||
delete serialized.DestinationName;
|
||||
return this.requestForCreateRecord(...arguments);
|
||||
},
|
||||
requestForDeleteRecord: function(request, serialized, data) {
|
||||
return request`
|
||||
DELETE /v1/connect/intentions/${data.LegacyID}?${{
|
||||
[API_DATACENTER_KEY]: data[DATACENTER_KEY],
|
||||
}}
|
||||
DELETE /v1/connect/intentions/exact?${{
|
||||
source: `${data.SourceNS}/${data.SourceName}`,
|
||||
destination: `${data.DestinationNS}/${data.DestinationName}`,
|
||||
[API_DATACENTER_KEY]: data[DATACENTER_KEY],
|
||||
}}
|
||||
`;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -92,7 +92,9 @@
|
|||
{{nspace.Name}}
|
||||
{{/if}}
|
||||
</PowerSelectWithCreate>
|
||||
{{#if create}}
|
||||
<em>For the destination, you may choose any namespace for which you have access.</em>
|
||||
{{/if}}
|
||||
</label>
|
||||
{{/if}}
|
||||
</fieldset>
|
||||
|
@ -112,12 +114,17 @@
|
|||
header="Deny"
|
||||
body="The source service will not be allowed to connect to the destination."
|
||||
)
|
||||
(hash
|
||||
intent=""
|
||||
header="L7 Permissions"
|
||||
body="The source service may or may not connect to the destination service via unique permissions based on L7 criteria: path, header, or method."
|
||||
)
|
||||
)
|
||||
as |_action|}}
|
||||
as |_action|}}
|
||||
<RadioCard
|
||||
class={{concat 'value-' _action.intent}}
|
||||
@value={{_action.intent}}
|
||||
@checked={{if (eq item.Action _action.intent) 'checked'}}
|
||||
@checked={{if (eq (or item.Action '') _action.intent) 'checked'}}
|
||||
@onchange={{action onchange}}
|
||||
@name="Action"
|
||||
as |radio|>
|
||||
|
@ -131,13 +138,10 @@
|
|||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
<label class="type-text{{if item.error.Description ' has-error'}}">
|
||||
<span>Description (Optional)</span>
|
||||
<input type="text" name="Description" value={{item.Description}} placeholder="Description (Optional)" onchange={{action onchange}} />
|
||||
</label>
|
||||
</fieldset>
|
||||
{{#if (not item.Legacy)}}
|
||||
<fieldset>
|
||||
{{#if (eq (or item.Action '') '')}}
|
||||
<fieldset class="permissions">
|
||||
<button type="button" onclick={{action (mut shouldShowPermissionForm) true}}>Add permission</button>
|
||||
<h2>Permissions</h2>
|
||||
{{#if (gt item.Permissions.length 0) }}
|
||||
<div class="notice info">
|
||||
|
@ -148,17 +152,21 @@
|
|||
<a href="{{env 'CONSUL_DOCS_URL'}}/guides/acl-migrate-tokens.html" target="_blank" rel="noopener noreferrer">documentation</a>
|
||||
</p>
|
||||
</div>
|
||||
<ConsulIntentionPermissionList @items={{item.Permissions}} />
|
||||
<ConsulIntentionPermissionList
|
||||
@items={{item.Permissions}}
|
||||
@onclick={{queue (action (mut permission)) (action (mut shouldShowPermissionForm) true)}}
|
||||
@ondelete={{action 'delete' 'Permissions' item}}
|
||||
/>
|
||||
{{else}}
|
||||
<EmptyState>
|
||||
<BlockSlot @name="header">
|
||||
<h3>
|
||||
Add permissions via CLI
|
||||
No permissions yet
|
||||
</h3>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="body">
|
||||
<p>
|
||||
Permissions intercept an Intention's traffic using L7 criteria, such as path prefixes and http headers. You can use the CLI to add permissions to this intention.
|
||||
Permissions intercept an Intention's traffic using L7 criteria, such as path prefixes and http headers.
|
||||
</p>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="actions">
|
||||
|
@ -173,4 +181,47 @@
|
|||
{{/if}}
|
||||
</fieldset>
|
||||
{{/if}}
|
||||
<fieldset>
|
||||
<label class="type-text{{if item.error.Description ' has-error'}}">
|
||||
<span>Description (Optional)</span>
|
||||
<input type="text" name="Description" value={{item.Description}} placeholder="Description (Optional)" onchange={{action onchange}} />
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
{{#if shouldShowPermissionForm}}
|
||||
<ModalDialog
|
||||
class="consul-intention-permission-modal"
|
||||
@onclose={{queue (action (mut shouldShowPermissionForm) false) (action (mut permission) undefined)}}
|
||||
as |modal|>
|
||||
<BlockSlot @name="header">
|
||||
<h3>Edit Permission</h3>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="body">
|
||||
<ConsulIntentionPermissionForm
|
||||
@item={{permission}}
|
||||
@onsubmit={{action 'add' 'Permissions' item}}
|
||||
as |permissionForm|>
|
||||
<Ref @target={{this}} @name="permissionForm" @value={{permissionForm}} />
|
||||
</ConsulIntentionPermissionForm>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="actions">
|
||||
<button
|
||||
type="button"
|
||||
class="type-submit"
|
||||
disabled={{if (not this.permissionForm.isDirty) 'disabled'}}
|
||||
onclick={{queue (action this.permissionForm.submit) (action modal.close)}}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="type-cancel"
|
||||
onclick={{queue (action this.permissionForm.reset) (action modal.close)}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</BlockSlot>
|
||||
</ModalDialog>
|
||||
{{/if}}
|
||||
|
||||
</div>
|
|
@ -2,6 +2,9 @@ import Component from '@ember/component';
|
|||
|
||||
export default Component.extend({
|
||||
tagName: '',
|
||||
|
||||
shouldShowPermissionForm: false,
|
||||
|
||||
actions: {
|
||||
createNewLabel: function(template, term) {
|
||||
return template.replace(/{{term}}/g, term);
|
||||
|
@ -9,5 +12,17 @@ export default Component.extend({
|
|||
isUnique: function(items, term) {
|
||||
return !items.findBy('Name', term);
|
||||
},
|
||||
add: function(name, changeset, value) {
|
||||
if (!(changeset.get(name) || []).includes(value) && value.isNew) {
|
||||
changeset.pushObject(name, value);
|
||||
changeset.validate();
|
||||
}
|
||||
},
|
||||
delete: function(name, changeset, value) {
|
||||
if ((changeset.get(name) || []).includes(value)) {
|
||||
changeset.removeObject(name, value);
|
||||
changeset.validate();
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
.consul-intention-fieldsets {
|
||||
[role='radiogroup'] {
|
||||
overflow: visible !important;
|
||||
display: grid;
|
||||
grid-gap: 12px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(270px, auto));
|
||||
}
|
||||
.radio-card {
|
||||
@extend %radio-card-with-icon;
|
||||
}
|
||||
.radio-card header > * {
|
||||
display: inline;
|
||||
}
|
||||
.value-allow > :last-child::before {
|
||||
@extend %with-arrow-right-color-icon, %as-pseudo;
|
||||
}
|
||||
.value-deny > :last-child::before {
|
||||
@extend %with-deny-color-icon, %as-pseudo;
|
||||
}
|
||||
.permissions > button {
|
||||
@extend %anchor;
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
.consul-intention-permission-modal [role="dialog"] > div > div {
|
||||
width: 100%;
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
@import './skin';
|
||||
@import './layout';
|
||||
.consul-intention-list td.source,
|
||||
.consul-intention-list td.destination {
|
||||
@extend %tbody-th;
|
||||
}
|
||||
.consul-intention-list td strong {
|
||||
@extend %pill-700;
|
||||
}
|
||||
.consul-intention-list td.intent-allow strong {
|
||||
@extend %pill-allow;
|
||||
}
|
||||
.consul-intention-list td.intent-deny strong {
|
||||
@extend %pill-deny;
|
||||
}
|
||||
.consul-intention-list td.intent-l7-rules strong {
|
||||
@extend %pill-l7;
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
.consul-intention-list {
|
||||
td {
|
||||
height: 59px;
|
||||
}
|
||||
tr > *:nth-child(1) {
|
||||
width: calc(30% - 50px);
|
||||
}
|
||||
tr > *:nth-child(2) {
|
||||
width: 100px;
|
||||
}
|
||||
tr > *:nth-child(3) {
|
||||
width: calc(30% - 50px);
|
||||
}
|
||||
tr > *:nth-child(4) {
|
||||
width: calc(40% - 220px);
|
||||
}
|
||||
tr > *:nth-child(5) {
|
||||
width: 160px;
|
||||
}
|
||||
tr > *:last-child {
|
||||
width: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
@media #{$--lt-horizontal-nav} {
|
||||
.consul-intention-list tr > :not(.source):not(.destination):not(.intent) {
|
||||
display: none;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
.consul-intention-list {
|
||||
td.permissions {
|
||||
color: $blue-500;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,135 @@
|
|||
<div
|
||||
...attributes
|
||||
class="consul-intention-permission-form"
|
||||
>
|
||||
{{yield (hash
|
||||
submit=(action 'submit' changeset)
|
||||
reset=(action 'reset' changeset)
|
||||
|
||||
isDirty=(and changeset.isValid)
|
||||
changeset=changeset
|
||||
)}}
|
||||
|
||||
<fieldset>
|
||||
<span class="label">
|
||||
Should this permission allow the source connect to the destination?
|
||||
</span>
|
||||
<div role="radiogroup" class={{if changeset.error.Action ' has-error'}}>
|
||||
{{#each intents as |intent|}}
|
||||
<label>
|
||||
<span>{{capitalize intent}}</span>
|
||||
<input
|
||||
type="radio"
|
||||
name="Action"
|
||||
value={{intent}}
|
||||
checked={{if (eq changeset.Action intent) 'checked'}}
|
||||
onchange={{action (changeset-set changeset 'Action') value="target.value"}}
|
||||
/>
|
||||
</label>
|
||||
{{/each}}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<header>
|
||||
<h2>Path</h2>
|
||||
</header>
|
||||
<div>
|
||||
<label class="type-select">
|
||||
<span>Path Type</span>
|
||||
<PowerSelect
|
||||
@options={{pathTypes}}
|
||||
@selected={{pathType}}
|
||||
@onChange={{action 'change' 'HTTP.PathType' changeset}} as |Type|>
|
||||
{{get pathLabels Type}}
|
||||
</PowerSelect>
|
||||
</label>
|
||||
{{#if shouldShowPathField}}
|
||||
<label class="type-text{{if changeset.error.HTTP.Path ' has-error'}}">
|
||||
<span>{{get pathLabels pathType}}</span>
|
||||
<input
|
||||
type="text"
|
||||
name="Path"
|
||||
value={{changeset-get changeset 'HTTP.Path'}}
|
||||
oninput={{action 'change' 'HTTP.Path' changeset}}
|
||||
/>
|
||||
{{#if changeset.error.HTTP.Path}}
|
||||
<strong>
|
||||
{{#if (eq (changeset-get changeset 'HTTP.PathType') 'PathRegex')}}
|
||||
Path Regex should not be blank
|
||||
{{else}}
|
||||
Path should begin with a '/'
|
||||
{{/if}}
|
||||
</strong>
|
||||
{{/if}}
|
||||
</label>
|
||||
{{/if}}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<h2>Methods</h2>
|
||||
<div class="type-toggle">
|
||||
<span>All methods are applied by default unless specified</span>
|
||||
<label class="type-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="{{name}}[allMethods]"
|
||||
checked={{if allMethods 'checked'}}
|
||||
onchange={{action 'change' 'allMethods' changeset}}
|
||||
/>
|
||||
<span>All methods</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{{#if shouldShowMethods}}
|
||||
<div class="checkbox-group" role="group">
|
||||
{{#each methods as |method|}}
|
||||
<label class="type-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="method"
|
||||
value={{method}}
|
||||
checked={{if (contains method changeset.HTTP.Methods) 'checked'}}
|
||||
onchange={{action 'change' 'method' changeset}}
|
||||
/>
|
||||
<span>{{method}}</span>
|
||||
</label>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<h2>Headers</h2>
|
||||
<ConsulIntentionPermissionHeaderList
|
||||
@items={{changeset-get changeset 'HTTP.Header'}}
|
||||
@ondelete={{action 'delete' 'HTTP.Header' changeset}}
|
||||
as |headerList|>
|
||||
|
||||
</ConsulIntentionPermissionHeaderList>
|
||||
|
||||
<ConsulIntentionPermissionHeaderForm
|
||||
@onsubmit={{action 'add' 'HTTP.Header' changeset}}
|
||||
as |headerForm|>
|
||||
<Ref @target={{this}} @name="headerForm" @value={{headerForm}} />
|
||||
</ConsulIntentionPermissionHeaderForm>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="type-submit"
|
||||
disabled={{if (not this.headerForm.isDirty) 'disabled'}}
|
||||
onclick={{action this.headerForm.submit}}
|
||||
>
|
||||
Add another header
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="type-cancel"
|
||||
onclick={{action this.headerForm.reset}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
|
||||
</fieldset>
|
||||
</div>
|
|
@ -0,0 +1,123 @@
|
|||
import Component from '@ember/component';
|
||||
import { get, set, computed } from '@ember/object';
|
||||
import { alias, not, equal } from '@ember/object/computed';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
const name = 'intention-permission';
|
||||
export default Component.extend({
|
||||
tagName: '',
|
||||
name: name,
|
||||
|
||||
schema: service('schema'),
|
||||
change: service('change'),
|
||||
repo: service(`repository/${name}`),
|
||||
|
||||
onsubmit: function() {},
|
||||
onreset: function() {},
|
||||
|
||||
intents: alias(`schema.${name}.Action.allowedValues`),
|
||||
methods: alias(`schema.${name}-http.Methods.allowedValues`),
|
||||
pathProps: alias(`schema.${name}-http.PathType.allowedValues`),
|
||||
|
||||
pathTypes: computed('pathProps', function() {
|
||||
return ['NoPath'].concat(this.pathProps);
|
||||
}),
|
||||
|
||||
pathLabels: computed(function() {
|
||||
return {
|
||||
NoPath: 'No Path',
|
||||
PathExact: 'Exact',
|
||||
PathPrefix: 'Prefixed by',
|
||||
PathRegex: 'Regular Expression',
|
||||
};
|
||||
}),
|
||||
|
||||
pathInputLabels: computed(function() {
|
||||
return {
|
||||
PathExact: 'Exact Path',
|
||||
PathPrefix: 'Path Prefix',
|
||||
PathRegex: 'Path Regular Expression',
|
||||
};
|
||||
}),
|
||||
|
||||
changeset: computed('item', function() {
|
||||
const changeset = this.change.changesetFor(name, this.item || this.repo.create());
|
||||
if (changeset.isNew) {
|
||||
changeset.validate();
|
||||
}
|
||||
return changeset;
|
||||
}),
|
||||
|
||||
pathType: computed('changeset._changes.HTTP.PathType', 'pathTypes.firstObject', function() {
|
||||
return this.changeset.HTTP.PathType || this.pathTypes.firstObject;
|
||||
}),
|
||||
noPathType: equal('pathType', 'NoPath'),
|
||||
shouldShowPathField: not('noPathType'),
|
||||
|
||||
allMethods: false,
|
||||
shouldShowMethods: not('allMethods'),
|
||||
|
||||
didReceiveAttrs: function() {
|
||||
if (!get(this, 'item.HTTP.Methods.length')) {
|
||||
set(this, 'allMethods', true);
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
change: function(name, changeset, e) {
|
||||
const value = typeof get(e, 'target.value') !== 'undefined' ? e.target.value : e;
|
||||
switch (name) {
|
||||
case 'allMethods':
|
||||
set(this, name, e.target.checked);
|
||||
break;
|
||||
case 'method':
|
||||
if (e.target.checked) {
|
||||
this.actions.add.apply(this, ['HTTP.Methods', changeset, value]);
|
||||
} else {
|
||||
this.actions.delete.apply(this, ['HTTP.Methods', changeset, value]);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
changeset.set(name, value);
|
||||
}
|
||||
changeset.validate();
|
||||
},
|
||||
add: function(prop, changeset, value) {
|
||||
changeset.pushObject(prop, value);
|
||||
changeset.validate();
|
||||
},
|
||||
delete: function(prop, changeset, value) {
|
||||
changeset.removeObject(prop, value);
|
||||
changeset.validate();
|
||||
},
|
||||
submit: function(changeset, e) {
|
||||
const pathChanged =
|
||||
typeof changeset.changes.find(
|
||||
({ key, value }) => key === 'HTTP.PathType' || key === 'HTTP.Path'
|
||||
) !== 'undefined';
|
||||
if (pathChanged) {
|
||||
this.pathProps.forEach(prop => {
|
||||
changeset.set(`HTTP.${prop}`, undefined);
|
||||
});
|
||||
if (changeset.HTTP.PathType !== 'NoPath') {
|
||||
changeset.set(`HTTP.${changeset.HTTP.PathType}`, changeset.HTTP.Path);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.allMethods) {
|
||||
changeset.set('HTTP.Methods', null);
|
||||
}
|
||||
// this will prevent the changeset from overwriting the
|
||||
// computed properties on the ED object
|
||||
delete changeset._changes.HTTP.PathType;
|
||||
delete changeset._changes.HTTP.Path;
|
||||
//
|
||||
this.repo.persist(changeset);
|
||||
this.onsubmit(changeset.data);
|
||||
},
|
||||
reset: function(changeset, e) {
|
||||
changeset.rollback();
|
||||
this.onreset(changeset.data);
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,2 @@
|
|||
@import './skin';
|
||||
@import './layout';
|
|
@ -0,0 +1,22 @@
|
|||
.consul-intention-permission-form {
|
||||
h2 {
|
||||
padding-top: 1.4em;
|
||||
margin-top: .2em;
|
||||
margin-bottom: .6em;
|
||||
}
|
||||
.consul-intention-permission-header-form {
|
||||
margin-top: 10px;
|
||||
}
|
||||
fieldset:nth-child(2) > div,
|
||||
.consul-intention-permission-header-form fieldset > div {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||
grid-gap: 12px;
|
||||
}
|
||||
fieldset:nth-child(2) > div label:last-child {
|
||||
grid-column: span 2;
|
||||
}
|
||||
.ember-basic-dropdown-trigger {
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
.consul-intention-permission-form {
|
||||
h2 {
|
||||
border-top: 1px solid $blue-500;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
<div
|
||||
...attributes
|
||||
class="consul-intention-permission-header-form"
|
||||
>
|
||||
{{yield (hash
|
||||
submit=(action 'submit' changeset)
|
||||
reset=(action 'reset' changeset)
|
||||
|
||||
isDirty=(and changeset.isValid changeset.isDirty)
|
||||
changeset=changeset
|
||||
)}}
|
||||
|
||||
<fieldset>
|
||||
<div>
|
||||
|
||||
<label class="type-select">
|
||||
<span>Header Type</span>
|
||||
<div>
|
||||
<PowerSelect
|
||||
@options={{headerTypes}}
|
||||
@selected={{headerType}}
|
||||
@onChange={{action 'change' 'HeaderType' changeset}} as |Type|>
|
||||
{{get headerLabels Type}}
|
||||
</PowerSelect>
|
||||
</div>
|
||||
</label>
|
||||
<label class="type-text{{if changeset.error.Name ' has-error'}}">
|
||||
<span>Header name</span>
|
||||
<input
|
||||
type="text"
|
||||
name={{concat name '[Name]'}}
|
||||
value={{changeset-get changeset 'Name'}}
|
||||
oninput={{action 'change' 'Name' changeset}}
|
||||
/>
|
||||
{{#if changeset.error.Name}}
|
||||
<strong>{{changeset.error.Name.validation}}</strong>
|
||||
{{/if}}
|
||||
</label>
|
||||
|
||||
{{#if shouldShowValueField}}
|
||||
<label class="type-text{{if changeset.error.Value ' has-error'}}">
|
||||
<span>Header {{lowercase (get headerLabels headerType)}}</span>
|
||||
<input
|
||||
type="text"
|
||||
name="Value"
|
||||
value={{changeset-get changeset 'Value'}}
|
||||
oninput={{action 'change' 'Value' changeset}}
|
||||
/>
|
||||
{{#if changeset.error.Value}}
|
||||
<strong>{{changeset.error.Value.validation}}</strong>
|
||||
{{/if}}
|
||||
</label>
|
||||
{{/if}}
|
||||
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
|
@ -0,0 +1,85 @@
|
|||
import Component from '@ember/component';
|
||||
import { get, set, computed } from '@ember/object';
|
||||
import { alias, equal, not } from '@ember/object/computed';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
const name = 'intention-permission-http-header';
|
||||
export default Component.extend({
|
||||
tagName: '',
|
||||
name: name,
|
||||
|
||||
schema: service('schema'),
|
||||
change: service('change'),
|
||||
repo: service(`repository/${name}`),
|
||||
|
||||
onsubmit: function() {},
|
||||
onreset: function() {},
|
||||
|
||||
changeset: computed('item', function() {
|
||||
return this.change.changesetFor(
|
||||
name,
|
||||
this.item ||
|
||||
this.repo.create({
|
||||
HeaderType: this.headerTypes.firstObject,
|
||||
})
|
||||
);
|
||||
}),
|
||||
|
||||
headerTypes: alias(`schema.${name}.HeaderType.allowedValues`),
|
||||
|
||||
headerLabels: computed(function() {
|
||||
return {
|
||||
Exact: 'Exactly Matching',
|
||||
Prefix: 'Prefixed by',
|
||||
Suffix: 'Suffixed by',
|
||||
Regex: 'Regular Expression',
|
||||
Present: 'Is present',
|
||||
};
|
||||
}),
|
||||
|
||||
headerType: computed('changeset.HeaderType', 'headerTypes.firstObject', function() {
|
||||
return this.changeset.HeaderType || this.headerTypes.firstObject;
|
||||
}),
|
||||
|
||||
headerTypeEqualsPresent: equal('headerType', 'Present'),
|
||||
shouldShowValueField: not('headerTypeEqualsPresent'),
|
||||
|
||||
actions: {
|
||||
change: function(name, changeset, e) {
|
||||
const value = typeof get(e, 'target.value') !== 'undefined' ? e.target.value : e;
|
||||
switch (name) {
|
||||
default:
|
||||
changeset.set(name, value);
|
||||
}
|
||||
changeset.validate();
|
||||
},
|
||||
submit: function(changeset) {
|
||||
this.headerTypes.forEach(prop => {
|
||||
changeset.set(prop, undefined);
|
||||
});
|
||||
// Present is a boolean, whereas all other header types have a value
|
||||
const value = changeset.HeaderType === 'Present' ? true : changeset.Value;
|
||||
changeset.set(changeset.HeaderType, value);
|
||||
|
||||
// this will prevent the changeset from overwriting the
|
||||
// computed properties on the ED object
|
||||
delete changeset._changes.HeaderType;
|
||||
delete changeset._changes.Value;
|
||||
//
|
||||
|
||||
this.repo.persist(changeset);
|
||||
this.onsubmit(changeset.data);
|
||||
|
||||
set(
|
||||
this,
|
||||
'item',
|
||||
this.repo.create({
|
||||
HeaderType: this.headerType,
|
||||
})
|
||||
);
|
||||
},
|
||||
reset: function(changeset, e) {
|
||||
changeset.rollback();
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,45 @@
|
|||
{{#if (gt items.length 0)}}
|
||||
<ListCollection
|
||||
class="consul-intention-permission-header-list"
|
||||
@items={{items}}
|
||||
@scroll="native"
|
||||
@cellHeight={{42}}
|
||||
as |item|>
|
||||
<BlockSlot @name="details">
|
||||
<dl>
|
||||
<dt>
|
||||
<Tooltip>
|
||||
Header
|
||||
</Tooltip>
|
||||
</dt>
|
||||
<dd>
|
||||
{{item.Name}} {{route-match item}}
|
||||
</dd>
|
||||
</dl>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="actions" as |Actions|>
|
||||
<Actions as |Action|>
|
||||
<Action data-test-delete-action @onclick={{action ondelete item}} class="dangerous">
|
||||
<BlockSlot @name="label">
|
||||
Delete
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="confirmation" as |Confirmation|>
|
||||
<Confirmation class="warning">
|
||||
<BlockSlot @name="header">
|
||||
Confirm delete
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="body">
|
||||
<p>
|
||||
Are you sure you want to delete this header?
|
||||
</p>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="confirm" as |Confirm|>
|
||||
<Confirm>Delete</Confirm>
|
||||
</BlockSlot>
|
||||
</Confirmation>
|
||||
</BlockSlot>
|
||||
</Action>
|
||||
</Actions>
|
||||
</BlockSlot>
|
||||
</ListCollection>
|
||||
{{/if}}
|
|
@ -0,0 +1,5 @@
|
|||
import Component from '@ember/component';
|
||||
|
||||
export default Component.extend({
|
||||
tagName: '',
|
||||
});
|
|
@ -0,0 +1,8 @@
|
|||
@import './skin';
|
||||
@import './layout';
|
||||
.consul-intention-permission-header-list {
|
||||
> ul > li {
|
||||
@extend %list-row-200;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
.consul-intention-permission-header-list {
|
||||
dt::before {
|
||||
@extend %with-glyph-icon;
|
||||
content: 'H';
|
||||
}
|
||||
}
|
||||
|
|
@ -1,35 +1,83 @@
|
|||
{{#if (gt items.length 0)}}
|
||||
<div class="consul-intention-permission-list">
|
||||
<ul>
|
||||
{{#each items as |item|}}
|
||||
<li>
|
||||
<ListCollection
|
||||
class="consul-intention-permission-list{{if (not onclick) ' readonly'}}"
|
||||
@scroll="native"
|
||||
@items={{items}}
|
||||
@cellHeight={{42}}
|
||||
as |item|>
|
||||
<BlockSlot @name="details">
|
||||
<div onclick={{action (optional onclick) item}}>
|
||||
<strong class={{concat 'intent-' item.Action}}>{{item.Action}}</strong>
|
||||
{{#if item.Http.Path}}
|
||||
<dl class="route-path">
|
||||
{{#if (gt item.HTTP.Methods.length 0)}}
|
||||
<dl class="permission-methods">
|
||||
<dt>
|
||||
<Tooltip>
|
||||
{{item.Http.PathType}}
|
||||
Methods
|
||||
</Tooltip>
|
||||
</dt>
|
||||
<dd>
|
||||
{{item.Http.Path}}
|
||||
{{#each item.HTTP.Methods as |item|}}
|
||||
{{item}}
|
||||
{{/each}}
|
||||
</dd>
|
||||
</dl>
|
||||
{{/if}}
|
||||
{{#each item.Http.Header as |item|}}
|
||||
<dl class="route-header">
|
||||
{{#if item.HTTP.Path}}
|
||||
<dl class="permission-path">
|
||||
<dt>
|
||||
<Tooltip>
|
||||
{{item.HTTP.PathType}}
|
||||
</Tooltip>
|
||||
</dt>
|
||||
<dd>
|
||||
{{item.HTTP.Path}}
|
||||
</dd>
|
||||
</dl>
|
||||
{{/if}}
|
||||
{{#each item.HTTP.Header as |item|}}
|
||||
<dl class="permission-header">
|
||||
<dt>
|
||||
<Tooltip>
|
||||
Header
|
||||
</Tooltip>
|
||||
</dt>
|
||||
<dd>
|
||||
{{item.Name}} {{route-match item}}
|
||||
{{item.Name}} {{route-match item}}
|
||||
</dd>
|
||||
</dl>
|
||||
{{/each}}
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</BlockSlot>
|
||||
{{#if onclick}}
|
||||
<BlockSlot @name="actions" as |Actions|>
|
||||
<Actions as |Action|>
|
||||
<Action data-test-edit-action @onclick={{action (optional onclick) item}} @close={{true}}>
|
||||
<BlockSlot @name="label">
|
||||
Edit
|
||||
</BlockSlot>
|
||||
</Action>
|
||||
<Action data-test-delete-action @onclick={{action ondelete item}} class="dangerous">
|
||||
<BlockSlot @name="label">
|
||||
Delete
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="confirmation" as |Confirmation|>
|
||||
<Confirmation class="warning">
|
||||
<BlockSlot @name="header">
|
||||
Confirm delete
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="body">
|
||||
<p>
|
||||
Are you sure you want to delete this permission?
|
||||
</p>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="confirm" as |Confirm|>
|
||||
<Confirm>Delete</Confirm>
|
||||
</BlockSlot>
|
||||
</Confirmation>
|
||||
</BlockSlot>
|
||||
</Action>
|
||||
</Actions>
|
||||
</BlockSlot>
|
||||
{{/if}}
|
||||
</ListCollection>
|
||||
{{/if}}
|
|
@ -0,0 +1,36 @@
|
|||
@import './skin';
|
||||
@import './layout';
|
||||
%list-row-200 {
|
||||
@extend %list-row;
|
||||
padding-top: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
%list-row-200 .detail {
|
||||
grid-row-start: header !important;
|
||||
grid-row-end: detail !important;
|
||||
align-self: center !important;
|
||||
}
|
||||
%list-row-200 .popover-menu > [type="checkbox"] + label {
|
||||
padding: 0;
|
||||
}
|
||||
%list-row-200 .popover-menu > [type="checkbox"] + label + div:not(.above) {
|
||||
top: 30px;
|
||||
}
|
||||
%list-row-200 dd {
|
||||
@extend %p2;
|
||||
}
|
||||
.consul-intention-permission-list > ul > li {
|
||||
@extend %list-row-200;
|
||||
}
|
||||
.consul-intention-permission-list:not(.readonly) > ul > li {
|
||||
@extend %with-composite-row-intent;
|
||||
}
|
||||
.consul-intention-permission-list strong {
|
||||
@extend %pill-500;
|
||||
}
|
||||
.consul-intention-permission-list .intent-allow {
|
||||
@extend %pill-allow;
|
||||
}
|
||||
.consul-intention-permission-list .intent-deny {
|
||||
@extend %pill-deny;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
.consul-intention-permission-list {
|
||||
.detail > div {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
strong {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
.consul-intention-permission-list {
|
||||
dt::before {
|
||||
@extend %with-glyph-icon;
|
||||
}
|
||||
dl.permission-methods dt::before {
|
||||
content: 'M';
|
||||
}
|
||||
dl.permission-path dt::before {
|
||||
content: 'P';
|
||||
}
|
||||
dl.permission-header dt::before {
|
||||
content: 'H';
|
||||
}
|
||||
}
|
|
@ -34,7 +34,9 @@
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<ConsulIntentionPermissionList @items={{item.Permissions}} @readonly={{true}} />
|
||||
<ConsulIntentionPermissionList
|
||||
@items={{item.Permissions}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
</div>
|
||||
|
|
|
@ -1,34 +1,69 @@
|
|||
{{on-window 'resize' (action "resize") }}
|
||||
{{yield}}
|
||||
<EmberNativeScrollable
|
||||
@tagName="ul"
|
||||
@content-size={{_contentSize}}
|
||||
@scroll-left={{_scrollLeft}}
|
||||
@scroll-top={{_scrollTop}}
|
||||
@scrollChange={{action "scrollChange"}}
|
||||
@clientSizeChange={{action "clientSizeChange"}}
|
||||
<div
|
||||
class="list-collection list-collection-scroll-{{scroll}}"
|
||||
style={{{style}}}
|
||||
id={{guid}}
|
||||
...attributes
|
||||
>
|
||||
<li></li>
|
||||
{{~#each _cells as |cell|~}}
|
||||
<li
|
||||
data-test-list-row
|
||||
onclick={{action 'click'}} style={{{cell.style}}}
|
||||
class={{if
|
||||
(compute (action (or linkable (noop)) cell.item))
|
||||
'linkable'
|
||||
}}
|
||||
>
|
||||
<YieldSlot @name="header"><div class="header">{{yield cell.item cell.index}}</div></YieldSlot>
|
||||
<YieldSlot @name="details"><div class="detail">{{yield cell.item cell.index}}</div></YieldSlot>
|
||||
<YieldSlot @name="actions"
|
||||
@params={{
|
||||
block-params (component 'more-popover-menu' expanded=(if (eq checked cell.index) true false) onchange=(action "change" cell.index))
|
||||
{{yield}}
|
||||
{{#if (eq scroll 'virtual')}}
|
||||
{{on-window 'resize' (action "resize") }}
|
||||
<EmberNativeScrollable
|
||||
@tagName="ul"
|
||||
@content-size={{_contentSize}}
|
||||
@scroll-left={{_scrollLeft}}
|
||||
@scroll-top={{_scrollTop}}
|
||||
@scrollChange={{action "scrollChange"}}
|
||||
@clientSizeChange={{action "clientSizeChange"}}
|
||||
>
|
||||
<li></li>
|
||||
{{~#each _cells as |cell|~}}
|
||||
<li
|
||||
data-test-list-row
|
||||
onclick={{action 'click'}} style={{{cell.style}}}
|
||||
class={{if
|
||||
(compute (action (or linkable (noop)) cell.item))
|
||||
'linkable'
|
||||
}}
|
||||
>
|
||||
<div class="actions">
|
||||
{{yield cell.item cell.index}}
|
||||
</div>
|
||||
</YieldSlot>
|
||||
</li>
|
||||
{{~/each~}}
|
||||
</EmberNativeScrollable>
|
||||
<YieldSlot @name="header"><div class="header">{{yield cell.item cell.index}}</div></YieldSlot>
|
||||
<YieldSlot @name="details"><div class="detail">{{yield cell.item cell.index}}</div></YieldSlot>
|
||||
<YieldSlot @name="actions"
|
||||
@params={{
|
||||
block-params (component 'more-popover-menu' expanded=(if (eq checked cell.index) true false) onchange=(action "change" cell.index))
|
||||
}}
|
||||
>
|
||||
<div class="actions">
|
||||
{{yield cell.item cell.index}}
|
||||
</div>
|
||||
</YieldSlot>
|
||||
</li>
|
||||
{{~/each~}}
|
||||
</EmberNativeScrollable>
|
||||
{{else}}
|
||||
<ul>
|
||||
<li style="display: none;"></li>
|
||||
{{~#each items as |item index|~}}
|
||||
<li
|
||||
data-test-list-row
|
||||
onclick={{action 'click'}}
|
||||
class={{if
|
||||
(compute (action (or linkable (noop)) item))
|
||||
'linkable'
|
||||
}}
|
||||
>
|
||||
<YieldSlot @name="header"><div class="header">{{yield item index}}</div></YieldSlot>
|
||||
<YieldSlot @name="details"><div class="detail">{{yield item index}}</div></YieldSlot>
|
||||
<YieldSlot @name="actions"
|
||||
@params={{
|
||||
block-params (component 'more-popover-menu' onchange=(action "change" index))
|
||||
}}
|
||||
>
|
||||
<div class="actions">
|
||||
{{yield item index}}
|
||||
</div>
|
||||
</YieldSlot>
|
||||
</li>
|
||||
{{~/each~}}
|
||||
</ul>
|
||||
{{/if}}
|
||||
</div>
|
|
@ -9,20 +9,23 @@ const formatItemStyle = PercentageColumns.prototype.formatItemStyle;
|
|||
|
||||
export default Component.extend(Slotted, {
|
||||
dom: service('dom'),
|
||||
tagName: 'div',
|
||||
attributeBindings: ['style'],
|
||||
tagName: '',
|
||||
height: 500,
|
||||
cellHeight: 70,
|
||||
style: style('getStyle'),
|
||||
classNames: ['list-collection'],
|
||||
checked: null,
|
||||
scroll: 'virtual',
|
||||
init: function() {
|
||||
this._super(...arguments);
|
||||
this.columns = [100];
|
||||
this.guid = this.dom.guid(this);
|
||||
},
|
||||
didInsertElement: function() {
|
||||
this._super(...arguments);
|
||||
this.actions.resize.apply(this, [{ target: this.dom.viewport() }]);
|
||||
this.$element = this.dom.element(`#${this.guid}`);
|
||||
if (this.scroll === 'virtual') {
|
||||
this.actions.resize.apply(this, [{ target: this.dom.viewport() }]);
|
||||
}
|
||||
},
|
||||
didReceiveAttrs: function() {
|
||||
this._super(...arguments);
|
||||
|
@ -41,6 +44,9 @@ export default Component.extend(Slotted, {
|
|||
};
|
||||
},
|
||||
getStyle: computed('height', function() {
|
||||
if (this.scroll !== 'virtual') {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
height: get(this, 'height'),
|
||||
};
|
||||
|
@ -49,12 +55,11 @@ export default Component.extend(Slotted, {
|
|||
resize: function(e) {
|
||||
// TODO: This top part is very similar to resize in tabular-collection
|
||||
// see if it make sense to DRY out
|
||||
const dom = get(this, 'dom');
|
||||
const $appContent = dom.element('main > div');
|
||||
const $appContent = this.dom.element('main > div');
|
||||
if ($appContent) {
|
||||
const border = 1;
|
||||
const rect = this.element.getBoundingClientRect();
|
||||
const $footer = dom.element('footer[role="contentinfo"]');
|
||||
const rect = this.$element.getBoundingClientRect();
|
||||
const $footer = this.dom.element('footer[role="contentinfo"]');
|
||||
const space = rect.top + $footer.clientHeight + border;
|
||||
const height = e.target.innerHeight - space;
|
||||
this.set('height', Math.max(0, height));
|
||||
|
|
|
@ -36,6 +36,7 @@ as |components|}}
|
|||
</ToggleButton>
|
||||
|
||||
<MenuPanel @position={{position}} id={{aria.controls}} aria-labelledby={{aria.labelledBy}} aria-expanded={{aria.expanded}} as |menu|>
|
||||
<Ref @target={{this}} @name="menu" @value={{menu}} />
|
||||
<BlockSlot @name="controls">
|
||||
<input type="checkbox" id={{concat 'popover-menu-' guid '-'}} />
|
||||
{{#each submenus as |sub|}}
|
||||
|
|
|
@ -32,7 +32,10 @@
|
|||
role="menuitem"
|
||||
aria-selected={{if selected 'true'}}
|
||||
tabindex="-1"
|
||||
onclick={{action (or this.onclick (noop))}}>
|
||||
onclick={{queue
|
||||
(action (or this.onclick (noop)))
|
||||
(action (if this.close menu.clickTrigger (noop)))
|
||||
}}>
|
||||
<YieldSlot @name="label">
|
||||
{{yield}}
|
||||
</YieldSlot>
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import Component from '@ember/component';
|
||||
import Slotted from 'block-slots';
|
||||
|
||||
export default Component.extend(Slotted, {
|
||||
export default Component.extend({
|
||||
tagName: '',
|
||||
});
|
||||
|
|
|
@ -1,17 +1,20 @@
|
|||
import { helper } from '@ember/component/helper';
|
||||
|
||||
export default helper(function routeMatch([item], hash) {
|
||||
const keys = Object.keys(item.data || item);
|
||||
switch (true) {
|
||||
case keys.includes('Present'):
|
||||
const prop = ['Present', 'Exact', 'Prefix', 'Suffix', 'Regex'].find(
|
||||
prop => typeof item[prop] !== 'undefined'
|
||||
);
|
||||
|
||||
switch (prop) {
|
||||
case 'Present':
|
||||
return `${item.Invert ? `NOT ` : ``}present`;
|
||||
case keys.includes('Exact'):
|
||||
case 'Exact':
|
||||
return `${item.Invert ? `NOT ` : ``}exactly matching "${item.Exact}"`;
|
||||
case keys.includes('Prefix'):
|
||||
case 'Prefix':
|
||||
return `${item.Invert ? `NOT ` : ``}prefixed by "${item.Prefix}"`;
|
||||
case keys.includes('Suffix'):
|
||||
case 'Suffix':
|
||||
return `${item.Invert ? `NOT ` : ``}suffixed by "${item.Suffix}"`;
|
||||
case keys.includes('Regex'):
|
||||
case 'Regex':
|
||||
return `${item.Invert ? `NOT ` : ``}matching the regex "${item.Regex}"`;
|
||||
}
|
||||
return '';
|
||||
|
|
|
@ -1,7 +1,18 @@
|
|||
import { computed } from '@ember/object';
|
||||
import { or } from '@ember/object/computed';
|
||||
import attr from 'ember-data/attr';
|
||||
|
||||
import Fragment from 'ember-data-model-fragments/fragment';
|
||||
|
||||
export const schema = {
|
||||
Name: {
|
||||
required: true,
|
||||
},
|
||||
HeaderType: {
|
||||
allowedValues: ['Exact', 'Prefix', 'Suffix', 'Regex', 'Present'],
|
||||
},
|
||||
};
|
||||
|
||||
export default Fragment.extend({
|
||||
Name: attr('string'),
|
||||
|
||||
|
@ -9,6 +20,10 @@ export default Fragment.extend({
|
|||
Prefix: attr('string'),
|
||||
Suffix: attr('string'),
|
||||
Regex: attr('string'),
|
||||
Present: attr('boolean'),
|
||||
Invert: attr('boolean'),
|
||||
Present: attr(), // this is a boolean but we don't want it to automatically be set to false
|
||||
|
||||
Value: or(...schema.HeaderType.allowedValues),
|
||||
HeaderType: computed(...schema.HeaderType.allowedValues, function() {
|
||||
return schema.HeaderType.allowedValues.find(prop => typeof this[prop] !== 'undefined');
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -5,7 +5,15 @@ import { or } from '@ember/object/computed';
|
|||
import Fragment from 'ember-data-model-fragments/fragment';
|
||||
import { fragmentArray, array } from 'ember-data-model-fragments/attributes';
|
||||
|
||||
const pathProps = ['PathPrefix', 'PathExact', 'PathRegex'];
|
||||
export const schema = {
|
||||
PathType: {
|
||||
allowedValues: ['PathPrefix', 'PathExact', 'PathRegex'],
|
||||
},
|
||||
Methods: {
|
||||
allowedValues: ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', 'PATCH'],
|
||||
},
|
||||
};
|
||||
|
||||
export default Fragment.extend({
|
||||
PathExact: attr('string'),
|
||||
PathPrefix: attr('string'),
|
||||
|
@ -14,8 +22,8 @@ export default Fragment.extend({
|
|||
Header: fragmentArray('intention-permission-http-header'),
|
||||
Methods: array('string'),
|
||||
|
||||
Path: or(...pathProps),
|
||||
PathType: computed(...pathProps, function() {
|
||||
return pathProps.find(prop => typeof this[prop] === 'string');
|
||||
Path: or(...schema.PathType.allowedValues),
|
||||
PathType: computed(...schema.PathType.allowedValues, function() {
|
||||
return schema.PathType.allowedValues.find(prop => typeof this[prop] === 'string');
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -3,7 +3,16 @@ import attr from 'ember-data/attr';
|
|||
import Fragment from 'ember-data-model-fragments/fragment';
|
||||
import { fragment } from 'ember-data-model-fragments/attributes';
|
||||
|
||||
export const schema = {
|
||||
Action: {
|
||||
defaultValue: 'allow',
|
||||
allowedValues: ['allow', 'deny'],
|
||||
},
|
||||
};
|
||||
|
||||
export default Fragment.extend({
|
||||
Action: attr('string', { defaultValue: 'allow' }),
|
||||
Http: fragment('intention-permission-http'),
|
||||
Action: attr('string', {
|
||||
defaultValue: schema.Action.defaultValue,
|
||||
}),
|
||||
HTTP: fragment('intention-permission-http'),
|
||||
});
|
||||
|
|
|
@ -10,17 +10,17 @@ export default Model.extend({
|
|||
[PRIMARY_KEY]: attr('string'),
|
||||
[SLUG_KEY]: attr('string'),
|
||||
Description: attr('string'),
|
||||
SourceNS: attr('string'),
|
||||
SourceNS: attr('string', { defaultValue: 'default' }),
|
||||
SourceName: attr('string', { defaultValue: '*' }),
|
||||
DestinationName: attr('string', { defaultValue: '*' }),
|
||||
DestinationNS: attr('string'),
|
||||
DestinationNS: attr('string', { defaultValue: 'default' }),
|
||||
Precedence: attr('number'),
|
||||
Permissions: fragmentArray('intention-permission'),
|
||||
SourceType: attr('string', { defaultValue: 'consul' }),
|
||||
Action: attr('string'),
|
||||
Meta: attr(),
|
||||
Legacy: attr('boolean', { defaultValue: true }),
|
||||
LegacyID: attr('string'),
|
||||
Legacy: attr('boolean', { defaultValue: true }),
|
||||
|
||||
IsManagedByCRD: computed('Meta', function() {
|
||||
const meta = Object.entries(this.Meta || {}).find(
|
||||
|
@ -29,7 +29,7 @@ export default Model.extend({
|
|||
return typeof meta !== 'undefined';
|
||||
}),
|
||||
IsEditable: computed('Legacy', 'IsManagedByCRD', function() {
|
||||
return this.Legacy && !this.IsManagedByCRD;
|
||||
return !this.IsManagedByCRD;
|
||||
}),
|
||||
SyncTime: attr('number'),
|
||||
Datacenter: attr('string'),
|
||||
|
|
|
@ -6,7 +6,7 @@ export default Route.extend({
|
|||
nspace: '*',
|
||||
dc: this.paramsFor('dc').dc,
|
||||
service: this.paramsFor('dc.services.show').name,
|
||||
src: params.intention,
|
||||
src: params.intention_id,
|
||||
};
|
||||
},
|
||||
setupController: function(controller, model) {
|
||||
|
|
|
@ -44,16 +44,24 @@ export default Serializer.extend({
|
|||
query
|
||||
);
|
||||
},
|
||||
respondForCreateRecord: function(respond, serialized, data) {
|
||||
const slugKey = this.slugKey;
|
||||
const primaryKey = this.primaryKey;
|
||||
return respond((headers, body) => {
|
||||
body = data;
|
||||
body.ID = this
|
||||
.uri`${serialized.SourceNS}:${serialized.SourceName}:${serialized.DestinationNS}:${serialized.DestinationName}`;
|
||||
return this.fingerprint(primaryKey, slugKey, body.Datacenter)(body);
|
||||
});
|
||||
},
|
||||
respondForUpdateRecord: function(respond, serialized, data) {
|
||||
return this._super(
|
||||
cb =>
|
||||
respond((headers, body) => {
|
||||
body.LegacyID = body.ID;
|
||||
body.ID = serialized.ID;
|
||||
return cb(headers, body);
|
||||
}),
|
||||
serialized,
|
||||
data
|
||||
);
|
||||
const slugKey = this.slugKey;
|
||||
const primaryKey = this.primaryKey;
|
||||
return respond((headers, body) => {
|
||||
body = data;
|
||||
body.LegacyID = body.ID;
|
||||
body.ID = serialized.ID;
|
||||
return this.fingerprint(primaryKey, slugKey, body.Datacenter)(body);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
import Service, { inject as service } from '@ember/service';
|
||||
|
||||
import lookupValidator from 'ember-changeset-validations';
|
||||
import { Changeset as createChangeset } from 'ember-changeset';
|
||||
|
||||
import Changeset from 'consul-ui/utils/form/changeset';
|
||||
|
||||
import intentionPermissionValidator from 'consul-ui/validations/intention-permission';
|
||||
import intentionPermissionHttpHeaderValidator from 'consul-ui/validations/intention-permission-http-header';
|
||||
|
||||
const validators = {
|
||||
'intention-permission': intentionPermissionValidator,
|
||||
'intention-permission-http-header': intentionPermissionHttpHeaderValidator,
|
||||
};
|
||||
|
||||
export default Service.extend({
|
||||
schema: service('schema'),
|
||||
|
||||
init: function() {
|
||||
this._super(...arguments);
|
||||
this._validators = new Map();
|
||||
},
|
||||
willDestroy: function() {
|
||||
this._validators = null;
|
||||
},
|
||||
changesetFor: function(modelName, model, options = {}) {
|
||||
const validator = this.validatorFor(modelName, options);
|
||||
let changeset;
|
||||
if (validator) {
|
||||
let validatorFunc = validator;
|
||||
if (typeof validator !== 'function') {
|
||||
validatorFunc = lookupValidator(validator);
|
||||
}
|
||||
changeset = createChangeset(model, validatorFunc, validator, { changeset: Changeset });
|
||||
} else {
|
||||
changeset = createChangeset(model);
|
||||
}
|
||||
return changeset;
|
||||
},
|
||||
validatorFor: function(modelName, options = {}) {
|
||||
if (!this._validators.has(modelName)) {
|
||||
const factory = validators[modelName];
|
||||
let validator;
|
||||
if (typeof factory !== 'undefined') {
|
||||
validator = factory(this.schema);
|
||||
}
|
||||
this._validators.set(modelName, validator);
|
||||
}
|
||||
return this._validators.get(modelName);
|
||||
},
|
||||
});
|
|
@ -2,6 +2,7 @@ import Service, { inject as service } from '@ember/service';
|
|||
import { assert } from '@ember/debug';
|
||||
import { typeOf } from '@ember/utils';
|
||||
import { get } from '@ember/object';
|
||||
import { isChangeset } from 'validated-changeset';
|
||||
|
||||
export default Service.extend({
|
||||
getModelName: function() {
|
||||
|
@ -67,6 +68,13 @@ export default Service.extend({
|
|||
return this.store.createRecord(this.getModelName(), obj);
|
||||
},
|
||||
persist: function(item) {
|
||||
// workaround for saving changesets that contain fragments
|
||||
// firstly commit the changes down onto the object if
|
||||
// its a changeset, then save as a normal object
|
||||
if (isChangeset(item)) {
|
||||
item.execute();
|
||||
item = item.data;
|
||||
}
|
||||
return item.save();
|
||||
},
|
||||
remove: function(obj) {
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
import RepositoryService from 'consul-ui/services/repository';
|
||||
|
||||
const modelName = 'intention-permission-http-header';
|
||||
export default RepositoryService.extend({
|
||||
getModelName: function() {
|
||||
return modelName;
|
||||
},
|
||||
create: function(obj = {}) {
|
||||
return this.store.createFragment(this.getModelName(), obj);
|
||||
},
|
||||
persist: function(item) {
|
||||
return item.execute();
|
||||
},
|
||||
});
|
|
@ -0,0 +1,18 @@
|
|||
import RepositoryService from 'consul-ui/services/repository';
|
||||
const modelName = 'intention-permission';
|
||||
export default RepositoryService.extend({
|
||||
getModelName: function() {
|
||||
return modelName;
|
||||
},
|
||||
create: function(obj = {}) {
|
||||
// intention-permission and intention-permission-http
|
||||
// are currently treated as one and the same
|
||||
return this.store.createFragment(this.getModelName(), {
|
||||
...obj,
|
||||
HTTP: this.store.createFragment('intention-permission-http', obj.HTTP || {}),
|
||||
});
|
||||
},
|
||||
persist: function(item) {
|
||||
return item.execute();
|
||||
},
|
||||
});
|
|
@ -1,3 +1,4 @@
|
|||
import { set, get } from '@ember/object';
|
||||
import RepositoryService from 'consul-ui/services/repository';
|
||||
import { PRIMARY_KEY } from 'consul-ui/models/intention';
|
||||
const modelName = 'intention';
|
||||
|
@ -15,6 +16,19 @@ export default RepositoryService.extend({
|
|||
...obj,
|
||||
});
|
||||
},
|
||||
persist: function(obj) {
|
||||
return this._super(...arguments).then(res => {
|
||||
// if Action is set it means we are an l4 type intention
|
||||
// we don't delete these at a UI level incase the user
|
||||
// would like to switch backwards and forwards between
|
||||
// allow/deny/l7 in the forms, but once its been saved
|
||||
// to the backend we then delete them
|
||||
if (get(res, 'Action.length')) {
|
||||
set(res, 'Permissions', []);
|
||||
}
|
||||
return res;
|
||||
});
|
||||
},
|
||||
findByService: function(slug, dc, nspace, configuration = {}) {
|
||||
const query = {
|
||||
dc: dc,
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import Service from '@ember/service';
|
||||
import { schema as intentionPermissionSchema } from 'consul-ui/models/intention-permission';
|
||||
import { schema as intentionPermissionHttpSchema } from 'consul-ui/models/intention-permission-http';
|
||||
import { schema as intentionPermissionHttpHeaderSchema } from 'consul-ui/models/intention-permission-http-header';
|
||||
|
||||
export default Service.extend({
|
||||
'intention-permission': intentionPermissionSchema,
|
||||
'intention-permission-http': intentionPermissionHttpSchema,
|
||||
'intention-permission-http-header': intentionPermissionHttpHeaderSchema,
|
||||
});
|
|
@ -54,6 +54,15 @@
|
|||
@import './components/footer';
|
||||
|
||||
/**/
|
||||
@import './components/consul-intention-list';
|
||||
@import './components/consul-intention-permission-list';
|
||||
@import './components/consul-intention-fieldsets';
|
||||
|
||||
/**
|
||||
* Migration: We are migrating our consul-* styles to use colocated styles
|
||||
* consul-* component styles should be moved or added under here
|
||||
* when convienient
|
||||
**/
|
||||
|
||||
@import 'consul-ui/components/consul-intention-list';
|
||||
@import 'consul-ui/components/consul-intention-form/fieldsets';
|
||||
@import 'consul-ui/components/consul-intention-permission-list';
|
||||
@import 'consul-ui/components/consul-intention-permission-form';
|
||||
@import 'consul-ui/components/consul-intention-permission-header-list';
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
button[type='submit'],
|
||||
button.type-submit,
|
||||
a.type-create {
|
||||
@extend %primary-button;
|
||||
}
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
@import './consul-intention-fieldsets/index';
|
||||
.consul-intention-fieldsets {
|
||||
@extend %consul-intention-fieldsets;
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
%consul-intention-fieldsets [role='radiogroup'] {
|
||||
overflow: visible !important;
|
||||
display: grid;
|
||||
grid-gap: 12px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(270px, 370px));
|
||||
}
|
||||
%consul-intention-fieldsets .radio-card {
|
||||
@extend %radio-card-with-icon;
|
||||
}
|
||||
%consul-intention-fieldsets .radio-card header > * {
|
||||
display: inline;
|
||||
}
|
||||
%consul-intention-fieldsets .value-allow > :last-child::before {
|
||||
@extend %with-arrow-right-color-icon, %as-pseudo;
|
||||
}
|
||||
%consul-intention-fieldsets .value-deny > :last-child::before {
|
||||
@extend %with-deny-color-icon, %as-pseudo;
|
||||
}
|
||||
%consul-intention-fieldsets .radio-card header span.code::before {
|
||||
@extend %with-deny-color-icon, %as-pseudo;
|
||||
margin-right: 5px;
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
@import './consul-intention-list/index';
|
||||
.consul-intention-list {
|
||||
@extend %consul-intention-list;
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
@import './skin';
|
||||
@import './layout';
|
||||
|
||||
%consul-intention-list td.source,
|
||||
%consul-intention-list td.destination {
|
||||
@extend %tbody-th;
|
||||
}
|
||||
%consul-intention-list td strong {
|
||||
@extend %pill-700;
|
||||
}
|
||||
%consul-intention-list td.intent-allow strong {
|
||||
@extend %pill-allow;
|
||||
}
|
||||
%consul-intention-list td.intent-deny strong {
|
||||
@extend %pill-deny;
|
||||
}
|
||||
%consul-intention-list td.intent-l7-rules strong {
|
||||
@extend %pill-l7;
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
%consul-intention-list td {
|
||||
height: 59px;
|
||||
}
|
||||
%consul-intention-list tr > *:nth-child(1) {
|
||||
width: calc(30% - 50px);
|
||||
}
|
||||
%consul-intention-list tr > *:nth-child(2) {
|
||||
width: 100px;
|
||||
}
|
||||
%consul-intention-list tr > *:nth-child(3) {
|
||||
width: calc(30% - 50px);
|
||||
}
|
||||
%consul-intention-list tr > *:nth-child(4) {
|
||||
width: calc(40% - 220px);
|
||||
}
|
||||
%consul-intention-list tr > *:nth-child(5) {
|
||||
width: 160px;
|
||||
}
|
||||
%consul-intention-list tr > *:last-child {
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
@media #{$--lt-horizontal-nav} {
|
||||
%consul-intention-list tr > :not(.source):not(.destination):not(.intent) {
|
||||
display: none;
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
%consul-intention-list td.permissions {
|
||||
color: $blue-500;
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
@import './consul-intention-permission-list/index';
|
||||
.consul-intention-permission-list {
|
||||
@extend %consul-intention-permission-list;
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
@import './skin';
|
||||
@import './layout';
|
||||
%consul-intention-permission-list > ul > li {
|
||||
@extend %list-row, %list-row-detail;
|
||||
}
|
||||
%consul-intention-permission-list strong {
|
||||
@extend %pill-500;
|
||||
}
|
||||
%consul-intention-permission-list .intent-allow {
|
||||
@extend %pill-allow;
|
||||
}
|
||||
%consul-intention-permission-list .intent-deny {
|
||||
@extend %pill-deny;
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
%consul-intention-permission-list strong {
|
||||
margin-right: 8px;
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
%consul-intention-permission-list dt::before {
|
||||
@extend %with-glyph-icon;
|
||||
}
|
||||
%consul-intention-permission-list dl.route-path dt::before {
|
||||
content: 'P';
|
||||
}
|
||||
%consul-intention-permission-list dl.route-header dt::before {
|
||||
content: 'H';
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
%flash-message {
|
||||
display: flex;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
z-index: 6;
|
||||
justify-content: center;
|
||||
margin: 0 15%;
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ label span {
|
|||
%main-content form {
|
||||
@extend %form;
|
||||
}
|
||||
%form span.label {
|
||||
span.label {
|
||||
@extend %form-element-label;
|
||||
}
|
||||
%form table,
|
||||
|
|
|
@ -6,13 +6,13 @@
|
|||
display: inline-flex;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
%icon-definition > dt {
|
||||
%icon-definition > * {
|
||||
align-self: center;
|
||||
}
|
||||
%icon-definition > dd {
|
||||
white-space: nowrap;
|
||||
margin-left: 4px;
|
||||
}
|
||||
%icon-definition > dt > * {
|
||||
display: none;
|
||||
}
|
||||
%icon-definition > dd {
|
||||
margin-left: 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
|
@ -1,13 +1,21 @@
|
|||
.list-collection {
|
||||
@extend %list-collection;
|
||||
}
|
||||
%list-collection {
|
||||
.list-collection-scroll-virtual {
|
||||
@extend %list-collection-scroll-virtual;
|
||||
}
|
||||
%list-collection-scroll-virtual {
|
||||
height: 500px;
|
||||
position: relative;
|
||||
}
|
||||
%list-collection > ul {
|
||||
border-top: 1px solid $gray-200;
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
%list-collection > ul > li {
|
||||
position: relative;
|
||||
}
|
||||
%list-collection-scroll-virtual > ul {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
%list-collection > ul > li:nth-child(2) .with-feedback p {
|
||||
bottom: auto;
|
||||
|
|
|
@ -1,20 +1,13 @@
|
|||
import { get, set, computed } from '@ember/object';
|
||||
import Changeset from 'ember-changeset';
|
||||
import { get, set } from '@ember/object';
|
||||
import { Changeset as createChangeset } from 'ember-changeset';
|
||||
import Changeset from 'consul-ui/utils/form/changeset';
|
||||
import lookupValidator from 'ember-changeset-validations';
|
||||
|
||||
// Keep these here for now so forms are easy to make
|
||||
// TODO: Probably move this to utils/form/parse-element-name
|
||||
import parseElementName from 'consul-ui/utils/get-form-name-property';
|
||||
// TODO: Currently supporting ember-data nicely like this
|
||||
// Unfortunately since post-ember 2.18, the only way to get this to work
|
||||
// is to hang stuff off the prototype (which then makes it available everywhere)
|
||||
// we should either try and figure out another way of doing this, or move this code
|
||||
// somewhere where it is more 'global' like an initializer
|
||||
Changeset.prototype.isSaving = computed('data.isSaving', function() {
|
||||
return this.data.isSaving;
|
||||
});
|
||||
const defaultChangeset = function(data, validators) {
|
||||
return new Changeset(data, lookupValidator(validators), validators);
|
||||
return createChangeset(data, lookupValidator(validators), validators, { changeset: Changeset });
|
||||
};
|
||||
/**
|
||||
* Form builder/Form factory
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
import { get } from '@ember/object';
|
||||
import { EmberChangeset as Changeset } from 'ember-changeset';
|
||||
const CHANGES = '_changes';
|
||||
export default class extends Changeset {
|
||||
pushObject(prop, value) {
|
||||
let val;
|
||||
if (typeof get(this, `${CHANGES}.${prop}`) === 'undefined') {
|
||||
val = get(this, `data.${prop}`);
|
||||
if (!val) {
|
||||
val = [];
|
||||
} else {
|
||||
val = val.toArray();
|
||||
}
|
||||
} else {
|
||||
val = this.get(prop).slice(0);
|
||||
}
|
||||
val.push(value);
|
||||
this.set(`${prop}`, val);
|
||||
}
|
||||
removeObject(prop, value) {
|
||||
let val;
|
||||
if (typeof get(this, `${CHANGES}.${prop}`) === 'undefined') {
|
||||
val = get(this, `data.${prop}`);
|
||||
if (typeof val === 'undefined') {
|
||||
val = [];
|
||||
} else {
|
||||
val = val.toArray();
|
||||
}
|
||||
} else {
|
||||
val = this.get(prop).slice(0);
|
||||
}
|
||||
const pos = val.indexOf(value);
|
||||
if (pos !== -1) {
|
||||
val.splice(pos, 1);
|
||||
}
|
||||
this.set(`${prop}`, val);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import { validatePresence } from 'ember-changeset-validations/validators';
|
||||
import validateSometimes from 'ember-changeset-conditional-validations/validators/sometimes';
|
||||
export default schema => ({
|
||||
Name: [validatePresence(true)],
|
||||
Value: validateSometimes([validatePresence(true)], function() {
|
||||
return this.get('HeaderType') !== 'Present';
|
||||
}),
|
||||
});
|
|
@ -0,0 +1,29 @@
|
|||
import {
|
||||
validateInclusion,
|
||||
validatePresence,
|
||||
validateFormat,
|
||||
} from 'ember-changeset-validations/validators';
|
||||
import validateSometimes from 'ember-changeset-conditional-validations/validators/sometimes';
|
||||
|
||||
const name = 'intention-permission';
|
||||
export default schema => ({
|
||||
'*': validateSometimes([validatePresence(true)], function() {
|
||||
const methods = this.get('HTTP.Methods') || [];
|
||||
const headers = this.get('HTTP.Header') || [];
|
||||
const pathType = this.get('HTTP.PathType') || 'NoPath';
|
||||
const path = this.get('HTTP.Path') || '';
|
||||
const isValid = [
|
||||
methods.length !== 0,
|
||||
headers.length !== 0,
|
||||
pathType !== 'NoPath' && path !== '',
|
||||
].includes(true);
|
||||
return !isValid;
|
||||
}),
|
||||
Action: [validateInclusion({ in: schema[name].Action.allowedValues })],
|
||||
HTTP: {
|
||||
Path: validateSometimes([validateFormat({ regex: /^\// })], function() {
|
||||
const pathType = this.get('HTTP.PathType');
|
||||
return typeof pathType !== 'undefined' && pathType !== 'NoPath';
|
||||
}),
|
||||
},
|
||||
});
|
|
@ -1,6 +1,19 @@
|
|||
import { validatePresence, validateLength } from 'ember-changeset-validations/validators';
|
||||
import validateSometimes from 'ember-changeset-conditional-validations/validators/sometimes';
|
||||
export default {
|
||||
'*': validateSometimes([validatePresence(true)], function() {
|
||||
const action = this.get('Action') || '';
|
||||
const permissions = this.get('Permissions') || [];
|
||||
if (action === '' && permissions.length === 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
SourceName: [validatePresence(true), validateLength({ min: 1 })],
|
||||
DestinationName: [validatePresence(true), validateLength({ min: 1 })],
|
||||
Action: validatePresence(true),
|
||||
Permissions: [
|
||||
validateSometimes([validateLength({ min: 1 })], function(changes, content) {
|
||||
return !this.get('Action');
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
|
|
@ -68,7 +68,8 @@
|
|||
"css.escape": "^1.5.1",
|
||||
"dart-sass": "^1.25.0",
|
||||
"ember-auto-import": "^1.5.3",
|
||||
"ember-changeset-validations": "^3.0.2",
|
||||
"ember-changeset-conditional-validations": "^0.6.0",
|
||||
"ember-changeset-validations": "^3.9.0",
|
||||
"ember-cli": "~3.20.2",
|
||||
"ember-cli-app-version": "^3.2.0",
|
||||
"ember-cli-autoprefixer": "^0.8.1",
|
||||
|
|
|
@ -33,7 +33,7 @@ Feature: dc / intentions / create: Intention Create
|
|||
# Specifically set deny
|
||||
And I click "[value=deny]"
|
||||
And I submit
|
||||
Then a POST request was made to "/v1/connect/intentions?dc=datacenter" from yaml
|
||||
Then a PUT request was made to "/v1/connect/intentions/exact?source=default%2Fweb&destination=default%2Fdb&dc=datacenter" from yaml
|
||||
---
|
||||
body:
|
||||
SourceName: web
|
||||
|
|
|
@ -2,13 +2,16 @@
|
|||
Feature: dc / intentions / deleting: Deleting items with confirmations, success and error notifications
|
||||
Background:
|
||||
Given 1 datacenter model with the value "datacenter"
|
||||
Scenario: Deleting a intention model from the intention listing page
|
||||
Given 1 intention model from yaml
|
||||
And 1 intention model from yaml
|
||||
---
|
||||
SourceNS: default
|
||||
SourceName: name
|
||||
DestinationNS: default
|
||||
DestinationName: destination
|
||||
ID: ee52203d-989f-4f7a-ab5a-2bef004164ca
|
||||
Meta: ~
|
||||
---
|
||||
Scenario: Deleting a intention model from the intention listing page
|
||||
When I visit the intentions page for yaml
|
||||
---
|
||||
dc: datacenter
|
||||
|
@ -16,7 +19,7 @@ Feature: dc / intentions / deleting: Deleting items with confirmations, success
|
|||
And I click actions on the intentions
|
||||
And I click delete on the intentions
|
||||
And I click confirmDelete on the intentions
|
||||
Then a DELETE request was made to "/v1/connect/intentions/ee52203d-989f-4f7a-ab5a-2bef004164ca?dc=datacenter"
|
||||
Then a DELETE request was made to "/v1/connect/intentions/exact?source=default%2Fname&destination=default%2Fdestination&dc=datacenter"
|
||||
And "[data-notification]" has the "notification-delete" class
|
||||
And "[data-notification]" has the "success" class
|
||||
Scenario: Deleting an intention from the intention detail page
|
||||
|
@ -27,7 +30,7 @@ Feature: dc / intentions / deleting: Deleting items with confirmations, success
|
|||
---
|
||||
And I click delete
|
||||
And I click confirmDelete
|
||||
Then a DELETE request was made to "/v1/connect/intentions/ee52203d-989f-4f7a-ab5a-2bef004164ca?dc=datacenter"
|
||||
Then a DELETE request was made to "/v1/connect/intentions/exact?source=default%2Fname&destination=default%2Fdestination&dc=datacenter"
|
||||
And "[data-notification]" has the "notification-delete" class
|
||||
And "[data-notification]" has the "success" class
|
||||
Scenario: Deleting an intention from the intention detail page and getting an error
|
||||
|
@ -36,7 +39,7 @@ Feature: dc / intentions / deleting: Deleting items with confirmations, success
|
|||
dc: datacenter
|
||||
intention: ee52203d-989f-4f7a-ab5a-2bef004164ca
|
||||
---
|
||||
Given the url "/v1/connect/intentions/ee52203d-989f-4f7a-ab5a-2bef004164ca?dc=datacenter" responds with a 500 status
|
||||
Given the url "/v1/connect/intentions/exact?source=default%2Fname&destination=default%2Fdestination&dc=datacenter" responds with a 500 status
|
||||
And I click delete
|
||||
And I click confirmDelete
|
||||
And "[data-notification]" has the "notification-update" class
|
||||
|
@ -47,7 +50,7 @@ Feature: dc / intentions / deleting: Deleting items with confirmations, success
|
|||
dc: datacenter
|
||||
intention: ee52203d-989f-4f7a-ab5a-2bef004164ca
|
||||
---
|
||||
Given the url "/v1/connect/intentions/ee52203d-989f-4f7a-ab5a-2bef004164ca?dc=datacenter" responds with from yaml
|
||||
Given the url "/v1/connect/intentions/exact?source=default%2Fname&destination=default%2Fdestination&dc=datacenter" responds with from yaml
|
||||
---
|
||||
status: 500
|
||||
body: "duplicate intention found:"
|
||||
|
|
|
@ -4,6 +4,10 @@ Feature: dc / intentions / update: Intention Update
|
|||
Given 1 datacenter model with the value "datacenter"
|
||||
And 1 intention model from yaml
|
||||
---
|
||||
SourceNS: default
|
||||
SourceName: web
|
||||
DestinationNS: default
|
||||
DestinationName: db
|
||||
ID: intention-id
|
||||
---
|
||||
When I visit the intention page for yaml
|
||||
|
@ -20,7 +24,7 @@ Feature: dc / intentions / update: Intention Update
|
|||
---
|
||||
And I click "[value=[Action]]"
|
||||
And I submit
|
||||
Then a PUT request was made to "/v1/connect/intentions/intention-id?dc=datacenter" with the body from yaml
|
||||
Then a PUT request was made to "/v1/connect/intentions/exact?source=default%2Fweb&destination=default%2Fdb&dc=datacenter" from yaml
|
||||
---
|
||||
Description: [Description]
|
||||
Action: [Action]
|
||||
|
@ -35,7 +39,7 @@ Feature: dc / intentions / update: Intention Update
|
|||
| Desc | allow |
|
||||
------------------------------
|
||||
Scenario: There was an error saving the intention
|
||||
Given the url "/v1/connect/intentions/intention-id" responds with a 500 status
|
||||
Given the url "/v1/connect/intentions/exact?source=default%2Fweb&destination=default%2Fdb&dc=datacenter" responds with a 500 status
|
||||
And I submit
|
||||
Then the url should be /datacenter/intentions/intention-id
|
||||
Then "[data-notification]" has the "notification-update" class
|
||||
|
|
|
@ -15,6 +15,11 @@ Feature: dc / services / show / intentions: Intentions per service
|
|||
- ID: 755b72bd-f5ab-4c92-90cc-bed0e7d8e9f0
|
||||
Action: allow
|
||||
Meta: ~
|
||||
SourceNS: default
|
||||
SourceName: name
|
||||
DestinationNS: default
|
||||
DestinationName: destination
|
||||
|
||||
- ID: 755b72bd-f5ab-4c92-90cc-bed0e7d8e9f1
|
||||
Action: deny
|
||||
Meta: ~
|
||||
|
@ -37,6 +42,6 @@ Feature: dc / services / show / intentions: Intentions per service
|
|||
And I click actions on the intentions
|
||||
And I click delete on the intentions
|
||||
And I click confirmDelete on the intentions
|
||||
Then a DELETE request was made to "/v1/connect/intentions/755b72bd-f5ab-4c92-90cc-bed0e7d8e9f0?dc=dc1"
|
||||
Then a DELETE request was made to "/v1/connect/intentions/exact?source=default%2Fname&destination=default%2Fdestination&dc=dc1"
|
||||
And "[data-notification]" has the "notification-delete" class
|
||||
And "[data-notification]" has the "success" class
|
||||
|
|
|
@ -3,7 +3,6 @@ import { setupTest } from 'ember-qunit';
|
|||
module('Integration | Adapter | intention', function(hooks) {
|
||||
setupTest(hooks);
|
||||
const dc = 'dc-1';
|
||||
const legacyId = 'intention-name';
|
||||
const id = 'SourceNS:SourceName:DestinationNS:DestinationName';
|
||||
test('requestForQuery returns the correct url', function(assert) {
|
||||
const adapter = this.owner.lookup('adapter:intention');
|
||||
|
@ -38,14 +37,17 @@ module('Integration | Adapter | intention', function(hooks) {
|
|||
test('requestForCreateRecord returns the correct url', function(assert) {
|
||||
const adapter = this.owner.lookup('adapter:intention');
|
||||
const client = this.owner.lookup('service:client/http');
|
||||
const expected = `POST /v1/connect/intentions?dc=${dc}`;
|
||||
const expected = `PUT /v1/connect/intentions/exact?source=SourceNS%2FSourceName&destination=DestinationNS%2FDestinationName&dc=${dc}`;
|
||||
const actual = adapter
|
||||
.requestForCreateRecord(
|
||||
client.url,
|
||||
{},
|
||||
{
|
||||
Datacenter: dc,
|
||||
ID: id,
|
||||
SourceNS: 'SourceNS',
|
||||
SourceName: 'SourceName',
|
||||
DestinationNS: 'DestinationNS',
|
||||
DestinationName: 'DestinationName',
|
||||
}
|
||||
)
|
||||
.split('\n')[0];
|
||||
|
@ -54,15 +56,17 @@ module('Integration | Adapter | intention', function(hooks) {
|
|||
test('requestForUpdateRecord returns the correct url', function(assert) {
|
||||
const adapter = this.owner.lookup('adapter:intention');
|
||||
const client = this.owner.lookup('service:client/http');
|
||||
const expected = `PUT /v1/connect/intentions/${legacyId}?dc=${dc}`;
|
||||
const expected = `PUT /v1/connect/intentions/exact?source=SourceNS%2FSourceName&destination=DestinationNS%2FDestinationName&dc=${dc}`;
|
||||
const actual = adapter
|
||||
.requestForUpdateRecord(
|
||||
client.url,
|
||||
{},
|
||||
{
|
||||
Datacenter: dc,
|
||||
ID: id,
|
||||
LegacyID: legacyId,
|
||||
SourceNS: 'SourceNS',
|
||||
SourceName: 'SourceName',
|
||||
DestinationNS: 'DestinationNS',
|
||||
DestinationName: 'DestinationName',
|
||||
}
|
||||
)
|
||||
.split('\n')[0];
|
||||
|
@ -71,15 +75,17 @@ module('Integration | Adapter | intention', function(hooks) {
|
|||
test('requestForDeleteRecord returns the correct url', function(assert) {
|
||||
const adapter = this.owner.lookup('adapter:intention');
|
||||
const client = this.owner.lookup('service:client/http');
|
||||
const expected = `DELETE /v1/connect/intentions/${legacyId}?dc=${dc}`;
|
||||
const expected = `DELETE /v1/connect/intentions/exact?source=SourceNS%2FSourceName&destination=DestinationNS%2FDestinationName&dc=${dc}`;
|
||||
const actual = adapter
|
||||
.requestForDeleteRecord(
|
||||
client.url,
|
||||
{},
|
||||
{
|
||||
Datacenter: dc,
|
||||
ID: id,
|
||||
LegacyID: legacyId,
|
||||
SourceNS: 'SourceNS',
|
||||
SourceName: 'SourceName',
|
||||
DestinationNS: 'DestinationNS',
|
||||
DestinationName: 'DestinationName',
|
||||
}
|
||||
)
|
||||
.split('\n')[0];
|
||||
|
|
100
ui-v2/yarn.lock
100
ui-v2/yarn.lock
|
@ -1487,6 +1487,14 @@
|
|||
"@glimmer/env" "^0.1.7"
|
||||
"@glimmer/validator" "^0.44.0"
|
||||
|
||||
"@glimmer/tracking@^1.0.1":
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@glimmer/tracking/-/tracking-1.0.2.tgz#4fe9ca89e3f4a2ae8e37c8bd8e4ea0d886d9abbf"
|
||||
integrity sha512-9Vp04TM2IDTShGFdxccfvnmcaj4NwqLrwbOXm4iju5KL/WkeB8mqoCSLtO3kUg+80DqU0pKE8tR460lQP8CutA==
|
||||
dependencies:
|
||||
"@glimmer/env" "^0.1.7"
|
||||
"@glimmer/validator" "^0.44.0"
|
||||
|
||||
"@glimmer/util@^0.44.0":
|
||||
version "0.44.0"
|
||||
resolved "https://registry.yarnpkg.com/@glimmer/util/-/util-0.44.0.tgz#45df98d73812440206ae7bda87cfe04aaae21ed9"
|
||||
|
@ -1520,14 +1528,14 @@
|
|||
js-yaml "^3.13.1"
|
||||
|
||||
"@hashicorp/consul-api-double@^5.0.0":
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@hashicorp/consul-api-double/-/consul-api-double-5.0.1.tgz#07880706ab26cc242332cef86b2c03b3b4ec4e56"
|
||||
integrity sha512-uptXq/XTGL5uzGqvwRqC0tzHKCJMVAaRMucPxjbMb4r9wOmOdT4Z2BUJD8GDcCSFIWE8hbWeqAlCXRrokZ3wbw==
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@hashicorp/consul-api-double/-/consul-api-double-5.2.1.tgz#a411139fa1afa0dfaf1b9973f21275530a39939b"
|
||||
integrity sha512-ASQv2I8iprnFmpAvbHEoKE8MXTpOxdeBan6nkgobmz4OyvMcqu/h29CGEXZ9j63NX6+nxmE84nV5yAqADRubGQ==
|
||||
|
||||
"@hashicorp/ember-cli-api-double@^3.1.0":
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@hashicorp/ember-cli-api-double/-/ember-cli-api-double-3.1.1.tgz#ba16a514131ce409054d1ae1a71483941d937d37"
|
||||
integrity sha512-VLvV/m+Sx+vG+tHK1FeVjiBXwt8KcIWqgFavglrEBTkVTA2o7uP0xN9nKOJjos49KK+h1K3fCwMK5ltz7Kt97w==
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@hashicorp/ember-cli-api-double/-/ember-cli-api-double-3.1.2.tgz#0eaee116da1431ed63e55eea9ff9c28028cf9f8c"
|
||||
integrity sha512-4j4JxIHBeo5KjTfcEAIrzjtiWBTxPzTTBMiigNgKAIWAtO6Hz58LQ6kDJl8MN52kSq2uSBlFJpp6aQhfwJaPtw==
|
||||
dependencies:
|
||||
"@hashicorp/api-double" "^1.6.1"
|
||||
array-range "^1.0.1"
|
||||
|
@ -2632,6 +2640,11 @@ babel-plugin-htmlbars-inline-precompile@^3.0.1:
|
|||
resolved "https://registry.yarnpkg.com/babel-plugin-htmlbars-inline-precompile/-/babel-plugin-htmlbars-inline-precompile-3.1.0.tgz#85085b50385277f2b331ebd54e22fa91aadc24e8"
|
||||
integrity sha512-ar6c4YVX6OV7Dzpq7xRyllQrHwVEzJf41qysYULnD6tu6TS+y1FxT2VcEvMC6b9Rq9xoHMzvB79HO3av89JCGg==
|
||||
|
||||
babel-plugin-htmlbars-inline-precompile@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-plugin-htmlbars-inline-precompile/-/babel-plugin-htmlbars-inline-precompile-3.2.0.tgz#c4882ea875d0f5683f0d91c1f72e29a4f14b5606"
|
||||
integrity sha512-IUeZmgs9tMUGXYu1vfke5I18yYJFldFGdNFQOWslXTnDWXzpwPih7QFduUqvT+awDpDuNtXpdt5JAf43Q1Hhzg==
|
||||
|
||||
babel-plugin-htmlbars-inline-precompile@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-plugin-htmlbars-inline-precompile/-/babel-plugin-htmlbars-inline-precompile-4.1.0.tgz#11796422e65d900a968481fa3fb37e0425c928dd"
|
||||
|
@ -4151,9 +4164,9 @@ caniuse-lite@^1.0.30000792, caniuse-lite@^1.0.30000805, caniuse-lite@^1.0.300010
|
|||
integrity sha512-PFTw9UyVfbkcMEFs82q8XVlRayj7HKvnhu5BLcmjGpv+SNyiWasCcWXPGJuO0rK0dhLRDJmtZcJ+LHUfypbw1w==
|
||||
|
||||
caniuse-lite@^1.0.30000844:
|
||||
version "1.0.30001125"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001125.tgz#2a1a51ee045a0a2207474b086f628c34725e997b"
|
||||
integrity sha512-9f+r7BW8Qli917mU3j0fUaTweT3f3vnX/Lcs+1C73V+RADmFme+Ih0Br8vONQi3X0lseOe6ZHfsZLCA8MSjxUA==
|
||||
version "1.0.30001137"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001137.tgz#6f0127b1d3788742561a25af3607a17fc778b803"
|
||||
integrity sha512-54xKQZTqZrKVHmVz0+UvdZR6kQc7pJDgfhsMYDG19ID1BWoNnDMFm5Q3uSBSU401pBvKYMsHAt9qhEDcxmk8aw==
|
||||
|
||||
capture-exit@^2.0.0:
|
||||
version "2.0.0"
|
||||
|
@ -5204,9 +5217,9 @@ electron-to-chromium@^1.3.30, electron-to-chromium@^1.3.488:
|
|||
integrity sha512-EOZuaDT3L1sCIMAVN5J0nGuGWVq5dThrdl0d8XeDYf4MOzbXqZ19OLKesN8TZj0RxtpYjqHpiw/fR6BKWdMwYA==
|
||||
|
||||
electron-to-chromium@^1.3.47:
|
||||
version "1.3.565"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.565.tgz#8511797ab2b66b767e1aef4eb17d636bf01a2c72"
|
||||
integrity sha512-me5dGlHFd8Q7mKhqbWRLIYnKjw4i0fO6hmW0JBxa7tM87fBfNEjWokRnDF7V+Qme/9IYpwhfMn+soWs40tXWqg==
|
||||
version "1.3.575"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.575.tgz#57065cfad7b977a817dba47b28e7eb4dbce3fc37"
|
||||
integrity sha512-031VrjcilnE8bXivDGhEeuGjMZrjTAeyAKm3XWPY9SvGYE6Hn8003gCqoNszFu6lh1v0gDx5hrM0VE1cPSMUkQ==
|
||||
|
||||
elliptic@^6.0.0, elliptic@^6.5.2:
|
||||
version "6.5.3"
|
||||
|
@ -5286,26 +5299,33 @@ ember-basic-dropdown@^3.0.3:
|
|||
ember-maybe-in-element "^0.4.0"
|
||||
ember-truth-helpers "2.1.0"
|
||||
|
||||
ember-changeset-validations@^3.0.2:
|
||||
version "3.7.0"
|
||||
resolved "https://registry.yarnpkg.com/ember-changeset-validations/-/ember-changeset-validations-3.7.0.tgz#74875705128f56ffe22bf54fe6856838dc9caa7d"
|
||||
integrity sha512-E4Um4IV5UO72FdT1GWcI8EbNOHE7eS16XFokk6UaKpXs3MLuI1Ev6Zb8Q9Z20g/9/IieBI4XddnGd+1FpXz6aA==
|
||||
ember-changeset-conditional-validations@^0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/ember-changeset-conditional-validations/-/ember-changeset-conditional-validations-0.6.0.tgz#78369ad3af0aea338e00a9bdf1b622fb512d9a00"
|
||||
integrity sha512-U9TZFhhLC+5XRqcI5sNfJwGVcVZvXJxwrRQrrTYLImHe/+tcgP/TagE0f0DBrgWV2u3lsztGHGwGUs86uc65rg==
|
||||
dependencies:
|
||||
ember-changeset "^3.7.0"
|
||||
ember-cli-babel "^6.16.0"
|
||||
|
||||
ember-changeset-validations@^3.9.0:
|
||||
version "3.9.1"
|
||||
resolved "https://registry.yarnpkg.com/ember-changeset-validations/-/ember-changeset-validations-3.9.1.tgz#5f44f56ca9a55ac079f667f8cbead1dfeb85a21d"
|
||||
integrity sha512-aTufViqh4zx8WNjiuxuQNSfYaDLDdXl7mQ6g18z2Wma55kEmGymxaltM3lrGgXK+IlWoJvINrNBd6i6AYRxaYA==
|
||||
dependencies:
|
||||
ember-changeset "^3.9.1"
|
||||
ember-cli-babel "^7.8.0"
|
||||
ember-cli-htmlbars "^4.0.5"
|
||||
ember-get-config "^0.2.4"
|
||||
ember-validators "^2.0.0"
|
||||
|
||||
ember-changeset@^3.7.0:
|
||||
version "3.7.1"
|
||||
resolved "https://registry.yarnpkg.com/ember-changeset/-/ember-changeset-3.7.1.tgz#5826e2bb7151f85494208aedac25a00400bddf13"
|
||||
integrity sha512-vYkF9LHoFwQJb9yytfbA9J888a82/4i+NQUeYtyuLtoD4Zty6Z1NzZ5eWwVU/RbsbK45x85T5FKmsxSS1/1cgw==
|
||||
ember-changeset@^3.9.1:
|
||||
version "3.9.1"
|
||||
resolved "https://registry.yarnpkg.com/ember-changeset/-/ember-changeset-3.9.1.tgz#53b50be95364a71f38e68a01c33eba12808cd8a4"
|
||||
integrity sha512-Ntf0fITb2klRZF+5s5xbBQ6HNuSn1IbwppyZPU8v7oh26QOlfjyxYE7QNWM3nZnybHkCrF1iSqN3s8xj3OoFCg==
|
||||
dependencies:
|
||||
"@glimmer/tracking" "^1.0.0"
|
||||
"@glimmer/tracking" "^1.0.1"
|
||||
ember-auto-import "^1.5.2"
|
||||
ember-cli-babel "^7.19.0"
|
||||
validated-changeset "~0.7.1"
|
||||
validated-changeset "~0.9.1"
|
||||
|
||||
ember-cli-app-version@^3.2.0:
|
||||
version "3.2.0"
|
||||
|
@ -5328,7 +5348,7 @@ ember-cli-babel-plugin-helpers@^1.0.0, ember-cli-babel-plugin-helpers@^1.1.0:
|
|||
resolved "https://registry.yarnpkg.com/ember-cli-babel-plugin-helpers/-/ember-cli-babel-plugin-helpers-1.1.0.tgz#de3baedd093163b6c2461f95964888c1676325ac"
|
||||
integrity sha512-Zr4my8Xn+CzO0gIuFNXji0eTRml5AxZUTDQz/wsNJ5AJAtyFWCY4QtKdoELNNbiCVGt1lq5yLiwTm4scGKu6xA==
|
||||
|
||||
ember-cli-babel@7:
|
||||
ember-cli-babel@7, ember-cli-babel@^7.8.0:
|
||||
version "7.22.1"
|
||||
resolved "https://registry.yarnpkg.com/ember-cli-babel/-/ember-cli-babel-7.22.1.tgz#cad28b89cf0e184c93b863d09bc5ba4ce1d2e453"
|
||||
integrity sha512-kCT8WbC1AYFtyOpU23ESm22a+gL6fWv8Nzwe8QFQ5u0piJzM9MEudfbjADEaoyKTrjMQTDsrWwEf3yjggDsOng==
|
||||
|
@ -5379,7 +5399,7 @@ ember-cli-babel@^6.0.0, ember-cli-babel@^6.0.0-beta.4, ember-cli-babel@^6.11.0,
|
|||
ember-cli-version-checker "^2.1.2"
|
||||
semver "^5.5.0"
|
||||
|
||||
ember-cli-babel@^7.1.0, ember-cli-babel@^7.1.2, ember-cli-babel@^7.1.3, ember-cli-babel@^7.10.0, ember-cli-babel@^7.11.0, ember-cli-babel@^7.11.1, ember-cli-babel@^7.12.0, ember-cli-babel@^7.13.0, ember-cli-babel@^7.17.2, ember-cli-babel@^7.18.0, ember-cli-babel@^7.19.0, ember-cli-babel@^7.20.0, ember-cli-babel@^7.20.5, ember-cli-babel@^7.21.0, ember-cli-babel@^7.7.3, ember-cli-babel@^7.8.0:
|
||||
ember-cli-babel@^7.1.0, ember-cli-babel@^7.1.2, ember-cli-babel@^7.1.3, ember-cli-babel@^7.10.0, ember-cli-babel@^7.11.0, ember-cli-babel@^7.11.1, ember-cli-babel@^7.12.0, ember-cli-babel@^7.13.0, ember-cli-babel@^7.17.2, ember-cli-babel@^7.18.0, ember-cli-babel@^7.19.0, ember-cli-babel@^7.20.0, ember-cli-babel@^7.20.5, ember-cli-babel@^7.21.0, ember-cli-babel@^7.7.3:
|
||||
version "7.21.0"
|
||||
resolved "https://registry.yarnpkg.com/ember-cli-babel/-/ember-cli-babel-7.21.0.tgz#c79e888876aee87dfc3260aee7cb580b74264bbc"
|
||||
integrity sha512-jHVi9melAibo0DrAG3GAxid+29xEyjBoU53652B4qcu3Xp58feZGTH/JGXovH7TjvbeNn65zgNyoV3bk1onULw==
|
||||
|
@ -5505,7 +5525,27 @@ ember-cli-htmlbars@^3.0.0, ember-cli-htmlbars@^3.0.1:
|
|||
json-stable-stringify "^1.0.1"
|
||||
strip-bom "^3.0.0"
|
||||
|
||||
ember-cli-htmlbars@^4.0.5, ember-cli-htmlbars@^4.2.0, ember-cli-htmlbars@^4.2.2, ember-cli-htmlbars@^4.3.1:
|
||||
ember-cli-htmlbars@^4.0.5:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/ember-cli-htmlbars/-/ember-cli-htmlbars-4.4.0.tgz#7ca17d5ca8f7550984346d9e6e93da0c3323f8d9"
|
||||
integrity sha512-ohgctqk7dXIZR4TgN0xRoUYltWhghFJgqmtuswQTpZ7p74RxI9PKx+E8WV/95mGcPzraesvMNBg5utQNvcqgNg==
|
||||
dependencies:
|
||||
"@ember/edition-utils" "^1.2.0"
|
||||
babel-plugin-htmlbars-inline-precompile "^3.2.0"
|
||||
broccoli-debug "^0.6.5"
|
||||
broccoli-persistent-filter "^2.3.1"
|
||||
broccoli-plugin "^3.1.0"
|
||||
common-tags "^1.8.0"
|
||||
ember-cli-babel-plugin-helpers "^1.1.0"
|
||||
fs-tree-diff "^2.0.1"
|
||||
hash-for-dep "^1.5.1"
|
||||
heimdalljs-logger "^0.1.10"
|
||||
json-stable-stringify "^1.0.1"
|
||||
semver "^6.3.0"
|
||||
strip-bom "^4.0.0"
|
||||
walk-sync "^2.0.2"
|
||||
|
||||
ember-cli-htmlbars@^4.2.0, ember-cli-htmlbars@^4.2.2, ember-cli-htmlbars@^4.3.1:
|
||||
version "4.3.1"
|
||||
resolved "https://registry.yarnpkg.com/ember-cli-htmlbars/-/ember-cli-htmlbars-4.3.1.tgz#4af8adc21ab3c4953f768956b7f7d207782cb175"
|
||||
integrity sha512-CW6AY/yzjeVqoRtItOKj3hcYzc5dWPRETmeCzr2Iqjt5vxiVtpl0z5VTqHqIlT5fsFx6sGWBQXNHIe+ivYsxXQ==
|
||||
|
@ -12957,10 +12997,10 @@ validate-npm-package-name@^3.0.0:
|
|||
dependencies:
|
||||
builtins "^1.0.3"
|
||||
|
||||
validated-changeset@~0.7.1:
|
||||
version "0.7.1"
|
||||
resolved "https://registry.yarnpkg.com/validated-changeset/-/validated-changeset-0.7.1.tgz#cb2c11c93d5acfb2286e5bfca07f4a516f83c844"
|
||||
integrity sha512-BbFK98Cp7WunEwLOW/oAi6qDZZFBOLkae0q5RZ3ne8ZkwB1sskOYfF5IqQhqubwxRb4emMhA2UgN5rdfOaZxXQ==
|
||||
validated-changeset@~0.9.1:
|
||||
version "0.9.1"
|
||||
resolved "https://registry.yarnpkg.com/validated-changeset/-/validated-changeset-0.9.1.tgz#bb4773c7c5392dcc7ecfbc8ccc10c7fef2840d42"
|
||||
integrity sha512-gEMvF+GN8ECLndHw5Ehc9ckXMgM+RRDK5+lCx2hGX9Qie1q68ixLEtbNXbPPhV4JHq6d7krVfTG+sEQ3X6zoHA==
|
||||
|
||||
vary@~1.1.2:
|
||||
version "1.1.2"
|
||||
|
|
Loading…
Reference in New Issue