Skip to content

Commit e7dfed9

Browse files
johncowenkaxcode
authored andcommitted
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
1 parent 1b93c01 commit e7dfed9

File tree

19 files changed

+239
-29
lines changed

19 files changed

+239
-29
lines changed

ui-v2/app/adapters/intention.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@ import { SLUG_KEY } from 'consul-ui/models/intention';
66

77
// TODO: Update to use this.formatDatacenter()
88
export default Adapter.extend({
9-
requestForQuery: function(request, { dc, index, id }) {
9+
requestForQuery: function(request, { dc, filter, index }) {
1010
return request`
1111
GET /v1/connect/intentions?${{ dc }}
1212
13-
${{ index }}
13+
${{
14+
index,
15+
filter,
16+
}}
1417
`;
1518
},
1619
requestForQueryRecord: function(request, { dc, index, id }) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import Controller from '@ember/controller';
2+
import { get, computed } from '@ember/object';
3+
import WithSearching from 'consul-ui/mixins/with-searching';
4+
5+
export default Controller.extend(WithSearching, {
6+
queryParams: {
7+
s: {
8+
as: 'filter',
9+
replace: true,
10+
},
11+
},
12+
init: function() {
13+
this.searchParams = {
14+
intention: 's',
15+
};
16+
this._super(...arguments);
17+
},
18+
searchable: computed('intentions', function() {
19+
return get(this, 'searchables.intention')
20+
.add(this.intentions)
21+
.search(get(this, this.searchParams.intention));
22+
}),
23+
actions: {
24+
route: function() {
25+
this.send(...arguments);
26+
},
27+
},
28+
});

ui-v2/app/instance-initializers/event-source.js

+1
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export function initialize(container) {
6161
services: {
6262
repo: 'repository/service/event-source',
6363
chainRepo: 'repository/discovery-chain/event-source',
64+
intentionRepo: 'repository/intention/event-source',
6465
},
6566
},
6667
{

ui-v2/app/mixins/intention/with-actions.js

+22
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Mixin from '@ember/object/mixin';
22
import WithBlockingActions from 'consul-ui/mixins/with-blocking-actions';
3+
import { get } from '@ember/object';
34

45
import { INTERNAL_SERVER_ERROR as HTTP_INTERNAL_SERVER_ERROR } from 'consul-ui/utils/http/status';
56
export default Mixin.create(WithBlockingActions, {
@@ -14,4 +15,25 @@ export default Mixin.create(WithBlockingActions, {
1415
}
1516
return type;
1617
},
18+
afterUpdate: function(item) {
19+
if (get(this, 'history.length') > 0) {
20+
return this.transitionTo(this.history[0].key, this.history[0].value);
21+
}
22+
return this._super(...arguments);
23+
},
24+
afterCreate: function(item) {
25+
if (get(this, 'history.length') > 0) {
26+
return this.transitionTo(this.history[0].key, this.history[0].value);
27+
}
28+
return this._super(...arguments);
29+
},
30+
afterDelete: function(item) {
31+
if (get(this, 'history.length') > 0) {
32+
return this.transitionTo(this.history[0].key, this.history[0].value);
33+
}
34+
if (this.routeName === 'dc.services.show') {
35+
return this.transitionTo(this.routeName, this._router.currentRoute.params.name);
36+
}
37+
return this._super(...arguments);
38+
},
1739
});

ui-v2/app/routes/dc/intentions/edit.js

+14-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,19 @@ export default Route.extend(WithIntentionActions, {
1010
repo: service('repository/intention'),
1111
servicesRepo: service('repository/service'),
1212
nspacesRepo: service('repository/nspace/disabled'),
13-
model: function(params) {
13+
buildRouteInfoMetadata: function() {
14+
return { history: this.history };
15+
},
16+
model: function(params, transition) {
17+
const from = get(transition, 'from');
18+
this.history = [];
19+
if (from && get(from, 'name') === 'dc.services.show.intentions') {
20+
this.history.push({
21+
key: get(from, 'name'),
22+
value: get(from, 'parent.params.name'),
23+
});
24+
}
25+
1426
const dc = this.modelFor('dc').dc.Name;
1527
// We load all of your services that you are able to see here
1628
// as even if it doesn't exist in the namespace you are targetting
@@ -21,6 +33,7 @@ export default Route.extend(WithIntentionActions, {
2133
item: this.repo.findBySlug(params.id, dc, nspace),
2234
services: this.servicesRepo.findAllByDatacenter(dc, nspace),
2335
nspaces: this.nspacesRepo.findAll(),
36+
history: this.history,
2437
}).then(function(model) {
2538
return {
2639
...model,

ui-v2/app/routes/dc/services/show.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,18 @@ import { get } from '@ember/object';
55

66
export default Route.extend({
77
repo: service('repository/service'),
8+
intentionRepo: service('repository/intention'),
89
chainRepo: service('repository/discovery-chain'),
910
settings: service('settings'),
10-
model: function(params) {
11+
model: function(params, transition = {}) {
1112
const dc = this.modelFor('dc').dc.Name;
1213
const nspace = this.modelFor('nspace').nspace.substr(1);
1314
return hash({
1415
item: this.repo.findBySlug(params.name, dc, nspace),
16+
intentions: this.intentionRepo.findByService(params.name, dc, nspace),
1517
urls: this.settings.findBySlug('urls'),
1618
dc: dc,
19+
nspace: nspace,
1720
}).then(model => {
1821
return hash({
1922
chain: ['connect-proxy', 'mesh-gateway'].includes(get(model, 'item.Service.Kind'))

ui-v2/app/routes/dc/services/show/intentions.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import Route from '@ember/routing/route';
2+
import { inject as service } from '@ember/service';
3+
import WithIntentionActions from 'consul-ui/mixins/intention/with-actions';
24

3-
export default Route.extend({
5+
export default Route.extend(WithIntentionActions, {
6+
repo: service('repository/intention'),
47
model: function() {
58
const parent = this.routeName
69
.split('.')
@@ -11,4 +14,8 @@ export default Route.extend({
1114
setupController: function(controller, model) {
1215
controller.setProperties(model);
1316
},
17+
// Overwrite default afterDelete action to just refresh
18+
afterDelete: function() {
19+
return this.refresh();
20+
},
1421
});

ui-v2/app/services/repository/intention.js

+13
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,17 @@ export default RepositoryService.extend({
88
getPrimaryKey: function() {
99
return PRIMARY_KEY;
1010
},
11+
findByService: function(slug, dc, nspace, configuration = {}) {
12+
const query = {
13+
dc: dc,
14+
nspace: nspace,
15+
filter: `SourceName == ${slug} or DestinationName == ${slug}`,
16+
};
17+
if (typeof configuration.cursor !== 'undefined') {
18+
query.index = configuration.cursor;
19+
}
20+
return this.store.query(this.getModelName(), {
21+
...query,
22+
});
23+
},
1124
});

ui-v2/app/templates/dc/intentions/edit.hbs

+8-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,14 @@
1010
</BlockSlot>
1111
<BlockSlot @name="breadcrumbs">
1212
<ol>
13-
<li><a data-test-back href={{href-to 'dc.intentions'}}>All Intentions</a></li>
13+
{{#if (gt history.length 0)}}
14+
<li><a href={{href-to 'dc.services'}}>All Services</a></li>
15+
{{#let history.firstObject as |back|}}
16+
<li><a data-test-back href={{href-to back.key back.value}}>{{concat 'Service (' back.value ')'}}</a></li>
17+
{{/let}}
18+
{{else}}
19+
<li><a data-test-back href={{href-to 'dc.intentions'}}>All Intentions</a></li>
20+
{{/if}}
1421
</ol>
1522
</BlockSlot>
1623
<BlockSlot @name="header">

ui-v2/app/templates/dc/services/show.hbs

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<AppView @class="service show">
33
<BlockSlot @name="notification" as |status type|>
44
{{partial 'dc/services/notifications'}}
5+
{{partial 'dc/intentions/notifications'}}
56
</BlockSlot>
67
<BlockSlot @name="breadcrumbs">
78
<ol>
@@ -27,6 +28,7 @@
2728
compact
2829
(array
2930
(hash label="Instances" href=(href-to "dc.services.show.instances") selected=(is-href "dc.services.show.instances"))
31+
(hash label="Intentions" href=(href-to "dc.services.show.intentions") selected=(is-href "dc.services.show.intentions"))
3032
(if (not-eq chain) (hash label="Routing" href=(href-to "dc.services.show.routing") selected=(is-href "dc.services.show.routing")) '')
3133
(hash label="Tags" href=(href-to "dc.services.show.tags") selected=(is-href "dc.services.show.tags"))
3234
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<div id="intentions" class="tab-section">
2+
<div role="tabpanel">
3+
{{#if (gt intentions.length 0) }}
4+
<input type="checkbox" id="toolbar-toggle" />
5+
<form class="filter-bar">
6+
<FreetextFilter @searchable={{searchable}} @value={{s}} @placeholder="Search" />
7+
</form>
8+
{{/if}}
9+
<ChangeableSet @dispatcher={{searchable}}>
10+
<BlockSlot @name="set" as |filtered|>
11+
<ConsulIntentionList
12+
@items={{filtered}}
13+
@ondelete={{action "route" "delete"}}
14+
/>
15+
</BlockSlot>
16+
<BlockSlot @name="empty">
17+
<p>
18+
There are no intentions for this service.
19+
</p>
20+
</BlockSlot>
21+
</ChangeableSet>
22+
</div>
23+
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
@setupApplicationTest
2+
Feature: dc / intentions / index
3+
Scenario: Viewing intentions in the listing
4+
Given 1 datacenter model with the value "dc-1"
5+
And 3 intention models
6+
When I visit the intentions page for yaml
7+
---
8+
dc: dc-1
9+
---
10+
Then the url should be /dc-1/intentions
11+
And the title should be "Intentions - Consul"
12+
Then I see 3 intention models
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
@setupApplicationTest
2+
Feature: dc / services / intentions: Intentions per service
3+
Background:
4+
Given 1 datacenter model with the value "dc1"
5+
And 1 node models
6+
And 1 service model from yaml
7+
---
8+
- Service:
9+
Kind: consul
10+
Name: service-0
11+
ID: service-0-with-id
12+
---
13+
And 3 intention models
14+
When I visit the service page for yaml
15+
---
16+
dc: dc1
17+
service: service-0
18+
---
19+
And the title should be "service-0 - Consul"
20+
And I see intentions on the tabs
21+
When I click intentions on the tabs
22+
And I see intentionsIsSelected on the tabs
23+
Scenario: I can see intentions
24+
And I see 3 intention models
25+
Scenario: I can delete intentions
26+
And I click actions on the intentions
27+
And I click delete on the intentions
28+
And I click confirmDelete on the intentions
29+
Then a DELETE request was made to "/v1/connect/intentions/ee52203d-989f-4f7a-ab5a-2bef004164ca?dc=dc1"
30+
And "[data-notification]" has the "notification-delete" class
31+
And "[data-notification]" has the "success" class
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import steps from '../../steps';
2+
3+
// step definitions that are shared between features should be moved to the
4+
// tests/acceptance/steps/steps.js file
5+
6+
export default function(assert) {
7+
return steps(assert).then('I should find a file', function() {
8+
assert.ok(true, this.step);
9+
});
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import steps from '../../../steps';
2+
3+
// step definitions that are shared between features should be moved to the
4+
// tests/acceptance/steps/steps.js file
5+
6+
export default function(assert) {
7+
return steps(assert).then('I should find a file', function() {
8+
assert.ok(true, this.step);
9+
});
10+
}

ui-v2/tests/pages.js

+19-4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import createSubmitable from 'consul-ui/tests/lib/page-object/createSubmitable';
1414
import createCreatable from 'consul-ui/tests/lib/page-object/createCreatable';
1515
import createCancelable from 'consul-ui/tests/lib/page-object/createCancelable';
1616

17+
// TODO: All component-like page objects should be moved into the component folder
18+
// along with all of its other dependencies once we can mae ember-cli ignore them
1719
import page from 'consul-ui/tests/pages/components/page';
1820
import radiogroup from 'consul-ui/tests/lib/page-object/radiogroup';
1921
import tabgroup from 'consul-ui/tests/lib/page-object/tabgroup';
@@ -26,6 +28,8 @@ import policyFormFactory from 'consul-ui/tests/pages/components/policy-form';
2628
import policySelectorFactory from 'consul-ui/tests/pages/components/policy-selector';
2729
import roleFormFactory from 'consul-ui/tests/pages/components/role-form';
2830
import roleSelectorFactory from 'consul-ui/tests/pages/components/role-selector';
31+
import consulIntentionListFactory from 'consul-ui/tests/pages/components/consul-intention-list';
32+
2933
// TODO: should this specifically be modal or form?
3034
// should all forms be forms?
3135

@@ -65,13 +69,26 @@ const policySelector = policySelectorFactory(clickable, deletable, collection, a
6569
const roleForm = roleFormFactory(submitable, cancelable, policySelector);
6670
const roleSelector = roleSelectorFactory(clickable, deletable, collection, alias, roleForm);
6771

72+
const consulIntentionList = consulIntentionListFactory(collection, clickable, attribute, deletable);
73+
6874
export default {
6975
index: create(index(visitable, collection)),
7076
dcs: create(dcs(visitable, clickable, attribute, collection)),
7177
services: create(
7278
services(visitable, clickable, text, attribute, collection, page, catalogFilter, radiogroup)
7379
),
74-
service: create(service(visitable, attribute, collection, text, catalogFilter, tabgroup)),
80+
service: create(
81+
service(
82+
visitable,
83+
clickable,
84+
attribute,
85+
collection,
86+
text,
87+
consulIntentionList,
88+
catalogFilter,
89+
tabgroup
90+
)
91+
),
7592
instance: create(instance(visitable, attribute, collection, text, tabgroup)),
7693
nodes: create(nodes(visitable, clickable, attribute, collection, catalogFilter)),
7794
node: create(node(visitable, deletable, clickable, attribute, collection, tabgroup)),
@@ -113,9 +130,7 @@ export default {
113130
token: create(
114131
token(visitable, submitable, deletable, cancelable, clickable, policySelector, roleSelector)
115132
),
116-
intentions: create(
117-
intentions(visitable, deletable, creatable, clickable, attribute, collection, intentionFilter)
118-
),
133+
intentions: create(intentions(visitable, creatable, consulIntentionList, intentionFilter)),
119134
intention: create(intention(visitable, submitable, deletable, cancelable)),
120135
nspaces: create(
121136
nspaces(visitable, deletable, creatable, clickable, attribute, collection, text, freetextFilter)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export default (collection, clickable, attribute, deletable) => () => {
2+
return collection('.consul-intention-list [data-test-tabular-row]', {
3+
source: attribute('data-test-intention-source', '[data-test-intention-source]'),
4+
destination: attribute('data-test-intention-destination', '[data-test-intention-destination]'),
5+
action: attribute('data-test-intention-action', '[data-test-intention-action]'),
6+
intention: clickable('a'),
7+
actions: clickable('label'),
8+
...deletable(),
9+
});
10+
};
+2-14
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,7 @@
1-
export default function(visitable, deletable, creatable, clickable, attribute, collection, filter) {
1+
export default function(visitable, creatable, intentions, filter) {
22
return creatable({
33
visit: visitable('/:dc/intentions'),
4-
intentions: collection(
5-
'[data-test-tabular-row]',
6-
deletable({
7-
source: attribute('data-test-intention-source', '[data-test-intention-source]'),
8-
destination: attribute(
9-
'data-test-intention-destination',
10-
'[data-test-intention-destination]'
11-
),
12-
action: attribute('data-test-intention-action', '[data-test-intention-action]'),
13-
intention: clickable('a'),
14-
actions: clickable('label'),
15-
})
16-
),
4+
intentions: intentions(),
175
filter: filter,
186
});
197
}

0 commit comments

Comments
 (0)