ui: Add deny SVG lines with icons (#8846)

* Refactor and color SVG Lines based on intention permissions

* Create Icon component with L7 and Deny icon styling

* Reposition icons on the lines when the lines are redrawn

* Create service/intention-permissions helper

* Use service/intention-permissions helper to return allow or deny lines

* Upgrade consul-api-double to v5.3.5

* Update HasPermission attribute
This commit is contained in:
Kenia 2020-10-08 11:52:09 -04:00 committed by GitHub
parent 164ce57db2
commit b373456c76
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 331 additions and 97 deletions

View File

@ -0,0 +1,62 @@
{{on-window 'resize' (action this.getIconPositions)}}
{{#if (gt @lines.length 0)}}
<svg
{{did-insert this.getIconPositions}}
{{did-update this.getIconPositions @lines}}
viewBox={{concat @view.x ' ' @view.y ' ' @view.width ' ' @view.height}}
preserveAspectRatio="none"
>
<defs>
<marker id="allow-dot" viewBox="-2 -2 15 15" refX="6" refY="6" markerWidth="6" markerHeight="6">
<circle
cx="6"
cy="6"
r="6"
/>
</marker>
<marker id="allow-arrow" viewBox="-1 -1 12 12" refX="5" refY="5"
markerWidth="6" markerHeight="6"
orient="auto-start-reverse">
<polygon points="0 0 10 5 0 10" />
</marker>
<marker id="deny-dot" viewBox="-2 -2 15 15" refX="6" refY="6" markerWidth="6" markerHeight="6">
<circle
cx="6"
cy="6"
r="6"
/>
</marker>
<marker id="deny-arrow" viewBox="-1 -1 12 12" refX="5" refY="5"
markerWidth="6" markerHeight="6"
orient="auto-start-reverse">
<polygon points="0 0 10 5 0 10" />
</marker>
</defs>
{{#each @lines as |line|}}
{{#if (eq line.permission 'deny')}}
<path
id={{line.id}}
d={{svg-curve line.dest src=line.src}}
marker-start="url(#deny-dot)"
marker-end="url(#deny-arrow)"
data-permission={{line.permission}}
/>
{{else}}
<path
id={{line.id}}
d={{svg-curve line.dest src=line.src}}
marker-start="url(#allow-dot)"
marker-end="url(#allow-arrow)"
data-permission={{line.permission}}
/>
{{/if}}
{{/each}}
</svg>
{{/if}}
<TopologyMetrics::Icon
@positions={{this.iconPositions}}
@items={{@items}}
/>

View File

@ -0,0 +1,24 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
export default class TopoloyMetricsDownLines extends Component {
@tracked iconPositions;
@action
getIconPositions() {
const view = this.args.view;
const lines = [...document.querySelectorAll('#downstream-lines path')];
this.iconPositions = lines.map(item => {
const pathLen = parseFloat(item.getTotalLength());
const thirdLen = item.getPointAtLength(Math.ceil(pathLen / 3));
return {
id: item.id,
x: thirdLen.x - view.x,
y: thirdLen.y - view.y,
};
});
}
}

View File

@ -0,0 +1,17 @@
{{#each @items as |item|}}
{{#let (find-by 'id' (concat item.Namespace item.Name) @positions) as |style|}}
{{#if (and (not item.Intention.Allowed) (not item.Intention.HasPermissions))}}
<span class="deny" style={{{ concat 'top:' style.y 'px;left:' style.x 'px;'}}}>
<Tooltip>
An intention is set to 'deny' that prohibits these services from connecting.
</Tooltip>
</span>
{{else if item.Intention.HasPermissions}}
<span class="L7" style={{{ concat 'top:' style.y 'px;left:' style.x 'px;'}}}>
<Tooltip>
The intention between these services has Layer 7 permissions, so certain requests may or may not be permitted.
</Tooltip>
</span>
{{/if}}
{{/let}}
{{/each}}

View File

@ -12,7 +12,10 @@
</span> </span>
</div> </div>
{{#each @downstreams as |downstream|}} {{#each @downstreams as |downstream|}}
<div class="card"> <div class="card"
data-permission={{service/intention-permissions downstream}}
id="{{downstream.Namespace}}{{downstream.Name}}"
>
<p> <p>
{{downstream.Name}} {{downstream.Name}}
</p> </p>
@ -58,34 +61,13 @@
</div> </div>
</div> </div>
<div id="downstream-lines"> <div id="downstream-lines">
{{#if (gt this.downLines.length 0)}} <TopologyMetrics::DownLines
<svg @type='downstream'
viewBox={{concat downView.x ' ' downView.y ' ' downView.width ' ' downView.height}} @view={{this.downView}}
preserveAspectRatio="none" @center={{this.centerDimensions}}
> @lines={{this.downLines}}
<defs> @items={{@downstreams}}
<marker id="dot" viewBox="-2 -2 15 15" refX="6" refY="6" markerWidth="6" markerHeight="6">
<circle
cx="6"
cy="6"
r="6"
/> />
</marker>
<marker id="arrow" viewBox="-1 -1 12 12" refX="5" refY="5"
markerWidth="6" markerHeight="6"
orient="auto-start-reverse">
<polygon points="0 0 10 5 0 10" />
</marker>
</defs>
{{#each this.downLines as |svg| }}
<path
d={{svg-curve svg.dest src=svg.src}}
marker-start="url(#dot)"
marker-end="url(#arrow)"
/>
{{/each}}
</svg>
{{/if}}
</div> </div>
{{#if (gt @upstreams.length 0)}} {{#if (gt @upstreams.length 0)}}
<div id="upstream-column"> <div id="upstream-column">
@ -93,7 +75,10 @@
<div id="upstream-container"> <div id="upstream-container">
<p>{{dc}}</p> <p>{{dc}}</p>
{{#each upstreams as |upstream|}} {{#each upstreams as |upstream|}}
<div class="card"> <div class="card"
data-permission={{service/intention-permissions upstream}}
id="{{upstream.Namespace}}{{upstream.Name}}"
>
<p> <p>
{{upstream.Name}} {{upstream.Name}}
</p> </p>
@ -142,33 +127,12 @@
</div> </div>
{{/if}} {{/if}}
<div id="upstream-lines"> <div id="upstream-lines">
{{#if (gt this.upLines.length 0)}} <TopologyMetrics::UpLines
<svg @type='upstream'
viewBox={{concat this.centerDimensions.x ' ' upView.y ' ' upView.width ' ' upView.height}} @view={{this.upView}}
preserveAspectRatio="none" @center={{this.centerDimensions}}
> @lines={{this.upLines}}
<defs> @items={{@upstreams}}
<marker id="dot" viewBox="-2 -2 15 15" refX="6" refY="6" markerWidth="6" markerHeight="6">
<circle
cx="6"
cy="6"
r="6"
/> />
</marker>
<marker id="arrow" viewBox="-1 -1 12 12" refX="5" refY="5"
markerWidth="6" markerHeight="6"
orient="auto-start-reverse">
<polygon points="0 0 10 5 0 10" />
</marker>
</defs>
{{#each this.upLines as |svg| }}
<path
d={{svg-curve svg.dest src=svg.src}}
marker-start="url(#dot)"
marker-end="url(#arrow)"
/>
{{/each}}
</svg>
{{/if}}
</div> </div>
</div> </div>

View File

@ -12,40 +12,56 @@ export default class TopologyMetrics extends Component {
// =methods // =methods
drawDownLines(items) { drawDownLines(items) {
return items.map(item => { const order = ['allow', 'deny'];
const dimensions = item.getBoundingClientRect();
const dest = { const dest = {
x: this.centerDimensions.x, x: this.centerDimensions.x,
y: this.centerDimensions.y + this.centerDimensions.height / 4, y: this.centerDimensions.y + this.centerDimensions.height / 4,
}; };
return items
.map(item => {
const dimensions = item.getBoundingClientRect();
const src = { const src = {
x: dimensions.x + dimensions.width, x: dimensions.x + dimensions.width,
y: dimensions.y + dimensions.height / 2, y: dimensions.y + dimensions.height / 2,
}; };
return { return {
id: item.id,
permission: item.getAttribute('data-permission'),
dest: dest, dest: dest,
src: src, src: src,
}; };
})
.sort((a, b) => {
return order.indexOf(a.permission) - order.indexOf(b.permission);
}); });
} }
drawUpLines(items) { drawUpLines(items) {
return items.map(item => { const order = ['allow', 'deny'];
const dimensions = item.getBoundingClientRect();
const dest = {
x: dimensions.x - dimensions.width - 26,
y: dimensions.y + dimensions.height / 2,
};
const src = { const src = {
x: this.centerDimensions.x + 20, x: this.centerDimensions.x + 20,
y: this.centerDimensions.y + this.centerDimensions.height / 4, y: this.centerDimensions.y + this.centerDimensions.height / 4,
}; };
return items
.map(item => {
const dimensions = item.getBoundingClientRect();
const dest = {
x: dimensions.x - dimensions.width - 26,
y: dimensions.y + dimensions.height / 2,
};
return { return {
id: item.id,
permission: item.getAttribute('data-permission'),
dest: dest, dest: dest,
src: src, src: src,
}; };
})
.sort((a, b) => {
return order.indexOf(a.permission) - order.indexOf(b.permission);
}); });
} }

View File

@ -94,15 +94,45 @@
circle { circle {
fill: $white; fill: $white;
} }
polygon { #allow-arrow {
fill: $gray-300; fill: $gray-300;
stroke-linejoin: round; stroke-linejoin: round;
} }
path, path,
circle, #allow-dot,
polygon { #allow-arrow {
stroke: $gray-300; stroke: $gray-300;
stroke-width: 2; stroke-width: 2;
} }
path[data-permission='deny'] {
stroke: $red-500;
}
#deny-dot {
stroke: $red-500;
stroke-width: 2;
}
#deny-arrow {
fill: $red-500;
stroke: $red-500;
stroke-linejoin: round;
}
}
// Icon on SVG Lines
#downstream-lines,
#upstream-lines {
.deny::before {
@extend %with-cancel-square-fill-color-mask, %as-pseudo;
background-color: $red-500;
}
.L7::before {
@extend %with-layers-mask, %as-pseudo;
background-color: $gray-300;
}
span {
transform: translate(-50%,-50%);
position: absolute;
background-color: $white;
}
} }

View File

@ -0,0 +1,61 @@
{{on-window 'resize' (action this.getIconPositions)}}
{{#if (gt @lines.length 0)}}
<svg
{{did-insert this.getIconPositions}}
{{did-update this.getIconPositions @lines}}
viewBox={{concat @center.x ' ' @view.y ' ' @view.width ' ' @view.height}}
preserveAspectRatio="none"
>
<defs>
<marker id="allow-dot" viewBox="-2 -2 15 15" refX="6" refY="6" markerWidth="6" markerHeight="6">
<circle
cx="6"
cy="6"
r="6"
/>
</marker>
<marker id="allow-arrow" viewBox="-1 -1 12 12" refX="5" refY="5"
markerWidth="6" markerHeight="6"
orient="auto-start-reverse">
<polygon points="0 0 10 5 0 10" />
</marker>
<marker id="deny-dot" viewBox="-2 -2 15 15" refX="6" refY="6" markerWidth="6" markerHeight="6">
<circle
cx="6"
cy="6"
r="6"
/>
</marker>
<marker id="deny-arrow" viewBox="-1 -1 12 12" refX="5" refY="5"
markerWidth="6" markerHeight="6"
orient="auto-start-reverse">
<polygon points="0 0 10 5 0 10" />
</marker>
</defs>
{{#each @lines as |line|}}
{{#if (eq line.permission 'deny')}}
<path
id={{line.id}}
d={{svg-curve line.dest src=line.src}}
marker-start="url(#deny-dot)"
marker-end="url(#deny-arrow)"
data-permission={{line.permission}}
/>
{{else}}
<path
id={{line.id}}
d={{svg-curve line.dest src=line.src}}
marker-start="url(#allow-dot)"
marker-end="url(#allow-arrow)"
data-permission={{line.permission}}
/>
{{/if}}
{{/each}}
</svg>
{{/if}}
<TopologyMetrics::Icon
@positions={{this.iconPositions}}
@items={{@items}}
/>

View File

@ -0,0 +1,23 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
export default class TopoloyMetricsUpLines extends Component {
@tracked iconPositions;
@action
getIconPositions() {
const center = this.args.center;
const lines = [...document.querySelectorAll('#upstream-lines path')];
this.iconPositions = lines.map(item => {
const pathLen = parseFloat(item.getTotalLength());
const partLen = item.getPointAtLength(Math.ceil(pathLen * 0.666));
return {
id: item.id,
x: partLen.x - center.x,
y: partLen.y - center.y * 0.81,
};
});
}
}

View File

@ -0,0 +1,15 @@
import { helper } from '@ember/component/helper';
export default helper(function serviceIntentionPermissions([params] /*, hash*/) {
const hasPermissions = params.Intention.HasPermissions;
const allowed = params.Intention.Allowed;
switch (true) {
case hasPermissions:
return 'allow';
case !allowed && !hasPermissions:
return 'deny';
default:
return 'allow';
}
});

View File

@ -56,7 +56,7 @@
"@ember/render-modifiers": "^1.0.2", "@ember/render-modifiers": "^1.0.2",
"@glimmer/component": "^1.0.0", "@glimmer/component": "^1.0.0",
"@glimmer/tracking": "^1.0.0", "@glimmer/tracking": "^1.0.0",
"@hashicorp/consul-api-double": "^5.2.3", "@hashicorp/consul-api-double": "^5.3.5",
"@hashicorp/ember-cli-api-double": "^3.1.0", "@hashicorp/ember-cli-api-double": "^3.1.0",
"@xstate/fsm": "^1.4.0", "@xstate/fsm": "^1.4.0",
"babel-eslint": "^10.0.3", "babel-eslint": "^10.0.3",

View File

@ -0,0 +1,22 @@
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/intention-permissions', function(hooks) {
setupRenderingTest(hooks);
// TODO: Replace this with your real tests.
test('it renders', async function(assert) {
this.set('inputValue', {
Intention: {
Allowed: false,
HasPermissions: true,
},
});
await render(hbs`{{service/intention-permissions inputValue}}`);
assert.equal(this.element.textContent.trim(), 'allow');
});
});

View File

@ -1527,10 +1527,10 @@
faker "^4.1.0" faker "^4.1.0"
js-yaml "^3.13.1" js-yaml "^3.13.1"
"@hashicorp/consul-api-double@^5.2.3": "@hashicorp/consul-api-double@^5.3.5":
version "5.2.3" version "5.3.5"
resolved "https://registry.yarnpkg.com/@hashicorp/consul-api-double/-/consul-api-double-5.2.3.tgz#c34cec063b519595c49bb3fce799541f7d967f66" resolved "https://registry.yarnpkg.com/@hashicorp/consul-api-double/-/consul-api-double-5.3.5.tgz#8e39d6af4ab6d32c7d8c469bb4aab23e16971bd3"
integrity sha512-NlnBUHoXLlQwTB1lFzYvaIUZnf5KOGnohXRm4D3B8xVC+D0py6dTP5dj3NpBuxrG5b0xSv2zTF3tz9Y5nehOzQ== integrity sha512-SiT2lLk0J8CwsxtuAobrweC5VdOT6b66M1gSLcT/Lcx62fOLH1X/DfMt6F2VKwC4BN8WBFZGTmn0rwdFOjKpmw==
"@hashicorp/ember-cli-api-double@^3.1.0": "@hashicorp/ember-cli-api-double@^3.1.0":
version "3.1.2" version "3.1.2"