Merge pull request #14947 from hashicorp/ui/feat/peer-detail-page

ui: peer detail view
This commit is contained in:
Michael Klein 2022-10-13 17:03:57 +02:00 committed by GitHub
commit e6cce385e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
57 changed files with 1896 additions and 645 deletions

3
.changelog/14947.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:feature
ui: Create peerings detail page
```

View File

@ -38,6 +38,5 @@ deps: clean
dist-vercel: clean dist-vercel: clean
mkdir -p dist/ui && \ mkdir -p dist/ui && \
cd packages/consul-ui && \ cd packages/consul-ui && \
CONSUL_UI_INSTALL_FLAGS=--focus \
$(MAKE) build-staging && \ $(MAKE) build-staging && \
mv dist/* ../../dist/ui mv dist/* ../../dist/ui

View File

@ -0,0 +1,71 @@
<Hds::Card::Container @level="base" @hasBorder={{true}} class="mt-6 mb-3">
<div class="flex h-24 p-6 overflow-x-scroll space-x-12">
<div class="shrink-0">
<div
class="mb-2 hds-typography-body-200 hds-font-weight-semibold text-hds-foreground-primary"
>Status</div>
<div class="flex items-center">
<Peerings::Badge @peering={{@peering}} />
</div>
</div>
<div class="shrink-0">
<div
class="mb-2 hds-typography-body-200 hds-font-weight-semibold text-hds-foreground-primary"
>Latest heartbeat</div>
<div class="flex items-center">
{{#if @peering.LastHeartbeat}}
{{#let (smart-date-format @peering.LastHeartbeat) as |smartDate|}}
<FlightIcon
@name="activity"
class="mr-0.5 text-hds-foreground-faint fill-current"
/>
{{#if smartDate.isNearDate}}
<span {{tooltip smartDate.friendly}}>{{smartDate.relative}}</span>
{{else}}
<span>{{smartDate.friendly}}</span>
{{/if}}
{{/let}}
{{else}}
<span>None yet</span>
{{/if}}
</div>
</div>
<div class="shrink-0">
<div
class="mb-2 hds-typography-body-200 hds-font-weight-semibold text-hds-foregrouny-primary"
>Latest receipt</div>
<div class="flex items-center">
{{#if @peering.LastReceive}}
{{#let (smart-date-format @peering.LastReceive) as |smartDate|}}
{{#if smartDate.isNearDate}}
<span {{tooltip smartDate.friendly}}>{{smartDate.relative}}</span>
{{else}}
<span>{{smartDate.friendly}}</span>
{{/if}}
{{/let}}
{{else}}
<span>None yet</span>
{{/if}}
</div>
</div>
<div class="shrink-0">
<div
class="mb-2 hds-typography-body-200 hds-font-weight-semibold text-hds-foreground-primary"
>Latest send</div>
<div class="flex items-center">
{{#if @peering.LastSend}}
{{#let (smart-date-format @peering.LastSend) as |smartDate|}}
{{#if smartDate.isNearDate}}
<span {{tooltip smartDate.friendly}}>{{smartDate.relative}}</span>
{{else}}
<span>{{smartDate.friendly}}</span>
{{/if}}
{{/let}}
{{else}}
<span>None yet</span>
{{/if}}
</div>
</div>
</div>
</Hds::Card::Container>

View File

@ -2,13 +2,22 @@
class="consul-peer-list" class="consul-peer-list"
...attributes ...attributes
@items={{@items}} @items={{@items}}
as |item index|> @linkable="linkable peers"
as |item index|
>
<BlockSlot @name="header"> <BlockSlot @name="header">
<p {{#if (can "delete peer" item=item)}}
data-test-peer={{item.Name}} <a
> data-test-peer={{item.Name}}
{{item.Name}} href={{href-to "dc.peers.show" item.Name}}
</p> >
{{item.Name}}
</a>
{{else}}
<p data-test-peer={{item.Name}}>
{{item.Name}}
</p>
{{/if}}
</BlockSlot> </BlockSlot>
<BlockSlot @name="details"> <BlockSlot @name="details">
<div class="peers__list__peer-detail"> <div class="peers__list__peer-detail">
@ -16,24 +25,22 @@ as |item index|>
<div <div
{{tooltip {{tooltip
(t 'routes.dc.peers.index.detail.imported.tooltip' (t "routes.dc.peers.index.detail.imported.tooltip" name=item.Name)
name=item.Name
)
}} }}
> >
{{t 'routes.dc.peers.index.detail.imported.count' {{t
"routes.dc.peers.index.detail.imported.count"
count=(format-number item.ImportedServiceCount) count=(format-number item.ImportedServiceCount)
}} }}
</div> </div>
<div <div
{{tooltip {{tooltip
(t 'routes.dc.peers.index.detail.exported.tooltip' (t "routes.dc.peers.index.detail.exported.tooltip" name=item.Name)
name=item.Name
)
}} }}
> >
{{t 'routes.dc.peers.index.detail.exported.count' {{t
"routes.dc.peers.index.detail.exported.count"
count=(format-number item.ExportedServiceCount) count=(format-number item.ExportedServiceCount)
}} }}
</div> </div>
@ -41,47 +48,51 @@ as |item index|>
</div> </div>
</BlockSlot> </BlockSlot>
<BlockSlot @name="actions" as |Actions|> <BlockSlot @name="actions" as |Actions|>
{{#if (can 'delete peer' item=item)}} {{#if (can "delete peer" item=item)}}
<Actions as |Action|> <Actions as |Action|>
{{#if (can "write peer" item=item)}} {{#if (and (can "write peer" item=item) item.isDialer)}}
<Action data-test-regenerate-action {{on "click" (fn @onedit item)}}>
<BlockSlot @name="label">
Regenerate token
</BlockSlot>
</Action>
{{/if}}
<Action <Action
data-test-regenerate-action data-test-view-action
{{on 'click' (fn @onedit item)}} @href={{href-to "dc.peers.show" item.Name}}
> >
<BlockSlot @name="label"> <BlockSlot @name="label">
Regenerate token View
</BlockSlot> </BlockSlot>
</Action> </Action>
{{/if}} <Action
<Action data-test-delete-action
data-test-delete-action @onclick={{fn @ondelete item}}
@onclick={{fn @ondelete item}} class="dangerous"
class="dangerous" >
> <BlockSlot @name="label">
<BlockSlot @name="label"> Delete
Delete </BlockSlot>
</BlockSlot> <BlockSlot @name="confirmation" as |Confirmation|>
<BlockSlot @name="confirmation" as |Confirmation|> <Confirmation class="warning">
<Confirmation class="warning"> <BlockSlot @name="header">
<BlockSlot @name="header"> Confirm delete
Confirm delete </BlockSlot>
</BlockSlot> <BlockSlot @name="body">
<BlockSlot @name="body"> <p>
<p> Are you sure you want to delete this peer?
Are you sure you want to delete this peer? </p>
</p> </BlockSlot>
</BlockSlot> <BlockSlot @name="confirm" as |Confirm|>
<BlockSlot @name="confirm" as |Confirm|> <Confirm>
<Confirm> Delete
Delete </Confirm>
</Confirm> </BlockSlot>
</BlockSlot> </Confirmation>
</Confirmation> </BlockSlot>
</BlockSlot> </Action>
</Action> </Actions>
</Actions> {{/if}}
{{/if}}
</BlockSlot> </BlockSlot>
</ListCollection> </ListCollection>

View File

@ -1,19 +1,19 @@
export const selectors = { export const selectors = {
$: '.consul-peer-list', $: ".consul-peer-list",
collection: { collection: {
$: '[data-test-list-row]', $: "[data-test-list-row]",
peer: { peer: {
$: 'li', $: "li",
name: { name: {
$: '[data-test-peer]' $: "[data-test-peer]",
} },
}, },
} },
}; };
export default (collection, isPresent, attribute, actions) => () => { export default (collection, isPresent, attribute, actions) => () => {
return collection(`${selectors.$} ${selectors.collection.$}`, { return collection(`${selectors.$} ${selectors.collection.$}`, {
peer: isPresent(selectors.collection.peer.$), peer: isPresent(selectors.collection.peer.$),
name: attribute('data-test-peer', selectors.collection.peer.name.$), name: attribute("data-test-peer", selectors.collection.peer.name.$),
...actions(['regenerate', 'delete']), ...actions(["regenerate", "delete", "view"]),
}); });
}; };

View File

@ -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;
}
}

View File

@ -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");
}
}

View File

@ -1,68 +0,0 @@
<Route
@name={{routeName}}
as |route|>
<DataLoader @src={{
uri '/${partition}/${nspace}/${dc}/peer/${name}'
(hash
partition=route.params.partition
nspace=route.params.nspace
dc=route.params.dc
name=route.params.name
)
}}
as |loader|>
<BlockSlot @name="error">
<AppError
@error={{loader.error}}
@login={{route.model.app.login.open}}
/>
</BlockSlot>
<BlockSlot @name="loaded">
{{#let
route.params.dc
route.params.partition
route.params.nspace
loader.data
as |dc partition nspace item|}}
<AppView>
<BlockSlot @name="breadcrumbs">
<ol>
<li><a data-test-back href={{href-to 'dc.peers'}}>All Peers</a></li>
</ol>
</BlockSlot>
<BlockSlot @name="header">
<h1>
<route.Title
@title={{item.Name}}
/>
</h1>
</BlockSlot>
<BlockSlot @name="content">
<TabNav @items={{
compact
(array
(hash
label="Addresses"
href=(href-to "dc.peers.edit.addresses")
selected=(is-href "dc.peers.edit.addresses")
)
)
}}/>
<Outlet
@name={{routeName}}
@model={{assign (hash
items=item.PeerServerAddresses
) route.model}}
as |o|>
{{outlet}}
</Outlet>
</BlockSlot>
</AppView>
{{/let}}
</BlockSlot>
</DataLoader>
</Route>

View File

@ -1,7 +0,0 @@
<Route
@name={{routeName}}
as |route|>
<Consul::Peer::Address::List
@items={{route.model.items}}
/>
</Route>

View File

@ -1,6 +0,0 @@
<Route
@name={{routeName}}
as |route|>
{{did-insert (route-action 'replaceWith' 'dc.peers.edit.addresses')}}
</Route>

View File

@ -0,0 +1,59 @@
<Route @name={{routeName}} as |route|>
<DataLoader
@src={{uri
"/${partition}/${nspace}/${dc}/peer/${name}"
(hash
partition=route.params.partition
nspace=route.params.nspace
dc=route.params.dc
name=route.params.name
)
}}
as |loader|
>
<BlockSlot @name="error">
<AppError @error={{loader.error}} @login={{route.model.app.login.open}} />
</BlockSlot>
<BlockSlot @name="loaded">
{{#let
route.params.dc
route.params.partition
route.params.nspace
loader.data
as |dc partition nspace item|
}}
<AppView>
<BlockSlot @name="breadcrumbs">
<ol>
<li><a data-test-back href={{href-to "dc.peers"}}>All Peers</a></li>
</ol>
</BlockSlot>
<BlockSlot @name="header">
<h1>
<route.Title @title={{item.Name}} />
</h1>
</BlockSlot>
<BlockSlot @name="content">
<Consul::Peer::BentoBox @peering={{item}} />
<Peerings::Provider @peer={{item}} as |peering|>
<TabNav @items={{peering.data.tabs}} />
</Peerings::Provider>
<Outlet
@name={{routeName}}
@model={{assign
(hash items=item.PeerServerAddresses peer=item)
route.model
}}
as |o|
>
{{outlet}}
</Outlet>
</BlockSlot>
</AppView>
{{/let}}
</BlockSlot>
</DataLoader>
</Route>

View File

@ -0,0 +1,38 @@
<Route @name={{routeName}} as |route|>
{{#if (gt route.model.items.length 0)}}
<Consul::Peer::Address::List @items={{route.model.items}} />
{{else}}
<EmptyState @login={{route.model.app.login.open}} data-test-addresses-empty>
<BlockSlot @name="header">
<h2>
{{t "routes.dc.peers.show.addresses.empty.header"}}
</h2>
</BlockSlot>
<BlockSlot @name="body">
{{t "routes.dc.peers.show.addresses.empty.body" htmlSafe=true}}
</BlockSlot>
<BlockSlot @name="actions">
<li class="docs-link">
<a
href="{{env 'CONSUL_DOCS_URL'}}/connect/cluster-peering"
rel="noopener noreferrer"
target="_blank"
>
Documentation on Peers
</a>
</li>
<li class="learn-link">
<a
href="{{env
'CONSUL_DOCS_URL'
}}/connect/cluster-peering/create-manage-peering"
rel="noopener noreferrer"
target="_blank"
>
Take the tutorial
</a>
</li>
</BlockSlot>
</EmptyState>
{{/if}}
</Route>

View File

@ -0,0 +1,134 @@
<Route @name={{routeName}} as |route|>
<DataLoader
@src={{uri
"/${partition}/${nspace}/${dc}/exported-services/${name}"
(hash
partition=route.params.partition
nspace=route.params.nspace
dc=route.params.dc
name=route.model.peer.Name
)
}}
as |api|
>
{{#let
(or route.params.partition route.model.user.token.Partition "default")
api.data
as |partition items|
}}
<BlockSlot @name="error">
<AppError @error={{api.error}} @login={{route.model.app.login.open}} />
</BlockSlot>
<BlockSlot @name="loaded">
{{#if items.length}}
<div class="search-bar">
<form class="filter-bar">
<FreetextFilter
@onsearch={{pick "target.value" this.updateSearch}}
@value={{this.search}}
@placeholder="Search"
class="!w-80"
/>
</form>
</div>
{{/if}}
<Providers::Search
@items={{items}}
@search={{this.search}}
@searchProperties={{array "Name"}}
as |search|
>
<Providers::Dimension as |p|>
{{#if p.data.height}}
<div
style={{p.data.fillRemainingHeightStyle}}
class="overflow-y-scroll"
>
{{#if search.data.items.length}}
<VerticalCollection
@tagName="ul"
@estimateHeight={{p.data.height}}
@items={{search.data.items}}
as |service index|
>
<li class="px-3 h-12 border-b border-hds-border-primary">
<a
data-test-service-name
class="hds-typography-display-300 text-hds-foreground-strong hds-font-weight-semibold h-full w-full flex items-center"
href={{href-to
"dc.services.show.index"
service.Name
params=(if
(not-eq service.Partition partition)
(hash
partition=service.Partition
nspace=service.Namespace
peer=service.PeerName
)
(hash peer=service.PeerName)
)
}}
>
{{service.Name}}
</a>
</li>
</VerticalCollection>
{{else}}
<EmptyState
@login={{route.model.app.login.open}}
data-test-exported-services-empty
>
<BlockSlot @name="header">
<h2>
{{t
"routes.dc.peers.show.exported.empty.header"
name=route.model.peer.Name
}}
</h2>
</BlockSlot>
<BlockSlot @name="body">
{{t
"routes.dc.peers.show.exported.empty.body"
items=items.length
name=route.model.peer.Name
htmlSafe=true
}}
</BlockSlot>
<BlockSlot @name="actions">
<li class="docs-link">
<a
href="{{env
'CONSUL_DOCS_URL'
}}/connect/cluster-peering"
rel="noopener noreferrer"
target="_blank"
>
Documentation on Peers
</a>
</li>
<li class="learn-link">
<a
href="{{env
'CONSUL_DOCS_URL'
}}/connect/cluster-peering/create-manage-peering"
rel="noopener noreferrer"
target="_blank"
>
Take the tutorial
</a>
</li>
</BlockSlot>
</EmptyState>
{{/if}}
</div>
{{/if}}
</Providers::Dimension>
</Providers::Search>
</BlockSlot>
{{/let}}
</DataLoader>
</Route>

View File

@ -0,0 +1,133 @@
<Route @name={{routeName}} as |route|>
<DataLoader
@src={{uri
"/${partition}/${nspace}/${dc}/services/${peer}/${peerId}"
(hash
partition=route.params.partition
nspace=route.params.nspace
dc=route.params.dc
peer=route.model.peer.Name
peerId=route.model.peer.id
)
}}
as |api|
>
<BlockSlot @name="error">
<AppError @error={{api.error}} @login={{route.model.app.login.open}} />
</BlockSlot>
<BlockSlot @name="loaded">
{{#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|}}
<Consul::Service::SearchBar
@sources={{get items "ExternalSources"}}
@partitions={{get items "Partitions"}}
@partition={{partition}}
@search={{search}}
@onsearch={{action (mut search) value="target.value"}}
@sort={{sort}}
@filter={{filters}}
@peer={{route.model.peer}}
/>
{{/let}}
{{/if}}
<DataCollection
@type="service"
@sort={{sort.value}}
@filters={{filters}}
@search={{search}}
@items={{items}}
as |collection|
>
<collection.Collection>
<Consul::Service::List
@items={{collection.items}}
@partition={{partition}}
@isPeerDetail={{true}}
/>
</collection.Collection>
<collection.Empty>
<EmptyState
@login={{route.model.app.login.open}}
data-test-imported-services-empty
>
<BlockSlot @name="header">
<h2>
{{t
"routes.dc.peers.show.imported.empty.header"
name=route.model.peer.Name
}}
</h2>
</BlockSlot>
<BlockSlot @name="body">
{{t
"routes.dc.peers.show.imported.empty.body"
items=items.length
name=route.model.peer.Name
htmlSafe=true
}}
</BlockSlot>
<BlockSlot @name="actions">
<li class="docs-link">
<a
href="{{env 'CONSUL_DOCS_URL'}}/connect/cluster-peering"
rel="noopener noreferrer"
target="_blank"
>
Documentation on Peers
</a>
</li>
<li class="learn-link">
<a
href="{{env
'CONSUL_DOCS_URL'
}}/connect/cluster-peering/create-manage-peering"
rel="noopener noreferrer"
target="_blank"
>
Take the tutorial
</a>
</li>
</BlockSlot>
</EmptyState>
</collection.Empty>
</DataCollection>
{{/let}}
</BlockSlot>
</DataLoader>
</Route>

View File

@ -0,0 +1,3 @@
<Route @name={{routeName}} as |route|>
{{did-insert this.transitionToImported}}
</Route>

View File

@ -1,30 +1,76 @@
(routes => routes({ ((routes) =>
dc: { routes({
peers: { dc: {
_options: { peers: {
path: '/peers'
},
index: {
_options: { _options: {
path: '/', path: "/peers",
queryParams: { },
sortBy: 'sort', index: {
state: 'state', _options: {
searchproperty: { path: "/",
as: 'searchproperty', queryParams: {
empty: [['Name', 'ID']], 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); data[`routes`] = JSON.stringify(json);
} }
); );

View File

@ -96,6 +96,12 @@ module.exports = {
urlSchema: 'auto', urlSchema: 'auto',
urlPrefix: 'docs/consul', 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`, root: `${path.dirname(require.resolve('consul-acls/package.json'))}/app/components`,
pattern: '**/README.mdx', pattern: '**/README.mdx',
@ -129,6 +135,7 @@ module.exports = {
].concat(user.sources), ].concat(user.sources),
labels: { labels: {
consul: 'Consul Components', consul: 'Consul Components',
providers: 'Provider Components',
...user.labels, ...user.labels,
}, },
}; };

