mirror of https://github.com/status-im/consul.git
ui: Intention Custom Resource Banners (#9018)
This commit is contained in:
parent
d3d9cb1d50
commit
948917c6b0
|
@ -1,14 +1,14 @@
|
|||
<DataForm
|
||||
@dc={{dc}}
|
||||
@nspace={{nspace}}
|
||||
@type="intention"
|
||||
@autofill={{autofill}}
|
||||
@item={{item}}
|
||||
@src={{src}}
|
||||
@onchange={{action "change"}}
|
||||
@onsubmit={{action onsubmit}}
|
||||
as |api|
|
||||
>
|
||||
@dc={{@dc}}
|
||||
@nspace={{@nspace}}
|
||||
@autofill={{@autofill}}
|
||||
@item={{@item}}
|
||||
@src={{@src}}
|
||||
@onchange={{action this.change}}
|
||||
@onsubmit={{action this.onsubmit}}
|
||||
as |api|>
|
||||
|
||||
<BlockSlot @name="error" as |Notification|>
|
||||
<Notification>
|
||||
<p data-notification role="alert" class="error notification-update">
|
||||
|
@ -27,43 +27,69 @@
|
|||
</BlockSlot>
|
||||
|
||||
<BlockSlot @name="form">
|
||||
|
||||
{{#let api.data as |item|}}
|
||||
{{#if item.IsEditable}}
|
||||
<DataSource
|
||||
@src={{concat '/' nspace '/' dc '/services'}}
|
||||
@onchange={{action "createServices" item}}
|
||||
@src={{concat '/' @nspace '/' @dc '/services'}}
|
||||
@onchange={{action this.createServices item}}
|
||||
/>
|
||||
|
||||
{{#if (env 'CONSUL_NSPACES_ENABLED')}}
|
||||
{{#if (env 'CONSUL_NSPACES_ENABLED')}}
|
||||
<DataSource
|
||||
@src="/*/*/namespaces"
|
||||
@onchange={{action "createNspaces" item}}
|
||||
@onchange={{action this.createNspaces item}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{#if (and api.isCreate this.isManagedByCRDs)}}
|
||||
<Consul::Intention::Notice::CustomResource @type="warning" />
|
||||
{{/if}}
|
||||
<form onsubmit={{action api.submit}}>
|
||||
<Consul::Intention::Form::Fieldsets
|
||||
@nspaces={{nspaces}}
|
||||
@services={{services}}
|
||||
@SourceName={{SourceName}}
|
||||
@SourceNS={{SourceNS}}
|
||||
@DestinationName={{DestinationName}}
|
||||
@DestinationNS={{DestinationNS}}
|
||||
@nspaces={{this.nspaces}}
|
||||
@services={{this.services}}
|
||||
@SourceName={{this.SourceName}}
|
||||
@SourceNS={{this.SourceNS}}
|
||||
@DestinationName={{this.DestinationName}}
|
||||
@DestinationNS={{this.DestinationNS}}
|
||||
@item={{item}}
|
||||
@disabled={{api.disabled}}
|
||||
@create={{api.isCreate}}
|
||||
@onchange={{api.change}}
|
||||
/>
|
||||
<div>
|
||||
<button type="submit" disabled={{or item.isInvalid api.disabled}}>Save</button>
|
||||
<button type="reset" onclick={{action oncancel item}} disabled={{api.disabled}}>Cancel</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={{or item.isInvalid api.disabled}}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
type="reset"
|
||||
disabled={{api.disabled}}
|
||||
{{on 'click' (fn this.oncancel item)}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
{{#if (not api.isCreate)}}
|
||||
{{#if (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 api.delete}} disabled={{api.disabled}}>Delete</button>
|
||||
<button
|
||||
data-test-delete
|
||||
type="button"
|
||||
class="type-delete"
|
||||
disabled={{api.disabled}}
|
||||
{{on 'click' (fn confirm api.delete)}}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="dialog" as |execute cancel message|>
|
||||
<DeleteConfirmation @message={{message}} @execute={{execute}} @cancel={{cancel}} />
|
||||
<DeleteConfirmation
|
||||
@message={{message}}
|
||||
@execute={{execute}}
|
||||
@cancel={{cancel}}
|
||||
/>
|
||||
</BlockSlot>
|
||||
</ConfirmationDialog>
|
||||
{{/if}}
|
||||
|
@ -71,10 +97,31 @@
|
|||
</div>
|
||||
</form>
|
||||
{{else}}
|
||||
<Consul::Intention::View
|
||||
@item={{item}}
|
||||
/>
|
||||
{{#if item.IsManagedByCRD}}
|
||||
<Notice
|
||||
class="crd"
|
||||
@type="warning"
|
||||
as |notice|>
|
||||
<notice.Header>
|
||||
<h3>
|
||||
Intention Custom Resource
|
||||
</h3>
|
||||
</notice.Header>
|
||||
<notice.Body>
|
||||
<p>
|
||||
This Intention is view only because it is managed through an Intention Custom Resource in your Kubernetes cluster.
|
||||
</p>
|
||||
<p>
|
||||
<a href="{{env 'CONSUL_DOCS_URL'}}/k8s/crds" target="_blank" rel="noopener noreferrer">Learn more about CRDs</a>
|
||||
</p>
|
||||
</notice.Body>
|
||||
</Notice>
|
||||
{{/if}}
|
||||
<Consul::Intention::View
|
||||
@item={{item}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{/let}}
|
||||
|
||||
</BlockSlot>
|
||||
</DataForm>
|
||||
|
|
|
@ -1,112 +1,150 @@
|
|||
import Component from '@ember/component';
|
||||
import { setProperties, set, get } from '@ember/object';
|
||||
import Component from '@glimmer/component';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
|
||||
export default Component.extend({
|
||||
tagName: '',
|
||||
ondelete: function() {
|
||||
this.onsubmit(...arguments);
|
||||
},
|
||||
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: item.SourceName };
|
||||
items = [source].concat(items);
|
||||
}
|
||||
let destination = items.findBy('Name', item.DestinationName);
|
||||
if (!destination) {
|
||||
destination = { Name: item.DestinationName };
|
||||
items = [destination].concat(items);
|
||||
}
|
||||
setProperties(this, {
|
||||
services: items,
|
||||
SourceName: source,
|
||||
DestinationName: destination,
|
||||
});
|
||||
},
|
||||
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,
|
||||
});
|
||||
},
|
||||
change: function(e, form, item) {
|
||||
const target = e.target;
|
||||
export default class ConsulIntentionForm extends Component {
|
||||
|
||||
let name, selected, match;
|
||||
switch (target.name) {
|
||||
case 'SourceName':
|
||||
case 'DestinationName':
|
||||
case 'SourceNS':
|
||||
case 'DestinationNS':
|
||||
name = selected = target.value;
|
||||
// Names can be selected Service EmberObjects or typed in strings
|
||||
// if its not a string, use the `Name` from the Service EmberObject
|
||||
if (typeof name !== 'string') {
|
||||
name = get(target.value, 'Name');
|
||||
@tracked services;
|
||||
@tracked SourceName;
|
||||
@tracked DestinationName;
|
||||
|
||||
@tracked nspaces;
|
||||
@tracked SourceNS;
|
||||
@tracked DestinationNS;
|
||||
|
||||
@tracked isManagedByCRDs;
|
||||
|
||||
@service('repository/intention') repo;
|
||||
|
||||
constructor(owner, args) {
|
||||
super(...arguments);
|
||||
this.updateCRDManagement();
|
||||
}
|
||||
|
||||
ondelete() {
|
||||
if(this.args.ondelete) {
|
||||
this.args.ondelete(...arguments);
|
||||
} else {
|
||||
this.onsubmit(...arguments);
|
||||
}
|
||||
}
|
||||
|
||||
oncancel() {
|
||||
if(this.args.oncancel) {
|
||||
this.args.oncancel(...arguments);
|
||||
} else {
|
||||
this.onsubmit(...arguments);
|
||||
}
|
||||
}
|
||||
|
||||
onsubmit() {
|
||||
if(this.args.onsubmit) {
|
||||
this.args.onsubmit(...arguments);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
updateCRDManagement() {
|
||||
this.isManagedByCRDs = this.repo.isManagedByCRDs();
|
||||
}
|
||||
|
||||
@action
|
||||
createServices (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: item.SourceName };
|
||||
items = [source].concat(items);
|
||||
}
|
||||
let destination = items.findBy('Name', item.DestinationName);
|
||||
if (!destination) {
|
||||
destination = { Name: item.DestinationName };
|
||||
items = [destination].concat(items);
|
||||
}
|
||||
this.services = items;
|
||||
this.SourceName = source;
|
||||
this.DestinationName = destination;
|
||||
}
|
||||
|
||||
@action
|
||||
createNspaces (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);
|
||||
}
|
||||
this.nspaces = items;
|
||||
this.SourceNS = source;
|
||||
this.DestinationNS = destination;
|
||||
}
|
||||
|
||||
@action
|
||||
change(e, form, item) {
|
||||
const target = e.target;
|
||||
|
||||
let name, selected, match;
|
||||
switch (target.name) {
|
||||
case 'SourceName':
|
||||
case 'DestinationName':
|
||||
case 'SourceNS':
|
||||
case 'DestinationNS':
|
||||
name = selected = target.value;
|
||||
// Names can be selected Service EmberObjects or typed in strings
|
||||
// if its not a string, use the `Name` from the Service EmberObject
|
||||
if (typeof name !== 'string') {
|
||||
name = target.value.Name;
|
||||
}
|
||||
// mutate the value with the string name
|
||||
// which will be handled by the form
|
||||
target.value = name;
|
||||
// these are 'non-form' variables so not on `item`
|
||||
// these variables also exist in the template so we know
|
||||
// the current selection
|
||||
// basically the difference between
|
||||
// `item.DestinationName` and just `DestinationName`
|
||||
// see if the name is already in the list
|
||||
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
|
||||
selected = { Name: name };
|
||||
switch (target.name) {
|
||||
case 'SourceName':
|
||||
case 'DestinationName':
|
||||
this.services = [selected].concat(this.services.toArray());
|
||||
break;
|
||||
case 'SourceNS':
|
||||
case 'DestinationNS':
|
||||
this.nspaces = [selected].concat(this.nspaces.toArray());
|
||||
break;
|
||||
}
|
||||
// mutate the value with the string name
|
||||
// which will be handled by the form
|
||||
target.value = name;
|
||||
// these are 'non-form' variables so not on `item`
|
||||
// these variables also exist in the template so we know
|
||||
// the current selection
|
||||
// basically the difference between
|
||||
// `item.DestinationName` and just `DestinationName`
|
||||
// see if the name is already in the list
|
||||
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
|
||||
selected = { Name: name };
|
||||
switch (target.name) {
|
||||
case 'SourceName':
|
||||
case 'DestinationName':
|
||||
set(this, 'services', [selected].concat(this.services.toArray()));
|
||||
break;
|
||||
case 'SourceNS':
|
||||
case 'DestinationNS':
|
||||
set(this, 'nspaces', [selected].concat(this.nspaces.toArray()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
set(this, target.name, selected);
|
||||
break;
|
||||
}
|
||||
form.handleEvent(e);
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
this[target.name] = selected;
|
||||
break;
|
||||
}
|
||||
form.handleEvent(e);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<div
|
||||
class="consul-intention-list"
|
||||
...attributes
|
||||
{{did-update this.updateCRDManagement @items}}
|
||||
>
|
||||
<DataWriter
|
||||
@sink={{concat '/' @dc '/' @nspace '/intention/'}}
|
||||
|
@ -10,16 +11,22 @@
|
|||
<BlockSlot @name="content">
|
||||
|
||||
{{#let (hash
|
||||
Check=(component 'consul/intention/list/check')
|
||||
Table=(component 'consul/intention/list/table' delete=writer.delete items=@items)
|
||||
Table=(component 'consul/intention/list/table' delete=writer.delete items=this.items)
|
||||
CheckNotice=(if this.checkedItem
|
||||
(component 'consul/intention/list/check' item=this.checkedItem)
|
||||
''
|
||||
)
|
||||
CustomResourceNotice=(if this.isManagedByCRDs
|
||||
(component 'consul/intention/notice/custom-resource')
|
||||
''
|
||||
)
|
||||
) as |api|}}
|
||||
|
||||
{{#if (gt @items.length 0)}}
|
||||
{{#if (gt this.items.length 0)}}
|
||||
{{yield api to="idle"}}
|
||||
{{else}}
|
||||
{{yield api to="empty"}}
|
||||
{{/if}}
|
||||
|
||||
{{/let}}
|
||||
|
||||
</BlockSlot>
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
import Component from '@glimmer/component';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { sort } from '@ember/object/computed';
|
||||
|
||||
export default class ConsulIntentionList extends Component {
|
||||
|
||||
@service('filter') filter;
|
||||
@service('sort') sort;
|
||||
@service('search') search;
|
||||
@service('repository/intention') repo;
|
||||
|
||||
@sort('searched', 'comparator') sorted;
|
||||
|
||||
@tracked isManagedByCRDs;
|
||||
|
||||
constructor(owner, args) {
|
||||
super(...arguments);
|
||||
this.updateCRDManagement(args.items);
|
||||
}
|
||||
get items() {
|
||||
return this.sorted;
|
||||
}
|
||||
get filtered() {
|
||||
const predicate = this.filter.predicate('intention');
|
||||
return this.args.items.filter(predicate(this.args.filters))
|
||||
}
|
||||
get searched() {
|
||||
if(typeof this.args.search === 'undefined') {
|
||||
return this.filtered;
|
||||
}
|
||||
const predicate = this.search.predicate('intention');
|
||||
return this.filtered.filter(predicate(this.args.search));
|
||||
}
|
||||
get comparator() {
|
||||
return [this.args.sort];
|
||||
}
|
||||
get checkedItem() {
|
||||
if(this.searched.length === 1) {
|
||||
return this.searched[0].SourceName === this.args.search ? this.searched[0] : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@action
|
||||
updateCRDManagement() {
|
||||
this.isManagedByCRDs = this.repo.isManagedByCRDs();
|
||||
}
|
||||
}
|
|
@ -1,10 +1,15 @@
|
|||
export default (collection, clickable, attribute, deletable) => () => {
|
||||
return collection('.consul-intention-list [data-test-tabular-row]', {
|
||||
export default (collection, clickable, attribute, isPresent, deletable) => (scope = '.consul-intention-list') => {
|
||||
const row = {
|
||||
source: attribute('data-test-intention-source', '[data-test-intention-source]'),
|
||||
destination: attribute('data-test-intention-destination', '[data-test-intention-destination]'),
|
||||
action: attribute('data-test-intention-action', '[data-test-intention-action]'),
|
||||
intention: clickable('a'),
|
||||
actions: clickable('label'),
|
||||
...deletable(),
|
||||
});
|
||||
};
|
||||
return {
|
||||
scope: scope,
|
||||
customResourceNotice: isPresent('.consul-intention-notice-custom-resource'),
|
||||
intentions: collection('[data-test-tabular-row]', row)
|
||||
}
|
||||
};
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
<Notice
|
||||
class="consul-intention-notice-custom-resource crd"
|
||||
...attributes
|
||||
@type={{or @type "info"}}
|
||||
as |notice|>
|
||||
<notice.Header>
|
||||
<h3>
|
||||
Intention Custom Resource
|
||||
</h3>
|
||||
</notice.Header>
|
||||
<notice.Body>
|
||||
<p>
|
||||
Some of your intentions are being managed through an Intention Custom Resource in your Kubernetes cluster. Those managed intentions will be view only in the UI. Any intentions created in the UI will work but will not be synced to the Custom Resource Definition (CRD) datastore.
|
||||
</p>
|
||||
<p>
|
||||
<a href="{{env 'CONSUL_DOCS_URL'}}/k8s/crds" target="_blank" rel="noopener noreferrer">Learn more about CRDs</a>
|
||||
</p>
|
||||
</notice.Body>
|
||||
</Notice>
|
|
@ -1,41 +0,0 @@
|
|||
import intention from 'consul-ui/search/filters/intention';
|
||||
import token from 'consul-ui/search/filters/token';
|
||||
import policy from 'consul-ui/search/filters/policy';
|
||||
import role from 'consul-ui/search/filters/role';
|
||||
import kv from 'consul-ui/search/filters/kv';
|
||||
import acl from 'consul-ui/search/filters/acl';
|
||||
import node from 'consul-ui/search/filters/node';
|
||||
// service instance
|
||||
import nodeService from 'consul-ui/search/filters/node/service';
|
||||
import serviceNode from 'consul-ui/search/filters/service/node';
|
||||
import service from 'consul-ui/search/filters/service';
|
||||
import nspace from 'consul-ui/search/filters/nspace';
|
||||
|
||||
import filterableFactory from 'consul-ui/utils/search/filterable';
|
||||
const filterable = filterableFactory();
|
||||
export function initialize(application) {
|
||||
// Service-less injection using private properties at a per-project level
|
||||
const Builder = application.resolveRegistration('service:search');
|
||||
const searchables = {
|
||||
intention: intention(filterable),
|
||||
token: token(filterable),
|
||||
acl: acl(filterable),
|
||||
policy: policy(filterable),
|
||||
role: role(filterable),
|
||||
kv: kv(filterable),
|
||||
node: node(filterable),
|
||||
serviceInstance: serviceNode(filterable),
|
||||
nodeservice: nodeService(filterable),
|
||||
service: service(filterable),
|
||||
nspace: nspace(filterable),
|
||||
};
|
||||
Builder.reopen({
|
||||
searchable: function(name) {
|
||||
return searchables[name];
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
initialize,
|
||||
};
|
|
@ -1,15 +0,0 @@
|
|||
import { get } from '@ember/object';
|
||||
export default function(filterable) {
|
||||
return filterable(function(item, { s = '' }) {
|
||||
const source = get(item, 'SourceName').toLowerCase();
|
||||
const destination = get(item, 'DestinationName').toLowerCase();
|
||||
const sLower = s.toLowerCase();
|
||||
const allLabel = 'All Services (*)'.toLowerCase();
|
||||
return (
|
||||
source.indexOf(sLower) !== -1 ||
|
||||
destination.indexOf(sLower) !== -1 ||
|
||||
(source === '*' && allLabel.indexOf(sLower) !== -1) ||
|
||||
(destination === '*' && allLabel.indexOf(sLower) !== -1)
|
||||
);
|
||||
});
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
export default () => (term) => (item) => {
|
||||
const source = item.SourceName.toLowerCase();
|
||||
const destination = item.DestinationName.toLowerCase();
|
||||
const allLabel = 'All Services (*)'.toLowerCase();
|
||||
const lowerTerm = term.toLowerCase();
|
||||
return (
|
||||
source.indexOf(lowerTerm) !== -1 ||
|
||||
destination.indexOf(lowerTerm) !== -1 ||
|
||||
(source === '*' && allLabel.indexOf(lowerTerm) !== -1) ||
|
||||
(destination === '*' && allLabel.indexOf(lowerTerm) !== -1)
|
||||
);
|
||||
}
|
|
@ -1,46 +1,59 @@
|
|||
import { set, get } from '@ember/object';
|
||||
import RepositoryService from 'consul-ui/services/repository';
|
||||
import { PRIMARY_KEY } from 'consul-ui/models/intention';
|
||||
|
||||
const modelName = 'intention';
|
||||
export default RepositoryService.extend({
|
||||
getModelName: function() {
|
||||
export default class IntentionRepository extends RepositoryService {
|
||||
|
||||
managedByCRDs = false;
|
||||
|
||||
getModelName() {
|
||||
return modelName;
|
||||
},
|
||||
getPrimaryKey: function() {
|
||||
}
|
||||
|
||||
getPrimaryKey() {
|
||||
return PRIMARY_KEY;
|
||||
},
|
||||
create: function(obj) {
|
||||
}
|
||||
|
||||
create(obj) {
|
||||
delete obj.Namespace;
|
||||
return this._super({
|
||||
return super.create({
|
||||
Action: 'allow',
|
||||
...obj,
|
||||
});
|
||||
},
|
||||
persist: function(obj) {
|
||||
return this._super(...arguments).then(res => {
|
||||
// if Action is set it means we are an l4 type intention
|
||||
// we don't delete these at a UI level incase the user
|
||||
// would like to switch backwards and forwards between
|
||||
// allow/deny/l7 in the forms, but once its been saved
|
||||
// to the backend we then delete them
|
||||
if (get(res, 'Action.length')) {
|
||||
set(res, 'Permissions', []);
|
||||
}
|
||||
return res;
|
||||
});
|
||||
},
|
||||
findByService: function(slug, dc, nspace, configuration = {}) {
|
||||
}
|
||||
|
||||
isManagedByCRDs() {
|
||||
if(!this.managedByCRDs) {
|
||||
this.managedByCRDs = this.store.peekAll(this.getModelName())
|
||||
.toArray().some(item => item.IsManagedByCRD);
|
||||
}
|
||||
return this.managedByCRDs;
|
||||
}
|
||||
|
||||
async persist(obj) {
|
||||
const res = await super.persist(...arguments);
|
||||
// if Action is set it means we are an l4 type intention
|
||||
// we don't delete these at a UI level incase the user
|
||||
// would like to switch backwards and forwards between
|
||||
// allow/deny/l7 in the forms, but once its been saved
|
||||
// to the backend we then delete them
|
||||
if (get(res, 'Action.length')) {
|
||||
set(res, 'Permissions', []);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
async findByService(slug, dc, nspace, configuration = {}) {
|
||||
const query = {
|
||||
dc: dc,
|
||||
nspace: nspace,
|
||||
dc,
|
||||
nspace,
|
||||
filter: `SourceName == "${slug}" or DestinationName == "${slug}" or SourceName == "*" or DestinationName == "*"`,
|
||||
};
|
||||
if (typeof configuration.cursor !== 'undefined') {
|
||||
query.index = configuration.cursor;
|
||||
query.uri = configuration.uri;
|
||||
}
|
||||
return this.store.query(this.getModelName(), {
|
||||
...query,
|
||||
});
|
||||
},
|
||||
});
|
||||
return this.store.query(this.getModelName(), query);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,40 @@
|
|||
import Service from '@ember/service';
|
||||
export default Service.extend({
|
||||
searchable: function() {
|
||||
return {
|
||||
addEventListener: function() {},
|
||||
removeEventListener: function() {},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
import intention from 'consul-ui/search/predicates/intention';
|
||||
import token from 'consul-ui/search/filters/token';
|
||||
import policy from 'consul-ui/search/filters/policy';
|
||||
import role from 'consul-ui/search/filters/role';
|
||||
import kv from 'consul-ui/search/filters/kv';
|
||||
import acl from 'consul-ui/search/filters/acl';
|
||||
import node from 'consul-ui/search/filters/node';
|
||||
// service instance
|
||||
import nodeService from 'consul-ui/search/filters/node/service';
|
||||
import serviceNode from 'consul-ui/search/filters/service/node';
|
||||
import service from 'consul-ui/search/filters/service';
|
||||
import nspace from 'consul-ui/search/filters/nspace';
|
||||
|
||||
import filterableFactory from 'consul-ui/utils/search/filterable';
|
||||
const filterable = filterableFactory();
|
||||
const searchables = {
|
||||
token: token(filterable),
|
||||
acl: acl(filterable),
|
||||
policy: policy(filterable),
|
||||
role: role(filterable),
|
||||
kv: kv(filterable),
|
||||
node: node(filterable),
|
||||
serviceInstance: serviceNode(filterable),
|
||||
nodeservice: nodeService(filterable),
|
||||
service: service(filterable),
|
||||
nspace: nspace(filterable),
|
||||
};
|
||||
const predicates = {
|
||||
intention: intention(),
|
||||
};
|
||||
export default class SearchService extends Service {
|
||||
searchable(name) {
|
||||
return searchables[name];
|
||||
}
|
||||
predicate(name) {
|
||||
return predicates[name];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
%notice {
|
||||
border-radius: $decor-radius-100;
|
||||
border: 1px solid;
|
||||
color: $black;
|
||||
}
|
||||
%notice p:last-child a:only-child {
|
||||
@extend %p3;
|
||||
|
@ -22,7 +23,6 @@
|
|||
%notice-info {
|
||||
border-color: $blue-100;
|
||||
background-color: $gray-010;
|
||||
color: $black;
|
||||
}
|
||||
%notice-info header * {
|
||||
color: $blue-700;
|
||||
|
@ -31,7 +31,11 @@
|
|||
@extend %frame-gray-800;
|
||||
}
|
||||
%notice-warning {
|
||||
@extend %frame-yellow-500;
|
||||
border-color: $yellow-100;
|
||||
background-color: $yellow-050;
|
||||
}
|
||||
%notice-warning header * {
|
||||
color: $yellow-800;
|
||||
}
|
||||
%notice-error {
|
||||
@extend %frame-red-500;
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
<a data-test-create href="{{href-to 'dc.intentions.create'}}" class="type-create">Create</a>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="toolbar">
|
||||
|
||||
{{#if (gt items.length 0) }}
|
||||
<Consul::Intention::SearchBar
|
||||
@search={{search}}
|
||||
|
@ -34,58 +35,55 @@
|
|||
@onfilter={{hash
|
||||
access=(action (mut access) value="target.selectedItems")
|
||||
}}
|
||||
/>
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="content">
|
||||
{{#let (filter (filter-predicate 'intention' filters) items) as |filtered|}}
|
||||
{{#let (sort-by (comparator 'intention' sort) filtered) as |sorted|}}
|
||||
<ChangeableSet @dispatcher={{searchable 'intention' sorted}} @terms={{search}}>
|
||||
<BlockSlot @name="content" as |searched|>
|
||||
<Consul::Intention::List
|
||||
@items={{searched}}
|
||||
@ondelete={{refresh-route}}
|
||||
>
|
||||
<:idle as |list|>
|
||||
<list.Table />
|
||||
</:idle>
|
||||
<:empty as |list|>
|
||||
<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>
|
||||
</:empty>
|
||||
</Consul::Intention::List>
|
||||
</BlockSlot>
|
||||
</ChangeableSet>
|
||||
{{/let}}
|
||||
{{/let}}
|
||||
</BlockSlot>
|
||||
</AppView>
|
||||
<Consul::Intention::List
|
||||
@sort={{sort}}
|
||||
@filters={{filters}}
|
||||
@search={{search}}
|
||||
@items={{items}}
|
||||
@ondelete={{refresh-route}}
|
||||
>
|
||||
<:idle as |list|>
|
||||
<list.CustomResourceNotice />
|
||||
<list.Table />
|
||||
</:idle>
|
||||
<:empty as |list|>
|
||||
<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>
|
||||
</:empty>
|
||||
</Consul::Intention::List>
|
||||
</BlockSlot>
|
||||
</AppView>
|
||||
{{/let}}
|
||||
{{/let}}
|
||||
{{/let}}
|
||||
|
|
|
@ -27,24 +27,17 @@
|
|||
}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{#let (filter (filter-predicate 'intention' filters) items) as |filtered|}}
|
||||
{{#let (sort-by (comparator 'intention' sort) filtered) as |sorted|}}
|
||||
<ChangeableSet @dispatcher={{searchable 'intention' sorted}} @terms={{search}}>
|
||||
<BlockSlot @name="content" as |searched|>
|
||||
|
||||
<Consul::Intention::List
|
||||
@items={{searched}}
|
||||
@sort={{sort}}
|
||||
@filters={{filters}}
|
||||
@search={{search}}
|
||||
@items={{items}}
|
||||
@ondelete={{refresh-route}}
|
||||
@routeName="dc.services.show.intentions.edit"
|
||||
>
|
||||
<:idle as |list|>
|
||||
{{#if (eq searched.length 1)}}
|
||||
{{#let searched.firstObject as |item|}}
|
||||
{{#if (eq search item.SourceName)}}
|
||||
<list.Check @item={{item}} />
|
||||
{{/if}}
|
||||
{{/let}}
|
||||
{{/if}}
|
||||
<list.CustomResourceNotice />
|
||||
<list.CheckNotice />
|
||||
<list.Table />
|
||||
</:idle>
|
||||
<:empty as |list|>
|
||||
|
@ -58,10 +51,6 @@
|
|||
</:empty>
|
||||
</Consul::Intention::List>
|
||||
|
||||
</BlockSlot>
|
||||
</ChangeableSet>
|
||||
{{/let}}
|
||||
{{/let}}
|
||||
</div>
|
||||
</div>
|
||||
{{/let}}
|
||||
|
|
|
@ -16,9 +16,9 @@ Feature: dc / intentions / deleting: Deleting items with confirmations, success
|
|||
---
|
||||
dc: datacenter
|
||||
---
|
||||
And I click actions on the intentions
|
||||
And I click delete on the intentions
|
||||
And I click confirmDelete on the intentions
|
||||
And I click actions on the intentionList.intentions
|
||||
And I click delete on the intentionList.intentions
|
||||
And I click confirmDelete on the intentionList.intentions
|
||||
Then a DELETE request was made to "/v1/connect/intentions/exact?source=default%2Fname&destination=default%2Fdestination&dc=datacenter"
|
||||
And "[data-notification]" has the "notification-delete" class
|
||||
And "[data-notification]" has the "success" class
|
||||
|
|
|
@ -9,4 +9,46 @@ Feature: dc / intentions / index
|
|||
---
|
||||
Then the url should be /dc-1/intentions
|
||||
And the title should be "Intentions - Consul"
|
||||
Then I see 3 intention models
|
||||
Then I see 3 intention models on the intentionList component
|
||||
Scenario: Viewing intentions in the listing live updates
|
||||
Given 1 datacenter model with the value "dc-1"
|
||||
Given 3 intention models
|
||||
And a network latency of 100
|
||||
When I visit the intentions page for yaml
|
||||
---
|
||||
dc: dc-1
|
||||
---
|
||||
Then the url should be /dc-1/intentions
|
||||
And pause until I see 3 intention models on the intentionList component
|
||||
And an external edit results in 5 intention models
|
||||
And pause until I see 5 intention models on the intentionList component
|
||||
And an external edit results in 1 intention model
|
||||
And pause until I see 1 intention models on the intentionList component
|
||||
And an external edit results in 0 intention models
|
||||
And pause until I see 0 intention models on the intentionList component
|
||||
Scenario: Viewing intentions in the listing with CRDs
|
||||
Given 1 datacenter model with the value "dc-1"
|
||||
And 1 intention models from yaml
|
||||
---
|
||||
Meta:
|
||||
external-source: kubernetes
|
||||
---
|
||||
When I visit the intentions page for yaml
|
||||
---
|
||||
dc: dc-1
|
||||
---
|
||||
Then the url should be /dc-1/intentions
|
||||
Then I see customResourceNotice on the intentionList
|
||||
Scenario: Viewing intentions in the listing without CRDs
|
||||
Given 1 datacenter model with the value "dc-1"
|
||||
And 1 intention models from yaml
|
||||
---
|
||||
Meta:
|
||||
external-source: consul
|
||||
---
|
||||
When I visit the intentions page for yaml
|
||||
---
|
||||
dc: dc-1
|
||||
---
|
||||
Then the url should be /dc-1/intentions
|
||||
Then I don't see customResourceNotice on the intentionList
|
||||
|
|
|
@ -21,12 +21,12 @@ Feature: dc / intentions / navigation
|
|||
---
|
||||
Then the url should be /dc-1/intentions
|
||||
And the title should be "Intentions - Consul"
|
||||
Then I see 3 intention models
|
||||
Then I see 3 intention models on the intentionList component
|
||||
Given 1 intention model from yaml
|
||||
---
|
||||
ID: 755b72bd-f5ab-4c92-90cc-bed0e7d8e9f0
|
||||
---
|
||||
When I click intention on the intentions
|
||||
When I click intention on the intentionList.intentions component
|
||||
Then a GET request was made to "/v1/internal/ui/services?dc=dc-1&ns=*"
|
||||
And I click "[data-test-back]"
|
||||
Then the url should be /dc-1/intentions
|
||||
|
@ -37,7 +37,7 @@ Feature: dc / intentions / navigation
|
|||
---
|
||||
Then the url should be /dc-1/intentions
|
||||
And the title should be "Intentions - Consul"
|
||||
Then I see 3 intention models
|
||||
Then I see 3 intention models on the intentionList component
|
||||
When I click create
|
||||
Then the url should be /dc-1/intentions/create
|
||||
And I click "[data-test-back]"
|
||||
|
|
|
@ -16,10 +16,10 @@ Feature: dc / intentions / sorting
|
|||
---
|
||||
dc: dc-1
|
||||
---
|
||||
Then I see 6 intention models
|
||||
Then I see 6 intention models on the intentionList component
|
||||
When I click selected on the sort
|
||||
When I click options.1.button on the sort
|
||||
Then I see action on the intentions vertically like yaml
|
||||
Then I see action on the intentionList.intentions vertically like yaml
|
||||
---
|
||||
- "deny"
|
||||
- "deny"
|
||||
|
@ -30,7 +30,7 @@ Feature: dc / intentions / sorting
|
|||
---
|
||||
When I click selected on the sort
|
||||
When I click options.0.button on the sort
|
||||
Then I see action on the intentions vertically like yaml
|
||||
Then I see action on the intentionList.intentions vertically like yaml
|
||||
---
|
||||
- "allow"
|
||||
- "allow"
|
||||
|
|
|
@ -24,7 +24,6 @@ Feature: dc / list-blocking
|
|||
------------------------------------------------
|
||||
| Page | Model | Url |
|
||||
| nodes | node | nodes |
|
||||
| intentions | intention | intentions |
|
||||
------------------------------------------------
|
||||
Scenario: Viewing detail pages with a listing for [Page]
|
||||
Given 3 [Model] models
|
||||
|
|
|
@ -37,11 +37,11 @@ Feature: dc / services / show / intentions: Intentions per service
|
|||
When I click intentions on the tabs
|
||||
And I see intentionsIsSelected on the tabs
|
||||
Scenario: I can see intentions
|
||||
And I see 3 intention models
|
||||
And I see 3 intention models on the intentionList component
|
||||
Scenario: I can delete intentions
|
||||
And I click actions on the intentions
|
||||
And I click delete on the intentions
|
||||
And I click confirmDelete on the intentions
|
||||
And I click actions on the intentionList.intentions component
|
||||
And I click delete on the intentionList.intentions component
|
||||
And I click confirmDelete on the intentionList.intentions
|
||||
Then a DELETE request was made to "/v1/connect/intentions/exact?source=default%2Fname&destination=default%2Fdestination&dc=dc1"
|
||||
And "[data-notification]" has the "notification-delete" class
|
||||
And "[data-notification]" has the "success" class
|
||||
|
|
|
@ -94,7 +94,7 @@ const morePopoverMenu = morePopoverMenuFactory(clickable);
|
|||
const popoverSelect = popoverSelectFactory(clickable, collection);
|
||||
const emptyState = emptyStateFactory(isPresent);
|
||||
|
||||
const consulIntentionList = consulIntentionListFactory(collection, clickable, attribute, deletable);
|
||||
const consulIntentionList = consulIntentionListFactory(collection, clickable, attribute, isPresent, deletable);
|
||||
const consulNspaceList = consulNspaceListFactory(
|
||||
collection,
|
||||
clickable,
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
export default function(visitable, creatable, clickable, intentions, popoverSelect) {
|
||||
return creatable({
|
||||
return {
|
||||
visit: visitable('/:dc/intentions'),
|
||||
intentions: intentions(),
|
||||
intentionList: intentions(),
|
||||
sort: popoverSelect('[data-test-sort-control]'),
|
||||
create: clickable('[data-test-create]'),
|
||||
});
|
||||
...creatable({})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ export default function(visitable, attribute, collection, text, intentions, filt
|
|||
instances: collection('.consul-service-instance-list > ul > li:not(:first-child)', {
|
||||
address: text('[data-test-address]'),
|
||||
}),
|
||||
intentions: intentions(),
|
||||
intentionList: intentions(),
|
||||
};
|
||||
page.tabs.upstreamsTab = {
|
||||
services: collection('.consul-upstream-list > ul > li:not(:first-child)', {
|
||||
|
|
|
@ -11,6 +11,18 @@ export default function(scenario, assert, find, currentPage, pauseUntil, plurali
|
|||
return retry();
|
||||
}, `Expected ${num} ${model}s`);
|
||||
})
|
||||
.then('pause until I see $number $model model[s]? on the $component component', function(num, model, component) {
|
||||
return pauseUntil(function(resolve, reject, retry) {
|
||||
const obj = find(component);
|
||||
const len = obj[pluralize(model)].filter(function(item) {
|
||||
return item.isVisible;
|
||||
}).length;
|
||||
if (len === num) {
|
||||
return resolve();
|
||||
}
|
||||
return retry();
|
||||
}, `Expected ${num} ${model}s`);
|
||||
})
|
||||
.then(['I see $num $model model[s]?'], function(num, model) {
|
||||
const len = currentPage()[pluralize(model)].filter(function(item) {
|
||||
return item.isVisible;
|
||||
|
|
|
@ -101,7 +101,7 @@ export default function(scenario, assert, find, currentPage, $) {
|
|||
component,
|
||||
yaml
|
||||
) {
|
||||
const _component = currentPage()[component];
|
||||
const _component = find(component);
|
||||
const iterator = new Array(_component.length).fill(true);
|
||||
assert.ok(iterator.length > 0);
|
||||
|
||||
|
|
|
@ -4,7 +4,11 @@ export default function(scenario, find, click) {
|
|||
return click(selector);
|
||||
})
|
||||
// TODO: Probably nicer to think of better vocab than having the 'without " rule'
|
||||
.when(['I click (?!")$property(?!")', 'I click $property on the $component'], function(
|
||||
.when([
|
||||
'I click (?!")$property(?!")',
|
||||
'I click $property on the $component',
|
||||
'I click $property on the $component component'
|
||||
], function(
|
||||
property,
|
||||
component,
|
||||
next
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import getFilter from 'consul-ui/search/filters/intention';
|
||||
import getPredicate from 'consul-ui/search/predicates/intention';
|
||||
import { module, test } from 'qunit';
|
||||
|
||||
module('Unit | Search | Filter | intention', function() {
|
||||
const filter = getFilter(cb => cb);
|
||||
module('Unit | Search | Predicate | intention', function() {
|
||||
const predicate = getPredicate();
|
||||
test('items are found by properties', function(assert) {
|
||||
[
|
||||
{
|
||||
|
@ -14,9 +14,7 @@ module('Unit | Search | Filter | intention', function() {
|
|||
DestinationName: 'hiT',
|
||||
},
|
||||
].forEach(function(item) {
|
||||
const actual = filter(item, {
|
||||
s: 'hit',
|
||||
});
|
||||
const actual = predicate('hit')(item);
|
||||
assert.ok(actual);
|
||||
});
|
||||
});
|
||||
|
@ -27,9 +25,7 @@ module('Unit | Search | Filter | intention', function() {
|
|||
DestinationName: 'destination',
|
||||
},
|
||||
].forEach(function(item) {
|
||||
const actual = filter(item, {
|
||||
s: '*',
|
||||
});
|
||||
const actual = predicate('*')(item);
|
||||
assert.notOk(actual);
|
||||
});
|
||||
});
|
||||
|
@ -44,9 +40,7 @@ module('Unit | Search | Filter | intention', function() {
|
|||
DestinationName: '*',
|
||||
},
|
||||
].forEach(function(item) {
|
||||
const actual = filter(item, {
|
||||
s: '*',
|
||||
});
|
||||
const actual = predicate('*')(item);
|
||||
assert.ok(actual);
|
||||
});
|
||||
});
|
||||
|
@ -62,9 +56,7 @@ module('Unit | Search | Filter | intention', function() {
|
|||
},
|
||||
].forEach(function(item) {
|
||||
['All Services (*)', 'SerVices', '(*)', '*', 'vIces', 'lL Ser'].forEach(function(term) {
|
||||
const actual = filter(item, {
|
||||
s: term,
|
||||
});
|
||||
const actual = predicate(term)(item);
|
||||
assert.ok(actual);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue