diff --git a/ui-v2/app/adapters/topology.js b/ui-v2/app/adapters/topology.js new file mode 100644 index 000000000..e31e11749 --- /dev/null +++ b/ui-v2/app/adapters/topology.js @@ -0,0 +1,18 @@ +import Adapter from './application'; +// TODO: Update to use this.formatDatacenter() +export default Adapter.extend({ + requestForQueryRecord: function(request, { dc, ns, index, id, uri }) { + if (typeof id === 'undefined') { + throw new Error('You must specify an id'); + } + return request` + GET /v1/internal/ui/service-topology/${id}?${{ dc }} + X-Request-ID: ${uri} + + ${{ + ...this.formatNspace(ns), + index, + }} + `; + }, +}); diff --git a/ui-v2/app/components/consul-service-list/index.hbs b/ui-v2/app/components/consul-service-list/index.hbs index f9ba9d0e2..647992c8a 100644 --- a/ui-v2/app/components/consul-service-list/index.hbs +++ b/ui-v2/app/components/consul-service-list/index.hbs @@ -20,19 +20,9 @@ {{#if (gt item.InstanceCount 0)}} - {{#if (eq item.Kind 'terminating-gateway')}} - - {{item.Name}} - - {{else if (eq item.Kind 'ingress-gateway')}} - - {{item.Name}} - - {{else}} - - {{item.Name}} - - {{/if}} + + {{item.Name}} + {{else}}

{{item.Name}} diff --git a/ui-v2/app/components/topology-metrics/index.hbs b/ui-v2/app/components/topology-metrics/index.hbs new file mode 100644 index 000000000..bfb0b3e0f --- /dev/null +++ b/ui-v2/app/components/topology-metrics/index.hbs @@ -0,0 +1,162 @@ +{{on-window 'resize' (action this.calculate)}} + +

