ui: Topology view with no dependencies (#11280)
This commit is contained in:
parent
b3af482e09
commit
24d8236219
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:feature
|
||||||
|
ui: Topology - New views for scenarios where no dependencies exist or ACLs are disabled
|
||||||
|
```
|
|
@ -1,5 +1,9 @@
|
||||||
{{#if (eq @item.Name '* (All Services)')}}
|
{{#if (eq @item.Datacenter '')}}
|
||||||
<a data-test-topology-metrics-card class="topology-metrics-card" href={{href-to 'dc.services.index'}}>
|
<a
|
||||||
|
class="topology-metrics-card"
|
||||||
|
href={{href-to 'dc.services.index'}}
|
||||||
|
data-permission={{service/card-permissions @item}}
|
||||||
|
>
|
||||||
<p class="empty">
|
<p class="empty">
|
||||||
{{@item.Name}}
|
{{@item.Name}}
|
||||||
</p>
|
</p>
|
||||||
|
@ -12,7 +16,7 @@
|
||||||
(href-to this.hrefPath @item.Datacenter @item.Name params=(hash nspace=@item.Namespace))
|
(href-to this.hrefPath @item.Datacenter @item.Name params=(hash nspace=@item.Namespace))
|
||||||
(href-to this.hrefPath @item.Name)
|
(href-to this.hrefPath @item.Name)
|
||||||
}}
|
}}
|
||||||
data-permission={{service/intention-permissions @item}}
|
data-permission={{service/card-permissions @item}}
|
||||||
id="{{@item.Namespace}}{{@item.Name}}"
|
id="{{@item.Namespace}}{{@item.Name}}"
|
||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
|
|
|
@ -84,7 +84,7 @@
|
||||||
|
|
||||||
{{#let (not (can 'update intention for service' item=@service.Service)) as |disabled|}}
|
{{#let (not (can 'update intention for service' item=@service.Service)) as |disabled|}}
|
||||||
{{#each @items as |item|}}
|
{{#each @items as |item|}}
|
||||||
{{#if (or (not item.Intention.Allowed) item.Intention.HasPermissions)}}
|
{{#if (and (not-eq item.Datacenter '') (or (not item.Intention.Allowed) item.Intention.HasPermissions))}}
|
||||||
<TopologyMetrics::Popover
|
<TopologyMetrics::Popover
|
||||||
@type={{if item.Intention.HasPermissions 'l7' 'deny'}}
|
@type={{if item.Intention.HasPermissions 'l7' 'deny'}}
|
||||||
@position={{find-by 'id' (concat this.guid item.Namespace item.Name) this.iconPositions}}
|
@position={{find-by 'id' (concat this.guid item.Namespace item.Name) this.iconPositions}}
|
||||||
|
@ -92,7 +92,7 @@
|
||||||
@disabled={{disabled}}
|
@disabled={{disabled}}
|
||||||
@oncreate={{action @oncreate item @service}}
|
@oncreate={{action @oncreate item @service}}
|
||||||
/>
|
/>
|
||||||
{{else if (and item.Intention.Allowed (not item.TransparentProxy) (eq item.Source 'specific-intention'))}}
|
{{else if (and (not-eq item.Datacenter '') item.Intention.Allowed (not item.TransparentProxy) (eq item.Source 'specific-intention'))}}
|
||||||
<TopologyMetrics::Popover
|
<TopologyMetrics::Popover
|
||||||
@type='not-defined'
|
@type='not-defined'
|
||||||
@service={{@service}}
|
@service={{@service}}
|
||||||
|
|
|
@ -2,12 +2,13 @@
|
||||||
{{on-resize this.calculate}}
|
{{on-resize this.calculate}}
|
||||||
class="topology-container consul-topology-metrics"
|
class="topology-container consul-topology-metrics"
|
||||||
>
|
>
|
||||||
{{#if (gt @topology.Downstreams.length 0)}}
|
{{#if (gt this.downstreams.length 0)}}
|
||||||
<div
|
<div
|
||||||
id="downstream-container"
|
id="downstream-container"
|
||||||
{{did-insert this.setHeight 'downstream-lines'}}
|
{{did-insert this.setHeight 'downstream-lines'}}
|
||||||
{{did-update this.setHeight 'downstream-lines' @topology.Downstreams}}
|
{{did-update this.setHeight 'downstream-lines' this.downstreams}}
|
||||||
>
|
>
|
||||||
|
{{#if (not this.emptyColumn)}}
|
||||||
<div>
|
<div>
|
||||||
<p>{{@dc.Name}}</p>
|
<p>{{@dc.Name}}</p>
|
||||||
<span>
|
<span>
|
||||||
|
@ -16,7 +17,8 @@
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{{#each @topology.Downstreams as |item|}}
|
{{/if}}
|
||||||
|
{{#each this.downstreams as |item|}}
|
||||||
<TopologyMetrics::Card
|
<TopologyMetrics::Card
|
||||||
@nspace={{@nspace}}
|
@nspace={{@nspace}}
|
||||||
@dc={{@dc.Name}}
|
@dc={{@dc.Name}}
|
||||||
|
@ -82,7 +84,7 @@
|
||||||
@view={{this.downView}}
|
@view={{this.downView}}
|
||||||
@center={{this.centerDimensions}}
|
@center={{this.centerDimensions}}
|
||||||
@lines={{this.downLines}}
|
@lines={{this.downLines}}
|
||||||
@items={{@topology.Downstreams}}
|
@items={{this.downstreams}}
|
||||||
@oncreate={{action @oncreate}}
|
@oncreate={{action @oncreate}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
import Component from '@glimmer/component';
|
import Component from '@glimmer/component';
|
||||||
import { tracked } from '@glimmer/tracking';
|
import { tracked } from '@glimmer/tracking';
|
||||||
import { action, get } from '@ember/object';
|
import { action, get } from '@ember/object';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
|
||||||
export default class TopologyMetrics extends Component {
|
export default class TopologyMetrics extends Component {
|
||||||
|
@service('env') env;
|
||||||
|
|
||||||
// =attributes
|
// =attributes
|
||||||
@tracked centerDimensions;
|
@tracked centerDimensions;
|
||||||
@tracked downView;
|
@tracked downView;
|
||||||
|
@ -66,19 +69,58 @@ export default class TopologyMetrics extends Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
emptyColumn() {
|
||||||
|
const noDependencies = get(this.args.topology, 'noDependencies');
|
||||||
|
return !this.env.var('CONSUL_ACLS_ENABLED') || noDependencies;
|
||||||
|
}
|
||||||
|
|
||||||
|
get downstreams() {
|
||||||
|
const downstreams = get(this.args.topology, 'Downstreams') || [];
|
||||||
|
const items = [...downstreams];
|
||||||
|
const noDependencies = get(this.args.topology, 'noDependencies');
|
||||||
|
|
||||||
|
if (!this.env.var('CONSUL_ACLS_ENABLED') && noDependencies) {
|
||||||
|
items.push({
|
||||||
|
Name: 'Downstreams unknown.',
|
||||||
|
Empty: true,
|
||||||
|
Datacenter: '',
|
||||||
|
Namespace: '',
|
||||||
|
});
|
||||||
|
} else if (downstreams.length === 0) {
|
||||||
|
items.push({
|
||||||
|
Name: 'No downstreams.',
|
||||||
|
Datacenter: '',
|
||||||
|
Namespace: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
get upstreams() {
|
get upstreams() {
|
||||||
const upstreams = get(this.args.topology, 'Upstreams') || [];
|
const upstreams = get(this.args.topology, 'Upstreams') || [];
|
||||||
const items = [...upstreams];
|
const items = [...upstreams];
|
||||||
const defaultACLPolicy = get(this.args.dc, 'DefaultACLPolicy');
|
const defaultACLPolicy = get(this.args.dc, 'DefaultACLPolicy');
|
||||||
const wildcardIntention = get(this.args.topology, 'wildcardIntention');
|
const wildcardIntention = get(this.args.topology, 'wildcardIntention');
|
||||||
if (defaultACLPolicy === 'allow' || wildcardIntention) {
|
const noDependencies = get(this.args.topology, 'noDependencies');
|
||||||
|
|
||||||
|
if (!this.env.var('CONSUL_ACLS_ENABLED') && noDependencies) {
|
||||||
|
items.push({
|
||||||
|
Name: 'Upstreams unknown.',
|
||||||
|
Datacenter: '',
|
||||||
|
Namespace: '',
|
||||||
|
});
|
||||||
|
} else if (defaultACLPolicy === 'allow' || wildcardIntention) {
|
||||||
items.push({
|
items.push({
|
||||||
Name: '* (All Services)',
|
Name: '* (All Services)',
|
||||||
Datacenter: '',
|
Datacenter: '',
|
||||||
Namespace: '',
|
Namespace: '',
|
||||||
Intention: {
|
});
|
||||||
Allowed: true,
|
} else if (upstreams.length === 0) {
|
||||||
},
|
items.push({
|
||||||
|
Name: 'No upstreams.',
|
||||||
|
Datacenter: '',
|
||||||
|
Namespace: '',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return items;
|
return items;
|
||||||
|
@ -112,10 +154,21 @@ export default class TopologyMetrics extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate viewBox dimensions
|
// Calculate viewBox dimensions
|
||||||
this.downView = document.getElementById('downstream-lines').getBoundingClientRect();
|
const downstreamLines = document.getElementById('downstream-lines').getBoundingClientRect();
|
||||||
const upstreamLines = document.getElementById('upstream-lines').getBoundingClientRect();
|
const upstreamLines = document.getElementById('upstream-lines').getBoundingClientRect();
|
||||||
const upstreamColumn = document.getElementById('upstream-column');
|
const upstreamColumn = document.getElementById('upstream-column');
|
||||||
|
|
||||||
|
if (this.emptyColumn) {
|
||||||
|
this.downView = {
|
||||||
|
x: downstreamLines.x,
|
||||||
|
y: downstreamLines.y,
|
||||||
|
width: downstreamLines.width,
|
||||||
|
height: downstreamLines.height + 10,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
this.downView = downstreamLines;
|
||||||
|
}
|
||||||
|
|
||||||
if (upstreamColumn) {
|
if (upstreamColumn) {
|
||||||
this.upView = {
|
this.upView = {
|
||||||
x: upstreamLines.x,
|
x: upstreamLines.x,
|
||||||
|
|
|
@ -65,7 +65,8 @@
|
||||||
stroke: rgb(var(--tone-gray-300));
|
stroke: rgb(var(--tone-gray-300));
|
||||||
stroke-width: 2;
|
stroke-width: 2;
|
||||||
}
|
}
|
||||||
path[data-permission='not-defined'] {
|
path[data-permission='not-defined'],
|
||||||
|
path[data-permission='empty'] {
|
||||||
stroke-dasharray: 4;
|
stroke-dasharray: 4;
|
||||||
}
|
}
|
||||||
path[data-permission='deny'] {
|
path[data-permission='deny'] {
|
||||||
|
|
|
@ -83,7 +83,7 @@
|
||||||
</svg>
|
</svg>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#each @items as |item|}}
|
{{#each @items as |item|}}
|
||||||
{{#if (or (not item.Intention.Allowed) item.Intention.HasPermissions)}}
|
{{#if (and (not-eq item.Datacenter '') (or (not item.Intention.Allowed) item.Intention.HasPermissions))}}
|
||||||
<TopologyMetrics::Popover
|
<TopologyMetrics::Popover
|
||||||
@type={{if item.Intention.HasPermissions 'l7' 'deny'}}
|
@type={{if item.Intention.HasPermissions 'l7' 'deny'}}
|
||||||
@position={{find-by 'id' (concat this.guid item.Namespace item.Name) this.iconPositions}}
|
@position={{find-by 'id' (concat this.guid item.Namespace item.Name) this.iconPositions}}
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { helper } from '@ember/component/helper';
|
||||||
|
|
||||||
|
export default helper(function serviceCardPermissions([params] /*, hash*/) {
|
||||||
|
if (params.Datacenter === '') {
|
||||||
|
return 'empty';
|
||||||
|
} else {
|
||||||
|
const hasPermissions = params.Intention.HasPermissions;
|
||||||
|
const allowed = params.Intention.Allowed;
|
||||||
|
const notExplicitlyDefined = params.Source === 'specific-intention' && !params.TransparentProxy;
|
||||||
|
|
||||||
|
switch (true) {
|
||||||
|
case hasPermissions:
|
||||||
|
return 'allow';
|
||||||
|
case !allowed && !hasPermissions:
|
||||||
|
return 'deny';
|
||||||
|
case allowed && notExplicitlyDefined:
|
||||||
|
return 'not-defined';
|
||||||
|
default:
|
||||||
|
return 'allow';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -1,18 +0,0 @@
|
||||||
import { helper } from '@ember/component/helper';
|
|
||||||
|
|
||||||
export default helper(function serviceIntentionPermissions([params] /*, hash*/) {
|
|
||||||
const hasPermissions = params.Intention.HasPermissions;
|
|
||||||
const allowed = params.Intention.Allowed;
|
|
||||||
const notExplicitlyDefined = params.Source === 'specific-intention' && !params.TransparentProxy;
|
|
||||||
|
|
||||||
switch (true) {
|
|
||||||
case hasPermissions:
|
|
||||||
return 'allow';
|
|
||||||
case !allowed && !hasPermissions:
|
|
||||||
return 'deny';
|
|
||||||
case allowed && notExplicitlyDefined:
|
|
||||||
return 'not-defined';
|
|
||||||
default:
|
|
||||||
return 'allow';
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -46,4 +46,9 @@ export default class Topology extends Model {
|
||||||
|
|
||||||
return downstreamWildcard || upstreamWildcard;
|
return downstreamWildcard || upstreamWildcard;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@computed('Downstreams', 'Upstreams')
|
||||||
|
get noDependencies() {
|
||||||
|
return this.Upstreams.length === 0 && this.Downstreams.length === 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,26 +27,6 @@ as |route|>
|
||||||
loader.data
|
loader.data
|
||||||
as |nspace dc items topology|}}
|
as |nspace dc items topology|}}
|
||||||
<div class="tab-section">
|
<div class="tab-section">
|
||||||
{{#if (and (eq topology.Upstreams.length 0) (eq topology.Downstreams.length 0) (not-eq dc.DefaultACLPolicy 'allow') (not topology.wildcardIntention))}}
|
|
||||||
<EmptyState>
|
|
||||||
<BlockSlot @name="header">
|
|
||||||
<h2>
|
|
||||||
No dependencies
|
|
||||||
</h2>
|
|
||||||
</BlockSlot>
|
|
||||||
<BlockSlot @name="body">
|
|
||||||
<p>
|
|
||||||
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.
|
|
||||||
</p>
|
|
||||||
</BlockSlot>
|
|
||||||
<BlockSlot @name="actions">
|
|
||||||
<li class="docs-link">
|
|
||||||
<a href="{{env 'CONSUL_DOCS_URL'}}/connect/registration/service-registration#complete-configuration-example" rel="noopener noreferrer" target="_blank">Documentation on upstreams</a>
|
|
||||||
</li>
|
|
||||||
</BlockSlot>
|
|
||||||
</EmptyState>
|
|
||||||
{{else}}
|
|
||||||
|
|
||||||
{{#let (collapsible-notices topology.FilteredByACLs (eq dc.DefaultACLPolicy 'allow') topology.wildcardIntention topology.notDefinedIntention) as |collapsible| }}
|
{{#let (collapsible-notices topology.FilteredByACLs (eq dc.DefaultACLPolicy 'allow') topology.wildcardIntention topology.notDefinedIntention) as |collapsible| }}
|
||||||
<CollapsibleNotices @collapsible={{collapsible}}>
|
<CollapsibleNotices @collapsible={{collapsible}}>
|
||||||
{{#if topology.FilteredByACLs}}
|
{{#if topology.FilteredByACLs}}
|
||||||
|
@ -85,6 +65,26 @@ as |nspace dc items topology|}}
|
||||||
@action={{true}}
|
@action={{true}}
|
||||||
/>
|
/>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
{{#if (and topology.noDependencies (can 'use acls'))}}
|
||||||
|
<TopologyMetrics::Notice
|
||||||
|
data-test-notice='no-dependencies'
|
||||||
|
@type="info"
|
||||||
|
@for="no-dependencies"
|
||||||
|
@link="{{env 'CONSUL_DOCS_URL'}}/connect/registration/service-registration#upstream-configuration-reference"
|
||||||
|
@internal={{false}}
|
||||||
|
@action={{true}}
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
{{#if (and topology.noDependencies (not (can 'use acls')))}}
|
||||||
|
<TopologyMetrics::Notice
|
||||||
|
data-test-notice='acls-disabled'
|
||||||
|
@type="warning"
|
||||||
|
@for="acls-disabled"
|
||||||
|
@link="{{env 'CONSUL_DOCS_URL'}}/security/acl/acl-system#configuring-acls"
|
||||||
|
@internal={{false}}
|
||||||
|
@action={{true}}
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
</CollapsibleNotices>
|
</CollapsibleNotices>
|
||||||
{{/let}}
|
{{/let}}
|
||||||
<DataSource
|
<DataSource
|
||||||
|
@ -113,8 +113,6 @@ as |nspace dc items topology|}}
|
||||||
/>
|
/>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</DataSource>
|
</DataSource>
|
||||||
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
</div>
|
||||||
{{/let}}
|
{{/let}}
|
||||||
</BlockSlot>
|
</BlockSlot>
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
|
@ -3,7 +3,7 @@ import { setupRenderingTest } from 'ember-qunit';
|
||||||
import { render } from '@ember/test-helpers';
|
import { render } from '@ember/test-helpers';
|
||||||
import { hbs } from 'ember-cli-htmlbars';
|
import { hbs } from 'ember-cli-htmlbars';
|
||||||
|
|
||||||
module('Integration | Helper | service/intention-permissions', function(hooks) {
|
module('Integration | Helper | service/card-permissions', function(hooks) {
|
||||||
setupRenderingTest(hooks);
|
setupRenderingTest(hooks);
|
||||||
|
|
||||||
// TODO: Replace this with your real tests.
|
// TODO: Replace this with your real tests.
|
||||||
|
@ -15,7 +15,7 @@ module('Integration | Helper | service/intention-permissions', function(hooks) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await render(hbs`{{service/intention-permissions inputValue}}`);
|
await render(hbs`{{service/card-permissions inputValue}}`);
|
||||||
|
|
||||||
assert.equal(this.element.textContent.trim(), 'allow');
|
assert.equal(this.element.textContent.trim(), 'allow');
|
||||||
});
|
});
|
|
@ -48,6 +48,12 @@ export default function(
|
||||||
notDefinedIntention: {
|
notDefinedIntention: {
|
||||||
see: isPresent('[data-test-notice="not-defined-intention"]'),
|
see: isPresent('[data-test-notice="not-defined-intention"]'),
|
||||||
},
|
},
|
||||||
|
noDependencies: {
|
||||||
|
see: isPresent('[data-test-notice="no-dependencies"]'),
|
||||||
|
},
|
||||||
|
aclsDisabled: {
|
||||||
|
see: isPresent('[data-test-notice="acls-disabled"]'),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
page.tabs.upstreamsTab = {
|
page.tabs.upstreamsTab = {
|
||||||
services: collection('.consul-upstream-list > ul > li:not(:first-child)', {
|
services: collection('.consul-upstream-list > ul > li:not(:first-child)', {
|
||||||
|
|
|
@ -143,6 +143,14 @@ topology-metrics:
|
||||||
footer:
|
footer:
|
||||||
name: Edit intentions
|
name: Edit intentions
|
||||||
URL: dc.services.show.intentions
|
URL: dc.services.show.intentions
|
||||||
|
no-dependencies:
|
||||||
|
header: No dependencies
|
||||||
|
body: The service you are viewing currently has no dependencies. You will only see metrics for the current service until dependencies are added.
|
||||||
|
footer: Read the documentation
|
||||||
|
acls-disabled:
|
||||||
|
header: Enable ACLs
|
||||||
|
body: This connect-native service may have dependencies, but Consul isn't aware of them when ACLs are disabled. Enable ACLs to make this view more useful.
|
||||||
|
footer: Read the documentation
|
||||||
popover:
|
popover:
|
||||||
l7:
|
l7:
|
||||||
header: Layer 7 permissions
|
header: Layer 7 permissions
|
||||||
|
|
Loading…
Reference in New Issue