View File

@ -1,7 +1,7 @@
import Adapter from './application'; import Adapter from './application';
export default class ServiceAdapter extends Adapter { 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') { if (typeof gateway !== 'undefined') {
return request` return request`
GET /v1/internal/ui/gateway-services-nodes/${gateway}?${{ dc }} GET /v1/internal/ui/gateway-services-nodes/${gateway}?${{ dc }}
@ -16,13 +16,14 @@ export default class ServiceAdapter extends Adapter {
`; `;
} else { } else {
return request` return request`
GET /v1/internal/ui/services?${{ dc }} GET /v1/internal/ui/services?${{ dc, peer }}
X-Request-ID: ${uri} X-Request-ID: ${uri}
${{ ${{
ns, ns,
partition, partition,
index, index,
peer,
}} }}
`; `;
} }

View File

@ -1,97 +1,89 @@
<ListCollection <ListCollection
class="consul-service-list" class='consul-service-list'
...attributes ...attributes
@items={{@items}} @items={{@items}}
@linkable="linkable service" @linkable='linkable service'
as |item index| as |item index|
> >
<BlockSlot @name="header"> <BlockSlot @name='header'>
<dl class={{item.MeshStatus}}> <dl class={{item.MeshStatus}}>
<dt> <dt>
Health Health
</dt> </dt>
<dd> <dd {{tooltip item.healthTooltipText}}></dd>
<Tooltip @position="top-start">
{{#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}}
</Tooltip>
</dd>
</dl> </dl>
{{#if (gt item.InstanceCount 0)}} {{#if (gt item.InstanceCount 0)}}
<a <a
data-test-service-name data-test-service-name
href={{href-to "dc.services.show.index" item.Name href={{href-to
params=(if (not-eq item.Partition @partition) 'dc.services.show.index'
(hash item.Name
partition=item.Partition params=(if
nspace=item.Namespace (not-eq item.Partition @partition)
peer=item.PeerName (hash partition=item.Partition nspace=item.Namespace peer=item.PeerName)
) (hash peer=item.PeerName)
(hash )
peer=item.PeerName }}
) >
) {{item.Name}}
}} </a>
> {{else}}
{{item.Name}} <p data-test-service-name>
</a> {{item.Name}}
{{else}} </p>
<p data-test-service-name> {{/if}}
{{item.Name}}
</p>
{{/if}}
</BlockSlot> </BlockSlot>
<BlockSlot @name="details"> <BlockSlot @name='details'>
<Consul::Kind @item={{item}} /> <Consul::Kind @item={{item}} />
<Consul::ExternalSource @item={{item}} /> <Consul::ExternalSource @item={{item}} />
{{#if (and (not-eq item.InstanceCount 0) (and (not-eq item.Kind 'terminating-gateway') (not-eq item.Kind 'ingress-gateway'))) }} {{#if
<span> (and
{{format-number item.InstanceCount}} {{pluralize item.InstanceCount 'instance' without-count=true}} (not-eq item.InstanceCount 0)
</span> (and (not-eq item.Kind 'terminating-gateway') (not-eq item.Kind 'ingress-gateway'))
{{/if}} )
<Consul::Bucket::List }}
@item={{item}} <span>
@nspace={{@nspace}} {{format-number item.InstanceCount}}
@partition={{@partition}} {{pluralize item.InstanceCount 'instance' without-count=true}}
/> </span>
{{#if (eq item.Kind 'terminating-gateway')}} {{/if}}
<span data-test-associated-service-count> {{! we are displaying imported-services - don't show bucket-list }}
{{format-number item.GatewayConfig.AssociatedServiceCount}} {{pluralize item.GatewayConfig.AssociatedServiceCount 'linked service' without-count=true}} {{#unless @isPeerDetail}}
</span> <Consul::Bucket::List @item={{item}} @nspace={{@nspace}} @partition={{@partition}} />
{{else if (eq item.Kind 'ingress-gateway')}} {{/unless}}
<span data-test-associated-service-count> {{#if (eq item.Kind 'terminating-gateway')}}
{{format-number item.GatewayConfig.AssociatedServiceCount}} {{pluralize item.GatewayConfig.AssociatedServiceCount 'upstream' without-count=true}} <span data-test-associated-service-count>
</span> {{format-number item.GatewayConfig.AssociatedServiceCount}}
{{/if}} {{pluralize item.GatewayConfig.AssociatedServiceCount 'linked service' without-count=true}}
{{#if (or item.ConnectedWithGateway item.ConnectedWithProxy)}} </span>
<dl class="mesh"> {{else if (eq item.Kind 'ingress-gateway')}}
<dt> <span data-test-associated-service-count>
<Tooltip> {{format-number item.GatewayConfig.AssociatedServiceCount}}
This service uses a proxy for the Consul service mesh {{pluralize item.GatewayConfig.AssociatedServiceCount 'upstream' without-count=true}}
</Tooltip> </span>
</dt> {{/if}}
{{#if (and item.ConnectedWithGateway item.ConnectedWithProxy )}} {{#if (or item.ConnectedWithGateway item.ConnectedWithProxy)}}
<dd data-test-mesh> <dl class='mesh'>
in service mesh with proxy and gateway <dt>
</dd> <Tooltip>
{{else if item.ConnectedWithProxy}} This service uses a proxy for the Consul service mesh
<dd data-test-mesh> </Tooltip>
in service mesh with proxy </dt>
</dd> {{#if (and item.ConnectedWithGateway item.ConnectedWithProxy)}}
{{else if item.ConnectedWithGateway}} <dd data-test-mesh>
<dd data-test-mesh> in service mesh with proxy and gateway
in service mesh with gateway </dd>
</dd> {{else if item.ConnectedWithProxy}}
{{/if}} <dd data-test-mesh>
</dl> in service mesh with proxy
{{/if}} </dd>
{{else if item.ConnectedWithGateway}}
<dd data-test-mesh>
in service mesh with gateway
</dd>
{{/if}}
</dl>
{{/if}}
<TagList @item={{item}} /> <TagList @item={{item}} />
</BlockSlot> </BlockSlot>
</ListCollection> </ListCollection>

View File

@ -1,195 +1,208 @@
<SearchBar <SearchBar class='consul-service-search-bar' ...attributes @filter={{@filter}}>
class="consul-service-search-bar" <:status as |search|>
...attributes
@filter={{@filter}}
>
<:status as |search|>
{{#let {{#let
(t
(t (concat "components.consul.service.search-bar." search.status.key) (concat 'components.consul.service.search-bar.' search.status.key)
default=(array default=(array
(concat "common.search." search.status.key) (concat 'common.search.' search.status.key) (concat 'common.consul.' search.status.key)
(concat "common.consul." search.status.key) )
) )
) (t
(concat 'components.consul.service.search-bar.' search.status.value)
(t (concat "components.consul.service.search-bar." search.status.value) default=(array
default=(array (concat 'common.search.' search.status.value)
(concat "common.search." search.status.value) (concat 'common.consul.' search.status.value)
(concat "common.consul." search.status.value) (concat 'common.brand.' search.status.value)
(concat "common.brand." search.status.value) )
) )
) as |key value|
}}
as |key value|}} <search.RemoveFilter aria-label={{t 'common.ui.remove' item=(concat key ' ' value)}}>
<search.RemoveFilter
aria-label={{t "common.ui.remove" item=(concat key " " value)}}
>
<dl> <dl>
<dt>{{key}}</dt> <dt>{{key}}</dt>
<dd>{{value}}</dd> <dd>{{value}}</dd>
</dl> </dl>
</search.RemoveFilter> </search.RemoveFilter>
{{/let}} {{/let}}
</:status> </:status>
<:search as |search|> <:search as |search|>
<search.Search <search.Search
@onsearch={{action @onsearch}} @onsearch={{action @onsearch}}
@value={{@search}} @value={{@search}}
@placeholder={{t "common.search.search"}} @placeholder={{t 'common.search.search'}}
>
<search.Select
class='type-search-properties'
@position='right'
@onchange={{action @filter.searchproperty.change}}
@multiple={{true}}
@required={{true}}
as |components|
> >
<search.Select <BlockSlot @name='selected'>
class="type-search-properties" <span>
@position="right" {{t 'common.search.searchproperty'}}
@onchange={{action @filter.searchproperty.change}} </span>
@multiple={{true}} </BlockSlot>
@required={{true}} <BlockSlot @name='options'>
as |components|> {{#let components.Optgroup components.Option as |Optgroup Option|}}
<BlockSlot @name="selected">
<span>
{{t "common.search.searchproperty"}}
</span>
</BlockSlot>
<BlockSlot @name="options">
{{#let components.Optgroup components.Option as |Optgroup Option|}}
{{#each @filter.searchproperty.default as |prop|}} {{#each @filter.searchproperty.default as |prop|}}
<Option @value={{prop}} @selected={{includes prop @filter.searchproperty.value}}> <Option @value={{prop}} @selected={{includes prop @filter.searchproperty.value}}>
{{t (concat "common.consul." (lowercase prop))}} {{t (concat 'common.consul.' (lowercase prop))}}
</Option> </Option>
{{/each}} {{/each}}
{{/let}} {{/let}}
</BlockSlot>
</search.Select>
</search.Search>
</:search>
<:filter as |search|>
<search.Select
class="type-status"
@position="left"
@onchange={{action @filter.status.change}}
@multiple={{true}}
as |components|>
<BlockSlot @name="selected">
<span>
{{t "common.consul.status"}}
</span>
</BlockSlot>
<BlockSlot @name="options">
{{#let components.Optgroup components.Option as |Optgroup Option|}}
{{#each (array "passing" "warning" "critical" "empty") as |state|}}
<Option class="value-{{state}}" @value={{state}} @selected={{includes state @filter.status.value}}>
{{t (concat "common.consul." state)
default=(array
(concat "common.search." state)
)
}}
</Option>
{{/each}}
{{/let}}
</BlockSlot> </BlockSlot>
</search.Select> </search.Select>
<search.Select </search.Search>
@position="left" </:search>
@onchange={{action @filter.kind.change}} <:filter as |search|>
@multiple={{true}} <search.Select
as |components|> class='type-status'
<BlockSlot @name="selected"> @position='left'
<span> @onchange={{action @filter.status.change}}
{{t "components.consul.service.search-bar.kind"}} @multiple={{true}}
</span> as |components|
</BlockSlot> >
<BlockSlot @name="options"> <BlockSlot @name='selected'>
{{#let components.Optgroup components.Option as |Optgroup Option|}} <span>
<Option @value="service" @selected={{includes 'service' @filter.kind.value}}> {{t 'common.consul.status'}}
{{t "common.consul.service"}} </span>
</BlockSlot>
<BlockSlot @name='options'>
{{#let components.Optgroup components.Option as |Optgroup Option|}}
{{#each this.healthStates as |state|}}
<Option
class='value-{{state}}'
@value={{state}}
@selected={{includes state @filter.status.value}}
>
{{t (concat 'common.consul.' state) default=(array (concat 'common.search.' state))}}
</Option>
{{/each}}
{{/let}}
</BlockSlot>
</search.Select>
<search.Select
@position='left'
@onchange={{action @filter.kind.change}}
@multiple={{true}}
as |components|
>
<BlockSlot @name='selected'>
<span>
{{t 'components.consul.service.search-bar.kind'}}
</span>
</BlockSlot>
<BlockSlot @name='options'>
{{#let components.Optgroup components.Option as |Optgroup Option|}}
<Option @value='service' @selected={{includes 'service' @filter.kind.value}}>
{{t 'common.consul.service'}}
</Option> </Option>
<Optgroup <Optgroup @label={{t 'common.consul.gateway'}}>
@label={{t "common.consul.gateway"}} {{#each (array 'ingress-gateway' 'terminating-gateway' 'mesh-gateway') as |kind|}}
> <Option @value={{kind}} @selected={{includes kind @filter.kind.value}}>
{{#each (array "ingress-gateway" "terminating-gateway" "mesh-gateway") as |kind|}} {{t (concat 'common.consul.' kind)}}
<Option @value={{kind}} @selected={{includes kind @filter.kind.value}}> </Option>
{{t (concat "common.consul." kind)}} {{/each}}
</Option>
{{/each}}
</Optgroup> </Optgroup>
<Optgroup <Optgroup @label={{t 'common.consul.mesh'}}>
@label={{t "common.consul.mesh"}} {{#each (array 'in-mesh' 'not-in-mesh') as |state|}}
> <Option @value={{state}} @selected={{includes state @filter.kind.value}}>
{{#each (array "in-mesh" "not-in-mesh") as |state|}} {{t (concat 'common.search.' state)}}
<Option @value={{state}} @selected={{includes state @filter.kind.value}}> </Option>
{{t (concat "common.search." state)}} {{/each}}
</Option>
{{/each}}
</Optgroup> </Optgroup>
{{/let}} {{/let}}
</BlockSlot> </BlockSlot>
</search.Select> </search.Select>
{{#if (gt @sources.length 0)}} {{#if (gt @sources.length 0)}}
<search.Select <search.Select
class="type-source" class='type-source'
@position="left" @position='left'
@onchange={{action @filter.source.change}} @onchange={{action @filter.source.change}}
@multiple={{true}} @multiple={{true}}
as |components|> as |components|
<BlockSlot @name="selected"> >
<BlockSlot @name='selected'>
<span> <span>
{{t "common.search.source"}} {{t 'common.search.source'}}
</span> </span>
</BlockSlot> </BlockSlot>
<BlockSlot @name="options"> <BlockSlot @name='options'>
{{#let components.Option as |Option|}} {{#let components.Option as |Option|}}
{{#if (gt @sources.length 0)}} {{#if (gt @sources.length 0)}}
{{#each @sources as |source|}} {{#each @sources as |source|}}
<Option class={{source}} @value={{source}} @selected={{includes source @filter.source.value}}> <Option
{{t (concat "common.brand." source)}} class={{source}}
</Option> @value={{source}}
{{/each}} @selected={{includes source @filter.source.value}}
<Option class="consul" @value='consul' @selected={{includes 'consul' @filter.source.value}}> >
{{t 'common.brand.consul'}} {{t (concat 'common.brand.' source)}}
</Option> </Option>
{{/if}} {{/each}}
{{/let}} <Option
class='consul'
@value='consul'
@selected={{includes 'consul' @filter.source.value}}
>
{{t 'common.brand.consul'}}
</Option>
{{/if}}
{{/let}}
</BlockSlot> </BlockSlot>
</search.Select> </search.Select>
{{/if}} {{/if}}
</:filter> </:filter>
<:sort as |search|> <:sort as |search|>
<search.Select <search.Select
class="type-sort" class='type-sort'
data-test-sort-control data-test-sort-control
@position="right" @position='right'
@onchange={{action @sort.change}} @onchange={{action @sort.change}}
@multiple={{false}} @multiple={{false}}
@required={{true}} @required={{true}}
as |components|> as |components|
<BlockSlot @name="selected"> >
<span> <BlockSlot @name='selected'>
{{#let (from-entries (array <span>
(array "Name:asc" (t "common.sort.alpha.asc")) {{#let
(array "Name:desc" (t "common.sort.alpha.desc")) (from-entries
(array "Status:asc" (t "common.sort.status.asc")) (array
(array "Status:desc" (t "common.sort.status.desc")) (array 'Name:asc' (t 'common.sort.alpha.asc'))
)) (array 'Name:desc' (t 'common.sort.alpha.desc'))
as |selectable| (array 'Status:asc' (t 'common.sort.status.asc'))
}} (array 'Status:desc' (t 'common.sort.status.desc'))
{{get selectable @sort.value}} )
{{/let}} )
</span> as |selectable|
</BlockSlot> }}
<BlockSlot @name="options"> {{get selectable @sort.value}}
{{#let components.Optgroup components.Option as |Optgroup Option|}} {{/let}}
<Optgroup @label={{t "common.consul.status"}}> </span>
<Option @value="Status:asc" @selected={{eq "Status:asc" @sort.value}}>{{t "common.sort.status.asc"}}</Option> </BlockSlot>
<Option @value="Status:desc" @selected={{eq "Status:desc" @sort.value}}>{{t "common.sort.status.desc"}}</Option> <BlockSlot @name='options'>
{{#let components.Optgroup components.Option as |Optgroup Option|}}
<Optgroup @label={{t 'common.consul.status'}}>
<Option @value='Status:asc' @selected={{eq 'Status:asc' @sort.value}}>{{t
'common.sort.status.asc'
}}</Option>
<Option @value='Status:desc' @selected={{eq 'Status:desc' @sort.value}}>{{t
'common.sort.status.desc'
}}</Option>
</Optgroup> </Optgroup>
<Optgroup @label={{t "common.consul.service-name"}}> <Optgroup @label={{t 'common.consul.service-name'}}>
<Option @value="Name:asc" @selected={{eq "Name:asc" @sort.value}}>{{t "common.sort.alpha.asc"}}</Option> <Option @value='Name:asc' @selected={{eq 'Name:asc' @sort.value}}>{{t
<Option @value="Name:desc" @selected={{eq "Name:desc" @sort.value}}>{{t "common.sort.alpha.desc"}}</Option> 'common.sort.alpha.asc'
}}</Option>
<Option @value='Name:desc' @selected={{eq 'Name:desc' @sort.value}}>{{t
'common.sort.alpha.desc'
}}</Option>
</Optgroup> </Optgroup>
{{/let}} {{/let}}
</BlockSlot> </BlockSlot>
</search.Select> </search.Select>
</:sort> </:sort>
</SearchBar> </SearchBar>

View File

@ -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'];
}
}
}

View File

@ -25,6 +25,11 @@
@extend %with-minus-square-fill-mask, %as-pseudo; @extend %with-minus-square-fill-mask, %as-pseudo;
color: rgb(var(--tone-gray-500)); 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 { %composite-row-header [rel='me'] dd::before {
@extend %with-check-circle-fill-mask, %as-pseudo; @extend %with-check-circle-fill-mask, %as-pseudo;

View File

@ -79,10 +79,14 @@ export default class Outlet extends Component {
this.previousState = this.state; this.previousState = this.state;
this.state = new State('loading'); this.state = new State('loading');
this.endTransition = this.routlet.transition(); this.endTransition = this.routlet.transition();
// if we have no transition-duration set immediately end the transition let duration;
const duration = window if (this.element) {
.getComputedStyle(this.element) // if we have no transition-duration set immediately end the transition
.getPropertyValue('transition-duration'); duration = window.getComputedStyle(this.element).getPropertyValue('transition-duration');
} else {
duration = 0;
}
if (parseFloat(duration) === 0) { if (parseFloat(duration) === 0) {
this.endTransition(); this.endTransition();
} }

View File

@ -0,0 +1 @@
{{yield (hash data=this.data)}}

View File

@ -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 }));
}
}

View File

@ -42,6 +42,10 @@
@extend %with-minus-square-fill-mask, %as-pseudo; @extend %with-minus-square-fill-mask, %as-pseudo;
color: rgb(var(--tone-gray-400)); 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 { %popover-select.type-source li:not(.partition) button {
text-transform: capitalize; text-transform: capitalize;
} }

View File

@ -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
<Providers::Dimension as |p|>
<div style={{p.data.fillRemainingHeightStyle}}>
Fill remaining space
</div>
</Providers::Dimension>
```
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
<div class='h-48 relative flex border border-hds-consul-foreground'>
<div class='h-full w-24 relative'>
<div
class='absolute bottom-0 w-full h-12 bg-hds-consul-gradient-primary-start flex items-center justify-center'
id='bottom-boundary'
>
Boundary
</div>
</div>
<div class='flex-1'>
<Providers::Dimension @bottomBoundary='#bottom-boundary' as |p|>
<div
style={{p.data.fillRemainingHeightStyle}}
class='bg-hds-consul-surface flex items-center justify-center'
>
We could use flexbox here instead
</div>
</Providers::Dimension>
</div>
</div>
```

View File

@ -0,0 +1,4 @@
<div {{create-ref 'element'}} {{did-insert this.measureDimensions}}>
{{on-window 'resize' this.handleWindowResize}}
{{yield (hash data=this.data)}}
</div>

View File

@ -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);
}
}

View File

@ -0,0 +1 @@
{{yield (hash data=this.data)}}

View File

@ -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,
};
}
}

View File

@ -1,47 +1,50 @@
{{#let {{#let (dom-position (set this 'style') offset=true) 'tab' as |select name|}}
(dom-position (set this 'style') offset=true) <nav
"tab" style={{{if
as |select name|}} this.style
<nav (concat
style={{{if this.style (concat '--selected-width:'
'--selected-width:' this.style.width ';' this.style.width
'--selected-left:' this.style.left ';' ';'
'--selected-height:' this.style.height ';' '--selected-left:'
'--selected-top:' this.style.top this.style.left
) ';'
undefined '--selected-height:'
}}} this.style.height
aria-label="Secondary" ';'
class={{concat 'tab-nav' ' animatable'}} '--selected-top:'
...attributes this.style.top
>
<ul>
{{#each @items as |item|}}
<li
{{on 'click' (fn select)}}
{{did-upsert
(if item.selected
(fn select)
(noop)
) )
@items.length undefined
}} }}}
data-test-tab={{concat name '_' (if item.label (slugify item.label) (slugify item))}} aria-label='Secondary'
class={{if (or item.selected (eq selected (if item.label (slugify item.label) (slugify item)))) 'selected'}} class={{concat 'tab-nav' ' animatable'}}
...attributes
> >
<Action <ul>
{{on 'click' {{#each @items as |item|}}
(fn this.onClick (uppercase item.label)) <li
}} {{on 'click' (fn select)}}
{{on 'click' {{did-upsert (if item.selected (fn select) (noop)) @items.length}}
(fn this.onTabClicked item) data-test-tab={{concat name '_' (if item.label (slugify item.label) (slugify item))}}
}} class={{if
@href={{item.href}} (or item.selected (eq selected (if item.label (slugify item.label) (slugify item))))
> 'selected'
{{item.label}} }}
</Action> >
</li> <Action
{{/each}} {{on 'click' (fn this.onClick (uppercase item.label))}}
</ul> {{on 'click' (fn this.onTabClicked item)}}
</nav> @href={{item.href}}
>
{{#if item.tooltip}}
<span {{tooltip item.tooltip}}>{{item.label}}</span>
{{else}}
{{item.label}}
{{/if}}
</Action>
</li>
{{/each}}
</ul>
</nav>
{{/let}} {{/let}}

View File

@ -1,4 +1,76 @@
import Component from '@glimmer/component'; 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
* <TabNav @items={{array
* (hash
* label="First Tab"
* href=(href-to "some.route")
* selected=(is-href "some.route")
* )
* (hash
* label="Second Tab"
* href=(href-to "some.route")
* selected=(is-href "some.route")
* )
* }}
* ```
*
* You can do the following in a js-file:
*
* ```javascript
* export default class WootComponent extends Component {
* // ...
* get tabs() {
* const { router } = this;
* const owner = getOwner(this);
* return [
* new Tab({
* label: 'First Tab',
* route: 'some.route',
* currentRouteName: router.currentRouteName,
* owner
* }),
* // ...
* ];
* }
* }
* ```
*
*/
export class Tab {
@tracked route;
@tracked label;
@tracked tooltip;
@tracked currentRouteName;
constructor(opts) {
const { currentRouteName, route, label, tooltip, owner } = opts;
this.currentRouteName = currentRouteName;
this.owner = owner;
this.route = route;
this.label = label;
this.tooltip = tooltip;
}
get selected() {
return this.currentRouteName === this.route;
}
get href() {
return hrefTo(this.owner, [this.route]);
}
}
function noop() {} function noop() {}
export default class TabNav extends Component { export default class TabNav extends Component {

View File

@ -14,6 +14,7 @@ export default {
warning: (item, value) => item.MeshStatus === value, warning: (item, value) => item.MeshStatus === value,
critical: (item, value) => item.MeshStatus === value, critical: (item, value) => item.MeshStatus === value,
empty: (item, value) => item.MeshChecksTotal === 0, empty: (item, value) => item.MeshChecksTotal === 0,
unknown: (item) => item.peerIsFailing || item.isZeroCountButPeered,
}, },
instance: { instance: {
registered: (item, value) => item.InstanceCount > 0, registered: (item, value) => item.InstanceCount > 0,

View File

@ -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',
}),
};
}
}

View File

@ -188,10 +188,14 @@ export default class FSMWithOptionalLocation {
/** /**
* Turns a routeName into a full URL string for anchor hrefs etc. * 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') { if (typeof hash.dc !== 'undefined') {
delete hash.dc; delete hash.dc;
} }
if (typeof hash.nspace !== 'undefined') { if (typeof hash.nspace !== 'undefined') {
hash.nspace = `~${hash.nspace}`; hash.nspace = `~${hash.nspace}`;
} }

View File

@ -17,17 +17,34 @@ export default class Peer extends Model {
@attr('string') Name; @attr('string') Name;
@attr('string') State; @attr('string') State;
@attr('string') ID; @attr('string') ID;
// only the side that establishes will hold this property
@attr('string') PeerID;
@attr() PeerServerAddresses;
// StreamStatus
@nullValue([]) @attr() ImportedServices; @nullValue([]) @attr() ImportedServices;
@nullValue([]) @attr() ExportedServices; @nullValue([]) @attr() ExportedServices;
@attr('date') LastHeartbeat; @attr('date') LastHeartbeat;
@attr('date') LastReceive; @attr('date') LastReceive;
@attr('date') LastSend; @attr('date') LastSend;
@attr() PeerServerAddresses;
get ImportedServiceCount() { get ImportedServiceCount() {
return this.ImportedServices.length; return this.ImportedServices.length;
} }
get ExportedServiceCount() { get ExportedServiceCount() {
return this.ExportedServices.length; 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;
}
} }

View File

@ -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 { computed } from '@ember/object';
import { tracked } from '@glimmer/tracking'; import { tracked } from '@glimmer/tracking';
import { fragment } from 'ember-data-model-fragments/attributes'; import { fragment } from 'ember-data-model-fragments/attributes';
@ -58,6 +58,18 @@ export default class Service extends Model {
@attr() meta; // {} @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') @computed('ChecksPassing', 'ChecksWarning', 'ChecksCritical')
get ChecksTotal() { get ChecksTotal() {
return this.ChecksPassing + this.ChecksWarning + this.ChecksCritical; return this.ChecksPassing + this.ChecksWarning + this.ChecksCritical;
@ -79,9 +91,19 @@ export default class Service extends Model {
return this.MeshEnabled || (this.Kind || '').length > 0; return this.MeshEnabled || (this.Kind || '').length > 0;
} }
@computed('MeshChecksPassing', 'MeshChecksWarning', 'MeshChecksCritical') @computed(
'MeshChecksPassing',
'MeshChecksWarning',
'MeshChecksCritical',
'isZeroCountButPeered',
'peerIsFailing'
)
get MeshStatus() { get MeshStatus() {
switch (true) { switch (true) {
case this.isZeroCountButPeered:
return 'unknown';
case this.peerIsFailing:
return 'unknown';
case this.MeshChecksCritical !== 0: case this.MeshChecksCritical !== 0:
return 'critical'; return 'critical';
case this.MeshChecksWarning !== 0: 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') @computed('ChecksPassing', 'Proxy.ChecksPassing')
get MeshChecksPassing() { get MeshChecksPassing() {
let proxyCount = 0; let proxyCount = 0;

View File

@ -1,6 +1,10 @@
import Serializer from './application'; import Serializer from './application';
import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/service'; import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/service';
import { get } from '@ember/object'; 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 { export default class ServiceSerializer extends Serializer {
primaryKey = PRIMARY_KEY; 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 // Services and proxies all come together in the same list. Here we
// map the proxies to their related services on a Service.Proxy // map the proxies to their related services on a Service.Proxy
// property for easy access later on // property for easy access later on
const services = {}; return cb(headers, this._transformServicesPayload(body));
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);
}), }),
query query
); );
@ -58,4 +38,56 @@ export default class ServiceSerializer extends Serializer {
query 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;
}
} }

View File

@ -1,5 +1,6 @@
import RepositoryService from 'consul-ui/services/repository'; import RepositoryService from 'consul-ui/services/repository';
import dataSource from 'consul-ui/decorators/data-source'; import dataSource from 'consul-ui/decorators/data-source';
import { inject as service } from '@ember/service';
function normalizePeerPayload(peerPayload, dc, partition) { function normalizePeerPayload(peerPayload, dc, partition) {
const { const {
@ -18,10 +19,31 @@ function normalizePeerPayload(peerPayload, dc, partition) {
}; };
} }
export default class PeerService extends RepositoryService { export default class PeerService extends RepositoryService {
@service store;
getModelName() { getModelName() {
return 'peer'; 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') @dataSource('/:partition/:ns/:dc/peering/token-for/:name')
async fetchToken({ dc, ns, partition, name }, configuration, request) { async fetchToken({ dc, ns, partition, name }, configuration, request) {
return ( return (
@ -85,6 +107,22 @@ export default class PeerService extends RepositoryService {
}} }}
` `
)((headers, body, cache) => { )((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 { return {
meta: { meta: {
version: 2, version: 2,

View File

@ -1,8 +1,11 @@
import RepositoryService from 'consul-ui/services/repository'; import RepositoryService from 'consul-ui/services/repository';
import dataSource from 'consul-ui/decorators/data-source'; import dataSource from 'consul-ui/decorators/data-source';
import { inject as service } from '@ember/service';
const modelName = 'service'; const modelName = 'service';
export default class ServiceService extends RepositoryService { export default class ServiceService extends RepositoryService {
@service store;
getModelName() { getModelName() {
return modelName; return modelName;
} }
@ -12,6 +15,23 @@ export default class ServiceService extends RepositoryService {
return super.findAll(...arguments); 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') @dataSource('/:partition/:ns/:dc/gateways/for-service/:gateway')
findGatewayBySlug(params, configuration = {}) { findGatewayBySlug(params, configuration = {}) {
if (typeof configuration.cursor !== 'undefined') { if (typeof configuration.cursor !== 'undefined') {

View File

@ -119,7 +119,8 @@ html:not(.with-data-source) .data-source-debug {
/* hi */ /* hi */
z-index: 100000; z-index: 100000;
} }
html.with-href-to [href^='console://']::before { html.with-href-to [href^='console://']::before
{
@extend %p3; @extend %p3;
@extend %debug-box; @extend %debug-box;
content: attr(href); content: attr(href);
@ -155,6 +156,7 @@ html.with-route-announcer .route-title {
margin-bottom: 100px; margin-bottom: 100px;
padding-top: 0 !important; padding-top: 0 !important;
} }
li.provider-components a::before,
li.consul-components a::before, li.consul-components a::before,
li.components a::before { li.components a::before {
@extend %with-logo-glimmer-color-icon, %as-pseudo; @extend %with-logo-glimmer-color-icon, %as-pseudo;
@ -164,6 +166,7 @@ html.with-route-announcer .route-title {
li.components.css-component a::before { li.components.css-component a::before {
@extend %with-logo-glimmer-color-icon, %as-pseudo; @extend %with-logo-glimmer-color-icon, %as-pseudo;
} }
li.provider-components.ember-component a::before,
li.consul-components.ember-component a::before, li.consul-components.ember-component a::before,
li.components.ember-component a::before { li.components.ember-component a::before {
@extend %with-logo-ember-circle-color-icon, %as-pseudo; @extend %with-logo-ember-circle-color-icon, %as-pseudo;

View File

@ -1,36 +1,32 @@
<Route <Route @name={{routeName}} as |route|>
@name={{routeName}}
as |route|>
<DataLoader <DataLoader
@src={{uri '/${partition}/${nspace}/${dc}/service-instance/${id}/${node}/${name}/${peer}' @src={{uri
(hash '/${partition}/${nspace}/${dc}/service-instance/${id}/${node}/${name}/${peer}'
partition=route.params.partition (hash
nspace=route.params.nspace partition=route.params.partition
dc=route.params.dc nspace=route.params.nspace
id=route.params.id dc=route.params.dc
node=route.params.node id=route.params.id
name=route.params.name node=route.params.node
peer=route.params.peer name=route.params.name
) peer=route.params.peer
}} )
as |loader|> }}
as |loader|
>
<BlockSlot @name="error"> <BlockSlot @name='error'>
<AppError <AppError @error={{loader.error}} @login={{route.model.app.login.open}} />
@error={{loader.error}}
@login={{route.model.app.login.open}}
/>
</BlockSlot> </BlockSlot>
<BlockSlot @name="disconnected" as |after|> <BlockSlot @name='disconnected' as |after|>
{{#if (eq loader.error.status "404")}} {{#if (eq loader.error.status '404')}}
<Notice <Notice
{{notification {{notification sticky=true}}
sticky=true class='notification-update'
}} @type='warning'
class="notification-update" as |notice|
@type="warning" >
as |notice|>
<notice.Header> <notice.Header>
<strong>Warning!</strong> <strong>Warning!</strong>
</notice.Header> </notice.Header>
@ -40,14 +36,8 @@ as |route|>
</p> </p>
</notice.Body> </notice.Body>
</Notice> </Notice>
{{else if (eq loader.error.status "403")}} {{else if (eq loader.error.status '403')}}
<Notice <Notice {{notification sticky=true}} class='notification-update' @type='error' as |notice|>
{{notification
sticky=true
}}
class="notification-update"
@type="error"
as |notice|>
<notice.Header> <notice.Header>
<strong>Error!</strong> <strong>Error!</strong>
</notice.Header> </notice.Header>
@ -58,13 +48,7 @@ as |route|>
</notice.Body> </notice.Body>
</Notice> </Notice>
{{else}} {{else}}
<Notice <Notice {{notification sticky=true}} class='notification-update' @type='error' as |notice|>
{{notification
sticky=true
}}
class="notification-update"
@type="error"
as |notice|>
<notice.Header> <notice.Header>
<strong>Warning!</strong> <strong>Warning!</strong>
</notice.Header> </notice.Header>
@ -77,64 +61,67 @@ as |route|>
{{/if}} {{/if}}
</BlockSlot> </BlockSlot>
<BlockSlot @name="loaded"> <BlockSlot @name='loaded'>
{{#let {{#let loader.data as |item|}}
{{#if item.IsOrigin}}
loader.data <DataSource
@src={{uri
as |item|}} '/${partition}/${nspace}/${dc}/proxy-instance/${id}/${node}/${name}'
{{#if item.IsOrigin}} (hash
<DataSource partition=route.params.partition
@src={{uri '/${partition}/${nspace}/${dc}/proxy-instance/${id}/${node}/${name}' nspace=route.params.nspace
(hash dc=route.params.dc
partition=route.params.partition id=route.params.id
nspace=route.params.nspace node=route.params.node
dc=route.params.dc name=route.params.name
id=route.params.id )
node=route.params.node }}
name=route.params.name @onchange={{action (mut meta) value='data'}}
) as |meta|
}} >
@onchange={{action (mut meta) value="data"}} {{! We only really need meta to get the correct ServiceID }}
as |meta|> {{! but we may as well use the NodeName and ServiceName }}
{{! We only really need meta to get the correct ServiceID }} {{! from meta also, but they should be the same as the instance }}
{{! but we may as well use the NodeName and ServiceName }} {{! so if we can ever get ServiceID from elsewhere we could save }}
{{! from meta also, but they should be the same as the instance }} {{! a HTTP request/long poll here }}
{{! so if we can ever get ServiceID from elsewhere we could save }} {{#if meta.data.ServiceID}}
{{! a HTTP request/long poll here }} {{! if we have a proxy then get the additional instance information }}
{{#if meta.data.ServiceID}} {{! for the proxy itself so if the service is called `backend` }}
{{! if we have a proxy then get the additional instance information }} {{! its likely to have a proxy service called `backend-sidecar-proxy` }}
{{! for the proxy itself so if the service is called `backend` }} {{! and this second request get the info for that instance and saves }}
{{! its likely to have a proxy service called `backend-sidecar-proxy` }} {{! it into the `proxy` variable }}
{{! and this second request get the info for that instance and saves }} <DataSource
{{! it into the `proxy` variable }} @src={{uri
<DataSource '/${partition}/${nspace}/${dc}/service-instance/${id}/${node}/${name}/${peer}'
@src={{uri '/${partition}/${nspace}/${dc}/service-instance/${id}/${node}/${name}/${peer}' (hash
(hash partition=route.params.partition
partition=route.params.partition nspace=route.params.nspace
nspace=route.params.nspace dc=route.params.dc
dc=route.params.dc id=meta.data.ServiceID
id=meta.data.ServiceID node=meta.data.NodeName
node=meta.data.NodeName name=meta.data.ServiceName
name=meta.data.ServiceName peer=route.params.peer
peer=route.params.peer )
) }}
}} @onchange={{action (mut proxy) value='data'}}
@onchange={{action (mut proxy) value="data"}} />
/> {{/if}}
{{/if}} </DataSource>
</DataSource> {{/if}}
{{/if}} <AppView>
<AppView> <BlockSlot @name='breadcrumbs'>
<BlockSlot @name="breadcrumbs"> <ol>
<ol> <li><a href={{href-to 'dc.services' params=(hash peer=undefined)}}>All Services</a></li>
<li><a href={{href-to 'dc.services' params=(hash peer=undefined)}}>All Services</a></li> <li><a
<li><a {{tooltip (concat "Service (" item.Service.Service ")")}} data-test-back href={{href-to 'dc.services.show'}}> {{tooltip (concat 'Service (' item.Service.Service ')')}}
Service ({{item.Service.Service}}) data-test-back
</a></li> href={{href-to 'dc.services.show'}}
</ol> >
Service ({{item.Service.Service}})
</a></li>
</ol>
</BlockSlot> </BlockSlot>
<BlockSlot @name="header"> <BlockSlot @name='header'>
<h1> <h1>
<route.Title @title={{item.Service.ID}} /> <route.Title @title={{item.Service.ID}} />
</h1> </h1>
@ -146,54 +133,92 @@ as |item|}}
<Consul::TransparentProxy /> <Consul::TransparentProxy />
{{/if}} {{/if}}
</BlockSlot> </BlockSlot>
<BlockSlot @name="nav"> <BlockSlot @name='nav'>
<dl> <dl>
<dt>Service Name</dt> <dt>Service Name</dt>
<dd><a href="{{href-to 'dc.services.show' item.Service.Service}}">{{item.Service.Service}}</a></dd> <dd><a
href='{{href-to "dc.services.show" item.Service.Service}}'
>{{item.Service.Service}}</a></dd>
</dl> </dl>
{{#unless item.Node.Meta.synthetic-node}} {{#unless item.Node.Meta.synthetic-node}}
<dl> <dl>
<dt>Node Name</dt> <dt>Node Name</dt>
<dd><a data-test-service-instance-node-name href="{{href-to 'dc.nodes.show' item.Node.Node}}">{{item.Node.Node}}</a></dd> <dd><a
data-test-service-instance-node-name
href='{{href-to "dc.nodes.show" item.Node.Node}}'
>{{item.Node.Node}}</a></dd>
</dl> </dl>
{{/unless}} {{/unless}}
{{#if item.Service.PeerName}} {{#if item.Service.PeerName}}
<dl> <dl>
<dt>Peer Name</dt> <dt>Peer Name</dt>
<dd>{{item.Service.PeerName}}</dd> <dd><a
data-test-service-instance-peer-name
href={{href-to
'dc.peers.show'
item.Service.PeerName
params=(hash peer=undefined)
}}
>{{item.Service.PeerName}}</a></dd>
</dl> </dl>
{{/if}} {{/if}}
</BlockSlot> </BlockSlot>
<BlockSlot @name="actions"> <BlockSlot @name='actions'>
{{#let (or item.Service.Address item.Node.Address) as |address|}} {{#let (or item.Service.Address item.Node.Address) as |address|}}
<CopyButton @value={{address}} @name="Address">{{address}}</CopyButton> <CopyButton @value={{address}} @name='Address'>{{address}}</CopyButton>
{{/let}} {{/let}}
</BlockSlot> </BlockSlot>
<BlockSlot @name="content"> <BlockSlot @name='content'>
<TabNav @items={{ <TabNav
compact @items={{compact
(array (array
(hash label="Health Checks" href=(href-to "dc.services.instance.healthchecks") selected=(is-href "dc.services.instance.healthchecks")) (hash
(if (eq item.Service.Kind 'mesh-gateway') (hash label="Addresses" href=(href-to "dc.services.instance.addresses") selected=(is-href "dc.services.instance.addresses"))) label='Health Checks'
(if proxy (hash label="Upstreams" href=(href-to "dc.services.instance.upstreams") selected=(is-href "dc.services.instance.upstreams"))) href=(href-to 'dc.services.instance.healthchecks')
(if proxy (hash label="Exposed Paths" href=(href-to "dc.services.instance.exposedpaths") selected=(is-href "dc.services.instance.exposedpaths"))) selected=(is-href 'dc.services.instance.healthchecks')
(hash label="Tags & Meta" href=(href-to "dc.services.instance.metadata") selected=(is-href "dc.services.instance.metadata"))
) )
(if
(eq item.Service.Kind 'mesh-gateway')
(hash
label='Addresses'
href=(href-to 'dc.services.instance.addresses')
selected=(is-href 'dc.services.instance.addresses')
)
)
(if
proxy
(hash
label='Upstreams'
href=(href-to 'dc.services.instance.upstreams')
selected=(is-href 'dc.services.instance.upstreams')
)
)
(if
proxy
(hash
label='Exposed Paths'
href=(href-to 'dc.services.instance.exposedpaths')
selected=(is-href 'dc.services.instance.exposedpaths')
)
)
(hash
label='Tags & Meta'
href=(href-to 'dc.services.instance.metadata')
selected=(is-href 'dc.services.instance.metadata')
)
)
}} }}
/> />
<Outlet <Outlet
@name={{routeName}} @name={{routeName}}
@model={{assign (hash @model={{assign (hash proxy=proxy meta=meta item=item) route.model}}
proxy=proxy as |o|
meta=meta >
item=item
) route.model}}
as |o|>
{{outlet}} {{outlet}}
</Outlet> </Outlet>
</BlockSlot> </BlockSlot>
</AppView> </AppView>
{{/let}} {{/let}}
</BlockSlot> </BlockSlot>
</DataLoader> </DataLoader>
</Route> </Route>

View File

@ -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}"
}
`;
}
)
}
]
`})}

View File

@ -1,7 +1,5 @@
{ {
"ID": "${fake.random.uuid()}", "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)}", "Name": "${location.pathname.get(2)}",
"State": "${fake.helpers.randomize([ "State": "${fake.helpers.randomize([
'ACTIVE', 'ACTIVE',
@ -12,6 +10,13 @@
'TERMINATED', 'TERMINATED',
'UNDEFINED' '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()}", "PeerID": "${fake.random.uuid()}",
"PeerServerName": "${fake.internet.domainName()}", "PeerServerName": "${fake.internet.domainName()}",
"PeerServerAddresses": [ "PeerServerAddresses": [

View File

@ -22,8 +22,8 @@
"LastHeartbeat": "${fake.date.past(10).toISOString()}", "LastHeartbeat": "${fake.date.past(10).toISOString()}",
"LastReceive": "${fake.date.past(10).toISOString()}", "LastReceive": "${fake.date.past(10).toISOString()}",
"LastSend": "${fake.date.past(10).toISOString()}", "LastSend": "${fake.date.past(10).toISOString()}",
"ExportedServices": ["${`export-service-${i}`}"], "ExportedServices": [${range(0, Math.floor(Math.random() * 10)).map((i) => `"exported-service-${i}"`)}],
"ImportedServices": ["${`import-service-${i}`}"] "ImportedServices": [${range(0, Math.floor(Math.random() * 10)).map((i) => `"imported-service-${i}"`)}]
}, },
"PeerID": "${id}", "PeerID": "${id}",
"PeerServerName": "${fake.internet.domainName()}", "PeerServerName": "${fake.internet.domainName()}",

View File

@ -71,6 +71,7 @@
"@hashicorp/design-system-tokens": "^1.0.0", "@hashicorp/design-system-tokens": "^1.0.0",
"@hashicorp/ember-cli-api-double": "^4.0.0", "@hashicorp/ember-cli-api-double": "^4.0.0",
"@hashicorp/ember-flight-icons": "^2.0.12", "@hashicorp/ember-flight-icons": "^2.0.12",
"@html-next/vertical-collection": "^4.0.0",
"@lit/reactive-element": "^1.2.1", "@lit/reactive-element": "^1.2.1",
"@xstate/fsm": "^1.4.0", "@xstate/fsm": "^1.4.0",
"a11y-dialog": "^6.0.1", "a11y-dialog": "^6.0.1",
@ -146,7 +147,7 @@
"ember-power-select": "^4.0.5", "ember-power-select": "^4.0.5",
"ember-power-select-with-create": "^0.8.0", "ember-power-select-with-create": "^0.8.0",
"ember-qunit": "^5.1.1", "ember-qunit": "^5.1.1",
"ember-ref-modifier": "^1.0.0", "ember-ref-bucket": "^4.1.0",
"ember-render-helpers": "^0.2.0", "ember-render-helpers": "^0.2.0",
"ember-resolver": "^8.0.2", "ember-resolver": "^8.0.2",
"ember-route-action-helper": "^2.0.8", "ember-route-action-helper": "^2.0.8",

View File

@ -34,7 +34,7 @@ function colorMapFromTokens(tokensPath) {
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
module.exports = { module.exports = {
content: ['./app/**/*.{html,js,hbs,mdx}', './docs/**/*.{html,js,hbs,mdx}'], content: ['../**/*.{html.js,hbs,mdx}'],
theme: { theme: {
colors: colorMapFromTokens( colors: colorMapFromTokens(
'../../node_modules/@hashicorp/design-system-tokens/dist/products/css/tokens.css' '../../node_modules/@hashicorp/design-system-tokens/dist/products/css/tokens.css'

View File

@ -6,6 +6,8 @@ Feature: dc / peers / regenerate: Regenerate Peer Token
--- ---
Name: peer-name Name: peer-name
State: ACTIVE State: ACTIVE
# dialer does not have a PeerID
PeerID: null
--- ---
And the url "/v1/peering/token" responds with from yaml 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 actions on the peers
And I click regenerate on the peers And I click regenerate on the peers
Then I see the text "an-encoded-token" in ".consul-peer-form-generate code" 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

View File

@ -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

View File

@ -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);
});
}

View File

@ -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 nspaces from 'consul-ui/tests/pages/dc/nspaces/index';
import nspace from 'consul-ui/tests/pages/dc/nspaces/edit'; import nspace from 'consul-ui/tests/pages/dc/nspaces/edit';
import peers from 'consul-ui/tests/pages/dc/peers/index'; import peers from 'consul-ui/tests/pages/dc/peers/index';
import peersShow from 'consul-ui/tests/pages/dc/peers/show';
// utils // utils
const deletable = createDeletable(clickable); const deletable = createDeletable(clickable);
@ -234,6 +235,7 @@ export default {
nspace(visitable, submitable, deletable, cancelable, policySelector, roleSelector) nspace(visitable, submitable, deletable, cancelable, policySelector, roleSelector)
), ),
peers: create(peers(visitable, creatable, consulPeerList, popoverSelect)), peers: create(peers(visitable, creatable, consulPeerList, popoverSelect)),
peer: create(peersShow(visitable)),
settings: create(settings(visitable, submitable, isPresent)), settings: create(settings(visitable, submitable, isPresent)),
routingConfig: create(routingConfig(visitable, text)), routingConfig: create(routingConfig(visitable, text)),
}; };

View File

@ -1,7 +1,10 @@
import tabgroup from 'consul-ui/components/tab-nav/pageobject';
export default function (visitable, creatable, items, popoverSelect) { export default function (visitable, creatable, items, popoverSelect) {
return creatable({ return creatable({
visit: visitable('/:dc/peers'), visit: visitable('/:dc/peers'),
peers: items(), peers: items(),
sort: popoverSelect('[data-test-sort-control]'), sort: popoverSelect('[data-test-sort-control]'),
tabs: tabgroup('tab', ['imported-services', 'exported-services', 'addresses']),
}); });
} }

View File

@ -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'),
};
}

View File

@ -29,5 +29,13 @@ export default function (scenario, pages, set, reset) {
// do I absolutely definitely need that all the time? // do I absolutely definitely need that all the time?
return set(pages[name]).visit(data); 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);
}
); );
} }

View File

@ -117,10 +117,10 @@ dc:
index: index:
empty: empty:
header: | header: |
{items, select, {items, select,
0 {Welcome to Peers} 0 {Welcome to Peers}
other {No peers found} other {No peers found}
} }
body: | body: |
{items, select, {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, 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: detail:
imported: imported:
count: | count: |
{count} imported services {count} imported services
tooltip: The number of services imported from {name} tooltip: The number of services imported from {name}
tab-tooltip: Services imported from {name}
exported: exported:
count: | count: |
{count} exported services {count} exported services
tooltip: The number of services exported from {name} 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: |
<div>
{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 <code>services:read</code> 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.}
}
</div>
exported:
empty:
header: No visible exported services to {name}
body: |
<div>
{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 <code>services:read</code> 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.}
}
</div>
addresses:
empty:
header: No server adddresses.
body: <div>There don't seem to be any server addresses for this peer.</div>
partitions: partitions:
index: index:
empty: empty:
@ -148,7 +178,7 @@ dc:
{items, select, {items, select,
0 {Welcome to Partitions} 0 {Welcome to Partitions}
other {No partitions found} other {No partitions found}
} }
body: | body: |
{items, select, {items, select,
0 {There don't seem to be any partitions{canUseACLs, select, 0 {There don't seem to be any partitions{canUseACLs, select,

View File

@ -3320,6 +3320,20 @@
resolved "https://registry.yarnpkg.com/@hashicorp/flight-icons/-/flight-icons-2.10.0.tgz#24b03043bacda16e505200e6591dfef896ddacf1" resolved "https://registry.yarnpkg.com/@hashicorp/flight-icons/-/flight-icons-2.10.0.tgz#24b03043bacda16e505200e6591dfef896ddacf1"
integrity sha512-jYUA0M6Tz+4RAudil+GW/fHbhZPcKCiIZZAguBDviqbLneMkMgPOBgbXWCGWsEQ1fJzP2cXbUaio8L0aQZPWQw== 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": "@humanwhocodes/config-array@^0.5.0":
version "0.5.0" version "0.5.0"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9" 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" resolved "https://registry.yarnpkg.com/@types/broccoli-plugin/-/broccoli-plugin-1.3.0.tgz#38f8462fecaebc4e09a32e4d4ed1b9808f75bbca"
integrity sha512-SLk4/hFc2kGvgwNFrpn2O1juxFOllcHAywvlo7VwxfExLzoz1GGJ0oIZCwj5fwSpvHw4AWpZjJ1fUvb62PDayQ== 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": "@types/chai-as-promised@^7.1.2":
version "7.1.3" version "7.1.3"
resolved "https://registry.yarnpkg.com/@types/chai-as-promised/-/chai-as-promised-7.1.3.tgz#779166b90fda611963a3adbfd00b339d03b747bd" 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" symlink-or-copy "^1.0.1"
sync-disk-cache "^2.0.0" 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: broccoli-plugin@1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/broccoli-plugin/-/broccoli-plugin-1.1.0.tgz#73e2cfa05f8ea1e3fc1420c40c3d9e7dc724bf02" 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" rimraf "^3.0.2"
symlink-or-copy "^1.3.1" 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: broccoli-postcss-single@^5.0.1:
version "5.0.2" version "5.0.2"
resolved "https://registry.yarnpkg.com/broccoli-postcss-single/-/broccoli-postcss-single-5.0.2.tgz#f23661b3011494d8a2dbd8ff39eb394e80313682" 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" symlink-or-copy "^1.2.0"
walk-sync "^1.1.3" 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: broccoli-sass-source-maps@^4.0.0:
version "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" 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" ember-cli-version-checker "^2.1.2"
semver "^5.5.0" 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" version "7.26.11"
resolved "https://registry.yarnpkg.com/ember-cli-babel/-/ember-cli-babel-7.26.11.tgz#50da0fe4dcd99aada499843940fec75076249a9f" resolved "https://registry.yarnpkg.com/ember-cli-babel/-/ember-cli-babel-7.26.11.tgz#50da0fe4dcd99aada499843940fec75076249a9f"
integrity sha512-JJYeYjiz/JTn34q7F5DSOjkkZqy8qwFOOxXfE6pe9yEJqWGu4qErKxlz8I22JoVEQ/aBUO+OcKTpmctvykM9YA== 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-destroyable-polyfill "^2.0.2"
ember-modifier-manager-polyfill "^1.2.0" 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" version "3.2.7"
resolved "https://registry.yarnpkg.com/ember-modifier/-/ember-modifier-3.2.7.tgz#f2d35b7c867cbfc549e1acd8d8903c5ecd02ea4b" resolved "https://registry.yarnpkg.com/ember-modifier/-/ember-modifier-3.2.7.tgz#f2d35b7c867cbfc549e1acd8d8903c5ecd02ea4b"
integrity sha512-ezcPQhH8jUfcJQbbHji4/ZG/h0yyj1jRDknfYue/ypQS8fM8LrGcCMo0rjDZLzL1Vd11InjNs3BD7BdxFlzGoA== integrity sha512-ezcPQhH8jUfcJQbbHji4/ZG/h0yyj1jRDknfYue/ypQS8fM8LrGcCMo0rjDZLzL1Vd11InjNs3BD7BdxFlzGoA==
@ -8981,12 +9017,21 @@ ember-qunit@^5.1.1:
silent-error "^1.1.1" silent-error "^1.1.1"
validate-peer-dependencies "^1.2.0" validate-peer-dependencies "^1.2.0"
ember-ref-modifier@^1.0.0: ember-raf-scheduler@^0.3.0:
version "1.0.1" version "0.3.0"
resolved "https://registry.yarnpkg.com/ember-ref-modifier/-/ember-ref-modifier-1.0.1.tgz#aeca56798ebc0fb750f0ccd36e86d667f5b1bc44" resolved "https://registry.yarnpkg.com/ember-raf-scheduler/-/ember-raf-scheduler-0.3.0.tgz#7657ee5c1d54f852731e61e9d0e0750a9a22f5f4"
integrity sha512-qmEFY/4WOWWXABRWvX50jLPleH30p/LPLUXEvaSlIj41F23e3Vul91IqZ1PFdw1Rxpkb8DHWO5BRchN8vnz4+Q== integrity sha512-i8JWQidNCX7n5TOTIKRDR0bnsQN9aJh/GtOJKINz2Wr+I7L7sYVhli6MFqMYNGKC9j9e6iWsznfAIxddheyEow==
dependencies: 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: ember-render-helpers@^0.2.0:
version "0.2.0" version "0.2.0"
@ -15179,6 +15224,13 @@ rollup@^1.12.0:
"@types/node" "*" "@types/node" "*"
acorn "^7.1.0" 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: route-recognizer@^0.3.3:
version "0.3.4" version "0.3.4"
resolved "https://registry.yarnpkg.com/route-recognizer/-/route-recognizer-0.3.4.tgz#39ab1ffbce1c59e6d2bdca416f0932611e4f3ca3" resolved "https://registry.yarnpkg.com/route-recognizer/-/route-recognizer-0.3.4.tgz#39ab1ffbce1c59e6d2bdca416f0932611e4f3ca3"