+{{#if (gt @downstreams.length 0)}} +
+
+

{{@dc}}

+ + + Only showing downstreams within the current datacenter for {{@service.Service.Service}}. + + +
+ {{#each @downstreams as |downstream|}} +
+

+ {{downstream.Name}} +

+
+ {{#let (service/health-percentage downstream) as |percentage|}} + {{#if (not-eq percentage.passing 0)}} + {{percentage.passing}}% + {{/if}} + {{#if (not-eq percentage.warning 0)}} + {{percentage.warning}}% + {{/if}} + {{#if (not-eq percentage.critical 0)}} + {{percentage.critical}}% + {{/if}} + {{/let}} +
+
+ {{/each}} +
+{{/if}} +
+
+ {{@service.Service.Service}} +
+
+ {{#if @metricsHref}} + Open metrics Dashboard + {{else}} + Configure metrics dashboard + {{/if}} +
+
+
+ {{#if (gt this.downLines.length 0)}} + + + + + + + + + + {{#each this.downLines as |svg| }} + + {{/each}} + + {{/if}} +
+{{#if (gt @upstreams.length 0)}} +
+ {{#each-in (group-by "Datacenter" @upstreams) as |dc upstreams|}} +
+

{{dc}}

+ {{#each upstreams as |upstream|}} +
+

+ {{upstream.Name}} +

+
+ {{#if (and nspace (env 'CONSUL_NSPACES_ENABLED'))}} +
+
+ + Namespace + +
+
+ {{upstream.Namespace}} +
+
+ {{/if}} + {{#if (eq upstream.Datacenter @dc)}} + {{#let (service/health-percentage upstream) as |percentage|}} + {{#if (not-eq percentage.passing 0)}} + {{percentage.passing}}% + {{/if}} + {{#if (not-eq percentage.warning 0)}} + {{percentage.warning}}% + {{/if}} + {{#if (not-eq percentage.critical 0)}} + {{percentage.critical}}% + {{/if}} + {{/let}} + {{else}} +
+
+ + We are unable to determine the health status of services in remote datacenters. + +
+
+ Health +
+
+ {{/if}} +
+
+ {{/each}} +
+ {{/each-in}} +
+{{/if}} +
+ {{#if (gt this.upLines.length 0)}} + + + + + + + + + + {{#each this.upLines as |svg| }} + + {{/each}} + + {{/if}} +
+
\ No newline at end of file diff --git a/ui-v2/app/components/topology-metrics/index.js b/ui-v2/app/components/topology-metrics/index.js new file mode 100644 index 000000000..b12776757 --- /dev/null +++ b/ui-v2/app/components/topology-metrics/index.js @@ -0,0 +1,73 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; + +export default class TopologyMetrics extends Component { + // =attributes + @tracked centerDimensions; + @tracked downView; + @tracked downLines = []; + @tracked upView; + @tracked upLines = []; + + // =methods + drawDownLines(items) { + return items.map(item => { + const dimensions = item.getBoundingClientRect(); + const dest = { + x: this.centerDimensions.x, + y: this.centerDimensions.y + this.centerDimensions.height / 4, + }; + const src = { + x: dimensions.x + dimensions.width, + y: dimensions.y + dimensions.height / 2, + }; + + return { + dest: dest, + src: src, + }; + }); + } + + drawUpLines(items) { + return items.map(item => { + const dimensions = item.getBoundingClientRect(); + const dest = { + x: dimensions.x - dimensions.width - 26, + y: dimensions.y + dimensions.height / 2, + }; + const src = { + x: this.centerDimensions.x + 20, + y: this.centerDimensions.y + this.centerDimensions.height / 4, + }; + + return { + dest: dest, + src: src, + }; + }); + } + + // =actions + @action + calculate() { + // Calculate viewBox dimensions + this.downView = document.querySelector('#downstream-lines').getBoundingClientRect(); + this.upView = document.querySelector('#upstream-lines').getBoundingClientRect(); + + // Get Card elements positions + const downCards = [...document.querySelectorAll('#downstream-container .card')]; + const grafanaCard = document.querySelector('#metrics-container'); + const upCards = [...document.querySelectorAll('#upstream-column .card')]; + + // Set center positioning points + this.centerDimensions = grafanaCard.getBoundingClientRect(); + + // Set Downstream Cards Positioning points + this.downLines = this.drawDownLines(downCards); + + // Set Upstream Cards Positioning points + this.upLines = this.drawUpLines(upCards); + } +} diff --git a/ui-v2/app/components/topology-metrics/index.scss b/ui-v2/app/components/topology-metrics/index.scss new file mode 100644 index 000000000..bc1825219 --- /dev/null +++ b/ui-v2/app/components/topology-metrics/index.scss @@ -0,0 +1,2 @@ +@import './skin'; +@import './layout'; diff --git a/ui-v2/app/components/topology-metrics/layout.scss b/ui-v2/app/components/topology-metrics/layout.scss new file mode 100644 index 000000000..50a9d3cb1 --- /dev/null +++ b/ui-v2/app/components/topology-metrics/layout.scss @@ -0,0 +1,98 @@ +.topology-container { + display: grid; + height: 100%; + align-items: start; + grid-template-columns: 2fr 20px 1fr 20px 2fr 20px 1fr 20px 2fr; + grid-template-rows: 50px 1fr 50px; +} + +// Grid Layout +#downstream-container { + grid-row: 1 / 3; + grid-column: 1 / 3; +} +#downstream-lines { + grid-row: 1 / 3; + grid-column: 2 / 5; +} +#upstream-lines { + grid-row: 1 / 3; + grid-column: 6 / 9; +} +#upstream-column { + grid-row: 1 / 3; + grid-column: 8 / 10; +} + +// Columns/Containers & Lines +#downstream-lines, +#upstream-lines { + z-index: 1; + position: relative; + height: 100%; +} +#downstream-container, +#upstream-container { + padding: 12px; +} +#downstream-container div:first-child { + display: inline-flex; + span::before { + @extend %with-info-circle-outline-mask, %as-pseudo; + margin-left: 4px; + } +} +#upstream-column #upstream-container:not(:last-child) { + margin-bottom: 8px; +} +#upstream-container .card:not(:last-child), +#downstream-container .card:not(:last-child) { + margin-bottom: 8px; +} +#upstream-container .card, +#downstream-container .card { + padding: 12px; + p { + font-size: 16px; + font-weight: 600; + margin-bottom: 0 !important; + } + div { + display: inline-flex; + dl { + display: inline-flex; + margin-right: 8px; + } + span { + margin-right: 8px; + } + span::before, + dt::before { + margin-right: 4px; + } + .nspace dt::before, + .health dt::before { + margin-top: 2px; + } + } +} + +// Metrics Container +#metrics-container { + grid-row: 2 / 3; + grid-column: 4 / 7; + + div:first-child { + padding: 12px; + border: none; + font-size: 16px; + font-weight: bold; + } + div:nth-child(2) { + padding: 18px; + a::before { + margin-right: 4px; + } + } +} + diff --git a/ui-v2/app/components/topology-metrics/skin.scss b/ui-v2/app/components/topology-metrics/skin.scss new file mode 100644 index 000000000..4bcb06703 --- /dev/null +++ b/ui-v2/app/components/topology-metrics/skin.scss @@ -0,0 +1,108 @@ +.topology-container { + color: $gray-700; +} + +// Columns/Containers & Lines +#downstream-container, +#metrics-container, +#upstream-container { + border: 1px solid $gray-200; + border-radius: $decor-radius-100; +} +#downstream-container, +#upstream-container { + background-color: $gray-100; +} +#downstream-container div:first-child { + display: inline-flex; + span::before { + @extend %with-info-circle-outline-mask, %as-pseudo; + background-color: $gray-500; + } +} +#upstream-container .card, +#downstream-container .card { + background-color: $white; + border-radius: $decor-radius-100; + border: 1px solid $gray-200; + div { + dd { + color: $gray-700; + } + .nspace dt::before { + @extend %with-folder-outline-mask, %as-pseudo; + } + .health dt::before { + @extend %with-help-circle-outline-mask, %as-pseudo; + } + .nspace dt::before { + @extend %with-folder-outline-mask, %as-pseudo; + } + .health dt::before { + @extend %with-help-circle-outline-mask, %as-pseudo; + } + .nspace dt::before, + .health dt::before { + background-color: $gray-500; + } + .passing::before { + @extend %with-check-circle-fill-color-mask, %as-pseudo; + background-color: $green-500; + } + .warning::before { + @extend %with-alert-triangle-color-mask, %as-pseudo; + background-color: $orange-500; + } + .critical::before { + @extend %with-cancel-square-fill-color-mask, %as-pseudo; + background-color: $red-500; + } + } +} + +// Metrics Container +#metrics-container { + div:first-child { + background-color: $white; + } + div:nth-child(2) { + background-color: $gray-100; + a { + color: $gray-700; + } + a::before { + background-color: $gray-500; + } + a:hover { + color: $color-action; + } + .metrics-link::before { + @extend %with-exit-mask, %as-pseudo; + } + .settings-link::before { + @extend %with-docs-mask, %as-pseudo; + } + } +} + +// SVG Line styling +#downstream-lines svg, +#upstream-lines svg { + path { + fill: $transparent; + } + circle { + fill: $white; + } + polygon { + fill: $gray-300; + stroke-linejoin: round; + } + path, + circle, + polygon { + stroke: $gray-300; + stroke-width: 2; + } + +} diff --git a/ui-v2/app/controllers/dc/services/show.js b/ui-v2/app/controllers/dc/services/show.js index 4941ddad0..0cc9c8431 100644 --- a/ui-v2/app/controllers/dc/services/show.js +++ b/ui-v2/app/controllers/dc/services/show.js @@ -18,13 +18,18 @@ export default Controller.extend({ action: 'update', }); } - [e.target, this.intentions, this.chain, this.proxies, this.gatewayServices].forEach( - function(item) { - if (item && typeof item.close === 'function') { - item.close(); - } + [ + e.target, + this.intentions, + this.chain, + this.proxies, + this.gatewayServices, + this.topology, + ].forEach(function(item) { + if (item && typeof item.close === 'function') { + item.close(); } - ); + }); } }, }, diff --git a/ui-v2/app/helpers/service/health-percentage.js b/ui-v2/app/helpers/service/health-percentage.js new file mode 100644 index 000000000..d8b9b0cf7 --- /dev/null +++ b/ui-v2/app/helpers/service/health-percentage.js @@ -0,0 +1,10 @@ +import { helper } from '@ember/component/helper'; + +export default helper(function serviceHealthPercentage([params] /*, hash*/) { + const total = params.ChecksCritical + params.ChecksPassing + params.ChecksWarning; + return { + passing: Math.round((params.ChecksPassing / total) * 100), + warning: Math.round((params.ChecksWarning / total) * 100), + critical: Math.round((params.ChecksCritical / total) * 100), + }; +}); diff --git a/ui-v2/app/models/topology.js b/ui-v2/app/models/topology.js new file mode 100644 index 000000000..2449a7044 --- /dev/null +++ b/ui-v2/app/models/topology.js @@ -0,0 +1,18 @@ +import Model from 'ember-data/model'; +import attr from 'ember-data/attr'; +import { computed } from '@ember/object'; + +export const PRIMARY_KEY = 'uid'; +export const SLUG_KEY = 'ServiceName'; +export default Model.extend({ + [PRIMARY_KEY]: attr('string'), + [SLUG_KEY]: attr('string'), + Datacenter: attr('string'), + Namespace: attr('string'), + Upstreams: attr(), + Downstreams: attr(), + meta: attr(), + Exists: computed(function() { + return true; + }), +}); diff --git a/ui-v2/app/router.js b/ui-v2/app/router.js index eb2617e62..c90e50fe1 100644 --- a/ui-v2/app/router.js +++ b/ui-v2/app/router.js @@ -25,6 +25,9 @@ export const routes = { _options: { path: '/create' }, }, }, + topology: { + _options: { path: '/topology' }, + }, services: { _options: { path: '/services' }, }, diff --git a/ui-v2/app/routes/dc/services/show.js b/ui-v2/app/routes/dc/services/show.js index 7e5571b1d..5005684bd 100644 --- a/ui-v2/app/routes/dc/services/show.js +++ b/ui-v2/app/routes/dc/services/show.js @@ -19,19 +19,33 @@ export default Route.extend({ urls: this.settings.findBySlug('urls'), chain: null, proxies: [], - }).then(model => { - return ['connect-proxy', 'mesh-gateway', 'ingress-gateway', 'terminating-gateway'].includes( - get(model, 'items.firstObject.Service.Kind') - ) - ? model - : hash({ - ...model, - chain: this.data.source(uri => uri`/${nspace}/${dc}/discovery-chain/${params.name}`), - proxies: this.data.source( - uri => uri`/${nspace}/${dc}/proxies/for-service/${params.name}` - ), - }); - }); + topology: null, + }) + .then(model => { + return ['connect-proxy', 'mesh-gateway', 'ingress-gateway', 'terminating-gateway'].includes( + get(model, 'items.firstObject.Service.Kind') + ) + ? model + : hash({ + ...model, + chain: this.data.source(uri => uri`/${nspace}/${dc}/discovery-chain/${params.name}`), + proxies: this.data.source( + uri => uri`/${nspace}/${dc}/proxies/for-service/${params.name}` + ), + }); + }) + .then(model => { + return ['mesh-gateway', 'terminating-gateway'].includes( + get(model, 'items.firstObject.Service.Kind') + ) + ? model + : hash({ + ...model, + topology: this.data.source( + uri => uri`/${nspace}/${dc}/topology/for-service/${params.name}` + ), + }); + }); }, setupController: function(controller, model) { this._super(...arguments); diff --git a/ui-v2/app/routes/dc/services/show/index.js b/ui-v2/app/routes/dc/services/show/index.js index ae75a29b7..5b9171d8e 100644 --- a/ui-v2/app/routes/dc/services/show/index.js +++ b/ui-v2/app/routes/dc/services/show/index.js @@ -1,6 +1,37 @@ -import Route from 'consul-ui/routing/route'; -import to from 'consul-ui/utils/routing/redirect-to'; +import Route from '@ember/routing/route'; +import { get } from '@ember/object'; export default Route.extend({ - redirect: to('instances'), + afterModel: function(model, transition) { + const parent = this.routeName + .split('.') + .slice(0, -1) + .join('.'); + // the default selected tab depends on whether you have any healthchecks or not + // so check the length here. + let to = 'topology'; + const parentModel = this.modelFor(parent); + + const kind = get(parentModel, 'items.firstObject.Service.Kind'); + + switch (kind) { + case 'ingress-gateway': + if (!get(parentModel, 'topology.Exists')) { + to = 'upstreams'; + } + break; + case 'terminating-gateway': + to = 'services'; + break; + case 'mesh-gateway': + to = 'instances'; + break; + default: + if (!get(parentModel, 'topology.Exists')) { + to = 'instances'; + } + } + + this.replaceWith(`${parent}.${to}`, parentModel); + }, }); diff --git a/ui-v2/app/routes/dc/services/show/topology.js b/ui-v2/app/routes/dc/services/show/topology.js new file mode 100644 index 000000000..4311f0d0c --- /dev/null +++ b/ui-v2/app/routes/dc/services/show/topology.js @@ -0,0 +1,15 @@ +import Route from '@ember/routing/route'; + +export default Route.extend({ + model: function() { + const parent = this.routeName + .split('.') + .slice(0, -1) + .join('.'); + return this.modelFor(parent); + }, + setupController: function(controller, model) { + this._super(...arguments); + controller.setProperties(model); + }, +}); diff --git a/ui-v2/app/serializers/topology.js b/ui-v2/app/serializers/topology.js new file mode 100644 index 000000000..b7b8cd9ef --- /dev/null +++ b/ui-v2/app/serializers/topology.js @@ -0,0 +1,17 @@ +import Serializer from './application'; +import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/topology'; + +export default Serializer.extend({ + primaryKey: PRIMARY_KEY, + slugKey: SLUG_KEY, + respondForQueryRecord: function(respond, query) { + return this._super(function(cb) { + return respond(function(headers, body) { + return cb(headers, { + ...body, + [SLUG_KEY]: query.id, + }); + }); + }, query); + }, +}); diff --git a/ui-v2/app/services/data-source/protocols/http.js b/ui-v2/app/services/data-source/protocols/http.js index 1c99bfcb1..0c7ac9721 100644 --- a/ui-v2/app/services/data-source/protocols/http.js +++ b/ui-v2/app/services/data-source/protocols/http.js @@ -13,6 +13,7 @@ export default Service.extend({ proxies: service('repository/proxy'), ['proxy-instance']: service('repository/proxy'), ['discovery-chain']: service('repository/discovery-chain'), + ['topology']: service('repository/topology'), coordinates: service('repository/coordinate'), sessions: service('repository/session'), namespaces: service('repository/nspace'), @@ -100,6 +101,14 @@ export default Service.extend({ break; } break; + case 'topology': + [method, slug] = rest; + switch (method) { + case 'for-service': + find = configuration => repo.findBySlug(slug, dc, nspace, configuration); + break; + } + break; case 'sessions': [method, ...slug] = rest; switch (method) { diff --git a/ui-v2/app/services/repository/topology.js b/ui-v2/app/services/repository/topology.js new file mode 100644 index 000000000..2532d5593 --- /dev/null +++ b/ui-v2/app/services/repository/topology.js @@ -0,0 +1,32 @@ +import RepositoryService from 'consul-ui/services/repository'; +import { inject as service } from '@ember/service'; +import { get, set } from '@ember/object'; + +const modelName = 'topology'; +const ERROR_MESH_DISABLED = 'Connect must be enabled in order to use this endpoint'; + +export default RepositoryService.extend({ + dcs: service('repository/dc'), + getModelName: function() { + return modelName; + }, + findBySlug: function(slug, dc, nspace, configuration = {}) { + const datacenter = this.dcs.peekOne(dc); + if (datacenter !== null && !get(datacenter, 'MeshEnabled')) { + return Promise.resolve(); + } + return this._super(...arguments).catch(e => { + const code = get(e, 'errors.firstObject.status'); + const body = get(e, 'errors.firstObject.detail').trim(); + switch (code) { + case '500': + if (datacenter !== null && body.endsWith(ERROR_MESH_DISABLED)) { + set(datacenter, 'MeshEnabled', false); + } + return; + default: + throw e; + } + }); + }, +}); diff --git a/ui-v2/app/styles/components.scss b/ui-v2/app/styles/components.scss index a49e66ee4..6e363f0fc 100644 --- a/ui-v2/app/styles/components.scss +++ b/ui-v2/app/styles/components.scss @@ -67,3 +67,4 @@ @import 'consul-ui/components/consul-intention-permission-form'; @import 'consul-ui/components/consul-intention-permission-header-list'; @import 'consul-ui/components/role-selector'; +@import 'consul-ui/components/topology-metrics'; diff --git a/ui-v2/app/templates/dc/services/show.hbs b/ui-v2/app/templates/dc/services/show.hbs index e2e5c50fe..8dea73a23 100644 --- a/ui-v2/app/templates/dc/services/show.hbs +++ b/ui-v2/app/templates/dc/services/show.hbs @@ -2,6 +2,8 @@ + + {{title item.Service.Service}} @@ -24,21 +26,24 @@ diff --git a/ui-v2/app/templates/dc/services/show/topology.hbs b/ui-v2/app/templates/dc/services/show/topology.hbs new file mode 100644 index 000000000..946108a43 --- /dev/null +++ b/ui-v2/app/templates/dc/services/show/topology.hbs @@ -0,0 +1,34 @@ +
+
+ {{#if topology}} + + {{else}} + + +

+ No dependencies +

+
+ +

+ This service has neither downstreams nor upstreams, which means that no services are configured to connect with it. Add upstreams and intentions to ensure this service is connected with the rest of your service mesh. +

+
+ + + +
+ {{/if}} +
+
diff --git a/ui-v2/package.json b/ui-v2/package.json index 1c105e9a6..5719f45b1 100644 --- a/ui-v2/package.json +++ b/ui-v2/package.json @@ -53,9 +53,10 @@ "@babel/plugin-proposal-class-properties": "^7.10.1", "@babel/plugin-proposal-object-rest-spread": "^7.5.5", "@ember/optional-features": "^1.3.0", + "@ember/render-modifiers": "^1.0.2", "@glimmer/component": "^1.0.0", "@glimmer/tracking": "^1.0.0", - "@hashicorp/consul-api-double": "^5.0.0", + "@hashicorp/consul-api-double": "^5.2.3", "@hashicorp/ember-cli-api-double": "^3.1.0", "@xstate/fsm": "^1.4.0", "babel-eslint": "^10.0.3", diff --git a/ui-v2/tests/acceptance/dc/list-blocking.feature b/ui-v2/tests/acceptance/dc/list-blocking.feature index b0d039883..2cbdfadc2 100644 --- a/ui-v2/tests/acceptance/dc/list-blocking.feature +++ b/ui-v2/tests/acceptance/dc/list-blocking.feature @@ -34,6 +34,7 @@ Feature: dc / list-blocking dc: dc-1 service: service --- + And I click instances on the tabs Then the url should be /dc-1/[Url] And pause until I see 3 [Model] models And an external edit results in 5 [Model] models diff --git a/ui-v2/tests/acceptance/dc/services/show-with-slashes.feature b/ui-v2/tests/acceptance/dc/services/show-with-slashes.feature index 26a5d536a..3cfde519e 100644 --- a/ui-v2/tests/acceptance/dc/services/show-with-slashes.feature +++ b/ui-v2/tests/acceptance/dc/services/show-with-slashes.feature @@ -17,5 +17,5 @@ Feature: dc / services / show-with-slashes: Show Service that has slashes in its Then the url should be /dc1/services Then I see 1 service model And I click service on the services - Then the url should be /dc1/services/hashicorp%2Fservice%2Fservice-0/instances + Then the url should be /dc1/services/hashicorp%2Fservice%2Fservice-0/topology diff --git a/ui-v2/tests/acceptance/dc/services/show.feature b/ui-v2/tests/acceptance/dc/services/show.feature index 2909e2c8f..47859dcf6 100644 --- a/ui-v2/tests/acceptance/dc/services/show.feature +++ b/ui-v2/tests/acceptance/dc/services/show.feature @@ -94,6 +94,7 @@ Feature: dc / services / show: Show Service dc: dc1 service: service-0 --- + And I click instances on the tabs Then I see address on the instances like yaml --- - "1.1.1.1:8080" diff --git a/ui-v2/tests/acceptance/dc/services/show/dc-switch.feature b/ui-v2/tests/acceptance/dc/services/show/dc-switch.feature index 15363040b..8d54746f4 100644 --- a/ui-v2/tests/acceptance/dc/services/show/dc-switch.feature +++ b/ui-v2/tests/acceptance/dc/services/show/dc-switch.feature @@ -19,9 +19,8 @@ Feature: dc / services / show / dc-switch : Switching Datacenters dc: dc-1 service: consul --- - Then the url should be /dc-1/services/consul/instances - And I see instancesUrl on the tabs contains "/dc-1/services/consul/instances" + + Then the url should be /dc-1/services/consul/topology When I click dc on the navigation And I click dcs.1.name on the navigation - Then the url should be /dc-2/services/consul/instances - And I see instancesUrl on the tabs contains "/dc-2/services/consul/instances" + Then the url should be /dc-2/services/consul/topology diff --git a/ui-v2/tests/acceptance/dc/services/show/intentions-error.feature b/ui-v2/tests/acceptance/dc/services/show/intentions-error.feature deleted file mode 100644 index fa020eba1..000000000 --- a/ui-v2/tests/acceptance/dc/services/show/intentions-error.feature +++ /dev/null @@ -1,20 +0,0 @@ -@setupApplicationTest -Feature: dc / services / intentions-error: An error with intentions doesn't 500 the page - Scenario: - Given 1 datacenter model with the value "dc1" - And 1 node model - And 1 service model from yaml - --- - - Service: - Kind: ~ - Name: service-0 - ID: service-0-with-id - --- - And the url "/v1/connect/intentions" responds with a 500 status - When I visit the service page for yaml - --- - dc: dc1 - service: service-0 - --- - And the title should be "service-0 - Consul" - And I see 1 instance model diff --git a/ui-v2/tests/acceptance/page-navigation.feature b/ui-v2/tests/acceptance/page-navigation.feature index aed251312..ed8be3a5d 100644 --- a/ui-v2/tests/acceptance/page-navigation.feature +++ b/ui-v2/tests/acceptance/page-navigation.feature @@ -41,7 +41,7 @@ Feature: page-navigation Where: ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | Item | Model | URL | Endpoint | Back | - | service | services | /dc-1/services/service-0/instances | /v1/discovery-chain/service-0?dc=dc-1&ns=@namespace | /dc-1/services | + | service | services | /dc-1/services/service-0/topology | /v1/discovery-chain/service-0?dc=dc-1&ns=@namespace | /dc-1/services | | kv | kvs | /dc-1/kv/0-key-value/edit | /v1/session/info/ee52203d-989f-4f7a-ab5a-2bef004164ca?dc=dc-1&ns=@namespace | /dc-1/kv | # | acl | acls | /dc-1/acls/anonymous | /v1/acl/info/anonymous?dc=dc-1 | /dc-1/acls | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/ui-v2/tests/integration/adapters/topology-test.js b/ui-v2/tests/integration/adapters/topology-test.js new file mode 100644 index 000000000..f53bf6561 --- /dev/null +++ b/ui-v2/tests/integration/adapters/topology-test.js @@ -0,0 +1,50 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; +import getNspaceRunner from 'consul-ui/tests/helpers/get-nspace-runner'; + +const nspaceRunner = getNspaceRunner('topology'); +module('Integration | Adapter | topology', function(hooks) { + setupTest(hooks); + const dc = 'dc-1'; + const id = 'slug'; + test('requestForQueryRecord returns the correct url/method', function(assert) { + const adapter = this.owner.lookup('adapter:topology'); + const client = this.owner.lookup('service:client/http'); + const expected = `GET /v1/internal/ui/service-topology/${id}?dc=${dc}`; + const actual = adapter.requestForQueryRecord(client.requestParams.bind(client), { + dc: dc, + id: id, + }); + assert.equal(`${actual.method} ${actual.url}`, expected); + }); + test("requestForQueryRecord throws if you don't specify an id", function(assert) { + const adapter = this.owner.lookup('adapter:topology'); + const client = this.owner.lookup('service:client/http'); + assert.throws(function() { + adapter.requestForQueryRecord(client.url, { + dc: dc, + }); + }); + }); + test('requestForQueryRecord returns the correct body', function(assert) { + return nspaceRunner( + (adapter, serializer, client) => { + return adapter.requestForQueryRecord(client.body, { + id: id, + dc: dc, + ns: 'team-1', + index: 1, + }); + }, + { + index: 1, + ns: 'team-1', + }, + { + index: 1, + }, + this, + assert + ); + }); +}); diff --git a/ui-v2/tests/integration/helpers/service/health-percentage-test.js b/ui-v2/tests/integration/helpers/service/health-percentage-test.js new file mode 100644 index 000000000..502ef2b2f --- /dev/null +++ b/ui-v2/tests/integration/helpers/service/health-percentage-test.js @@ -0,0 +1,17 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +module('Integration | Helper | service/health-percentage', function(hooks) { + setupRenderingTest(hooks); + + // Replace this with your real tests. + test('it renders', async function(assert) { + this.set('inputValue', {}); + + await render(hbs`{{service/health-percentage inputValue}}`); + + assert.equal(this.element.textContent.trim(), {}); + }); +}); diff --git a/ui-v2/tests/integration/serializers/topology-test.js b/ui-v2/tests/integration/serializers/topology-test.js new file mode 100644 index 000000000..aed5be4cb --- /dev/null +++ b/ui-v2/tests/integration/serializers/topology-test.js @@ -0,0 +1,37 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +import { get } from 'consul-ui/tests/helpers/api'; +import { HEADERS_SYMBOL as META } from 'consul-ui/utils/http/consul'; + +module('Integration | Serializer | topology', function(hooks) { + setupTest(hooks); + test('respondForQueryRecord returns the correct data for item endpoint', function(assert) { + const serializer = this.owner.lookup('serializer:topology'); + const dc = 'dc-1'; + const id = 'slug'; + const request = { + url: `/v1/internal/ui/service-topology/${id}?dc=${dc}`, + }; + return get(request.url).then(function(payload) { + const expected = { + Datacenter: dc, + [META]: {}, + uid: `["default","${dc}","${id}"]`, + }; + const actual = serializer.respondForQueryRecord( + function(cb) { + const headers = {}; + const body = payload; + return cb(headers, body); + }, + { + dc: dc, + id: id, + } + ); + assert.equal(actual.Datacenter, expected.Datacenter); + assert.equal(actual.uid, expected.uid); + }); + }); +}); diff --git a/ui-v2/tests/integration/services/repository/topology-test.js b/ui-v2/tests/integration/services/repository/topology-test.js new file mode 100644 index 000000000..78e73a51d --- /dev/null +++ b/ui-v2/tests/integration/services/repository/topology-test.js @@ -0,0 +1,42 @@ +import { moduleFor, test } from 'ember-qunit'; +import repo from 'consul-ui/tests/helpers/repo'; + +moduleFor('service:repository/topology', 'Integration | Repository | topology', { + // Specify the other units that are required for this test. + integration: true, +}); +const dc = 'dc-1'; +const id = 'slug'; +test('findBySlug returns the correct data for item endpoint', function(assert) { + return repo( + 'Service', + 'findBySlug', + this.subject(), + function retrieveStub(stub) { + return stub(`/v1/internal/ui/service-topology/${id}?dc=${dc}`, { + CONSUL_DISCOVERY_CHAIN_COUNT: 1, + }); + }, + function performTest(service) { + return service.findBySlug(id, dc); + }, + function performAssertion(actual, expected) { + const result = expected(function(payload) { + return Object.assign( + {}, + { + Datacenter: dc, + uid: `["default","${dc}","${id}"]`, + meta: { + cacheControl: undefined, + cursor: undefined, + }, + }, + payload + ); + }); + assert.equal(actual.Datacenter, result.Datacenter); + assert.equal(actual.uid, result.uid); + } + ); +}); diff --git a/ui-v2/yarn.lock b/ui-v2/yarn.lock index 0666398c2..68f381a8c 100644 --- a/ui-v2/yarn.lock +++ b/ui-v2/yarn.lock @@ -1527,10 +1527,10 @@ faker "^4.1.0" js-yaml "^3.13.1" -"@hashicorp/consul-api-double@^5.0.0": - version "5.2.1" - resolved "https://registry.yarnpkg.com/@hashicorp/consul-api-double/-/consul-api-double-5.2.1.tgz#a411139fa1afa0dfaf1b9973f21275530a39939b" - integrity sha512-ASQv2I8iprnFmpAvbHEoKE8MXTpOxdeBan6nkgobmz4OyvMcqu/h29CGEXZ9j63NX6+nxmE84nV5yAqADRubGQ== +"@hashicorp/consul-api-double@^5.2.3": + version "5.2.3" + resolved "https://registry.yarnpkg.com/@hashicorp/consul-api-double/-/consul-api-double-5.2.3.tgz#c34cec063b519595c49bb3fce799541f7d967f66" + integrity sha512-NlnBUHoXLlQwTB1lFzYvaIUZnf5KOGnohXRm4D3B8xVC+D0py6dTP5dj3NpBuxrG5b0xSv2zTF3tz9Y5nehOzQ== "@hashicorp/ember-cli-api-double@^3.1.0": version "3.1.2"