ui: Redesign - Instance Detail Proxy Info tab (#7745)

* Fix clickFirstAnchor bug

* Create Proxy Info Tab for Instance Detail Page

* Create tests for ProxyInfo and update other scenarios with Proxy data

* ui: Refactors our app-view/%app-view component (#7752)

Co-authored-by: John Cowen <johncowen@users.noreply.github.com>
This commit is contained in:
Kenia 2020-05-07 09:57:15 -04:00 committed by John Cowen
parent abc43b6a0f
commit 4b2ff91b45
41 changed files with 625 additions and 327 deletions

View File

@ -45,19 +45,25 @@
</FlashMessage>
{{/each}}
<div>
<div class="actions">
{{#if authorized}}
<YieldSlot @name="actions">{{yield}}</YieldSlot>
{{/if}}
</div>
<div>
{{#if authorized}}
<nav aria-label="Breadcrumb">
<YieldSlot @name="breadcrumbs">{{yield}}</YieldSlot>
</nav>
{{/if}}
<YieldSlot @name="header">{{yield}}</YieldSlot>
<div class="title">
<YieldSlot @name="header">
{{yield}}
</YieldSlot>
<div class="actions">
{{#if authorized}}
<YieldSlot @name="actions">{{yield}}</YieldSlot>
{{/if}}
</div>
</div>
<YieldSlot @name="nav">
{{yield}}
</YieldSlot>
</div>
</div>
{{#if authorized}}

View File

@ -1,3 +1,5 @@
import Component from '@ember/component';
export default Component.extend({});
export default Component.extend({
tagName: '',
});

View File

@ -7,8 +7,13 @@
</BlockSlot>
<BlockSlot @name="content">
<dl>
{{#if (eq item.ServiceName "")}}
<dt>NodeName</dt>
<dd>{{item.Node}}</dd>
{{else}}
<dt>ServiceName</dt>
<dd>{{or item.ServiceName '-'}}</dd>
<dd>{{item.ServiceName}}</dd>
{{/if}}
</dl>
<dl>
<dt>CheckID</dt>

View File

@ -47,7 +47,7 @@ export default Component.extend(WithResizing, {
},
actions: {
click: function(e) {
return this.dom.clickFirstAnchor(e, 'li');
return this.dom.clickFirstAnchor(e, '.list-collection > ul > li');
},
},
});

View File

@ -31,6 +31,9 @@ export const routes = {
healthchecks: {
_options: { path: '/health-checks' },
},
proxy: {
_options: { path: '/proxy' },
},
upstreams: {
_options: { path: '/upstreams' },
},

View File

@ -16,12 +16,25 @@ export default Route.extend({
// its highly unlikely that a service will suddenly change to being a
// connect-proxy or vice versa so leave as is for now
return hash({
proxy:
proxyMeta:
// proxies and mesh-gateways can't have proxies themselves so don't even look
['connect-proxy', 'mesh-gateway'].includes(get(model.item, 'Kind'))
? null
: this.proxyRepo.findInstanceBySlug(params.id, params.node, params.name, dc, nspace),
...model,
}).then(model => {
if (typeof get(model, 'proxyMeta.ServiceID') === 'undefined') {
return model;
}
const proxyName = get(model, 'proxyMeta.ServiceName');
const proxyID = get(model, 'proxyMeta.ServiceID');
const proxyNode = get(model, 'proxyMeta.Node');
return hash({
// Proxies have identical dc/nspace as their parent instance
// No need to use Proxy's dc/nspace response
proxy: this.repo.findInstanceBySlug(proxyID, proxyNode, proxyName, dc, nspace),
...model,
});
});
});
},

View File

@ -0,0 +1,14 @@
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) {
controller.setProperties(model);
},
});

View File

@ -14,6 +14,7 @@
@import 'routes/dc/settings/index';
@import 'routes/dc/nodes/index';
@import 'routes/dc/services/index';
@import 'routes/dc/intention/index';
@import 'routes/dc/kv/index';
@import 'routes/dc/acls/index';

View File

@ -29,7 +29,6 @@
%table td.no-actions ~ .actions {
display: none;
}
%table td:not(.actions),
%table td:not(.actions) > *:only-child {
overflow-x: hidden;
}
@ -41,6 +40,7 @@
}
%table td {
height: 50px;
vertical-align: middle;
}
%table caption {
margin-bottom: 0.8em;

View File

@ -22,9 +22,12 @@
display: inline-block;
padding: 16px 13px;
}
%tab-section section h3 {
margin: 24px 0;
%tab-section section {
padding-bottom: 24px;
}
%tab-section section:not(:last-child) {
border-bottom: 1px solid $gray-200;
}
%tab-section section > h3 {
margin: 24px 0;
}

View File

@ -4,66 +4,19 @@
@import '../base/components/popover-menu/index';
%app-view-header .actions > [type='checkbox'] {
@extend %more-popover-menu;
}
%more-popover-menu-panel [type='checkbox']:checked ~ * {
/* this needs to autocalculate */
min-height: 143px;
max-height: 143px;
}
%more-popover-menu-panel [id$='logout']:checked ~ * {
/* this needs to autocalculate */
min-height: 163px;
max-height: 163px;
}
%more-popover-menu-panel [id$='delete']:checked ~ ul label[for$='delete'] + [role='menu'],
%more-popover-menu-panel [id$='logout']:checked ~ ul label[for$='logout'] + [role='menu'],
%more-popover-menu-panel [id$='use']:checked ~ ul label[for$='use'] + [role='menu'] {
display: block;
}
%app-view-header .actions label + div {
// We need this extra to allow tooltips to show
%app-view-actions label + div {
/* We need this extra to allow tooltips to show */
overflow: visible !important;
}
main {
@extend %app-view;
}
%app-view > div > header {
@extend %app-view-header;
}
%app-view > div > div {
@extend %app-view-content;
}
%app-view header form {
%app-view-header form {
@extend %filter-bar;
}
@media #{$--lt-spacious-page-header} {
%app-view-header .actions {
margin-top: 9px;
}
}
// TODO: This should be its own component
%app-view h1 {
padding-bottom: 0.2em;
}
%app-view h1 span[data-tooltip] {
@extend %with-external-source-icon;
margin-top: 13px;
}
%app-view h1 span.kind-proxy {
@extend %frame-gray-900;
@extend %pill;
}
%app-view h1 span.kind-proxy::before {
width: 0.3em !important;
}
%app-view h1 em {
color: $gray-600;
}
%app-view-header .actions a,
%app-view-header .actions button {
%app-view-actions a,
%app-view-actions button {
@extend %button-compact;
}
%app-view-content div > dl {
@ -97,12 +50,14 @@ main {
display: none;
}
}
@media #{$--lt-spacious-page-header} {
%app-view-actions {
margin-top: 9px;
}
}
// reduced search magnifying icon layout
@media #{$--lt-horizontal-selects} {
%app-view header h1 {
display: inline-block;
}
%app-view header h1 {
%app-view-header h1 {
display: inline-block;
}
// on the instance detail page we don't have the magnifier

View File

@ -1,2 +1,14 @@
@import './skin';
@import './layout';
%app-view > div > header {
@extend %app-view-header;
}
%app-view-header .title {
@extend %app-view-title;
}
%app-view-header .actions {
@extend %app-view-actions;
}
%app-view > div > div {
@extend %app-view-content;
}

View File

@ -1,67 +1,61 @@
/* layout */
%app-view-header > div:last-of-type > div:first-child {
flex-grow: 1;
}
%app-view {
position: relative;
}
%app-view-header .actions {
float: right;
display: flex;
align-items: flex-start;
margin-top: 9px;
}
%app-view-header dl {
float: left;
margin-top: 25px;
margin-right: 50px;
margin-bottom: 20px;
}
%app-view-header dt {
font-weight: bold;
}
%app-view-header dd > a {
color: $black;
}
%app-view-header .title-bar {
%app-view-title {
display: flex;
align-items: center;
}
%app-view-header .title-bar > h1 {
border: 0;
%app-view-actions {
margin-left: auto;
display: flex;
align-items: flex-start;
}
%app-view-header .title-bar > span {
margin-left: 8px;
%app-view-header dl {
float: left;
}
/* units */
%app-view {
margin-top: 50px;
}
/* give anything after the header a bit of room */
%app-view-header + div > *:first-child {
margin-top: 1.8em;
}
%app-view h2 {
%app-view-title {
padding-bottom: 0.2em;
margin-bottom: 0.5em;
}
%app-view-header .actions > *:not(:last-child) {
%app-view-title > :not(:last-child) {
margin-right: 8px;
}
%app-view-actions {
margin-top: 9px;
}
%app-view-actions > *:not(:last-child) {
margin-right: 12px;
}
// content
%app-view-content div > dl > dt {
position: absolute;
%app-view-header dl {
margin-top: 19px;
margin-bottom: 23px;
margin-right: 50px;
}
%app-view-content div > dl {
position: relative;
/* content */
%app-view-content h2 {
padding-bottom: 0.2em;
margin-bottom: 0.5em;
}
%app-view-content-empty {
margin-top: 0;
padding: 50px;
text-align: center;
}
%app-view-content form:not(:last-child) {
margin-bottom: 2.2em;
/* this should probably be its own component */
%app-view-content div > dl {
position: relative;
}
%app-view-content div > dl > dt {
position: absolute;
}
%app-view-content div > dl > dt {
width: 140px;
@ -73,7 +67,8 @@
min-height: 1em;
margin-bottom: 0.4em;
}
// TODO: Think about an %app-form or similar
/* */
/* TODO: Think about an %app-form or similar */
%app-view-content fieldset:not(.freetext-filter) {
padding-bottom: 0.3em;
margin-bottom: 2em;

View File

@ -1,52 +1,33 @@
/*TODO: Rename this to %app-view-brand-icon or similar */
%with-external-source-icon {
background-repeat: no-repeat;
background-size: contain;
width: 18px;
height: 18px;
--kubernetes-icon: #{$kubernetes-logo-color-svg};
--terraform-icon: #{$terraform-logo-color-svg};
--nomad-icon: #{$nomad-logo-color-svg};
--consul-icon: #{$consul-logo-color-svg};
--aws-icon: #{$aws-logo-color-svg};
%app-view-content-empty {
@extend %frame-gray-500;
}
%app-view h2,
%app-view fieldset {
%app-view-title {
border-bottom: $decor-border-200;
}
%app-view fieldset h2 {
%app-view-content h2,
%app-view-content fieldset {
border-bottom: $decor-border-200;
}
%app-view-content fieldset h2 {
border-bottom: none;
}
@media #{$--horizontal-selects} {
%app-view header h1 {
border-bottom: $decor-border-200;
}
%app-view-header h1 > em {
color: $gray-600;
}
@media #{$--lt-horizontal-selects} {
%app-view header > div > div:last-child {
border-bottom: $decor-border-200;
}
}
%app-view header > div > div:last-child,
%app-view header h1,
%app-view h2,
%app-view fieldset {
border-color: $gray-200;
}
// We know that any sibling navs might have a top border
// by default. As its squashed up to a h1, in this
// case hide its border to avoid double border
@media #{$--horizontal-selects} {
%app-view header h1 ~ nav {
border-top: 0 !important;
}
%app-view-header dd > a {
color: $black;
}
%app-view-content div > dl > dd {
color: $gray-400;
}
[role='tabpanel'] > p:only-child,
.template-error > div,
%app-view-content > p:only-child {
@extend %frame-gray-500;
%app-view-title,
%app-view-content h2,
%app-view-content fieldset {
border-color: $gray-200;
}
// We know that any sibling navs might have a top border
// by default. As its squashed up to a %app-view-title, in this
// case hide its border to avoid double border
%app-view-title ~ nav {
border-top: 0 !important;
}

View File

@ -1,11 +1,12 @@
@import './layout';
@import './skin';
%composite-row:hover,
%composite-row:focus,
%composite-row:active {
%with-composite-row-intent:hover,
%with-composite-row-intent:focus,
%with-composite-row-intent:active {
@extend %composite-row-intent;
}
%composite-row > a {
%composite-row > a,
%composite-row > p {
@extend %composite-row-header;
}
%composite-row > ul {

View File

@ -1,11 +1,13 @@
%composite-row {
cursor: pointer;
display: block;
box-sizing: border-box;
padding: 12px;
padding-right: 0;
border: 1px solid;
}
%composite-row-header {
margin-bottom: 0 !important;
}
%composite-row-intent {
border: 1px solid;
position: relative;
@ -23,3 +25,11 @@
%composite-row-detail .node::before {
margin-top: 2px;
}
// In this case we do not need a background on the icon
%composite-row-detail .port button {
padding: 0 !important;
margin-top: 1px !important;
}
%composite-row-detail .port button:hover {
background-color: transparent !important;
}

View File

@ -8,6 +8,7 @@
%composite-row-intent {
border-color: $gray-200;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
cursor: pointer;
}
%composite-row-header {
color: $black;
@ -53,3 +54,11 @@
@extend %with-swap-horizontal-mask, %as-pseudo;
background-color: $gray-500;
}
%composite-row .datacenter::before {
@extend %with-user-organization-mask, %as-pseudo;
background-color: $gray-500;
}
%composite-row .nspace::before {
@extend %with-folder-outline-mask, %as-pseudo;
background-color: $gray-500;
}

View File

@ -5,5 +5,5 @@
@extend %consul-service-instance-row;
}
%consul-service-instance-row {
@extend %composite-row;
@extend %composite-row, %with-composite-row-intent;
}

View File

@ -5,7 +5,7 @@
@extend %consul-service-row;
}
%consul-service-row {
@extend %composite-row;
@extend %composite-row, %with-composite-row-intent;
}
%consul-service-row > ul {
margin-left: 26px;

View File

@ -1 +1,5 @@
@import './layout';
%dom-recycling-table {
@extend %table-flex;
}

View File

@ -1,8 +1,11 @@
%filter-bar {
padding: 4px;
display: block;
margin-bottom: 8px;
margin-top: 0 !important;
margin-bottom: 8px !important;
}
%filter-bar + :not(.notice) {
margin-top: 1.8em;
}
@media #{$--horizontal-filters} {
%filter-bar {

View File

@ -1,9 +1,9 @@
%healthcheck-output {
display: flex;
padding: 20px 16px;
padding-right: 24px;
}
%healthcheck-output:not(:last-child) {
margin-bottom: 24px;
}
%healthcheck-output::before {

View File

@ -3,8 +3,8 @@
}
%healthcheck-output::before {
@extend %as-pseudo;
min-width: 26px;
min-height: 26px;
min-width: 20px;
min-height: 20px;
margin-right: 15px;
}
@media #{$--lt-spacious-healthcheck-output} {

View File

@ -1,12 +1,26 @@
@import '../base/components/table/index';
@import '../base/components/popover-menu/index';
table {
@extend %table, %table-flex;
%main-content table {
@extend %table;
}
%table-actions > [type='checkbox'] {
@extend %more-popover-menu;
}
%more-popover-menu-panel [type='checkbox']:checked ~ * {
/* this needs to autocalculate */
min-height: 143px;
max-height: 143px;
}
%more-popover-menu-panel [id$='logout']:checked ~ * {
/* this needs to autocalculate */
min-height: 183px;
max-height: 183px;
}
%more-popover-menu-panel [id$='delete']:checked ~ ul label[for$='delete'] + [role='menu'],
%more-popover-menu-panel [id$='logout']:checked ~ ul label[for$='logout'] + [role='menu'],
%more-popover-menu-panel [id$='use']:checked ~ ul label[for$='use'] + [role='menu'] {
display: block;
}
%table-actions .confirmation-alert {
@extend %confirmation-alert;
}
@ -16,18 +30,27 @@ table {
right: 15px;
}
html.template-service.template-list td:first-child a span,
html.template-node.template-show #services td:first-child a span,
html.template-service.template-show #instances td:first-child a span {
/*TODO: Rename this to %app-view-brand-icon or similar */
%with-external-source-icon {
background-repeat: no-repeat;
background-size: contain;
width: 18px;
height: 18px;
--kubernetes-icon: #{$kubernetes-logo-color-svg};
--terraform-icon: #{$terraform-logo-color-svg};
--nomad-icon: #{$nomad-logo-color-svg};
--consul-icon: #{$consul-logo-color-svg};
--aws-icon: #{$aws-logo-color-svg};
}
html.template-node.template-show #services td:first-child a span {
@extend %with-external-source-icon;
float: left;
margin-right: 10px;
margin-top: 2px;
}
/* This nudges the th in for the external source icons */
html.template-node.template-show #services th:first-child,
html.template-service.template-show #instances th:first-child,
html.template-service.template-list main th:first-child {
html.template-node.template-show #services th:first-child {
text-indent: 28px;
}
@ -73,18 +96,12 @@ th span em {
html.template-policy.template-list tr > :nth-child(2) {
display: none;
}
html.template-service.template-list tr > :nth-child(2) {
display: none;
}
}
@media #{$--lt-wide-table} {
/* hide actions on narrow screens, you can always click in do everything from there */
tr > .actions {
display: none;
}
html.template-service.template-list tr > :last-child {
display: none;
}
html.template-node.template-show #services tr > :last-child {
display: none;
}

View File

@ -41,7 +41,8 @@ pre code,
%notice p,
%flash-message p,
%filter-bar input,
%phrase-editor input {
%phrase-editor input,
%tab-section section p {
@extend %p1;
}
%menu-panel dl,
@ -79,6 +80,7 @@ pre code,
%button {
font-weight: $typo-weight-semibold;
}
%app-view-header dt,
%menu-panel dt,
%route-card section dt,
%route-card header:not(.short) dd,

View File

@ -0,0 +1,17 @@
.proxy-upstreams > ul {
@extend %proxy-upstreams-list;
}
%proxy-upstreams-list > li {
@extend %composite-row;
}
.proxy-exposed-paths tbody tr {
@extend %proxy-exposed-paths-row;
cursor: default !important;
}
%proxy-exposed-paths-row:hover {
box-shadow: none !important;
}
%proxy-exposed-paths-row .combined-address button:hover {
// In this case we do not need a background on the icon
background-color: transparent !important;
}

View File

@ -16,9 +16,11 @@
<h1>
Access Controls
</h1>
{{#if isAuthorized }}
{{partial 'dc/acls/nav'}}
{{/if}}
</BlockSlot>
<BlockSlot @name="nav">
{{#if isAuthorized }}
{{partial 'dc/acls/nav'}}
{{/if}}
</BlockSlot>
<BlockSlot @name="disabled">
{{partial 'dc/acls/disabled'}}

View File

@ -16,9 +16,11 @@
<h1>
Access Controls
</h1>
{{#if isAuthorized }}
{{partial 'dc/acls/nav'}}
{{/if}}
</BlockSlot>
<BlockSlot @name="nav">
{{#if isAuthorized }}
{{partial 'dc/acls/nav'}}
{{/if}}
</BlockSlot>
<BlockSlot @name="disabled">
{{partial 'dc/acls/disabled'}}

View File

@ -16,6 +16,8 @@
<h1>
Access Controls
</h1>
</BlockSlot>
<BlockSlot @name="nav">
{{#if isAuthorized }}
{{partial 'dc/acls/nav'}}
{{/if}}

View File

@ -14,6 +14,8 @@
{{ item.Node }}
</h1>
<label for="toolbar-toggle"></label>
</BlockSlot>
<BlockSlot @name="nav">
<TabNav @items={{
compact
(array

View File

@ -10,12 +10,12 @@
</ol>
</BlockSlot>
<BlockSlot @name="header">
<div class="title-bar">
<h1>
{{ item.ID }}
</h1>
<ConsulExternalSource @item={{item}} />
</div>
<h1>
{{ item.ID }}
</h1>
<ConsulExternalSource @item={{item}} />
</BlockSlot>
<BlockSlot @name="nav">
<dl>
<dt>Service Name</dt>
<dd><a href="{{href-to 'dc.services.show' item.Service}}">{{item.Service}}</a></dd>
@ -24,44 +24,19 @@
<dt>Node Name</dt>
<dd><a href="{{href-to 'dc.nodes.show' item.Node.Node}}">{{item.Node.Node}}</a></dd>
</dl>
{{#if proxy.ServiceName}}
<dl>
</dl>
{{/if}}
{{#if (eq item.Kind 'connect-proxy')}}
{{#if item.Proxy.DestinationServiceID}}
<dl>
<dt data-test-proxy-destination="instance">Dest. Service Instance</dt>
<dd><a href="{{href-to 'dc.services.instance' item.Proxy.DestinationServiceName item.Node.Node item.Proxy.DestinationServiceID}}">{{item.Proxy.DestinationServiceID}}</a></dd>
</dl>
<dl>
<dt>Local Service Address</dt>
<dd>{{item.Proxy.LocalServiceAddress}}:{{item.Proxy.LocalServicePort}}</dd>
</dl>
{{else}}
<dl>
<dt data-test-proxy-destination="service">Dest. Service</dt>
<dd><a href="{{href-to 'dc.services.show' item.Proxy.DestinationServiceName}}">{{item.Proxy.DestinationServiceName}}</a></dd>
</dl>
{{/if}}
{{/if}}
</BlockSlot>
<BlockSlot @name="content">
<TabNav @items={{
compact
(array
(hash label="Health Checks" href=(href-to "dc.services.instance.healthchecks") selected=(is-href "dc.services.instance.healthchecks"))
(if
(eq item.Kind 'connect-proxy')
(hash label="Upstreams" href=(href-to "dc.services.instance.upstreams") selected=(is-href "dc.services.instance.upstreams")) ""
)
(if
(and (eq item.Kind 'connect-proxy') (gt item.Proxy.Expose.Paths.length 0))
(hash label="Exposed Paths" href=(href-to "dc.services.instance.exposedpaths") selected=(is-href "dc.services.instance.exposedpaths")) ""
)
(if
(eq item.Kind 'mesh-gateway')
(hash label="Addresses" href=(href-to "dc.services.instance.addresses") selected=(is-href "dc.services.instance.addresses")) ""
)
(if proxy
(hash label="Proxy Info" href=(href-to "dc.services.instance.proxy") selected=(is-href "dc.services.instance.proxy"))
)
(hash label="Tags" href=(href-to "dc.services.instance.tags") selected=(is-href "dc.services.instance.tags"))
(hash label="Metadata" href=(href-to "dc.services.instance.metadata") selected=(is-href "dc.services.instance.metadata"))

View File

@ -1,37 +0,0 @@
<div id="exposed-paths" class="tab-section">
<div role="tabpanel">
<p>
You can expose individual HTTP paths like /metrics through Envoy for external services like Prometheus.
</p>
<TabularCollection
data-test-exposedpaths
class="exposedpaths"
@items={{item.Proxy.Expose.Paths}} as |path index|
>
<BlockSlot @name="header">
<th>Path</th>
<th>Protocol</th>
<th>Listener port</th>
<th>Local path port</th>
<th>Combined address<span><em role="tooltip">Service address, listener port, and path all combined into one URL.</em></span></th>
</BlockSlot>
<BlockSlot @name="row">
<td>
<span>{{path.Path}}</span>
</td>
<td>
{{path.Protocol}}
</td>
<td>
{{path.ListenerPort}}
</td>
<td>
{{path.LocalPathPort}}
</td>
<td>
<span data-test-combined-address>{{item.Address}}:{{path.ListenerPort}}{{path.Path}}</span>
</td>
</BlockSlot>
</TabularCollection>
</div>
</div>

View File

@ -0,0 +1,96 @@
<div class="tab-section">
<div role="tabpanel">
{{#if (or (gt proxy.ServiceChecks.length 0) (gt proxy.NodeChecks.length 0))}}
<section>
<h3>Proxy health checks</h3>
<HealthcheckList data-test-proxy-checks @items={{append proxy.ServiceChecks proxy.NodeChecks}} />
</section>
{{/if}}
{{#if (gt proxy.Proxy.Upstreams.length 0)}}
<section class="proxy-upstreams">
<h3>Upstreams</h3>
<ul data-test-proxy-upstreams>
{{#let proxy.Datacenter as |proxyDatacenter|}}
{{#each proxy.Proxy.Upstreams as |item|}}
<li>
<p data-test-destination-name>
{{item.DestinationName}}
</p>
<ul>
{{#if (env 'CONSUL_NSPACES_ENABLED')}}
{{#if (not-eq item.DestinationType 'prepared_query')}}
<li class="nspace">
{{or item.DestinationNamespace 'default'}}
</li>
{{/if}}
{{/if}}
{{#if (and (not-eq item.Datacenter proxyDatacenter) (not-eq item.Datacenter ""))}}
<li class="datacenter">
{{item.Datacenter}}
</li>
{{/if}}
{{#if (gt item.LocalBindPort 0)}}
{{#let (concat (or item.LocalBindAddress '127.0.0.1') ':' item.LocalBindPort) as |combinedAddress| }}
<li class="port">
<CopyButtonFeedback
@copy={{combinedAddress}}
@name="Address"
/>
<span>{{combinedAddress}}</span>
</li>
{{/let}}
{{/if}}
</ul>
</li>
{{/each}}
{{/let}}
</ul>
</section>
{{/if}}
{{#if (gt proxy.Proxy.Upstreams.length 0)}}
<section class="proxy-exposed-paths">
<h3>Exposed paths</h3>
<p>
The following list shows individual HTTP paths exposed through Envoy for external services like Prometheus. Read more about this in our documentation.
</p>
<table data-test-proxy-exposed-paths>
<thead>
<th>Path</th>
<th>Protocol</th>
<th>Listener port</th>
<th>Local path port</th>
<th>Combined address<span><em role="tooltip">Service address, listener port, and path all combined into one URL.</em></span></th>
</thead>
<tbody>
{{#each proxy.Proxy.Expose.Paths as |path|}}
<tr>
<td>
<span>{{or path.Path '-'}}</span>
</td>
<td>
{{or path.Protocol '-'}}
</td>
<td>
{{or path.ListenerPort '-'}}
</td>
<td>
{{or path.LocalPathPort '-'}}
</td>
{{#let (concat item.Address ':' path.ListenerPort path.Path) as |combinedAddress| }}
<td class="combined-address">
{{#if combinedAddress}}
<span data-test-combined-address>{{combinedAddress}}</span>
<CopyButtonFeedback @copy={{combinedAddress}} @name="Combined Address" />
{{else}}
{{'-'}}
{{/if}}
</td>
{{/let}}
</tr>
{{/each}}
</tbody>
</table>
</section>
{{/if}}
</div>
</div>

View File

@ -1,43 +0,0 @@
<div id="upstreams" class="tab-section">
<div role="tabpanel">
{{#if (gt item.Proxy.Upstreams.length 0) }}
<TabularCollection
data-test-upstreams
@items={{item.Proxy.Upstreams}} as |item index|
>
<BlockSlot @name="header">
<th>Upstream</th>
<th>Datacenter</th>
<th>Type</th>
<th>Local Bind Address</th>
</BlockSlot>
<BlockSlot @name="row">
<td>
<a data-test-destination-name>
{{item.DestinationName}}
{{#if (env 'CONSUL_NSPACES_ENABLED')}}
{{#if (not-eq item.DestinationType 'prepared_query')}}
{{! TODO: slugify }}
<em class={{concat 'nspace-' (or item.DestinationNamespace 'default')}}>{{or item.DestinationNamespace 'default'}}</em>
{{/if}}
{{/if}}
</a>
</td>
<td data-test-destination-datacenter>
{{item.Datacenter}}
</td>
<td data-test-destination-type>
{{item.DestinationType}}
</td>
<td data-test-local-bind-address>
{{item.LocalBindAddress}}:{{item.LocalBindPort}}
</td>
</BlockSlot>
</TabularCollection>
{{else}}
<p>
There are no upstreams.
</p>
{{/if}}
</div>
</div>

View File

@ -10,19 +10,19 @@
</ol>
</BlockSlot>
<BlockSlot @name="header">
<div class="title-bar">
<h1>
{{item.Service.Service}}
</h1>
<ConsulExternalSource @item={{item.Service}} />
</div>
</BlockSlot>
<BlockSlot @name="nav">
{{#if (not item.Service.Kind)}}
<TabNav @items={{
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")) '')
(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"))
)
}}/>
@ -36,4 +36,4 @@
<BlockSlot @name="content">
{{outlet}}
</BlockSlot>
</AppView>
</AppView>

View File

@ -2,6 +2,12 @@
Feature: dc / services / instances / gateway: Show Gateway Service Instance
Scenario: A Gateway Service instance
Given 1 datacenter model with the value "dc1"
Given 1 proxy model from yaml
---
- ServiceProxy:
DestinationServiceName: service-1
DestinationServiceID: ~
---
And 1 instance model from yaml
---
- Service:

View File

@ -0,0 +1,215 @@
@setupApplicationTest
Feature: dc / services / instances / show: Proxy Info tab
Background:
Given 1 datacenter model with the value "dc1"
Scenario: A Service instance without a Proxy does not display Proxy Info tab
Given 1 proxy model from yaml
---
- ServiceProxy:
DestinationServiceName: service-1
DestinationServiceID: ~
---
When I visit the instance page for yaml
---
dc: dc1
service: service-0
node: node-0
id: service-0-with-id
---
Then the url should be /dc1/services/service-0/instances/node-0/service-0-with-id/health-checks
And I don't see proxyInfo on the tabs
Scenario: A Service instance with a Proxy displays Proxy Info tab
When I visit the instance page for yaml
---
dc: dc1
service: service-0
node: node-0
id: service-0-with-id
---
Then the url should be /dc1/services/service-0/instances/node-0/service-0-with-id/health-checks
And I see proxyInfo on the tabs
When I click proxyInfo on the tabs
Then the url should be /dc1/services/service-0/instances/node-0/service-0-with-id/proxy
And I see proxyInfoIsSelected on the tabs
@notNamespaceable
Scenario: A Proxy with health checks, upstreams, and exposed paths displays all info
Given 2 instance models from yaml
---
- Service:
ID: service-0-with-id
Kind: consul
Node:
Node: node-0
- Service:
ID: service-0-with-id-proxy
Kind: connect-proxy
Proxy:
DestinationServiceName: service-0
Expose:
Checks: false
Paths:
- Path: /grpc-metrics
Protocol: grpc
LocalPathPort: 8081
ListenerPort: 8080
- Path: /http-metrics
Protocol: http
LocalPathPort: 8082
ListenerPort: 8083
- Path: /http-metrics-2
Protocol: http
LocalPathPort: 8083
ListenerPort: 8084
Upstreams:
- DestinationType: service
DestinationName: service-2
DestinationNamespace: default
LocalBindAddress: 127.0.0.1
LocalBindPort: 1111
- DestinationType: prepared_query
DestinationName: service-3
LocalBindAddress: 127.0.0.1
LocalBindPort: 1112
Node:
Node: node-0
Checks:
- Name: Service check
ServiceID: service-0-proxy
Output: Output of check
Status: passing
- Name: Service check
ServiceID: service-0-proxy
Output: Output of check
Status: warning
- Name: Service check
Type: http
ServiceID: service-0-proxy
Output: Output of check
Status: critical
- Name: Node check
ServiceID: ""
Output: Output of check
Status: passing
- Name: Node check
ServiceID: ""
Output: Output of check
Status: warning
- Name: Node check
ServiceID: ""
Output: Output of check
Status: critical
---
When I visit the instance page for yaml
---
dc: dc1
service: service-0
node: node-0
id: service-0-with-id
---
Then the url should be /dc1/services/service-0/instances/node-0/service-0-with-id/health-checks
And I see proxyInfo on the tabs
When I click proxyInfo on the tabs
Then the url should be /dc1/services/service-0/instances/node-0/service-0-with-id/proxy
And I see 6 of the proxyChecks object
And I see 2 of the upstreams object
And I see name on the upstreams like yaml
---
- service-2
- service-3
---
Scenario: A Proxy without health checks does not display Proxy Health section
And 2 instance models from yaml
---
- Service:
ID: service-0-with-id
Kind: consul
Node:
Node: node-0
- Service:
ID: service-0-with-id-proxy
Kind: connect-proxy
Node:
Node: node-0
Checks: []
---
When I visit the instance page for yaml
---
dc: dc1
service: service-0
node: node-0
id: service-0-with-id
---
Then the url should be /dc1/services/service-0/instances/node-0/service-0-with-id/health-checks
And I see proxyInfo on the tabs
When I click proxyInfo on the tabs
Then the url should be /dc1/services/service-0/instances/node-0/service-0-with-id/proxy
And I see 0 of the proxyChecks object
Scenario: A Proxy without upstreams does not display Upstreams section
And 2 instance models from yaml
---
- Service:
ID: service-0-with-id
Kind: consul
Node:
Node: node-0
- Service:
ID: service-0-with-id-proxy
Kind: connect-proxy
Proxy:
Upstreams: []
Node:
Node: node-0
---
When I visit the instance page for yaml
---
dc: dc1
service: service-0
node: node-0
id: service-0-with-id
---
Then the url should be /dc1/services/service-0/instances/node-0/service-0-with-id/health-checks
And I see proxyInfo on the tabs
When I click proxyInfo on the tabs
Then the url should be /dc1/services/service-0/instances/node-0/service-0-with-id/proxy
And I see 0 of the upstreams object
Scenario: A Proxy without exposed path does not display Exposed Paths section
And 2 instance models from yaml
---
- Service:
ID: service-0-with-id
Kind: consul
Node:
Node: node-0
- Service:
ID: service-0-with-id-proxy
Kind: connect-proxy
Proxy:
Expose:
Checks: false
Paths: []
Node:
Node: node-0
---
When I visit the instance page for yaml
---
dc: dc1
service: service-0
node: node-0
id: service-0-with-id
---
Then the url should be /dc1/services/service-0/instances/node-0/service-0-with-id/health-checks
And I see proxyInfo on the tabs
When I click proxyInfo on the tabs
Then the url should be /dc1/services/service-0/instances/node-0/service-0-with-id/proxy
And I see 0 of the exposedPaths object

View File

@ -11,7 +11,7 @@ Feature: dc / services / instances / show: Show Service Instance
Node:
Node: node-0
- Service:
ID: service-0-with-id
ID: service-1-with-id
Tags: ['Tag1', 'Tag2']
Meta:
consul-dashboard-url: http://url.com
@ -47,14 +47,20 @@ Feature: dc / services / instances / show: Show Service Instance
Status: critical
---
Scenario: A Service instance has no Proxy
Given 1 proxy model from yaml
---
- ServiceProxy:
DestinationServiceName: service-1
DestinationServiceID: ~
---
When I visit the instance page for yaml
---
dc: dc1
service: service-0
node: another-node
id: service-0-with-id
id: service-1-with-id
---
Then the url should be /dc1/services/service-0/instances/another-node/service-0-with-id/health-checks
Then the url should be /dc1/services/service-0/instances/another-node/service-1-with-id/health-checks
Then I see externalSource like "nomad"
And I don't see upstreams on the tabs
@ -71,9 +77,15 @@ Feature: dc / services / instances / show: Show Service Instance
When I click metadata on the tabs
And I see metadataIsSelected on the tabs
And I see 3 of the metadata object
And the title should be "service-0-with-id - Consul"
And the title should be "service-1-with-id - Consul"
Scenario: A Service instance warns when deregistered whilst blocking
Given 1 proxy model from yaml
---
- ServiceProxy:
DestinationServiceName: service-1
DestinationServiceID: ~
---
Given settings from yaml
---
consul:client:
@ -91,3 +103,19 @@ Feature: dc / services / instances / show: Show Service Instance
Then the url should be /dc1/services/service-0/instances/node-0/service-0-with-id/health-checks
And an external edit results in 0 instance models
And pause until I see the text "deregistered" in "[data-notification]"
Scenario: A Service instance without a Proxy does not display Proxy Info tab
Given 1 proxy model from yaml
---
- ServiceProxy:
DestinationServiceName: service-1
DestinationServiceID: ~
---
When I visit the instance page for yaml
---
dc: dc1
service: service-0
node: node-0
id: service-0-with-id
---
Then the url should be /dc1/services/service-0/instances/node-0/service-0-with-id/health-checks
And I don't see proxy on the tabs

View File

@ -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);
});
}

View File

@ -2,31 +2,18 @@ export default function(visitable, attribute, collection, text, tabs) {
return {
visit: visitable('/:dc/services/:service/instances/:node/:id'),
externalSource: attribute('data-test-external-source', '[data-test-external-source]', {
scope: '.title-bar',
scope: '.title',
}),
tabs: tabs('tab', [
'health-checks',
'addresses',
'upstreams',
'exposed-paths',
'tags',
'metadata',
]),
serviceChecks: collection('[data-test-service-checks] li', {
exposed: attribute('data-test-exposed', '[data-test-exposed]'),
}),
nodeChecks: collection('[data-test-node-checks] li', {
exposed: attribute('data-test-exposed', '[data-test-exposed]'),
}),
upstreams: collection('#upstreams [data-test-tabular-row]', {
tabs: tabs('tab', ['health-checks', 'proxy-info', 'addresses', 'tags', 'metadata']),
serviceChecks: collection('[data-test-service-checks] li'),
nodeChecks: collection('[data-test-node-checks] li'),
upstreams: collection('[data-test-proxy-upstreams] > li', {
name: text('[data-test-destination-name]'),
datacenter: text('[data-test-destination-datacenter]'),
type: text('[data-test-destination-type]'),
address: text('[data-test-local-bind-address]'),
}),
exposedPaths: collection('#exposed-paths [data-test-tabular-row]', {
exposedPaths: collection('[data-test-proxy-exposed-paths] > tbody tr', {
combinedAddress: text('[data-test-combined-address]'),
}),
proxyChecks: collection('[data-test-proxy-checks] li'),
addresses: collection('#addresses [data-test-tabular-row]', {
address: text('[data-test-address]'),
}),

View File

@ -2,7 +2,7 @@ export default function(visitable, attribute, collection, text, intentions, filt
return {
visit: visitable('/:dc/services/:service'),
externalSource: attribute('data-test-external-source', '[data-test-external-source]', {
scope: '.title-bar',
scope: '.title',
}),
dashboardAnchor: {
href: attribute('href', '[data-test-dashboard-anchor]'),