From 7243f1f4f9a803624a772583628e6268e8cb3cad Mon Sep 17 00:00:00 2001 From: Kenia <19161242+kaxcode@users.noreply.github.com> Date: Thu, 12 Nov 2020 10:40:15 -0500 Subject: [PATCH] ui: Topology Intentions Popovers (#9137) * Refactor grid styling for Topology page * Crate TopologyMetrics Button component and move styling * Create intention ID * fixup button styling * Return a link to the create intention page * Rename Button to Popover component * Fixup serializer test * ui: Inline Topology Intention Actions (#9153) * Add arrow and dot to/from metrics back in * Add addional space to have metrics wrap and show in smaller screens * Move logic for finding positioning * Use color variables Co-authored-by: John Cowen --- .../app/components/topology-metrics/card.hbs | 22 +++-- .../topology-metrics/down-lines/index.hbs | 11 ++- .../app/components/topology-metrics/icon.hbs | 17 ---- .../app/components/topology-metrics/index.hbs | 22 +++-- .../app/components/topology-metrics/index.js | 6 +- .../components/topology-metrics/layout.scss | 28 +++--- .../topology-metrics/popover/index.hbs | 71 +++++++++++++++ .../topology-metrics/popover/index.js | 14 +++ .../topology-metrics/popover/index.scss | 87 +++++++++++++++++++ .../topology-metrics/series/layout.scss | 3 +- .../app/components/topology-metrics/skin.scss | 19 ---- .../topology-metrics/stats/index.scss | 6 +- .../topology-metrics/up-lines/index.hbs | 11 ++- .../topology-metrics/up-lines/index.js | 2 +- .../consul-ui/app/routes/dc/services/show.js | 22 ++++- .../consul-ui/app/serializers/topology.js | 18 ++++ .../consul-ui/app/styles/components.scss | 1 + .../templates/dc/services/show/topology.hbs | 2 + ui/packages/consul-ui/package.json | 1 + .../integration/serializers/topology-test.js | 4 +- ui/yarn.lock | 23 +++++ 21 files changed, 302 insertions(+), 88 deletions(-) delete mode 100644 ui/packages/consul-ui/app/components/topology-metrics/icon.hbs create mode 100644 ui/packages/consul-ui/app/components/topology-metrics/popover/index.hbs create mode 100644 ui/packages/consul-ui/app/components/topology-metrics/popover/index.js create mode 100644 ui/packages/consul-ui/app/components/topology-metrics/popover/index.scss diff --git a/ui/packages/consul-ui/app/components/topology-metrics/card.hbs b/ui/packages/consul-ui/app/components/topology-metrics/card.hbs index fe551d45bf..473de730dc 100644 --- a/ui/packages/consul-ui/app/components/topology-metrics/card.hbs +++ b/ui/packages/consul-ui/app/components/topology-metrics/card.hbs @@ -1,11 +1,10 @@ -{{#each @items as |item|}}

- {{item.Name}} + {{@item.Name}}

{{#if (and (and nspace (env 'CONSUL_NSPACES_ENABLED')) @type)}} @@ -16,12 +15,12 @@
- {{item.Namespace}} + {{@item.Namespace}}
{{/if}} -{{#if (eq item.Datacenter @dc)}} - {{#let (service/health-percentage item) as |percentage|}} +{{#if (eq @item.Datacenter @dc)}} + {{#let (service/health-percentage @item) as |percentage|}} {{#if (not-eq percentage.passing 0)}} {{percentage.passing}}% {{/if}} @@ -50,17 +49,16 @@ {{else}} {{/if}} {{/if}} -
-{{/each}} \ No newline at end of file + \ No newline at end of file diff --git a/ui/packages/consul-ui/app/components/topology-metrics/down-lines/index.hbs b/ui/packages/consul-ui/app/components/topology-metrics/down-lines/index.hbs index b1ab5a5e08..56200bcfb3 100644 --- a/ui/packages/consul-ui/app/components/topology-metrics/down-lines/index.hbs +++ b/ui/packages/consul-ui/app/components/topology-metrics/down-lines/index.hbs @@ -55,8 +55,11 @@ {{/if}} - +{{#each @items as |item|}} + +{{/each}} diff --git a/ui/packages/consul-ui/app/components/topology-metrics/icon.hbs b/ui/packages/consul-ui/app/components/topology-metrics/icon.hbs deleted file mode 100644 index df75295bd4..0000000000 --- a/ui/packages/consul-ui/app/components/topology-metrics/icon.hbs +++ /dev/null @@ -1,17 +0,0 @@ -{{#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))}} - - - An intention is set to 'deny' that prohibits these services from connecting. - - - {{else if item.Intention.HasPermissions}} - - - The intention between these services has Layer 7 permissions, so certain requests may or may not be permitted. - - - {{/if}} -{{/let}} -{{/each}} \ No newline at end of file diff --git a/ui/packages/consul-ui/app/components/topology-metrics/index.hbs b/ui/packages/consul-ui/app/components/topology-metrics/index.hbs index 9b16546288..6425c23228 100644 --- a/ui/packages/consul-ui/app/components/topology-metrics/index.hbs +++ b/ui/packages/consul-ui/app/components/topology-metrics/index.hbs @@ -1,6 +1,10 @@ {{on-window 'resize' (action this.calculate)}} -
+
{{#if (gt @topology.Downstreams.length 0)}}
@@ -11,13 +15,15 @@
+ {{#each @topology.Downstreams as |item|}} + {{/each}}
{{/if}}
@@ -52,25 +58,29 @@
+ @oncreate={{action @oncreate}} + />
{{#if (gt @topology.Upstreams.length 0)}}
{{#each-in (group-by "Datacenter" @topology.Upstreams) as |dc upstreams|}}

{{dc}}

+ {{#each upstreams as |item|}} + {{/each}}
{{/each-in}}
@@ -78,10 +88,12 @@
+ @oncreate={{action @oncreate}} + />
\ No newline at end of file diff --git a/ui/packages/consul-ui/app/components/topology-metrics/index.js b/ui/packages/consul-ui/app/components/topology-metrics/index.js index aeef7647f7..cb2846e27b 100644 --- a/ui/packages/consul-ui/app/components/topology-metrics/index.js +++ b/ui/packages/consul-ui/app/components/topology-metrics/index.js @@ -38,7 +38,7 @@ export default class TopologyMetrics extends Component { drawDownLines(items) { const order = ['allow', 'deny']; const dest = { - x: this.centerDimensions.x, + x: this.centerDimensions.x - 7, y: this.centerDimensions.y + this.centerDimensions.height / 2, }; @@ -65,7 +65,7 @@ export default class TopologyMetrics extends Component { drawUpLines(items) { const order = ['allow', 'deny']; const src = { - x: this.centerDimensions.x + 20, + x: this.centerDimensions.x + 5.5, y: this.centerDimensions.y + this.centerDimensions.height / 2, }; @@ -73,7 +73,7 @@ export default class TopologyMetrics extends Component { .map(item => { const dimensions = item.getBoundingClientRect(); const dest = { - x: dimensions.x - dimensions.width - 26, + x: dimensions.x - dimensions.width - 25, y: dimensions.y + dimensions.height / 2, }; diff --git a/ui/packages/consul-ui/app/components/topology-metrics/layout.scss b/ui/packages/consul-ui/app/components/topology-metrics/layout.scss index 771201b721..97992e4e43 100644 --- a/ui/packages/consul-ui/app/components/topology-metrics/layout.scss +++ b/ui/packages/consul-ui/app/components/topology-metrics/layout.scss @@ -2,35 +2,40 @@ display: grid; height: 100%; align-items: start; - grid-template-columns: 2fr 20px 1fr 20px 2fr 20px 1fr 20px 2fr; + grid-template-columns: 2fr 1fr 2fr 1fr 2fr; grid-template-rows: 50px 1fr 50px; + grid-template-areas: + "down-cards down-lines . up-lines up-cards" + "down-cards down-lines metrics up-lines up-cards" + "down-cards down-lines . up-lines up-cards"; } // Grid Layout #downstream-container { - grid-row: 1 / 3; - grid-column: 1 / 3; + grid-area: down-cards; } #downstream-lines { - grid-row: 1 / 3; - grid-column: 2 / 5; + grid-area: down-lines; } #upstream-lines { - grid-row: 1 / 3; - grid-column: 6 / 9; + grid-area: up-lines; } #upstream-column { - grid-row: 1 / 3; - grid-column: 8 / 10; + grid-area: up-cards; } // Columns/Containers & Lines #downstream-lines, #upstream-lines { - z-index: 1; position: relative; height: 100%; } +#downstream-lines { + margin-left: -20px; +} +#upstream-lines { + margin-right: -20px; +} #downstream-container, #upstream-container { padding: 12px; @@ -86,8 +91,7 @@ // Metrics Container #metrics-container { - grid-row: 2 / 3; - grid-column: 4 / 7; + grid-area: metrics; div:first-child { padding: 12px; diff --git a/ui/packages/consul-ui/app/components/topology-metrics/popover/index.hbs b/ui/packages/consul-ui/app/components/topology-metrics/popover/index.hbs new file mode 100644 index 0000000000..ed1c2d66fe --- /dev/null +++ b/ui/packages/consul-ui/app/components/topology-metrics/popover/index.hbs @@ -0,0 +1,71 @@ +
+{{#if (and (not @item.Intention.Allowed) (not @item.Intention.HasPermissions))}} + + {{/if}} + +
+ + +{{else if @item.Intention.HasPermissions}} + +
+ + +{{/if}} +
+ + diff --git a/ui/packages/consul-ui/app/components/topology-metrics/popover/index.js b/ui/packages/consul-ui/app/components/topology-metrics/popover/index.js new file mode 100644 index 0000000000..d84750af59 --- /dev/null +++ b/ui/packages/consul-ui/app/components/topology-metrics/popover/index.js @@ -0,0 +1,14 @@ +import Component from '@glimmer/component'; +import { action } from '@ember/object'; +import { tracked } from '@glimmer/tracking'; + +export default class TopoloyMetricsButton extends Component { + // =attributes + @tracked showToggleablePopover = false; + + // =actions + @action + togglePopover() { + this.showToggleablePopover = !this.showToggleablePopover; + } +} diff --git a/ui/packages/consul-ui/app/components/topology-metrics/popover/index.scss b/ui/packages/consul-ui/app/components/topology-metrics/popover/index.scss new file mode 100644 index 0000000000..daa3282b6e --- /dev/null +++ b/ui/packages/consul-ui/app/components/topology-metrics/popover/index.scss @@ -0,0 +1,87 @@ +.topology-metrics-popover { + .deny-target, + .L7-target { + transform: translate(-50%,-50%); + position: absolute; + background-color: $white; + padding: 1px 2px; + } + .deny-target:hover, + .L7-target:hover { + cursor:pointer; + } + .deny-target:active, + .deny-target:focus, + .L7-target:active, + .L7-target:focus { + outline: none; + } + .deny-target::before { + @extend %with-cancel-square-fill-color-mask, %as-pseudo; + background-color: $red-500; + } + .L7-target::before { + @extend %with-layers-mask, %as-pseudo; + background-color: $gray-300; + } + .body { + padding: 12px 12px 16px 25px; + h3 { + font-size: 14px; + padding-bottom: 4px; + } + p { + font-size: 12px; + } + } + .actions { + border-top: 1px solid $gray-300; + width: 100%; + display: inline-flex; + a, + button { + width: 50%; + height: 36px; + padding: 10px 0; + font-weight: $typo-weight-medium; + text-align: center; + line-height: normal; + } + button:first-child { + color: $blue-500; + } + button:nth-child(2) { + color: $gray-800; + } + button:hover { + cursor: pointer; + } + } + .ember-popover { + padding: 0; + width: 200px; + z-index: 3 !important; + } +} +.L7-popover { + .body { + background-color: $white; + } + h3 { + margin-left: -20px; + color: $blue-500; + } + h3::before { + @extend %with-info-circle-fill-mask, %as-pseudo; + color: $blue-500; + margin-right: 4px; + } +} +.deny-popover { + .body { + background-color: $white; + } + h3 { + color: $red-800; + } +} \ No newline at end of file diff --git a/ui/packages/consul-ui/app/components/topology-metrics/series/layout.scss b/ui/packages/consul-ui/app/components/topology-metrics/series/layout.scss index 9a07002ee9..c0a763095d 100644 --- a/ui/packages/consul-ui/app/components/topology-metrics/series/layout.scss +++ b/ui/packages/consul-ui/app/components/topology-metrics/series/layout.scss @@ -4,7 +4,6 @@ padding: 0; margin: 0; height: 70px; - z-index: 2; svg.sparkline { width: 100%; @@ -16,7 +15,7 @@ .tooltip { visibility: hidden; position: absolute; - z-index: 100; + z-index: 10; bottom: 78px; width: 217px; } diff --git a/ui/packages/consul-ui/app/components/topology-metrics/skin.scss b/ui/packages/consul-ui/app/components/topology-metrics/skin.scss index 7fd9589740..5a1372c4a4 100644 --- a/ui/packages/consul-ui/app/components/topology-metrics/skin.scss +++ b/ui/packages/consul-ui/app/components/topology-metrics/skin.scss @@ -123,22 +123,3 @@ 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; - } -} \ No newline at end of file diff --git a/ui/packages/consul-ui/app/components/topology-metrics/stats/index.scss b/ui/packages/consul-ui/app/components/topology-metrics/stats/index.scss index 5bd4056e75..8df203eaa4 100644 --- a/ui/packages/consul-ui/app/components/topology-metrics/stats/index.scss +++ b/ui/packages/consul-ui/app/components/topology-metrics/stats/index.scss @@ -1,15 +1,13 @@ .stats { - padding: 12px; + padding: 12px 12px 0 12px; display: flex; flex-flow: row wrap; justify-content: space-between; align-items: stretch; width: 100%; - height: 46px; - overflow: hidden; dl { display:flex; - margin-bottom: 50px; // pushes wrapped metrics well out of the bounding box to hide them. + padding-bottom: 12px; } dt { margin-right: 5px; diff --git a/ui/packages/consul-ui/app/components/topology-metrics/up-lines/index.hbs b/ui/packages/consul-ui/app/components/topology-metrics/up-lines/index.hbs index 4987ce8211..41bbfacd93 100644 --- a/ui/packages/consul-ui/app/components/topology-metrics/up-lines/index.hbs +++ b/ui/packages/consul-ui/app/components/topology-metrics/up-lines/index.hbs @@ -55,7 +55,10 @@ {{/if}} - +{{#each @items as |item|}} + +{{/each}} diff --git a/ui/packages/consul-ui/app/components/topology-metrics/up-lines/index.js b/ui/packages/consul-ui/app/components/topology-metrics/up-lines/index.js index 81544cfb82..3c622e5490 100644 --- a/ui/packages/consul-ui/app/components/topology-metrics/up-lines/index.js +++ b/ui/packages/consul-ui/app/components/topology-metrics/up-lines/index.js @@ -2,7 +2,7 @@ import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; -export default class TopoloyMetricsUpLines extends Component { +export default class TopologyMetricsUpLines extends Component { @tracked iconPositions; @action diff --git a/ui/packages/consul-ui/app/routes/dc/services/show.js b/ui/packages/consul-ui/app/routes/dc/services/show.js index d6ca70b180..b3fa4c15e1 100644 --- a/ui/packages/consul-ui/app/routes/dc/services/show.js +++ b/ui/packages/consul-ui/app/routes/dc/services/show.js @@ -2,13 +2,27 @@ import { inject as service } from '@ember/service'; import Route from 'consul-ui/routing/route'; import { hash } from 'rsvp'; import { get } from '@ember/object'; +import { action, setProperties } from '@ember/object'; export default class ShowRoute extends Route { - @service('data-source/service') - data; + @service('data-source/service') data; + @service('repository/intention') repo; + @service('ui-config') config; - @service('ui-config') - config; + @action + async createIntention(source, destination) { + const intention = service.Intention; + const model = this.repo.create({ + Datacenter: source.Datacenter, + SourceName: source.Name, + SourceNS: source.Namespace || 'default', + DestinationName: destination.Name, + DestinationNS: destination.Namespace || 'default', + Action: 'allow', + }); + await this.repo.persist(model); + this.refresh(); + } model(params, transition) { const dc = this.modelFor('dc').dc.Name; diff --git a/ui/packages/consul-ui/app/serializers/topology.js b/ui/packages/consul-ui/app/serializers/topology.js index bf9b33de7d..05ff6a2891 100644 --- a/ui/packages/consul-ui/app/serializers/topology.js +++ b/ui/packages/consul-ui/app/serializers/topology.js @@ -1,13 +1,31 @@ import Serializer from './application'; import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/topology'; +import { inject as service } from '@ember/service'; export default class TopologySerializer extends Serializer { + @service('store') store; + primaryKey = PRIMARY_KEY; slugKey = SLUG_KEY; respondForQueryRecord(respond, query) { + const intentionSerializer = this.store.serializerFor('intention'); return super.respondForQueryRecord(function(cb) { return respond(function(headers, body) { + body.Downstreams.forEach(item => { + item.Intention.SourceName = item.Name; + item.Intention.SourceNS = item.Namespace; + item.Intention.DestinationName = query.id; + item.Intention.DestinationNS = query.ns || 'default'; + intentionSerializer.ensureID(item.Intention); + }); + body.Upstreams.forEach(item => { + item.Intention.SourceName = query.id; + item.Intention.SourceNS = query.ns || 'default'; + item.Intention.DestinationName = item.Name; + item.Intention.DestinationNS = item.Namespace; + intentionSerializer.ensureID(item.Intention); + }); return cb(headers, { ...body, [SLUG_KEY]: query.id, diff --git a/ui/packages/consul-ui/app/styles/components.scss b/ui/packages/consul-ui/app/styles/components.scss index 87b26968b7..c91c69bfe3 100644 --- a/ui/packages/consul-ui/app/styles/components.scss +++ b/ui/packages/consul-ui/app/styles/components.scss @@ -62,6 +62,7 @@ @import 'consul-ui/components/consul/intention'; @import 'consul-ui/components/role-selector'; @import 'consul-ui/components/topology-metrics'; +@import 'consul-ui/components/topology-metrics/popover'; @import 'consul-ui/components/topology-metrics/series'; @import 'consul-ui/components/topology-metrics/stats'; @import 'consul-ui/components/topology-metrics/status'; diff --git a/ui/packages/consul-ui/app/templates/dc/services/show/topology.hbs b/ui/packages/consul-ui/app/templates/dc/services/show/topology.hbs index 9f691dc40f..4a6fdaa29c 100644 --- a/ui/packages/consul-ui/app/templates/dc/services/show/topology.hbs +++ b/ui/packages/consul-ui/app/templates/dc/services/show/topology.hbs @@ -11,6 +11,8 @@ Datacenter=dc Service=items.firstObject )}} + + @oncreate={{route-action 'createIntention'}} /> {{else}} diff --git a/ui/packages/consul-ui/package.json b/ui/packages/consul-ui/package.json index eb6226a972..b031d10465 100644 --- a/ui/packages/consul-ui/package.json +++ b/ui/packages/consul-ui/package.json @@ -123,6 +123,7 @@ "ember-ref-modifier": "^1.0.0", "ember-render-helpers": "^0.1.1", "ember-resolver": "^8.0.0", + "ember-route-action-helper": "^2.0.8", "ember-router-helpers": "^0.4.0", "ember-sinon-qunit": "5.0.0", "ember-source": "~3.20.5", diff --git a/ui/packages/consul-ui/tests/integration/serializers/topology-test.js b/ui/packages/consul-ui/tests/integration/serializers/topology-test.js index aed5be4cb9..4806908f20 100644 --- a/ui/packages/consul-ui/tests/integration/serializers/topology-test.js +++ b/ui/packages/consul-ui/tests/integration/serializers/topology-test.js @@ -10,8 +10,9 @@ module('Integration | Serializer | topology', function(hooks) { const serializer = this.owner.lookup('serializer:topology'); const dc = 'dc-1'; const id = 'slug'; + const kind = ''; const request = { - url: `/v1/internal/ui/service-topology/${id}?dc=${dc}`, + url: `/v1/internal/ui/service-topology/${id}?dc=${dc}&kind=${kind}`, }; return get(request.url).then(function(payload) { const expected = { @@ -28,6 +29,7 @@ module('Integration | Serializer | topology', function(hooks) { { dc: dc, id: id, + kind: kind, } ); assert.equal(actual.Datacenter, expected.Datacenter); diff --git a/ui/yarn.lock b/ui/yarn.lock index c287fc680b..2fabb5364d 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -8031,6 +8031,13 @@ ember-export-application-global@^2.0.1: resolved "https://registry.yarnpkg.com/ember-export-application-global/-/ember-export-application-global-2.0.1.tgz#b120a70e322ab208defc9e2daebe8d0dfc2dcd46" integrity sha512-B7wiurPgsxsSGzJuPFkpBWnaeuCu2PGpG2BjyrfA1VcL7//o+5RSnZqiCEY326y7qmxb2GoCgo0ft03KBU0rRw== +ember-factory-for-polyfill@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/ember-factory-for-polyfill/-/ember-factory-for-polyfill-1.3.1.tgz#b446ed64916d293c847a4955240eb2c993b86eae" + integrity sha512-y3iG2iCzH96lZMTWQw6LWNLAfOmDC4pXKbZP6FxG8lt7GGaNFkZjwsf+Z5GAe7kxfD7UG4lVkF7x37K82rySGA== + dependencies: + ember-cli-version-checker "^2.1.0" + ember-get-config@^0.2.4: version "0.2.4" resolved "https://registry.yarnpkg.com/ember-get-config/-/ember-get-config-0.2.4.tgz#118492a2a03d73e46004ed777928942021fe1ecd" @@ -8039,6 +8046,14 @@ ember-get-config@^0.2.4: broccoli-file-creator "^1.1.1" ember-cli-babel "^6.3.0" +ember-getowner-polyfill@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/ember-getowner-polyfill/-/ember-getowner-polyfill-2.2.0.tgz#38e7dccbcac69d5ec694000329ec0b2be651d2b2" + integrity sha512-rwGMJgbGzxIAiWYjdpAh04Abvt0s3HuS/VjHzUFhVyVg2pzAuz45B9AzOxYXzkp88vFC7FPaiA4kE8NxNk4A4Q== + dependencies: + ember-cli-version-checker "^2.1.0" + ember-factory-for-polyfill "^1.3.1" + ember-href-to@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/ember-href-to/-/ember-href-to-3.1.0.tgz#704f66c2b555a2685fac9ddc74eb9c95abaf5b8f" @@ -8222,6 +8237,14 @@ ember-rfc176-data@^0.3.12, ember-rfc176-data@^0.3.13, ember-rfc176-data@^0.3.16: resolved "https://registry.yarnpkg.com/ember-rfc176-data/-/ember-rfc176-data-0.3.16.tgz#2ace0ac9cf9016d493a74a1d931643a308679803" integrity sha512-IYAzffS90r2ybAcx8c2qprYfkxa70G+/UPkxMN1hw55DU5S2aLOX6v3umKDZItoRhrvZMCnzwsdfKSrKdC9Wbg== +ember-route-action-helper@^2.0.8: + version "2.0.8" + resolved "https://registry.yarnpkg.com/ember-route-action-helper/-/ember-route-action-helper-2.0.8.tgz#f227fcccb73e839b65e9b814e241b322fe8c02fc" + integrity sha512-V+4uKwqaYveriVt2rl4e+9mzHJiQOr1B8dCPQQ2TS3iAcmi5RD2giRDFGtCK9d2XY9Arb/f9hJh0obP20iyt3A== + dependencies: + ember-cli-babel "^6.8.1" + ember-getowner-polyfill "^2.0.0" + ember-router-generator@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ember-router-generator/-/ember-router-generator-2.0.0.tgz#d04abfed4ba8b42d166477bbce47fccc672dbde0"