Merge pull request #14947 from hashicorp/ui/feat/peer-detail-page
ui: peer detail view
This commit is contained in:
commit
e6cce385e7
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:feature
|
||||||
|
ui: Create peerings detail page
|
||||||
|
```
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
|
@ -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)}}
|
||||||
|
<a
|
||||||
data-test-peer={{item.Name}}
|
data-test-peer={{item.Name}}
|
||||||
|
href={{href-to "dc.peers.show" item.Name}}
|
||||||
>
|
>
|
||||||
{{item.Name}}
|
{{item.Name}}
|
||||||
|
</a>
|
||||||
|
{{else}}
|
||||||
|
<p data-test-peer={{item.Name}}>
|
||||||
|
{{item.Name}}
|
||||||
</p>
|
</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,19 +48,24 @@ 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
|
<Action data-test-regenerate-action {{on "click" (fn @onedit item)}}>
|
||||||
data-test-regenerate-action
|
|
||||||
{{on 'click' (fn @onedit item)}}
|
|
||||||
>
|
|
||||||
<BlockSlot @name="label">
|
<BlockSlot @name="label">
|
||||||
Regenerate token
|
Regenerate token
|
||||||
</BlockSlot>
|
</BlockSlot>
|
||||||
</Action>
|
</Action>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
<Action
|
||||||
|
data-test-view-action
|
||||||
|
@href={{href-to "dc.peers.show" item.Name}}
|
||||||
|
>
|
||||||
|
<BlockSlot @name="label">
|
||||||
|
View
|
||||||
|
</BlockSlot>
|
||||||
|
</Action>
|
||||||
<Action
|
<Action
|
||||||
data-test-delete-action
|
data-test-delete-action
|
||||||
@onclick={{fn @ondelete item}}
|
@onclick={{fn @ondelete item}}
|
||||||
|
@ -81,7 +93,6 @@ as |item index|>
|
||||||
</BlockSlot>
|
</BlockSlot>
|
||||||
</Action>
|
</Action>
|
||||||
</Actions>
|
</Actions>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</BlockSlot>
|
</BlockSlot>
|
||||||
</ListCollection>
|
</ListCollection>
|
||||||
|
|
||||||
|
|
|
@ -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"]),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
|
@ -1,7 +0,0 @@
|
||||||
<Route
|
|
||||||
@name={{routeName}}
|
|
||||||
as |route|>
|
|
||||||
<Consul::Peer::Address::List
|
|
||||||
@items={{route.model.items}}
|
|
||||||
/>
|
|
||||||
</Route>
|
|
|
@ -1,6 +0,0 @@
|
||||||
<Route
|
|
||||||
@name={{routeName}}
|
|
||||||
as |route|>
|
|
||||||
{{did-insert (route-action 'replaceWith' 'dc.peers.edit.addresses')}}
|
|
||||||
</Route>
|
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,3 @@
|
||||||
|
<Route @name={{routeName}} as |route|>
|
||||||
|
{{did-insert this.transitionToImported}}
|
||||||
|
</Route>
|
|
@ -1,30 +1,76 @@
|
||||||
(routes => routes({
|
((routes) =>
|
||||||
|
routes({
|
||||||
dc: {
|
dc: {
|
||||||
peers: {
|
peers: {
|
||||||
_options: {
|
_options: {
|
||||||
path: '/peers'
|
path: "/peers",
|
||||||
},
|
},
|
||||||
index: {
|
index: {
|
||||||
_options: {
|
_options: {
|
||||||
path: '/',
|
path: "/",
|
||||||
queryParams: {
|
queryParams: {
|
||||||
sortBy: 'sort',
|
sortBy: "sort",
|
||||||
state: 'state',
|
state: "state",
|
||||||
searchproperty: {
|
searchproperty: {
|
||||||
as: 'searchproperty',
|
as: "searchproperty",
|
||||||
empty: [['Name', 'ID']],
|
empty: [["Name", "ID"]],
|
||||||
},
|
},
|
||||||
search: {
|
search: {
|
||||||
as: 'filter',
|
as: "filter",
|
||||||
replace: true,
|
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,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}))(
|
},
|
||||||
(json, data = (typeof document !== 'undefined' ? document.currentScript.dataset : module.exports)) => {
|
},
|
||||||
|
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
|
||||||
|
) => {
|
||||||
data[`routes`] = JSON.stringify(json);
|
data[`routes`] = JSON.stringify(json);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -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,
|
||||||
}}
|
}}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,83 +1,75 @@
|
||||||
<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}}
|
{{item.Name}}
|
||||||
</a>
|
</a>
|
||||||
{{else}}
|
{{else}}
|
||||||
<p data-test-service-name>
|
<p data-test-service-name>
|
||||||
{{item.Name}}
|
{{item.Name}}
|
||||||
</p>
|
</p>
|
||||||
{{/if}}
|
{{/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
|
||||||
|
(and
|
||||||
|
(not-eq item.InstanceCount 0)
|
||||||
|
(and (not-eq item.Kind 'terminating-gateway') (not-eq item.Kind 'ingress-gateway'))
|
||||||
|
)
|
||||||
|
}}
|
||||||
<span>
|
<span>
|
||||||
{{format-number item.InstanceCount}} {{pluralize item.InstanceCount 'instance' without-count=true}}
|
{{format-number item.InstanceCount}}
|
||||||
|
{{pluralize item.InstanceCount 'instance' without-count=true}}
|
||||||
</span>
|
</span>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
<Consul::Bucket::List
|
{{! we are displaying imported-services - don't show bucket-list }}
|
||||||
@item={{item}}
|
{{#unless @isPeerDetail}}
|
||||||
@nspace={{@nspace}}
|
<Consul::Bucket::List @item={{item}} @nspace={{@nspace}} @partition={{@partition}} />
|
||||||
@partition={{@partition}}
|
{{/unless}}
|
||||||
/>
|
|
||||||
{{#if (eq item.Kind 'terminating-gateway')}}
|
{{#if (eq item.Kind 'terminating-gateway')}}
|
||||||
<span data-test-associated-service-count>
|
<span data-test-associated-service-count>
|
||||||
{{format-number item.GatewayConfig.AssociatedServiceCount}} {{pluralize item.GatewayConfig.AssociatedServiceCount 'linked service' without-count=true}}
|
{{format-number item.GatewayConfig.AssociatedServiceCount}}
|
||||||
|
{{pluralize item.GatewayConfig.AssociatedServiceCount 'linked service' without-count=true}}
|
||||||
</span>
|
</span>
|
||||||
{{else if (eq item.Kind 'ingress-gateway')}}
|
{{else if (eq item.Kind 'ingress-gateway')}}
|
||||||
<span data-test-associated-service-count>
|
<span data-test-associated-service-count>
|
||||||
{{format-number item.GatewayConfig.AssociatedServiceCount}} {{pluralize item.GatewayConfig.AssociatedServiceCount 'upstream' without-count=true}}
|
{{format-number item.GatewayConfig.AssociatedServiceCount}}
|
||||||
|
{{pluralize item.GatewayConfig.AssociatedServiceCount 'upstream' without-count=true}}
|
||||||
</span>
|
</span>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#if (or item.ConnectedWithGateway item.ConnectedWithProxy)}}
|
{{#if (or item.ConnectedWithGateway item.ConnectedWithProxy)}}
|
||||||
<dl class="mesh">
|
<dl class='mesh'>
|
||||||
<dt>
|
<dt>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
This service uses a proxy for the Consul service mesh
|
This service uses a proxy for the Consul service mesh
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</dt>
|
</dt>
|
||||||
{{#if (and item.ConnectedWithGateway item.ConnectedWithProxy )}}
|
{{#if (and item.ConnectedWithGateway item.ConnectedWithProxy)}}
|
||||||
<dd data-test-mesh>
|
<dd data-test-mesh>
|
||||||
in service mesh with proxy and gateway
|
in service mesh with proxy and gateway
|
||||||
</dd>
|
</dd>
|
||||||
|
|
|
@ -1,62 +1,56 @@
|
||||||
<SearchBar
|
<SearchBar class='consul-service-search-bar' ...attributes @filter={{@filter}}>
|
||||||
class="consul-service-search-bar"
|
|
||||||
...attributes
|
|
||||||
@filter={{@filter}}
|
|
||||||
>
|
|
||||||
<:status as |search|>
|
<: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
|
||||||
(t (concat "components.consul.service.search-bar." search.status.value)
|
(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
|
<search.RemoveFilter aria-label={{t 'common.ui.remove' item=(concat key ' ' value)}}>
|
||||||
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
|
<search.Select
|
||||||
class="type-search-properties"
|
class='type-search-properties'
|
||||||
@position="right"
|
@position='right'
|
||||||
@onchange={{action @filter.searchproperty.change}}
|
@onchange={{action @filter.searchproperty.change}}
|
||||||
@multiple={{true}}
|
@multiple={{true}}
|
||||||
@required={{true}}
|
@required={{true}}
|
||||||
as |components|>
|
as |components|
|
||||||
<BlockSlot @name="selected">
|
>
|
||||||
|
<BlockSlot @name='selected'>
|
||||||
<span>
|
<span>
|
||||||
{{t "common.search.searchproperty"}}
|
{{t 'common.search.searchproperty'}}
|
||||||
</span>
|
</span>
|
||||||
</BlockSlot>
|
</BlockSlot>
|
||||||
<BlockSlot @name="options">
|
<BlockSlot @name='options'>
|
||||||
{{#let components.Optgroup components.Option as |Optgroup Option|}}
|
{{#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}}
|
||||||
|
@ -66,60 +60,58 @@ as |key value|}}
|
||||||
</:search>
|
</:search>
|
||||||
<:filter as |search|>
|
<:filter as |search|>
|
||||||
<search.Select
|
<search.Select
|
||||||
class="type-status"
|
class='type-status'
|
||||||
@position="left"
|
@position='left'
|
||||||
@onchange={{action @filter.status.change}}
|
@onchange={{action @filter.status.change}}
|
||||||
@multiple={{true}}
|
@multiple={{true}}
|
||||||
as |components|>
|
as |components|
|
||||||
<BlockSlot @name="selected">
|
>
|
||||||
|
<BlockSlot @name='selected'>
|
||||||
<span>
|
<span>
|
||||||
{{t "common.consul.status"}}
|
{{t 'common.consul.status'}}
|
||||||
</span>
|
</span>
|
||||||
</BlockSlot>
|
</BlockSlot>
|
||||||
<BlockSlot @name="options">
|
<BlockSlot @name='options'>
|
||||||
{{#let components.Optgroup components.Option as |Optgroup Option|}}
|
{{#let components.Optgroup components.Option as |Optgroup Option|}}
|
||||||
{{#each (array "passing" "warning" "critical" "empty") as |state|}}
|
{{#each this.healthStates as |state|}}
|
||||||
<Option class="value-{{state}}" @value={{state}} @selected={{includes state @filter.status.value}}>
|
<Option
|
||||||
{{t (concat "common.consul." state)
|
class='value-{{state}}'
|
||||||
default=(array
|
@value={{state}}
|
||||||
(concat "common.search." state)
|
@selected={{includes state @filter.status.value}}
|
||||||
)
|
>
|
||||||
}}
|
{{t (concat 'common.consul.' state) default=(array (concat 'common.search.' state))}}
|
||||||
</Option>
|
</Option>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
{{/let}}
|
{{/let}}
|
||||||
</BlockSlot>
|
</BlockSlot>
|
||||||
</search.Select>
|
</search.Select>
|
||||||
<search.Select
|
<search.Select
|
||||||
@position="left"
|
@position='left'
|
||||||
@onchange={{action @filter.kind.change}}
|
@onchange={{action @filter.kind.change}}
|
||||||
@multiple={{true}}
|
@multiple={{true}}
|
||||||
as |components|>
|
as |components|
|
||||||
<BlockSlot @name="selected">
|
>
|
||||||
|
<BlockSlot @name='selected'>
|
||||||
<span>
|
<span>
|
||||||
{{t "components.consul.service.search-bar.kind"}}
|
{{t 'components.consul.service.search-bar.kind'}}
|
||||||
</span>
|
</span>
|
||||||
</BlockSlot>
|
</BlockSlot>
|
||||||
<BlockSlot @name="options">
|
<BlockSlot @name='options'>
|
||||||
{{#let components.Optgroup components.Option as |Optgroup Option|}}
|
{{#let components.Optgroup components.Option as |Optgroup Option|}}
|
||||||
<Option @value="service" @selected={{includes 'service' @filter.kind.value}}>
|
<Option @value='service' @selected={{includes 'service' @filter.kind.value}}>
|
||||||
{{t "common.consul.service"}}
|
{{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|}}
|
||||||
>
|
|
||||||
{{#each (array "ingress-gateway" "terminating-gateway" "mesh-gateway") as |kind|}}
|
|
||||||
<Option @value={{kind}} @selected={{includes kind @filter.kind.value}}>
|
<Option @value={{kind}} @selected={{includes kind @filter.kind.value}}>
|
||||||
{{t (concat "common.consul." kind)}}
|
{{t (concat 'common.consul.' kind)}}
|
||||||
</Option>
|
</Option>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</Optgroup>
|
</Optgroup>
|
||||||
<Optgroup
|
<Optgroup @label={{t 'common.consul.mesh'}}>
|
||||||
@label={{t "common.consul.mesh"}}
|
{{#each (array 'in-mesh' 'not-in-mesh') as |state|}}
|
||||||
>
|
|
||||||
{{#each (array "in-mesh" "not-in-mesh") as |state|}}
|
|
||||||
<Option @value={{state}} @selected={{includes state @filter.kind.value}}>
|
<Option @value={{state}} @selected={{includes state @filter.kind.value}}>
|
||||||
{{t (concat "common.search." state)}}
|
{{t (concat 'common.search.' state)}}
|
||||||
</Option>
|
</Option>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</Optgroup>
|
</Optgroup>
|
||||||
|
@ -128,28 +120,37 @@ as |key value|}}
|
||||||
</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}}
|
||||||
|
@value={{source}}
|
||||||
|
@selected={{includes source @filter.source.value}}
|
||||||
|
>
|
||||||
|
{{t (concat 'common.brand.' source)}}
|
||||||
</Option>
|
</Option>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
<Option class="consul" @value='consul' @selected={{includes 'consul' @filter.source.value}}>
|
<Option
|
||||||
|
class='consul'
|
||||||
|
@value='consul'
|
||||||
|
@selected={{includes 'consul' @filter.source.value}}
|
||||||
|
>
|
||||||
{{t 'common.brand.consul'}}
|
{{t 'common.brand.consul'}}
|
||||||
</Option>
|
</Option>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/let}}
|
{{/let}}
|
||||||
</BlockSlot>
|
</BlockSlot>
|
||||||
</search.Select>
|
</search.Select>
|
||||||
|
@ -157,36 +158,48 @@ as |key value|}}
|
||||||
</: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">
|
>
|
||||||
|
<BlockSlot @name='selected'>
|
||||||
<span>
|
<span>
|
||||||
{{#let (from-entries (array
|
{{#let
|
||||||
(array "Name:asc" (t "common.sort.alpha.asc"))
|
(from-entries
|
||||||
(array "Name:desc" (t "common.sort.alpha.desc"))
|
(array
|
||||||
(array "Status:asc" (t "common.sort.status.asc"))
|
(array 'Name:asc' (t 'common.sort.alpha.asc'))
|
||||||
(array "Status:desc" (t "common.sort.status.desc"))
|
(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|
|
as |selectable|
|
||||||
}}
|
}}
|
||||||
{{get selectable @sort.value}}
|
{{get selectable @sort.value}}
|
||||||
{{/let}}
|
{{/let}}
|
||||||
</span>
|
</span>
|
||||||
</BlockSlot>
|
</BlockSlot>
|
||||||
<BlockSlot @name="options">
|
<BlockSlot @name='options'>
|
||||||
{{#let components.Optgroup components.Option as |Optgroup Option|}}
|
{{#let components.Optgroup components.Option as |Optgroup Option|}}
|
||||||
<Optgroup @label={{t "common.consul.status"}}>
|
<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:asc' @selected={{eq 'Status:asc' @sort.value}}>{{t
|
||||||
<Option @value="Status:desc" @selected={{eq "Status:desc" @sort.value}}>{{t "common.sort.status.desc"}}</Option>
|
'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>
|
||||||
|
|
|
@ -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'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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();
|
||||||
|
let duration;
|
||||||
|
if (this.element) {
|
||||||
// if we have no transition-duration set immediately end the transition
|
// if we have no transition-duration set immediately end the transition
|
||||||
const duration = window
|
duration = window.getComputedStyle(this.element).getPropertyValue('transition-duration');
|
||||||
.getComputedStyle(this.element)
|
} else {
|
||||||
.getPropertyValue('transition-duration');
|
duration = 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (parseFloat(duration) === 0) {
|
if (parseFloat(duration) === 0) {
|
||||||
this.endTransition();
|
this.endTransition();
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
{{yield (hash data=this.data)}}
|
|
@ -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 }));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
```
|
|
@ -0,0 +1,4 @@
|
||||||
|
<div {{create-ref 'element'}} {{did-insert this.measureDimensions}}>
|
||||||
|
{{on-window 'resize' this.handleWindowResize}}
|
||||||
|
{{yield (hash data=this.data)}}
|
||||||
|
</div>
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
{{yield (hash data=this.data)}}
|
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
';'
|
||||||
|
'--selected-height:'
|
||||||
|
this.style.height
|
||||||
|
';'
|
||||||
|
'--selected-top:'
|
||||||
|
this.style.top
|
||||||
)
|
)
|
||||||
undefined
|
undefined
|
||||||
}}}
|
}}}
|
||||||
aria-label="Secondary"
|
aria-label='Secondary'
|
||||||
class={{concat 'tab-nav' ' animatable'}}
|
class={{concat 'tab-nav' ' animatable'}}
|
||||||
...attributes
|
...attributes
|
||||||
>
|
>
|
||||||
<ul>
|
<ul>
|
||||||
{{#each @items as |item|}}
|
{{#each @items as |item|}}
|
||||||
<li
|
<li
|
||||||
{{on 'click' (fn select)}}
|
{{on 'click' (fn select)}}
|
||||||
{{did-upsert
|
{{did-upsert (if item.selected (fn select) (noop)) @items.length}}
|
||||||
(if item.selected
|
|
||||||
(fn select)
|
|
||||||
(noop)
|
|
||||||
)
|
|
||||||
@items.length
|
|
||||||
}}
|
|
||||||
data-test-tab={{concat name '_' (if item.label (slugify item.label) (slugify item))}}
|
data-test-tab={{concat name '_' (if item.label (slugify item.label) (slugify item))}}
|
||||||
class={{if (or item.selected (eq selected (if item.label (slugify item.label) (slugify item)))) 'selected'}}
|
class={{if
|
||||||
|
(or item.selected (eq selected (if item.label (slugify item.label) (slugify item))))
|
||||||
|
'selected'
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Action
|
<Action
|
||||||
{{on 'click'
|
{{on 'click' (fn this.onClick (uppercase item.label))}}
|
||||||
(fn this.onClick (uppercase item.label))
|
{{on 'click' (fn this.onTabClicked item)}}
|
||||||
}}
|
|
||||||
{{on 'click'
|
|
||||||
(fn this.onTabClicked item)
|
|
||||||
}}
|
|
||||||
@href={{item.href}}
|
@href={{item.href}}
|
||||||
>
|
>
|
||||||
|
{{#if item.tooltip}}
|
||||||
|
<span {{tooltip item.tooltip}}>{{item.label}}</span>
|
||||||
|
{{else}}
|
||||||
{{item.label}}
|
{{item.label}}
|
||||||
|
{{/if}}
|
||||||
</Action>
|
</Action>
|
||||||
</li>
|
</li>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
{{/let}}
|
{{/let}}
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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',
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -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}`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,6 +17,54 @@ 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
|
||||||
|
return cb(headers, this._transformServicesPayload(body));
|
||||||
|
}),
|
||||||
|
query
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
respondForQueryRecord(respond, query) {
|
||||||
|
// Name is added here from the query, which is used to make the uid
|
||||||
|
// Datacenter gets added in the ApplicationSerializer
|
||||||
|
return super.respondForQueryRecord(
|
||||||
|
(cb) =>
|
||||||
|
respond((headers, body) => {
|
||||||
|
return cb(headers, {
|
||||||
|
Name: query.id,
|
||||||
|
Namespace: get(body, 'firstObject.Service.Namespace'),
|
||||||
|
Nodes: body,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
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 = {};
|
const services = {};
|
||||||
body
|
body
|
||||||
.filter(function (item) {
|
.filter(function (item) {
|
||||||
|
@ -36,26 +88,6 @@ export default class ServiceSerializer extends Serializer {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
return body;
|
||||||
return cb(headers, body);
|
|
||||||
}),
|
|
||||||
query
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
respondForQueryRecord(respond, query) {
|
|
||||||
// Name is added here from the query, which is used to make the uid
|
|
||||||
// Datacenter gets added in the ApplicationSerializer
|
|
||||||
return super.respondForQueryRecord(
|
|
||||||
(cb) =>
|
|
||||||
respond((headers, body) => {
|
|
||||||
return cb(headers, {
|
|
||||||
Name: query.id,
|
|
||||||
Namespace: get(body, 'firstObject.Service.Namespace'),
|
|
||||||
Nodes: body,
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
query
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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') {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
<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
|
||||||
|
'/${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
|
||||||
|
@ -13,24 +12,21 @@ as |route|>
|
||||||
peer=route.params.peer
|
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,15 +61,12 @@ as |route|>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</BlockSlot>
|
</BlockSlot>
|
||||||
|
|
||||||
<BlockSlot @name="loaded">
|
<BlockSlot @name='loaded'>
|
||||||
{{#let
|
{{#let loader.data as |item|}}
|
||||||
|
|
||||||
loader.data
|
|
||||||
|
|
||||||
as |item|}}
|
|
||||||
{{#if item.IsOrigin}}
|
{{#if item.IsOrigin}}
|
||||||
<DataSource
|
<DataSource
|
||||||
@src={{uri '/${partition}/${nspace}/${dc}/proxy-instance/${id}/${node}/${name}'
|
@src={{uri
|
||||||
|
'/${partition}/${nspace}/${dc}/proxy-instance/${id}/${node}/${name}'
|
||||||
(hash
|
(hash
|
||||||
partition=route.params.partition
|
partition=route.params.partition
|
||||||
nspace=route.params.nspace
|
nspace=route.params.nspace
|
||||||
|
@ -95,8 +76,9 @@ as |item|}}
|
||||||
name=route.params.name
|
name=route.params.name
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
@onchange={{action (mut meta) value="data"}}
|
@onchange={{action (mut meta) value='data'}}
|
||||||
as |meta|>
|
as |meta|
|
||||||
|
>
|
||||||
{{! We only really need meta to get the correct ServiceID }}
|
{{! We only really need meta to get the correct ServiceID }}
|
||||||
{{! but we may as well use the NodeName and ServiceName }}
|
{{! but we may as well use the NodeName and ServiceName }}
|
||||||
{{! from meta also, but they should be the same as the instance }}
|
{{! from meta also, but they should be the same as the instance }}
|
||||||
|
@ -109,7 +91,8 @@ as |item|}}
|
||||||
{{! and this second request get the info for that instance and saves }}
|
{{! and this second request get the info for that instance and saves }}
|
||||||
{{! it into the `proxy` variable }}
|
{{! it into the `proxy` variable }}
|
||||||
<DataSource
|
<DataSource
|
||||||
@src={{uri '/${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
|
||||||
|
@ -120,21 +103,25 @@ as |item|}}
|
||||||
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 {{tooltip (concat "Service (" item.Service.Service ")")}} data-test-back href={{href-to 'dc.services.show'}}>
|
<li><a
|
||||||
|
{{tooltip (concat 'Service (' item.Service.Service ')')}}
|
||||||
|
data-test-back
|
||||||
|
href={{href-to 'dc.services.show'}}
|
||||||
|
>
|
||||||
Service ({{item.Service.Service}})
|
Service ({{item.Service.Service}})
|
||||||
</a></li>
|
</a></li>
|
||||||
</ol>
|
</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>
|
|
@ -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}"
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
`})}
|
|
@ -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": [
|
||||||
|
|
|
@ -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()}",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
|
@ -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)),
|
||||||
};
|
};
|
||||||
|
|
|
@ -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']),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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'),
|
||||||
|
};
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -137,10 +137,40 @@ dc:
|
||||||
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:
|
||||||
|
|
92
ui/yarn.lock
92
ui/yarn.lock
|
@ -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"
|
||||||
|
|
Loading…
Reference in New Issue