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
|
||||
mkdir -p dist/ui && \
|
||||
cd packages/consul-ui && \
|
||||
CONSUL_UI_INSTALL_FLAGS=--focus \
|
||||
$(MAKE) build-staging && \
|
||||
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"
|
||||
...attributes
|
||||
@items={{@items}}
|
||||
as |item index|>
|
||||
@linkable="linkable peers"
|
||||
as |item index|
|
||||
>
|
||||
<BlockSlot @name="header">
|
||||
<p
|
||||
{{#if (can "delete peer" item=item)}}
|
||||
<a
|
||||
data-test-peer={{item.Name}}
|
||||
href={{href-to "dc.peers.show" item.Name}}
|
||||
>
|
||||
{{item.Name}}
|
||||
</a>
|
||||
{{else}}
|
||||
<p data-test-peer={{item.Name}}>
|
||||
{{item.Name}}
|
||||
</p>
|
||||
{{/if}}
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="details">
|
||||
<div class="peers__list__peer-detail">
|
||||
|
@ -16,24 +25,22 @@ as |item index|>
|
|||
|
||||
<div
|
||||
{{tooltip
|
||||
(t 'routes.dc.peers.index.detail.imported.tooltip'
|
||||
name=item.Name
|
||||
)
|
||||
(t "routes.dc.peers.index.detail.imported.tooltip" name=item.Name)
|
||||
}}
|
||||
>
|
||||
{{t 'routes.dc.peers.index.detail.imported.count'
|
||||
{{t
|
||||
"routes.dc.peers.index.detail.imported.count"
|
||||
count=(format-number item.ImportedServiceCount)
|
||||
}}
|
||||
</div>
|
||||
|
||||
<div
|
||||
{{tooltip
|
||||
(t 'routes.dc.peers.index.detail.exported.tooltip'
|
||||
name=item.Name
|
||||
)
|
||||
(t "routes.dc.peers.index.detail.exported.tooltip" name=item.Name)
|
||||
}}
|
||||
>
|
||||
{{t 'routes.dc.peers.index.detail.exported.count'
|
||||
{{t
|
||||
"routes.dc.peers.index.detail.exported.count"
|
||||
count=(format-number item.ExportedServiceCount)
|
||||
}}
|
||||
</div>
|
||||
|
@ -41,19 +48,24 @@ as |item index|>
|
|||
</div>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="actions" as |Actions|>
|
||||
{{#if (can 'delete peer' item=item)}}
|
||||
{{#if (can "delete peer" item=item)}}
|
||||
|
||||
<Actions as |Action|>
|
||||
{{#if (can "write peer" item=item)}}
|
||||
<Action
|
||||
data-test-regenerate-action
|
||||
{{on 'click' (fn @onedit item)}}
|
||||
>
|
||||
{{#if (and (can "write peer" item=item) item.isDialer)}}
|
||||
<Action data-test-regenerate-action {{on "click" (fn @onedit item)}}>
|
||||
<BlockSlot @name="label">
|
||||
Regenerate token
|
||||
</BlockSlot>
|
||||
</Action>
|
||||
{{/if}}
|
||||
<Action
|
||||
data-test-view-action
|
||||
@href={{href-to "dc.peers.show" item.Name}}
|
||||
>
|
||||
<BlockSlot @name="label">
|
||||
View
|
||||
</BlockSlot>
|
||||
</Action>
|
||||
<Action
|
||||
data-test-delete-action
|
||||
@onclick={{fn @ondelete item}}
|
||||
|
@ -84,4 +96,3 @@ as |item index|>
|
|||
{{/if}}
|
||||
</BlockSlot>
|
||||
</ListCollection>
|
||||
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
export const selectors = {
|
||||
$: '.consul-peer-list',
|
||||
$: ".consul-peer-list",
|
||||
collection: {
|
||||
$: '[data-test-list-row]',
|
||||
$: "[data-test-list-row]",
|
||||
peer: {
|
||||
$: 'li',
|
||||
$: "li",
|
||||
name: {
|
||||
$: '[data-test-peer]'
|
||||
}
|
||||
$: "[data-test-peer]",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
||||
export default (collection, isPresent, attribute, actions) => () => {
|
||||
return collection(`${selectors.$} ${selectors.collection.$}`, {
|
||||
peer: isPresent(selectors.collection.peer.$),
|
||||
name: attribute('data-test-peer', selectors.collection.peer.name.$),
|
||||
...actions(['regenerate', 'delete']),
|
||||
name: attribute("data-test-peer", selectors.collection.peer.name.$),
|
||||
...actions(["regenerate", "delete", "view"]),
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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: {
|
||||
peers: {
|
||||
_options: {
|
||||
path: '/peers'
|
||||
path: "/peers",
|
||||
},
|
||||
index: {
|
||||
_options: {
|
||||
path: '/',
|
||||
path: "/",
|
||||
queryParams: {
|
||||
sortBy: 'sort',
|
||||
state: 'state',
|
||||
sortBy: "sort",
|
||||
state: "state",
|
||||
searchproperty: {
|
||||
as: 'searchproperty',
|
||||
empty: [['Name', 'ID']],
|
||||
as: "searchproperty",
|
||||
empty: [["Name", "ID"]],
|
||||
},
|
||||
search: {
|
||||
as: 'filter',
|
||||
as: "filter",
|
||||
replace: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
show: {
|
||||
_options: {
|
||||
path: "/:name",
|
||||
},
|
||||
imported: {
|
||||
_options: {
|
||||
path: "/imported-services",
|
||||
queryParams: {
|
||||
sortBy: "sort",
|
||||
status: "status",
|
||||
source: "source",
|
||||
kind: "kind",
|
||||
searchproperty: {
|
||||
as: "searchproperty",
|
||||
empty: [["Name", "Tags"]],
|
||||
},
|
||||
search: {
|
||||
as: "filter",
|
||||
replace: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
exported: {
|
||||
_options: {
|
||||
path: "/exported-services",
|
||||
queryParams: {
|
||||
search: {
|
||||
as: "filter",
|
||||
replace: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
addresses: {
|
||||
_options: {
|
||||
path: "/addresses",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}))(
|
||||
(json, data = (typeof document !== 'undefined' ? document.currentScript.dataset : module.exports)) => {
|
||||
(
|
||||
json,
|
||||
data = typeof document !== "undefined"
|
||||
? document.currentScript.dataset
|
||||
: module.exports
|
||||
) => {
|
||||
data[`routes`] = JSON.stringify(json);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -96,6 +96,12 @@ module.exports = {
|
|||
urlSchema: 'auto',
|
||||
urlPrefix: 'docs/consul',
|
||||
},
|
||||
{
|
||||
root: path.resolve(__dirname, 'app/components/providers'),
|
||||
pattern: '**/README.mdx',
|
||||
urlSchema: 'auto',
|
||||
urlPrefix: 'docs/providers',
|
||||
},
|
||||
{
|
||||
root: `${path.dirname(require.resolve('consul-acls/package.json'))}/app/components`,
|
||||
pattern: '**/README.mdx',
|
||||
|
@ -129,6 +135,7 @@ module.exports = {
|
|||
].concat(user.sources),
|
||||
labels: {
|
||||
consul: 'Consul Components',
|
||||
providers: 'Provider Components',
|
||||
...user.labels,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import Adapter from './application';
|
||||
|
||||
export default class ServiceAdapter extends Adapter {
|
||||
requestForQuery(request, { dc, ns, partition, index, gateway, uri }) {
|
||||
requestForQuery(request, { dc, ns, partition, index, gateway, uri, peer }) {
|
||||
if (typeof gateway !== 'undefined') {
|
||||
return request`
|
||||
GET /v1/internal/ui/gateway-services-nodes/${gateway}?${{ dc }}
|
||||
|
@ -16,13 +16,14 @@ export default class ServiceAdapter extends Adapter {
|
|||
`;
|
||||
} else {
|
||||
return request`
|
||||
GET /v1/internal/ui/services?${{ dc }}
|
||||
GET /v1/internal/ui/services?${{ dc, peer }}
|
||||
X-Request-ID: ${uri}
|
||||
|
||||
${{
|
||||
ns,
|
||||
partition,
|
||||
index,
|
||||
peer,
|
||||
}}
|
||||
`;
|
||||
}
|
||||
|
|
|
@ -1,42 +1,27 @@
|
|||
<ListCollection
|
||||
class="consul-service-list"
|
||||
class='consul-service-list'
|
||||
...attributes
|
||||
@items={{@items}}
|
||||
@linkable="linkable service"
|
||||
@linkable='linkable service'
|
||||
as |item index|
|
||||
>
|
||||
<BlockSlot @name="header">
|
||||
<BlockSlot @name='header'>
|
||||
<dl class={{item.MeshStatus}}>
|
||||
<dt>
|
||||
Health
|
||||
</dt>
|
||||
<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>
|
||||
<dd {{tooltip item.healthTooltipText}}></dd>
|
||||
</dl>
|
||||
{{#if (gt item.InstanceCount 0)}}
|
||||
<a
|
||||
data-test-service-name
|
||||
href={{href-to "dc.services.show.index" item.Name
|
||||
params=(if (not-eq item.Partition @partition)
|
||||
(hash
|
||||
partition=item.Partition
|
||||
nspace=item.Namespace
|
||||
peer=item.PeerName
|
||||
)
|
||||
(hash
|
||||
peer=item.PeerName
|
||||
)
|
||||
href={{href-to
|
||||
'dc.services.show.index'
|
||||
item.Name
|
||||
params=(if
|
||||
(not-eq item.Partition @partition)
|
||||
(hash partition=item.Partition nspace=item.Namespace peer=item.PeerName)
|
||||
(hash peer=item.PeerName)
|
||||
)
|
||||
}}
|
||||
>
|
||||
|
@ -48,30 +33,37 @@
|
|||
</p>
|
||||
{{/if}}
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="details">
|
||||
<BlockSlot @name='details'>
|
||||
<Consul::Kind @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>
|
||||
{{format-number item.InstanceCount}} {{pluralize item.InstanceCount 'instance' without-count=true}}
|
||||
{{format-number item.InstanceCount}}
|
||||
{{pluralize item.InstanceCount 'instance' without-count=true}}
|
||||
</span>
|
||||
{{/if}}
|
||||
<Consul::Bucket::List
|
||||
@item={{item}}
|
||||
@nspace={{@nspace}}
|
||||
@partition={{@partition}}
|
||||
/>
|
||||
{{! we are displaying imported-services - don't show bucket-list }}
|
||||
{{#unless @isPeerDetail}}
|
||||
<Consul::Bucket::List @item={{item}} @nspace={{@nspace}} @partition={{@partition}} />
|
||||
{{/unless}}
|
||||
{{#if (eq item.Kind 'terminating-gateway')}}
|
||||
<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>
|
||||
{{else if (eq item.Kind 'ingress-gateway')}}
|
||||
<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>
|
||||
{{/if}}
|
||||
{{#if (or item.ConnectedWithGateway item.ConnectedWithProxy)}}
|
||||
<dl class="mesh">
|
||||
<dl class='mesh'>
|
||||
<dt>
|
||||
<Tooltip>
|
||||
This service uses a proxy for the Consul service mesh
|
||||
|
|
|
@ -1,31 +1,24 @@
|
|||
<SearchBar
|
||||
class="consul-service-search-bar"
|
||||
...attributes
|
||||
@filter={{@filter}}
|
||||
>
|
||||
<SearchBar class='consul-service-search-bar' ...attributes @filter={{@filter}}>
|
||||
<:status as |search|>
|
||||
|
||||
{{#let
|
||||
|
||||
(t (concat "components.consul.service.search-bar." search.status.key)
|
||||
(t
|
||||
(concat 'components.consul.service.search-bar.' search.status.key)
|
||||
default=(array
|
||||
(concat "common.search." search.status.key)
|
||||
(concat "common.consul." search.status.key)
|
||||
(concat 'common.search.' search.status.key) (concat 'common.consul.' search.status.key)
|
||||
)
|
||||
)
|
||||
|
||||
(t (concat "components.consul.service.search-bar." search.status.value)
|
||||
(t
|
||||
(concat 'components.consul.service.search-bar.' search.status.value)
|
||||
default=(array
|
||||
(concat "common.search." search.status.value)
|
||||
(concat "common.consul." search.status.value)
|
||||
(concat "common.brand." search.status.value)
|
||||
(concat 'common.search.' search.status.value)
|
||||
(concat 'common.consul.' search.status.value)
|
||||
(concat 'common.brand.' search.status.value)
|
||||
)
|
||||
)
|
||||
|
||||
as |key value|}}
|
||||
<search.RemoveFilter
|
||||
aria-label={{t "common.ui.remove" item=(concat key " " value)}}
|
||||
>
|
||||
as |key value|
|
||||
}}
|
||||
<search.RemoveFilter aria-label={{t 'common.ui.remove' item=(concat key ' ' value)}}>
|
||||
<dl>
|
||||
<dt>{{key}}</dt>
|
||||
<dd>{{value}}</dd>
|
||||
|
@ -38,25 +31,26 @@ as |key value|}}
|
|||
<search.Search
|
||||
@onsearch={{action @onsearch}}
|
||||
@value={{@search}}
|
||||
@placeholder={{t "common.search.search"}}
|
||||
@placeholder={{t 'common.search.search'}}
|
||||
>
|
||||
<search.Select
|
||||
class="type-search-properties"
|
||||
@position="right"
|
||||
class='type-search-properties'
|
||||
@position='right'
|
||||
@onchange={{action @filter.searchproperty.change}}
|
||||
@multiple={{true}}
|
||||
@required={{true}}
|
||||
as |components|>
|
||||
<BlockSlot @name="selected">
|
||||
as |components|
|
||||
>
|
||||
<BlockSlot @name='selected'>
|
||||
<span>
|
||||
{{t "common.search.searchproperty"}}
|
||||
{{t 'common.search.searchproperty'}}
|
||||
</span>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="options">
|
||||
<BlockSlot @name='options'>
|
||||
{{#let components.Optgroup components.Option as |Optgroup Option|}}
|
||||
{{#each @filter.searchproperty.default as |prop|}}
|
||||
<Option @value={{prop}} @selected={{includes prop @filter.searchproperty.value}}>
|
||||
{{t (concat "common.consul." (lowercase prop))}}
|
||||
{{t (concat 'common.consul.' (lowercase prop))}}
|
||||
</Option>
|
||||
{{/each}}
|
||||
{{/let}}
|
||||
|
@ -66,60 +60,58 @@ as |key value|}}
|
|||
</:search>
|
||||
<:filter as |search|>
|
||||
<search.Select
|
||||
class="type-status"
|
||||
@position="left"
|
||||
class='type-status'
|
||||
@position='left'
|
||||
@onchange={{action @filter.status.change}}
|
||||
@multiple={{true}}
|
||||
as |components|>
|
||||
<BlockSlot @name="selected">
|
||||
as |components|
|
||||
>
|
||||
<BlockSlot @name='selected'>
|
||||
<span>
|
||||
{{t "common.consul.status"}}
|
||||
{{t 'common.consul.status'}}
|
||||
</span>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="options">
|
||||
<BlockSlot @name='options'>
|
||||
{{#let components.Optgroup components.Option as |Optgroup Option|}}
|
||||
{{#each (array "passing" "warning" "critical" "empty") as |state|}}
|
||||
<Option class="value-{{state}}" @value={{state}} @selected={{includes state @filter.status.value}}>
|
||||
{{t (concat "common.consul." state)
|
||||
default=(array
|
||||
(concat "common.search." state)
|
||||
)
|
||||
}}
|
||||
{{#each this.healthStates as |state|}}
|
||||
<Option
|
||||
class='value-{{state}}'
|
||||
@value={{state}}
|
||||
@selected={{includes state @filter.status.value}}
|
||||
>
|
||||
{{t (concat 'common.consul.' state) default=(array (concat 'common.search.' state))}}
|
||||
</Option>
|
||||
{{/each}}
|
||||
{{/let}}
|
||||
</BlockSlot>
|
||||
</search.Select>
|
||||
<search.Select
|
||||
@position="left"
|
||||
@position='left'
|
||||
@onchange={{action @filter.kind.change}}
|
||||
@multiple={{true}}
|
||||
as |components|>
|
||||
<BlockSlot @name="selected">
|
||||
as |components|
|
||||
>
|
||||
<BlockSlot @name='selected'>
|
||||
<span>
|
||||
{{t "components.consul.service.search-bar.kind"}}
|
||||
{{t 'components.consul.service.search-bar.kind'}}
|
||||
</span>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="options">
|
||||
<BlockSlot @name='options'>
|
||||
{{#let components.Optgroup components.Option as |Optgroup Option|}}
|
||||
<Option @value="service" @selected={{includes 'service' @filter.kind.value}}>
|
||||
{{t "common.consul.service"}}
|
||||
<Option @value='service' @selected={{includes 'service' @filter.kind.value}}>
|
||||
{{t 'common.consul.service'}}
|
||||
</Option>
|
||||
<Optgroup
|
||||
@label={{t "common.consul.gateway"}}
|
||||
>
|
||||
{{#each (array "ingress-gateway" "terminating-gateway" "mesh-gateway") as |kind|}}
|
||||
<Optgroup @label={{t 'common.consul.gateway'}}>
|
||||
{{#each (array 'ingress-gateway' 'terminating-gateway' 'mesh-gateway') as |kind|}}
|
||||
<Option @value={{kind}} @selected={{includes kind @filter.kind.value}}>
|
||||
{{t (concat "common.consul." kind)}}
|
||||
{{t (concat 'common.consul.' kind)}}
|
||||
</Option>
|
||||
{{/each}}
|
||||
</Optgroup>
|
||||
<Optgroup
|
||||
@label={{t "common.consul.mesh"}}
|
||||
>
|
||||
{{#each (array "in-mesh" "not-in-mesh") as |state|}}
|
||||
<Optgroup @label={{t 'common.consul.mesh'}}>
|
||||
{{#each (array 'in-mesh' 'not-in-mesh') as |state|}}
|
||||
<Option @value={{state}} @selected={{includes state @filter.kind.value}}>
|
||||
{{t (concat "common.search." state)}}
|
||||
{{t (concat 'common.search.' state)}}
|
||||
</Option>
|
||||
{{/each}}
|
||||
</Optgroup>
|
||||
|
@ -128,25 +120,34 @@ as |key value|}}
|
|||
</search.Select>
|
||||
{{#if (gt @sources.length 0)}}
|
||||
<search.Select
|
||||
class="type-source"
|
||||
@position="left"
|
||||
class='type-source'
|
||||
@position='left'
|
||||
@onchange={{action @filter.source.change}}
|
||||
@multiple={{true}}
|
||||
as |components|>
|
||||
<BlockSlot @name="selected">
|
||||
as |components|
|
||||
>
|
||||
<BlockSlot @name='selected'>
|
||||
<span>
|
||||
{{t "common.search.source"}}
|
||||
{{t 'common.search.source'}}
|
||||
</span>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="options">
|
||||
<BlockSlot @name='options'>
|
||||
{{#let components.Option as |Option|}}
|
||||
{{#if (gt @sources.length 0)}}
|
||||
{{#each @sources as |source|}}
|
||||
<Option class={{source}} @value={{source}} @selected={{includes source @filter.source.value}}>
|
||||
{{t (concat "common.brand." source)}}
|
||||
<Option
|
||||
class={{source}}
|
||||
@value={{source}}
|
||||
@selected={{includes source @filter.source.value}}
|
||||
>
|
||||
{{t (concat 'common.brand.' source)}}
|
||||
</Option>
|
||||
{{/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'}}
|
||||
</Option>
|
||||
{{/if}}
|
||||
|
@ -157,36 +158,48 @@ as |key value|}}
|
|||
</:filter>
|
||||
<:sort as |search|>
|
||||
<search.Select
|
||||
class="type-sort"
|
||||
class='type-sort'
|
||||
data-test-sort-control
|
||||
@position="right"
|
||||
@position='right'
|
||||
@onchange={{action @sort.change}}
|
||||
@multiple={{false}}
|
||||
@required={{true}}
|
||||
as |components|>
|
||||
<BlockSlot @name="selected">
|
||||
as |components|
|
||||
>
|
||||
<BlockSlot @name='selected'>
|
||||
<span>
|
||||
{{#let (from-entries (array
|
||||
(array "Name:asc" (t "common.sort.alpha.asc"))
|
||||
(array "Name:desc" (t "common.sort.alpha.desc"))
|
||||
(array "Status:asc" (t "common.sort.status.asc"))
|
||||
(array "Status:desc" (t "common.sort.status.desc"))
|
||||
))
|
||||
{{#let
|
||||
(from-entries
|
||||
(array
|
||||
(array 'Name:asc' (t 'common.sort.alpha.asc'))
|
||||
(array 'Name:desc' (t 'common.sort.alpha.desc'))
|
||||
(array 'Status:asc' (t 'common.sort.status.asc'))
|
||||
(array 'Status:desc' (t 'common.sort.status.desc'))
|
||||
)
|
||||
)
|
||||
as |selectable|
|
||||
}}
|
||||
{{get selectable @sort.value}}
|
||||
{{/let}}
|
||||
</span>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="options">
|
||||
<BlockSlot @name='options'>
|
||||
{{#let components.Optgroup components.Option as |Optgroup Option|}}
|
||||
<Optgroup @label={{t "common.consul.status"}}>
|
||||
<Option @value="Status:asc" @selected={{eq "Status:asc" @sort.value}}>{{t "common.sort.status.asc"}}</Option>
|
||||
<Option @value="Status:desc" @selected={{eq "Status:desc" @sort.value}}>{{t "common.sort.status.desc"}}</Option>
|
||||
<Optgroup @label={{t 'common.consul.status'}}>
|
||||
<Option @value='Status:asc' @selected={{eq 'Status:asc' @sort.value}}>{{t
|
||||
'common.sort.status.asc'
|
||||
}}</Option>
|
||||
<Option @value='Status:desc' @selected={{eq 'Status:desc' @sort.value}}>{{t
|
||||
'common.sort.status.desc'
|
||||
}}</Option>
|
||||
</Optgroup>
|
||||
<Optgroup @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:desc" @selected={{eq "Name:desc" @sort.value}}>{{t "common.sort.alpha.desc"}}</Option>
|
||||
<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:desc' @selected={{eq 'Name:desc' @sort.value}}>{{t
|
||||
'common.sort.alpha.desc'
|
||||
}}</Option>
|
||||
</Optgroup>
|
||||
{{/let}}
|
||||
</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;
|
||||
color: rgb(var(--tone-gray-500));
|
||||
}
|
||||
%icon-definition.unknown dt::before,
|
||||
%composite-row-header .unknown dd::before {
|
||||
@extend %with-help-circle-outline-mask, %as-pseudo;
|
||||
color: rgb(var(--tone-gray-500));
|
||||
}
|
||||
|
||||
%composite-row-header [rel='me'] dd::before {
|
||||
@extend %with-check-circle-fill-mask, %as-pseudo;
|
||||
|
|
|
@ -79,10 +79,14 @@ export default class Outlet extends Component {
|
|||
this.previousState = this.state;
|
||||
this.state = new State('loading');
|
||||
this.endTransition = this.routlet.transition();
|
||||
let duration;
|
||||
if (this.element) {
|
||||
// if we have no transition-duration set immediately end the transition
|
||||
const duration = window
|
||||
.getComputedStyle(this.element)
|
||||
.getPropertyValue('transition-duration');
|
||||
duration = window.getComputedStyle(this.element).getPropertyValue('transition-duration');
|
||||
} else {
|
||||
duration = 0;
|
||||
}
|
||||
|
||||
if (parseFloat(duration) === 0) {
|
||||
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;
|
||||
color: rgb(var(--tone-gray-400));
|
||||
}
|
||||
%popover-select .value-unknown button::before {
|
||||
@extend %with-help-circle-outline-mask, %as-pseudo;
|
||||
color: rgb(var(--tone-gray-500));
|
||||
}
|
||||
%popover-select.type-source li:not(.partition) button {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
|
|
@ -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,17 +1,23 @@
|
|||
{{#let
|
||||
(dom-position (set this 'style') offset=true)
|
||||
"tab"
|
||||
as |select name|}}
|
||||
{{#let (dom-position (set this 'style') offset=true) 'tab' as |select name|}}
|
||||
<nav
|
||||
style={{{if this.style (concat
|
||||
'--selected-width:' this.style.width ';'
|
||||
'--selected-left:' this.style.left ';'
|
||||
'--selected-height:' this.style.height ';'
|
||||
'--selected-top:' this.style.top
|
||||
style={{{if
|
||||
this.style
|
||||
(concat
|
||||
'--selected-width:'
|
||||
this.style.width
|
||||
';'
|
||||
'--selected-left:'
|
||||
this.style.left
|
||||
';'
|
||||
'--selected-height:'
|
||||
this.style.height
|
||||
';'
|
||||
'--selected-top:'
|
||||
this.style.top
|
||||
)
|
||||
undefined
|
||||
}}}
|
||||
aria-label="Secondary"
|
||||
aria-label='Secondary'
|
||||
class={{concat 'tab-nav' ' animatable'}}
|
||||
...attributes
|
||||
>
|
||||
|
@ -19,26 +25,23 @@ as |select name|}}
|
|||
{{#each @items as |item|}}
|
||||
<li
|
||||
{{on 'click' (fn select)}}
|
||||
{{did-upsert
|
||||
(if item.selected
|
||||
(fn select)
|
||||
(noop)
|
||||
)
|
||||
@items.length
|
||||
}}
|
||||
{{did-upsert (if item.selected (fn select) (noop)) @items.length}}
|
||||
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
|
||||
{{on 'click'
|
||||
(fn this.onClick (uppercase item.label))
|
||||
}}
|
||||
{{on 'click'
|
||||
(fn this.onTabClicked item)
|
||||
}}
|
||||
{{on 'click' (fn this.onClick (uppercase item.label))}}
|
||||
{{on 'click' (fn this.onTabClicked item)}}
|
||||
@href={{item.href}}
|
||||
>
|
||||
{{#if item.tooltip}}
|
||||
<span {{tooltip item.tooltip}}>{{item.label}}</span>
|
||||
{{else}}
|
||||
{{item.label}}
|
||||
{{/if}}
|
||||
</Action>
|
||||
</li>
|
||||
{{/each}}
|
||||
|
|
|
@ -1,4 +1,76 @@
|
|||
import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { hrefTo } from 'consul-ui/helpers/href-to';
|
||||
|
||||
/**
|
||||
* A class that encapsulates the data abstraction that we expect the TabNav to
|
||||
* be passed as `@items`.
|
||||
*
|
||||
* You can use this class when you want to create tab-nav from javascript.
|
||||
*
|
||||
* Instead of doing this in the template layer:
|
||||
*
|
||||
* ```handlebars
|
||||
* <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() {}
|
||||
export default class TabNav extends Component {
|
||||
|
|
|
@ -14,6 +14,7 @@ export default {
|
|||
warning: (item, value) => item.MeshStatus === value,
|
||||
critical: (item, value) => item.MeshStatus === value,
|
||||
empty: (item, value) => item.MeshChecksTotal === 0,
|
||||
unknown: (item) => item.peerIsFailing || item.isZeroCountButPeered,
|
||||
},
|
||||
instance: {
|
||||
registered: (item, value) => item.InstanceCount > 0,
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
hrefTo(routeName, params, hash) {
|
||||
hrefTo(routeName, params, _hash) {
|
||||
// copy to always work with a new hash even when helper persists hash
|
||||
const hash = { ..._hash };
|
||||
|
||||
if (typeof hash.dc !== 'undefined') {
|
||||
delete hash.dc;
|
||||
}
|
||||
|
||||
if (typeof hash.nspace !== 'undefined') {
|
||||
hash.nspace = `~${hash.nspace}`;
|
||||
}
|
||||
|
|
|
@ -17,17 +17,34 @@ export default class Peer extends Model {
|
|||
@attr('string') Name;
|
||||
@attr('string') State;
|
||||
@attr('string') ID;
|
||||
|
||||
// only the side that establishes will hold this property
|
||||
@attr('string') PeerID;
|
||||
|
||||
@attr() PeerServerAddresses;
|
||||
|
||||
// StreamStatus
|
||||
@nullValue([]) @attr() ImportedServices;
|
||||
@nullValue([]) @attr() ExportedServices;
|
||||
@attr('date') LastHeartbeat;
|
||||
@attr('date') LastReceive;
|
||||
@attr('date') LastSend;
|
||||
@attr() PeerServerAddresses;
|
||||
|
||||
get ImportedServiceCount() {
|
||||
return this.ImportedServices.length;
|
||||
}
|
||||
|
||||
get ExportedServiceCount() {
|
||||
return this.ExportedServices.length;
|
||||
}
|
||||
|
||||
// if we receive a PeerID we know that we are dealing with the side that
|
||||
// established the peering
|
||||
get isReceiver() {
|
||||
return this.PeerID;
|
||||
}
|
||||
|
||||
get isDialer() {
|
||||
return !this.isReceiver;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import Model, { attr } from '@ember-data/model';
|
||||
import Model, { attr, belongsTo } from '@ember-data/model';
|
||||
import { computed } from '@ember/object';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { fragment } from 'ember-data-model-fragments/attributes';
|
||||
|
@ -58,6 +58,18 @@ export default class Service extends Model {
|
|||
|
||||
@attr() meta; // {}
|
||||
|
||||
@belongsTo({ async: false }) peer;
|
||||
|
||||
@computed('peer', 'InstanceCount')
|
||||
get isZeroCountButPeered() {
|
||||
return this.peer && this.InstanceCount === 0;
|
||||
}
|
||||
|
||||
@computed('peer.State')
|
||||
get peerIsFailing() {
|
||||
return this.peer && this.peer.State === 'FAILING';
|
||||
}
|
||||
|
||||
@computed('ChecksPassing', 'ChecksWarning', 'ChecksCritical')
|
||||
get ChecksTotal() {
|
||||
return this.ChecksPassing + this.ChecksWarning + this.ChecksCritical;
|
||||
|
@ -79,9 +91,19 @@ export default class Service extends Model {
|
|||
return this.MeshEnabled || (this.Kind || '').length > 0;
|
||||
}
|
||||
|
||||
@computed('MeshChecksPassing', 'MeshChecksWarning', 'MeshChecksCritical')
|
||||
@computed(
|
||||
'MeshChecksPassing',
|
||||
'MeshChecksWarning',
|
||||
'MeshChecksCritical',
|
||||
'isZeroCountButPeered',
|
||||
'peerIsFailing'
|
||||
)
|
||||
get MeshStatus() {
|
||||
switch (true) {
|
||||
case this.isZeroCountButPeered:
|
||||
return 'unknown';
|
||||
case this.peerIsFailing:
|
||||
return 'unknown';
|
||||
case this.MeshChecksCritical !== 0:
|
||||
return 'critical';
|
||||
case this.MeshChecksWarning !== 0:
|
||||
|
@ -93,6 +115,27 @@ export default class Service extends Model {
|
|||
}
|
||||
}
|
||||
|
||||
@computed('isZeroCountButPeered', 'peerIsFailing', 'MeshStatus')
|
||||
get healthTooltipText() {
|
||||
const { MeshStatus, isZeroCountButPeered, peerIsFailing } = this;
|
||||
if (isZeroCountButPeered) {
|
||||
return 'This service currently has 0 instances. Check with the operator of its peer to make sure this is expected behavior.';
|
||||
}
|
||||
if (peerIsFailing) {
|
||||
return 'This peer is out of sync, so the current health statuses of its services are unknown.';
|
||||
}
|
||||
if (MeshStatus === 'critical') {
|
||||
return 'At least one health check on one instance is failing.';
|
||||
}
|
||||
if (MeshStatus === 'warning') {
|
||||
return 'At least one health check on one instance has a warning.';
|
||||
}
|
||||
if (MeshStatus == 'passing') {
|
||||
return 'All health checks are passing.';
|
||||
}
|
||||
return 'There are no health checks';
|
||||
}
|
||||
|
||||
@computed('ChecksPassing', 'Proxy.ChecksPassing')
|
||||
get MeshChecksPassing() {
|
||||
let proxyCount = 0;
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import Serializer from './application';
|
||||
import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/service';
|
||||
import { get } from '@ember/object';
|
||||
import {
|
||||
HEADERS_NAMESPACE as HTTP_HEADERS_NAMESPACE,
|
||||
HEADERS_PARTITION as HTTP_HEADERS_PARTITION,
|
||||
} from 'consul-ui/utils/http/consul';
|
||||
|
||||
export default class ServiceSerializer extends Serializer {
|
||||
primaryKey = PRIMARY_KEY;
|
||||
|
@ -13,6 +17,54 @@ export default class ServiceSerializer extends Serializer {
|
|||
// Services and proxies all come together in the same list. Here we
|
||||
// map the proxies to their related services on a Service.Proxy
|
||||
// property for easy access later on
|
||||
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 = {};
|
||||
body
|
||||
.filter(function (item) {
|
||||
|
@ -36,26 +88,6 @@ export default class ServiceSerializer extends Serializer {
|
|||
});
|
||||
}
|
||||
});
|
||||
|
||||
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
|
||||
);
|
||||
return body;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import RepositoryService from 'consul-ui/services/repository';
|
||||
import dataSource from 'consul-ui/decorators/data-source';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
function normalizePeerPayload(peerPayload, dc, partition) {
|
||||
const {
|
||||
|
@ -18,10 +19,31 @@ function normalizePeerPayload(peerPayload, dc, partition) {
|
|||
};
|
||||
}
|
||||
export default class PeerService extends RepositoryService {
|
||||
@service store;
|
||||
|
||||
getModelName() {
|
||||
return 'peer';
|
||||
}
|
||||
|
||||
@dataSource('/:partition/:ns/:ds/exported-services/:name')
|
||||
async fetchExportedServices({ dc, ns, partition, name }, configuration, request) {
|
||||
return (
|
||||
await request`
|
||||
GET /v1/internal/ui/exported-services
|
||||
|
||||
${{
|
||||
peer: name,
|
||||
}}
|
||||
`
|
||||
)((headers, body, cache) => {
|
||||
const serviceSerializer = this.store.serializerFor('service');
|
||||
|
||||
return this.store.push(
|
||||
serviceSerializer.createJSONApiDocumentFromServicesPayload(headers, body, dc)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@dataSource('/:partition/:ns/:dc/peering/token-for/:name')
|
||||
async fetchToken({ dc, ns, partition, name }, configuration, request) {
|
||||
return (
|
||||
|
@ -85,6 +107,22 @@ export default class PeerService extends RepositoryService {
|
|||
}}
|
||||
`
|
||||
)((headers, body, cache) => {
|
||||
// we can't easily use fragments as we are working around the serializer
|
||||
// layer
|
||||
const { StreamStatus } = body;
|
||||
|
||||
if (StreamStatus) {
|
||||
if (StreamStatus.LastHeartbeat) {
|
||||
StreamStatus.LastHeartbeat = new Date(StreamStatus.LastHeartbeat);
|
||||
}
|
||||
if (StreamStatus.LastReceive) {
|
||||
StreamStatus.LastReceive = new Date(StreamStatus.LastReceive);
|
||||
}
|
||||
if (StreamStatus.LastSend) {
|
||||
StreamStatus.LastSend = new Date(StreamStatus.LastSend);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
meta: {
|
||||
version: 2,
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import RepositoryService from 'consul-ui/services/repository';
|
||||
import dataSource from 'consul-ui/decorators/data-source';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
const modelName = 'service';
|
||||
export default class ServiceService extends RepositoryService {
|
||||
@service store;
|
||||
|
||||
getModelName() {
|
||||
return modelName;
|
||||
}
|
||||
|
@ -12,6 +15,23 @@ export default class ServiceService extends RepositoryService {
|
|||
return super.findAll(...arguments);
|
||||
}
|
||||
|
||||
@dataSource('/:partition/:ns/:dc/services/:peer/:peerId')
|
||||
async findAllImportedServices(params, configuration) {
|
||||
// remember peer.id so that we can add it to to the service later on to setup relationship
|
||||
const { peerId } = params;
|
||||
|
||||
// don't send peerId with query
|
||||
delete params.peerId;
|
||||
|
||||
// assign the peer as a belongs-to. we don't have access to any information
|
||||
// we could use to do this in the serializer so we need to do it manually here
|
||||
return super.findAll(params, configuration).then((services) => {
|
||||
const peer = this.store.peekRecord('peer', peerId);
|
||||
services.forEach((service) => (service.peer = peer));
|
||||
return services;
|
||||
});
|
||||
}
|
||||
|
||||
@dataSource('/:partition/:ns/:dc/gateways/for-service/:gateway')
|
||||
findGatewayBySlug(params, configuration = {}) {
|
||||
if (typeof configuration.cursor !== 'undefined') {
|
||||
|
|
|
@ -119,7 +119,8 @@ html:not(.with-data-source) .data-source-debug {
|
|||
/* hi */
|
||||
z-index: 100000;
|
||||
}
|
||||
html.with-href-to [href^='console://']::before {
|
||||
html.with-href-to [href^='console://']::before
|
||||
{
|
||||
@extend %p3;
|
||||
@extend %debug-box;
|
||||
content: attr(href);
|
||||
|
@ -155,6 +156,7 @@ html.with-route-announcer .route-title {
|
|||
margin-bottom: 100px;
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
li.provider-components a::before,
|
||||
li.consul-components a::before,
|
||||
li.components a::before {
|
||||
@extend %with-logo-glimmer-color-icon, %as-pseudo;
|
||||
|
@ -164,6 +166,7 @@ html.with-route-announcer .route-title {
|
|||
li.components.css-component a::before {
|
||||
@extend %with-logo-glimmer-color-icon, %as-pseudo;
|
||||
}
|
||||
li.provider-components.ember-component a::before,
|
||||
li.consul-components.ember-component a::before,
|
||||
li.components.ember-component a::before {
|
||||
@extend %with-logo-ember-circle-color-icon, %as-pseudo;
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
<Route
|
||||
@name={{routeName}}
|
||||
as |route|>
|
||||
<Route @name={{routeName}} as |route|>
|
||||
<DataLoader
|
||||
@src={{uri '/${partition}/${nspace}/${dc}/service-instance/${id}/${node}/${name}/${peer}'
|
||||
@src={{uri
|
||||
'/${partition}/${nspace}/${dc}/service-instance/${id}/${node}/${name}/${peer}'
|
||||
(hash
|
||||
partition=route.params.partition
|
||||
nspace=route.params.nspace
|
||||
|
@ -13,24 +12,21 @@ as |route|>
|
|||
peer=route.params.peer
|
||||
)
|
||||
}}
|
||||
as |loader|>
|
||||
as |loader|
|
||||
>
|
||||
|
||||
<BlockSlot @name="error">
|
||||
<AppError
|
||||
@error={{loader.error}}
|
||||
@login={{route.model.app.login.open}}
|
||||
/>
|
||||
<BlockSlot @name='error'>
|
||||
<AppError @error={{loader.error}} @login={{route.model.app.login.open}} />
|
||||
</BlockSlot>
|
||||
|
||||
<BlockSlot @name="disconnected" as |after|>
|
||||
{{#if (eq loader.error.status "404")}}
|
||||
<BlockSlot @name='disconnected' as |after|>
|
||||
{{#if (eq loader.error.status '404')}}
|
||||
<Notice
|
||||
{{notification
|
||||
sticky=true
|
||||
}}
|
||||
class="notification-update"
|
||||
@type="warning"
|
||||
as |notice|>
|
||||
{{notification sticky=true}}
|
||||
class='notification-update'
|
||||
@type='warning'
|
||||
as |notice|
|
||||
>
|
||||
<notice.Header>
|
||||
<strong>Warning!</strong>
|
||||
</notice.Header>
|
||||
|
@ -40,14 +36,8 @@ as |route|>
|
|||
</p>
|
||||
</notice.Body>
|
||||
</Notice>
|
||||
{{else if (eq loader.error.status "403")}}
|
||||
<Notice
|
||||
{{notification
|
||||
sticky=true
|
||||
}}
|
||||
class="notification-update"
|
||||
@type="error"
|
||||
as |notice|>
|
||||
{{else if (eq loader.error.status '403')}}
|
||||
<Notice {{notification sticky=true}} class='notification-update' @type='error' as |notice|>
|
||||
<notice.Header>
|
||||
<strong>Error!</strong>
|
||||
</notice.Header>
|
||||
|
@ -58,13 +48,7 @@ as |route|>
|
|||
</notice.Body>
|
||||
</Notice>
|
||||
{{else}}
|
||||
<Notice
|
||||
{{notification
|
||||
sticky=true
|
||||
}}
|
||||
class="notification-update"
|
||||
@type="error"
|
||||
as |notice|>
|
||||
<Notice {{notification sticky=true}} class='notification-update' @type='error' as |notice|>
|
||||
<notice.Header>
|
||||
<strong>Warning!</strong>
|
||||
</notice.Header>
|
||||
|
@ -77,15 +61,12 @@ as |route|>
|
|||
{{/if}}
|
||||
</BlockSlot>
|
||||
|
||||
<BlockSlot @name="loaded">
|
||||
{{#let
|
||||
|
||||
loader.data
|
||||
|
||||
as |item|}}
|
||||
<BlockSlot @name='loaded'>
|
||||
{{#let loader.data as |item|}}
|
||||
{{#if item.IsOrigin}}
|
||||
<DataSource
|
||||
@src={{uri '/${partition}/${nspace}/${dc}/proxy-instance/${id}/${node}/${name}'
|
||||
@src={{uri
|
||||
'/${partition}/${nspace}/${dc}/proxy-instance/${id}/${node}/${name}'
|
||||
(hash
|
||||
partition=route.params.partition
|
||||
nspace=route.params.nspace
|
||||
|
@ -95,8 +76,9 @@ as |item|}}
|
|||
name=route.params.name
|
||||
)
|
||||
}}
|
||||
@onchange={{action (mut meta) value="data"}}
|
||||
as |meta|>
|
||||
@onchange={{action (mut meta) value='data'}}
|
||||
as |meta|
|
||||
>
|
||||
{{! We only really need meta to get the correct ServiceID }}
|
||||
{{! but we may as well use the NodeName and ServiceName }}
|
||||
{{! from meta also, but they should be the same as the instance }}
|
||||
|
@ -109,7 +91,8 @@ as |item|}}
|
|||
{{! and this second request get the info for that instance and saves }}
|
||||
{{! it into the `proxy` variable }}
|
||||
<DataSource
|
||||
@src={{uri '/${partition}/${nspace}/${dc}/service-instance/${id}/${node}/${name}/${peer}'
|
||||
@src={{uri
|
||||
'/${partition}/${nspace}/${dc}/service-instance/${id}/${node}/${name}/${peer}'
|
||||
(hash
|
||||
partition=route.params.partition
|
||||
nspace=route.params.nspace
|
||||
|
@ -120,21 +103,25 @@ as |item|}}
|
|||
peer=route.params.peer
|
||||
)
|
||||
}}
|
||||
@onchange={{action (mut proxy) value="data"}}
|
||||
@onchange={{action (mut proxy) value='data'}}
|
||||
/>
|
||||
{{/if}}
|
||||
</DataSource>
|
||||
{{/if}}
|
||||
<AppView>
|
||||
<BlockSlot @name="breadcrumbs">
|
||||
<BlockSlot @name='breadcrumbs'>
|
||||
<ol>
|
||||
<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}})
|
||||
</a></li>
|
||||
</ol>
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="header">
|
||||
<BlockSlot @name='header'>
|
||||
<h1>
|
||||
<route.Title @title={{item.Service.ID}} />
|
||||
</h1>
|
||||
|
@ -146,49 +133,87 @@ as |item|}}
|
|||
<Consul::TransparentProxy />
|
||||
{{/if}}
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="nav">
|
||||
<BlockSlot @name='nav'>
|
||||
<dl>
|
||||
<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>
|
||||
{{#unless item.Node.Meta.synthetic-node}}
|
||||
<dl>
|
||||
<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>
|
||||
{{/unless}}
|
||||
{{#if item.Service.PeerName}}
|
||||
<dl>
|
||||
<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>
|
||||
{{/if}}
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="actions">
|
||||
<BlockSlot @name='actions'>
|
||||
{{#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}}
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="content">
|
||||
<TabNav @items={{
|
||||
compact
|
||||
<BlockSlot @name='content'>
|
||||
<TabNav
|
||||
@items={{compact
|
||||
(array
|
||||
(hash label="Health Checks" href=(href-to "dc.services.instance.healthchecks") selected=(is-href "dc.services.instance.healthchecks"))
|
||||
(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"))
|
||||
(hash
|
||||
label='Health Checks'
|
||||
href=(href-to 'dc.services.instance.healthchecks')
|
||||
selected=(is-href 'dc.services.instance.healthchecks')
|
||||
)
|
||||
(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
|
||||
@name={{routeName}}
|
||||
@model={{assign (hash
|
||||
proxy=proxy
|
||||
meta=meta
|
||||
item=item
|
||||
) route.model}}
|
||||
as |o|>
|
||||
@model={{assign (hash proxy=proxy meta=meta item=item) route.model}}
|
||||
as |o|
|
||||
>
|
||||
{{outlet}}
|
||||
</Outlet>
|
||||
</BlockSlot>
|
||||
|
|
|
@ -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()}",
|
||||
"ImportedServiceCount": ${fake.random.number({min: 0, max: 4000})},
|
||||
"ExportedServiceCount": ${fake.random.number({min: 0, max: 4000})},
|
||||
"Name": "${location.pathname.get(2)}",
|
||||
"State": "${fake.helpers.randomize([
|
||||
'ACTIVE',
|
||||
|
@ -12,6 +10,13 @@
|
|||
'TERMINATED',
|
||||
'UNDEFINED'
|
||||
])}",
|
||||
"StreamStatus": {
|
||||
"LastHeartbeat": "${fake.date.past(10).toISOString()}",
|
||||
"LastReceive": "${fake.date.past(10).toISOString()}",
|
||||
"LastSend": "${fake.date.past(10).toISOString()}",
|
||||
"ExportedServices": [${range(0, Math.floor(Math.random() * 10)).map((i) => `"exported-service-${i}"`)}],
|
||||
"ImportedServices": [${range(0, Math.floor(Math.random() * 10)).map((i) => `"imported-service-${i}"`)}]
|
||||
},
|
||||
"PeerID": "${fake.random.uuid()}",
|
||||
"PeerServerName": "${fake.internet.domainName()}",
|
||||
"PeerServerAddresses": [
|
||||
|
|
|
@ -22,8 +22,8 @@
|
|||
"LastHeartbeat": "${fake.date.past(10).toISOString()}",
|
||||
"LastReceive": "${fake.date.past(10).toISOString()}",
|
||||
"LastSend": "${fake.date.past(10).toISOString()}",
|
||||
"ExportedServices": ["${`export-service-${i}`}"],
|
||||
"ImportedServices": ["${`import-service-${i}`}"]
|
||||
"ExportedServices": [${range(0, Math.floor(Math.random() * 10)).map((i) => `"exported-service-${i}"`)}],
|
||||
"ImportedServices": [${range(0, Math.floor(Math.random() * 10)).map((i) => `"imported-service-${i}"`)}]
|
||||
},
|
||||
"PeerID": "${id}",
|
||||
"PeerServerName": "${fake.internet.domainName()}",
|
||||
|
|
|
@ -71,6 +71,7 @@
|
|||
"@hashicorp/design-system-tokens": "^1.0.0",
|
||||
"@hashicorp/ember-cli-api-double": "^4.0.0",
|
||||
"@hashicorp/ember-flight-icons": "^2.0.12",
|
||||
"@html-next/vertical-collection": "^4.0.0",
|
||||
"@lit/reactive-element": "^1.2.1",
|
||||
"@xstate/fsm": "^1.4.0",
|
||||
"a11y-dialog": "^6.0.1",
|
||||
|
@ -146,7 +147,7 @@
|
|||
"ember-power-select": "^4.0.5",
|
||||
"ember-power-select-with-create": "^0.8.0",
|
||||
"ember-qunit": "^5.1.1",
|
||||
"ember-ref-modifier": "^1.0.0",
|
||||
"ember-ref-bucket": "^4.1.0",
|
||||
"ember-render-helpers": "^0.2.0",
|
||||
"ember-resolver": "^8.0.2",
|
||||
"ember-route-action-helper": "^2.0.8",
|
||||
|
|
|
@ -34,7 +34,7 @@ function colorMapFromTokens(tokensPath) {
|
|||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ['./app/**/*.{html,js,hbs,mdx}', './docs/**/*.{html,js,hbs,mdx}'],
|
||||
content: ['../**/*.{html.js,hbs,mdx}'],
|
||||
theme: {
|
||||
colors: colorMapFromTokens(
|
||||
'../../node_modules/@hashicorp/design-system-tokens/dist/products/css/tokens.css'
|
||||
|
|
|
@ -6,6 +6,8 @@ Feature: dc / peers / regenerate: Regenerate Peer Token
|
|||
---
|
||||
Name: peer-name
|
||||
State: ACTIVE
|
||||
# dialer does not have a PeerID
|
||||
PeerID: null
|
||||
---
|
||||
And the url "/v1/peering/token" responds with from yaml
|
||||
---
|
||||
|
@ -19,3 +21,19 @@ Feature: dc / peers / regenerate: Regenerate Peer Token
|
|||
And I click actions on the peers
|
||||
And I click regenerate on the peers
|
||||
Then I see the text "an-encoded-token" in ".consul-peer-form-generate code"
|
||||
|
||||
Scenario:
|
||||
Given 1 datacenter model with the value "datacenter"
|
||||
And 1 peer model from yaml
|
||||
---
|
||||
Name: peer-name
|
||||
State: ACTIVE
|
||||
# receiver holds a PeerID
|
||||
PeerID: some-id
|
||||
---
|
||||
When I visit the peers page for yaml
|
||||
---
|
||||
dc: datacenter
|
||||
---
|
||||
And I click actions on the peers
|
||||
Then I don't see regenerate on peers
|
||||
|
|
|
@ -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 nspace from 'consul-ui/tests/pages/dc/nspaces/edit';
|
||||
import peers from 'consul-ui/tests/pages/dc/peers/index';
|
||||
import peersShow from 'consul-ui/tests/pages/dc/peers/show';
|
||||
|
||||
// utils
|
||||
const deletable = createDeletable(clickable);
|
||||
|
@ -234,6 +235,7 @@ export default {
|
|||
nspace(visitable, submitable, deletable, cancelable, policySelector, roleSelector)
|
||||
),
|
||||
peers: create(peers(visitable, creatable, consulPeerList, popoverSelect)),
|
||||
peer: create(peersShow(visitable)),
|
||||
settings: create(settings(visitable, submitable, isPresent)),
|
||||
routingConfig: create(routingConfig(visitable, text)),
|
||||
};
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import tabgroup from 'consul-ui/components/tab-nav/pageobject';
|
||||
|
||||
export default function (visitable, creatable, items, popoverSelect) {
|
||||
return creatable({
|
||||
visit: visitable('/:dc/peers'),
|
||||
peers: items(),
|
||||
sort: popoverSelect('[data-test-sort-control]'),
|
||||
tabs: tabgroup('tab', ['imported-services', 'exported-services', 'addresses']),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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?
|
||||
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} imported services
|
||||
tooltip: The number of services imported from {name}
|
||||
tab-tooltip: Services imported from {name}
|
||||
exported:
|
||||
count: |
|
||||
{count} exported services
|
||||
tooltip: The number of services exported from {name}
|
||||
tab-tooltip: Services exported from {name}
|
||||
addresses:
|
||||
tooltip: The number of services exported from {name}
|
||||
show:
|
||||
imported:
|
||||
empty:
|
||||
header: No visible imported services from {name}
|
||||
body: |
|
||||
<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:
|
||||
index:
|
||||
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"
|
||||
integrity sha512-jYUA0M6Tz+4RAudil+GW/fHbhZPcKCiIZZAguBDviqbLneMkMgPOBgbXWCGWsEQ1fJzP2cXbUaio8L0aQZPWQw==
|
||||
|
||||
"@html-next/vertical-collection@^4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@html-next/vertical-collection/-/vertical-collection-4.0.0.tgz#b3b3d52358e15e7ed46e028d12424dab994690ed"
|
||||
integrity sha512-/c4y6ASmkMwyG+rcoXH3kx50LiK2MuPX0bsktd+j9LhOD6zkJyT4wJ73m20dCEvxjgwA/nCQ8hj3lApQlHG0CQ==
|
||||
dependencies:
|
||||
babel6-plugin-strip-class-callcheck "^6.0.0"
|
||||
broccoli-funnel "^3.0.8"
|
||||
broccoli-merge-trees "^4.2.0"
|
||||
broccoli-rollup "^5.0.0"
|
||||
ember-cli-babel "^7.12.0"
|
||||
ember-cli-htmlbars "^6.0.0"
|
||||
ember-cli-version-checker "^5.1.2"
|
||||
ember-raf-scheduler "^0.3.0"
|
||||
|
||||
"@humanwhocodes/config-array@^0.5.0":
|
||||
version "0.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9"
|
||||
|
@ -3511,6 +3525,13 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/broccoli-plugin/-/broccoli-plugin-1.3.0.tgz#38f8462fecaebc4e09a32e4d4ed1b9808f75bbca"
|
||||
integrity sha512-SLk4/hFc2kGvgwNFrpn2O1juxFOllcHAywvlo7VwxfExLzoz1GGJ0oIZCwj5fwSpvHw4AWpZjJ1fUvb62PDayQ==
|
||||
|
||||
"@types/broccoli-plugin@^3.0.0":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/broccoli-plugin/-/broccoli-plugin-3.0.0.tgz#290fda2270c47a568edfd0cefab8bb840d8bb7b2"
|
||||
integrity sha512-f+TcsARR2PovfFRKFdCX0kfH/QoM3ZVD2h1rl2mNvrKO0fq2uBNCBsTU3JanfU4COCt5cXpTfARyUsERlC8vIw==
|
||||
dependencies:
|
||||
broccoli-plugin "*"
|
||||
|
||||
"@types/chai-as-promised@^7.1.2":
|
||||
version "7.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/chai-as-promised/-/chai-as-promised-7.1.3.tgz#779166b90fda611963a3adbfd00b339d03b747bd"
|
||||
|
@ -5945,6 +5966,19 @@ broccoli-persistent-filter@^3.1.2:
|
|||
symlink-or-copy "^1.0.1"
|
||||
sync-disk-cache "^2.0.0"
|
||||
|
||||
broccoli-plugin@*, broccoli-plugin@^4.0.5, broccoli-plugin@^4.0.7:
|
||||
version "4.0.7"
|
||||
resolved "https://registry.yarnpkg.com/broccoli-plugin/-/broccoli-plugin-4.0.7.tgz#dd176a85efe915ed557d913744b181abe05047db"
|
||||
integrity sha512-a4zUsWtA1uns1K7p9rExYVYG99rdKeGRymW0qOCNkvDPHQxVi3yVyJHhQbM3EZwdt2E0mnhr5e0c/bPpJ7p3Wg==
|
||||
dependencies:
|
||||
broccoli-node-api "^1.7.0"
|
||||
broccoli-output-wrapper "^3.2.5"
|
||||
fs-merger "^3.2.1"
|
||||
promise-map-series "^0.3.0"
|
||||
quick-temp "^0.1.8"
|
||||
rimraf "^3.0.2"
|
||||
symlink-or-copy "^1.3.1"
|
||||
|
||||
broccoli-plugin@1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/broccoli-plugin/-/broccoli-plugin-1.1.0.tgz#73e2cfa05f8ea1e3fc1420c40c3d9e7dc724bf02"
|
||||
|
@ -6001,19 +6035,6 @@ broccoli-plugin@^4.0.0, broccoli-plugin@^4.0.1, broccoli-plugin@^4.0.2, broccoli
|
|||
rimraf "^3.0.2"
|
||||
symlink-or-copy "^1.3.1"
|
||||
|
||||
broccoli-plugin@^4.0.5, broccoli-plugin@^4.0.7:
|
||||
version "4.0.7"
|
||||
resolved "https://registry.yarnpkg.com/broccoli-plugin/-/broccoli-plugin-4.0.7.tgz#dd176a85efe915ed557d913744b181abe05047db"
|
||||
integrity sha512-a4zUsWtA1uns1K7p9rExYVYG99rdKeGRymW0qOCNkvDPHQxVi3yVyJHhQbM3EZwdt2E0mnhr5e0c/bPpJ7p3Wg==
|
||||
dependencies:
|
||||
broccoli-node-api "^1.7.0"
|
||||
broccoli-output-wrapper "^3.2.5"
|
||||
fs-merger "^3.2.1"
|
||||
promise-map-series "^0.3.0"
|
||||
quick-temp "^0.1.8"
|
||||
rimraf "^3.0.2"
|
||||
symlink-or-copy "^1.3.1"
|
||||
|
||||
broccoli-postcss-single@^5.0.1:
|
||||
version "5.0.2"
|
||||
resolved "https://registry.yarnpkg.com/broccoli-postcss-single/-/broccoli-postcss-single-5.0.2.tgz#f23661b3011494d8a2dbd8ff39eb394e80313682"
|
||||
|
@ -6052,6 +6073,21 @@ broccoli-rollup@^4.1.1:
|
|||
symlink-or-copy "^1.2.0"
|
||||
walk-sync "^1.1.3"
|
||||
|
||||
broccoli-rollup@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/broccoli-rollup/-/broccoli-rollup-5.0.0.tgz#a77b53bcef1b70e988913fee82265c0a4ca530da"
|
||||
integrity sha512-QdMuXHwsdz/LOS8zu4HP91Sfi4ofimrOXoYP/lrPdRh7lJYD87Lfq4WzzUhGHsxMfzANIEvl/7qVHKD3cFJ4tA==
|
||||
dependencies:
|
||||
"@types/broccoli-plugin" "^3.0.0"
|
||||
broccoli-plugin "^4.0.7"
|
||||
fs-tree-diff "^2.0.1"
|
||||
heimdalljs "^0.2.6"
|
||||
node-modules-path "^1.0.1"
|
||||
rollup "^2.50.0"
|
||||
rollup-pluginutils "^2.8.1"
|
||||
symlink-or-copy "^1.2.0"
|
||||
walk-sync "^2.2.0"
|
||||
|
||||
broccoli-sass-source-maps@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/broccoli-sass-source-maps/-/broccoli-sass-source-maps-4.0.0.tgz#1ee4c10a810b10955b0502e28f85ab672f5961a2"
|
||||
|
@ -7946,7 +7982,7 @@ ember-cli-babel@^6.0.0, ember-cli-babel@^6.0.0-beta.4, ember-cli-babel@^6.11.0,
|
|||
ember-cli-version-checker "^2.1.2"
|
||||
semver "^5.5.0"
|
||||
|
||||
ember-cli-babel@^7.13.2, ember-cli-babel@^7.26.1, ember-cli-babel@^7.26.11, ember-cli-babel@^7.26.3, ember-cli-babel@^7.26.5, ember-cli-babel@^7.4.0:
|
||||
ember-cli-babel@^7.12.0, ember-cli-babel@^7.13.2, ember-cli-babel@^7.26.1, ember-cli-babel@^7.26.11, ember-cli-babel@^7.26.3, ember-cli-babel@^7.26.5, ember-cli-babel@^7.4.0:
|
||||
version "7.26.11"
|
||||
resolved "https://registry.yarnpkg.com/ember-cli-babel/-/ember-cli-babel-7.26.11.tgz#50da0fe4dcd99aada499843940fec75076249a9f"
|
||||
integrity sha512-JJYeYjiz/JTn34q7F5DSOjkkZqy8qwFOOxXfE6pe9yEJqWGu4qErKxlz8I22JoVEQ/aBUO+OcKTpmctvykM9YA==
|
||||
|
@ -8881,7 +8917,7 @@ ember-modifier@^2.1.0, ember-modifier@^2.1.1:
|
|||
ember-destroyable-polyfill "^2.0.2"
|
||||
ember-modifier-manager-polyfill "^1.2.0"
|
||||
|
||||
"ember-modifier@^2.1.2 || ^3.1.0 || ^4.0.0":
|
||||
"ember-modifier@^2.1.2 || ^3.1.0 || ^4.0.0", ember-modifier@^3.2.7:
|
||||
version "3.2.7"
|
||||
resolved "https://registry.yarnpkg.com/ember-modifier/-/ember-modifier-3.2.7.tgz#f2d35b7c867cbfc549e1acd8d8903c5ecd02ea4b"
|
||||
integrity sha512-ezcPQhH8jUfcJQbbHji4/ZG/h0yyj1jRDknfYue/ypQS8fM8LrGcCMo0rjDZLzL1Vd11InjNs3BD7BdxFlzGoA==
|
||||
|
@ -8981,12 +9017,21 @@ ember-qunit@^5.1.1:
|
|||
silent-error "^1.1.1"
|
||||
validate-peer-dependencies "^1.2.0"
|
||||
|
||||
ember-ref-modifier@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ember-ref-modifier/-/ember-ref-modifier-1.0.1.tgz#aeca56798ebc0fb750f0ccd36e86d667f5b1bc44"
|
||||
integrity sha512-qmEFY/4WOWWXABRWvX50jLPleH30p/LPLUXEvaSlIj41F23e3Vul91IqZ1PFdw1Rxpkb8DHWO5BRchN8vnz4+Q==
|
||||
ember-raf-scheduler@^0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/ember-raf-scheduler/-/ember-raf-scheduler-0.3.0.tgz#7657ee5c1d54f852731e61e9d0e0750a9a22f5f4"
|
||||
integrity sha512-i8JWQidNCX7n5TOTIKRDR0bnsQN9aJh/GtOJKINz2Wr+I7L7sYVhli6MFqMYNGKC9j9e6iWsznfAIxddheyEow==
|
||||
dependencies:
|
||||
ember-cli-babel "^7.20.5"
|
||||
ember-cli-babel "^7.26.6"
|
||||
|
||||
ember-ref-bucket@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/ember-ref-bucket/-/ember-ref-bucket-4.1.0.tgz#2a52e72a395a14033d034c834fab648f26d74baa"
|
||||
integrity sha512-oEUU2mDtuYuMM039U9YEqrrOCVHH6rQfvbFOmh3WxOVEgubmLVyKEpGgU4P/6j0B/JxTqqTwM3ULTQyDto8dKg==
|
||||
dependencies:
|
||||
ember-cli-babel "^7.26.11"
|
||||
ember-cli-htmlbars "^6.0.1"
|
||||
ember-modifier "^3.2.7"
|
||||
|
||||
ember-render-helpers@^0.2.0:
|
||||
version "0.2.0"
|
||||
|
@ -15179,6 +15224,13 @@ rollup@^1.12.0:
|
|||
"@types/node" "*"
|
||||
acorn "^7.1.0"
|
||||
|
||||
rollup@^2.50.0:
|
||||
version "2.79.1"
|
||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.1.tgz#bedee8faef7c9f93a2647ac0108748f497f081c7"
|
||||
integrity sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||
|
||||
route-recognizer@^0.3.3:
|
||||
version "0.3.4"
|
||||
resolved "https://registry.yarnpkg.com/route-recognizer/-/route-recognizer-0.3.4.tgz#39ab1ffbce1c59e6d2bdca416f0932611e4f3ca3"
|
||||
|
|
Loading…
Reference in New Issue