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 (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 @@
+
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"