mirror of https://github.com/status-im/consul.git
ui: New Intention Form/List components (#8172)
This commit is contained in:
parent
5e5dbedd47
commit
0d35548519
|
@ -22,6 +22,7 @@ export default Adapter.extend({
|
|||
}
|
||||
return request`
|
||||
GET /v1/connect/intentions/${id}?${{ dc }}
|
||||
Cache-Control: no-store
|
||||
|
||||
${{ index }}
|
||||
`;
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
<AppView @class="error show">
|
||||
<BlockSlot @name="header">
|
||||
<h1>
|
||||
Error {{error.status}}
|
||||
</h1>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="content">
|
||||
<ErrorState @error={{error}} />
|
||||
</BlockSlot>
|
||||
</AppView>
|
|
@ -0,0 +1,5 @@
|
|||
import Component from '@ember/component';
|
||||
|
||||
export default Component.extend({
|
||||
tagName: '',
|
||||
});
|
|
@ -1,8 +1,10 @@
|
|||
{{yield}}
|
||||
{{#if (not loading)}}
|
||||
<header>
|
||||
{{#each flashMessages.queue as |flash|}}
|
||||
<FlashMessage @flash={{flash}} as |component flash|>
|
||||
{{#if flash.dom}}
|
||||
{{{flash.dom}}}
|
||||
{{else}}
|
||||
{{#let (lowercase component.flashType) (lowercase flash.action) as |status type|}}
|
||||
{{! flashes automatically ucfirst the type }}
|
||||
|
||||
|
@ -42,6 +44,7 @@
|
|||
{{/yield-slot}}
|
||||
</p>
|
||||
{{/let}}
|
||||
{{/if}}
|
||||
</FlashMessage>
|
||||
{{/each}}
|
||||
<div>
|
||||
|
@ -57,7 +60,10 @@
|
|||
</YieldSlot>
|
||||
<div class="actions">
|
||||
{{#if authorized}}
|
||||
<YieldSlot @name="actions">{{yield}}</YieldSlot>
|
||||
<YieldSlot @name="actions">
|
||||
<PortalTarget @name="app-view-actions" />
|
||||
{{yield}}
|
||||
</YieldSlot>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -73,11 +79,7 @@
|
|||
</YieldSlot>
|
||||
{{/if}}
|
||||
</header>
|
||||
{{/if}}
|
||||
<div>
|
||||
{{#if loading}}
|
||||
<ConsulLoader />
|
||||
{{else}}
|
||||
{{#if (not enabled) }}
|
||||
<YieldSlot @name="disabled">{{yield}}</YieldSlot>
|
||||
{{else if (not authorized)}}
|
||||
|
@ -85,5 +87,4 @@
|
|||
{{else}}
|
||||
<YieldSlot @name="content">{{yield}}</YieldSlot>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
|
|
@ -3,7 +3,6 @@ import SlotsMixin from 'block-slots';
|
|||
import { inject as service } from '@ember/service';
|
||||
import templatize from 'consul-ui/utils/templatize';
|
||||
export default Component.extend(SlotsMixin, {
|
||||
loading: false,
|
||||
authorized: true,
|
||||
enabled: true,
|
||||
classNames: ['app-view'],
|
||||
|
@ -13,12 +12,12 @@ export default Component.extend(SlotsMixin, {
|
|||
this._super(...arguments);
|
||||
// right now only manually added classes are hoisted to <html>
|
||||
const $root = this.dom.root();
|
||||
let cls = this['class'] || '';
|
||||
if (this.loading) {
|
||||
cls += ' loading';
|
||||
$root.classList.add('loading');
|
||||
} else {
|
||||
$root.classList.remove(...templatize(['loading']));
|
||||
$root.classList.remove('loading');
|
||||
}
|
||||
let cls = this['class'] || '';
|
||||
if (cls) {
|
||||
// its possible for 'layout' templates to change after insert
|
||||
// check for these specific layouts and clear them out
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{{yield}}
|
||||
<YieldSlot @name="content" @params={{block-params items}}>{{yield}}</YieldSlot>
|
||||
{{#if (gt items.length 0)}}
|
||||
<YieldSlot @name="set" @params={{block-params items}}>{{yield}}</YieldSlot>
|
||||
{{else}}
|
||||
|
|
|
@ -1,123 +1,168 @@
|
|||
<form onsubmit={{action 'submit' _item}}>
|
||||
<fieldset>
|
||||
<div role="group">
|
||||
<fieldset>
|
||||
<h2>Source</h2>
|
||||
<label data-test-source-element class="type-select{{if _item.error.SourceName ' has-error'}}">
|
||||
<span>Source Service</span>
|
||||
<PowerSelectWithCreate
|
||||
@options={{_services}}
|
||||
@searchField="Name"
|
||||
@selected={{SourceName}}
|
||||
@searchPlaceholder="Type service name"
|
||||
@buildSuggestion={{action "createNewLabel" "Use a Consul Service called '{{term}}'"}}
|
||||
@showCreateWhen={{action "isUnique"}}
|
||||
@onCreate={{action "change" "SourceName"}}
|
||||
@onChange={{action "change" "SourceName"}} as |service|>
|
||||
{{#if (eq service.Name '*') }}
|
||||
* (All Services)
|
||||
{{else}}
|
||||
{{service.Name}}
|
||||
{{/if}}
|
||||
</PowerSelectWithCreate>
|
||||
<em>Search for an existing service, or enter any Service name.</em>
|
||||
</label>
|
||||
{{#if (env 'CONSUL_NSPACES_ENABLED')}}
|
||||
<label data-test-source-nspace class="type-select{{if _item.error.SourceNS ' has-error'}}">
|
||||
<span>Source Namespace</span>
|
||||
<PowerSelectWithCreate
|
||||
@options={{_nspaces}}
|
||||
@searchField="Name"
|
||||
@selected={{SourceNS}}
|
||||
@searchPlaceholder="Type namespace name"
|
||||
@buildSuggestion={{action "createNewLabel" "Use a Consul Namespace called '{{term}}'"}}
|
||||
@showCreateWhen={{action "isUnique"}}
|
||||
@onCreate={{action "change" "SourceNS"}}
|
||||
@onChange={{action "change" "SourceNS"}} as |nspace|>
|
||||
{{#if (eq nspace.Name '*') }}
|
||||
* (All Namespaces)
|
||||
{{else}}
|
||||
{{nspace.Name}}
|
||||
{{/if}}
|
||||
</PowerSelectWithCreate>
|
||||
<em>Search for an existing namespace, or enter any Namespace name.</em>
|
||||
</label>
|
||||
{{/if}}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<h2>Destination</h2>
|
||||
<label data-test-destination-element class="type-select{{if _item.error.DestinationName ' has-error'}}">
|
||||
<span>Destination Service</span>
|
||||
<PowerSelectWithCreate
|
||||
@options={{_services}}
|
||||
@searchField="Name"
|
||||
@selected={{DestinationName}}
|
||||
@searchPlaceholder="Type service name"
|
||||
@buildSuggestion={{action "createNewLabel" "Use a Consul Service called '{{term}}'"}}
|
||||
@showCreateWhen={{action "isUnique"}}
|
||||
@onCreate={{action "change" "DestinationName"}}
|
||||
@onChange={{action "change" "DestinationName"}} as |service|>
|
||||
{{#if (eq service.Name '*') }}
|
||||
* (All Services)
|
||||
{{else}}
|
||||
{{service.Name}}
|
||||
{{/if}}
|
||||
</PowerSelectWithCreate>
|
||||
<em>Search for an existing service, or enter any Service name.</em>
|
||||
</label>
|
||||
{{#if (env 'CONSUL_NSPACES_ENABLED')}}
|
||||
<label data-test-destination-nspace class="type-select{{if _item.error.DestinationNS ' has-error'}}">
|
||||
<span>Destination Namespace</span>
|
||||
<PowerSelectWithCreate
|
||||
@options={{_nspaces}}
|
||||
@searchField="Name"
|
||||
@selected={{DestinationNS}}
|
||||
@searchPlaceholder="Type namespace name"
|
||||
@buildSuggestion={{action "createNewLabel" "Use a future Consul Namespace called '{{term}}'"}}
|
||||
@showCreateWhen={{action "isUnique"}}
|
||||
@onCreate={{action "change" "DestinationNS"}}
|
||||
@onChange={{action "change" "DestinationNS"}} as |nspace|>
|
||||
{{#if (eq nspace.Name '*') }}
|
||||
* (All Namespaces)
|
||||
{{else}}
|
||||
{{nspace.Name}}
|
||||
{{/if}}
|
||||
</PowerSelectWithCreate>
|
||||
<em>For the destination, you may choose any namespace for which you have access.</em>
|
||||
</label>
|
||||
{{/if}}
|
||||
</fieldset>
|
||||
</div>
|
||||
<div role="radiogroup" class={{if _item.error.Action ' has-error'}}>
|
||||
{{#each (array 'allow' 'deny') as |intent|}}
|
||||
<label>
|
||||
<span>{{ capitalize intent }}</span>
|
||||
<input type="radio" name="Action" value="{{intent}}" checked={{if (eq _item.Action intent) 'checked'}} onchange={{ action 'change' }}/>
|
||||
</label>
|
||||
{{/each}}
|
||||
</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 'change'}} />
|
||||
</label>
|
||||
</fieldset>
|
||||
<div>
|
||||
{{#if _item.isNew }}
|
||||
<button type="submit" disabled={{if (or _item.isPristine _item.isInvalid) 'disabled'}}>Save</button>
|
||||
{{ else }}
|
||||
<button type="submit" disabled={{if _item.isInvalid 'disabled'}}>Save</button>
|
||||
{{/if}}
|
||||
<button type="reset" onclick={{action oncancel _item}}>Cancel</button>
|
||||
{{# if (and _item.ID (not-eq _item.ID 'anonymous')) }}
|
||||
<ConfirmationDialog @message="Are you sure you want to delete this Intention?">
|
||||
<BlockSlot @name="action" as |confirm|>
|
||||
<button data-test-delete type="button" class="type-delete" {{action confirm ondelete _item}}>Delete</button>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="dialog" as |execute cancel message|>
|
||||
<DeleteConfirmation @message={{message}} @execute={{execute}} @cancel={{cancel}} />
|
||||
</BlockSlot>
|
||||
</ConfirmationDialog>
|
||||
{{/if}}
|
||||
</div>
|
||||
</form>
|
||||
<DataForm
|
||||
@dc={{dc}}
|
||||
@nspace={{nspace}}
|
||||
@type="intention"
|
||||
@autofill={{autofill}}
|
||||
@item={{item}}
|
||||
@src={{src}}
|
||||
@onchange={{action "change"}}
|
||||
@onsubmit={{action onsubmit}}
|
||||
as |api|
|
||||
>
|
||||
<BlockSlot @name="error" as |Notification|>
|
||||
<Notification>
|
||||
<p data-notification role="alert" class="error notification-update">
|
||||
{{#if (starts-with 'duplicate intention found:' api.error.detail)}}
|
||||
<strong>Intention exists</strong>
|
||||
An intention already exists for this Source-Destination pair. Please enter a different combination of Services, or search the intentions to edit an existing intention.
|
||||
{{else}}
|
||||
<strong>Error!</strong>
|
||||
There was an error saving your intention.
|
||||
{{#if (and api.error.status api.error.detail)}}
|
||||
<br />{{api.error.status}}: {{api.error.detail}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</p>
|
||||
</Notification>
|
||||
</BlockSlot>
|
||||
|
||||
<BlockSlot @name="form">
|
||||
|
||||
<DataSource
|
||||
@src={{concat '/' nspace '/' dc '/services'}}
|
||||
@onchange={{action "createServices" api.data}}
|
||||
/>
|
||||
|
||||
{{#if (env 'CONSUL_NSPACES_ENABLED')}}
|
||||
<DataSource
|
||||
@src="/*/*/namespaces"
|
||||
@onchange={{action "createNspaces" api.data}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
<form onsubmit={{action api.submit}}>
|
||||
|
||||
<fieldset disabled={{api.disabled}}>
|
||||
<div role="group">
|
||||
<fieldset>
|
||||
<h2>Source</h2>
|
||||
<label data-test-source-element class="type-select{{if api.data.error.SourceName ' has-error'}}">
|
||||
<span>Source Service</span>
|
||||
<PowerSelectWithCreate
|
||||
@options={{services}}
|
||||
@searchField="Name"
|
||||
@selected={{SourceName}}
|
||||
@searchPlaceholder="Type service name"
|
||||
@buildSuggestion={{action "createNewLabel" "Use a Consul Service called '{{term}}'"}}
|
||||
@showCreateWhen={{action "isUnique" services}}
|
||||
@onCreate={{action api.change "SourceName"}}
|
||||
@onChange={{action api.change "SourceName"}} as |service|>
|
||||
{{#if (eq service.Name '*') }}
|
||||
* (All Services)
|
||||
{{else}}
|
||||
{{service.Name}}
|
||||
{{/if}}
|
||||
</PowerSelectWithCreate>
|
||||
<em>Search for an existing service, or enter any Service name.</em>
|
||||
</label>
|
||||
{{#if (env 'CONSUL_NSPACES_ENABLED')}}
|
||||
<label data-test-source-nspace class="type-select{{if api.data.error.SourceNS ' has-error'}}">
|
||||
<span>Source Namespace</span>
|
||||
<PowerSelectWithCreate
|
||||
@options={{nspaces}}
|
||||
@searchField="Name"
|
||||
@selected={{SourceNS}}
|
||||
@searchPlaceholder="Type namespace name"
|
||||
@buildSuggestion={{action "createNewLabel" "Use a Consul Namespace called '{{term}}'"}}
|
||||
@showCreateWhen={{action "isUnique" nspaces}}
|
||||
@onCreate={{action api.change "SourceNS"}}
|
||||
@onChange={{action api.change "SourceNS"}} as |nspace|>
|
||||
{{#if (eq nspace.Name '*') }}
|
||||
* (All Namespaces)
|
||||
{{else}}
|
||||
{{nspace.Name}}
|
||||
{{/if}}
|
||||
</PowerSelectWithCreate>
|
||||
<em>Search for an existing namespace, or enter any Namespace name.</em>
|
||||
</label>
|
||||
{{/if}}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<h2>Destination</h2>
|
||||
<label data-test-destination-element class="type-select{{if api.data.error.DestinationName ' has-error'}}">
|
||||
<span>Destination Service</span>
|
||||
<PowerSelectWithCreate
|
||||
@options={{services}}
|
||||
@searchField="Name"
|
||||
@selected={{DestinationName}}
|
||||
@searchPlaceholder="Type service name"
|
||||
@buildSuggestion={{action "createNewLabel" "Use a Consul Service called '{{term}}'"}}
|
||||
@showCreateWhen={{action "isUnique" services}}
|
||||
@onCreate={{action api.change "DestinationName"}}
|
||||
@onChange={{action api.change "DestinationName"}} as |service|>
|
||||
{{#if (eq service.Name '*') }}
|
||||
* (All Services)
|
||||
{{else}}
|
||||
{{service.Name}}
|
||||
{{/if}}
|
||||
</PowerSelectWithCreate>
|
||||
<em>Search for an existing service, or enter any Service name.</em>
|
||||
</label>
|
||||
{{#if (env 'CONSUL_NSPACES_ENABLED')}}
|
||||
<label data-test-destination-nspace class="type-select{{if api.data.error.DestinationNS ' has-error'}}">
|
||||
<span>Destination Namespace</span>
|
||||
<PowerSelectWithCreate
|
||||
@options={{nspaces}}
|
||||
@searchField="Name"
|
||||
@selected={{DestinationNS}}
|
||||
@searchPlaceholder="Type namespace name"
|
||||
@buildSuggestion={{action "createNewLabel" "Use a future Consul Namespace called '{{term}}'"}}
|
||||
@showCreateWhen={{action "isUnique" nspaces}}
|
||||
@onCreate={{action api.change "DestinationNS"}}
|
||||
@onChange={{action api.change "DestinationNS"}} as |nspace|>
|
||||
{{#if (eq nspace.Name '*') }}
|
||||
* (All Namespaces)
|
||||
{{else}}
|
||||
{{nspace.Name}}
|
||||
{{/if}}
|
||||
</PowerSelectWithCreate>
|
||||
<em>For the destination, you may choose any namespace for which you have access.</em>
|
||||
</label>
|
||||
{{/if}}
|
||||
</fieldset>
|
||||
</div>
|
||||
<div role="radiogroup" class={{if api.data.error.Action ' has-error'}}>
|
||||
{{#each (array 'allow' 'deny') as |intent|}}
|
||||
<label>
|
||||
<span>{{capitalize intent}}</span>
|
||||
<input type="radio" name="Action" value={{intent}} checked={{if (eq api.data.Action intent) 'checked'}} onchange={{action api.change}}/>
|
||||
</label>
|
||||
{{/each}}
|
||||
</div>
|
||||
<label class="type-text{{if api.data.error.Description ' has-error'}}">
|
||||
<span>Description (Optional)</span>
|
||||
<input type="text" name="Description" value={{api.data.Description}} placeholder="Description (Optional)" onchange={{action api.change}} />
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<div>
|
||||
<button type="submit" disabled={{or api.data.isInvalid api.disabled}}>Save</button>
|
||||
{{#if (not api.isCreate)}}
|
||||
<button type="reset" onclick={{action oncancel api.data}} disabled={{api.disabled}}>Cancel</button>
|
||||
{{#if (not-eq form.item.ID 'anonymous') }}
|
||||
<ConfirmationDialog @message="Are you sure you want to delete this Intention?">
|
||||
<BlockSlot @name="action" as |confirm|>
|
||||
<button data-test-delete type="button" class="type-delete" {{action confirm api.delete}} disabled={{api.disabled}}>Delete</button>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="dialog" as |execute cancel message|>
|
||||
<DeleteConfirmation @message={{message}} @execute={{execute}} @cancel={{cancel}} />
|
||||
</BlockSlot>
|
||||
</ConfirmationDialog>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
</BlockSlot>
|
||||
</DataForm>
|
||||
|
|
|
@ -1,71 +1,76 @@
|
|||
import Component from '@ember/component';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { setProperties, set, get } from '@ember/object';
|
||||
import { assert } from '@ember/debug';
|
||||
|
||||
export default Component.extend({
|
||||
tagName: '',
|
||||
dom: service('dom'),
|
||||
builder: service('form'),
|
||||
init: function() {
|
||||
this._super(...arguments);
|
||||
this.form = this.builder.form('intention');
|
||||
ondelete: function() {
|
||||
this.onsubmit(...arguments);
|
||||
},
|
||||
didReceiveAttrs: function() {
|
||||
this._super(...arguments);
|
||||
if (this.item && this.services && this.nspaces) {
|
||||
let services = this.services || [];
|
||||
let nspaces = this.nspaces || [];
|
||||
let source = services.findBy('Name', this.item.SourceName);
|
||||
oncancel: function() {
|
||||
this.onsubmit(...arguments);
|
||||
},
|
||||
onsubmit: function() {},
|
||||
actions: {
|
||||
createServices: function(item, e) {
|
||||
// Services in the menus should:
|
||||
// 1. Be unique (they potentially could be duplicated due to services from different namespaces)
|
||||
// 2. Only include services that shold have intentions
|
||||
// 3. Include an 'All Services' option
|
||||
// 4. Include the current Source and Destination incase they are virtual services/don't exist yet
|
||||
let items = e.data
|
||||
.uniqBy('Name')
|
||||
.toArray()
|
||||
.filter(
|
||||
item => !['connect-proxy', 'mesh-gateway', 'terminating-gateway'].includes(item.Kind)
|
||||
)
|
||||
.sort((a, b) => a.Name.localeCompare(b.Name));
|
||||
items = [{ Name: '*' }].concat(items);
|
||||
let source = items.findBy('Name', item.SourceName);
|
||||
if (!source) {
|
||||
source = { Name: this.item.SourceName };
|
||||
services = [source].concat(services);
|
||||
source = { Name: item.SourceName };
|
||||
items = [source].concat(items);
|
||||
}
|
||||
let destination = services.findBy('Name', this.item.DestinationName);
|
||||
let destination = items.findBy('Name', item.DestinationName);
|
||||
if (!destination) {
|
||||
destination = { Name: this.item.DestinationName };
|
||||
services = [destination].concat(services);
|
||||
destination = { Name: item.DestinationName };
|
||||
items = [destination].concat(items);
|
||||
}
|
||||
|
||||
let sourceNS = nspaces.findBy('Name', this.item.SourceNS);
|
||||
if (!sourceNS) {
|
||||
sourceNS = { Name: this.item.SourceNS };
|
||||
nspaces = [sourceNS].concat(nspaces);
|
||||
}
|
||||
let destinationNS = this.nspaces.findBy('Name', this.item.DestinationNS);
|
||||
if (!destinationNS) {
|
||||
destinationNS = { Name: this.item.DestinationNS };
|
||||
nspaces = [destinationNS].concat(nspaces);
|
||||
}
|
||||
// TODO: Use this.{item,services} when we have this.args
|
||||
setProperties(this, {
|
||||
_item: this.form.setData(this.item).getData(),
|
||||
_services: services,
|
||||
_nspaces: nspaces,
|
||||
services: items,
|
||||
SourceName: source,
|
||||
DestinationName: destination,
|
||||
SourceNS: sourceNS,
|
||||
DestinationNS: destinationNS,
|
||||
});
|
||||
} else {
|
||||
assert('@item, @services and @nspaces are required arguments', false);
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
},
|
||||
createNspaces: function(item, e) {
|
||||
// Nspaces in the menus should:
|
||||
// 1. Include an 'All Namespaces' option
|
||||
// 2. Include the current SourceNS and DestinationNS incase they don't exist yet
|
||||
let items = e.data.toArray().sort((a, b) => a.Name.localeCompare(b.Name));
|
||||
items = [{ Name: '*' }].concat(items);
|
||||
let source = items.findBy('Name', item.SourceNS);
|
||||
if (!source) {
|
||||
source = { Name: item.SourceNS };
|
||||
items = [source].concat(items);
|
||||
}
|
||||
let destination = items.findBy('Name', item.DestinationNS);
|
||||
if (!destination) {
|
||||
destination = { Name: item.DestinationNS };
|
||||
items = [destination].concat(items);
|
||||
}
|
||||
setProperties(this, {
|
||||
nspaces: items,
|
||||
SourceNS: source,
|
||||
DestinationNS: destination,
|
||||
});
|
||||
},
|
||||
createNewLabel: function(template, term) {
|
||||
return template.replace(/{{term}}/g, term);
|
||||
},
|
||||
isUnique: function(term) {
|
||||
return !this._services.findBy('Name', term);
|
||||
isUnique: function(items, term) {
|
||||
return !items.findBy('Name', term);
|
||||
},
|
||||
submit: function(item, e) {
|
||||
e.preventDefault();
|
||||
this.onsubmit(...arguments);
|
||||
},
|
||||
change: function(e, value, item) {
|
||||
const event = this.dom.normalizeEvent(e, value);
|
||||
const form = this.form;
|
||||
const target = event.target;
|
||||
change: function(e, form, item) {
|
||||
const target = e.target;
|
||||
|
||||
let name, selected, match;
|
||||
switch (target.name) {
|
||||
|
@ -88,7 +93,7 @@ export default Component.extend({
|
|||
// basically the difference between
|
||||
// `item.DestinationName` and just `DestinationName`
|
||||
// see if the name is already in the list
|
||||
match = this._services.filterBy('Name', name);
|
||||
match = this.services.filterBy('Name', name);
|
||||
if (match.length === 0) {
|
||||
// if its not make a new 'fake' Service that doesn't exist yet
|
||||
// and add it to the possible services to make an intention between
|
||||
|
@ -96,18 +101,18 @@ export default Component.extend({
|
|||
switch (target.name) {
|
||||
case 'SourceName':
|
||||
case 'DestinationName':
|
||||
set(this, '_services', [selected].concat(this._services.toArray()));
|
||||
set(this, 'services', [selected].concat(this.services.toArray()));
|
||||
break;
|
||||
case 'SourceNS':
|
||||
case 'DestinationNS':
|
||||
set(this, '_nspaces', [selected].concat(this._nspaces.toArray()));
|
||||
set(this, 'nspaces', [selected].concat(this.nspaces.toArray()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
set(this, target.name, selected);
|
||||
break;
|
||||
}
|
||||
form.handleEvent(event);
|
||||
form.handleEvent(e);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,73 +1,87 @@
|
|||
<TabularCollection class="consul-intention-list" @items={{items}} as |item index|>
|
||||
<BlockSlot @name="header">
|
||||
<th>Source</th>
|
||||
<th> </th>
|
||||
<th>Destination</th>
|
||||
<th>Precedence</th>
|
||||
<DataWriter
|
||||
@sink={{concat '/' dc '/' nspace '/intention/'}}
|
||||
@type="intention"
|
||||
@ondelete={{action ondelete}}
|
||||
as |writer|>
|
||||
<BlockSlot @name="content">
|
||||
{{#if (gt items.length 0)}}
|
||||
|
||||
<TabularCollection class="consul-intention-list" @items={{items}} as |item index|>
|
||||
<BlockSlot @name="header">
|
||||
<th>Source</th>
|
||||
<th> </th>
|
||||
<th>Destination</th>
|
||||
<th>Precedence</th>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="row">
|
||||
<td class="source" data-test-intention={{item.ID}}>
|
||||
<a href={{href-to (or routeName 'dc.intentions.edit') item.ID}} data-test-intention-source={{item.SourceName}}>
|
||||
{{#if (eq item.SourceName '*') }}
|
||||
All Services (*)
|
||||
{{else}}
|
||||
{{item.SourceName}}
|
||||
{{/if}}
|
||||
{{! TODO: slugify }}
|
||||
<em class={{concat 'nspace-' (or item.SourceNS 'default')}}>{{or item.SourceNS 'default'}}</em>
|
||||
</a>
|
||||
</td>
|
||||
<td class="intent-{{item.Action}}" data-test-intention-action="{{item.Action}}">
|
||||
<strong>{{item.Action}}</strong>
|
||||
</td>
|
||||
<td class="destination" data-test-intention-destination="{{item.DestinationName}}">
|
||||
<span>
|
||||
{{#if (eq item.DestinationName '*') }}
|
||||
All Services (*)
|
||||
{{else}}
|
||||
{{item.DestinationName}}
|
||||
{{/if}}
|
||||
{{! TODO: slugify }}
|
||||
<em class={{concat 'nspace-' (or item.DestinationNS 'default')}}>{{or item.DestinationNS 'default'}}</em>
|
||||
</span>
|
||||
</td>
|
||||
<td class="precedence">
|
||||
{{item.Precedence}}
|
||||
</td>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="actions" as |index change checked|>
|
||||
<PopoverMenu @expanded={{if (eq checked index) true false}} @onchange={{action change index}} @keyboardAccess={{false}}>
|
||||
<BlockSlot @name="trigger">
|
||||
More
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="menu" as |confirm send keypressClick change|>
|
||||
<li role="none">
|
||||
<a role="menuitem" tabindex="-1" href={{href-to (or routeName 'dc.intentions.edit') item.ID}}>Edit</a>
|
||||
</li>
|
||||
<li role="none" class="dangerous">
|
||||
<label for={{confirm}} role="menuitem" tabindex="-1" onkeypress={{keypressClick}} data-test-delete>Delete</label>
|
||||
<div role="menu">
|
||||
<div class="confirmation-alert warning">
|
||||
<div>
|
||||
<header>
|
||||
Confirm Delete
|
||||
</header>
|
||||
<p>
|
||||
Are you sure you want to delete this intention?
|
||||
</p>
|
||||
</div>
|
||||
<ul>
|
||||
<li class="dangerous">
|
||||
<button tabindex="-1" type="button" class="type-delete" onclick={{queue (action change) (action writer.delete item)}}>Delete</button>
|
||||
</li>
|
||||
<li>
|
||||
<label for={{confirm}}>Cancel</label>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</BlockSlot>
|
||||
</PopoverMenu>
|
||||
</BlockSlot>
|
||||
</TabularCollection>
|
||||
|
||||
{{else}}
|
||||
{{yield}}
|
||||
{{/if}}
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="row">
|
||||
<td class="source" data-test-intention={{item.ID}}>
|
||||
<a href={{href-to 'dc.intentions.edit' item.ID}} data-test-intention-source={{item.SourceName}}>
|
||||
{{#if (eq item.SourceName '*') }}
|
||||
All Services (*)
|
||||
{{else}}
|
||||
{{item.SourceName}}
|
||||
{{/if}}
|
||||
{{! TODO: slugify }}
|
||||
<em class={{concat 'nspace-' (or item.SourceNS 'default')}}>{{or item.SourceNS 'default'}}</em>
|
||||
</a>
|
||||
</td>
|
||||
<td class="intent-{{item.Action}}" data-test-intention-action="{{item.Action}}">
|
||||
<strong>{{item.Action}}</strong>
|
||||
</td>
|
||||
<td class="destination" data-test-intention-destination="{{item.DestinationName}}">
|
||||
<span>
|
||||
{{#if (eq item.DestinationName '*') }}
|
||||
All Services (*)
|
||||
{{else}}
|
||||
{{item.DestinationName}}
|
||||
{{/if}}
|
||||
{{! TODO: slugify }}
|
||||
<em class={{concat 'nspace-' (or item.DestinationNS 'default')}}>{{or item.DestinationNS 'default'}}</em>
|
||||
</span>
|
||||
</td>
|
||||
<td class="precedence">
|
||||
{{item.Precedence}}
|
||||
</td>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="actions" as |index change checked|>
|
||||
<PopoverMenu @expanded={{if (eq checked index) true false}} @onchange={{action change index}} @keyboardAccess={{false}}>
|
||||
<BlockSlot @name="trigger">
|
||||
More
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="menu" as |confirm send keypressClick change|>
|
||||
<li role="none">
|
||||
<a role="menuitem" tabindex="-1" href={{href-to 'dc.intentions.edit' item.ID}}>Edit</a>
|
||||
</li>
|
||||
<li role="none" class="dangerous">
|
||||
<label for={{confirm}} role="menuitem" tabindex="-1" onkeypress={{keypressClick}} data-test-delete>Delete</label>
|
||||
<div role="menu">
|
||||
<div class="confirmation-alert warning">
|
||||
<div>
|
||||
<header>
|
||||
Confirm Delete
|
||||
</header>
|
||||
<p>
|
||||
Are you sure you want to delete this intention?
|
||||
</p>
|
||||
</div>
|
||||
<ul>
|
||||
<li class="dangerous">
|
||||
<button tabindex="-1" type="button" class="type-delete" onclick={{queue (action change) (action ondelete item)}}>Delete</button>
|
||||
</li>
|
||||
<li>
|
||||
<label for={{confirm}}>Cancel</label>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</BlockSlot>
|
||||
</PopoverMenu>
|
||||
</BlockSlot>
|
||||
</TabularCollection>
|
||||
</DataWriter>
|
||||
|
|
|
@ -2,4 +2,5 @@ import Component from '@ember/component';
|
|||
|
||||
export default Component.extend({
|
||||
tagName: '',
|
||||
ondelete: function() {},
|
||||
});
|
||||
|
|
|
@ -1,53 +1,55 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="44px" height="44px" viewBox="0 0 44 44" version="1.1">
|
||||
<g>
|
||||
<circle r="1" cx="27" cy="2" style="transform-origin: 27px 2px" />
|
||||
<circle r="1" cx="17" cy="2" style="transform-origin: 17px 2px" />
|
||||
<circle r="1" cx="27" cy="42" style="transform-origin: 27px 42px" />
|
||||
<circle r="1" cx="17" cy="42" style="transform-origin: 17px 42px" />
|
||||
<circle r="1" cx="2" cy="17" style="transform-origin: 2px 17px" />
|
||||
<circle r="1" cx="2" cy="27" style="transform-origin: 2px 27px" />
|
||||
<circle r="1" cx="42" cy="17" style="transform-origin: 42px 17px" />
|
||||
<circle r="1" cx="42" cy="27" style="transform-origin: 42px 27px" />
|
||||
<circle r="1" cx="33" cy="4" style="transform-origin: 33px 4px" />
|
||||
<circle r="1" cx="11" cy="4" style="transform-origin: 11px 4px" />
|
||||
<circle r="1" cx="33" cy="40" style="transform-origin: 33px 40px" />
|
||||
<circle r="1" cx="11" cy="40" style="transform-origin: 11px 40px" />
|
||||
<circle r="1" cx="40" cy="11" style="transform-origin: 40px 11px" />
|
||||
<circle r="1" cx="4" cy="33" style="transform-origin: 4px 33px" />
|
||||
<circle r="1" cx="40" cy="33" style="transform-origin: 40px 33px" />
|
||||
<circle r="1" cx="4" cy="11" style="transform-origin: 4px 11px" />
|
||||
</g>
|
||||
<g>
|
||||
<circle r="2" cx="22" cy="4" style="transform-origin: 22px 4px" />
|
||||
<circle r="2" cx="22" cy="40" style="transform-origin: 22px 40px" />
|
||||
<circle r="2" cx="4" cy="22" style="transform-origin: 4px 22px" />
|
||||
<circle r="2" cx="40" cy="22" style="transform-origin: 40px 22px" />
|
||||
<circle r="2" cx="9" cy="9" style="transform-origin: 9px 9px" />
|
||||
<circle r="2" cx="35" cy="35" style="transform-origin: 35px 35px" />
|
||||
<circle r="2" cx="35" cy="9" style="transform-origin: 35px 9px" />
|
||||
<circle r="2" cx="9" cy="35" style="transform-origin: 9px 35px" />
|
||||
</g>
|
||||
<g>
|
||||
<circle r="2" cx="28" cy="8" style="transform-origin: 28px 8px" />
|
||||
<circle r="2" cx="16" cy="8" style="transform-origin: 16px 8px" />
|
||||
<circle r="2" cx="28" cy="36" style="transform-origin: 28px 36px" />
|
||||
<circle r="2" cx="16" cy="36" style="transform-origin: 16px 36px" />
|
||||
<circle r="2" cx="8" cy="28" style="transform-origin: 8px 28px" />
|
||||
<circle r="2" cx="8" cy="16" style="transform-origin: 8px 16px" />
|
||||
<circle r="2" cx="36" cy="28" style="transform-origin: 36px 28px" />
|
||||
<circle r="2" cx="36" cy="16" style="transform-origin: 36px 16px" />
|
||||
</g>
|
||||
<g>
|
||||
<circle r="5" cx="22" cy="12" style="transform-origin: 22px 12px" />
|
||||
<circle r="5" cx="22" cy="32" style="transform-origin: 22px 32px" />
|
||||
<circle r="5" cx="12" cy="22" style="transform-origin: 12px 22px" />
|
||||
<circle r="5" cx="32" cy="22" style="transform-origin: 32px 22px" />
|
||||
<circle r="5" cx="15" cy="15" style="transform-origin: 15px 15px" />
|
||||
<circle r="5" cx="29" cy="29" style="transform-origin: 29px 29px" />
|
||||
<circle r="5" cx="29" cy="15" style="transform-origin: 29px 15px" />
|
||||
<circle r="5" cx="15" cy="29" style="transform-origin: 15px 29px" />
|
||||
</g>
|
||||
<g>
|
||||
<circle r="9" cx="22" cy="22" style="transform-origin: 22px 22px" />
|
||||
</g>
|
||||
</svg>
|
||||
<div class="consul-loader" ...attributes>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="44px" height="44px" viewBox="0 0 44 44" version="1.1">
|
||||
<g>
|
||||
<circle r="1" cx="27" cy="2" style="transform-origin: 27px 2px" />
|
||||
<circle r="1" cx="17" cy="2" style="transform-origin: 17px 2px" />
|
||||
<circle r="1" cx="27" cy="42" style="transform-origin: 27px 42px" />
|
||||
<circle r="1" cx="17" cy="42" style="transform-origin: 17px 42px" />
|
||||
<circle r="1" cx="2" cy="17" style="transform-origin: 2px 17px" />
|
||||
<circle r="1" cx="2" cy="27" style="transform-origin: 2px 27px" />
|
||||
<circle r="1" cx="42" cy="17" style="transform-origin: 42px 17px" />
|
||||
<circle r="1" cx="42" cy="27" style="transform-origin: 42px 27px" />
|
||||
<circle r="1" cx="33" cy="4" style="transform-origin: 33px 4px" />
|
||||
<circle r="1" cx="11" cy="4" style="transform-origin: 11px 4px" />
|
||||
<circle r="1" cx="33" cy="40" style="transform-origin: 33px 40px" />
|
||||
<circle r="1" cx="11" cy="40" style="transform-origin: 11px 40px" />
|
||||
<circle r="1" cx="40" cy="11" style="transform-origin: 40px 11px" />
|
||||
<circle r="1" cx="4" cy="33" style="transform-origin: 4px 33px" />
|
||||
<circle r="1" cx="40" cy="33" style="transform-origin: 40px 33px" />
|
||||
<circle r="1" cx="4" cy="11" style="transform-origin: 4px 11px" />
|
||||
</g>
|
||||
<g>
|
||||
<circle r="2" cx="22" cy="4" style="transform-origin: 22px 4px" />
|
||||
<circle r="2" cx="22" cy="40" style="transform-origin: 22px 40px" />
|
||||
<circle r="2" cx="4" cy="22" style="transform-origin: 4px 22px" />
|
||||
<circle r="2" cx="40" cy="22" style="transform-origin: 40px 22px" />
|
||||
<circle r="2" cx="9" cy="9" style="transform-origin: 9px 9px" />
|
||||
<circle r="2" cx="35" cy="35" style="transform-origin: 35px 35px" />
|
||||
<circle r="2" cx="35" cy="9" style="transform-origin: 35px 9px" />
|
||||
<circle r="2" cx="9" cy="35" style="transform-origin: 9px 35px" />
|
||||
</g>
|
||||
<g>
|
||||
<circle r="2" cx="28" cy="8" style="transform-origin: 28px 8px" />
|
||||
<circle r="2" cx="16" cy="8" style="transform-origin: 16px 8px" />
|
||||
<circle r="2" cx="28" cy="36" style="transform-origin: 28px 36px" />
|
||||
<circle r="2" cx="16" cy="36" style="transform-origin: 16px 36px" />
|
||||
<circle r="2" cx="8" cy="28" style="transform-origin: 8px 28px" />
|
||||
<circle r="2" cx="8" cy="16" style="transform-origin: 8px 16px" />
|
||||
<circle r="2" cx="36" cy="28" style="transform-origin: 36px 28px" />
|
||||
<circle r="2" cx="36" cy="16" style="transform-origin: 36px 16px" />
|
||||
</g>
|
||||
<g>
|
||||
<circle r="5" cx="22" cy="12" style="transform-origin: 22px 12px" />
|
||||
<circle r="5" cx="22" cy="32" style="transform-origin: 22px 32px" />
|
||||
<circle r="5" cx="12" cy="22" style="transform-origin: 12px 22px" />
|
||||
<circle r="5" cx="32" cy="22" style="transform-origin: 32px 22px" />
|
||||
<circle r="5" cx="15" cy="15" style="transform-origin: 15px 15px" />
|
||||
<circle r="5" cx="29" cy="29" style="transform-origin: 29px 29px" />
|
||||
<circle r="5" cx="29" cy="15" style="transform-origin: 29px 15px" />
|
||||
<circle r="5" cx="15" cy="29" style="transform-origin: 15px 29px" />
|
||||
</g>
|
||||
<g>
|
||||
<circle r="9" cx="22" cy="22" style="transform-origin: 22px 22px" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
|
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.4 KiB |
|
@ -0,0 +1,40 @@
|
|||
<DataLoader @items={{item}} @src={{concat '/' nspace '/' dc '/' type '/' src}} @onchange={{action "setData"}} @once={{true}}>
|
||||
<BlockSlot @name="loaded">
|
||||
|
||||
<DataWriter
|
||||
@sink={{concat '/' nspace '/' (or data.Datacenter dc) '/' type '/'}}
|
||||
@type={{type}}
|
||||
@ondelete={{action ondelete}}
|
||||
@onchange={{action onsubmit}}
|
||||
as |writer|>
|
||||
|
||||
{{#let (hash
|
||||
data=data
|
||||
change=(action "change")
|
||||
isCreate=create
|
||||
error=writer.error
|
||||
disabled=writer.inflight
|
||||
submit=(action writer.persist data)
|
||||
delete=(action writer.delete data)
|
||||
) as |api|}}
|
||||
|
||||
{{yield api}}
|
||||
|
||||
<BlockSlot @name="error">
|
||||
<YieldSlot @name="error">
|
||||
{{yield api}}
|
||||
</YieldSlot>
|
||||
</BlockSlot>
|
||||
|
||||
<BlockSlot @name="content">
|
||||
<YieldSlot @name="form">
|
||||
{{yield api}}
|
||||
</YieldSlot>
|
||||
</BlockSlot>
|
||||
|
||||
{{/let}}
|
||||
|
||||
</DataWriter>
|
||||
|
||||
</BlockSlot>
|
||||
</DataLoader>
|
|
@ -0,0 +1,59 @@
|
|||
import Component from '@ember/component';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { set, get } from '@ember/object';
|
||||
import Slotted from 'block-slots';
|
||||
|
||||
export default Component.extend(Slotted, {
|
||||
tagName: '',
|
||||
dom: service('dom'),
|
||||
builder: service('form'),
|
||||
create: false,
|
||||
ondelete: function() {
|
||||
return this.onsubmit(...arguments);
|
||||
},
|
||||
oncancel: function() {
|
||||
return this.onsubmit(...arguments);
|
||||
},
|
||||
onsubmit: function() {},
|
||||
onchange: function(e, form) {
|
||||
return form.handleEvent(e);
|
||||
},
|
||||
didReceiveAttrs: function() {
|
||||
this._super(...arguments);
|
||||
try {
|
||||
this.form = this.builder.form(this.type);
|
||||
} catch (e) {
|
||||
// passthrough
|
||||
// this lets us load view only data that doesn't have a form
|
||||
}
|
||||
},
|
||||
willDestroyElement: function() {
|
||||
this._super(...arguments);
|
||||
if (get(this, 'data.isNew')) {
|
||||
this.data.rollbackAttributes();
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
setData: function(data) {
|
||||
let changeset = data;
|
||||
// convert to a real changeset
|
||||
if (typeof this.form !== 'undefined') {
|
||||
changeset = this.form.setData(data).getData();
|
||||
}
|
||||
// mark as creating
|
||||
// and autofill the new record if required
|
||||
if (get(changeset, 'isNew')) {
|
||||
set(this, 'create', true);
|
||||
changeset = Object.entries(this.autofill || {}).reduce(function(prev, [key, value]) {
|
||||
set(prev, key, value);
|
||||
return prev;
|
||||
}, changeset);
|
||||
}
|
||||
set(this, 'data', changeset);
|
||||
return this.data;
|
||||
},
|
||||
change: function(e, value, item) {
|
||||
this.onchange(this.dom.normalizeEvent(e, value), this.form, this.form.getData());
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,46 @@
|
|||
export default {
|
||||
id: 'data-loader',
|
||||
initial: 'load',
|
||||
on: {
|
||||
ERROR: {
|
||||
target: 'disconnected',
|
||||
},
|
||||
LOAD: [
|
||||
{
|
||||
target: 'idle',
|
||||
cond: 'loaded',
|
||||
},
|
||||
{
|
||||
target: 'loading',
|
||||
},
|
||||
],
|
||||
},
|
||||
states: {
|
||||
load: {},
|
||||
loading: {
|
||||
on: {
|
||||
SUCCESS: {
|
||||
target: 'idle',
|
||||
},
|
||||
ERROR: {
|
||||
target: 'error',
|
||||
},
|
||||
},
|
||||
},
|
||||
idle: {},
|
||||
error: {
|
||||
on: {
|
||||
RETRY: {
|
||||
target: 'load',
|
||||
},
|
||||
},
|
||||
},
|
||||
disconnected: {
|
||||
on: {
|
||||
RETRY: {
|
||||
target: 'load',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,68 @@
|
|||
{{yield}}
|
||||
<StateChart @src={{chart}} as |State Guard Action dispatch state|>
|
||||
|
||||
<Ref @target={{this}} @name="dispatch" @value={{dispatch}} />
|
||||
<Guard @name="loaded" @cond={{action "isLoaded"}} />
|
||||
|
||||
{{did-update (fn dispatch "LOAD") src=src}}
|
||||
|
||||
{{#let (hash
|
||||
data=data
|
||||
error=error
|
||||
) as |api|}}
|
||||
|
||||
{{! if we didn't specify any data}}
|
||||
{{#if (not items)}}
|
||||
{{! try and load the data if we aren't in an error state}}
|
||||
<State @notMatches={{array "error" "disconnected"}}>
|
||||
{{! but only if we only asked for a single load and we are in loading state}}
|
||||
{{#if (or (not once) (state-matches state "loading"))}}
|
||||
<DataSource
|
||||
@open={{open}}
|
||||
@src={{src}}
|
||||
@onchange={{queue (action "change" value="data") (action dispatch "SUCCESS")}}
|
||||
@onerror={{queue (action (mut error) value="error.errors.firstObject") (action dispatch "ERROR")}}
|
||||
/>
|
||||
{{/if}}
|
||||
</State>
|
||||
{{/if}}
|
||||
|
||||
<State @matches="loading">
|
||||
{{#yield-slot name="loading"}}
|
||||
{{yield api}}
|
||||
{{else}}
|
||||
<ConsulLoader />
|
||||
{{/yield-slot}}
|
||||
</State>
|
||||
|
||||
<State @matches="error">
|
||||
{{#yield-slot name="error"}}
|
||||
{{yield api}}
|
||||
{{else}}
|
||||
<ErrorState @error={{error}} />
|
||||
{{/yield-slot}}
|
||||
</State>
|
||||
|
||||
<State @matches={{array "idle" "disconnected"}}>
|
||||
|
||||
<State @matches="disconnected">
|
||||
{{#yield-slot name="disconnected" params=(block-params (component 'notification' after=(action dispatch "RESET")))}}
|
||||
{{yield api}}
|
||||
{{else}}
|
||||
<Notification @sticky={{true}}>
|
||||
<p data-notification role="alert" class="warning notification-update">
|
||||
<strong>Warning!</strong>
|
||||
An error was returned whilst loading this data, refresh to try again.
|
||||
</p>
|
||||
</Notification>
|
||||
{{/yield-slot}}
|
||||
</State>
|
||||
|
||||
<YieldSlot @name="loaded">
|
||||
{{yield api}}
|
||||
</YieldSlot>
|
||||
|
||||
</State>
|
||||
|
||||
{{/let}}
|
||||
</StateChart>
|
|
@ -0,0 +1,31 @@
|
|||
import Component from '@ember/component';
|
||||
import { set } from '@ember/object';
|
||||
import Slotted from 'block-slots';
|
||||
|
||||
import chart from './chart.xstate';
|
||||
export default Component.extend(Slotted, {
|
||||
tagName: '',
|
||||
onchange: data => data,
|
||||
init: function() {
|
||||
this._super(...arguments);
|
||||
this.chart = chart;
|
||||
},
|
||||
didReceiveAttrs: function() {
|
||||
this._super(...arguments);
|
||||
if (typeof this.items !== 'undefined') {
|
||||
this.actions.change.apply(this, [this.items]);
|
||||
}
|
||||
},
|
||||
didInsertElement: function() {
|
||||
this._super(...arguments);
|
||||
this.dispatch('LOAD');
|
||||
},
|
||||
actions: {
|
||||
isLoaded: function() {
|
||||
return typeof this.items !== 'undefined';
|
||||
},
|
||||
change: function(data) {
|
||||
set(this, 'data', this.onchange(data));
|
||||
},
|
||||
},
|
||||
});
|
|
@ -74,31 +74,35 @@ export default Component.extend({
|
|||
},
|
||||
didInsertElement: function() {
|
||||
this._super(...arguments);
|
||||
if (typeof this.data !== 'undefined') {
|
||||
this.actions.open.apply(this, [this.data]);
|
||||
if (typeof this.data !== 'undefined' || typeof this.item !== 'undefined') {
|
||||
this.actions.open.apply(this, [this.data, this.item]);
|
||||
}
|
||||
},
|
||||
persist: function(data, instance) {
|
||||
set(this, 'instance', this.service.prepare(this.sink, data, instance));
|
||||
if (typeof data !== 'undefined') {
|
||||
set(this, 'instance', this.service.prepare(this.sink, data, instance));
|
||||
} else {
|
||||
set(this, 'instance', instance);
|
||||
}
|
||||
this.source(() => this.service.persist(this.sink, this.instance));
|
||||
},
|
||||
remove: function(instance) {
|
||||
set(this, 'instance', this.service.prepare(this.sink, null, instance));
|
||||
this.source(() => this.service.remove(this.sink, this.instance));
|
||||
set(this, 'instance', instance);
|
||||
this.source(() => this.service.remove(this.sink, instance));
|
||||
},
|
||||
actions: {
|
||||
open: function(data, instance) {
|
||||
if (instance instanceof Event) {
|
||||
instance = undefined;
|
||||
open: function(data, item) {
|
||||
if (item instanceof Event) {
|
||||
item = undefined;
|
||||
}
|
||||
if (typeof data === 'undefined') {
|
||||
if (typeof data === 'undefined' && typeof item === 'undefined') {
|
||||
throw new Error('You must specify data to save, or null to remove');
|
||||
}
|
||||
// potentially allow {} and "" as 'remove' flags
|
||||
if (data === null || data === '') {
|
||||
this.remove(instance);
|
||||
this.remove(item);
|
||||
} else {
|
||||
this.persist(data, instance);
|
||||
this.persist(data, item);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
| --- | --- | --- | --- |
|
||||
| `src` | `String` | | The source to subscribe to updates to, this should map to a string based URI |
|
||||
| `loading` | `String` | eager | Allows the browser to defer loading offscreen DataSources (`eager\|lazy`). Setting to `lazy` only loads the data when the DataSource is visible in the DOM (inc. `display: none\|block;`) |
|
||||
| `open` | `Boolean` | false | Force the DataSource to open, used to force non-blocking data to refresh (has no effect for blocking data) |
|
||||
| `onchange` | `Function` | | The action to fire when the data changes. Emits an Event-like object with a `data` property containing the data. |
|
||||
| `onerror` | `Function` | | The action to fire when an error occurs. Emits ErrorEvent object with an `error` property containing the Error. |
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import Component from '@ember/component';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { set } from '@ember/object';
|
||||
import { set, get } from '@ember/object';
|
||||
import { schedule } from '@ember/runloop';
|
||||
|
||||
/**
|
||||
|
@ -72,17 +72,22 @@ export default Component.extend({
|
|||
this._lazyListeners.remove();
|
||||
}
|
||||
if (this.loading === 'eager' || this.isIntersecting) {
|
||||
this.actions.open.bind(this)();
|
||||
this.actions.open.apply(this, []);
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
// keep this argumentless
|
||||
open: function() {
|
||||
// get a new source and replace the old one, cleaning up as we go
|
||||
const source = replace(this, 'source', this.data.open(this.src, this), (prev, source) => {
|
||||
// Makes sure any previous source (if different) is ALWAYS closed
|
||||
this.data.close(prev, this);
|
||||
});
|
||||
const source = replace(
|
||||
this,
|
||||
'source',
|
||||
this.data.open(this.src, this, this.open),
|
||||
(prev, source) => {
|
||||
// Makes sure any previous source (if different) is ALWAYS closed
|
||||
this.data.close(prev, this);
|
||||
}
|
||||
);
|
||||
const error = err => {
|
||||
try {
|
||||
this.onerror(err);
|
||||
|
@ -100,7 +105,11 @@ export default Component.extend({
|
|||
error(err);
|
||||
}
|
||||
},
|
||||
error: e => error(e),
|
||||
error: e => {
|
||||
if (get(e, 'error.errors.firstObject.status') !== '429') {
|
||||
error(e);
|
||||
}
|
||||
},
|
||||
});
|
||||
replace(this, '_remove', remove);
|
||||
// dispatch the current data of the source if we have any
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
export default {
|
||||
id: 'data-writer',
|
||||
initial: 'idle',
|
||||
states: {
|
||||
idle: {
|
||||
on: {
|
||||
PERSIST: {
|
||||
target: 'persisting',
|
||||
},
|
||||
REMOVE: {
|
||||
target: 'removing',
|
||||
},
|
||||
},
|
||||
},
|
||||
removing: {
|
||||
on: {
|
||||
SUCCESS: {
|
||||
target: 'removed',
|
||||
},
|
||||
ERROR: {
|
||||
target: 'error',
|
||||
},
|
||||
},
|
||||
},
|
||||
persisting: {
|
||||
on: {
|
||||
SUCCESS: {
|
||||
target: 'persisted',
|
||||
},
|
||||
ERROR: {
|
||||
target: 'error',
|
||||
},
|
||||
},
|
||||
},
|
||||
removed: {
|
||||
on: {
|
||||
RESET: {
|
||||
target: 'idle',
|
||||
},
|
||||
},
|
||||
},
|
||||
persisted: {
|
||||
on: {
|
||||
RESET: {
|
||||
target: 'idle',
|
||||
},
|
||||
},
|
||||
},
|
||||
error: {
|
||||
on: {
|
||||
RESET: {
|
||||
target: 'idle',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,80 @@
|
|||
<StateChart @src={{chart}} as |State Guard Action dispatch state|>
|
||||
|
||||
<Ref @target={{this}} @name="dispatch" @value={{dispatch}} />
|
||||
|
||||
{{#let (hash
|
||||
data=data
|
||||
error=error
|
||||
persist=(action "persist")
|
||||
delete=(queue (action (mut data)) (action dispatch "REMOVE"))
|
||||
inflight=(state-matches state (array "persisting" "removing"))
|
||||
) as |api|}}
|
||||
|
||||
{{yield api}}
|
||||
|
||||
<State @matches="removing">
|
||||
<DataSink
|
||||
@sink={{sink}}
|
||||
@item={{data}}
|
||||
@data={{null}}
|
||||
@onchange={{action dispatch "SUCCESS"}}
|
||||
@onerror={{queue (action (mut error) value="error.errors.firstObject") (action dispatch "ERROR")}}
|
||||
/>
|
||||
</State>
|
||||
|
||||
<State @matches="persisting">
|
||||
<DataSink
|
||||
@sink={{sink}}
|
||||
@item={{data}}
|
||||
@onchange={{action dispatch "SUCCESS"}}
|
||||
@onerror={{queue (action (mut error) value="error.errors.firstObject") (action dispatch "ERROR")}}
|
||||
/>
|
||||
</State>
|
||||
|
||||
<State @matches="removed">
|
||||
<Notification @after={{queue (action dispatch "RESET") (action ondelete)}}>
|
||||
{{#yield-slot name="removed"}}
|
||||
{{yield api}}
|
||||
{{else}}
|
||||
<p data-notification role="alert" class="success notification-delete">
|
||||
<strong>Success!</strong>
|
||||
Your {{type}} has been deleted.
|
||||
</p>
|
||||
{{/yield-slot}}
|
||||
</Notification>
|
||||
</State>
|
||||
|
||||
<State @matches="persisted">
|
||||
<Notification @after={{action onchange}}>
|
||||
{{#yield-slot name="persisted"}}
|
||||
{{yield api}}
|
||||
{{else}}
|
||||
<p data-notification role="alert" class="success notification-update">
|
||||
<strong>Success!</strong>
|
||||
Your {{type}} has been saved.
|
||||
</p>
|
||||
{{/yield-slot}}
|
||||
</Notification>
|
||||
</State>
|
||||
|
||||
<State @matches="error">
|
||||
{{#yield-slot name="error" params=(block-params (component 'notification' after=(action dispatch "RESET")))}}
|
||||
{{yield api}}
|
||||
{{else}}
|
||||
<Notification @after={{action dispatch "RESET"}}>
|
||||
<p data-notification role="alert" class="error notification-update">
|
||||
<strong>Error!</strong>
|
||||
There was an error saving your {{type}}.
|
||||
{{#if (and api.error.status api.error.detail)}}
|
||||
<br />{{api.error.status}}: {{api.error.detail}}
|
||||
{{/if}}
|
||||
</p>
|
||||
</Notification>
|
||||
{{/yield-slot}}
|
||||
</State>
|
||||
<YieldSlot @name="content">
|
||||
{{yield api}}
|
||||
</YieldSlot>
|
||||
|
||||
{{/let}}
|
||||
</StateChart>
|
|
@ -0,0 +1,25 @@
|
|||
import Component from '@ember/component';
|
||||
import { set } from '@ember/object';
|
||||
import Slotted from 'block-slots';
|
||||
import chart from './chart.xstate';
|
||||
|
||||
export default Component.extend(Slotted, {
|
||||
tagName: '',
|
||||
ondelete: function() {
|
||||
return this.onchange(...arguments);
|
||||
},
|
||||
onchange: function() {},
|
||||
init: function() {
|
||||
this._super(...arguments);
|
||||
this.chart = chart;
|
||||
},
|
||||
actions: {
|
||||
persist: function(data, e) {
|
||||
if (e && typeof e.preventDefault === 'function') {
|
||||
e.preventDefault();
|
||||
}
|
||||
set(this, 'data', data);
|
||||
this.dispatch('PERSIST');
|
||||
},
|
||||
},
|
||||
});
|
|
@ -1,7 +1,7 @@
|
|||
<p>
|
||||
{{ message }}
|
||||
{{message}}
|
||||
</p>
|
||||
<button type="button" class="type-delete" {{action execute}}>
|
||||
<button type="button" class="type-delete" onclick={{action execute}}>
|
||||
Confirm Delete
|
||||
</button>
|
||||
<button type="button" class="type-cancel" {{action cancel}}>Cancel</button>
|
||||
<button type="button" class="type-cancel" onclick={{action cancel}}>Cancel</button>
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
{{#if (not-eq error.status "403")}}
|
||||
<EmptyState class={{concat "status-" error.status}}>
|
||||
<BlockSlot @name="header">
|
||||
<h2>{{or error.message "Consul returned an error"}}</h2>
|
||||
</BlockSlot>
|
||||
{{#if error.status }}
|
||||
<BlockSlot @name="subheader">
|
||||
<h3 data-test-status={{error.status}}>Error {{error.status}}</h3>
|
||||
</BlockSlot>
|
||||
{{/if}}
|
||||
<BlockSlot @name="body">
|
||||
<p>
|
||||
You may have visited a URL that is loading an unknown resource, so you can try going back to the root or try re-submitting your ACL Token/SecretID by going back to ACLs.
|
||||
</p>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="actions">
|
||||
<li class="back-link">
|
||||
<a data-test-home rel="home" href={{href-to 'index'}}>Go back to root</a>
|
||||
</li>
|
||||
<li class="docs-link">
|
||||
<a href="{{env 'CONSUL_DOCS_URL'}}" rel="noopener noreferrer" target="_blank">Read the documentation</a>
|
||||
</li>
|
||||
</BlockSlot>
|
||||
</EmptyState>
|
||||
{{else}}
|
||||
<EmptyState class="status-403">
|
||||
<BlockSlot @name="header">
|
||||
<h2 data-test-status={{error.status}}>You are not authorized</h2>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="subheader">
|
||||
<h3>Error 403</h3>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="body">
|
||||
<p>
|
||||
You must be granted permissions to view this data. Ask your administrator if you think you should have access.
|
||||
</p>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="actions">
|
||||
<li class="docs-link">
|
||||
<a href="{{env 'CONSUL_DOCS_URL'}}/acl/index.html" rel="noopener noreferrer" target="_blank">Read the documentation</a>
|
||||
</li>
|
||||
<li class="learn-link">
|
||||
<a href="{{env 'CONSUL_DOCS_LEARN_URL'}}/consul/security-networking/production-acls" rel="noopener noreferrer" target="_blank">Follow the guide</a>
|
||||
</li>
|
||||
</BlockSlot>
|
||||
</EmptyState>
|
||||
{{/if}}
|
|
@ -0,0 +1,5 @@
|
|||
import Component from '@ember/component';
|
||||
|
||||
export default Component.extend({
|
||||
tagName: '',
|
||||
});
|
|
@ -0,0 +1,3 @@
|
|||
<div id={{guid}}>
|
||||
{{yield}}
|
||||
</div>
|
|
@ -0,0 +1,34 @@
|
|||
import Component from '@ember/component';
|
||||
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default Component.extend({
|
||||
tagName: '',
|
||||
notify: service('flashMessages'),
|
||||
dom: service('dom'),
|
||||
oncomplete: function() {},
|
||||
init: function() {
|
||||
this._super(...arguments);
|
||||
this.guid = this.dom.guid(this);
|
||||
},
|
||||
didInsertElement: function() {
|
||||
const $el = this.dom.element(`#${this.guid}`);
|
||||
const options = {
|
||||
timeout: 6000,
|
||||
extendedTimeout: 300,
|
||||
dom: $el.innerHTML,
|
||||
};
|
||||
if (this.sticky) {
|
||||
options.sticky = true;
|
||||
}
|
||||
$el.remove();
|
||||
this.notify.clearMessages();
|
||||
if (typeof this.after === 'function') {
|
||||
Promise.resolve(this.after()).then(res => {
|
||||
this.notify.add(options);
|
||||
});
|
||||
} else {
|
||||
this.notify.add(options);
|
||||
}
|
||||
},
|
||||
});
|
|
@ -12,6 +12,8 @@ export default Component.extend({
|
|||
let match = true;
|
||||
if (typeof this.matches !== 'undefined') {
|
||||
match = this.service.matches(this.state, this.matches);
|
||||
} else if (typeof this.notMatches !== 'undefined') {
|
||||
match = !this.service.matches(this.state, this.notMatches);
|
||||
}
|
||||
set(this, 'rendering', match);
|
||||
},
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
import Controller from './edit';
|
||||
export default Controller.extend();
|
|
@ -1,8 +0,0 @@
|
|||
import Controller from '@ember/controller';
|
||||
export default Controller.extend({
|
||||
actions: {
|
||||
route: function() {
|
||||
this.send(...arguments);
|
||||
},
|
||||
},
|
||||
});
|
|
@ -9,9 +9,4 @@ export default Controller.extend({
|
|||
replace: true,
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
route: function() {
|
||||
this.send(...arguments);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import Controller from '@ember/controller';
|
||||
|
||||
export default Controller.extend({
|
||||
queryParams: {
|
||||
filterBy: {
|
||||
|
@ -10,9 +9,4 @@ export default Controller.extend({
|
|||
replace: true,
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
route: function() {
|
||||
this.send(...arguments);
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,14 @@
|
|||
import Helper from '@ember/component/helper';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { getOwner } from '@ember/application';
|
||||
|
||||
export default Helper.extend({
|
||||
router: service('router'),
|
||||
compute(params, hash) {
|
||||
return () => {
|
||||
const container = getOwner(this);
|
||||
const routeName = this.router.currentRoute.name;
|
||||
return container.lookup(`route:${routeName}`).refresh();
|
||||
};
|
||||
},
|
||||
});
|
|
@ -1,74 +0,0 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import { env } from 'consul-ui/env';
|
||||
|
||||
import { routes } from 'consul-ui/router';
|
||||
import flat from 'flat';
|
||||
|
||||
let initialize = function() {};
|
||||
Route.reopen(
|
||||
['modelFor', 'transitionTo', 'replaceWith', 'paramsFor'].reduce(function(prev, item) {
|
||||
prev[item] = function(routeName, ...rest) {
|
||||
const isNspaced = this.routeName.startsWith('nspace.');
|
||||
if (routeName === 'nspace') {
|
||||
if (isNspaced || this.routeName === 'nspace') {
|
||||
return this._super(...arguments);
|
||||
} else {
|
||||
return {
|
||||
nspace: '~',
|
||||
};
|
||||
}
|
||||
}
|
||||
if (isNspaced && routeName.startsWith('dc')) {
|
||||
return this._super(...[`nspace.${routeName}`, ...rest]);
|
||||
}
|
||||
return this._super(...arguments);
|
||||
};
|
||||
return prev;
|
||||
}, {})
|
||||
);
|
||||
if (env('CONSUL_NSPACES_ENABLED')) {
|
||||
const dotRe = /\./g;
|
||||
initialize = function(container) {
|
||||
const register = function(route, path) {
|
||||
route.reopen({
|
||||
templateName: path
|
||||
.replace('/root-create', '/create')
|
||||
.replace('/create', '/edit')
|
||||
.replace('/folder', '/index'),
|
||||
});
|
||||
container.register(`route:nspace/${path}`, route);
|
||||
const controller = container.resolveRegistration(`controller:${path}`);
|
||||
if (controller) {
|
||||
container.register(`controller:nspace/${path}`, controller);
|
||||
}
|
||||
};
|
||||
const all = Object.keys(flat(routes))
|
||||
.filter(function(item) {
|
||||
return item.startsWith('dc');
|
||||
})
|
||||
.map(function(item) {
|
||||
return item.replace('._options.path', '').replace(dotRe, '/');
|
||||
});
|
||||
all.forEach(function(item) {
|
||||
let route = container.resolveRegistration(`route:${item}`);
|
||||
let indexed;
|
||||
// if the route doesn't exist it probably has an index route instead
|
||||
if (!route) {
|
||||
item = `${item}/index`;
|
||||
route = container.resolveRegistration(`route:${item}`);
|
||||
} else {
|
||||
// if the route does exists
|
||||
// then check to see if it also has an index route
|
||||
indexed = `${item}/index`;
|
||||
const index = container.resolveRegistration(`route:${indexed}`);
|
||||
if (typeof index !== 'undefined') {
|
||||
register(index, indexed);
|
||||
}
|
||||
}
|
||||
register(route, item);
|
||||
});
|
||||
};
|
||||
}
|
||||
export default {
|
||||
initialize,
|
||||
};
|
|
@ -1,11 +1,108 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import { routes } from 'consul-ui/router';
|
||||
import { env } from 'consul-ui/env';
|
||||
import flat from 'flat';
|
||||
|
||||
const withNspace = function(currentRouteName, requestedRouteName, ...rest) {
|
||||
const isNspaced = currentRouteName.startsWith('nspace.');
|
||||
if (isNspaced && requestedRouteName.startsWith('dc')) {
|
||||
return [`nspace.${requestedRouteName}`, ...rest];
|
||||
}
|
||||
return [requestedRouteName, ...rest];
|
||||
};
|
||||
|
||||
const register = function(container, route, path) {
|
||||
route.reopen({
|
||||
templateName: path
|
||||
.replace('/root-create', '/create')
|
||||
.replace('/create', '/edit')
|
||||
.replace('/folder', '/index'),
|
||||
});
|
||||
container.register(`route:nspace/${path}`, route);
|
||||
const controller = container.resolveRegistration(`controller:${path}`);
|
||||
if (controller) {
|
||||
container.register(`controller:nspace/${path}`, controller);
|
||||
}
|
||||
};
|
||||
|
||||
export function initialize(container) {
|
||||
// patch Route routeName-like methods for navigation to support nspace relative routes
|
||||
Route.reopen(
|
||||
['transitionTo', 'replaceWith'].reduce(function(prev, item) {
|
||||
prev[item] = function(requestedRouteName, ...rest) {
|
||||
return this._super(...withNspace(this.routeName, requestedRouteName, ...rest));
|
||||
};
|
||||
return prev;
|
||||
}, {})
|
||||
);
|
||||
|
||||
// patch Route routeName-like methods for data to support nspace relative routes
|
||||
Route.reopen(
|
||||
['modelFor', 'paramsFor'].reduce(function(prev, item) {
|
||||
prev[item] = function(requestedRouteName, ...rest) {
|
||||
const isNspaced = this.routeName.startsWith('nspace.');
|
||||
if (requestedRouteName === 'nspace' && !isNspaced && this.routeName !== 'nspace') {
|
||||
return {
|
||||
nspace: '~',
|
||||
};
|
||||
}
|
||||
return this._super(...withNspace(this.routeName, requestedRouteName, ...rest));
|
||||
};
|
||||
return prev;
|
||||
}, {})
|
||||
);
|
||||
|
||||
// extend router service with a nspace aware router to support nspace relative routes
|
||||
const nspacedRouter = container.resolveRegistration('service:router').extend({
|
||||
transitionTo: function(requestedRouteName, ...rest) {
|
||||
return this._super(...withNspace(this.currentRoute.name, requestedRouteName, ...rest));
|
||||
},
|
||||
replaceWith: function(requestedRouteName, ...rest) {
|
||||
return this._super(...withNspace(this.currentRoute.name, requestedRouteName, ...rest));
|
||||
},
|
||||
urlFor: function(requestedRouteName, ...rest) {
|
||||
return this._super(...withNspace(this.currentRoute.name, requestedRouteName, ...rest));
|
||||
},
|
||||
});
|
||||
container.register('service:router', nspacedRouter);
|
||||
|
||||
if (env('CONSUL_NSPACES_ENABLED')) {
|
||||
// enable the nspace repo
|
||||
['dc', 'settings', 'dc.intentions.edit', 'dc.intentions.create'].forEach(function(item) {
|
||||
container.inject(`route:${item}`, 'nspacesRepo', 'service:repository/nspace/enabled');
|
||||
container.inject(`route:nspace.${item}`, 'nspacesRepo', 'service:repository/nspace/enabled');
|
||||
});
|
||||
container.inject('route:application', 'nspacesRepo', 'service:repository/nspace/enabled');
|
||||
|
||||
const dotRe = /\./g;
|
||||
// register automatic 'index' routes and controllers that start with 'dc'
|
||||
Object.keys(flat(routes))
|
||||
.filter(function(item) {
|
||||
return item.startsWith('dc');
|
||||
})
|
||||
.map(function(item) {
|
||||
return item.replace('._options.path', '').replace(dotRe, '/');
|
||||
})
|
||||
.forEach(function(item) {
|
||||
let route = container.resolveRegistration(`route:${item}`);
|
||||
let indexed;
|
||||
// if the route doesn't exist it probably has an index route instead
|
||||
if (!route) {
|
||||
item = `${item}/index`;
|
||||
route = container.resolveRegistration(`route:${item}`);
|
||||
} else {
|
||||
// if the route does exist
|
||||
// then check to see if it also has an index route
|
||||
indexed = `${item}/index`;
|
||||
const index = container.resolveRegistration(`route:${indexed}`);
|
||||
if (typeof index !== 'undefined') {
|
||||
register(container, index, indexed);
|
||||
}
|
||||
}
|
||||
register(container, route, item);
|
||||
});
|
||||
|
||||
// tell the view we have nspaces enabled
|
||||
container
|
||||
.lookup('service:dom')
|
||||
.root()
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
import Mixin from '@ember/object/mixin';
|
||||
import WithBlockingActions from 'consul-ui/mixins/with-blocking-actions';
|
||||
import { get } from '@ember/object';
|
||||
|
||||
import { INTERNAL_SERVER_ERROR as HTTP_INTERNAL_SERVER_ERROR } from 'consul-ui/utils/http/status';
|
||||
export default Mixin.create(WithBlockingActions, {
|
||||
errorCreate: function(type, e) {
|
||||
if (e && e.errors && e.errors[0]) {
|
||||
const error = e.errors[0];
|
||||
if (parseInt(error.status) === HTTP_INTERNAL_SERVER_ERROR) {
|
||||
if (error.detail.indexOf('duplicate intention found:') === 0) {
|
||||
return 'exists';
|
||||
}
|
||||
}
|
||||
}
|
||||
return type;
|
||||
},
|
||||
afterUpdate: function(item) {
|
||||
if (get(this, 'history.length') > 0) {
|
||||
return this.transitionTo(this.history[0].key, this.history[0].value);
|
||||
}
|
||||
return this._super(...arguments);
|
||||
},
|
||||
afterCreate: function(item) {
|
||||
if (get(this, 'history.length') > 0) {
|
||||
return this.transitionTo(this.history[0].key, this.history[0].value);
|
||||
}
|
||||
return this._super(...arguments);
|
||||
},
|
||||
afterDelete: function(item) {
|
||||
if (get(this, 'history.length') > 0) {
|
||||
return this.transitionTo(this.history[0].key, this.history[0].value);
|
||||
}
|
||||
if (this.routeName === 'dc.services.show') {
|
||||
return this.transitionTo(this.routeName, this._router.currentRoute.params.name);
|
||||
}
|
||||
return this._super(...arguments);
|
||||
},
|
||||
});
|
|
@ -8,12 +8,12 @@ export default Model.extend({
|
|||
[SLUG_KEY]: attr('string'),
|
||||
Description: attr('string'),
|
||||
SourceNS: attr('string'),
|
||||
SourceName: attr('string'),
|
||||
DestinationName: attr('string'),
|
||||
SourceName: attr('string', { defaultValue: '*' }),
|
||||
DestinationName: attr('string', { defaultValue: '*' }),
|
||||
DestinationNS: attr('string'),
|
||||
Precedence: attr('number'),
|
||||
SourceType: attr('string', { defaultValue: 'consul' }),
|
||||
Action: attr('string', { defaultValue: 'deny' }),
|
||||
Action: attr('string', { defaultValue: 'allow' }),
|
||||
Meta: attr(),
|
||||
SyncTime: attr('number'),
|
||||
Datacenter: attr('string'),
|
||||
|
|
|
@ -18,6 +18,12 @@ export const routes = {
|
|||
},
|
||||
intentions: {
|
||||
_options: { path: '/intentions' },
|
||||
edit: {
|
||||
_options: { path: '/:intention' },
|
||||
},
|
||||
create: {
|
||||
_options: { path: '/create' },
|
||||
},
|
||||
},
|
||||
services: {
|
||||
_options: { path: '/services' },
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { hash } from 'rsvp';
|
||||
import { next } from '@ember/runloop';
|
||||
import { get, set } from '@ember/object';
|
||||
|
||||
import WithBlockingActions from 'consul-ui/mixins/with-blocking-actions';
|
||||
|
||||
const removeLoading = function($from) {
|
||||
return $from.classList.remove('ember-loading');
|
||||
return $from.classList.remove('ember-loading', 'loading');
|
||||
};
|
||||
export default Route.extend(WithBlockingActions, {
|
||||
dom: service('dom'),
|
||||
|
@ -38,31 +38,16 @@ export default Route.extend(WithBlockingActions, {
|
|||
},
|
||||
actions: {
|
||||
loading: function(transition, originRoute) {
|
||||
const from = get(transition, 'from.name') || 'application';
|
||||
const controller = this.controllerFor(from);
|
||||
|
||||
set(controller, 'loading', true);
|
||||
const $root = this.dom.root();
|
||||
let dc = null;
|
||||
if (originRoute.routeName !== 'dc' && originRoute.routeName !== 'application') {
|
||||
const app = this.modelFor('application');
|
||||
const model = this.modelFor('dc') || { dc: { Name: null } };
|
||||
dc = this.repo.getActive(model.dc.Name, app.dcs);
|
||||
}
|
||||
hash({
|
||||
loading: !$root.classList.contains('ember-loading'),
|
||||
dc: dc,
|
||||
nspace: this.nspacesRepo.getActive(),
|
||||
}).then(model => {
|
||||
next(() => {
|
||||
const controller = this.controllerFor('application');
|
||||
controller.setProperties(model);
|
||||
transition.promise.finally(function() {
|
||||
removeLoading($root);
|
||||
controller.setProperties({
|
||||
loading: false,
|
||||
dc: model.dc,
|
||||
});
|
||||
});
|
||||
});
|
||||
$root.classList.add('loading');
|
||||
transition.promise.finally(() => {
|
||||
set(controller, 'loading', false);
|
||||
removeLoading($root);
|
||||
});
|
||||
return true;
|
||||
},
|
||||
error: function(e, transition) {
|
||||
// TODO: Normalize all this better
|
||||
|
|
|
@ -1,45 +1,5 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { hash } from 'rsvp';
|
||||
import { get } from '@ember/object';
|
||||
import WithIntentionActions from 'consul-ui/mixins/intention/with-actions';
|
||||
import Route from './edit';
|
||||
|
||||
// TODO: This route and the edit Route need merging somehow
|
||||
export default Route.extend(WithIntentionActions, {
|
||||
export default Route.extend({
|
||||
templateName: 'dc/intentions/edit',
|
||||
repo: service('repository/intention'),
|
||||
servicesRepo: service('repository/service'),
|
||||
nspacesRepo: service('repository/nspace/disabled'),
|
||||
model: function(params) {
|
||||
const dc = this.modelFor('dc').dc.Name;
|
||||
const nspace = '*';
|
||||
this.item = this.repo.create({
|
||||
Datacenter: dc,
|
||||
});
|
||||
return hash({
|
||||
create: true,
|
||||
isLoading: false,
|
||||
item: this.item,
|
||||
services: this.servicesRepo.findAllByDatacenter(dc, nspace),
|
||||
nspaces: this.nspacesRepo.findAll(),
|
||||
}).then(function(model) {
|
||||
return {
|
||||
...model,
|
||||
...{
|
||||
services: [{ Name: '*' }].concat(
|
||||
model.services.toArray().filter(item => get(item, 'Kind') !== 'connect-proxy')
|
||||
),
|
||||
nspaces: [{ Name: '*' }].concat(model.nspaces.toArray()),
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
setupController: function(controller, model) {
|
||||
controller.setProperties(model);
|
||||
},
|
||||
deactivate: function() {
|
||||
if (get(this.item, 'isNew')) {
|
||||
this.item.rollbackAttributes();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,49 +1,18 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { hash } from 'rsvp';
|
||||
import { get } from '@ember/object';
|
||||
|
||||
import WithIntentionActions from 'consul-ui/mixins/intention/with-actions';
|
||||
|
||||
// TODO: This route and the create Route need merging somehow
|
||||
export default Route.extend(WithIntentionActions, {
|
||||
export default Route.extend({
|
||||
repo: service('repository/intention'),
|
||||
servicesRepo: service('repository/service'),
|
||||
nspacesRepo: service('repository/nspace/disabled'),
|
||||
buildRouteInfoMetadata: function() {
|
||||
return { history: this.history };
|
||||
},
|
||||
model: function(params, transition) {
|
||||
const from = get(transition, 'from');
|
||||
this.history = [];
|
||||
if (from && get(from, 'name') === 'dc.services.show.intentions') {
|
||||
this.history.push({
|
||||
key: get(from, 'name'),
|
||||
value: get(from, 'parent.params.name'),
|
||||
});
|
||||
}
|
||||
|
||||
const dc = this.modelFor('dc').dc.Name;
|
||||
// We load all of your services that you are able to see here
|
||||
// as even if it doesn't exist in the namespace you are targetting
|
||||
// you may want to add it after you've added the intention
|
||||
const nspace = '*';
|
||||
return hash({
|
||||
isLoading: false,
|
||||
item: this.repo.findBySlug(params.id, dc, nspace),
|
||||
services: this.servicesRepo.findAllByDatacenter(dc, nspace),
|
||||
nspaces: this.nspacesRepo.findAll(),
|
||||
history: this.history,
|
||||
}).then(function(model) {
|
||||
return {
|
||||
...model,
|
||||
...{
|
||||
services: [{ Name: '*' }].concat(
|
||||
model.services.toArray().filter(item => get(item, 'Kind') !== 'connect-proxy')
|
||||
),
|
||||
nspaces: [{ Name: '*' }].concat(model.nspaces.toArray()),
|
||||
},
|
||||
};
|
||||
dc: dc,
|
||||
nspace: nspace,
|
||||
item:
|
||||
typeof params.id !== 'undefined' ? this.repo.findBySlug(params.id, dc, nspace) : undefined,
|
||||
});
|
||||
},
|
||||
setupController: function(controller, model) {
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { hash } from 'rsvp';
|
||||
|
||||
import WithIntentionActions from 'consul-ui/mixins/intention/with-actions';
|
||||
|
||||
export default Route.extend(WithIntentionActions, {
|
||||
repo: service('repository/intention'),
|
||||
export default Route.extend({
|
||||
queryParams: {
|
||||
filterBy: {
|
||||
as: 'action',
|
||||
|
@ -16,12 +11,10 @@ export default Route.extend(WithIntentionActions, {
|
|||
},
|
||||
},
|
||||
model: function(params) {
|
||||
return hash({
|
||||
items: this.repo.findAllByDatacenter(
|
||||
this.modelFor('dc').dc.Name,
|
||||
this.modelFor('nspace').nspace.substr(1)
|
||||
),
|
||||
});
|
||||
return {
|
||||
dc: this.modelFor('dc').dc.Name,
|
||||
nspace: this.modelFor('nspace').nspace.substr(1) || 'default',
|
||||
};
|
||||
},
|
||||
setupController: function(controller, model) {
|
||||
controller.setProperties(model);
|
||||
|
|
|
@ -5,7 +5,6 @@ import { get } from '@ember/object';
|
|||
|
||||
export default Route.extend({
|
||||
repo: service('repository/service'),
|
||||
intentionRepo: service('repository/intention'),
|
||||
chainRepo: service('repository/discovery-chain'),
|
||||
proxyRepo: service('repository/proxy'),
|
||||
settings: service('settings'),
|
||||
|
@ -13,6 +12,7 @@ export default Route.extend({
|
|||
const dc = this.modelFor('dc').dc.Name;
|
||||
const nspace = this.modelFor('nspace').nspace.substr(1);
|
||||
return hash({
|
||||
slug: params.name,
|
||||
dc: dc,
|
||||
nspace: nspace || 'default',
|
||||
item: this.repo.findBySlug(params.name, dc, nspace),
|
||||
|
@ -25,11 +25,6 @@ export default Route.extend({
|
|||
)
|
||||
? model
|
||||
: hash({
|
||||
intentions: this.intentionRepo
|
||||
.findByService(params.name, dc, nspace)
|
||||
.catch(function() {
|
||||
return null;
|
||||
}),
|
||||
chain: this.chainRepo.findBySlug(params.name, dc, nspace),
|
||||
proxies: this.proxyRepo.findAllBySlug(params.name, dc, nspace),
|
||||
...model,
|
||||
|
|
|
@ -1,27 +1,3 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import WithIntentionActions from 'consul-ui/mixins/intention/with-actions';
|
||||
|
||||
export default Route.extend(WithIntentionActions, {
|
||||
queryParams: {
|
||||
search: {
|
||||
as: 'filter',
|
||||
replace: true,
|
||||
},
|
||||
},
|
||||
repo: service('repository/intention'),
|
||||
model: function() {
|
||||
const parent = this.routeName
|
||||
.split('.')
|
||||
.slice(0, -1)
|
||||
.join('.');
|
||||
return this.modelFor(parent);
|
||||
},
|
||||
setupController: function(controller, model) {
|
||||
controller.setProperties(model);
|
||||
},
|
||||
// Overwrite default afterDelete action to just refresh
|
||||
afterDelete: function() {
|
||||
return this.refresh();
|
||||
},
|
||||
});
|
||||
export default Route.extend();
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
import Route from './edit';
|
||||
|
||||
export default Route.extend({
|
||||
templateName: 'dc/services/show/intentions/edit',
|
||||
});
|
|
@ -0,0 +1,15 @@
|
|||
import Route from '@ember/routing/route';
|
||||
|
||||
export default Route.extend({
|
||||
model: function(params, transition) {
|
||||
return {
|
||||
nspace: '*',
|
||||
dc: this.paramsFor('dc').dc,
|
||||
service: this.paramsFor('dc.services.show').name,
|
||||
src: params.intention,
|
||||
};
|
||||
},
|
||||
setupController: function(controller, model) {
|
||||
controller.setProperties(model);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,20 @@
|
|||
import Route from '@ember/routing/route';
|
||||
|
||||
export default Route.extend({
|
||||
queryParams: {
|
||||
search: {
|
||||
as: 'filter',
|
||||
replace: true,
|
||||
},
|
||||
},
|
||||
model: function(params) {
|
||||
return {
|
||||
dc: this.modelFor('dc').dc.Name,
|
||||
nspace: this.modelFor('nspace').nspace.substr(1) || 'default',
|
||||
slug: this.paramsFor('dc.services.show').name,
|
||||
};
|
||||
},
|
||||
setupController: function(controller, model) {
|
||||
controller.setProperties(model);
|
||||
},
|
||||
});
|
|
@ -5,27 +5,15 @@ export default Service.extend({
|
|||
settings: service('settings'),
|
||||
intention: service('repository/intention'),
|
||||
prepare: function(sink, data, instance) {
|
||||
const [, dc, nspace, model, slug] = sink.split('/');
|
||||
const repo = this[model];
|
||||
if (slug === '') {
|
||||
instance = repo.create({
|
||||
Datacenter: dc,
|
||||
Namespace: nspace,
|
||||
});
|
||||
} else {
|
||||
if (typeof instance === 'undefined') {
|
||||
instance = repo.peek(slug);
|
||||
}
|
||||
}
|
||||
return setProperties(instance, data);
|
||||
},
|
||||
persist: function(sink, instance) {
|
||||
const [, , , /*dc*/ /*nspace*/ model] = sink.split('/');
|
||||
const [, , , model] = sink.split('/');
|
||||
const repo = this[model];
|
||||
return repo.persist(instance);
|
||||
},
|
||||
remove: function(sink, instance) {
|
||||
const [, , , /*dc*/ /*nspace*/ model] = sink.split('/');
|
||||
const [, , , model] = sink.split('/');
|
||||
const repo = this[model];
|
||||
return repo.remove(instance);
|
||||
},
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import Service, { inject as service } from '@ember/service';
|
||||
|
||||
const parts = function(uri) {
|
||||
if (uri.indexOf('://') === -1) {
|
||||
uri = `consul://${uri}`;
|
||||
}
|
||||
return uri.split('://');
|
||||
};
|
||||
export default Service.extend({
|
||||
data: service('data-sink/protocols/http'),
|
||||
consul: service('data-sink/protocols/http'),
|
||||
settings: service('data-sink/protocols/local-storage'),
|
||||
|
||||
prepare: function(uri, data, assign) {
|
||||
|
|
|
@ -3,11 +3,16 @@ import { get } from '@ember/object';
|
|||
|
||||
export default Service.extend({
|
||||
datacenters: service('repository/dc'),
|
||||
services: service('repository/service'),
|
||||
namespaces: service('repository/nspace'),
|
||||
intentions: service('repository/intention'),
|
||||
intention: service('repository/intention'),
|
||||
kv: service('repository/kv'),
|
||||
token: service('repository/token'),
|
||||
policies: service('repository/policy'),
|
||||
policy: service('repository/policy'),
|
||||
roles: service('repository/role'),
|
||||
|
||||
oidc: service('repository/oidc-provider'),
|
||||
type: service('data-source/protocols/http/blocking'),
|
||||
source: function(src, configuration) {
|
||||
|
@ -33,14 +38,13 @@ export default Service.extend({
|
|||
let method, slug;
|
||||
switch (model) {
|
||||
case 'datacenters':
|
||||
find = configuration => repo.findAll(configuration);
|
||||
break;
|
||||
case 'namespaces':
|
||||
find = configuration => repo.findAll(configuration);
|
||||
break;
|
||||
case 'token':
|
||||
find = configuration => repo.self(rest[1], dc);
|
||||
break;
|
||||
case 'services':
|
||||
case 'roles':
|
||||
case 'policies':
|
||||
find = configuration => repo.findAllByDatacenter(dc, nspace, configuration);
|
||||
|
@ -48,6 +52,28 @@ export default Service.extend({
|
|||
case 'policy':
|
||||
find = configuration => repo.findBySlug(rest[0], dc, nspace, configuration);
|
||||
break;
|
||||
case 'intentions':
|
||||
[method, ...slug] = rest;
|
||||
switch (method) {
|
||||
case 'for-service':
|
||||
// TODO: Are we going to need to encode/decode here...?
|
||||
find = configuration => repo.findByService(slug.join('/'), dc, nspace, configuration);
|
||||
break;
|
||||
default:
|
||||
find = configuration => repo.findAllByDatacenter(dc, nspace, configuration);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'intention':
|
||||
// TODO: Are we going to need to encode/decode here...?
|
||||
slug = rest.join('/');
|
||||
if (slug) {
|
||||
find = configuration => repo.findBySlug(slug, dc, nspace, configuration);
|
||||
} else {
|
||||
find = configuration =>
|
||||
Promise.resolve(repo.create({ Datacenter: dc, Namespace: nspace }));
|
||||
}
|
||||
break;
|
||||
case 'oidc':
|
||||
[method, ...slug] = rest;
|
||||
switch (method) {
|
||||
|
|
|
@ -36,7 +36,7 @@ export default Service.extend({
|
|||
usage = null;
|
||||
},
|
||||
|
||||
open: function(uri, ref) {
|
||||
open: function(uri, ref, open = false) {
|
||||
let source;
|
||||
// Check the cache for an EventSource that is already being used
|
||||
// for this uri. If we don't have one, set one up.
|
||||
|
@ -61,21 +61,30 @@ export default Service.extend({
|
|||
// only cache data if we have any
|
||||
if (typeof event !== 'undefined' && typeof cursor !== 'undefined') {
|
||||
cache.set(uri, {
|
||||
currentEvent: source.getCurrentEvent(),
|
||||
cursor: source.configuration.cursor,
|
||||
currentEvent: event,
|
||||
cursor: cursor,
|
||||
});
|
||||
}
|
||||
// the data is cached delete the EventSource
|
||||
sources.delete(uri);
|
||||
if (!usage.has(source)) {
|
||||
sources.delete(uri);
|
||||
}
|
||||
},
|
||||
});
|
||||
sources.set(uri, source);
|
||||
} else {
|
||||
source = sources.get(uri);
|
||||
}
|
||||
// only open if its not already being used
|
||||
// in the case of blocking queries being disabled
|
||||
// you may want to specifically force an open
|
||||
// if blocking queries are enabled then opening an already
|
||||
// open blocking query does nothing
|
||||
if (!usage.has(source) || open) {
|
||||
source.open();
|
||||
}
|
||||
// set/increase the usage counter
|
||||
usage.set(source, ref);
|
||||
source.open();
|
||||
return source;
|
||||
},
|
||||
close: function(source, ref) {
|
||||
|
|
|
@ -32,7 +32,7 @@ export default Service.extend({
|
|||
}
|
||||
}
|
||||
const date = get(item, 'SyncTime');
|
||||
if (typeof date !== 'undefined' && date != meta.date) {
|
||||
if (!item.isDeleted && typeof date !== 'undefined' && date != meta.date) {
|
||||
this.store.unloadRecord(item);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,9 +8,15 @@ export default RepositoryService.extend({
|
|||
getPrimaryKey: function() {
|
||||
return PRIMARY_KEY;
|
||||
},
|
||||
create: function(obj) {
|
||||
delete obj.Namespace;
|
||||
return this._super(obj);
|
||||
},
|
||||
shouldReconcile: function(method) {
|
||||
switch (method) {
|
||||
case 'findByService':
|
||||
// TODO: This is to be switched out for something at an adapter level
|
||||
// so it works for both methods of interacting with data-sources
|
||||
switch (true) {
|
||||
case method === 'findByService' || method.indexOf('for-service') !== -1:
|
||||
return false;
|
||||
}
|
||||
return this._super(...arguments);
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
@import 'core/layout';
|
||||
|
||||
@import 'routes/dc/settings/index';
|
||||
@import 'routes/dc/services/index';
|
||||
@import 'routes/dc/nodes/index';
|
||||
@import 'routes/dc/intention/index';
|
||||
@import 'routes/dc/kv/index';
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
padding: 1px 0 30px 0;
|
||||
}
|
||||
%app-view-content-empty {
|
||||
margin-top: 0;
|
||||
margin-top: 0 !important;
|
||||
padding: 50px;
|
||||
text-align: center;
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
%empty-state[class*='status-'] header::before {
|
||||
@extend %as-pseudo;
|
||||
}
|
||||
%empty-state.status- header::before {
|
||||
%empty-state header::before {
|
||||
@extend %with-alert-circle-outline-mask;
|
||||
}
|
||||
%empty-state.status-404 header::before {
|
||||
|
@ -32,9 +32,6 @@
|
|||
%empty-state.status-403 header::before {
|
||||
@extend %with-disabled-mask;
|
||||
}
|
||||
%empty-state[class*='status-5'] header::before {
|
||||
@extend %with-alert-circle-outline-mask;
|
||||
}
|
||||
%empty-state li[class*='-link'] > *::after {
|
||||
@extend %as-pseudo;
|
||||
margin-left: 5px;
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
@import './loader/index';
|
||||
html.template-loading main > div {
|
||||
.consul-loader {
|
||||
@extend %loader;
|
||||
}
|
||||
%loader circle {
|
||||
fill: $magenta-100;
|
||||
}
|
||||
html:not(.loading) .view-loader {
|
||||
display: none;
|
||||
}
|
||||
html.loading .app-view {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
@ -3,5 +3,9 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: calc(100vh - 90px - 48px - 50px);
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
@media #{$--horizontal-tabs} {
|
||||
.template-service.template-show main header .actions {
|
||||
position: relative;
|
||||
top: 48px;
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
|
@ -10,14 +10,7 @@
|
|||
@nspace={{or nspace nspaces.firstObject}}
|
||||
@onchange={{action "reauthorize"}}
|
||||
>
|
||||
{{#if (not loading)}}
|
||||
{{outlet}}
|
||||
{{else}}
|
||||
<AppView @class="loading show">
|
||||
<BlockSlot @name="content">
|
||||
<ConsulLoader />
|
||||
</BlockSlot>
|
||||
</AppView>
|
||||
{{/if}}
|
||||
<ConsulLoader class="view-loader" />
|
||||
</HashicorpConsul>
|
||||
{{/if}}
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
{{#if (eq type 'create')}}
|
||||
{{#if (eq status 'success') }}
|
||||
Your intention has been added.
|
||||
{{else if (eq status 'exists') }}
|
||||
An intention already exists for this Source-Destination pair. Please enter a different combination of Services, or search the intentions to edit an existing intention.
|
||||
{{else}}
|
||||
There was an error adding your intention.
|
||||
{{/if}}
|
||||
{{else if (eq type 'update') }}
|
||||
{{#if (eq status 'success') }}
|
||||
Your intention has been saved.
|
||||
{{else if (eq status 'error')}}
|
||||
There was an error saving your intention.
|
||||
{{/if}}
|
||||
{{ else if (eq type 'delete')}}
|
||||
{{#if (eq status 'success') }}
|
||||
Your intention was deleted.
|
||||
{{else if (eq status 'error')}}
|
||||
There was an error deleting your intention.
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{#let error.errors.firstObject as |error|}}
|
||||
{{#if error.detail }}
|
||||
<br />{{concat '(' (if error.status (concat error.status ': ')) error.detail ')'}}
|
||||
{{/if}}
|
||||
{{/let}}
|
||||
|
|
@ -1,23 +1,12 @@
|
|||
{{#if item.ID }}
|
||||
{{#if item.ID}}
|
||||
{{title 'Edit Intention'}}
|
||||
{{else}}
|
||||
{{title 'New Intention'}}
|
||||
{{/if}}
|
||||
|
||||
<AppView @class="intention edit" @loading={{isLoading}}>
|
||||
<BlockSlot @name="notification" as |status type item error|>
|
||||
{{partial 'dc/intentions/notifications'}}
|
||||
</BlockSlot>
|
||||
<AppView @class="intention edit">
|
||||
<BlockSlot @name="breadcrumbs">
|
||||
<ol>
|
||||
{{#if (gt history.length 0)}}
|
||||
<li><a href={{href-to 'dc.services'}}>All Services</a></li>
|
||||
{{#let history.firstObject as |back|}}
|
||||
<li><a data-test-back href={{href-to back.key back.value}}>{{concat 'Service (' back.value ')'}}</a></li>
|
||||
{{/let}}
|
||||
{{else}}
|
||||
<li><a data-test-back href={{href-to 'dc.intentions'}}>All Intentions</a></li>
|
||||
{{/if}}
|
||||
</ol>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="header">
|
||||
|
@ -30,7 +19,7 @@
|
|||
</h1>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="actions">
|
||||
{{#if (not create) }}
|
||||
{{#if item.ID}}
|
||||
<CopyButton @value={{item.ID}} @name="UUID">
|
||||
Copy UUID
|
||||
</CopyButton>
|
||||
|
@ -39,11 +28,9 @@
|
|||
<BlockSlot @name="content">
|
||||
<ConsulIntentionForm
|
||||
@item={{item}}
|
||||
@services={{uniq-by 'Name' services}}
|
||||
@nspaces={{nspaces}}
|
||||
@ondelete={{action "route" "delete"}}
|
||||
@onsubmit={{action "route" (if item.isNew "create" "update")}}
|
||||
@oncancel={{action "route" "cancel"}}
|
||||
@dc={{dc}}
|
||||
@nspace={{nspace}}
|
||||
@onsubmit={{transition-to 'dc.intentions.index' dc}}
|
||||
/>
|
||||
</BlockSlot>
|
||||
</AppView>
|
|
@ -1,79 +1,89 @@
|
|||
{{title 'Intentions'}}
|
||||
<EventSource @src={{items}} />
|
||||
{{#let (filter-by "Action" "deny" items) as |denied|}}
|
||||
{{#let (selectable-key-values
|
||||
(array "" (concat "All (" items.length ")"))
|
||||
(array "allow" (concat "Allow (" (sub items.length denied.length) ")"))
|
||||
(array "deny" (concat "Deny (" denied.length ")"))
|
||||
selected=filterBy
|
||||
)
|
||||
as |filter|
|
||||
}}
|
||||
<AppView @class="intention list">
|
||||
<BlockSlot @name="notification" as |status type|>
|
||||
{{partial 'dc/intentions/notifications'}}
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="header">
|
||||
<h1>
|
||||
Intentions <em>{{format-number items.length}} total</em>
|
||||
</h1>
|
||||
<label for="toolbar-toggle"></label>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="actions">
|
||||
<a data-test-create href="{{href-to 'dc.intentions.create'}}" class="type-create">Create</a>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="toolbar">
|
||||
{{#if (gt items.length 0) }}
|
||||
<SearchBar
|
||||
data-test-intention-filter="true"
|
||||
@value={{search}}
|
||||
@onsearch={{action (mut search) value="target.value"}}
|
||||
@selected={{filter.selected}}
|
||||
@options={{filter.items}}
|
||||
@onchange={{action (mut filterBy) value='target.value'}}
|
||||
/>
|
||||
{{/if}}
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="content">
|
||||
<ChangeableSet @dispatcher={{searchable 'intention' (if (eq filter.selected.key "") items (filter-by "Action" filter.selected.key items))}} @terms={{search}}>
|
||||
<BlockSlot @name="set" as |filtered|>
|
||||
<ConsulIntentionList
|
||||
@items={{filtered}}
|
||||
@ondelete={{action "route" "delete"}}
|
||||
/>
|
||||
<DataLoader @src={{concat '/' nspace '/' dc '/intentions'}} as |api|>
|
||||
|
||||
<BlockSlot @name="error">
|
||||
<AppError @error={{api.error}} />
|
||||
</BlockSlot>
|
||||
|
||||
<BlockSlot @name="loaded">
|
||||
|
||||
{{#let (filter-by "Action" "deny" api.data) as |denied|}}
|
||||
{{#let (selectable-key-values
|
||||
(array "" (concat "All (" api.data.length ")"))
|
||||
(array "allow" (concat "Allow (" (sub api.data.length denied.length) ")"))
|
||||
(array "deny" (concat "Deny (" denied.length ")"))
|
||||
selected=filterBy
|
||||
)
|
||||
as |filter|
|
||||
}}
|
||||
|
||||
<AppView @class="intention list">
|
||||
<BlockSlot @name="header">
|
||||
<h1>
|
||||
Intentions <em>{{format-number api.data.length}} total</em>
|
||||
</h1>
|
||||
<label for="toolbar-toggle"></label>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="actions">
|
||||
<a data-test-create href="{{href-to 'dc.intentions.create'}}" class="type-create">Create</a>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="toolbar">
|
||||
{{#if (gt api.data.length 0) }}
|
||||
<SearchBar
|
||||
data-test-intention-filter="true"
|
||||
@value={{search}}
|
||||
@onsearch={{action (mut search) value="target.value"}}
|
||||
@selected={{filter.selected}}
|
||||
@options={{filter.items}}
|
||||
@onchange={{action (mut filterBy) value='target.value'}}
|
||||
/>
|
||||
{{/if}}
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="content">
|
||||
|
||||
<ChangeableSet @dispatcher={{searchable 'intention' (if (eq filter.selected.key "") api.data (filter-by "Action" filter.selected.key api.data))}} @terms={{search}}>
|
||||
<BlockSlot @name="content" as |filtered|>
|
||||
<ConsulIntentionList
|
||||
@items={{filtered}}
|
||||
@ondelete={{refresh-route}}
|
||||
>
|
||||
<EmptyState @allowLogin={{true}}>
|
||||
<BlockSlot @name="header">
|
||||
<h2>
|
||||
{{#if (gt api.data.length 0)}}
|
||||
No intentions found
|
||||
{{else}}
|
||||
Welcome to Intentions
|
||||
{{/if}}
|
||||
</h2>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="body">
|
||||
<p>
|
||||
{{#if (gt api.data.length 0)}}
|
||||
No intentions where found matching that search, or you may not have access to view the intentions you are searching for.
|
||||
{{else}}
|
||||
There don't seem to be any intentions, or you may not have access to view intentions yet.
|
||||
{{/if}}
|
||||
</p>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="actions">
|
||||
<li class="docs-link">
|
||||
<a href="{{env 'CONSUL_DOCS_URL'}}/commands/intention" rel="noopener noreferrer" target="_blank">Documentation on intentions</a>
|
||||
</li>
|
||||
<li class="learn-link">
|
||||
<a href="{{env 'CONSUL_DOCS_LEARN_URL'}}/consul/getting-started/connect" rel="noopener noreferrer" target="_blank">Read the guide</a>
|
||||
</li>
|
||||
</BlockSlot>
|
||||
</EmptyState>
|
||||
</ConsulIntentionList>
|
||||
</BlockSlot>
|
||||
</ChangeableSet>
|
||||
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="empty">
|
||||
<EmptyState @allowLogin={{true}}>
|
||||
<BlockSlot @name="header">
|
||||
<h2>
|
||||
{{#if (gt items.length 0)}}
|
||||
No intentions found
|
||||
{{else}}
|
||||
Welcome to Intentions
|
||||
{{/if}}
|
||||
</h2>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="body">
|
||||
<p>
|
||||
{{#if (gt items.length 0)}}
|
||||
No intentions where found matching that search, or you may not have access to view the intentions you are searching for.
|
||||
{{else}}
|
||||
There don't seem to be any intentions, or you may not have access to view intentions yet.
|
||||
{{/if}}
|
||||
</p>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="actions">
|
||||
<li class="docs-link">
|
||||
<a href="{{env 'CONSUL_DOCS_URL'}}/commands/intention" rel="noopener noreferrer" target="_blank">Documentation on intentions</a>
|
||||
</li>
|
||||
<li class="learn-link">
|
||||
<a href="{{env 'CONSUL_DOCS_LEARN_URL'}}/consul/getting-started/connect" rel="noopener noreferrer" target="_blank">Read the guide</a>
|
||||
</li>
|
||||
</BlockSlot>
|
||||
</EmptyState>
|
||||
</BlockSlot>
|
||||
</ChangeableSet>
|
||||
</BlockSlot>
|
||||
</AppView>
|
||||
{{/let}}
|
||||
{{/let}}
|
||||
</AppView>
|
||||
|
||||
{{/let}}
|
||||
{{/let}}
|
||||
|
||||
</BlockSlot>
|
||||
</DataLoader>
|
|
@ -5,7 +5,6 @@
|
|||
<AppView @class="service show">
|
||||
<BlockSlot @name="notification" as |status type|>
|
||||
{{partial 'dc/services/notifications'}}
|
||||
{{partial 'dc/intentions/notifications'}}
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="breadcrumbs">
|
||||
<ol>
|
||||
|
@ -31,7 +30,7 @@
|
|||
(hash label="Upstreams" href=(href-to "dc.services.show.upstreams") selected=(is-href "dc.services.show.upstreams"))
|
||||
'')
|
||||
(hash label="Instances" href=(href-to "dc.services.show.instances") selected=(is-href "dc.services.show.instances"))
|
||||
(if (not item.Service.Kind)
|
||||
(if (not-eq item.Service.Kind 'terminating-gateway')
|
||||
(hash label="Intentions" href=(href-to "dc.services.show.intentions") selected=(is-href "dc.services.show.intentions"))
|
||||
'')
|
||||
(if chain.Chain
|
||||
|
|
|
@ -1,49 +1 @@
|
|||
{{#let (filter-by "Action" "deny" intentions) as |denied|}}
|
||||
{{#let (selectable-key-values
|
||||
(array "" (concat "All (" intentions.length ")"))
|
||||
(array "allow" (concat "Allow (" (sub intentions.length denied.length) ")"))
|
||||
(array "deny" (concat "Deny (" denied.length ")"))
|
||||
selected=filterBy
|
||||
)
|
||||
as |filter|
|
||||
}}
|
||||
<div id="intentions" class="tab-section">
|
||||
<div role="tabpanel">
|
||||
{{#if (gt intentions.length 0) }}
|
||||
<input type="checkbox" id="toolbar-toggle" />
|
||||
<SearchBar
|
||||
@value={{search}}
|
||||
@onsearch={{action (mut search) value="target.value"}}
|
||||
@selected={{filter.selected}}
|
||||
@options={{filter.items}}
|
||||
@onchange={{action (mut filterBy) value='target.value'}}
|
||||
/>
|
||||
{{/if}}
|
||||
<ChangeableSet
|
||||
@dispatcher={{
|
||||
searchable
|
||||
'intention'
|
||||
(if (eq filter.selected.key "") intentions (filter-by "Action" filter.selected.key intentions))
|
||||
}}
|
||||
@terms={{search}}
|
||||
>
|
||||
<BlockSlot @name="set" as |filtered|>
|
||||
<ConsulIntentionList
|
||||
@items={{filtered}}
|
||||
@ondelete={{action "route" "delete"}}
|
||||
/>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="empty">
|
||||
<EmptyState>
|
||||
<BlockSlot @name="body">
|
||||
<p>
|
||||
There are no intentions for this service.
|
||||
</p>
|
||||
</BlockSlot>
|
||||
</EmptyState>
|
||||
</BlockSlot>
|
||||
</ChangeableSet>
|
||||
</div>
|
||||
</div>
|
||||
{{/let}}
|
||||
{{/let}}
|
||||
{{outlet}}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
<ConsulIntentionForm
|
||||
@nspace={{nspace}}
|
||||
@dc={{dc}}
|
||||
@src={{src}}
|
||||
@autofill={{hash
|
||||
SourceName=service
|
||||
}}
|
||||
@onsubmit={{transition-to 'dc.services.show.intentions.index'}}
|
||||
/>
|
|
@ -0,0 +1,59 @@
|
|||
<DataLoader @src={{concat '/' nspace '/' dc '/intentions/for-service/' slug}} as |api|>
|
||||
<BlockSlot @name="error">
|
||||
<ErrorState @error={{api.error}} />
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="loaded">
|
||||
{{#let (filter-by "Action" "deny" api.data) as |denied|}}
|
||||
{{#let (selectable-key-values
|
||||
(array "" (concat "All (" api.data.length ")"))
|
||||
(array "allow" (concat "Allow (" (sub api.data.length denied.length) ")"))
|
||||
(array "deny" (concat "Deny (" denied.length ")"))
|
||||
selected=filterBy
|
||||
)
|
||||
as |filter|
|
||||
}}
|
||||
<div id="intentions" class="tab-section">
|
||||
<div role="tabpanel">
|
||||
<Portal @target="app-view-actions">
|
||||
<a data-test-create href={{href-to 'dc.services.show.intentions.create'}} class="type-create">Create</a>
|
||||
</Portal>
|
||||
{{#if (gt api.data.length 0) }}
|
||||
<input type="checkbox" id="toolbar-toggle" />
|
||||
<SearchBar
|
||||
@value={{search}}
|
||||
@onsearch={{action (mut search) value="target.value"}}
|
||||
@selected={{filter.selected}}
|
||||
@options={{filter.items}}
|
||||
@onchange={{action (mut filterBy) value='target.value'}}
|
||||
/>
|
||||
{{/if}}
|
||||
<ChangeableSet
|
||||
@dispatcher={{
|
||||
searchable
|
||||
'intention'
|
||||
(if (eq filter.selected.key "") api.data (filter-by "Action" filter.selected.key api.data))
|
||||
}}
|
||||
@terms={{search}}
|
||||
>
|
||||
<BlockSlot @name="content" as |filtered|>
|
||||
<ConsulIntentionList
|
||||
@items={{filtered}}
|
||||
@ondelete={{refresh-route}}
|
||||
@routeName="dc.services.show.intentions.edit"
|
||||
>
|
||||
<EmptyState>
|
||||
<BlockSlot @name="body">
|
||||
<p>
|
||||
There are no intentions {{if (gt intentions.length 0) 'found '}} for this service.
|
||||
</p>
|
||||
</BlockSlot>
|
||||
</EmptyState>
|
||||
</ConsulIntentionList>
|
||||
</BlockSlot>
|
||||
</ChangeableSet>
|
||||
</div>
|
||||
</div>
|
||||
{{/let}}
|
||||
{{/let}}
|
||||
</BlockSlot>
|
||||
</DataLoader>
|
|
@ -1,58 +1,3 @@
|
|||
{{#if error}}
|
||||
<AppView @class="error show">
|
||||
<BlockSlot @name="header">
|
||||
<h1>
|
||||
Error {{error.status}}
|
||||
</h1>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="content">
|
||||
{{#if (not-eq error.status "403")}}
|
||||
<EmptyState class={{concat "status-" error.status}}>
|
||||
<BlockSlot @name="header">
|
||||
<h2>{{or error.message "Consul returned an error"}}</h2>
|
||||
</BlockSlot>
|
||||
{{#if error.status }}
|
||||
<BlockSlot @name="subheader">
|
||||
<h3 data-test-status={{error.status}}>Error {{error.status}}</h3>
|
||||
</BlockSlot>
|
||||
{{/if}}
|
||||
<BlockSlot @name="body">
|
||||
<p>
|
||||
You may have visited a URL that is loading an unknown resource, so you can try going back to the root or try re-submitting your ACL Token/SecretID by going back to ACLs.
|
||||
</p>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="actions">
|
||||
<li class="back-link">
|
||||
<a data-test-home rel="home" href={{href-to 'index'}}>Go back to root</a>
|
||||
</li>
|
||||
<li class="docs-link">
|
||||
<a href="{{env 'CONSUL_DOCS_URL'}}" rel="noopener noreferrer" target="_blank">Read the documentation</a>
|
||||
</li>
|
||||
</BlockSlot>
|
||||
</EmptyState>
|
||||
{{else}}
|
||||
<EmptyState class="status-403">
|
||||
<BlockSlot @name="header">
|
||||
<h2 data-test-status={{error.status}}>You are not authorized</h2>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="subheader">
|
||||
<h3>Error 403</h3>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="body">
|
||||
<p>
|
||||
You must be granted permissions to view this data. Ask your administrator if you think you should have access.
|
||||
</p>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="actions">
|
||||
<li class="docs-link">
|
||||
<a href="{{env 'CONSUL_DOCS_URL'}}/acl/index.html" rel="noopener noreferrer" target="_blank">Read the documentation</a>
|
||||
</li>
|
||||
<li class="learn-link">
|
||||
<a href="{{env 'CONSUL_DOCS_LEARN_URL'}}/consul/security-networking/production-acls" rel="noopener noreferrer" target="_blank">Follow the guide</a>
|
||||
</li>
|
||||
</BlockSlot>
|
||||
</EmptyState>
|
||||
{{/if}}
|
||||
</BlockSlot>
|
||||
</AppView>
|
||||
<AppError @error={{error}} />
|
||||
{{/if}}
|
||||
|
|
|
@ -1,15 +1,6 @@
|
|||
import { validatePresence, validateLength } from 'ember-changeset-validations/validators';
|
||||
import config from 'consul-ui/config/environment';
|
||||
export default Object.assign(
|
||||
{
|
||||
SourceName: [validatePresence(true), validateLength({ min: 1 })],
|
||||
DestinationName: [validatePresence(true), validateLength({ min: 1 })],
|
||||
Action: validatePresence(true),
|
||||
},
|
||||
config.CONSUL_NAMESPACES_ENABLED
|
||||
? {
|
||||
SourceNS: [validatePresence(true), validateLength({ min: 1 })],
|
||||
DestinationNS: [validatePresence(true), validateLength({ min: 1 })],
|
||||
}
|
||||
: {}
|
||||
);
|
||||
export default {
|
||||
SourceName: [validatePresence(true), validateLength({ min: 1 })],
|
||||
DestinationName: [validatePresence(true), validateLength({ min: 1 })],
|
||||
Action: validatePresence(true),
|
||||
};
|
||||
|
|
|
@ -51,6 +51,8 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.2.2",
|
||||
"@babel/helper-call-delegate": "^7.10.1",
|
||||
"@babel/plugin-proposal-class-properties": "^7.10.1",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.5.5",
|
||||
"@ember/optional-features": "^1.3.0",
|
||||
"@glimmer/component": "^1.0.0",
|
||||
|
@ -103,9 +105,12 @@
|
|||
"ember-power-select-with-create": "^0.7.0",
|
||||
"ember-qunit": "^4.6.0",
|
||||
"ember-ref-modifier": "^1.0.0",
|
||||
"ember-render-helpers": "^0.1.1",
|
||||
"ember-resolver": "^7.0.0",
|
||||
"ember-router-helpers": "^0.4.0",
|
||||
"ember-sinon-qunit": "4.0.1",
|
||||
"ember-source": "~3.16.0",
|
||||
"ember-stargate": "^0.2.0",
|
||||
"ember-test-selectors": "^4.0.0",
|
||||
"ember-tooltips": "^3.4.3",
|
||||
"ember-truth-helpers": "^2.0.0",
|
||||
|
|
|
@ -33,13 +33,14 @@ 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" with the body from yaml
|
||||
Then a POST request was made to "/v1/connect/intentions?dc=datacenter" from yaml
|
||||
---
|
||||
SourceName: web
|
||||
DestinationName: db
|
||||
Action: deny
|
||||
body:
|
||||
SourceName: web
|
||||
DestinationName: db
|
||||
Action: deny
|
||||
---
|
||||
Then the url should be /datacenter/intentions
|
||||
And the title should be "Intentions - Consul"
|
||||
And "[data-notification]" has the "notification-create" class
|
||||
And "[data-notification]" has the "notification-update" class
|
||||
And "[data-notification]" has the "success" class
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
@setupApplicationTest
|
||||
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
|
||||
---
|
||||
SourceName: name
|
||||
ID: ee52203d-989f-4f7a-ab5a-2bef004164ca
|
||||
---
|
||||
When I visit the intentions page for yaml
|
||||
---
|
||||
dc: datacenter
|
||||
---
|
||||
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"
|
||||
And "[data-notification]" has the "notification-delete" class
|
||||
And "[data-notification]" has the "success" class
|
||||
Scenario: Deleting an intention from the intention detail page
|
||||
When I visit the intention page for yaml
|
||||
---
|
||||
dc: datacenter
|
||||
intention: ee52203d-989f-4f7a-ab5a-2bef004164ca
|
||||
---
|
||||
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"
|
||||
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
|
||||
When I visit the intention page for yaml
|
||||
---
|
||||
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
|
||||
And I click delete
|
||||
And I click confirmDelete
|
||||
And "[data-notification]" has the "notification-update" class
|
||||
And "[data-notification]" has the "error" class
|
||||
Scenario: Deleting an intention from the intention detail page and getting an error due to a duplicate intention
|
||||
When I visit the intention page for yaml
|
||||
---
|
||||
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
|
||||
---
|
||||
status: 500
|
||||
body: "duplicate intention found:"
|
||||
---
|
||||
And I click delete
|
||||
And I click confirmDelete
|
||||
And "[data-notification]" has the "notification-update" class
|
||||
And "[data-notification]" has the "error" class
|
||||
And I see the text "Intention exists" in "[data-notification] strong"
|
|
@ -16,6 +16,11 @@ Feature: dc / intentions / filtered-select: Intention Service Select Dropdowns
|
|||
- Name: service-3
|
||||
Kind: connect-proxy
|
||||
---
|
||||
And 1 intention model from yaml
|
||||
---
|
||||
SourceName: 'service-0'
|
||||
DestinationName: 'service-1'
|
||||
---
|
||||
When I visit the intention page for yaml
|
||||
---
|
||||
dc: datacenter
|
||||
|
@ -42,6 +47,11 @@ Feature: dc / intentions / filtered-select: Intention Service Select Dropdowns
|
|||
Namespace: nspace
|
||||
Kind: ~
|
||||
---
|
||||
And 1 intention model from yaml
|
||||
---
|
||||
SourceName: 'service-0'
|
||||
DestinationName: 'service-0'
|
||||
---
|
||||
When I visit the intention page for yaml
|
||||
---
|
||||
dc: datacenter
|
||||
|
|
|
@ -24,7 +24,6 @@ Feature: deleting: Deleting items with confirmations, success and error notifica
|
|||
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
| Edit | Listing | Method | URL | Data |
|
||||
| kv | kvs | DELETE | /v1/kv/key-name?dc=datacenter&ns=@!namespace | ["key-name"] |
|
||||
| intention | intentions | DELETE | /v1/connect/intentions/ee52203d-989f-4f7a-ab5a-2bef004164ca?dc=datacenter | {"SourceName": "name", "ID": "ee52203d-989f-4f7a-ab5a-2bef004164ca"} |
|
||||
| token | tokens | DELETE | /v1/acl/token/001fda31-194e-4ff1-a5ec-589abf2cafd0?dc=datacenter&ns=@!namespace | {"AccessorID": "001fda31-194e-4ff1-a5ec-589abf2cafd0"} |
|
||||
# | acl | acls | PUT | /v1/acl/destroy/something?dc=datacenter | {"Name": "something", "ID": "something"} |
|
||||
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
|
@ -53,10 +52,6 @@ Feature: deleting: Deleting items with confirmations, success and error notifica
|
|||
-----------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
| Model | Method | URL | Slug |
|
||||
| kv | DELETE | /v1/kv/key-name?dc=datacenter&ns=@!namespace | kv: key-name |
|
||||
| intention | DELETE | /v1/connect/intentions/ee52203d-989f-4f7a-ab5a-2bef004164ca?dc=datacenter | intention: ee52203d-989f-4f7a-ab5a-2bef004164ca |
|
||||
| token | DELETE | /v1/acl/token/001fda31-194e-4ff1-a5ec-589abf2cafd0?dc=datacenter&ns=@!namespace | token: 001fda31-194e-4ff1-a5ec-589abf2cafd0 |
|
||||
# | acl | PUT | /v1/acl/destroy/something?dc=datacenter | acl: something |
|
||||
-----------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
@ignore
|
||||
Scenario: Sort out the wide tables ^
|
||||
Then ok
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import steps from '../../steps';
|
||||
|
||||
// step definitions that are shared between features should be moved to the
|
||||
// tests/acceptance/steps/steps.js file
|
||||
|
||||
export default function(assert) {
|
||||
return steps(assert).then('I should find a file', function() {
|
||||
assert.ok(true, this.step);
|
||||
});
|
||||
}
|
|
@ -17,7 +17,6 @@ Feature: submit-blank
|
|||
| Model | Slug |
|
||||
| kv | kv |
|
||||
| acl | acls |
|
||||
| intention | intentions |
|
||||
--------------------------
|
||||
@ignore
|
||||
Scenario: The button is disabled
|
||||
|
|
|
@ -8,6 +8,9 @@ export default function(type) {
|
|||
env: function() {
|
||||
return CONSUL_NSPACES_ENABLED;
|
||||
},
|
||||
var: function() {
|
||||
return CONSUL_NSPACES_ENABLED;
|
||||
},
|
||||
})
|
||||
);
|
||||
const adapter = container.owner.lookup(`adapter:${type}`);
|
||||
|
|
|
@ -11,6 +11,9 @@ export default function(type) {
|
|||
case 'proxy':
|
||||
requests = ['/v1/catalog/connect'];
|
||||
break;
|
||||
case 'intention':
|
||||
requests = ['/v1/connect/intentions'];
|
||||
break;
|
||||
case 'node':
|
||||
requests = ['/v1/internal/ui/nodes', '/v1/internal/ui/node/'];
|
||||
break;
|
||||
|
|
|
@ -13,14 +13,16 @@ module('Integration | Adapter | intention', function(hooks) {
|
|||
});
|
||||
assert.equal(actual, expected);
|
||||
});
|
||||
test('urlForQueryRecord returns the correct url', function(assert) {
|
||||
test('requestForQueryRecord returns the correct url', function(assert) {
|
||||
const adapter = this.owner.lookup('adapter:intention');
|
||||
const client = this.owner.lookup('service:client/http');
|
||||
const expected = `GET /v1/connect/intentions/${id}?dc=${dc}`;
|
||||
const actual = adapter.requestForQueryRecord(client.url, {
|
||||
dc: dc,
|
||||
id: id,
|
||||
});
|
||||
const actual = adapter
|
||||
.requestForQueryRecord(client.url, {
|
||||
dc: dc,
|
||||
id: id,
|
||||
})
|
||||
.split('\n')[0];
|
||||
assert.equal(actual, expected);
|
||||
});
|
||||
test("requestForQueryRecord throws if you don't specify an id", function(assert) {
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Controller | dc/intentions/create', function(hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function(assert) {
|
||||
let controller = this.owner.lookup('controller:dc/intentions/create');
|
||||
assert.ok(controller);
|
||||
});
|
||||
});
|
|
@ -1,12 +0,0 @@
|
|||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Controller | dc/intentions/edit', function(hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function(assert) {
|
||||
let controller = this.owner.lookup('controller:dc/intentions/edit');
|
||||
assert.ok(controller);
|
||||
});
|
||||
});
|
|
@ -1,37 +0,0 @@
|
|||
import { module } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import test from 'ember-sinon-qunit/test-support/test';
|
||||
import Route from '@ember/routing/route';
|
||||
import Mixin from 'consul-ui/mixins/intention/with-actions';
|
||||
|
||||
module('Unit | Mixin | intention/with actions', function(hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
hooks.beforeEach(function() {
|
||||
this.subject = function() {
|
||||
const MixedIn = Route.extend(Mixin);
|
||||
this.owner.register('test-container:intention/with-actions-object', MixedIn);
|
||||
return this.owner.lookup('test-container:intention/with-actions-object');
|
||||
};
|
||||
});
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it works', function(assert) {
|
||||
const subject = this.subject();
|
||||
assert.ok(subject);
|
||||
});
|
||||
test('errorCreate returns a different status code if a duplicate intention is found', function(assert) {
|
||||
const subject = this.subject();
|
||||
const expected = 'exists';
|
||||
const actual = subject.errorCreate('error', {
|
||||
errors: [{ status: '500', detail: 'duplicate intention found:' }],
|
||||
});
|
||||
assert.equal(actual, expected);
|
||||
});
|
||||
test('errorCreate returns the same code if there is no error', function(assert) {
|
||||
const subject = this.subject();
|
||||
const expected = 'error';
|
||||
const actual = subject.errorCreate('error', {});
|
||||
assert.equal(actual, expected);
|
||||
});
|
||||
});
|
2096
ui-v2/yarn.lock
2096
ui-v2/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue