diff --git a/ui-v2/app/adapters/intention.js b/ui-v2/app/adapters/intention.js index 9f76f58be2..73553ba46a 100644 --- a/ui-v2/app/adapters/intention.js +++ b/ui-v2/app/adapters/intention.js @@ -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], + }} `; }, }); diff --git a/ui-v2/app/components/consul-intention-form/fieldsets/index.hbs b/ui-v2/app/components/consul-intention-form/fieldsets/index.hbs index 867b33d135..4a47457514 100644 --- a/ui-v2/app/components/consul-intention-form/fieldsets/index.hbs +++ b/ui-v2/app/components/consul-intention-form/fieldsets/index.hbs @@ -92,7 +92,9 @@ {{nspace.Name}} {{/if}} + {{#if create}} For the destination, you may choose any namespace for which you have access. + {{/if}} {{/if}} @@ -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|}} @@ -131,13 +138,10 @@ {{/each}} - -{{#if (not item.Legacy)}} -
+{{#if (eq (or item.Action '') '')}} +
+

Permissions

{{#if (gt item.Permissions.length 0) }}
@@ -148,17 +152,21 @@ documentation

- + {{else}}

- Add permissions via CLI + No permissions yet

- 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.

@@ -173,4 +181,47 @@ {{/if}}
{{/if}} +
+ +
+ +{{#if shouldShowPermissionForm}} + + +

Edit Permission

+
+ + + + + + + + + +
+{{/if}} + \ No newline at end of file diff --git a/ui-v2/app/components/consul-intention-form/fieldsets/index.js b/ui-v2/app/components/consul-intention-form/fieldsets/index.js index c09a8d717c..b2d001be57 100644 --- a/ui-v2/app/components/consul-intention-form/fieldsets/index.js +++ b/ui-v2/app/components/consul-intention-form/fieldsets/index.js @@ -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(); + } + }, }, }); diff --git a/ui-v2/app/styles/components/consul-intention-fieldsets/index.scss b/ui-v2/app/components/consul-intention-form/fieldsets/index.scss similarity index 100% rename from ui-v2/app/styles/components/consul-intention-fieldsets/index.scss rename to ui-v2/app/components/consul-intention-form/fieldsets/index.scss diff --git a/ui-v2/app/components/consul-intention-form/fieldsets/layout.scss b/ui-v2/app/components/consul-intention-form/fieldsets/layout.scss new file mode 100644 index 0000000000..70f18cfdf0 --- /dev/null +++ b/ui-v2/app/components/consul-intention-form/fieldsets/layout.scss @@ -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%; +} diff --git a/ui-v2/app/styles/components/consul-intention-fieldsets/skin.scss b/ui-v2/app/components/consul-intention-form/fieldsets/skin.scss similarity index 100% rename from ui-v2/app/styles/components/consul-intention-fieldsets/skin.scss rename to ui-v2/app/components/consul-intention-form/fieldsets/skin.scss diff --git a/ui-v2/app/components/consul-intention-list/index.scss b/ui-v2/app/components/consul-intention-list/index.scss new file mode 100644 index 0000000000..b083149c87 --- /dev/null +++ b/ui-v2/app/components/consul-intention-list/index.scss @@ -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; +} + diff --git a/ui-v2/app/components/consul-intention-list/layout.scss b/ui-v2/app/components/consul-intention-list/layout.scss new file mode 100644 index 0000000000..515bcbd525 --- /dev/null +++ b/ui-v2/app/components/consul-intention-list/layout.scss @@ -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; + } +} diff --git a/ui-v2/app/components/consul-intention-list/skin.scss b/ui-v2/app/components/consul-intention-list/skin.scss new file mode 100644 index 0000000000..fb67d3232c --- /dev/null +++ b/ui-v2/app/components/consul-intention-list/skin.scss @@ -0,0 +1,5 @@ +.consul-intention-list { + td.permissions { + color: $blue-500; + } +} diff --git a/ui-v2/app/components/consul-intention-permission-form/index.hbs b/ui-v2/app/components/consul-intention-permission-form/index.hbs new file mode 100644 index 0000000000..c5f6f46374 --- /dev/null +++ b/ui-v2/app/components/consul-intention-permission-form/index.hbs @@ -0,0 +1,135 @@ +
+ {{yield (hash + submit=(action 'submit' changeset) + reset=(action 'reset' changeset) + + isDirty=(and changeset.isValid) + changeset=changeset + )}} + +
+ + Should this permission allow the source connect to the destination? + +
+ {{#each intents as |intent|}} + + {{/each}} +
+
+ +
+
+

Path

+
+
+ +{{#if shouldShowPathField}} + +{{/if}} +
+
+ +
+

Methods

+
+ All methods are applied by default unless specified + +
+ +{{#if shouldShowMethods}} +
+ {{#each methods as |method|}} + + {{/each}} +
+{{/if}} +
+ +
+

Headers

+ + + + + + + + + + + +
+
\ No newline at end of file diff --git a/ui-v2/app/components/consul-intention-permission-form/index.js b/ui-v2/app/components/consul-intention-permission-form/index.js new file mode 100644 index 0000000000..d52c0f885d --- /dev/null +++ b/ui-v2/app/components/consul-intention-permission-form/index.js @@ -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); + }, + }, +}); diff --git a/ui-v2/app/components/consul-intention-permission-form/index.scss b/ui-v2/app/components/consul-intention-permission-form/index.scss new file mode 100644 index 0000000000..bc18252196 --- /dev/null +++ b/ui-v2/app/components/consul-intention-permission-form/index.scss @@ -0,0 +1,2 @@ +@import './skin'; +@import './layout'; diff --git a/ui-v2/app/components/consul-intention-permission-form/layout.scss b/ui-v2/app/components/consul-intention-permission-form/layout.scss new file mode 100644 index 0000000000..b68163f51f --- /dev/null +++ b/ui-v2/app/components/consul-intention-permission-form/layout.scss @@ -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; + } +} diff --git a/ui-v2/app/components/consul-intention-permission-form/skin.scss b/ui-v2/app/components/consul-intention-permission-form/skin.scss new file mode 100644 index 0000000000..8a3a0018e4 --- /dev/null +++ b/ui-v2/app/components/consul-intention-permission-form/skin.scss @@ -0,0 +1,6 @@ +.consul-intention-permission-form { + h2 { + border-top: 1px solid $blue-500; + } +} + diff --git a/ui-v2/app/components/consul-intention-permission-header-form/index.hbs b/ui-v2/app/components/consul-intention-permission-header-form/index.hbs new file mode 100644 index 0000000000..8d406dd5cb --- /dev/null +++ b/ui-v2/app/components/consul-intention-permission-header-form/index.hbs @@ -0,0 +1,57 @@ +
+ {{yield (hash + submit=(action 'submit' changeset) + reset=(action 'reset' changeset) + + isDirty=(and changeset.isValid changeset.isDirty) + changeset=changeset + )}} + +
+
+ + + + + {{#if shouldShowValueField}} + + {{/if}} + +
+
+
\ No newline at end of file diff --git a/ui-v2/app/components/consul-intention-permission-header-form/index.js b/ui-v2/app/components/consul-intention-permission-header-form/index.js new file mode 100644 index 0000000000..80c79a0f4c --- /dev/null +++ b/ui-v2/app/components/consul-intention-permission-header-form/index.js @@ -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(); + }, + }, +}); diff --git a/ui-v2/app/components/consul-intention-permission-header-list/index.hbs b/ui-v2/app/components/consul-intention-permission-header-list/index.hbs new file mode 100644 index 0000000000..13cf81d59d --- /dev/null +++ b/ui-v2/app/components/consul-intention-permission-header-list/index.hbs @@ -0,0 +1,45 @@ +{{#if (gt items.length 0)}} + + +
+
+ + Header + +
+
+ {{item.Name}} {{route-match item}} +
+
+
+ + + + + Delete + + + + + Confirm delete + + +

+ Are you sure you want to delete this header? +

+
+ + Delete + +
+
+
+
+
+
+{{/if}} \ No newline at end of file diff --git a/ui-v2/app/components/consul-intention-permission-header-list/index.js b/ui-v2/app/components/consul-intention-permission-header-list/index.js new file mode 100644 index 0000000000..4798652642 --- /dev/null +++ b/ui-v2/app/components/consul-intention-permission-header-list/index.js @@ -0,0 +1,5 @@ +import Component from '@ember/component'; + +export default Component.extend({ + tagName: '', +}); diff --git a/ui-v2/app/components/consul-intention-permission-header-list/index.scss b/ui-v2/app/components/consul-intention-permission-header-list/index.scss new file mode 100644 index 0000000000..d59989e40f --- /dev/null +++ b/ui-v2/app/components/consul-intention-permission-header-list/index.scss @@ -0,0 +1,8 @@ +@import './skin'; +@import './layout'; +.consul-intention-permission-header-list { + > ul > li { + @extend %list-row-200; + } +} + diff --git a/ui-v2/app/components/consul-intention-permission-header-list/layout.scss b/ui-v2/app/components/consul-intention-permission-header-list/layout.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ui-v2/app/components/consul-intention-permission-header-list/skin.scss b/ui-v2/app/components/consul-intention-permission-header-list/skin.scss new file mode 100644 index 0000000000..913490daac --- /dev/null +++ b/ui-v2/app/components/consul-intention-permission-header-list/skin.scss @@ -0,0 +1,7 @@ +.consul-intention-permission-header-list { + dt::before { + @extend %with-glyph-icon; + content: 'H'; + } +} + diff --git a/ui-v2/app/components/consul-intention-permission-list/index.hbs b/ui-v2/app/components/consul-intention-permission-list/index.hbs index 2c860cab53..22bf2e3eb6 100644 --- a/ui-v2/app/components/consul-intention-permission-list/index.hbs +++ b/ui-v2/app/components/consul-intention-permission-list/index.hbs @@ -1,35 +1,83 @@ {{#if (gt items.length 0)}} -
-
    - {{#each items as |item|}} -
  • + + +
    {{item.Action}} -{{#if item.Http.Path}} -
    +{{#if (gt item.HTTP.Methods.length 0)}} +
    - {{item.Http.PathType}} + Methods
    - {{item.Http.Path}} +{{#each item.HTTP.Methods as |item|}} + {{item}} +{{/each}}
    {{/if}} -{{#each item.Http.Header as |item|}} -
    +{{#if item.HTTP.Path}} +
    +
    + + {{item.HTTP.PathType}} + +
    +
    + {{item.HTTP.Path}} +
    +
    +{{/if}} +{{#each item.HTTP.Header as |item|}} +
    Header
    - {{item.Name}} {{route-match item}} + {{item.Name}} {{route-match item}}
    {{/each}} -
  • - {{/each}} -
-
+ + +{{#if onclick}} + + + + + Edit + + + + + Delete + + + + + Confirm delete + + +

+ Are you sure you want to delete this permission? +

+
+ + Delete + +
+
+
+
+
+{{/if}} + {{/if}} \ No newline at end of file diff --git a/ui-v2/app/components/consul-intention-permission-list/index.scss b/ui-v2/app/components/consul-intention-permission-list/index.scss new file mode 100644 index 0000000000..9ed02408e0 --- /dev/null +++ b/ui-v2/app/components/consul-intention-permission-list/index.scss @@ -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; +} diff --git a/ui-v2/app/components/consul-intention-permission-list/layout.scss b/ui-v2/app/components/consul-intention-permission-list/layout.scss new file mode 100644 index 0000000000..902c790f51 --- /dev/null +++ b/ui-v2/app/components/consul-intention-permission-list/layout.scss @@ -0,0 +1,9 @@ +.consul-intention-permission-list { + .detail > div { + display: flex; + width: 100%; + } + strong { + margin-right: 8px; + } +} diff --git a/ui-v2/app/components/consul-intention-permission-list/skin.scss b/ui-v2/app/components/consul-intention-permission-list/skin.scss new file mode 100644 index 0000000000..d6b2ab23e1 --- /dev/null +++ b/ui-v2/app/components/consul-intention-permission-list/skin.scss @@ -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'; + } +} diff --git a/ui-v2/app/components/consul-intention-view/index.hbs b/ui-v2/app/components/consul-intention-view/index.hbs index 3c9d8b1bcc..e5772a7d1c 100644 --- a/ui-v2/app/components/consul-intention-view/index.hbs +++ b/ui-v2/app/components/consul-intention-view/index.hbs @@ -34,7 +34,9 @@

- + {{/if}} diff --git a/ui-v2/app/components/list-collection/index.hbs b/ui-v2/app/components/list-collection/index.hbs index 80c75b66f2..ce7fbe30cc 100644 --- a/ui-v2/app/components/list-collection/index.hbs +++ b/ui-v2/app/components/list-collection/index.hbs @@ -1,34 +1,69 @@ -{{on-window 'resize' (action "resize") }} -{{yield}} - -
  • - {{~#each _cells as |cell|~}} -
  • -
    {{yield cell.item cell.index}}
    -
    {{yield cell.item cell.index}}
    - +
  • + {{~#each _cells as |cell|~}} +
  • -
    - {{yield cell.item cell.index}} -
    - -
  • - {{~/each~}} -
    \ No newline at end of file +
    {{yield cell.item cell.index}}
    +
    {{yield cell.item cell.index}}
    + +
    + {{yield cell.item cell.index}} +
    +
    + + {{~/each~}} + +{{else}} +
      +
    • + {{~#each items as |item index|~}} +
    • +
      {{yield item index}}
      +
      {{yield item index}}
      + +
      + {{yield item index}} +
      +
      +
    • + {{~/each~}} +
    +{{/if}} + \ No newline at end of file diff --git a/ui-v2/app/components/list-collection/index.js b/ui-v2/app/components/list-collection/index.js index 6ef8cd0495..05226d77f8 100644 --- a/ui-v2/app/components/list-collection/index.js +++ b/ui-v2/app/components/list-collection/index.js @@ -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)); diff --git a/ui-v2/app/components/popover-menu/index.hbs b/ui-v2/app/components/popover-menu/index.hbs index 769f944060..b72f25d661 100644 --- a/ui-v2/app/components/popover-menu/index.hbs +++ b/ui-v2/app/components/popover-menu/index.hbs @@ -36,6 +36,7 @@ as |components|}} + {{#each submenus as |sub|}} diff --git a/ui-v2/app/components/popover-menu/menu-item/index.hbs b/ui-v2/app/components/popover-menu/menu-item/index.hbs index 7cce6c7cf3..ef42ac9dc9 100644 --- a/ui-v2/app/components/popover-menu/menu-item/index.hbs +++ b/ui-v2/app/components/popover-menu/menu-item/index.hbs @@ -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))) + }}> {{yield}} diff --git a/ui-v2/app/components/radio-card/index.js b/ui-v2/app/components/radio-card/index.js index a7be4db131..4798652642 100644 --- a/ui-v2/app/components/radio-card/index.js +++ b/ui-v2/app/components/radio-card/index.js @@ -1,6 +1,5 @@ import Component from '@ember/component'; -import Slotted from 'block-slots'; -export default Component.extend(Slotted, { +export default Component.extend({ tagName: '', }); diff --git a/ui-v2/app/helpers/route-match.js b/ui-v2/app/helpers/route-match.js index e1fee0031c..3705462454 100644 --- a/ui-v2/app/helpers/route-match.js +++ b/ui-v2/app/helpers/route-match.js @@ -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 ''; diff --git a/ui-v2/app/models/intention-permission-http-header.js b/ui-v2/app/models/intention-permission-http-header.js index 0060904d28..d663bea810 100644 --- a/ui-v2/app/models/intention-permission-http-header.js +++ b/ui-v2/app/models/intention-permission-http-header.js @@ -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'); + }), }); diff --git a/ui-v2/app/models/intention-permission-http.js b/ui-v2/app/models/intention-permission-http.js index 8a988aefac..7e551eaa0f 100644 --- a/ui-v2/app/models/intention-permission-http.js +++ b/ui-v2/app/models/intention-permission-http.js @@ -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'); }), }); diff --git a/ui-v2/app/models/intention-permission.js b/ui-v2/app/models/intention-permission.js index 4786d23eac..af1e8e4cec 100644 --- a/ui-v2/app/models/intention-permission.js +++ b/ui-v2/app/models/intention-permission.js @@ -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'), }); diff --git a/ui-v2/app/models/intention.js b/ui-v2/app/models/intention.js index 5a2b1c874d..d2e04b452a 100644 --- a/ui-v2/app/models/intention.js +++ b/ui-v2/app/models/intention.js @@ -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'), diff --git a/ui-v2/app/routes/dc/services/show/intentions/edit.js b/ui-v2/app/routes/dc/services/show/intentions/edit.js index b044b155c1..af43c7987b 100644 --- a/ui-v2/app/routes/dc/services/show/intentions/edit.js +++ b/ui-v2/app/routes/dc/services/show/intentions/edit.js @@ -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) { diff --git a/ui-v2/app/serializers/intention.js b/ui-v2/app/serializers/intention.js index d35b79a49f..c85057fdeb 100644 --- a/ui-v2/app/serializers/intention.js +++ b/ui-v2/app/serializers/intention.js @@ -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); + }); }, }); diff --git a/ui-v2/app/services/change.js b/ui-v2/app/services/change.js new file mode 100644 index 0000000000..a5150464d8 --- /dev/null +++ b/ui-v2/app/services/change.js @@ -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); + }, +}); diff --git a/ui-v2/app/services/repository.js b/ui-v2/app/services/repository.js index a48b4f13e4..195022b88d 100644 --- a/ui-v2/app/services/repository.js +++ b/ui-v2/app/services/repository.js @@ -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) { diff --git a/ui-v2/app/services/repository/intention-permission-http-header.js b/ui-v2/app/services/repository/intention-permission-http-header.js new file mode 100644 index 0000000000..7fb1ee0932 --- /dev/null +++ b/ui-v2/app/services/repository/intention-permission-http-header.js @@ -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(); + }, +}); diff --git a/ui-v2/app/services/repository/intention-permission.js b/ui-v2/app/services/repository/intention-permission.js new file mode 100644 index 0000000000..79e2c906fc --- /dev/null +++ b/ui-v2/app/services/repository/intention-permission.js @@ -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(); + }, +}); diff --git a/ui-v2/app/services/repository/intention.js b/ui-v2/app/services/repository/intention.js index 8217ed07ee..3e1033571b 100644 --- a/ui-v2/app/services/repository/intention.js +++ b/ui-v2/app/services/repository/intention.js @@ -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, diff --git a/ui-v2/app/services/schema.js b/ui-v2/app/services/schema.js new file mode 100644 index 0000000000..bf4e9c22ff --- /dev/null +++ b/ui-v2/app/services/schema.js @@ -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, +}); diff --git a/ui-v2/app/styles/components.scss b/ui-v2/app/styles/components.scss index fb2b3d5d0b..11bd086a94 100644 --- a/ui-v2/app/styles/components.scss +++ b/ui-v2/app/styles/components.scss @@ -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'; diff --git a/ui-v2/app/styles/components/buttons.scss b/ui-v2/app/styles/components/buttons.scss index 765df2edf0..5b1077de44 100644 --- a/ui-v2/app/styles/components/buttons.scss +++ b/ui-v2/app/styles/components/buttons.scss @@ -1,4 +1,5 @@ button[type='submit'], +button.type-submit, a.type-create { @extend %primary-button; } diff --git a/ui-v2/app/styles/components/consul-intention-fieldsets.scss b/ui-v2/app/styles/components/consul-intention-fieldsets.scss deleted file mode 100644 index aa137a88da..0000000000 --- a/ui-v2/app/styles/components/consul-intention-fieldsets.scss +++ /dev/null @@ -1,4 +0,0 @@ -@import './consul-intention-fieldsets/index'; -.consul-intention-fieldsets { - @extend %consul-intention-fieldsets; -} diff --git a/ui-v2/app/styles/components/consul-intention-fieldsets/layout.scss b/ui-v2/app/styles/components/consul-intention-fieldsets/layout.scss deleted file mode 100644 index 4b4713d8a1..0000000000 --- a/ui-v2/app/styles/components/consul-intention-fieldsets/layout.scss +++ /dev/null @@ -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; -} diff --git a/ui-v2/app/styles/components/consul-intention-list.scss b/ui-v2/app/styles/components/consul-intention-list.scss deleted file mode 100644 index 642c10286a..0000000000 --- a/ui-v2/app/styles/components/consul-intention-list.scss +++ /dev/null @@ -1,4 +0,0 @@ -@import './consul-intention-list/index'; -.consul-intention-list { - @extend %consul-intention-list; -} diff --git a/ui-v2/app/styles/components/consul-intention-list/index.scss b/ui-v2/app/styles/components/consul-intention-list/index.scss deleted file mode 100644 index d049a9e8fa..0000000000 --- a/ui-v2/app/styles/components/consul-intention-list/index.scss +++ /dev/null @@ -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; -} diff --git a/ui-v2/app/styles/components/consul-intention-list/layout.scss b/ui-v2/app/styles/components/consul-intention-list/layout.scss deleted file mode 100644 index 9b01854717..0000000000 --- a/ui-v2/app/styles/components/consul-intention-list/layout.scss +++ /dev/null @@ -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; - } -} diff --git a/ui-v2/app/styles/components/consul-intention-list/skin.scss b/ui-v2/app/styles/components/consul-intention-list/skin.scss deleted file mode 100644 index c96a4e7af7..0000000000 --- a/ui-v2/app/styles/components/consul-intention-list/skin.scss +++ /dev/null @@ -1,3 +0,0 @@ -%consul-intention-list td.permissions { - color: $blue-500; -} diff --git a/ui-v2/app/styles/components/consul-intention-permission-list.scss b/ui-v2/app/styles/components/consul-intention-permission-list.scss deleted file mode 100644 index 139c5be691..0000000000 --- a/ui-v2/app/styles/components/consul-intention-permission-list.scss +++ /dev/null @@ -1,4 +0,0 @@ -@import './consul-intention-permission-list/index'; -.consul-intention-permission-list { - @extend %consul-intention-permission-list; -} diff --git a/ui-v2/app/styles/components/consul-intention-permission-list/index.scss b/ui-v2/app/styles/components/consul-intention-permission-list/index.scss deleted file mode 100644 index a46573bbd9..0000000000 --- a/ui-v2/app/styles/components/consul-intention-permission-list/index.scss +++ /dev/null @@ -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; -} diff --git a/ui-v2/app/styles/components/consul-intention-permission-list/layout.scss b/ui-v2/app/styles/components/consul-intention-permission-list/layout.scss deleted file mode 100644 index 589ba10ca4..0000000000 --- a/ui-v2/app/styles/components/consul-intention-permission-list/layout.scss +++ /dev/null @@ -1,3 +0,0 @@ -%consul-intention-permission-list strong { - margin-right: 8px; -} diff --git a/ui-v2/app/styles/components/consul-intention-permission-list/skin.scss b/ui-v2/app/styles/components/consul-intention-permission-list/skin.scss deleted file mode 100644 index cb5b17de24..0000000000 --- a/ui-v2/app/styles/components/consul-intention-permission-list/skin.scss +++ /dev/null @@ -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'; -} diff --git a/ui-v2/app/styles/components/flash-message/layout.scss b/ui-v2/app/styles/components/flash-message/layout.scss index 8239a88caf..02ea5d922b 100644 --- a/ui-v2/app/styles/components/flash-message/layout.scss +++ b/ui-v2/app/styles/components/flash-message/layout.scss @@ -1,7 +1,7 @@ %flash-message { display: flex; position: relative; - z-index: 2; + z-index: 6; justify-content: center; margin: 0 15%; } diff --git a/ui-v2/app/styles/components/form-elements.scss b/ui-v2/app/styles/components/form-elements.scss index 40081f33b1..14cf5ef320 100644 --- a/ui-v2/app/styles/components/form-elements.scss +++ b/ui-v2/app/styles/components/form-elements.scss @@ -13,7 +13,7 @@ label span { %main-content form { @extend %form; } -%form span.label { +span.label { @extend %form-element-label; } %form table, diff --git a/ui-v2/app/styles/components/icon-definition/layout.scss b/ui-v2/app/styles/components/icon-definition/layout.scss index 2bfc368a50..923a730d45 100644 --- a/ui-v2/app/styles/components/icon-definition/layout.scss +++ b/ui-v2/app/styles/components/icon-definition/layout.scss @@ -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; -} diff --git a/ui-v2/app/styles/components/list-collection.scss b/ui-v2/app/styles/components/list-collection.scss index 0e39f15707..4b365b7d2b 100644 --- a/ui-v2/app/styles/components/list-collection.scss +++ b/ui-v2/app/styles/components/list-collection.scss @@ -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; diff --git a/ui-v2/app/utils/form/builder.js b/ui-v2/app/utils/form/builder.js index a0f1e4128a..b0091ee564 100644 --- a/ui-v2/app/utils/form/builder.js +++ b/ui-v2/app/utils/form/builder.js @@ -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 diff --git a/ui-v2/app/utils/form/changeset.js b/ui-v2/app/utils/form/changeset.js new file mode 100644 index 0000000000..8073a68f79 --- /dev/null +++ b/ui-v2/app/utils/form/changeset.js @@ -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); + } +} diff --git a/ui-v2/app/validations/intention-permission-http-header.js b/ui-v2/app/validations/intention-permission-http-header.js new file mode 100644 index 0000000000..de67f06192 --- /dev/null +++ b/ui-v2/app/validations/intention-permission-http-header.js @@ -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'; + }), +}); diff --git a/ui-v2/app/validations/intention-permission.js b/ui-v2/app/validations/intention-permission.js new file mode 100644 index 0000000000..ad855130e6 --- /dev/null +++ b/ui-v2/app/validations/intention-permission.js @@ -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'; + }), + }, +}); diff --git a/ui-v2/app/validations/intention.js b/ui-v2/app/validations/intention.js index 922ba9e413..12e3115b27 100644 --- a/ui-v2/app/validations/intention.js +++ b/ui-v2/app/validations/intention.js @@ -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'); + }), + ], }; diff --git a/ui-v2/package.json b/ui-v2/package.json index c81988f4bc..1c105e9a69 100644 --- a/ui-v2/package.json +++ b/ui-v2/package.json @@ -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", diff --git a/ui-v2/tests/acceptance/dc/intentions/create.feature b/ui-v2/tests/acceptance/dc/intentions/create.feature index 5c29e704eb..ab46d96131 100644 --- a/ui-v2/tests/acceptance/dc/intentions/create.feature +++ b/ui-v2/tests/acceptance/dc/intentions/create.feature @@ -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 diff --git a/ui-v2/tests/acceptance/dc/intentions/delete.feature b/ui-v2/tests/acceptance/dc/intentions/delete.feature index dd8feec099..58202d686e 100644 --- a/ui-v2/tests/acceptance/dc/intentions/delete.feature +++ b/ui-v2/tests/acceptance/dc/intentions/delete.feature @@ -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:" diff --git a/ui-v2/tests/acceptance/dc/intentions/update.feature b/ui-v2/tests/acceptance/dc/intentions/update.feature index 7617d4580f..3c3effc24c 100644 --- a/ui-v2/tests/acceptance/dc/intentions/update.feature +++ b/ui-v2/tests/acceptance/dc/intentions/update.feature @@ -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 diff --git a/ui-v2/tests/acceptance/dc/services/show/intentions.feature b/ui-v2/tests/acceptance/dc/services/show/intentions.feature index 3bde9f6070..58706574e4 100644 --- a/ui-v2/tests/acceptance/dc/services/show/intentions.feature +++ b/ui-v2/tests/acceptance/dc/services/show/intentions.feature @@ -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 diff --git a/ui-v2/tests/integration/adapters/intention-test.js b/ui-v2/tests/integration/adapters/intention-test.js index 8262602148..fa8ceea4d9 100644 --- a/ui-v2/tests/integration/adapters/intention-test.js +++ b/ui-v2/tests/integration/adapters/intention-test.js @@ -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]; diff --git a/ui-v2/yarn.lock b/ui-v2/yarn.lock index 3c85be4f73..0666398c2b 100644 --- a/ui-v2/yarn.lock +++ b/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"