ui: Per Service Intentions Tab (#7615)
* Add model layer support for filtering intentions by service * Add Route, Controller and template for services.show.intentions tab We are still loading the intentions themselves in the parent Route for the moment * Load the intentions in in the parent route for the moment * Temporarily add support for returning to history -1 Once we have an intention form underneath the service/intention tab this will no longer be needed * Add the new tab and enable blocking queries for it * Add some further acceptance testing around intention listings
This commit is contained in:
parent
b43ecb8fa7
commit
455dfe0a1b
|
@ -6,11 +6,14 @@ import { SLUG_KEY } from 'consul-ui/models/intention';
|
|||
|
||||
// TODO: Update to use this.formatDatacenter()
|
||||
export default Adapter.extend({
|
||||
requestForQuery: function(request, { dc, index, id }) {
|
||||
requestForQuery: function(request, { dc, filter, index }) {
|
||||
return request`
|
||||
GET /v1/connect/intentions?${{ dc }}
|
||||
|
||||
${{ index }}
|
||||
${{
|
||||
index,
|
||||
filter,
|
||||
}}
|
||||
`;
|
||||
},
|
||||
requestForQueryRecord: function(request, { dc, index, id }) {
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
import Controller from '@ember/controller';
|
||||
import { get, computed } from '@ember/object';
|
||||
import WithSearching from 'consul-ui/mixins/with-searching';
|
||||
|
||||
export default Controller.extend(WithSearching, {
|
||||
queryParams: {
|
||||
s: {
|
||||
as: 'filter',
|
||||
replace: true,
|
||||
},
|
||||
},
|
||||
init: function() {
|
||||
this.searchParams = {
|
||||
intention: 's',
|
||||
};
|
||||
this._super(...arguments);
|
||||
},
|
||||
searchable: computed('intentions', function() {
|
||||
return get(this, 'searchables.intention')
|
||||
.add(this.intentions)
|
||||
.search(get(this, this.searchParams.intention));
|
||||
}),
|
||||
actions: {
|
||||
route: function() {
|
||||
this.send(...arguments);
|
||||
},
|
||||
},
|
||||
});
|
|
@ -61,6 +61,7 @@ export function initialize(container) {
|
|||
services: {
|
||||
repo: 'repository/service/event-source',
|
||||
chainRepo: 'repository/discovery-chain/event-source',
|
||||
intentionRepo: 'repository/intention/event-source',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
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, {
|
||||
|
@ -14,4 +15,25 @@ export default Mixin.create(WithBlockingActions, {
|
|||
}
|
||||
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);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -10,7 +10,19 @@ export default Route.extend(WithIntentionActions, {
|
|||
repo: service('repository/intention'),
|
||||
servicesRepo: service('repository/service'),
|
||||
nspacesRepo: service('repository/nspace/disabled'),
|
||||
model: function(params) {
|
||||
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
|
||||
|
@ -21,6 +33,7 @@ export default Route.extend(WithIntentionActions, {
|
|||
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,
|
||||
|
|
|
@ -5,15 +5,18 @@ import { get } from '@ember/object';
|
|||
|
||||
export default Route.extend({
|
||||
repo: service('repository/service'),
|
||||
intentionRepo: service('repository/intention'),
|
||||
chainRepo: service('repository/discovery-chain'),
|
||||
settings: service('settings'),
|
||||
model: function(params) {
|
||||
model: function(params, transition = {}) {
|
||||
const dc = this.modelFor('dc').dc.Name;
|
||||
const nspace = this.modelFor('nspace').nspace.substr(1);
|
||||
return hash({
|
||||
item: this.repo.findBySlug(params.name, dc, nspace),
|
||||
intentions: this.intentionRepo.findByService(params.name, dc, nspace),
|
||||
urls: this.settings.findBySlug('urls'),
|
||||
dc: dc,
|
||||
nspace: nspace,
|
||||
}).then(model => {
|
||||
return hash({
|
||||
chain: ['connect-proxy', 'mesh-gateway'].includes(get(model, 'item.Service.Kind'))
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
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({
|
||||
export default Route.extend(WithIntentionActions, {
|
||||
repo: service('repository/intention'),
|
||||
model: function() {
|
||||
const parent = this.routeName
|
||||
.split('.')
|
||||
|
@ -11,4 +14,8 @@ export default Route.extend({
|
|||
setupController: function(controller, model) {
|
||||
controller.setProperties(model);
|
||||
},
|
||||
// Overwrite default afterDelete action to just refresh
|
||||
afterDelete: function() {
|
||||
return this.refresh();
|
||||
},
|
||||
});
|
||||
|
|
|
@ -8,4 +8,17 @@ export default RepositoryService.extend({
|
|||
getPrimaryKey: function() {
|
||||
return PRIMARY_KEY;
|
||||
},
|
||||
findByService: function(slug, dc, nspace, configuration = {}) {
|
||||
const query = {
|
||||
dc: dc,
|
||||
nspace: nspace,
|
||||
filter: `SourceName == ${slug} or DestinationName == ${slug}`,
|
||||
};
|
||||
if (typeof configuration.cursor !== 'undefined') {
|
||||
query.index = configuration.cursor;
|
||||
}
|
||||
return this.store.query(this.getModelName(), {
|
||||
...query,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
@ -10,7 +10,14 @@
|
|||
</BlockSlot>
|
||||
<BlockSlot @name="breadcrumbs">
|
||||
<ol>
|
||||
<li><a data-test-back href={{href-to 'dc.intentions'}}>All Intentions</a></li>
|
||||
{{#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">
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
<AppView @class="service show">
|
||||
<BlockSlot @name="notification" as |status type|>
|
||||
{{partial 'dc/services/notifications'}}
|
||||
{{partial 'dc/intentions/notifications'}}
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="breadcrumbs">
|
||||
<ol>
|
||||
|
@ -27,6 +28,7 @@
|
|||
compact
|
||||
(array
|
||||
(hash label="Instances" href=(href-to "dc.services.show.instances") selected=(is-href "dc.services.show.instances"))
|
||||
(hash label="Intentions" href=(href-to "dc.services.show.intentions") selected=(is-href "dc.services.show.intentions"))
|
||||
(if (not-eq chain) (hash label="Routing" href=(href-to "dc.services.show.routing") selected=(is-href "dc.services.show.routing")) '')
|
||||
(hash label="Tags" href=(href-to "dc.services.show.tags") selected=(is-href "dc.services.show.tags"))
|
||||
)
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
<div id="intentions" class="tab-section">
|
||||
<div role="tabpanel">
|
||||
{{#if (gt intentions.length 0) }}
|
||||
<input type="checkbox" id="toolbar-toggle" />
|
||||
<form class="filter-bar">
|
||||
<FreetextFilter @searchable={{searchable}} @value={{s}} @placeholder="Search" />
|
||||
</form>
|
||||
{{/if}}
|
||||
<ChangeableSet @dispatcher={{searchable}}>
|
||||
<BlockSlot @name="set" as |filtered|>
|
||||
<ConsulIntentionList
|
||||
@items={{filtered}}
|
||||
@ondelete={{action "route" "delete"}}
|
||||
/>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="empty">
|
||||
<p>
|
||||
There are no intentions for this service.
|
||||
</p>
|
||||
</BlockSlot>
|
||||
</ChangeableSet>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,12 @@
|
|||
@setupApplicationTest
|
||||
Feature: dc / intentions / index
|
||||
Scenario: Viewing intentions in the listing
|
||||
Given 1 datacenter model with the value "dc-1"
|
||||
And 3 intention models
|
||||
When I visit the intentions page for yaml
|
||||
---
|
||||
dc: dc-1
|
||||
---
|
||||
Then the url should be /dc-1/intentions
|
||||
And the title should be "Intentions - Consul"
|
||||
Then I see 3 intention models
|
|
@ -0,0 +1,31 @@
|
|||
@setupApplicationTest
|
||||
Feature: dc / services / intentions: Intentions per service
|
||||
Background:
|
||||
Given 1 datacenter model with the value "dc1"
|
||||
And 1 node models
|
||||
And 1 service model from yaml
|
||||
---
|
||||
- Service:
|
||||
Kind: consul
|
||||
Name: service-0
|
||||
ID: service-0-with-id
|
||||
---
|
||||
And 3 intention models
|
||||
When I visit the service page for yaml
|
||||
---
|
||||
dc: dc1
|
||||
service: service-0
|
||||
---
|
||||
And the title should be "service-0 - Consul"
|
||||
And I see intentions on the tabs
|
||||
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
|
||||
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
|
||||
Then a DELETE request was made to "/v1/connect/intentions/ee52203d-989f-4f7a-ab5a-2bef004164ca?dc=dc1"
|
||||
And "[data-notification]" has the "notification-delete" class
|
||||
And "[data-notification]" has the "success" class
|
|
@ -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);
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
}
|
|
@ -14,6 +14,8 @@ import createSubmitable from 'consul-ui/tests/lib/page-object/createSubmitable';
|
|||
import createCreatable from 'consul-ui/tests/lib/page-object/createCreatable';
|
||||
import createCancelable from 'consul-ui/tests/lib/page-object/createCancelable';
|
||||
|
||||
// TODO: All component-like page objects should be moved into the component folder
|
||||
// along with all of its other dependencies once we can mae ember-cli ignore them
|
||||
import page from 'consul-ui/tests/pages/components/page';
|
||||
import radiogroup from 'consul-ui/tests/lib/page-object/radiogroup';
|
||||
import tabgroup from 'consul-ui/tests/lib/page-object/tabgroup';
|
||||
|
@ -26,6 +28,8 @@ import policyFormFactory from 'consul-ui/tests/pages/components/policy-form';
|
|||
import policySelectorFactory from 'consul-ui/tests/pages/components/policy-selector';
|
||||
import roleFormFactory from 'consul-ui/tests/pages/components/role-form';
|
||||
import roleSelectorFactory from 'consul-ui/tests/pages/components/role-selector';
|
||||
import consulIntentionListFactory from 'consul-ui/tests/pages/components/consul-intention-list';
|
||||
|
||||
// TODO: should this specifically be modal or form?
|
||||
// should all forms be forms?
|
||||
|
||||
|
@ -65,13 +69,26 @@ const policySelector = policySelectorFactory(clickable, deletable, collection, a
|
|||
const roleForm = roleFormFactory(submitable, cancelable, policySelector);
|
||||
const roleSelector = roleSelectorFactory(clickable, deletable, collection, alias, roleForm);
|
||||
|
||||
const consulIntentionList = consulIntentionListFactory(collection, clickable, attribute, deletable);
|
||||
|
||||
export default {
|
||||
index: create(index(visitable, collection)),
|
||||
dcs: create(dcs(visitable, clickable, attribute, collection)),
|
||||
services: create(
|
||||
services(visitable, clickable, text, attribute, collection, page, catalogFilter, radiogroup)
|
||||
),
|
||||
service: create(service(visitable, attribute, collection, text, catalogFilter, tabgroup)),
|
||||
service: create(
|
||||
service(
|
||||
visitable,
|
||||
clickable,
|
||||
attribute,
|
||||
collection,
|
||||
text,
|
||||
consulIntentionList,
|
||||
catalogFilter,
|
||||
tabgroup
|
||||
)
|
||||
),
|
||||
instance: create(instance(visitable, attribute, collection, text, tabgroup)),
|
||||
nodes: create(nodes(visitable, clickable, attribute, collection, catalogFilter)),
|
||||
node: create(node(visitable, deletable, clickable, attribute, collection, tabgroup)),
|
||||
|
@ -113,9 +130,7 @@ export default {
|
|||
token: create(
|
||||
token(visitable, submitable, deletable, cancelable, clickable, policySelector, roleSelector)
|
||||
),
|
||||
intentions: create(
|
||||
intentions(visitable, deletable, creatable, clickable, attribute, collection, intentionFilter)
|
||||
),
|
||||
intentions: create(intentions(visitable, creatable, consulIntentionList, intentionFilter)),
|
||||
intention: create(intention(visitable, submitable, deletable, cancelable)),
|
||||
nspaces: create(
|
||||
nspaces(visitable, deletable, creatable, clickable, attribute, collection, text, freetextFilter)
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
export default (collection, clickable, attribute, deletable) => () => {
|
||||
return collection('.consul-intention-list [data-test-tabular-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(),
|
||||
});
|
||||
};
|
|
@ -1,19 +1,7 @@
|
|||
export default function(visitable, deletable, creatable, clickable, attribute, collection, filter) {
|
||||
export default function(visitable, creatable, intentions, filter) {
|
||||
return creatable({
|
||||
visit: visitable('/:dc/intentions'),
|
||||
intentions: collection(
|
||||
'[data-test-tabular-row]',
|
||||
deletable({
|
||||
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'),
|
||||
})
|
||||
),
|
||||
intentions: intentions(),
|
||||
filter: filter,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,14 +1,26 @@
|
|||
export default function(visitable, attribute, collection, text, filter, tabs) {
|
||||
export default function(
|
||||
visitable,
|
||||
clickable,
|
||||
attribute,
|
||||
collection,
|
||||
text,
|
||||
intentions,
|
||||
filter,
|
||||
tabs
|
||||
) {
|
||||
return {
|
||||
visit: visitable('/:dc/services/:service'),
|
||||
externalSource: attribute('data-test-external-source', 'h1 span'),
|
||||
instances: collection('#instances [data-test-tabular-row]', {
|
||||
address: text('[data-test-address]'),
|
||||
}),
|
||||
dashboardAnchor: {
|
||||
href: attribute('href', '[data-test-dashboard-anchor]'),
|
||||
},
|
||||
tabs: tabs('tab', ['instances', 'routing', 'tags']),
|
||||
tabs: tabs('tab', ['instances', 'intentions', 'routing', 'tags']),
|
||||
filter: filter,
|
||||
|
||||
// TODO: These need to somehow move to subpages
|
||||
instances: collection('#instances [data-test-tabular-row]', {
|
||||
address: text('[data-test-address]'),
|
||||
}),
|
||||
intentions: intentions(),
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue