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 @@
+
+
+
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|}}
-
-
-
- All Peers
-
-
-
-
-
-
-
-
-
-
- {{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|
+ }}
+
+
+
+ All Peers
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{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}}
+
+
+
+
+ Documentation on Peers
+
+
+
+
+ Take the tutorial
+
+
+
+
+ {{/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}}
+
+
+
+
+ {{/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
+ }}
+
+
+
+
+ Documentation on Peers
+
+
+
+
+ Take the tutorial
+
+
+
+
+
+
+ {{/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|}}
- {{t (concat "common.consul." (lowercase prop))}}
+ {{t (concat 'common.consul.' (lowercase 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|}}
-
- {{t (concat "common.consul." state)
- default=(array
- (concat "common.search." state)
- )
- }}
-
- {{/each}}
- {{/let}}
+ {{/let}}
-
-
-
- {{t "components.consul.service.search-bar.kind"}}
-
-
-
- {{#let components.Optgroup components.Option as |Optgroup Option|}}
-
- {{t "common.consul.service"}}
+
+
+ <:filter as |search|>
+
+
+
+ {{t 'common.consul.status'}}
+
+
+
+ {{#let components.Optgroup components.Option as |Optgroup Option|}}
+ {{#each this.healthStates as |state|}}
+
+ {{t (concat 'common.consul.' state) default=(array (concat 'common.search.' state))}}
+
+ {{/each}}
+ {{/let}}
+
+
+
+
+
+ {{t 'components.consul.service.search-bar.kind'}}
+
+
+
+ {{#let components.Optgroup components.Option as |Optgroup Option|}}
+
+ {{t 'common.consul.service'}}
-
- {{#each (array "ingress-gateway" "terminating-gateway" "mesh-gateway") as |kind|}}
-
- {{t (concat "common.consul." kind)}}
-
- {{/each}}
+
+ {{#each (array 'ingress-gateway' 'terminating-gateway' 'mesh-gateway') as |kind|}}
+
+ {{t (concat 'common.consul.' kind)}}
+
+ {{/each}}
-
- {{#each (array "in-mesh" "not-in-mesh") as |state|}}
-
- {{t (concat "common.search." state)}}
-
- {{/each}}
+
+ {{#each (array 'in-mesh' 'not-in-mesh') as |state|}}
+
+ {{t (concat 'common.search.' 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|}}
-
- {{t (concat "common.brand." source)}}
-
- {{/each}}
-
- {{t 'common.brand.consul'}}
-
-{{/if}}
- {{/let}}
+
+ {{#let components.Option as |Option|}}
+ {{#if (gt @sources.length 0)}}
+ {{#each @sources as |source|}}
+
+ {{t (concat 'common.brand.' source)}}
+
+ {{/each}}
+
+ {{t 'common.brand.consul'}}
+
+ {{/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|}}
-
- {{t "common.sort.status.asc"}}
- {{t "common.sort.status.desc"}}
+ {{/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|}}
+
+ {{t
+ 'common.sort.status.asc'
+ }}
+ {{t
+ 'common.sort.status.desc'
+ }}
-
- {{t "common.sort.alpha.asc"}}
- {{t "common.sort.alpha.desc"}}
+
+ {{t
+ 'common.sort.alpha.asc'
+ }}
+ {{t
+ 'common.sort.alpha.desc'
+ }}
- {{/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
+
+
+
+
+
+ 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|}}
-
-
-{{#each @items as |item|}}
-
-
- {{item.label}}
-
-
-{{/each}}
-
-
+
+ {{#each @items as |item|}}
+
+
+ {{#if item.tooltip}}
+ {{item.label}}
+ {{else}}
+ {{item.label}}
+ {{/if}}
+
+
+ {{/each}}
+
+
{{/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}}
-
-
-
- All Services
-
- Service ({{item.Service.Service}})
-
-
+
+ {{#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}}
+
+
+
+ All Services
+
+ Service ({{item.Service.Service}})
+
+
-
+
@@ -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"