From 8e9fca9be665bbe2438727738d4e4b620961ad14 Mon Sep 17 00:00:00 2001 From: John Cowen Date: Thu, 16 Apr 2020 15:15:45 +0100 Subject: [PATCH] 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 --- ui-v2/app/adapters/intention.js | 7 +++-- .../dc/services/show/intentions.js | 28 +++++++++++++++++ .../app/instance-initializers/event-source.js | 1 + ui-v2/app/mixins/intention/with-actions.js | 22 +++++++++++++ ui-v2/app/routes/dc/intentions/edit.js | 15 ++++++++- ui-v2/app/routes/dc/services/show.js | 5 ++- .../app/routes/dc/services/show/intentions.js | 9 +++++- ui-v2/app/services/repository/intention.js | 13 ++++++++ ui-v2/app/templates/dc/intentions/edit.hbs | 9 +++++- ui-v2/app/templates/dc/services/show.hbs | 2 ++ .../templates/dc/services/show/intentions.hbs | 23 ++++++++++++++ .../acceptance/dc/intentions/index.feature | 12 +++++++ .../dc/services/show/intentions.feature | 31 +++++++++++++++++++ .../steps/dc/intentions/index-steps.js | 10 ++++++ .../dc/services/show/intentions-steps.js | 10 ++++++ ui-v2/tests/pages.js | 23 +++++++++++--- .../pages/components/consul-intention-list.js | 10 ++++++ ui-v2/tests/pages/dc/intentions/index.js | 16 ++-------- ui-v2/tests/pages/dc/services/show.js | 22 ++++++++++--- 19 files changed, 239 insertions(+), 29 deletions(-) create mode 100644 ui-v2/app/controllers/dc/services/show/intentions.js create mode 100644 ui-v2/app/templates/dc/services/show/intentions.hbs create mode 100644 ui-v2/tests/acceptance/dc/intentions/index.feature create mode 100644 ui-v2/tests/acceptance/dc/services/show/intentions.feature create mode 100644 ui-v2/tests/acceptance/steps/dc/intentions/index-steps.js create mode 100644 ui-v2/tests/acceptance/steps/dc/services/show/intentions-steps.js create mode 100644 ui-v2/tests/pages/components/consul-intention-list.js diff --git a/ui-v2/app/adapters/intention.js b/ui-v2/app/adapters/intention.js index fb27a5a0aa..d98afe66b9 100644 --- a/ui-v2/app/adapters/intention.js +++ b/ui-v2/app/adapters/intention.js @@ -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 }) { diff --git a/ui-v2/app/controllers/dc/services/show/intentions.js b/ui-v2/app/controllers/dc/services/show/intentions.js new file mode 100644 index 0000000000..89173ddd50 --- /dev/null +++ b/ui-v2/app/controllers/dc/services/show/intentions.js @@ -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); + }, + }, +}); diff --git a/ui-v2/app/instance-initializers/event-source.js b/ui-v2/app/instance-initializers/event-source.js index 27acd00c5d..599d3fdddd 100644 --- a/ui-v2/app/instance-initializers/event-source.js +++ b/ui-v2/app/instance-initializers/event-source.js @@ -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', }, }, { diff --git a/ui-v2/app/mixins/intention/with-actions.js b/ui-v2/app/mixins/intention/with-actions.js index 8a1e707627..2e061a1466 100644 --- a/ui-v2/app/mixins/intention/with-actions.js +++ b/ui-v2/app/mixins/intention/with-actions.js @@ -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); + }, }); diff --git a/ui-v2/app/routes/dc/intentions/edit.js b/ui-v2/app/routes/dc/intentions/edit.js index 27f9967e59..1a06f2debd 100644 --- a/ui-v2/app/routes/dc/intentions/edit.js +++ b/ui-v2/app/routes/dc/intentions/edit.js @@ -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, diff --git a/ui-v2/app/routes/dc/services/show.js b/ui-v2/app/routes/dc/services/show.js index faa90c8485..816c804fb7 100644 --- a/ui-v2/app/routes/dc/services/show.js +++ b/ui-v2/app/routes/dc/services/show.js @@ -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')) diff --git a/ui-v2/app/routes/dc/services/show/intentions.js b/ui-v2/app/routes/dc/services/show/intentions.js index 9793d62c02..d3d4abae8e 100644 --- a/ui-v2/app/routes/dc/services/show/intentions.js +++ b/ui-v2/app/routes/dc/services/show/intentions.js @@ -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(); + }, }); diff --git a/ui-v2/app/services/repository/intention.js b/ui-v2/app/services/repository/intention.js index 7df0cb8a57..bade408e09 100644 --- a/ui-v2/app/services/repository/intention.js +++ b/ui-v2/app/services/repository/intention.js @@ -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, + }); + }, }); diff --git a/ui-v2/app/templates/dc/intentions/edit.hbs b/ui-v2/app/templates/dc/intentions/edit.hbs index 82c03f09f1..79f58397eb 100644 --- a/ui-v2/app/templates/dc/intentions/edit.hbs +++ b/ui-v2/app/templates/dc/intentions/edit.hbs @@ -10,7 +10,14 @@
    -
  1. All Intentions
  2. + {{#if (gt history.length 0)}} +
  3. All Services
  4. + {{#let history.firstObject as |back|}} +
  5. {{concat 'Service (' back.value ')'}}
  6. + {{/let}} + {{else}} +
  7. All Intentions
  8. + {{/if}}
diff --git a/ui-v2/app/templates/dc/services/show.hbs b/ui-v2/app/templates/dc/services/show.hbs index 39cf8f198c..469bb3e63e 100644 --- a/ui-v2/app/templates/dc/services/show.hbs +++ b/ui-v2/app/templates/dc/services/show.hbs @@ -2,6 +2,7 @@ {{partial 'dc/services/notifications'}} + {{partial 'dc/intentions/notifications'}}
    @@ -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")) ) diff --git a/ui-v2/app/templates/dc/services/show/intentions.hbs b/ui-v2/app/templates/dc/services/show/intentions.hbs new file mode 100644 index 0000000000..bf98d437c1 --- /dev/null +++ b/ui-v2/app/templates/dc/services/show/intentions.hbs @@ -0,0 +1,23 @@ +
    +
    +{{#if (gt intentions.length 0) }} + +
    + + +{{/if}} + + + + + +

    + There are no intentions for this service. +

    +
    +
    +
    +
    diff --git a/ui-v2/tests/acceptance/dc/intentions/index.feature b/ui-v2/tests/acceptance/dc/intentions/index.feature new file mode 100644 index 0000000000..5e1aa58b9f --- /dev/null +++ b/ui-v2/tests/acceptance/dc/intentions/index.feature @@ -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 diff --git a/ui-v2/tests/acceptance/dc/services/show/intentions.feature b/ui-v2/tests/acceptance/dc/services/show/intentions.feature new file mode 100644 index 0000000000..542fe97266 --- /dev/null +++ b/ui-v2/tests/acceptance/dc/services/show/intentions.feature @@ -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 diff --git a/ui-v2/tests/acceptance/steps/dc/intentions/index-steps.js b/ui-v2/tests/acceptance/steps/dc/intentions/index-steps.js new file mode 100644 index 0000000000..ba1093295f --- /dev/null +++ b/ui-v2/tests/acceptance/steps/dc/intentions/index-steps.js @@ -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); + }); +} diff --git a/ui-v2/tests/acceptance/steps/dc/services/show/intentions-steps.js b/ui-v2/tests/acceptance/steps/dc/services/show/intentions-steps.js new file mode 100644 index 0000000000..3231912b98 --- /dev/null +++ b/ui-v2/tests/acceptance/steps/dc/services/show/intentions-steps.js @@ -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); + }); +} diff --git a/ui-v2/tests/pages.js b/ui-v2/tests/pages.js index f42a76e85e..42f31cb1d6 100644 --- a/ui-v2/tests/pages.js +++ b/ui-v2/tests/pages.js @@ -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) diff --git a/ui-v2/tests/pages/components/consul-intention-list.js b/ui-v2/tests/pages/components/consul-intention-list.js new file mode 100644 index 0000000000..59844bcee1 --- /dev/null +++ b/ui-v2/tests/pages/components/consul-intention-list.js @@ -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(), + }); +}; diff --git a/ui-v2/tests/pages/dc/intentions/index.js b/ui-v2/tests/pages/dc/intentions/index.js index 5bd21010b5..0a0ba2e50c 100644 --- a/ui-v2/tests/pages/dc/intentions/index.js +++ b/ui-v2/tests/pages/dc/intentions/index.js @@ -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, }); } diff --git a/ui-v2/tests/pages/dc/services/show.js b/ui-v2/tests/pages/dc/services/show.js index 152b53df00..cd9b0f22a0 100644 --- a/ui-v2/tests/pages/dc/services/show.js +++ b/ui-v2/tests/pages/dc/services/show.js @@ -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(), }; }