diff --git a/.changelog/14947.txt b/.changelog/14947.txt new file mode 100644 index 0000000000..844e2f1617 --- /dev/null +++ b/.changelog/14947.txt @@ -0,0 +1,3 @@ +```release-note:feature +ui: Create peerings detail page +``` diff --git a/ui/GNUmakefile b/ui/GNUmakefile index 290bda9aed..ab70bbc61b 100644 --- a/ui/GNUmakefile +++ b/ui/GNUmakefile @@ -38,6 +38,5 @@ deps: clean dist-vercel: clean mkdir -p dist/ui && \ cd packages/consul-ui && \ - CONSUL_UI_INSTALL_FLAGS=--focus \ $(MAKE) build-staging && \ mv dist/* ../../dist/ui diff --git a/ui/packages/consul-peerings/app/components/consul/peer/bento-box/index.hbs b/ui/packages/consul-peerings/app/components/consul/peer/bento-box/index.hbs new file mode 100644 index 0000000000..7ffe6e92d7 --- /dev/null +++ b/ui/packages/consul-peerings/app/components/consul/peer/bento-box/index.hbs @@ -0,0 +1,71 @@ + +
+
+
Status
+
+ +
+
+
+
Latest heartbeat
+
+ {{#if @peering.LastHeartbeat}} + {{#let (smart-date-format @peering.LastHeartbeat) as |smartDate|}} + + {{#if smartDate.isNearDate}} + {{smartDate.relative}} + {{else}} + {{smartDate.friendly}} + {{/if}} + {{/let}} + {{else}} + None yet + {{/if}} +
+
+
+
Latest receipt
+
+ {{#if @peering.LastReceive}} + {{#let (smart-date-format @peering.LastReceive) as |smartDate|}} + {{#if smartDate.isNearDate}} + {{smartDate.relative}} + {{else}} + {{smartDate.friendly}} + {{/if}} + {{/let}} + {{else}} + None yet + {{/if}} +
+
+
+
Latest send
+
+ {{#if @peering.LastSend}} + {{#let (smart-date-format @peering.LastSend) as |smartDate|}} + {{#if smartDate.isNearDate}} + {{smartDate.relative}} + {{else}} + {{smartDate.friendly}} + {{/if}} + {{/let}} + {{else}} + None yet + {{/if}} +
+ +
+
+
diff --git a/ui/packages/consul-peerings/app/components/consul/peer/list/index.hbs b/ui/packages/consul-peerings/app/components/consul/peer/list/index.hbs index d31e1fa15a..b743baee6d 100644 --- a/ui/packages/consul-peerings/app/components/consul/peer/list/index.hbs +++ b/ui/packages/consul-peerings/app/components/consul/peer/list/index.hbs @@ -2,13 +2,22 @@ class="consul-peer-list" ...attributes @items={{@items}} -as |item index|> + @linkable="linkable peers" + as |item index| +> -

- {{item.Name}} -

+ {{#if (can "delete peer" item=item)}} + + {{item.Name}} + + {{else}} +

+ {{item.Name}} +

+ {{/if}}
@@ -16,24 +25,22 @@ as |item index|>
- {{t 'routes.dc.peers.index.detail.imported.count' + {{t + "routes.dc.peers.index.detail.imported.count" count=(format-number item.ImportedServiceCount) }}
- {{t 'routes.dc.peers.index.detail.exported.count' + {{t + "routes.dc.peers.index.detail.exported.count" count=(format-number item.ExportedServiceCount) }}
@@ -41,47 +48,51 @@ as |item index|>
-{{#if (can 'delete peer' item=item)}} + {{#if (can "delete peer" item=item)}} - -{{#if (can "write peer" item=item)}} + + {{#if (and (can "write peer" item=item) item.isDialer)}} + + + Regenerate token + + + {{/if}} - Regenerate token + View -{{/if}} - - - Delete - - - - - Confirm delete - - -

- Are you sure you want to delete this peer? -

-
- - - Delete - - -
-
-
-
-{{/if}} + + + Delete + + + + + Confirm delete + + +

+ Are you sure you want to delete this peer? +

+
+ + + Delete + + +
+
+
+
+ {{/if}}
- - + \ No newline at end of file diff --git a/ui/packages/consul-peerings/app/components/consul/peer/list/test-support.js b/ui/packages/consul-peerings/app/components/consul/peer/list/test-support.js index 126e444b9b..04507f0943 100644 --- a/ui/packages/consul-peerings/app/components/consul/peer/list/test-support.js +++ b/ui/packages/consul-peerings/app/components/consul/peer/list/test-support.js @@ -1,19 +1,19 @@ export const selectors = { - $: '.consul-peer-list', + $: ".consul-peer-list", collection: { - $: '[data-test-list-row]', + $: "[data-test-list-row]", peer: { - $: 'li', + $: "li", name: { - $: '[data-test-peer]' - } + $: "[data-test-peer]", + }, }, - } + }, }; export default (collection, isPresent, attribute, actions) => () => { return collection(`${selectors.$} ${selectors.collection.$}`, { peer: isPresent(selectors.collection.peer.$), - name: attribute('data-test-peer', selectors.collection.peer.name.$), - ...actions(['regenerate', 'delete']), + name: attribute("data-test-peer", selectors.collection.peer.name.$), + ...actions(["regenerate", "delete", "view"]), }); }; diff --git a/ui/packages/consul-peerings/app/controllers/dc/peers/show/exported.js b/ui/packages/consul-peerings/app/controllers/dc/peers/show/exported.js new file mode 100644 index 0000000000..59e61c489c --- /dev/null +++ b/ui/packages/consul-peerings/app/controllers/dc/peers/show/exported.js @@ -0,0 +1,17 @@ +import Controller from "@ember/controller"; +import { tracked } from "@glimmer/tracking"; +import { action } from "@ember/object"; + +export default class PeersEditExportedController extends Controller { + queryParams = { + search: { + as: "filter", + }, + }; + + @tracked search = ""; + + @action updateSearch(value) { + this.search = value; + } +} diff --git a/ui/packages/consul-peerings/app/controllers/dc/peers/show/index.js b/ui/packages/consul-peerings/app/controllers/dc/peers/show/index.js new file mode 100644 index 0000000000..1c63d8407d --- /dev/null +++ b/ui/packages/consul-peerings/app/controllers/dc/peers/show/index.js @@ -0,0 +1,11 @@ +import Controller from "@ember/controller"; +import { inject as service } from "@ember/service"; +import { action } from "@ember/object"; + +export default class DcPeersEditIndexController extends Controller { + @service router; + + @action transitionToImported() { + this.router.replaceWith("dc.peers.show.imported"); + } +} diff --git a/ui/packages/consul-peerings/app/templates/dc/peers/edit.hbs b/ui/packages/consul-peerings/app/templates/dc/peers/edit.hbs deleted file mode 100644 index 32753ec3a1..0000000000 --- a/ui/packages/consul-peerings/app/templates/dc/peers/edit.hbs +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - - - -{{#let - - route.params.dc - route.params.partition - route.params.nspace - - loader.data -as |dc partition nspace item|}} - - -
    -
  1. All Peers
  2. -
-
- -

- -

-
- - - - {{outlet}} - - -
-{{/let}} -
-
-
diff --git a/ui/packages/consul-peerings/app/templates/dc/peers/edit/addresses.hbs b/ui/packages/consul-peerings/app/templates/dc/peers/edit/addresses.hbs deleted file mode 100644 index 8e100066b5..0000000000 --- a/ui/packages/consul-peerings/app/templates/dc/peers/edit/addresses.hbs +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/ui/packages/consul-peerings/app/templates/dc/peers/edit/index.hbs b/ui/packages/consul-peerings/app/templates/dc/peers/edit/index.hbs deleted file mode 100644 index 40663165a2..0000000000 --- a/ui/packages/consul-peerings/app/templates/dc/peers/edit/index.hbs +++ /dev/null @@ -1,6 +0,0 @@ - - {{did-insert (route-action 'replaceWith' 'dc.peers.edit.addresses')}} - - diff --git a/ui/packages/consul-peerings/app/templates/dc/peers/show.hbs b/ui/packages/consul-peerings/app/templates/dc/peers/show.hbs new file mode 100644 index 0000000000..25177b78e6 --- /dev/null +++ b/ui/packages/consul-peerings/app/templates/dc/peers/show.hbs @@ -0,0 +1,59 @@ + + + + + + + + + {{#let + route.params.dc + route.params.partition + route.params.nspace + loader.data + as |dc partition nspace item| + }} + + +
    +
  1. All Peers
  2. +
+
+ +

+ +

+
+ + + + + + + + {{outlet}} + + +
+ {{/let}} +
+
+
\ No newline at end of file diff --git a/ui/packages/consul-peerings/app/templates/dc/peers/show/addresses.hbs b/ui/packages/consul-peerings/app/templates/dc/peers/show/addresses.hbs new file mode 100644 index 0000000000..4e179dc320 --- /dev/null +++ b/ui/packages/consul-peerings/app/templates/dc/peers/show/addresses.hbs @@ -0,0 +1,38 @@ + + {{#if (gt route.model.items.length 0)}} + + {{else}} + + +

+ {{t "routes.dc.peers.show.addresses.empty.header"}} +

+
+ + {{t "routes.dc.peers.show.addresses.empty.body" htmlSafe=true}} + + + + + +
+ {{/if}} +
\ No newline at end of file diff --git a/ui/packages/consul-peerings/app/templates/dc/peers/show/exported.hbs b/ui/packages/consul-peerings/app/templates/dc/peers/show/exported.hbs new file mode 100644 index 0000000000..10af30503c --- /dev/null +++ b/ui/packages/consul-peerings/app/templates/dc/peers/show/exported.hbs @@ -0,0 +1,134 @@ + + + {{#let + (or route.params.partition route.model.user.token.Partition "default") + api.data + as |partition items| + }} + + + + + + + {{#if items.length}} + + {{/if}} + + + {{#if p.data.height}} +
+ {{#if search.data.items.length}} + +
  • + + {{service.Name}} + +
  • +
    + {{else}} + + +

    + {{t + "routes.dc.peers.show.exported.empty.header" + name=route.model.peer.Name + }} +

    +
    + + {{t + "routes.dc.peers.show.exported.empty.body" + items=items.length + name=route.model.peer.Name + htmlSafe=true + }} + + + + + +
    + {{/if}} +
    + {{/if}} +
    + +
    +
    + {{/let}} +
    + +
    \ No newline at end of file diff --git a/ui/packages/consul-peerings/app/templates/dc/peers/show/imported.hbs b/ui/packages/consul-peerings/app/templates/dc/peers/show/imported.hbs new file mode 100644 index 0000000000..2286f0dc3d --- /dev/null +++ b/ui/packages/consul-peerings/app/templates/dc/peers/show/imported.hbs @@ -0,0 +1,133 @@ + + + + + + + + + {{#let + (hash + value=(or sortBy "Status:asc") + change=(action (mut sortBy) value="target.selected") + ) + (hash + status=(hash + value=(if status (split status ",") undefined) + change=(action (mut status) value="target.selectedItems") + ) + kind=(hash + value=(if kind (split kind ",") undefined) + change=(action (mut kind) value="target.selectedItems") + ) + source=(hash + value=(if source (split source ",") undefined) + change=(action (mut source) value="target.selectedItems") + ) + searchproperty=(hash + value=(if + (not-eq searchproperty undefined) + (split searchproperty ",") + this.searchProperties + ) + change=(action (mut searchproperty) value="target.selectedItems") + default=this.searchProperties + ) + ) + (reject-by "Kind" "connect-proxy" api.data) + (or route.params.partition route.model.user.token.Partition "default") + (or route.params.nspace route.model.user.token.Namespace "default") + as |sort filters items partition nspace| + }} + + {{#if (gt items.length 0)}} + {{#let (collection items) as |items|}} + + {{/let}} + {{/if}} + + + + + + + +

    + {{t + "routes.dc.peers.show.imported.empty.header" + name=route.model.peer.Name + }} +

    +
    + + {{t + "routes.dc.peers.show.imported.empty.body" + items=items.length + name=route.model.peer.Name + htmlSafe=true + }} + + + + + +
    +
    +
    + {{/let}} +
    +
    +
    \ No newline at end of file diff --git a/ui/packages/consul-peerings/app/templates/dc/peers/show/index.hbs b/ui/packages/consul-peerings/app/templates/dc/peers/show/index.hbs new file mode 100644 index 0000000000..25acb7e4c2 --- /dev/null +++ b/ui/packages/consul-peerings/app/templates/dc/peers/show/index.hbs @@ -0,0 +1,3 @@ + + {{did-insert this.transitionToImported}} + \ No newline at end of file diff --git a/ui/packages/consul-peerings/vendor/consul-peerings/routes.js b/ui/packages/consul-peerings/vendor/consul-peerings/routes.js index fd2a92aca0..1f8675c1c3 100644 --- a/ui/packages/consul-peerings/vendor/consul-peerings/routes.js +++ b/ui/packages/consul-peerings/vendor/consul-peerings/routes.js @@ -1,30 +1,76 @@ -(routes => routes({ - dc: { - peers: { - _options: { - path: '/peers' - }, - index: { +((routes) => + routes({ + dc: { + peers: { _options: { - path: '/', - queryParams: { - sortBy: 'sort', - state: 'state', - searchproperty: { - as: 'searchproperty', - empty: [['Name', 'ID']], + path: "/peers", + }, + index: { + _options: { + path: "/", + queryParams: { + sortBy: "sort", + state: "state", + searchproperty: { + as: "searchproperty", + empty: [["Name", "ID"]], + }, + search: { + as: "filter", + replace: true, + }, }, - search: { - as: 'filter', - replace: true, + }, + }, + show: { + _options: { + path: "/:name", + }, + imported: { + _options: { + path: "/imported-services", + queryParams: { + sortBy: "sort", + status: "status", + source: "source", + kind: "kind", + searchproperty: { + as: "searchproperty", + empty: [["Name", "Tags"]], + }, + search: { + as: "filter", + replace: true, + }, + }, + }, + }, + exported: { + _options: { + path: "/exported-services", + queryParams: { + search: { + as: "filter", + replace: true, + }, + }, + }, + }, + addresses: { + _options: { + path: "/addresses", }, }, }, }, }, - }, -}))( - (json, data = (typeof document !== 'undefined' ? document.currentScript.dataset : module.exports)) => { + }))( + ( + json, + data = typeof document !== "undefined" + ? document.currentScript.dataset + : module.exports + ) => { data[`routes`] = JSON.stringify(json); } ); diff --git a/ui/packages/consul-ui/.docfy-config.js b/ui/packages/consul-ui/.docfy-config.js index bd85d31ff3..bb728972bb 100644 --- a/ui/packages/consul-ui/.docfy-config.js +++ b/ui/packages/consul-ui/.docfy-config.js @@ -96,6 +96,12 @@ module.exports = { urlSchema: 'auto', urlPrefix: 'docs/consul', }, + { + root: path.resolve(__dirname, 'app/components/providers'), + pattern: '**/README.mdx', + urlSchema: 'auto', + urlPrefix: 'docs/providers', + }, { root: `${path.dirname(require.resolve('consul-acls/package.json'))}/app/components`, pattern: '**/README.mdx', @@ -129,6 +135,7 @@ module.exports = { ].concat(user.sources), labels: { consul: 'Consul Components', + providers: 'Provider Components', ...user.labels, }, }; diff --git a/ui/packages/consul-ui/app/adapters/service.js b/ui/packages/consul-ui/app/adapters/service.js index bc15702ada..c867dff064 100644 --- a/ui/packages/consul-ui/app/adapters/service.js +++ b/ui/packages/consul-ui/app/adapters/service.js @@ -1,7 +1,7 @@ import Adapter from './application'; export default class ServiceAdapter extends Adapter { - requestForQuery(request, { dc, ns, partition, index, gateway, uri }) { + requestForQuery(request, { dc, ns, partition, index, gateway, uri, peer }) { if (typeof gateway !== 'undefined') { return request` GET /v1/internal/ui/gateway-services-nodes/${gateway}?${{ dc }} @@ -16,13 +16,14 @@ export default class ServiceAdapter extends Adapter { `; } else { return request` - GET /v1/internal/ui/services?${{ dc }} + GET /v1/internal/ui/services?${{ dc, peer }} X-Request-ID: ${uri} ${{ ns, partition, index, + peer, }} `; } diff --git a/ui/packages/consul-ui/app/components/consul/service/list/index.hbs b/ui/packages/consul-ui/app/components/consul/service/list/index.hbs index a97184b984..7991a11243 100644 --- a/ui/packages/consul-ui/app/components/consul/service/list/index.hbs +++ b/ui/packages/consul-ui/app/components/consul/service/list/index.hbs @@ -1,97 +1,89 @@ - +
    Health
    -
    - - {{#if (eq 'critical' item.MeshStatus)}} - At least one health check on one instance is failing. - {{else if (eq 'warning' item.MeshStatus)}} - At least one health check on one instance has a warning. - {{else if (eq 'passing' item.MeshStatus)}} - All health checks are passing. - {{else}} - There are no health checks. - {{/if}} - -
    +
    -{{#if (gt item.InstanceCount 0)}} - - {{item.Name}} - -{{else}} -

    - {{item.Name}} -

    -{{/if}} + {{#if (gt item.InstanceCount 0)}} + + {{item.Name}} + + {{else}} +

    + {{item.Name}} +

    + {{/if}}
    - + - {{#if (and (not-eq item.InstanceCount 0) (and (not-eq item.Kind 'terminating-gateway') (not-eq item.Kind 'ingress-gateway'))) }} - - {{format-number item.InstanceCount}} {{pluralize item.InstanceCount 'instance' without-count=true}} - - {{/if}} - - {{#if (eq item.Kind 'terminating-gateway')}} - - {{format-number item.GatewayConfig.AssociatedServiceCount}} {{pluralize item.GatewayConfig.AssociatedServiceCount 'linked service' without-count=true}} - - {{else if (eq item.Kind 'ingress-gateway')}} - - {{format-number item.GatewayConfig.AssociatedServiceCount}} {{pluralize item.GatewayConfig.AssociatedServiceCount 'upstream' without-count=true}} - - {{/if}} - {{#if (or item.ConnectedWithGateway item.ConnectedWithProxy)}} -
    -
    - - This service uses a proxy for the Consul service mesh - -
    - {{#if (and item.ConnectedWithGateway item.ConnectedWithProxy )}} -
    - in service mesh with proxy and gateway -
    - {{else if item.ConnectedWithProxy}} -
    - in service mesh with proxy -
    - {{else if item.ConnectedWithGateway}} -
    - in service mesh with gateway -
    - {{/if}} -
    - {{/if}} + {{#if + (and + (not-eq item.InstanceCount 0) + (and (not-eq item.Kind 'terminating-gateway') (not-eq item.Kind 'ingress-gateway')) + ) + }} + + {{format-number item.InstanceCount}} + {{pluralize item.InstanceCount 'instance' without-count=true}} + + {{/if}} + {{! we are displaying imported-services - don't show bucket-list }} + {{#unless @isPeerDetail}} + + {{/unless}} + {{#if (eq item.Kind 'terminating-gateway')}} + + {{format-number item.GatewayConfig.AssociatedServiceCount}} + {{pluralize item.GatewayConfig.AssociatedServiceCount 'linked service' without-count=true}} + + {{else if (eq item.Kind 'ingress-gateway')}} + + {{format-number item.GatewayConfig.AssociatedServiceCount}} + {{pluralize item.GatewayConfig.AssociatedServiceCount 'upstream' without-count=true}} + + {{/if}} + {{#if (or item.ConnectedWithGateway item.ConnectedWithProxy)}} +
    +
    + + This service uses a proxy for the Consul service mesh + +
    + {{#if (and item.ConnectedWithGateway item.ConnectedWithProxy)}} +
    + in service mesh with proxy and gateway +
    + {{else if item.ConnectedWithProxy}} +
    + in service mesh with proxy +
    + {{else if item.ConnectedWithGateway}} +
    + in service mesh with gateway +
    + {{/if}} +
    + {{/if}}
    \ No newline at end of file diff --git a/ui/packages/consul-ui/app/components/consul/service/search-bar/index.hbs b/ui/packages/consul-ui/app/components/consul/service/search-bar/index.hbs index a73d0b6cad..df7abdc695 100644 --- a/ui/packages/consul-ui/app/components/consul/service/search-bar/index.hbs +++ b/ui/packages/consul-ui/app/components/consul/service/search-bar/index.hbs @@ -1,195 +1,208 @@ - - <:status as |search|> + + <:status as |search|> -{{#let - - (t (concat "components.consul.service.search-bar." search.status.key) - default=(array - (concat "common.search." search.status.key) - (concat "common.consul." search.status.key) + {{#let + (t + (concat 'components.consul.service.search-bar.' search.status.key) + default=(array + (concat 'common.search.' search.status.key) (concat 'common.consul.' search.status.key) + ) ) - ) - - (t (concat "components.consul.service.search-bar." search.status.value) - default=(array - (concat "common.search." search.status.value) - (concat "common.consul." search.status.value) - (concat "common.brand." search.status.value) + (t + (concat 'components.consul.service.search-bar.' search.status.value) + default=(array + (concat 'common.search.' search.status.value) + (concat 'common.consul.' search.status.value) + (concat 'common.brand.' search.status.value) + ) ) - ) - -as |key value|}} - + as |key value| + }} +
    {{key}}
    {{value}}
    -{{/let}} + {{/let}} - - <:search as |search|> - + <:search as |search|> + + - - - - {{t "common.search.searchproperty"}} - - - - {{#let components.Optgroup components.Option as |Optgroup Option|}} + + + {{t 'common.search.searchproperty'}} + + + + {{#let components.Optgroup components.Option as |Optgroup Option|}} {{#each @filter.searchproperty.default as |prop|}} {{/each}} - {{/let}} - - - - - <:filter as |search|> - - - - {{t "common.consul.status"}} - - - - {{#let components.Optgroup components.Option as |Optgroup Option|}} - {{#each (array "passing" "warning" "critical" "empty") as |state|}} - - {{/each}} - {{/let}} + {{/let}} - - - - {{t "components.consul.service.search-bar.kind"}} - - - - {{#let components.Optgroup components.Option as |Optgroup Option|}} - + + <:filter as |search|> + + + + {{t 'common.consul.status'}} + + + + {{#let components.Optgroup components.Option as |Optgroup Option|}} + {{#each this.healthStates as |state|}} + + {{/each}} + {{/let}} + + + + + + {{t 'components.consul.service.search-bar.kind'}} + + + + {{#let components.Optgroup components.Option as |Optgroup Option|}} + - - {{#each (array "ingress-gateway" "terminating-gateway" "mesh-gateway") as |kind|}} - - {{/each}} + + {{#each (array 'ingress-gateway' 'terminating-gateway' 'mesh-gateway') as |kind|}} + + {{/each}} - - {{#each (array "in-mesh" "not-in-mesh") as |state|}} - - {{/each}} + + {{#each (array 'in-mesh' 'not-in-mesh') as |state|}} + + {{/each}} - {{/let}} - - - {{#if (gt @sources.length 0)}} + {{/let}} + + + {{#if (gt @sources.length 0)}} - + as |components| + > + - {{t "common.search.source"}} + {{t 'common.search.source'}} - - {{#let components.Option as |Option|}} -{{#if (gt @sources.length 0)}} - {{#each @sources as |source|}} - - {{/each}} - -{{/if}} - {{/let}} + + {{#let components.Option as |Option|}} + {{#if (gt @sources.length 0)}} + {{#each @sources as |source|}} + + {{/each}} + + {{/if}} + {{/let}} - {{/if}} - - <:sort as |search|> - - - - {{#let (from-entries (array - (array "Name:asc" (t "common.sort.alpha.asc")) - (array "Name:desc" (t "common.sort.alpha.desc")) - (array "Status:asc" (t "common.sort.status.asc")) - (array "Status:desc" (t "common.sort.status.desc")) - )) - as |selectable| - }} - {{get selectable @sort.value}} - {{/let}} - - - - {{#let components.Optgroup components.Option as |Optgroup Option|}} - - - + {{/if}} + + <:sort as |search|> + + + + {{#let + (from-entries + (array + (array 'Name:asc' (t 'common.sort.alpha.asc')) + (array 'Name:desc' (t 'common.sort.alpha.desc')) + (array 'Status:asc' (t 'common.sort.status.asc')) + (array 'Status:desc' (t 'common.sort.status.desc')) + ) + ) + as |selectable| + }} + {{get selectable @sort.value}} + {{/let}} + + + + {{#let components.Optgroup components.Option as |Optgroup Option|}} + + + - - - + + + - {{/let}} - - - -
    + {{/let}} + + + +
    \ No newline at end of file diff --git a/ui/packages/consul-ui/app/components/consul/service/search-bar/index.js b/ui/packages/consul-ui/app/components/consul/service/search-bar/index.js new file mode 100644 index 0000000000..0db51f3404 --- /dev/null +++ b/ui/packages/consul-ui/app/components/consul/service/search-bar/index.js @@ -0,0 +1,11 @@ +import Component from '@glimmer/component'; + +export default class ConsulServiceSearchBar extends Component { + get healthStates() { + if (this.args.peer) { + return ['passing', 'warning', 'critical', 'unknown', 'empty']; + } else { + return ['passing', 'warning', 'critical', 'empty']; + } + } +} diff --git a/ui/packages/consul-ui/app/components/icon-definition/index.scss b/ui/packages/consul-ui/app/components/icon-definition/index.scss index 9595850e50..262412ef22 100644 --- a/ui/packages/consul-ui/app/components/icon-definition/index.scss +++ b/ui/packages/consul-ui/app/components/icon-definition/index.scss @@ -25,6 +25,11 @@ @extend %with-minus-square-fill-mask, %as-pseudo; color: rgb(var(--tone-gray-500)); } +%icon-definition.unknown dt::before, +%composite-row-header .unknown dd::before { + @extend %with-help-circle-outline-mask, %as-pseudo; + color: rgb(var(--tone-gray-500)); +} %composite-row-header [rel='me'] dd::before { @extend %with-check-circle-fill-mask, %as-pseudo; diff --git a/ui/packages/consul-ui/app/components/outlet/index.js b/ui/packages/consul-ui/app/components/outlet/index.js index e63ec68e21..693cdbeed6 100644 --- a/ui/packages/consul-ui/app/components/outlet/index.js +++ b/ui/packages/consul-ui/app/components/outlet/index.js @@ -79,10 +79,14 @@ export default class Outlet extends Component { this.previousState = this.state; this.state = new State('loading'); this.endTransition = this.routlet.transition(); - // if we have no transition-duration set immediately end the transition - const duration = window - .getComputedStyle(this.element) - .getPropertyValue('transition-duration'); + let duration; + if (this.element) { + // if we have no transition-duration set immediately end the transition + duration = window.getComputedStyle(this.element).getPropertyValue('transition-duration'); + } else { + duration = 0; + } + if (parseFloat(duration) === 0) { this.endTransition(); } diff --git a/ui/packages/consul-ui/app/components/peerings/provider/index.hbs b/ui/packages/consul-ui/app/components/peerings/provider/index.hbs new file mode 100644 index 0000000000..c55695d4db --- /dev/null +++ b/ui/packages/consul-ui/app/components/peerings/provider/index.hbs @@ -0,0 +1 @@ +{{yield (hash data=this.data)}} \ No newline at end of file diff --git a/ui/packages/consul-ui/app/components/peerings/provider/index.js b/ui/packages/consul-ui/app/components/peerings/provider/index.js new file mode 100644 index 0000000000..23bd99d9b1 --- /dev/null +++ b/ui/packages/consul-ui/app/components/peerings/provider/index.js @@ -0,0 +1,41 @@ +import Component from '@glimmer/component'; +import { inject as service } from '@ember/service'; +import { getOwner } from '@ember/application'; +import { Tab } from 'consul-ui/components/tab-nav'; + +export default class PeeringsProvider extends Component { + @service router; + @service intl; + + get data() { + return { + tabs: this.tabs, + }; + } + + get tabs() { + const { peer } = this.args; + const { router } = this; + const owner = getOwner(this); + + const { isReceiver, Name: name } = peer; + let tabs = [ + { + label: 'Imported Services', + route: 'dc.peers.show.imported', + tooltip: this.intl.t('routes.dc.peers.index.detail.imported.tab-tooltip', { name }), + }, + { + label: 'Exported Services', + route: 'dc.peers.show.exported', + tooltip: this.intl.t('routes.dc.peers.index.detail.exported.tab-tooltip', { name }), + }, + ]; + + if (isReceiver) { + tabs = [...tabs, { label: 'Addresses', route: 'dc.peers.show.addresses' }]; + } + + return tabs.map((tab) => new Tab({ ...tab, currentRouteName: router.currentRouteName, owner })); + } +} diff --git a/ui/packages/consul-ui/app/components/popover-select/index.scss b/ui/packages/consul-ui/app/components/popover-select/index.scss index 90dd0fd27f..dad07b2cdd 100644 --- a/ui/packages/consul-ui/app/components/popover-select/index.scss +++ b/ui/packages/consul-ui/app/components/popover-select/index.scss @@ -42,6 +42,10 @@ @extend %with-minus-square-fill-mask, %as-pseudo; color: rgb(var(--tone-gray-400)); } +%popover-select .value-unknown button::before { + @extend %with-help-circle-outline-mask, %as-pseudo; + color: rgb(var(--tone-gray-500)); +} %popover-select.type-source li:not(.partition) button { text-transform: capitalize; } diff --git a/ui/packages/consul-ui/app/components/providers/dimension/README.mdx b/ui/packages/consul-ui/app/components/providers/dimension/README.mdx new file mode 100644 index 0000000000..fa2fcb7066 --- /dev/null +++ b/ui/packages/consul-ui/app/components/providers/dimension/README.mdx @@ -0,0 +1,43 @@ +# Providers::Dimension + +A provider component that helps you to make a container fill the remaining space of the viewport. +Usually, you would **use flexbox** to do so but for scenarios where this isn't possible you +can use this component to make it easy to take up the remaining space. + +```hbs + +
    + Fill remaining space +
    +
    +``` + +By default, this component will take up the remaining viewport space by taking the +top of itself as the top-boundary and the `footer[role="contentinfo"]` as the +bottom-boundary. In terms of Consul-UI this means _take up the entire space but +stop at the footer that holds the consul version info at the bottom of the screen_. + +You can pass a different `bottomBoundary` if need be: + +```hbs preview-template +
    +
    +
    + Boundary +
    +
    +
    + +
    + We could use flexbox here instead +
    +
    +
    +
    +``` diff --git a/ui/packages/consul-ui/app/components/providers/dimension/index.hbs b/ui/packages/consul-ui/app/components/providers/dimension/index.hbs new file mode 100644 index 0000000000..4d36322965 --- /dev/null +++ b/ui/packages/consul-ui/app/components/providers/dimension/index.hbs @@ -0,0 +1,4 @@ +
    + {{on-window 'resize' this.handleWindowResize}} + {{yield (hash data=this.data)}} +
    \ No newline at end of file diff --git a/ui/packages/consul-ui/app/components/providers/dimension/index.js b/ui/packages/consul-ui/app/components/providers/dimension/index.js new file mode 100644 index 0000000000..7a27eb37c8 --- /dev/null +++ b/ui/packages/consul-ui/app/components/providers/dimension/index.js @@ -0,0 +1,44 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; +import { ref } from 'ember-ref-bucket'; +import { htmlSafe } from '@ember/template'; + +export default class DimensionsProvider extends Component { + @ref('element') element; + + @tracked height; + + get data() { + const { height, fillRemainingHeightStyle } = this; + + return { + height, + fillRemainingHeightStyle, + }; + } + + get fillRemainingHeightStyle() { + return htmlSafe(`height: ${this.height}px;`); + } + + get bottomBoundary() { + return document.querySelector(this.args.bottomBoundary) || this.footer; + } + + get footer() { + return document.querySelector('footer[role="contentinfo"]'); + } + + @action measureDimensions(element) { + const bb = this.bottomBoundary.getBoundingClientRect(); + const e = element.getBoundingClientRect(); + this.height = bb.top + bb.height - e.top; + } + + @action handleWindowResize() { + const { element } = this; + + this.measureDimensions(element); + } +} diff --git a/ui/packages/consul-ui/app/components/providers/search/index.hbs b/ui/packages/consul-ui/app/components/providers/search/index.hbs new file mode 100644 index 0000000000..c55695d4db --- /dev/null +++ b/ui/packages/consul-ui/app/components/providers/search/index.hbs @@ -0,0 +1 @@ +{{yield (hash data=this.data)}} \ No newline at end of file diff --git a/ui/packages/consul-ui/app/components/providers/search/index.js b/ui/packages/consul-ui/app/components/providers/search/index.js new file mode 100644 index 0000000000..3cf9449198 --- /dev/null +++ b/ui/packages/consul-ui/app/components/providers/search/index.js @@ -0,0 +1,36 @@ +import Component from '@glimmer/component'; + +export default class SearchProvider extends Component { + // custom base route / router abstraction is doing weird things + get _search() { + return this.args.search || ''; + } + + get items() { + const { items, searchProperties } = this.args; + const { _search: search } = this; + + if (search.length > 0) { + return items.filter((item) => { + const matchesInSearchProperties = searchProperties.reduce((acc, searchProperty) => { + const match = item[searchProperty].indexOf(search) !== -1; + if (match) { + return [...acc, match]; + } else { + return acc; + } + }, []); + return matchesInSearchProperties.length > 0; + }); + } else { + return items; + } + } + + get data() { + const { items } = this; + return { + items, + }; + } +} diff --git a/ui/packages/consul-ui/app/components/tab-nav/index.hbs b/ui/packages/consul-ui/app/components/tab-nav/index.hbs index 6b8acd7651..236f692f7f 100644 --- a/ui/packages/consul-ui/app/components/tab-nav/index.hbs +++ b/ui/packages/consul-ui/app/components/tab-nav/index.hbs @@ -1,47 +1,50 @@ -{{#let - (dom-position (set this 'style') offset=true) - "tab" -as |select name|}} - + + {{/let}} \ No newline at end of file diff --git a/ui/packages/consul-ui/app/components/tab-nav/index.js b/ui/packages/consul-ui/app/components/tab-nav/index.js index c2afccbdf2..ac66f5ae45 100644 --- a/ui/packages/consul-ui/app/components/tab-nav/index.js +++ b/ui/packages/consul-ui/app/components/tab-nav/index.js @@ -1,4 +1,76 @@ import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { hrefTo } from 'consul-ui/helpers/href-to'; + +/** + * A class that encapsulates the data abstraction that we expect the TabNav to + * be passed as `@items`. + * + * You can use this class when you want to create tab-nav from javascript. + * + * Instead of doing this in the template layer: + * + * ```handlebars + * item.MeshStatus === value, critical: (item, value) => item.MeshStatus === value, empty: (item, value) => item.MeshChecksTotal === 0, + unknown: (item) => item.peerIsFailing || item.isZeroCountButPeered, }, instance: { registered: (item, value) => item.InstanceCount > 0, diff --git a/ui/packages/consul-ui/app/helpers/smart-date-format.js b/ui/packages/consul-ui/app/helpers/smart-date-format.js new file mode 100644 index 0000000000..45dce293c5 --- /dev/null +++ b/ui/packages/consul-ui/app/helpers/smart-date-format.js @@ -0,0 +1,38 @@ +import Helper from '@ember/component/helper'; +import { inject as service } from '@ember/service'; + +const MILLISECONDS_IN_DAY = 1000 * 60 * 60 * 24; +const MILLISECONDS_IN_WEEK = MILLISECONDS_IN_DAY * 7; +/** + * A function that returns if a date is within a week of the current time + * @param {*} date - the date to check + * + */ +function isNearDate(date) { + const now = new Date(); + const aWeekAgo = +now - MILLISECONDS_IN_WEEK; + const aWeekInFuture = +now + MILLISECONDS_IN_WEEK; + + return date >= aWeekAgo && date <= aWeekInFuture; +} + +export default class SmartDateFormat extends Helper { + @service temporal; + @service intl; + + compute([value], hash) { + return { + isNearDate: isNearDate(value), + relative: `${this.temporal.format(value)} ago`, + friendly: this.intl.formatTime(value, { + month: 'short', + day: 'numeric', + year: 'numeric', + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + hourCycle: 'h24', + }), + }; + } +} diff --git a/ui/packages/consul-ui/app/locations/fsm-with-optional.js b/ui/packages/consul-ui/app/locations/fsm-with-optional.js index 12657737b9..370ca47e0f 100644 --- a/ui/packages/consul-ui/app/locations/fsm-with-optional.js +++ b/ui/packages/consul-ui/app/locations/fsm-with-optional.js @@ -188,10 +188,14 @@ export default class FSMWithOptionalLocation { /** * Turns a routeName into a full URL string for anchor hrefs etc. */ - hrefTo(routeName, params, hash) { + hrefTo(routeName, params, _hash) { + // copy to always work with a new hash even when helper persists hash + const hash = { ..._hash }; + if (typeof hash.dc !== 'undefined') { delete hash.dc; } + if (typeof hash.nspace !== 'undefined') { hash.nspace = `~${hash.nspace}`; } diff --git a/ui/packages/consul-ui/app/models/peer.js b/ui/packages/consul-ui/app/models/peer.js index 1923e31aab..6695253d8f 100644 --- a/ui/packages/consul-ui/app/models/peer.js +++ b/ui/packages/consul-ui/app/models/peer.js @@ -17,17 +17,34 @@ export default class Peer extends Model { @attr('string') Name; @attr('string') State; @attr('string') ID; + + // only the side that establishes will hold this property + @attr('string') PeerID; + + @attr() PeerServerAddresses; + + // StreamStatus @nullValue([]) @attr() ImportedServices; @nullValue([]) @attr() ExportedServices; @attr('date') LastHeartbeat; @attr('date') LastReceive; @attr('date') LastSend; - @attr() PeerServerAddresses; get ImportedServiceCount() { return this.ImportedServices.length; } + get ExportedServiceCount() { return this.ExportedServices.length; } + + // if we receive a PeerID we know that we are dealing with the side that + // established the peering + get isReceiver() { + return this.PeerID; + } + + get isDialer() { + return !this.isReceiver; + } } diff --git a/ui/packages/consul-ui/app/models/service.js b/ui/packages/consul-ui/app/models/service.js index 70a753f44f..7b50315d77 100644 --- a/ui/packages/consul-ui/app/models/service.js +++ b/ui/packages/consul-ui/app/models/service.js @@ -1,4 +1,4 @@ -import Model, { attr } from '@ember-data/model'; +import Model, { attr, belongsTo } from '@ember-data/model'; import { computed } from '@ember/object'; import { tracked } from '@glimmer/tracking'; import { fragment } from 'ember-data-model-fragments/attributes'; @@ -58,6 +58,18 @@ export default class Service extends Model { @attr() meta; // {} + @belongsTo({ async: false }) peer; + + @computed('peer', 'InstanceCount') + get isZeroCountButPeered() { + return this.peer && this.InstanceCount === 0; + } + + @computed('peer.State') + get peerIsFailing() { + return this.peer && this.peer.State === 'FAILING'; + } + @computed('ChecksPassing', 'ChecksWarning', 'ChecksCritical') get ChecksTotal() { return this.ChecksPassing + this.ChecksWarning + this.ChecksCritical; @@ -79,9 +91,19 @@ export default class Service extends Model { return this.MeshEnabled || (this.Kind || '').length > 0; } - @computed('MeshChecksPassing', 'MeshChecksWarning', 'MeshChecksCritical') + @computed( + 'MeshChecksPassing', + 'MeshChecksWarning', + 'MeshChecksCritical', + 'isZeroCountButPeered', + 'peerIsFailing' + ) get MeshStatus() { switch (true) { + case this.isZeroCountButPeered: + return 'unknown'; + case this.peerIsFailing: + return 'unknown'; case this.MeshChecksCritical !== 0: return 'critical'; case this.MeshChecksWarning !== 0: @@ -93,6 +115,27 @@ export default class Service extends Model { } } + @computed('isZeroCountButPeered', 'peerIsFailing', 'MeshStatus') + get healthTooltipText() { + const { MeshStatus, isZeroCountButPeered, peerIsFailing } = this; + if (isZeroCountButPeered) { + return 'This service currently has 0 instances. Check with the operator of its peer to make sure this is expected behavior.'; + } + if (peerIsFailing) { + return 'This peer is out of sync, so the current health statuses of its services are unknown.'; + } + if (MeshStatus === 'critical') { + return 'At least one health check on one instance is failing.'; + } + if (MeshStatus === 'warning') { + return 'At least one health check on one instance has a warning.'; + } + if (MeshStatus == 'passing') { + return 'All health checks are passing.'; + } + return 'There are no health checks'; + } + @computed('ChecksPassing', 'Proxy.ChecksPassing') get MeshChecksPassing() { let proxyCount = 0; diff --git a/ui/packages/consul-ui/app/serializers/service.js b/ui/packages/consul-ui/app/serializers/service.js index 9caa9b2cdd..f8871fb189 100644 --- a/ui/packages/consul-ui/app/serializers/service.js +++ b/ui/packages/consul-ui/app/serializers/service.js @@ -1,6 +1,10 @@ import Serializer from './application'; import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/service'; import { get } from '@ember/object'; +import { + HEADERS_NAMESPACE as HTTP_HEADERS_NAMESPACE, + HEADERS_PARTITION as HTTP_HEADERS_PARTITION, +} from 'consul-ui/utils/http/consul'; export default class ServiceSerializer extends Serializer { primaryKey = PRIMARY_KEY; @@ -13,31 +17,7 @@ export default class ServiceSerializer extends Serializer { // Services and proxies all come together in the same list. Here we // map the proxies to their related services on a Service.Proxy // property for easy access later on - const services = {}; - body - .filter(function (item) { - return item.Kind !== 'connect-proxy'; - }) - .forEach((item) => { - services[item.Name] = item; - }); - body - .filter(function (item) { - return item.Kind === 'connect-proxy'; - }) - .forEach((item) => { - // Iterating to cover the usecase of a proxy being used by more - // than one service - if (item.ProxyFor) { - item.ProxyFor.forEach((service) => { - if (typeof services[service] !== 'undefined') { - services[service].Proxy = item; - } - }); - } - }); - - return cb(headers, body); + return cb(headers, this._transformServicesPayload(body)); }), query ); @@ -58,4 +38,56 @@ export default class ServiceSerializer extends Serializer { query ); } + + createJSONApiDocumentFromServicesPayload(headers, responseBody, dc) { + const { primaryKey, slugKey, fingerprint } = this; + + const transformedBody = this._transformServicesPayload(responseBody); + const attributes = transformedBody.map( + fingerprint( + primaryKey, + slugKey, + dc, + headers[HTTP_HEADERS_NAMESPACE], + headers[HTTP_HEADERS_PARTITION] + ) + ); + + return { + data: attributes.map((attr) => { + return { + id: attr.uid, + type: 'service', + attributes: attr, + }; + }), + }; + } + + _transformServicesPayload(body) { + const services = {}; + body + .filter(function (item) { + return item.Kind !== 'connect-proxy'; + }) + .forEach((item) => { + services[item.Name] = item; + }); + body + .filter(function (item) { + return item.Kind === 'connect-proxy'; + }) + .forEach((item) => { + // Iterating to cover the usecase of a proxy being used by more + // than one service + if (item.ProxyFor) { + item.ProxyFor.forEach((service) => { + if (typeof services[service] !== 'undefined') { + services[service].Proxy = item; + } + }); + } + }); + return body; + } } diff --git a/ui/packages/consul-ui/app/services/repository/peer.js b/ui/packages/consul-ui/app/services/repository/peer.js index b650278548..c39accbb51 100644 --- a/ui/packages/consul-ui/app/services/repository/peer.js +++ b/ui/packages/consul-ui/app/services/repository/peer.js @@ -1,5 +1,6 @@ import RepositoryService from 'consul-ui/services/repository'; import dataSource from 'consul-ui/decorators/data-source'; +import { inject as service } from '@ember/service'; function normalizePeerPayload(peerPayload, dc, partition) { const { @@ -18,10 +19,31 @@ function normalizePeerPayload(peerPayload, dc, partition) { }; } export default class PeerService extends RepositoryService { + @service store; + getModelName() { return 'peer'; } + @dataSource('/:partition/:ns/:ds/exported-services/:name') + async fetchExportedServices({ dc, ns, partition, name }, configuration, request) { + return ( + await request` + GET /v1/internal/ui/exported-services + + ${{ + peer: name, + }} + ` + )((headers, body, cache) => { + const serviceSerializer = this.store.serializerFor('service'); + + return this.store.push( + serviceSerializer.createJSONApiDocumentFromServicesPayload(headers, body, dc) + ); + }); + } + @dataSource('/:partition/:ns/:dc/peering/token-for/:name') async fetchToken({ dc, ns, partition, name }, configuration, request) { return ( @@ -85,6 +107,22 @@ export default class PeerService extends RepositoryService { }} ` )((headers, body, cache) => { + // we can't easily use fragments as we are working around the serializer + // layer + const { StreamStatus } = body; + + if (StreamStatus) { + if (StreamStatus.LastHeartbeat) { + StreamStatus.LastHeartbeat = new Date(StreamStatus.LastHeartbeat); + } + if (StreamStatus.LastReceive) { + StreamStatus.LastReceive = new Date(StreamStatus.LastReceive); + } + if (StreamStatus.LastSend) { + StreamStatus.LastSend = new Date(StreamStatus.LastSend); + } + } + return { meta: { version: 2, diff --git a/ui/packages/consul-ui/app/services/repository/service.js b/ui/packages/consul-ui/app/services/repository/service.js index 5ce9439b46..7219983329 100644 --- a/ui/packages/consul-ui/app/services/repository/service.js +++ b/ui/packages/consul-ui/app/services/repository/service.js @@ -1,8 +1,11 @@ import RepositoryService from 'consul-ui/services/repository'; import dataSource from 'consul-ui/decorators/data-source'; +import { inject as service } from '@ember/service'; const modelName = 'service'; export default class ServiceService extends RepositoryService { + @service store; + getModelName() { return modelName; } @@ -12,6 +15,23 @@ export default class ServiceService extends RepositoryService { return super.findAll(...arguments); } + @dataSource('/:partition/:ns/:dc/services/:peer/:peerId') + async findAllImportedServices(params, configuration) { + // remember peer.id so that we can add it to to the service later on to setup relationship + const { peerId } = params; + + // don't send peerId with query + delete params.peerId; + + // assign the peer as a belongs-to. we don't have access to any information + // we could use to do this in the serializer so we need to do it manually here + return super.findAll(params, configuration).then((services) => { + const peer = this.store.peekRecord('peer', peerId); + services.forEach((service) => (service.peer = peer)); + return services; + }); + } + @dataSource('/:partition/:ns/:dc/gateways/for-service/:gateway') findGatewayBySlug(params, configuration = {}) { if (typeof configuration.cursor !== 'undefined') { diff --git a/ui/packages/consul-ui/app/styles/debug.scss b/ui/packages/consul-ui/app/styles/debug.scss index 66cecc7ed3..04e673a5b2 100644 --- a/ui/packages/consul-ui/app/styles/debug.scss +++ b/ui/packages/consul-ui/app/styles/debug.scss @@ -119,7 +119,8 @@ html:not(.with-data-source) .data-source-debug { /* hi */ z-index: 100000; } -html.with-href-to [href^='console://']::before { +html.with-href-to [href^='console://']::before +{ @extend %p3; @extend %debug-box; content: attr(href); @@ -155,6 +156,7 @@ html.with-route-announcer .route-title { margin-bottom: 100px; padding-top: 0 !important; } + li.provider-components a::before, li.consul-components a::before, li.components a::before { @extend %with-logo-glimmer-color-icon, %as-pseudo; @@ -164,6 +166,7 @@ html.with-route-announcer .route-title { li.components.css-component a::before { @extend %with-logo-glimmer-color-icon, %as-pseudo; } + li.provider-components.ember-component a::before, li.consul-components.ember-component a::before, li.components.ember-component a::before { @extend %with-logo-ember-circle-color-icon, %as-pseudo; diff --git a/ui/packages/consul-ui/app/templates/dc/services/instance.hbs b/ui/packages/consul-ui/app/templates/dc/services/instance.hbs index 884e4eb01f..dcbb0a3ddb 100644 --- a/ui/packages/consul-ui/app/templates/dc/services/instance.hbs +++ b/ui/packages/consul-ui/app/templates/dc/services/instance.hbs @@ -1,36 +1,32 @@ - + + @src={{uri + '/${partition}/${nspace}/${dc}/service-instance/${id}/${node}/${name}/${peer}' + (hash + partition=route.params.partition + nspace=route.params.nspace + dc=route.params.dc + id=route.params.id + node=route.params.node + name=route.params.name + peer=route.params.peer + ) + }} + as |loader| + > - - + + - - {{#if (eq loader.error.status "404")}} + + {{#if (eq loader.error.status '404')}} + {{notification sticky=true}} + class='notification-update' + @type='warning' + as |notice| + > Warning! @@ -40,14 +36,8 @@ as |route|>

    - {{else if (eq loader.error.status "403")}} - + {{else if (eq loader.error.status '403')}} + Error! @@ -58,13 +48,7 @@ as |route|> {{else}} - + Warning! @@ -77,64 +61,67 @@ as |route|> {{/if}}
    - -{{#let - - loader.data - -as |item|}} - {{#if item.IsOrigin}} - - {{! We only really need meta to get the correct ServiceID }} - {{! but we may as well use the NodeName and ServiceName }} - {{! from meta also, but they should be the same as the instance }} - {{! so if we can ever get ServiceID from elsewhere we could save }} - {{! a HTTP request/long poll here }} - {{#if meta.data.ServiceID}} - {{! if we have a proxy then get the additional instance information }} - {{! for the proxy itself so if the service is called `backend` }} - {{! its likely to have a proxy service called `backend-sidecar-proxy` }} - {{! and this second request get the info for that instance and saves }} - {{! it into the `proxy` variable }} - - {{/if}} - - {{/if}} - - -
      -
    1. All Services
    2. -
    3. - Service ({{item.Service.Service}}) -
    4. -
    + + {{#let loader.data as |item|}} + {{#if item.IsOrigin}} + + {{! We only really need meta to get the correct ServiceID }} + {{! but we may as well use the NodeName and ServiceName }} + {{! from meta also, but they should be the same as the instance }} + {{! so if we can ever get ServiceID from elsewhere we could save }} + {{! a HTTP request/long poll here }} + {{#if meta.data.ServiceID}} + {{! if we have a proxy then get the additional instance information }} + {{! for the proxy itself so if the service is called `backend` }} + {{! its likely to have a proxy service called `backend-sidecar-proxy` }} + {{! and this second request get the info for that instance and saves }} + {{! it into the `proxy` variable }} + + {{/if}} + + {{/if}} + + +
      +
    1. All Services
    2. +
    3. + Service ({{item.Service.Service}}) +
    4. +
    - +

    @@ -146,54 +133,92 @@ as |item|}} {{/if}}
    - +
    Service Name
    -
    {{item.Service.Service}}
    +
    {{item.Service.Service}}
    {{#unless item.Node.Meta.synthetic-node}}
    Node Name
    -
    {{item.Node.Node}}
    +
    {{item.Node.Node}}
    {{/unless}} {{#if item.Service.PeerName}}
    Peer Name
    -
    {{item.Service.PeerName}}
    +
    {{item.Service.PeerName}}
    {{/if}}
    - + {{#let (or item.Service.Address item.Node.Address) as |address|}} - {{address}} + {{address}} {{/let}} - - + + @model={{assign (hash proxy=proxy meta=meta item=item) route.model}} + as |o| + > {{outlet}} -
    -{{/let}} +
    + {{/let}}
    -
    +
    \ No newline at end of file diff --git a/ui/packages/consul-ui/mock-api/v1/internal/ui/exported-services b/ui/packages/consul-ui/mock-api/v1/internal/ui/exported-services new file mode 100644 index 0000000000..8bfe712541 --- /dev/null +++ b/ui/packages/consul-ui/mock-api/v1/internal/ui/exported-services @@ -0,0 +1,31 @@ + +${[0].map( + () => { + let prevKind; + let name; + const gateways = ['mesh-gateway', 'ingress-gateway', 'terminating-gateway']; + return ` +[ + ${ + range( + env( + 'CONSUL_SERVICE_COUNT', + Math.floor( + ( + Math.random() * env('CONSUL_SERVICE_MAX', 10) + ) + parseInt(env('CONSUL_SERVICE_MIN', 1)) + ) + ) + ).map( + function(item, i) + { + return ` + { + "Name": "service-${i}" + } + `; + } + ) + } +] +`})} diff --git a/ui/packages/consul-ui/mock-api/v1/peering/_ b/ui/packages/consul-ui/mock-api/v1/peering/_ index 46c1a1af2a..306990c9c4 100644 --- a/ui/packages/consul-ui/mock-api/v1/peering/_ +++ b/ui/packages/consul-ui/mock-api/v1/peering/_ @@ -1,7 +1,5 @@ { "ID": "${fake.random.uuid()}", - "ImportedServiceCount": ${fake.random.number({min: 0, max: 4000})}, - "ExportedServiceCount": ${fake.random.number({min: 0, max: 4000})}, "Name": "${location.pathname.get(2)}", "State": "${fake.helpers.randomize([ 'ACTIVE', @@ -12,6 +10,13 @@ 'TERMINATED', 'UNDEFINED' ])}", + "StreamStatus": { + "LastHeartbeat": "${fake.date.past(10).toISOString()}", + "LastReceive": "${fake.date.past(10).toISOString()}", + "LastSend": "${fake.date.past(10).toISOString()}", + "ExportedServices": [${range(0, Math.floor(Math.random() * 10)).map((i) => `"exported-service-${i}"`)}], + "ImportedServices": [${range(0, Math.floor(Math.random() * 10)).map((i) => `"imported-service-${i}"`)}] + }, "PeerID": "${fake.random.uuid()}", "PeerServerName": "${fake.internet.domainName()}", "PeerServerAddresses": [ diff --git a/ui/packages/consul-ui/mock-api/v1/peerings b/ui/packages/consul-ui/mock-api/v1/peerings index f240e49d21..059fc83f32 100644 --- a/ui/packages/consul-ui/mock-api/v1/peerings +++ b/ui/packages/consul-ui/mock-api/v1/peerings @@ -22,8 +22,8 @@ "LastHeartbeat": "${fake.date.past(10).toISOString()}", "LastReceive": "${fake.date.past(10).toISOString()}", "LastSend": "${fake.date.past(10).toISOString()}", - "ExportedServices": ["${`export-service-${i}`}"], - "ImportedServices": ["${`import-service-${i}`}"] + "ExportedServices": [${range(0, Math.floor(Math.random() * 10)).map((i) => `"exported-service-${i}"`)}], + "ImportedServices": [${range(0, Math.floor(Math.random() * 10)).map((i) => `"imported-service-${i}"`)}] }, "PeerID": "${id}", "PeerServerName": "${fake.internet.domainName()}", diff --git a/ui/packages/consul-ui/package.json b/ui/packages/consul-ui/package.json index da781d9928..146ff10df2 100644 --- a/ui/packages/consul-ui/package.json +++ b/ui/packages/consul-ui/package.json @@ -71,6 +71,7 @@ "@hashicorp/design-system-tokens": "^1.0.0", "@hashicorp/ember-cli-api-double": "^4.0.0", "@hashicorp/ember-flight-icons": "^2.0.12", + "@html-next/vertical-collection": "^4.0.0", "@lit/reactive-element": "^1.2.1", "@xstate/fsm": "^1.4.0", "a11y-dialog": "^6.0.1", @@ -146,7 +147,7 @@ "ember-power-select": "^4.0.5", "ember-power-select-with-create": "^0.8.0", "ember-qunit": "^5.1.1", - "ember-ref-modifier": "^1.0.0", + "ember-ref-bucket": "^4.1.0", "ember-render-helpers": "^0.2.0", "ember-resolver": "^8.0.2", "ember-route-action-helper": "^2.0.8", diff --git a/ui/packages/consul-ui/tailwind.config.js b/ui/packages/consul-ui/tailwind.config.js index d10c682315..69ae42f3e4 100644 --- a/ui/packages/consul-ui/tailwind.config.js +++ b/ui/packages/consul-ui/tailwind.config.js @@ -34,7 +34,7 @@ function colorMapFromTokens(tokensPath) { /** @type {import('tailwindcss').Config} */ module.exports = { - content: ['./app/**/*.{html,js,hbs,mdx}', './docs/**/*.{html,js,hbs,mdx}'], + content: ['../**/*.{html.js,hbs,mdx}'], theme: { colors: colorMapFromTokens( '../../node_modules/@hashicorp/design-system-tokens/dist/products/css/tokens.css' diff --git a/ui/packages/consul-ui/tests/acceptance/dc/peers/regenerate.feature b/ui/packages/consul-ui/tests/acceptance/dc/peers/regenerate.feature index a925f7887b..ce4d2e3f7f 100644 --- a/ui/packages/consul-ui/tests/acceptance/dc/peers/regenerate.feature +++ b/ui/packages/consul-ui/tests/acceptance/dc/peers/regenerate.feature @@ -6,6 +6,8 @@ Feature: dc / peers / regenerate: Regenerate Peer Token --- Name: peer-name State: ACTIVE + # dialer does not have a PeerID + PeerID: null --- And the url "/v1/peering/token" responds with from yaml --- @@ -19,3 +21,19 @@ Feature: dc / peers / regenerate: Regenerate Peer Token And I click actions on the peers And I click regenerate on the peers Then I see the text "an-encoded-token" in ".consul-peer-form-generate code" + + Scenario: + Given 1 datacenter model with the value "datacenter" + And 1 peer model from yaml + --- + Name: peer-name + State: ACTIVE + # receiver holds a PeerID + PeerID: some-id + --- + When I visit the peers page for yaml + --- + dc: datacenter + --- + And I click actions on the peers + Then I don't see regenerate on peers diff --git a/ui/packages/consul-ui/tests/acceptance/dc/peers/show.feature b/ui/packages/consul-ui/tests/acceptance/dc/peers/show.feature new file mode 100644 index 0000000000..4fc7f74056 --- /dev/null +++ b/ui/packages/consul-ui/tests/acceptance/dc/peers/show.feature @@ -0,0 +1,136 @@ +@setupApplcationTest +Feature: dc / peers / show: Peers show + Scenario: Dialer side tabs + And 1 datacenter model with the value "dc-1" + And 1 peer models from yaml + --- + Name: a-peer + State: ACTIVE + # dialer side + PeerID: null + --- + When I visit the peers page for yaml + --- + dc: dc-1 + --- + And I click actions on the peers + And I click view on the peers + Then the url should be /dc-1/peers/a-peer/imported-services + Then I see importedServicesIsVisible on the tabs + And I see exportedServicesIsVisible on the tabs + And I don't see addressesIsVisible on the tabs + + Scenario: Receiver side tabs + And 1 datacenter model with the value "dc-1" + And 1 peer models from yaml + --- + Name: a-peer + State: ACTIVE + # receiver side + PeerID: 'some-peer' + --- + When I visit the peers page for yaml + --- + dc: dc-1 + --- + And I click actions on the peers + And I click view on the peers + Then the url should be /dc-1/peers/a-peer/imported-services + Then I see importedServicesIsVisible on the tabs + And I see exportedServicesIsVisible on the tabs + And I see addressesIsVisible on the tabs + + Scenario: Imported Services Empty + And 1 datacenter model with the value "dc-1" + And 1 peer models from yaml + --- + Name: a-peer + State: ACTIVE + --- + And 0 service models + When I visit the peer page for yaml + --- + dc: dc-1 + peer: a-peer + --- + Then I see the "[data-test-imported-services-empty]" element + + Scenario: Imported Services not empty + And 1 datacenter model with the value "dc-1" + And 1 peer models from yaml + --- + Name: a-peer + State: ACTIVE + --- + And 1 service models from yaml + --- + Name: 'service-for-peer-a' + --- + When I visit the peer page for yaml + --- + dc: dc-1 + peer: a-peer + --- + Then I don't see the "[data-test-imported-services-empty]" element + + Scenario: Exported Services Empty + And 1 datacenter model with the value "dc-1" + And 1 peer models from yaml + --- + Name: a-peer + State: ACTIVE + --- + And 0 service models + When I visitExported the peer page for yaml + --- + dc: dc-1 + peer: a-peer + --- + Then I see the "[data-test-exported-services-empty]" element + + Scenario: Exported Services not empty + And 1 datacenter model with the value "dc-1" + And 1 peer models from yaml + --- + Name: a-peer + State: ACTIVE + --- + And 1 service models from yaml + --- + Name: 'service-for-peer-a' + --- + When I visitExported the peer page for yaml + --- + dc: dc-1 + peer: a-peer + --- + Then I don't see the "[data-test-exported-services-empty]" element + + Scenario: Addresses Empty + And 1 datacenter model with the value "dc-1" + And 1 peer models from yaml + --- + Name: a-peer + State: ACTIVE + PeerServerAddresses: null + --- + When I visitAddresses the peer page for yaml + --- + dc: dc-1 + peer: a-peer + --- + Then I see the "[data-test-addresses-empty]" element + + Scenario: Addresses Not Empty + And 1 datacenter model with the value "dc-1" + And 1 peer models from yaml + --- + Name: a-peer + State: ACTIVE + --- + When I visitAddresses the peer page for yaml + --- + dc: dc-1 + peer: a-peer + --- + Then I don't see the "[data-test-addresses-empty]" element \ No newline at end of file diff --git a/ui/packages/consul-ui/tests/acceptance/steps/dc/peers/show-steps.js b/ui/packages/consul-ui/tests/acceptance/steps/dc/peers/show-steps.js new file mode 100644 index 0000000000..06173e1e01 --- /dev/null +++ b/ui/packages/consul-ui/tests/acceptance/steps/dc/peers/show-steps.js @@ -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); + }); +} diff --git a/ui/packages/consul-ui/tests/pages.js b/ui/packages/consul-ui/tests/pages.js index c3ceb2af58..a14d23293c 100644 --- a/ui/packages/consul-ui/tests/pages.js +++ b/ui/packages/consul-ui/tests/pages.js @@ -75,6 +75,7 @@ import intention from 'consul-ui/tests/pages/dc/intentions/edit'; import nspaces from 'consul-ui/tests/pages/dc/nspaces/index'; import nspace from 'consul-ui/tests/pages/dc/nspaces/edit'; import peers from 'consul-ui/tests/pages/dc/peers/index'; +import peersShow from 'consul-ui/tests/pages/dc/peers/show'; // utils const deletable = createDeletable(clickable); @@ -234,6 +235,7 @@ export default { nspace(visitable, submitable, deletable, cancelable, policySelector, roleSelector) ), peers: create(peers(visitable, creatable, consulPeerList, popoverSelect)), + peer: create(peersShow(visitable)), settings: create(settings(visitable, submitable, isPresent)), routingConfig: create(routingConfig(visitable, text)), }; diff --git a/ui/packages/consul-ui/tests/pages/dc/peers/index.js b/ui/packages/consul-ui/tests/pages/dc/peers/index.js index b46701909e..d60b3e41eb 100644 --- a/ui/packages/consul-ui/tests/pages/dc/peers/index.js +++ b/ui/packages/consul-ui/tests/pages/dc/peers/index.js @@ -1,7 +1,10 @@ +import tabgroup from 'consul-ui/components/tab-nav/pageobject'; + export default function (visitable, creatable, items, popoverSelect) { return creatable({ visit: visitable('/:dc/peers'), peers: items(), sort: popoverSelect('[data-test-sort-control]'), + tabs: tabgroup('tab', ['imported-services', 'exported-services', 'addresses']), }); } diff --git a/ui/packages/consul-ui/tests/pages/dc/peers/show.js b/ui/packages/consul-ui/tests/pages/dc/peers/show.js new file mode 100644 index 0000000000..25dcbe60f2 --- /dev/null +++ b/ui/packages/consul-ui/tests/pages/dc/peers/show.js @@ -0,0 +1,8 @@ +export default function (visitable) { + return { + visit: visitable('/:dc/peers/:peer'), + visitExported: visitable('/:dc/peers/:peer/exported-services'), + visitImported: visitable('/:dc/peers/:peer/imported-services'), + visitAddresses: visitable('/:dc/peers/:peer/addresses'), + }; +} diff --git a/ui/packages/consul-ui/tests/steps/interactions/visit.js b/ui/packages/consul-ui/tests/steps/interactions/visit.js index 5a4551cf57..fc382e6c71 100644 --- a/ui/packages/consul-ui/tests/steps/interactions/visit.js +++ b/ui/packages/consul-ui/tests/steps/interactions/visit.js @@ -29,5 +29,13 @@ export default function (scenario, pages, set, reset) { // do I absolutely definitely need that all the time? return set(pages[name]).visit(data); } + ) + .when( + ['I $method the $name page for yaml\n$yaml', 'I $method the $name page for json\n$json'], + function (method, name, data) { + reset(); + + return set(pages[name])[method](data); + } ); } diff --git a/ui/packages/consul-ui/translations/routes/en-us.yaml b/ui/packages/consul-ui/translations/routes/en-us.yaml index e6ed415227..4e157d9dff 100644 --- a/ui/packages/consul-ui/translations/routes/en-us.yaml +++ b/ui/packages/consul-ui/translations/routes/en-us.yaml @@ -117,10 +117,10 @@ dc: index: empty: header: | - {items, select, - 0 {Welcome to Peers} - other {No peers found} - } + {items, select, + 0 {Welcome to Peers} + other {No peers found} + } body: | {items, select, 0 {Cluster peering is the recommended way to connect services across or within Consul datacenters. Peering is a one-to-one relationship in which each peer is either a open-source Consul datacenter or a Consul enterprise admin partition. There don't seem to be any peers for this {canUsePartitions, select, @@ -135,12 +135,42 @@ dc: detail: imported: count: | - {count} imported services + {count} imported services tooltip: The number of services imported from {name} + tab-tooltip: Services imported from {name} exported: count: | - {count} exported services + {count} exported services tooltip: The number of services exported from {name} + tab-tooltip: Services exported from {name} + addresses: + tooltip: The number of services exported from {name} + show: + imported: + empty: + header: No visible imported services from {name} + body: | +
    + {items, select, + 0 {Services must be exported from one peer to another to enable service communication across two peers. There don't seem to be any services imported from {name} yet, or you may not have services:read permissions to access to this view.} + other {No services where found matching that search, or you may not have access to view the services you are searching for.} + } +
    + exported: + empty: + header: No visible exported services to {name} + body: | +
    + {items, select, + 0 {Services must be exported from one peer to another to enable service communication across two peers. There don't seem to be any services exported to {name} yet, or you may not have services:read permissions to access to this view.} + other {No services where found matching that search, or you may not have access to view the services you are searching for.} + } +
    + + addresses: + empty: + header: No server adddresses. + body:
    There don't seem to be any server addresses for this peer.
    partitions: index: empty: @@ -148,7 +178,7 @@ dc: {items, select, 0 {Welcome to Partitions} other {No partitions found} - } + } body: | {items, select, 0 {There don't seem to be any partitions{canUseACLs, select, diff --git a/ui/yarn.lock b/ui/yarn.lock index 2b8e0eca8e..a1b3fb4c01 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -3320,6 +3320,20 @@ resolved "https://registry.yarnpkg.com/@hashicorp/flight-icons/-/flight-icons-2.10.0.tgz#24b03043bacda16e505200e6591dfef896ddacf1" integrity sha512-jYUA0M6Tz+4RAudil+GW/fHbhZPcKCiIZZAguBDviqbLneMkMgPOBgbXWCGWsEQ1fJzP2cXbUaio8L0aQZPWQw== +"@html-next/vertical-collection@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@html-next/vertical-collection/-/vertical-collection-4.0.0.tgz#b3b3d52358e15e7ed46e028d12424dab994690ed" + integrity sha512-/c4y6ASmkMwyG+rcoXH3kx50LiK2MuPX0bsktd+j9LhOD6zkJyT4wJ73m20dCEvxjgwA/nCQ8hj3lApQlHG0CQ== + dependencies: + babel6-plugin-strip-class-callcheck "^6.0.0" + broccoli-funnel "^3.0.8" + broccoli-merge-trees "^4.2.0" + broccoli-rollup "^5.0.0" + ember-cli-babel "^7.12.0" + ember-cli-htmlbars "^6.0.0" + ember-cli-version-checker "^5.1.2" + ember-raf-scheduler "^0.3.0" + "@humanwhocodes/config-array@^0.5.0": version "0.5.0" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9" @@ -3511,6 +3525,13 @@ resolved "https://registry.yarnpkg.com/@types/broccoli-plugin/-/broccoli-plugin-1.3.0.tgz#38f8462fecaebc4e09a32e4d4ed1b9808f75bbca" integrity sha512-SLk4/hFc2kGvgwNFrpn2O1juxFOllcHAywvlo7VwxfExLzoz1GGJ0oIZCwj5fwSpvHw4AWpZjJ1fUvb62PDayQ== +"@types/broccoli-plugin@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/broccoli-plugin/-/broccoli-plugin-3.0.0.tgz#290fda2270c47a568edfd0cefab8bb840d8bb7b2" + integrity sha512-f+TcsARR2PovfFRKFdCX0kfH/QoM3ZVD2h1rl2mNvrKO0fq2uBNCBsTU3JanfU4COCt5cXpTfARyUsERlC8vIw== + dependencies: + broccoli-plugin "*" + "@types/chai-as-promised@^7.1.2": version "7.1.3" resolved "https://registry.yarnpkg.com/@types/chai-as-promised/-/chai-as-promised-7.1.3.tgz#779166b90fda611963a3adbfd00b339d03b747bd" @@ -5945,6 +5966,19 @@ broccoli-persistent-filter@^3.1.2: symlink-or-copy "^1.0.1" sync-disk-cache "^2.0.0" +broccoli-plugin@*, broccoli-plugin@^4.0.5, broccoli-plugin@^4.0.7: + version "4.0.7" + resolved "https://registry.yarnpkg.com/broccoli-plugin/-/broccoli-plugin-4.0.7.tgz#dd176a85efe915ed557d913744b181abe05047db" + integrity sha512-a4zUsWtA1uns1K7p9rExYVYG99rdKeGRymW0qOCNkvDPHQxVi3yVyJHhQbM3EZwdt2E0mnhr5e0c/bPpJ7p3Wg== + dependencies: + broccoli-node-api "^1.7.0" + broccoli-output-wrapper "^3.2.5" + fs-merger "^3.2.1" + promise-map-series "^0.3.0" + quick-temp "^0.1.8" + rimraf "^3.0.2" + symlink-or-copy "^1.3.1" + broccoli-plugin@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/broccoli-plugin/-/broccoli-plugin-1.1.0.tgz#73e2cfa05f8ea1e3fc1420c40c3d9e7dc724bf02" @@ -6001,19 +6035,6 @@ broccoli-plugin@^4.0.0, broccoli-plugin@^4.0.1, broccoli-plugin@^4.0.2, broccoli rimraf "^3.0.2" symlink-or-copy "^1.3.1" -broccoli-plugin@^4.0.5, broccoli-plugin@^4.0.7: - version "4.0.7" - resolved "https://registry.yarnpkg.com/broccoli-plugin/-/broccoli-plugin-4.0.7.tgz#dd176a85efe915ed557d913744b181abe05047db" - integrity sha512-a4zUsWtA1uns1K7p9rExYVYG99rdKeGRymW0qOCNkvDPHQxVi3yVyJHhQbM3EZwdt2E0mnhr5e0c/bPpJ7p3Wg== - dependencies: - broccoli-node-api "^1.7.0" - broccoli-output-wrapper "^3.2.5" - fs-merger "^3.2.1" - promise-map-series "^0.3.0" - quick-temp "^0.1.8" - rimraf "^3.0.2" - symlink-or-copy "^1.3.1" - broccoli-postcss-single@^5.0.1: version "5.0.2" resolved "https://registry.yarnpkg.com/broccoli-postcss-single/-/broccoli-postcss-single-5.0.2.tgz#f23661b3011494d8a2dbd8ff39eb394e80313682" @@ -6052,6 +6073,21 @@ broccoli-rollup@^4.1.1: symlink-or-copy "^1.2.0" walk-sync "^1.1.3" +broccoli-rollup@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/broccoli-rollup/-/broccoli-rollup-5.0.0.tgz#a77b53bcef1b70e988913fee82265c0a4ca530da" + integrity sha512-QdMuXHwsdz/LOS8zu4HP91Sfi4ofimrOXoYP/lrPdRh7lJYD87Lfq4WzzUhGHsxMfzANIEvl/7qVHKD3cFJ4tA== + dependencies: + "@types/broccoli-plugin" "^3.0.0" + broccoli-plugin "^4.0.7" + fs-tree-diff "^2.0.1" + heimdalljs "^0.2.6" + node-modules-path "^1.0.1" + rollup "^2.50.0" + rollup-pluginutils "^2.8.1" + symlink-or-copy "^1.2.0" + walk-sync "^2.2.0" + broccoli-sass-source-maps@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/broccoli-sass-source-maps/-/broccoli-sass-source-maps-4.0.0.tgz#1ee4c10a810b10955b0502e28f85ab672f5961a2" @@ -7946,7 +7982,7 @@ ember-cli-babel@^6.0.0, ember-cli-babel@^6.0.0-beta.4, ember-cli-babel@^6.11.0, ember-cli-version-checker "^2.1.2" semver "^5.5.0" -ember-cli-babel@^7.13.2, ember-cli-babel@^7.26.1, ember-cli-babel@^7.26.11, ember-cli-babel@^7.26.3, ember-cli-babel@^7.26.5, ember-cli-babel@^7.4.0: +ember-cli-babel@^7.12.0, ember-cli-babel@^7.13.2, ember-cli-babel@^7.26.1, ember-cli-babel@^7.26.11, ember-cli-babel@^7.26.3, ember-cli-babel@^7.26.5, ember-cli-babel@^7.4.0: version "7.26.11" resolved "https://registry.yarnpkg.com/ember-cli-babel/-/ember-cli-babel-7.26.11.tgz#50da0fe4dcd99aada499843940fec75076249a9f" integrity sha512-JJYeYjiz/JTn34q7F5DSOjkkZqy8qwFOOxXfE6pe9yEJqWGu4qErKxlz8I22JoVEQ/aBUO+OcKTpmctvykM9YA== @@ -8881,7 +8917,7 @@ ember-modifier@^2.1.0, ember-modifier@^2.1.1: ember-destroyable-polyfill "^2.0.2" ember-modifier-manager-polyfill "^1.2.0" -"ember-modifier@^2.1.2 || ^3.1.0 || ^4.0.0": +"ember-modifier@^2.1.2 || ^3.1.0 || ^4.0.0", ember-modifier@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/ember-modifier/-/ember-modifier-3.2.7.tgz#f2d35b7c867cbfc549e1acd8d8903c5ecd02ea4b" integrity sha512-ezcPQhH8jUfcJQbbHji4/ZG/h0yyj1jRDknfYue/ypQS8fM8LrGcCMo0rjDZLzL1Vd11InjNs3BD7BdxFlzGoA== @@ -8981,12 +9017,21 @@ ember-qunit@^5.1.1: silent-error "^1.1.1" validate-peer-dependencies "^1.2.0" -ember-ref-modifier@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/ember-ref-modifier/-/ember-ref-modifier-1.0.1.tgz#aeca56798ebc0fb750f0ccd36e86d667f5b1bc44" - integrity sha512-qmEFY/4WOWWXABRWvX50jLPleH30p/LPLUXEvaSlIj41F23e3Vul91IqZ1PFdw1Rxpkb8DHWO5BRchN8vnz4+Q== +ember-raf-scheduler@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/ember-raf-scheduler/-/ember-raf-scheduler-0.3.0.tgz#7657ee5c1d54f852731e61e9d0e0750a9a22f5f4" + integrity sha512-i8JWQidNCX7n5TOTIKRDR0bnsQN9aJh/GtOJKINz2Wr+I7L7sYVhli6MFqMYNGKC9j9e6iWsznfAIxddheyEow== dependencies: - ember-cli-babel "^7.20.5" + ember-cli-babel "^7.26.6" + +ember-ref-bucket@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/ember-ref-bucket/-/ember-ref-bucket-4.1.0.tgz#2a52e72a395a14033d034c834fab648f26d74baa" + integrity sha512-oEUU2mDtuYuMM039U9YEqrrOCVHH6rQfvbFOmh3WxOVEgubmLVyKEpGgU4P/6j0B/JxTqqTwM3ULTQyDto8dKg== + dependencies: + ember-cli-babel "^7.26.11" + ember-cli-htmlbars "^6.0.1" + ember-modifier "^3.2.7" ember-render-helpers@^0.2.0: version "0.2.0" @@ -15179,6 +15224,13 @@ rollup@^1.12.0: "@types/node" "*" acorn "^7.1.0" +rollup@^2.50.0: + version "2.79.1" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.1.tgz#bedee8faef7c9f93a2647ac0108748f497f081c7" + integrity sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw== + optionalDependencies: + fsevents "~2.3.2" + route-recognizer@^0.3.3: version "0.3.4" resolved "https://registry.yarnpkg.com/route-recognizer/-/route-recognizer-0.3.4.tgz#39ab1ffbce1c59e6d2bdca416f0932611e4f3ca3"