ui: feature-flagged peering mvp (#13425)

* add peers route

* add peers to nav

* use regular app ui patterns peers template

* use empty state in peers UI

* mock `v1/peerings` request

* implement custom adapter/serializer for `peers`-model

* index request for peerings on peers route

* update peers list to show as proper list

* Use tailwind for easier styling

* Unique ids in peerings response mock-api

* Add styling peerings list

* Allow creating empty tooltip

To make it easier to iterate over a set of items where some items
should not display a tooltip and others should.

* Add tooltip Peerings:Badge

* Add undefined peering state badge

* Remove imported/exported services count peering

This won't be included in the initial version of the API response

* Implement Peerings::Search

* Make it possible to filter peerings by name

* Install ember-keyboard

For idiomatic handling of key-presses.

* Clear peering search input when pressing `Escape`

* use peers.index instead of peers for peerings listing

* Allow to include peered services in services-query

* update services mock to add peerName

* add Consul::Peer component

To surface peering information on a resource

* add PeerName as attribute to service model

* surface peering information in service list

* Add tooltip to Consul::Peer

* Make services searchable by peer-name

* Allow passing optional query-params to href-to

* Add peer query-param to dc.services.show

* Pass peer as query-param services listing

* support option peer route-param

* set peer-name undefined in services serializer when empty

* update peer route-param when navigating to peered service

* request sercice with peer-name if need be

* make sure to reset peer route-param when leaving service.show

* componentize services.peer-info

* surface peer info services.show

* make sure to reset peer route-param in main nav

* fix services breadcrumb services.intentions

we need to reset peer route-param here to not break the app

* surface peer when querying for it on service api call

* query for peer info service-instance api calls

* surface peer info service-instance.show

* Camelize peer attributes to match rest of app

* Refactor peers.index to reflect camelized attributes for peer

* Remove unused query-params services.show

* make logo href reset peer route-param

* Cleanup optional peer param query service-instance

* Use replace decorator instead of serializer for empty peerName

* make sure to only send peer info when correct qp is passed

* Always send qp for querying peers services request

* rename with-imports to with-peers

* Use css for peer-icon

* Refactor bucket-list component to surface peer-info

* Remove Consul::Peer component

This info is now displayed via the bucket-list component

* Fix bucket-list component to surface service again

* Update bucket-list docs to reflect peer-info addition

* Remove tailwind related styles

* Remove consul-tailwind package

We won't be using tailwind for now

* Fix typo badge scss

* Add with-import handling mock-api nodes

* Add peerName to node attributes

* include peers when querying nodes

* reflect api updates node list mock

* Create consul::node::peer-info component

* Surface peer-info in nodes list

* Mock peer response for node request

* Make it possible to add peer-name to node request

* Update peer route-param when linking to node

* Reset peers route-param when leaving nodes.show

We need to reset the route-param to not introduce a bug - otherwise
subsequent node show request would request with the old peer query-param

* Add sourcePeer intentions api mock

* add SourcePeer attr to intentions model

* Surface peering info on intentions list

* Request peered intentions differently intentions.edit

* Handle peer info in intentions/exact mock

* Surface peering info intention view

* Add randomized peer data topology mock

* Surface peer info topology view

* fix service/peer-info styling

We aren't using tailwind anymore - we need to create a custom scss file

* Update peerings api mocks

* Update peerings::badge with updated styling

* cleanup intentions/exact mock

* Create watcher component to declaratively register polling

* Poll peers in background when on peers route

* use existing colors for peering-badge

* Add test for requesting service with `with-peers`-query

* add imported/exported count to peers model

* update mock-api to surface exported/imported count on peers

* Show exported/imported peers count on peers list

* Use translations for service import/export UI peers

* Make sure to ask for nodes with peers

* Add match-url step for easier url testing of service urls

* Add test for peer-name on peered services

* Add test for service navigation peered service

* Implement feature-flag handling

* Enable peering feature in test and development

* Redirect peers to services.index when feature-flag is disabled

* Only query for peers when feature is enabled

* Only show peers in nav when feature is enabled

* Componentize peering service count detail

* Handle non-state Peerings::Badge

* Use Peerings::ServiceCount in peerings list

* Only send peer query for peered service-instances.

* Add step to visit url directly

* add test for accessing peered service directly

* Remove unused service import peers.index

* Only query for peer when peer provided node-adapter

* fix tests
This commit is contained in:
Michael Klein 2022-06-23 15:16:26 +02:00 committed by GitHub
parent e8f7a1f2c1
commit 272c878559
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
90 changed files with 1300 additions and 214 deletions

View File

@ -36,16 +36,40 @@ export default class IntentionAdapter extends Adapter {
// get the information we need from the id, which has been previously
// encoded
const [
SourcePartition,
SourceNS,
SourceName,
DestinationPartition,
DestinationNS,
DestinationName,
] = id.split(':').map(decodeURIComponent);
if (id.match(/^peer:/)) {
// id indicates we are dealing with a peered intention - handle this with
// different query-param for source - we need to prepend the peer
const [
peerIdentifier,
SourcePeer,
SourceNS,
SourceName,
DestinationPartition,
DestinationNS,
DestinationName,
] = id.split(':').map(decodeURIComponent);
return request`
return request`
GET /v1/connect/intentions/exact?${{
source: `${peerIdentifier}:${SourcePeer}/${SourceNS}/${SourceName}`,
destination: `${DestinationPartition}/${DestinationNS}/${DestinationName}`,
dc: dc,
}}
Cache-Control: no-store
${{ index }}
`;
} else {
const [
SourcePartition,
SourceNS,
SourceName,
DestinationPartition,
DestinationNS,
DestinationName,
] = id.split(':').map(decodeURIComponent);
return request`
GET /v1/connect/intentions/exact?${{
source: `${SourcePartition}/${SourceNS}/${SourceName}`,
destination: `${DestinationPartition}/${DestinationNS}/${DestinationName}`,
@ -55,6 +79,7 @@ export default class IntentionAdapter extends Adapter {
${{ index }}
`;
}
}
requestForCreateRecord(request, serialized, data) {

View File

@ -1,4 +1,5 @@
import Adapter from './application';
import { inject as service } from '@ember/service';
// TODO: Update to use this.formatDatacenter()
@ -10,6 +11,18 @@ import Adapter from './application';
// to the node.
export default class NodeAdapter extends Adapter {
@service features;
get peeringQuery() {
const query = {};
if (this.features.isEnabled('peering')) {
query['with-peers'] = true;
}
return query;
}
requestForQuery(request, { dc, ns, partition, index, id, uri }) {
return request`
GET /v1/internal/ui/nodes?${{ dc }}
@ -19,23 +32,32 @@ export default class NodeAdapter extends Adapter {
ns,
partition,
index,
...this.peeringQuery,
}}
`;
}
requestForQueryRecord(request, { dc, ns, partition, index, id, uri }) {
requestForQueryRecord(request, { dc, ns, partition, index, id, uri, peer }) {
if (typeof id === 'undefined') {
throw new Error('You must specify an id');
}
let options = {
ns,
partition,
index,
};
if (peer) {
options = {
...options,
peer,
};
}
return request`
GET /v1/internal/ui/node/${id}?${{ dc }}
X-Request-ID: ${uri}
${{
ns,
partition,
index,
}}
${options}
`;
}

View File

@ -0,0 +1,9 @@
import JSONAPIAdapter from '@ember-data/adapter/json-api';
export default class PeerAdapter extends JSONAPIAdapter {
namespace = 'v1';
pathForType(_modelName) {
return 'peerings';
}
}

View File

@ -2,20 +2,30 @@ import Adapter from './application';
// TODO: Update to use this.formatDatacenter()
export default class ServiceInstanceAdapter extends Adapter {
requestForQuery(request, { dc, ns, partition, index, id, uri }) {
requestForQuery(request, { dc, ns, partition, index, id, uri, peer }) {
if (typeof id === 'undefined') {
throw new Error('You must specify an id');
}
let options = {
ns,
partition,
index,
};
if (peer) {
options = {
...options,
peer,
};
}
return request`
GET /v1/health/service/${id}?${{ dc }}
X-Request-ID: ${uri}
X-Range: ${id}
${{
ns,
partition,
index,
}}
${options}
`;
}

View File

@ -1,6 +1,19 @@
import Adapter from './application';
import { inject as service } from '@ember/service';
export default class ServiceAdapter extends Adapter {
@service features;
get peeringQuery() {
const query = {};
if (this.features.isEnabled('peering')) {
query['with-peers'] = true;
}
return query;
}
requestForQuery(request, { dc, ns, partition, index, gateway, uri }) {
if (typeof gateway !== 'undefined') {
return request`
@ -23,6 +36,7 @@ export default class ServiceAdapter extends Adapter {
ns,
partition,
index,
...this.peeringQuery,
}}
`;
}

View File

@ -22,6 +22,20 @@ At the time of writing, this is not currently used across the entire UI
```hbs preview-template
<figure>
<figcaption>Show everything</figcaption>
<Consul::Bucket::List
@item={{hash
Namespace="different-nspace"
Partition="different-partition"
Service="service-name"
PeerName="billing-app"
}}
@nspace={{'nspace'}}
@partition={{'partition'}}
@service={{true}}
/>
</figure>
<figure>
<figcaption>Show everything without peer</figcaption>
<Consul::Bucket::List
@item={{hash
Namespace="different-nspace"
@ -46,6 +60,29 @@ At the time of writing, this is not currently used across the entire UI
/>
</figure>
<figure>
<figcaption>Show only peer-info</figcaption>
<Consul::Bucket::List
@item={{hash
Namespace="default"
PeerName="billing-app"
}}
@nspace={{'nspace'}}
/>
</figure>
<figure>
<figcaption>Don't surface anything - no relevant info to show</figcaption>
<Consul::Bucket::List
@item={{hash
Namespace="default"
Partition="default"
}}
@nspace={{'default'}}
@partition={{'default'}}
/>
</figure>
```
## Arguments

View File

@ -1,60 +1,12 @@
{{#if (and @partition (can 'use partitions'))}}
{{#if (not-eq @item.Partition @partition)}}
<dl class="consul-bucket-list">
<dt
class="partition"
{{tooltip}}
>
Admin Partition
{{#if this.itemsToDisplay.length}}
<dl class="consul-bucket-list">
{{#each this.itemsToDisplay as |item|}}
<dt class={{item.type}} {{tooltip}}>
{{item.label}}
</dt>
<dd>
{{@item.Partition}}
</dd>
<dt
class="nspace"
{{tooltip}}
>
Namespace
</dt>
<dd>
{{@item.Namespace}}
</dd>
{{#if (and @service @item.Service)}}
<dt
class="service"
>
Service
</dt>
<dd>
{{@item.Service}}
<dd data-test-bucket-item={{item.type}}>
{{item.item}}
</dd>
{{/if}}
</dl>
{{/if}}
{{else if (and @nspace (can 'use nspace'))}}
{{#if (not-eq @item.Namespace @nspace)}}
<dl class="consul-bucket-list">
<dt
class="nspace"
{{tooltip}}
>
Namespace
</dt>
<dd>
{{@item.Namespace}}
</dd>
{{#if (and @service @item.Service)}}
<dt
class="service"
>
Service
</dt>
<dd>
{{@item.Service}}
</dd>
{{/if}}
</dl>
{{/if}}
{{/each}}
</dl>
{{/if}}

View File

@ -0,0 +1,88 @@
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
export default class ConsulBucketList extends Component {
@service abilities;
get itemsToDisplay() {
const { item, partition, nspace } = this.args;
const { abilities } = this;
let items = [];
if (partition && abilities.can('use partitions')) {
if (item.Partition !== partition) {
this._addPeer(items);
this._addPartition(items);
this._addNamespace(items);
this._addService(items);
} else {
this._addPeerInfo(items);
}
} else if (nspace && abilities.can('use nspace')) {
if (item.Namespace !== nspace) {
this._addPeerInfo(items);
this._addService(items);
} else {
this._addPeerInfo(items);
}
} else {
this._addPeerInfo(items);
}
return items;
}
_addPeerInfo(items) {
const { item } = this.args;
if (item.PeerName) {
this._addPeer(items);
this._addNamespace(items);
}
}
_addPartition(items) {
const { item } = this.args;
items.push({
type: 'partition',
label: 'Admin Partition',
item: item.Partition,
});
}
_addNamespace(items) {
const { item } = this.args;
items.push({
type: 'nspace',
label: 'Namespace',
item: item.Namespace,
});
}
_addService(items) {
const { service, item } = this.args;
if (service && item.Service) {
items.push({
type: 'service',
label: 'Service',
item: item.Service,
});
}
}
_addPeer(items) {
const { item } = this.args;
if (item?.PeerName) {
items.push({
type: 'peer',
label: 'Peer',
item: item.PeerName,
});
}
}
}

View File

@ -11,6 +11,9 @@
.service {
@extend %visually-hidden;
}
.peer::before {
@extend %with-network-alt-mask, %as-pseudo;
}
.service + dd {
font-weight: var(--typo-weight-semibold);
}

View File

@ -26,10 +26,37 @@ as |item index|>
{{/if}}
{{#if (or (can 'use nspaces') (can 'use partitions'))}}
{{! TODO: slugify }}
<em>
<em class="consul-intention-list-table__meta-info">
{{#if item.SourcePeer}}
<span class="consul-intention-list-table__meta-info__peer">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" {{tooltip "Peer" }} class="-mr-1">
<path
d="M16 8C16 7.80109 15.921 7.61032 15.7803 7.46967L12.2803 3.96967C11.9874 3.67678 11.5126 3.67678 11.2197 3.96967C10.9268 4.26256 10.9268 4.73744 11.2197 5.03033L14.1893 8L11.2197 10.9697C10.9268 11.2626 10.9268 11.7374 11.2197 12.0303C11.5126 12.3232 11.9874 12.3232 12.2803 12.0303L15.7803 8.53033C15.921 8.38968 16 8.19891 16 8Z"
fill="#77838A" />
<path
d="M0.21967 8.53033C-0.0732233 8.23744 -0.0732233 7.76256 0.21967 7.46967L3.71967 3.96967C4.01256 3.67678 4.48744 3.67678 4.78033 3.96967C5.07322 4.26256 5.07322 4.73744 4.78033 5.03033L1.81066 8L4.78033 10.9697C5.07322 11.2626 5.07322 11.7374 4.78033 12.0303C4.48744 12.3232 4.01256 12.3232 3.71967 12.0303L0.21967 8.53033Z"
fill="#77838A" />
<path
d="M5 7C4.44772 7 4 7.44772 4 8C4 8.55229 4.44772 9 5 9H5.01C5.56228 9 6.01 8.55229 6.01 8C6.01 7.44772 5.56228 7 5.01 7H5Z"
fill="#77838A" />
<path
d="M7 8C7 7.44772 7.44772 7 8 7H8.01C8.56228 7 9.01 7.44772 9.01 8C9.01 8.55229 8.56228 9 8.01 9H8C7.44772 9 7 8.55229 7 8Z"
fill="#77838A" />
<path
d="M11 7C10.4477 7 10 7.44772 10 8C10 8.55229 10.4477 9 11 9H11.01C11.5623 9 12.01 8.55229 12.01 8C12.01 7.44772 11.5623 7 11.01 7H11Z"
fill="#77838A" />
</svg>
<span>{{item.SourcePeer}}</span>
</span>
{{else}}
<span
class={{concat 'partition-' (or item.SourcePartition 'default')}}
>
{{or item.SourcePartition 'default'}}
</span>
{{/if}}
/
<span
class={{concat 'partition-' (or item.SourcePartition 'default')}}
>{{or item.SourcePartition 'default'}}</span> / <span
class={{concat 'nspace-' (or item.SourceNS 'default')}}
>{{or item.SourceNS 'default'}}</span>
</em>

View File

@ -0,0 +1,8 @@
.consul-intention-list-table__meta-info {
display: flex;
.consul-intention-list-table__meta-info__peer {
display: flex;
align-items: center;
}
}

View File

@ -25,6 +25,7 @@
Namespace=item.SourceNS
Partition=item.SourcePartition
Service=item.SourceName
PeerName=item.SourcePeer
}}
@nspace="-"
@partition="-"

View File

@ -21,14 +21,15 @@ as |item index|>
</Tooltip>
</dd>
</dl>
<a data-test-node href={{href-to "dc.nodes.show" item.Node}}>
<a data-test-node href={{href-to "dc.nodes.show" item.Node params=(hash peer=item.PeerName)}}>
{{item.Node}}
</a>
</BlockSlot>
<BlockSlot @name="details">
{{#if (eq item.Address @leader.Address)}}
<span class="leader" data-test-leader={{@leader.Address}}>Leader</span>
{{/if}}
<Consul::Node::PeerInfo @item={{item}} />
{{#if (eq item.Address @leader.Address)}}
<span class="leader" data-test-leader={{@leader.Address}}>Leader</span>
{{/if}}
<span>
{{format-number item.MeshServiceInstances.length}} {{pluralize item.MeshServiceInstances.length 'Service' without-count=true}}
</span>

View File

@ -0,0 +1,22 @@
{{#if @item.PeerName}}
<span class="consul-node-peer-info">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" {{tooltip "Peer" }}>
<path
d="M16 8C16 7.80109 15.921 7.61032 15.7803 7.46967L12.2803 3.96967C11.9874 3.67678 11.5126 3.67678 11.2197 3.96967C10.9268 4.26256 10.9268 4.73744 11.2197 5.03033L14.1893 8L11.2197 10.9697C10.9268 11.2626 10.9268 11.7374 11.2197 12.0303C11.5126 12.3232 11.9874 12.3232 12.2803 12.0303L15.7803 8.53033C15.921 8.38968 16 8.19891 16 8Z"
fill="#77838A" />
<path
d="M0.21967 8.53033C-0.0732233 8.23744 -0.0732233 7.76256 0.21967 7.46967L3.71967 3.96967C4.01256 3.67678 4.48744 3.67678 4.78033 3.96967C5.07322 4.26256 5.07322 4.73744 4.78033 5.03033L1.81066 8L4.78033 10.9697C5.07322 11.2626 5.07322 11.7374 4.78033 12.0303C4.48744 12.3232 4.01256 12.3232 3.71967 12.0303L0.21967 8.53033Z"
fill="#77838A" />
<path
d="M5 7C4.44772 7 4 7.44772 4 8C4 8.55229 4.44772 9 5 9H5.01C5.56228 9 6.01 8.55229 6.01 8C6.01 7.44772 5.56228 7 5.01 7H5Z"
fill="#77838A" />
<path
d="M7 8C7 7.44772 7.44772 7 8 7H8.01C8.56228 7 9.01 7.44772 9.01 8C9.01 8.55229 8.56228 9 8.01 9H8C7.44772 9 7 8.55229 7 8Z"
fill="#77838A" />
<path
d="M11 7C10.4477 7 10 7.44772 10 8C10 8.55229 10.4477 9 11 9H11.01C11.5623 9 12.01 8.55229 12.01 8C12.01 7.44772 11.5623 7 11.01 7H11Z"
fill="#77838A" />
</svg>
<span class="consul-node-peer-info__name">{{@item.PeerName}}</span>
</span>
{{/if}}

View File

@ -0,0 +1,8 @@
.consul-node-peer-info {
display: flex;
align-items: center;
.consul-node-peer-info__name {
margin-left: 4px;
}
}

View File

@ -32,8 +32,11 @@
(hash
partition=item.Partition
nspace=item.Namespace
peer=item.PeerName
)
(hash
peer=item.PeerName
)
(hash)
)
}}
>

View File

@ -0,0 +1,23 @@
{{#if @service.PeerName}}
<div class="consul-service-peer-info" data-test-service-peer-info>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" {{tooltip "Peer" }}>
<path
d="M16 8C16 7.80109 15.921 7.61032 15.7803 7.46967L12.2803 3.96967C11.9874 3.67678 11.5126 3.67678 11.2197 3.96967C10.9268 4.26256 10.9268 4.73744 11.2197 5.03033L14.1893 8L11.2197 10.9697C10.9268 11.2626 10.9268 11.7374 11.2197 12.0303C11.5126 12.3232 11.9874 12.3232 12.2803 12.0303L15.7803 8.53033C15.921 8.38968 16 8.19891 16 8Z"
fill="#77838A" />
<path
d="M0.21967 8.53033C-0.0732233 8.23744 -0.0732233 7.76256 0.21967 7.46967L3.71967 3.96967C4.01256 3.67678 4.48744 3.67678 4.78033 3.96967C5.07322 4.26256 5.07322 4.73744 4.78033 5.03033L1.81066 8L4.78033 10.9697C5.07322 11.2626 5.07322 11.7374 4.78033 12.0303C4.48744 12.3232 4.01256 12.3232 3.71967 12.0303L0.21967 8.53033Z"
fill="#77838A" />
<path
d="M5 7C4.44772 7 4 7.44772 4 8C4 8.55229 4.44772 9 5 9H5.01C5.56228 9 6.01 8.55229 6.01 8C6.01 7.44772 5.56228 7 5.01 7H5Z"
fill="#77838A" />
<path
d="M7 8C7 7.44772 7.44772 7 8 7H8.01C8.56228 7 9.01 7.44772 9.01 8C9.01 8.55229 8.56228 9 8.01 9H8C7.44772 9 7 8.55229 7 8Z"
fill="#77838A" />
<path
d="M11 7C10.4477 7 10 7.44772 10 8C10 8.55229 10.4477 9 11 9H11.01C11.5623 9 12.01 8.55229 12.01 8C12.01 7.44772 11.5623 7 11.01 7H11Z"
fill="#77838A" />
</svg>
<span class="consul-service-peer-info__description">Imported from <span data-test-peer-name>{{@service.PeerName}}</span></span>
</div>
{{/if}}

View File

@ -0,0 +1,13 @@
.consul-service-peer-info {
background: rgb(var(--gray-100));
color: rgb(var(--gray-600));
padding: 0px 8px;
border-radius: 2px;
display: flex;
align-items: center;
.consul-service-peer-info__description {
margin-left: 4px;
}
}

View File

@ -81,7 +81,7 @@
<:home-nav>
<a
href={{href-to 'index'}}
href={{href-to 'index' params=(hash peer=undefined)}}
><Consul::Logo /></a>
</:home-nav>
@ -115,7 +115,7 @@
}}
>
<Action
@href={{href-to 'dc.show' @dc.Name}}
@href={{href-to 'dc.show' @dc.Name params=(hash peer=undefined)}}
>
Overview
</Action>
@ -123,22 +123,22 @@
{{/if}}
{{#if (can "read services")}}
<li data-test-main-nav-services class={{if (is-href 'dc.services' @dc.Name) 'is-active'}}>
<a href={{href-to 'dc.services' @dc.Name}}>Services</a>
<a href={{href-to 'dc.services' @dc.Name params=(hash peer=undefined)}}>Services</a>
</li>
{{/if}}
{{#if (can "read nodes")}}
<li data-test-main-nav-nodes class={{if (is-href 'dc.nodes' @dc.Name) 'is-active'}}>
<a href={{href-to 'dc.nodes' @dc.Name}}>Nodes</a>
<a href={{href-to 'dc.nodes' @dc.Name params=(hash peer=undefined)}}>Nodes</a>
</li>
{{/if}}
{{#if (can "read kv")}}
<li data-test-main-nav-kvs class={{if (is-href 'dc.kv' @dc.Name) 'is-active'}}>
<a href={{href-to 'dc.kv' @dc.Name}}>Key/Value</a>
<a href={{href-to 'dc.kv' @dc.Name params=(hash peer=undefined)}}>Key/Value</a>
</li>
{{/if}}
{{#if (can "read intentions")}}
<li data-test-main-nav-intentions class={{if (is-href 'dc.intentions' @dc.Name) 'is-active'}}>
<a href={{href-to 'dc.intentions' @dc.Name}}>Intentions</a>
<a href={{href-to 'dc.intentions' @dc.Name params=(hash peer=undefined)}}>Intentions</a>
</li>
{{/if}}
<Consul::Acl::Selector
@ -146,6 +146,14 @@
@partition={{@partition}}
@nspace={{@nspace}}
/>
{{#if (feature-flag "peering")}}
<li role="separator">
Organization
</li>
<li data-test-main-nav-peers class={{if (is-href 'dc.peers' @dc.Name) 'is-active' }}>
<a href={{href-to 'dc.peers' @dc.Name params=(hash peer=undefined)}}>Peers</a>
</li>
{{/if}}
</ul>
</:main-nav>

View File

@ -0,0 +1,86 @@
{{#if (or (eq @state "PENDING") (eq @state "ESTABLISHING"))}}
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
...attributes
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M8 1.5C6.14798 1.5 4.47788 2.27358 3.29301 3.51732C3.0073 3.81723 2.53256 3.82874 2.23266 3.54303C1.93275 3.25732 1.92125 2.78258 2.20696 2.48268C3.66316 0.954124 5.72078 0 8 0C10.2792 0 12.3368 0.954124 13.793 2.48268C14.0788 2.78258 14.0672 3.25732 13.7673 3.54303C13.4674 3.82874 12.9927 3.81723 12.707 3.51732C11.5221 2.27358 9.85202 1.5 8 1.5ZM1.23586 5.27899C1.63407 5.39303 1.86443 5.80828 1.75039 6.20649C1.58749 6.7753 1.5 7.3768 1.5 8C1.5 11.0649 3.62199 13.636 6.47785 14.321C6.88064 14.4176 7.12885 14.8224 7.03225 15.2252C6.93565 15.628 6.53081 15.8762 6.12802 15.7796C2.61312 14.9366 0 11.7744 0 8C0 7.23572 0.107387 6.49527 0.30836 5.79351C0.422401 5.39531 0.837659 5.16494 1.23586 5.27899ZM14.7641 5.27899C15.1623 5.16494 15.5776 5.39531 15.6916 5.79351C15.8926 6.49527 16 7.23572 16 8C16 11.7744 13.3869 14.9366 9.87199 15.7796C9.4692 15.8762 9.06436 15.628 8.96775 15.2252C8.87115 14.8224 9.11936 14.4176 9.52215 14.321C12.378 13.636 14.5 11.0649 14.5 8C14.5 7.3768 14.4125 6.7753 14.2496 6.20649C14.1356 5.80828 14.3659 5.39303 14.7641 5.27899Z"
fill="#3B3D45"
/>
<path
opacity="0.2"
fill-rule="evenodd"
clip-rule="evenodd"
d="M8 4.5C6.067 4.5 4.5 6.067 4.5 8C4.5 9.933 6.067 11.5 8 11.5C9.933 11.5 11.5 9.933 11.5 8C11.5 6.067 9.933 4.5 8 4.5ZM3 8C3 5.23858 5.23858 3 8 3C10.7614 3 13 5.23858 13 8C13 10.7614 10.7614 13 8 13C5.23858 13 3 10.7614 3 8Z"
fill="#000001"
/>
</svg>
{{/if}}
{{#if (eq @state "ACTIVE")}}
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
...attributes
>
<path
d="M14.7803 4.28033C15.0732 3.98744 15.0732 3.51256 14.7803 3.21967C14.4874 2.92678 14.0126 2.92678 13.7197 3.21967L5.75 11.1893L2.28033 7.71967C1.98744 7.42678 1.51256 7.42678 1.21967 7.71967C0.926777 8.01256 0.926777 8.48744 1.21967 8.78033L5.21967 12.7803C5.51256 13.0732 5.98744 13.0732 6.28033 12.7803L14.7803 4.28033Z"
fill="#00781E"
/>
</svg>
{{/if}}
{{#if (eq @state "FAILING")}}
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
...attributes
>
<path
d="M12.7803 4.28033C13.0732 3.98744 13.0732 3.51256 12.7803 3.21967C12.4874 2.92678 12.0126 2.92678 11.7197 3.21967L8 6.93934L4.28033 3.21967C3.98744 2.92678 3.51256 2.92678 3.21967 3.21967C2.92678 3.51256 2.92678 3.98744 3.21967 4.28033L6.93934 8L3.21967 11.7197C2.92678 12.0126 2.92678 12.4874 3.21967 12.7803C3.51256 13.0732 3.98744 13.0732 4.28033 12.7803L8 9.06066L11.7197 12.7803C12.0126 13.0732 12.4874 13.0732 12.7803 12.7803C13.0732 12.4874 13.0732 12.0126 12.7803 11.7197L9.06066 8L12.7803 4.28033Z"
fill="#C00005"
/>
</svg>
{{/if}}
{{#if (eq @state "TERMINATED")}}
<svg
width="16"
height="17"
viewBox="0 0 16 17"
fill="none"
xmlns="http://www.w3.org/2000/svg"
...attributes
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M3.13889 2.55566C2.78604 2.55566 2.5 2.8417 2.5 3.19455V12.9168C2.5 13.2696 2.78604 13.5557 3.13889 13.5557H12.8611C13.214 13.5557 13.5 13.2696 13.5 12.9168V3.19455C13.5 2.8417 13.214 2.55566 12.8611 2.55566H3.13889ZM1 3.19455C1 2.01328 1.95761 1.05566 3.13889 1.05566H12.8611C14.0424 1.05566 15 2.01328 15 3.19455V12.9168C15 14.0981 14.0424 15.0557 12.8611 15.0557H3.13889C1.95761 15.0557 1 14.0981 1 12.9168V3.19455ZM4.71967 4.77533C5.01256 4.48244 5.48744 4.48244 5.78033 4.77533L8 6.995L10.2197 4.77533C10.5126 4.48244 10.9874 4.48244 11.2803 4.77533C11.5732 5.06823 11.5732 5.5431 11.2803 5.83599L9.06066 8.05566L11.2803 10.2753C11.5732 10.5682 11.5732 11.0431 11.2803 11.336C10.9874 11.6289 10.5126 11.6289 10.2197 11.336L8 9.11632L5.78033 11.336C5.48744 11.6289 5.01256 11.6289 4.71967 11.336C4.42678 11.0431 4.42678 10.5682 4.71967 10.2753L6.93934 8.05566L4.71967 5.83599C4.42678 5.5431 4.42678 5.06823 4.71967 4.77533Z"
fill="#3B3D45"
/>
</svg>
{{/if}}
{{#if (eq @state "UNDEFINED")}}
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" ...attributes>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.1969 4.52275C7.83582 4.45975 7.46375 4.52849 7.14594 4.7185C6.82781 4.9087 6.58324 5.20915 6.45878 5.56907C6.32341 5.96054 5.89632 6.16815 5.50485 6.03278C5.11338 5.89741 4.90577 5.47032 5.04114 5.07886C5.27962 4.3892 5.75141 3.80461 6.37621 3.43106C7.00132 3.05732 7.73786 2.91999 8.45475 3.04508C9.17148 3.17015 9.81887 3.54878 10.2837 4.11048C10.7481 4.67171 11.0009 5.37994 11 6.10959C10.9999 6.59724 10.9078 7.01534 10.7254 7.37628C10.5432 7.73694 10.2936 7.9952 10.0464 8.19341C9.85239 8.34899 9.63602 8.48431 9.46464 8.59149C9.431 8.61253 9.39909 8.63248 9.36942 8.65129C9.16778 8.77916 9.02667 8.87887 8.91689 8.99055C8.81461 9.0946 8.77388 9.18682 8.75706 9.23816C8.74978 9.26038 8.74659 9.27628 8.74537 9.28347C8.72786 9.68216 8.3991 10 7.9961 10C7.58189 10 7.2461 9.66422 7.2461 9.25C7.24626 9.08689 7.28103 8.92552 7.33163 8.77109C7.41129 8.52797 7.56353 8.22758 7.84718 7.93902C8.0857 7.69637 8.35223 7.52016 8.56613 7.38452C8.61117 7.35596 8.65343 7.32942 8.69337 7.30434C8.8616 7.1987 8.98859 7.11896 9.10803 7.02318C9.24074 6.91676 9.32751 6.81683 9.38666 6.69978C9.44562 6.5831 9.49996 6.4041 9.49996 6.10918L9.49996 6.10808C9.50052 5.72536 9.36781 5.35654 9.12803 5.06677C8.88848 4.77728 8.55813 4.58578 8.1969 4.52275Z" fill="#3B3D45"/>
<path d="M8 11C7.44772 11 7 11.4477 7 12C7 12.5523 7.44772 13 8 13H8.00667C8.55895 13 9.00667 12.5523 9.00667 12C9.00667 11.4477 8.55895 11 8.00667 11H8Z" fill="#3B3D45"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 8C0 3.58172 3.58172 0 8 0C12.4183 0 16 3.58172 16 8C16 12.4183 12.4183 16 8 16C3.58172 16 0 12.4183 0 8ZM8 1.5C4.41015 1.5 1.5 4.41015 1.5 8C1.5 11.5899 4.41015 14.5 8 14.5C11.5899 14.5 14.5 11.5899 14.5 8C14.5 4.41015 11.5899 1.5 8 1.5Z" fill="#3B3D45"/>
</svg>
{{/if}}
{{#if (eq @state "DELETING")}}
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.2" fill-rule="evenodd" clip-rule="evenodd" d="M8 1.5C4.41015 1.5 1.5 4.41015 1.5 8C1.5 11.5899 4.41015 14.5 8 14.5C11.5899 14.5 14.5 11.5899 14.5 8C14.5 4.41015 11.5899 1.5 8 1.5ZM0 8C0 3.58172 3.58172 0 8 0C12.4183 0 16 3.58172 16 8C16 12.4183 12.4183 16 8 16C3.58172 16 0 12.4183 0 8Z" fill="#000001"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.25 0.75C7.25 0.335786 7.58579 0 8 0C12.4183 0 16 3.58172 16 8C16 8.41421 15.6642 8.75 15.25 8.75C14.8358 8.75 14.5 8.41421 14.5 8C14.5 4.41015 11.5899 1.5 8 1.5C7.58579 1.5 7.25 1.16421 7.25 0.75Z" fill="#3B3D45"/>
</svg>
{{/if}}

View File

@ -0,0 +1,6 @@
{{#if @peering.State}}
<div class="peerings-badge {{lowercase @peering.State}}" {{tooltip this.tooltip options=(hash hideTooltip=(not this.tooltip))}}>
<Peerings::Badge::Icon @state="{{@peering.State}}" />
<span class="peerings-badge__text">{{capitalize (lowercase @peering.State)}}</span>
</div>
{{/if}}

View File

@ -0,0 +1,37 @@
import Component from '@glimmer/component';
const BADGE_LOOKUP = {
ACTIVE: {
tooltip: 'This peer connection is currently active.',
},
PENDING: {
tooltip: 'This peering connection has not been established yet.',
},
ESTABLISHING: {
tooltip: 'This peering connection is in the process of being established.',
},
FAILING: {
tooltip:
'This peering connection has some intermittent errors (usually network related). It will continue to retry. ',
},
DELETING: {
tooltip: 'This peer is in the process of being deleted.',
},
TERMINATED: {
tooltip: 'Someone in the other peer may have deleted this peering connection.',
},
UNDEFINED: {},
};
export default class PeeingsBadge extends Component {
get styles() {
const {
peering: { State },
} = this.args;
return BADGE_LOOKUP[State];
}
get tooltip() {
return this.styles.tooltip;
}
}

View File

@ -0,0 +1,42 @@
.peerings-badge {
display: flex;
align-items: center;
justify-content: center;
padding: 2px 8px;
border-radius: 5px;
gap: 4px;
&.active {
background: rgb(var(--tone-green-050));
color: rgb(var(--tone-green-600));
}
&.pending {
background: rgb(var(--tone-strawberry-050));
color: rgb(var(--tone-strawberry-500));
}
&.establishing {
background: rgb(var(--tone-blue-050));
color: rgb(var(--tone-blue-500));
}
&.failing {
background: rgb(var(--tone-red-050));
color: rgb(var(--tone-red-500));
}
&.deleting {
background: rgb(var(--tone-yellow-050));
color: rgb(var(--tone-yellow-800));
}
&.terminated {
background: rgb(var(--tone-gray-150));
color: rgb(var(--tone-gray-800));
}
&.undefined {
background: rgb(var(--tone-gray-150));
color: rgb(var(--tone-gray-800));
}
.peerings-badge__text {
font-weight: 500;
font-size: 13px;
}
}

View File

@ -0,0 +1,28 @@
<div class="peerings-search">
<div class="peerings-search__input">
<label for="peer-search" class="peerings-search__input__label">
<svg width="16" height="16" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M8.33334 7.33334H7.80667L7.62 7.15334C8.29592 6.36935 8.6674 5.36846 8.66667 4.33334C8.66667 3.47628 8.41252 2.63848 7.93637 1.92586C7.46022 1.21325 6.78344 0.657837 5.99163 0.329857C5.19982 0.00187757 4.32853 -0.0839369 3.48794 0.0832657C2.64736 0.250468 1.87523 0.663178 1.26921 1.26921C0.663178 1.87523 0.250468 2.64736 0.0832657 3.48794C-0.0839369 4.32853 0.00187757 5.19982 0.329857 5.99163C0.657837 6.78344 1.21325 7.46022 1.92586 7.93637C2.63848 8.41252 3.47628 8.66667 4.33334 8.66667C5.40667 8.66667 6.39333 8.27334 7.15334 7.62L7.33334 7.80667V8.33334L10.6667 11.66L11.66 10.6667L8.33334 7.33334ZM4.33334 7.33334C2.67334 7.33334 1.33334 5.99334 1.33334 4.33334C1.33334 2.67334 2.67334 1.33334 4.33334 1.33334C5.99334 1.33334 7.33334 2.67334 7.33334 4.33334C7.33334 5.99334 5.99334 7.33334 4.33334 7.33334Z"
fill="#8E96A3" />
</svg>
</label>
<input id="peer-search" placeholder="Search" class="peerings-search__input__input" value="{{@search}}" {{on "input"
(pick "target.value" @onSearch)}} {{on-key "Escape" (fn @onSearch "" )}} />
{{#if @search}}
<button
type="button"
class="peerings-search__input__clear-button"
{{on "click" (fn @onSearch "" )}}
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M12.7803 4.28033C13.0732 3.98744 13.0732 3.51256 12.7803 3.21967C12.4874 2.92678 12.0126 2.92678 11.7197 3.21967L8 6.93934L4.28033 3.21967C3.98744 2.92678 3.51256 2.92678 3.21967 3.21967C2.92678 3.51256 2.92678 3.98744 3.21967 4.28033L6.93934 8L3.21967 11.7197C2.92678 12.0126 2.92678 12.4874 3.21967 12.7803C3.51256 13.0732 3.98744 13.0732 4.28033 12.7803L8 9.06066L11.7197 12.7803C12.0126 13.0732 12.4874 13.0732 12.7803 12.7803C13.0732 12.4874 13.0732 12.0126 12.7803 11.7197L9.06066 8L12.7803 4.28033Z"
fill="currentColor"
/>
</svg>
</button>
{{/if}}
</div>
</div>

View File

@ -0,0 +1,36 @@
.peerings-search {
display: flex;
padding: 4px 8px;
background: rgb(var(--gray-010));
.peerings-search__input {
position: relative;
border-width: 1px;
border-radius: 0.125rem;
}
.peerings-search__input__label {
position: absolute;
display: flex;
justify-content: center;
align-items: center;
width: 32px;
height: 100%;
}
.peerings-search__input__input {
padding: 8px 32px;
border-radius: 2px;
border: 1px solid rgb(var(--gray-300));
}
.peerings-search__input__clear-button {
position: absolute;
right: 4px;
top: 0px;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
}

View File

@ -0,0 +1,5 @@
{{#if this.count}}
<div {{tooltip this.tooltipText}}>
{{this.text}}
</div>
{{/if}}

View File

@ -0,0 +1,29 @@
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
export default class PeeringsServiceCount extends Component {
@service intl;
get count() {
const { peering, kind } = this.args;
return peering[`${kind.capitalize()}ServiceCount`];
}
get text() {
const { kind } = this.args;
const { intl, count } = this;
return intl.t(`routes.dc.peers.index.detail.${kind}.count`, { count });
}
get tooltipText() {
const {
kind,
peering: { name },
} = this.args;
const { intl } = this;
return intl.t(`routes.dc.peers.index.detail.${kind}.tooltip`, { name });
}
}

View File

@ -92,7 +92,7 @@
</div>
{{#if (gt this.upstreams.length 0)}}
<div id="upstream-column">
{{#each-in (group-by "Datacenter" this.upstreams) as |dc upstreams|}}
{{#each-in (group-by "PeerOrDatacenter" this.upstreams) as |dc upstreams|}}
<div
id="upstream-container"
{{did-insert this.setHeight 'upstream-lines'}}

View File

@ -99,6 +99,9 @@ export default class TopologyMetrics extends Component {
get upstreams() {
const upstreams = get(this.args.topology, 'Upstreams') || [];
upstreams.forEach(u => {
u.PeerOrDatacenter = u.PeerName || u.Datacenter;
});
const items = [...upstreams];
const defaultACLPolicy = get(this.args.dc, 'DefaultACLPolicy');
const wildcardIntention = get(this.args.topology, 'wildcardIntention');
@ -108,18 +111,21 @@ export default class TopologyMetrics extends Component {
items.push({
Name: 'Upstreams unknown.',
Datacenter: '',
PeerOrDatacenter: '',
Namespace: '',
});
} else if (defaultACLPolicy === 'allow' || wildcardIntention) {
items.push({
Name: '* (All Services)',
Datacenter: '',
PeerOrDatacenter: '',
Namespace: '',
});
} else if (upstreams.length === 0) {
items.push({
Name: 'No upstreams.',
Datacenter: '',
PeerOrDatacenter: '',
Namespace: '',
});
}

View File

@ -0,0 +1,6 @@
{{yield (hash
fns=(hash
start=this.start
stop=this.stop
)
)}}

View File

@ -0,0 +1,61 @@
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { later, cancel as _cancel } from '@ember/runloop';
import { inject as service } from '@ember/service';
const DEFAULT_TIMEOUT = 10000;
const TESTING_TIMEOUT = 300;
export default class Watcher extends Component {
@service env;
@tracked _isPolling = false;
@tracked cancel = null;
get timeout() {
if (this.isTesting) {
return TESTING_TIMEOUT;
} else {
return this.args.timeout || DEFAULT_TIMEOUT;
}
}
get isTesting() {
return this.env.var('environment') === 'testing';
}
get isPolling() {
const { isTesting, _isPolling: isPolling } = this;
return !isTesting && isPolling;
}
@action start() {
this._isPolling = true;
this.watchTask();
}
@action stop() {
this._isPolling = false;
_cancel(this.cancel);
}
watchTask() {
const cancel = later(
this,
() => {
this.args.watch?.();
if (this.isPolling) {
this.watchTask();
}
},
this.timeout
);
this.cancel = cancel;
}
}

View File

@ -0,0 +1,29 @@
import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
export default class PeersController extends Controller {
queryParams = ['filter'];
@tracked filter = '';
get peers() {
return this.model.peers;
}
get filteredPeers() {
const { peers, filter } = this;
if (filter) {
const filterRegex = new RegExp(`${filter}`, 'gi');
return peers.filter(peer => peer.Name.match(filterRegex));
}
return peers;
}
@action handleSearchChanged(newSearchTerm) {
this.filter = newSearchTerm;
}
}

View File

@ -0,0 +1,10 @@
import Helper from '@ember/component/helper';
import { inject as service } from '@ember/service';
export default class extends Helper {
@service features;
compute([feature]) {
return this.features.isEnabled(feature);
}
}

View File

@ -8,6 +8,8 @@ if (env('CONSUL_NSPACES_ENABLED')) {
OPTIONAL.nspace = /^~([a-zA-Z0-9]([a-zA-Z0-9-]{0,62}[a-zA-Z0-9])?)$/;
}
OPTIONAL.peer = /^:([a-zA-Z0-9]([a-zA-Z0-9-]{0,62}[a-zA-Z0-9])?)$/;
const trailingSlashRe = /\/$/;
// see below re: ember double slashes
@ -165,7 +167,7 @@ export default class FSMWithOptionalLocation {
optionalParams() {
let optional = this.optional || {};
return ['partition', 'nspace'].reduce((prev, item) => {
return ['partition', 'nspace', 'peer'].reduce((prev, item) => {
let value = '';
if (typeof optional[item] !== 'undefined') {
value = optional[item].match;
@ -196,6 +198,10 @@ export default class FSMWithOptionalLocation {
if (typeof hash.partition !== 'undefined') {
hash.partition = `_${hash.partition}`;
}
if (typeof hash.peer !== 'undefined') {
hash.peer = `:${hash.peer}`;
}
if (typeof this.router === 'undefined') {
this.router = this.container.lookup('router:main');
}

View File

@ -13,6 +13,7 @@ export default class Intention extends Model {
@attr('string') Datacenter;
@attr('string') Description;
@attr('string') SourcePeer;
@attr('string', { defaultValue: () => '*' }) SourceName;
@attr('string', { defaultValue: () => '*' }) DestinationName;
@attr('string', { defaultValue: () => 'default' }) SourceNS;

View File

@ -11,6 +11,7 @@ export default class Node extends Model {
@attr('string') ID;
@attr('string') Datacenter;
@attr('string') PeerName;
@attr('string') Partition;
@attr('string') Address;
@attr('string') Node;

View File

@ -0,0 +1,10 @@
import Model, { attr } from '@ember-data/model';
export default class Peer extends Model {
@attr('string') Name;
@attr('string') State;
@attr('string') CreateIndex;
@attr('string') ModifyIndex;
@attr('number') ImportedServiceCount;
@attr('number') ExportedServiceCount;
}

View File

@ -2,7 +2,7 @@ import Model, { attr } from '@ember-data/model';
import { computed } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { fragment } from 'ember-data-model-fragments/attributes';
import { nullValue } from 'consul-ui/decorators/replace';
import replace, { nullValue } from 'consul-ui/decorators/replace';
export const PRIMARY_KEY = 'uid';
export const SLUG_KEY = 'Name';
@ -36,6 +36,7 @@ export default class Service extends Model {
@attr('string') Namespace;
@attr('string') Partition;
@attr('string') Kind;
@replace('', undefined) @attr('string') PeerName;
@attr('number') ChecksPassing;
@attr('number') ChecksCritical;
@attr('number') ChecksWarning;

View File

@ -10,64 +10,66 @@ import tippy, { followCursor } from 'tippy.js';
export default modifier(($element, [content], hash = {}) => {
const options = hash.options || {};
let $anchor = $element;
if (!options.hideTooltip) {
let $anchor = $element;
// make it easy to specify the modified element as the actual tooltip
if (typeof options.triggerTarget === 'string') {
const $el = $anchor;
switch (options.triggerTarget) {
case 'parentNode':
$anchor = $anchor.parentNode;
break;
default:
$anchor = $anchor.querySelectorAll(options.triggerTarget);
// make it easy to specify the modified element as the actual tooltip
if (typeof options.triggerTarget === 'string') {
const $el = $anchor;
switch (options.triggerTarget) {
case 'parentNode':
$anchor = $anchor.parentNode;
break;
default:
$anchor = $anchor.querySelectorAll(options.triggerTarget);
}
content = $anchor.cloneNode(true);
$el.remove();
hash.options.triggerTarget = undefined;
}
content = $anchor.cloneNode(true);
$el.remove();
hash.options.triggerTarget = undefined;
}
// {{tooltip}} will just use the HTML content
if (typeof content === 'undefined') {
content = $anchor.innerHTML;
$anchor.innerHTML = '';
}
let interval;
if (options.trigger === 'manual') {
// if we are manually triggering, a out delay means only show for the
// amount of time specified by the delay
const delay = options.delay || [];
if (typeof delay[1] !== 'undefined') {
hash.options.onShown = tooltip => {
clearInterval(interval);
interval = setTimeout(() => {
tooltip.hide();
}, delay[1]);
};
// {{tooltip}} will just use the HTML content
if (typeof content === 'undefined') {
content = $anchor.innerHTML;
$anchor.innerHTML = '';
}
}
let $trigger = $anchor;
let needsTabIndex = false;
if (!$trigger.hasAttribute('tabindex')) {
needsTabIndex = true;
$trigger.setAttribute('tabindex', '0');
}
const tooltip = tippy($anchor, {
theme: 'tooltip',
triggerTarget: $trigger,
content: $anchor => content,
// showOnCreate: true,
// hideOnClick: false,
plugins: [typeof options.followCursor !== 'undefined' ? followCursor : undefined].filter(item =>
Boolean(item)
),
...hash.options,
});
let interval;
if (options.trigger === 'manual') {
// if we are manually triggering, a out delay means only show for the
// amount of time specified by the delay
const delay = options.delay || [];
if (typeof delay[1] !== 'undefined') {
hash.options.onShown = tooltip => {
clearInterval(interval);
interval = setTimeout(() => {
tooltip.hide();
}, delay[1]);
};
}
}
let $trigger = $anchor;
let needsTabIndex = false;
if (!$trigger.hasAttribute('tabindex')) {
needsTabIndex = true;
$trigger.setAttribute('tabindex', '0');
}
const tooltip = tippy($anchor, {
theme: 'tooltip',
triggerTarget: $trigger,
content: $anchor => content,
// showOnCreate: true,
// hideOnClick: false,
plugins: [
typeof options.followCursor !== 'undefined' ? followCursor : undefined,
].filter(item => Boolean(item)),
...hash.options,
});
return () => {
if (needsTabIndex) {
$trigger.removeAttribute('tabindex');
}
clearInterval(interval);
tooltip.destroy();
};
return () => {
if (needsTabIndex) {
$trigger.removeAttribute('tabindex');
}
clearInterval(interval);
tooltip.destroy();
};
}
});

View File

@ -0,0 +1,12 @@
import { inject as service } from '@ember/service';
import Route from '@ember/routing/route';
export default class PeersRoute extends Route {
@service features;
beforeModel() {
if (!this.features.isEnabled('peering')) {
this.transitionTo('dc.services.index');
}
}
}

View File

@ -0,0 +1,17 @@
import Route from '@ember/routing/route';
import { action } from '@ember/object';
export default class PeersRoute extends Route {
model() {
return this.store.findAll('peer').then(peers => {
return {
peers,
loadPeers: this.loadPeers,
};
});
}
@action loadPeers() {
return this.store.findAll('peer');
}
}

View File

@ -1,4 +1,5 @@
export default {
Name: item => item.Name,
Tags: item => item.Tags || [],
PeerName: item => item.PeerName,
};

View File

@ -21,8 +21,15 @@ export default class IntentionSerializer extends Serializer {
item.Legacy = true;
item.LegacyID = item.ID;
}
item.ID = this
.uri`${item.SourcePartition}:${item.SourceNS}:${item.SourceName}:${item.DestinationPartition}:${item.DestinationNS}:${item.DestinationName}`;
if (item.SourcePeer) {
item.ID = this
.uri`peer:${item.SourcePeer}:${item.SourceNS}:${item.SourceName}:${item.DestinationPartition}:${item.DestinationNS}:${item.DestinationName}`;
} else {
item.ID = this
.uri`${item.SourcePartition}:${item.SourceNS}:${item.SourceName}:${item.DestinationPartition}:${item.DestinationNS}:${item.DestinationName}`;
}
return item;
}

View File

@ -37,6 +37,9 @@ export default class NodeSerializer extends Serializer.extend(EmbeddedRecordsMix
}
checks[item.ServiceID].push(item);
});
if (item.PeerName === '') {
item.PeerName = undefined;
}
serializer = this.store.serializerFor(relationship.type);
item.Services = item.Services.map(service =>
serializer.transformHasManyResponseFromNode(item, service, checks)

View File

@ -0,0 +1,21 @@
import JSONAPISerializer from '@ember-data/serializer/json-api';
export default class PeerSerializer extends JSONAPISerializer {
keyForAttribute(key) {
return key.capitalize();
}
normalizeFindAllResponse(store, primaryModelClass, payload, id, requestType) {
const data = payload.map(peering => {
return {
type: 'peer',
id: peering.ID,
attributes: {
...peering,
},
};
});
return super.normalizeFindAllResponse(store, primaryModelClass, { data }, id, requestType);
}
}

View File

@ -36,6 +36,7 @@ export default class ServiceSerializer extends Serializer {
});
}
});
return cb(headers, body);
}),
query

View File

@ -0,0 +1,13 @@
import Service, { inject as service } from '@ember/service';
export default class FeatureService extends Service {
@service env;
get features() {
return this.env.var('features');
}
isEnabled(featureName) {
return !!this.features?.[featureName];
}
}

View File

@ -12,7 +12,7 @@ export default class NodeService extends RepositoryService {
return super.findAllByDatacenter(...arguments);
}
@dataSource('/:partition/:ns/:dc/node/:id')
@dataSource('/:partition/:ns/:dc/node/:id/:peer')
async findBySlug() {
return super.findBySlug(...arguments);
}

View File

@ -13,14 +13,14 @@ export default class ServiceInstanceService extends RepositoryService {
return super.shouldReconcile(...arguments) && item.Service.Service === params.id;
}
@dataSource('/:partition/:ns/:dc/service-instances/for-service/:id')
@dataSource('/:partition/:ns/:dc/service-instances/for-service/:id/:peer')
async findByService(params, configuration = {}) {
if (typeof configuration.cursor !== 'undefined') {
params.index = configuration.cursor;
params.uri = configuration.uri;
}
return this.authorizeBySlug(
async (resources) => {
async resources => {
const instances = await this.query(params);
set(instances, 'firstObject.Service.Resources', resources);
return instances;
@ -30,7 +30,7 @@ export default class ServiceInstanceService extends RepositoryService {
);
}
@dataSource('/:partition/:ns/:dc/service-instance/:serviceId/:node/:id')
@dataSource('/:partition/:ns/:dc/service-instance/:serviceId/:node/:id/:peer')
async findBySlug(params, configuration = {}) {
return super.findBySlug(...arguments);
}

View File

@ -1,4 +1,3 @@
// @import './alert-circle-fill/index.scss';
@import './alert-circle-outline/index.scss';
@import './alert-triangle/index.scss';
@ -453,7 +452,7 @@
// @import './navigation/index.scss';
// @import './navigation-alt/index.scss';
// @import './network/index.scss';
// @import './network-alt/index.scss';
@import './network-alt/index.scss';
// @import './newspaper/index.scss';
// @import './node/index.scss';
// @import './nomad/index.scss';

View File

@ -104,3 +104,8 @@
@import 'consul-ui/components/topology-metrics/series';
@import 'consul-ui/components/topology-metrics/stats';
@import 'consul-ui/components/topology-metrics/status';
@import 'consul-ui/components/peerings/badge';
@import 'consul-ui/components/peerings/search';
@import 'consul-ui/components/consul/node/peer-info';
@import 'consul-ui/components/consul/intention/list/table';
@import 'consul-ui/components/consul/service/peer-info';

View File

@ -5,3 +5,4 @@
@import 'routes/dc/intentions/index';
@import 'routes/dc/overview/serverstatus';
@import 'routes/dc/overview/license';
@import 'routes/dc/peers';

View File

@ -0,0 +1,6 @@
.peers__list__peer-detail {
display: flex;
align-content: center;
overflow-x: scroll;
gap: 18px;
}

View File

@ -10,12 +10,13 @@ as |route|>
)
}} as |tomography|>
<DataLoader
@src={{uri '/${partition}/${nspace}/${dc}/node/${name}'
@src={{uri '/${partition}/${nspace}/${dc}/node/${name}/${peer}'
(hash
partition=route.params.partition
nspace=route.params.nspace
dc=route.params.dc
name=route.params.name
peer=route.params.peer
)
}}
as |loader|>
@ -94,7 +95,7 @@ as |item tomography|}}
</BlockSlot>
<BlockSlot @name="breadcrumbs">
<ol>
<li><a data-test-back href={{href-to 'dc.nodes'}}>All Nodes</a></li>
<li><a data-test-back href={{href-to 'dc.nodes' params=(hash peer=undefined)}}>All Nodes</a></li>
</ol>
</BlockSlot>
<BlockSlot @name="header">

View File

@ -0,0 +1,61 @@
<Route @name={{route}} as |route|>
<Watcher @watch={{this.model.loadPeers}} as |w|>
{{did-insert w.fns.start}}
{{will-destroy w.fns.stop}}
</Watcher>
<AppView>
<BlockSlot @name="header">
<h1>
<route.Title @title="Peers" />
</h1>
</BlockSlot>
<BlockSlot @name="toolbar">
<Peerings::Search @search={{this.filter}} @onSearch={{this.handleSearchChanged}} />
</BlockSlot>
<BlockSlot @name="content">
{{#if this.filteredPeers.length}}
<ListCollection @items={{this.filteredPeers}} as |item index|>
<BlockSlot @name="header">
<p>{{item.Name}}</p>
</BlockSlot>
<BlockSlot @name="details">
<div class="peers__list__peer-detail">
<Peerings::Badge @peering={{item}} />
<Peerings::ServiceCount @peering={{item}} @kind="imported"/>
<Peerings::ServiceCount @peering={{item}} @kind="exported"/>
</div>
</BlockSlot>
</ListCollection>
{{else}}
{{!-- TODO: do we need to check permissions here or will we receive an error automatically? --}}
<EmptyState @login={{route.model.app.login.open}}>
<BlockSlot @name="header">
<h2>Welcome to Peers</h2>
</BlockSlot>
<BlockSlot @name="body">
<p>
Peering allows an admin partition in one datacenter to communicate with a partition in a different
datacenter. There don't seem to be any peers for this admin partition, or you may not have
<code>peering:read</code> permissions to
access this view.
</p>
</BlockSlot>
<BlockSlot @name="actions">
<li class="docs-link">
{{!-- what's the docs for peering?--}}
<a href="https://www.consul.io/docs/agent/kv" rel="noopener noreferrer" target="_blank">
Documentation on Peers
</a>
</li>
<li class="learn-link">
<a href="https://learn.hashicorp.com/consul/getting-started/kv" rel="noopener noreferrer" target="_blank">
Take the tutorial
</a>
</li>
</BlockSlot>
</EmptyState>
{{/if}}
</BlockSlot>
</AppView>
</Route>

View File

@ -3,7 +3,7 @@
as |route|>
<DataLoader @src={{
uri '/${partition}/${nspace}/${dc}/services'
uri '/${partition}/${nspace}/${dc}/services'
(hash
partition=route.params.partition
nspace=route.params.nspace

View File

@ -2,7 +2,7 @@
@name={{routeName}}
as |route|>
<DataLoader
@src={{uri '/${partition}/${nspace}/${dc}/service-instance/${id}/${node}/${name}'
@src={{uri '/${partition}/${nspace}/${dc}/service-instance/${id}/${node}/${name}/${peer}'
(hash
partition=route.params.partition
nspace=route.params.nspace
@ -10,6 +10,7 @@ as |route|>
id=route.params.id
node=route.params.node
name=route.params.name
peer=route.params.peer
)
}}
as |loader|>
@ -108,7 +109,7 @@ 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}'
@src={{uri '/${partition}/${nspace}/${dc}/service-instance/${id}/${node}/${name}/${peer}'
(hash
partition=route.params.partition
nspace=route.params.nspace
@ -116,6 +117,7 @@ as |item|}}
id=meta.data.ServiceID
node=meta.data.NodeName
name=meta.data.ServiceName
peer=route.params.peer
)
}}
@onchange={{action (mut proxy) value="data"}}
@ -126,7 +128,7 @@ as |item|}}
<AppView>
<BlockSlot @name="breadcrumbs">
<ol>
<li><a href={{href-to 'dc.services'}}>All Services</a></li>
<li><a href={{href-to 'dc.services' params=(hash peer=undefined)}}>All Services</a></li>
<li><a data-test-back href={{href-to 'dc.services.show'}}>Service ({{item.Service.Service}})</a></li>
</ol>
</BlockSlot>
@ -151,6 +153,12 @@ as |item|}}
<dt>Node Name</dt>
<dd><a href="{{href-to 'dc.nodes.show' item.Node.Node}}">{{item.Node.Node}}</a></dd>
</dl>
{{#if item.Service.PeerName}}
<dl>
<dt>Peer Name</dt>
<dd>{{item.Service.PeerName}}</dd>
</dl>
{{/if}}
</BlockSlot>
<BlockSlot @name="actions">
{{#let (or item.Service.Address item.Node.Address) as |address|}}

View File

@ -2,12 +2,13 @@
@name={{routeName}}
as |route|>
<DataLoader
@src={{uri '/${partition}/${nspace}/${dc}/service-instances/for-service/${name}'
@src={{uri '/${partition}/${nspace}/${dc}/service-instances/for-service/${name}/${peer}'
(hash
partition=route.params.partition
nspace=route.params.nspace
dc=route.params.dc
name=route.params.name
peer=route.params.peer
)
}}
as |loader|>
@ -135,7 +136,7 @@ as |items item dc|}}
</BlockSlot>
<BlockSlot @name="breadcrumbs">
<ol>
<li><a data-test-back href={{href-to 'dc.services'}}>All Services</a></li>
<li><a data-test-back href={{href-to 'dc.services' params=(hash peer=undefined)}}>All Services</a></li>
</ol>
</BlockSlot>
<BlockSlot @name="header">
@ -144,6 +145,7 @@ as |items item dc|}}
</h1>
<Consul::ExternalSource @item={{item.Service}} @withInfo={{true}} />
<Consul::Kind @item={{item.Service}} @withInfo={{true}} />
<Consul::Service::PeerInfo @service={{item.Service}} />
</BlockSlot>
<BlockSlot @name="nav">
{{#if (not-eq item.Service.Kind 'mesh-gateway')}}

View File

@ -46,12 +46,13 @@ as |sort filters items proxyMeta|}}
{{/if}}
{{#if proxyMeta.ServiceName}}
<DataSource
@src={{uri '/${partition}/${nspace}/${dc}/service-instances/for-service/${name}'
@src={{uri '/${partition}/${nspace}/${dc}/service-instances/for-service/${name}/${peer}'
(hash
partition=route.params.partition
nspace=route.params.nspace
dc=route.params.dc
name=proxyMeta.ServiceName
peer=route.params.peer
)
}}
@onchange={{action (mut proxies) value="data"}}

View File

@ -26,7 +26,7 @@ module.exports = function(environment, $ = process.env) {
historySupportMiddleware: true,
torii: {
disableRedirectInitializer: false
disableRedirectInitializer: false,
},
EmberENV: {
@ -110,6 +110,10 @@ module.exports = function(environment, $ = process.env) {
PrimaryDatacenter: env('CONSUL_DATACENTER_PRIMARY', 'dc1'),
},
features: {
peering: true,
},
'@hashicorp/ember-cli-api-double': {
'auto-import': false,
enabled: true,
@ -134,14 +138,17 @@ module.exports = function(environment, $ = process.env) {
case environment === 'development':
ENV = Object.assign({}, ENV, {
torii: {
disableRedirectInitializer: true
disableRedirectInitializer: true,
},
features: {
peering: true,
},
});
break;
case environment === 'staging':
ENV = Object.assign({}, ENV, {
torii: {
disableRedirectInitializer: true
disableRedirectInitializer: true,
},
// On staging sites everything defaults to being turned on by
// different staging sites can be built with certain features disabled

View File

@ -22,6 +22,7 @@ ${legacy ? `
"Action": "${fake.helpers.randomize(['allow', 'deny'])}",
`:``}
"Description": "${fake.lorem.sentence()}",
"SourcePeer": "${fake.helpers.randomize(['billing', ''])}",
"SourceName": "${fake.hacker.noun()}-${i}",
"DestinationName": "${fake.hacker.noun()}",
"SourceNS": "default",

View File

@ -3,6 +3,10 @@ ${range(1).map(item => {
const legacy = ID.indexOf('%3A') === -1;
const source = location.search.source.split('/');
const destination = location.search.destination.split('/');
const sourceIsPeered = !!source[0].match(/^peer:/)?.length
const sourcePeerString = `"SourcePeer": "${source[0].split(':')[1]}",`
const sourcePartitionString = `"SourcePartition": "${source[0]}",`
return `
{
"ID": "${legacy ? ID : ''}"
@ -12,7 +16,7 @@ ${ http.method !== "PUT" ? `
"DestinationName": "${destination[2]}",
"SourceNS": "${source[1]}",
"DestinationNS": "${destination[1]}",
"SourcePartition": "${source[0]}",
${sourceIsPeered ? sourcePeerString : sourcePartitionString}
"DestinationPartition": "${destination[0]}",
"SourceType": "${fake.helpers.randomize(['consul', 'externaluri'])}",
${legacy ? `

View File

@ -43,11 +43,17 @@
"Datacenter":"dc1",
"TaggedAddresses":{"lan":"${ip}","wan":"${ip}"},
"Meta":{"${service}-network-segment":""},
${typeof location.search.peer !== 'undefined' ? `
"PeerName": "${location.search.peer}",
` : ``}
"CreateIndex":5,
"ModifyIndex":6
},
"Service":{
"ID": "${ i === 0 ? id : fake.helpers.randomize([service, service + '-ID'])}",
${typeof location.search.peer !== 'undefined' ? `
"PeerName": "${location.search.peer}",
` : ``}
"Service":"${service}",
${typeof location.search.ns !== 'undefined' ? `
"Namespace": "${location.search.ns}",

View File

@ -19,9 +19,12 @@ ${[1].map(() => {
}
);
const peerNameString = location.search.peer ? `"PeerName": "${location.search.peer}",`: ''
return `
{
"ID":"${node = location.pathname.get(4)}",
${peerNameString}
"Node":"${node}",
"Address":"${ip = fake.internet.ip()}",
"TaggedAddresses":{"lan":"${ip}","wan":"${ip}"},

View File

@ -12,10 +12,12 @@
).map(
function(item, i)
{
const peerNameString = i === 0 ? '"PeerName": "billing",' : '"PeerName": "",'
return `
{
"ID":"${fake.random.uuid()}",
"Node":"node-${i}",
${location.search["with-peers"] ? peerNameString : ''}
"Address":"${fake.internet.ip()}",
"TaggedAddresses":{
"lan":"${fake.internet.ip()}",

View File

@ -64,8 +64,10 @@ ${
${
upstreams.map((item, i) => {
const hasPerms = fake.random.boolean();
const isPeered = fake.random.boolean();
// if hasPerms is true allowed is always false as some restrictions apply
const allowed = hasPerms ? false : fake.random.boolean();
const peerString = isPeered ? `"PeerName": "${fake.random.word()}",` : '';
return `
{
${(Math.random(1) > 0.3) ? `
@ -79,6 +81,7 @@ ${(Math.random(1) > 0.3) ? `
"ChecksWarning":${fake.random.number({min: 0, max: env('CONSUL_CHECK_COUNT', fake.random.number(10))})},
"ChecksCritical":${fake.random.number({min: 0, max: env('CONSUL_CHECK_COUNT', fake.random.number(10))})},
"Source": "${fake.helpers.randomize(['routing-config', 'proxy-registration', 'default-allow', 'wildcard-intention'])}",
${peerString}
"TransparentProxy": ${fake.random.boolean()},
"Intention": {
"Allowed": ${allowed},

View File

@ -19,9 +19,11 @@ ${[0].map(
function(item, i)
{
let kind;
let peerName;
switch(i) {
case 0:
kind = '';
peerName = 'billing'
break;
case 1:
kind = 'connect-proxy';
@ -40,6 +42,8 @@ ${[0].map(
} else {
name = `service-${i}${ kind !== '' ? `-${kind.replace('connect-', '')}` : '' }`;
}
const peerNameString = `"PeerName": "${peerName || ''}",`
return `
{
"Name":"${name}",
@ -49,6 +53,7 @@ ${typeof location.search.ns !== 'undefined' ? `
${typeof location.search.partition !== 'undefined' ? `
"Partition": "${fake.helpers.randomize([env('CONSUL_PARTITION_EXPORTER', location.search.partition), location.search.partition])}",
` : ``}
${location.search['with-peers'] ? peerNameString : ''}
"Tags": [
${
range(

View File

@ -0,0 +1,65 @@
[
{
"ID": "2ccc588f-efc4-0a7c-1a73-c25cfcf34b94",
"Name": "web",
"State": "ACTIVE",
"ImportedServiceCount": 10,
"ExportedServiceCount": 3,
"CreateIndex": 18,
"ModifyIndex": 18
},
{
"ID": "a25cdcc4-9e09-5276-bcd7-e2e4743ca687",
"Name": "billing",
"State": "PENDING",
"ImportedServiceCount": 5,
"ExportedServiceCount": 2,
"CreateIndex": 16,
"ModifyIndex": 16
},
{
"ID": "a25cdcc4-9e09-5276-bcd7-e2e4743ca688",
"Name": "peer-1",
"State": "ESTABLISHING",
"ImportedServiceCount": 2,
"ExportedServiceCount": 4,
"CreateIndex": 16,
"ModifyIndex": 16
},
{
"ID": "2ccc588f-efc4-0a7c-1a73-c25cfcf34b95",
"Name": "db",
"State": "FAILING",
"ImportedServiceCount": 4,
"ExportedServiceCount": 3,
"CreateIndex": 19,
"ModifyIndex": 19
},
{
"ID": "2ccc588f-efc4-0a7c-1a73-c25cfcf34b98",
"Name": "legacy deleted",
"State": "DELETING",
"ImportedServiceCount": 2,
"ExportedServiceCount": 4,
"CreateIndex": 20,
"ModifyIndex": 20
},
{
"ID": "2ccc588f-efc4-0a7c-1a73-c25cfcf34b96",
"Name": "legacy",
"State": "TERMINATED",
"ImportedServiceCount": 0,
"ExportedServiceCount": 0,
"CreateIndex": 20,
"ModifyIndex": 20
},
{
"ID": "2ccc588f-efc4-0a7c-1a73-c25cfcf34b97",
"Name": "legacy undefined",
"ImportedServiceCount": 0,
"ExportedServiceCount": 0,
"State": "UNDEFINED",
"CreateIndex": 20,
"ModifyIndex": 20
}
]

View File

@ -123,6 +123,7 @@
"ember-in-viewport": "^3.8.1",
"ember-inflector": "^4.0.1",
"ember-intl": "^5.5.1",
"ember-keyboard": "^7.0.1",
"ember-load-initializers": "^2.1.1",
"ember-math-helpers": "^2.4.0",
"ember-maybe-import-regenerator": "^0.1.6",

View File

@ -27,7 +27,7 @@ Feature: dc / intentions / navigation
ID: 755b72bd-f5ab-4c92-90cc-bed0e7d8e9f0
---
When I click intention on the intentionList.intentions component
Then a GET request was made to "/v1/internal/ui/services?dc=dc-1&ns=*"
Then a GET request was made to "/v1/internal/ui/services?dc=dc-1&with-peers=true&ns=*"
And I click "[data-test-back]"
Then the url should be /dc-1/intentions
Scenario: Clicking the create button and back again

View File

@ -45,6 +45,7 @@ Feature: dc / nodes / index
---
Then the url should be /dc-1/nodes
And the title should be "Nodes - Consul"
And a GET request was made to "/v1/internal/ui/nodes?dc=dc-1&with-peers=true"
Then I see 3 node models
Scenario: Seeing the leader in node listing
Given 3 node models from yaml

View File

@ -1,20 +1,38 @@
@setupApplicationTest
Feature: dc / services / list
Scenario: Listing service
Given 1 datacenter model with the value "dc-1"
And 3 service models from yaml
---
- Name: Service-0
Kind: ~
- Name: Service-1
Kind: ~
- Name: Service-2
Kind: ~
---
When I visit the services page for yaml
---
dc: dc-1
---
Then the url should be /dc-1/services
Then I see 3 service models
@setupApplicationTest
Feature: dc / services / list
Scenario: Listing service
Given 1 datacenter model with the value "dc-1"
And 3 service models from yaml
---
- Name: Service-0
Kind: ~
- Name: Service-1
Kind: ~
- Name: Service-2
Kind: ~
---
When I visit the services page for yaml
---
dc: dc-1
---
Then the url should be /dc-1/services
And a GET request was made to "/v1/internal/ui/services?dc=dc-1&with-peers=true"
Then I see 3 service models
Scenario: Listing peered service
Given 1 datacenter model with the value "dc-1"
And 1 service models from yaml
---
- Name: Service-0
Kind: ~
PeerName: billing-app
---
When I visit the services page for yaml
---
dc: dc-1
---
Then the url should be /dc-1/services
Then I see 1 service model with the peer "billing-app"

View File

@ -14,3 +14,15 @@ Feature: dc / services / navigation
And I click "[data-test-back]"
Then the url should be /dc-1/services
Scenario: Clicking a peered service in the listing and back again
Given 1 datacenter model with the value "dc-1"
And 1 service model
When I visit the services page for yaml
---
dc: dc-1
---
When I click service on the services
Then the url should match /:billing/dc-1/services/service-0
And I click "[data-test-back]"
Then the url should be /dc-1/services

View File

@ -17,5 +17,5 @@ Feature: dc / services / show-with-slashes: Show Service that has slashes in its
Then the url should be /dc1/services
Then I see 1 service model
And I click service on the services
Then the url should be /dc1/services/hashicorp%2Fservice%2Fservice-0/topology
Then the url should be /:billing/dc1/services/hashicorp%2Fservice%2Fservice-0/topology

View File

@ -41,7 +41,7 @@ Feature: dc / services / show / intentions / index: Intentions per service
Scenario: I can see intentions
And I see 3 intention models on the intentionList component
And I click intention on the intentionList.intentions component
Then the url should be /dc1/services/service-0/intentions/default:default:name:default:default:destination
Then the url should be /dc1/services/service-0/intentions/peer:billing:default:name:default:default:destination
Scenario: I can delete intentions
And I click actions on the intentionList.intentions component
And I click delete on the intentionList.intentions component

View File

@ -0,0 +1,7 @@
@setupApplicationTest
Feature: dc / services / show / navigation
Scenario: Accessing peered service directly
Given 1 datacenter model with the value "dc-1"
And 1 service models
When I visit the service page with the url /:billing/dc-1/services/service-0
Then I see peer like "billing"

View File

@ -20,14 +20,14 @@ Feature: page-navigation
Then the url should be [URL]
Then a GET request was made to "[Endpoint]"
Where:
-------------------------------------------------------------------------------------
| Link | URL | Endpoint |
| nodes | /dc1/nodes | /v1/internal/ui/nodes?dc=dc1&ns=@namespace |
---------------------------------------------------------------------------------------------------
| Link | URL | Endpoint |
| nodes | /dc1/nodes | /v1/internal/ui/nodes?dc=dc1&with-peers=true&ns=@namespace |
# FIXME
# | kvs | /dc1/kv | /v1/kv/?keys&dc=dc1&separator=%2F&ns=@namespace |
| tokens | /dc1/acls/tokens | /v1/acl/tokens?dc=dc1&ns=@namespace |
# | settings | /settings | /v1/catalog/datacenters |
-------------------------------------------------------------------------------------
# | kvs | /dc1/kv | /v1/kv/?keys&dc=dc1&separator=%2F&ns=@namespace |
| tokens | /dc1/acls/tokens | /v1/acl/tokens?dc=dc1&ns=@namespace |
# | settings | /settings | /v1/catalog/datacenters |
---------------------------------------------------------------------------------------------------
# FIXME
@ignore
Scenario: Clicking a [Item] in the [Model] listing and back again

View File

@ -0,0 +1,10 @@
import steps from '../../../steps';
// step definitions that are shared between features should be moved to the
// tests/acceptance/steps/steps.js file
export default function(assert) {
return steps(assert).then('I should find a file', function() {
assert.ok(true, this.step);
});
}

View File

@ -11,7 +11,7 @@ Feature: token-header
dc: dc1
---
Then the url should be /dc1/services
And a GET request was made to "/v1/internal/ui/services?dc=dc1&ns=@namespace" from yaml
And a GET request was made to "/v1/internal/ui/services?dc=dc1&with-peers=true&ns=@namespace" from yaml
---
headers:
X-Consul-Token: ''
@ -35,7 +35,7 @@ Feature: token-header
dc: dc1
---
Then the url should be /dc1/services
And a GET request was made to "/v1/internal/ui/services?dc=dc1&ns=@namespace" from yaml
And a GET request was made to "/v1/internal/ui/services?dc=dc1&with-peers=true&ns=@namespace" from yaml
---
headers:
X-Consul-Token: [Token]

View File

@ -16,7 +16,7 @@ module('Integration | Adapter | node', function(hooks) {
const request = client.requestParams.bind(client);
const expected = `GET /v1/internal/ui/nodes?dc=${dc}${
shouldHaveNspace(nspace) ? `&ns=${nspace}` : ``
}`;
}&with-peers=true`;
const actual = adapter.requestForQuery(request, {
dc: dc,
ns: nspace,

View File

@ -16,7 +16,7 @@ module('Integration | Adapter | service', function(hooks) {
const request = client.requestParams.bind(client);
const expected = `GET /v1/internal/ui/services?dc=${dc}${
shouldHaveNspace(nspace) ? `&ns=${nspace}` : ``
}`;
}&with-peers=true`;
let actual = adapter.requestForQuery(request, {
dc: dc,
ns: nspace,

View File

@ -19,16 +19,19 @@ module('Integration | Serializer | intention', function(hooks) {
url: `/v1/connect/intentions?dc=${dc}`,
};
return get(request.url).then(function(payload) {
const expected = payload.map(item =>
Object.assign({}, item, {
const expected = payload.map(item => {
if (item.SourcePeer) {
delete item.SourcePeer;
}
return Object.assign({}, item, {
Datacenter: dc,
// TODO: default isn't required here, once we've
// refactored out our Serializer this can go
Namespace: nspace,
Partition: partition,
uid: `["${partition}","${nspace}","${dc}","${item.SourcePartition}:${item.SourceNS}:${item.SourceName}:${item.DestinationPartition}:${item.DestinationNS}:${item.DestinationName}"]`,
})
);
});
});
const actual = serializer.respondForQuery(
function(cb) {
const headers = {

View File

@ -4,6 +4,7 @@ export default function(visitable, clickable, text, attribute, present, collecti
service: clickable('a'),
externalSource: attribute('data-test-external-source', '[data-test-external-source]'),
kind: attribute('data-test-kind', '[data-test-kind]'),
peer: text('[data-test-bucket-item="peer"]'),
mesh: present('[data-test-mesh]'),
associatedServiceCount: present('[data-test-associated-service-count]'),
};

View File

@ -19,6 +19,7 @@ export default function(
metricsAnchor: {
href: attribute('href', '[data-test-metrics-anchor]'),
},
peer: text('[data-test-service-peer-info] [data-test-peer-name]'),
tabs: tabs('tab', [
'topology',
'instances',

View File

@ -68,6 +68,13 @@ export default function(scenario, assert, pauseUntil, find, currentURL, clipboar
assert.strictEqual(actual, expected, `Expected settings to be ${expected} was ${actual}`);
});
})
.then('the url should match $url', function(url) {
const currentUrl = currentURL() || '';
const matches = !!currentUrl.match(url);
assert.ok(matches, `Expected currentURL to match the provided regex: ${url}`);
})
.then('the url should be $url', function(url) {
// TODO: nice! $url should be wrapped in ""
if (url === "''") {

View File

@ -1,3 +1,5 @@
import { visit } from '@ember/test-helpers';
export default function(scenario, pages, set, reset) {
scenario
.when('I visit the $name page', function(name) {
@ -10,6 +12,11 @@ export default function(scenario, pages, set, reset) {
[model]: id,
});
})
.when('I visit the $name page with the url $url', function(name, url) {
reset();
set(pages[name]);
return visit(url);
})
.when(
['I visit the $name page for yaml\n$yaml', 'I visit the $name page for json\n$json'],
function(name, data) {

View File

@ -46,6 +46,7 @@ consul:
failuretolerance: Fault tolerance
readreplica: Read replica
redundancyzone: Redundancy zone
peername: Peer
search:
search: Search
searchproperty: Search Across

View File

@ -105,6 +105,17 @@ dc:
<p>
This node has a failing serf node check. The health statuses shown on this page are the statuses as they were known before the node became unreachable.
</p>
peers:
index:
detail:
imported:
count: |
{count} imported services
tooltip: The number of services imported from {name}
exported:
count: |
{count} exported services
tooltip: The number of services exported from {name}
services:
index:
empty:

View File

@ -18,7 +18,7 @@
serverstatus: {
_options: {
path: '/server-status',
abilities: ['read servers']
abilities: ['read servers'],
},
},
cataloghealth: {
@ -30,7 +30,15 @@
license: {
_options: {
path: '/license',
abilities: ['read license']
abilities: ['read license'],
},
},
},
peers: {
_options: { path: '/peers' },
index: {
_options: {
path: '/',
},
},
},
@ -46,7 +54,7 @@
kind: 'kind',
searchproperty: {
as: 'searchproperty',
empty: [['Name', 'Tags']],
empty: [['Name', 'Tags', 'PeerName']],
},
search: {
as: 'filter',
@ -56,7 +64,9 @@
},
},
show: {
_options: { path: '/:name' },
_options: {
path: '/:name',
},
instances: {
_options: {
path: '/instances',

View File

@ -2944,6 +2944,13 @@ ansi-styles@~1.0.0:
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.0.0.tgz#cb102df1c56f5123eab8b67cd7b98027a0279178"
integrity sha1-yxAt8cVvUSPquLZ817mAJ6AnkXg=
ansi-to-html@^0.6.15:
version "0.6.15"
resolved "https://registry.yarnpkg.com/ansi-to-html/-/ansi-to-html-0.6.15.tgz#ac6ad4798a00f6aa045535d7f6a9cb9294eebea7"
integrity sha512-28ijx2aHJGdzbs+O5SNQF65r6rrKYnkuwTYm8lZlChuoJ9P1vVzIpWO20sQTqTPDXYp6NFwk326vApTtLVFXpQ==
dependencies:
entities "^2.0.0"
ansi-to-html@^0.6.6:
version "0.6.14"
resolved "https://registry.yarnpkg.com/ansi-to-html/-/ansi-to-html-0.6.14.tgz#65fe6d08bba5dd9db33f44a20aec331e0010dad8"
@ -6647,6 +6654,27 @@ ember-cli-htmlbars@^6.0.0:
strip-bom "^4.0.0"
walk-sync "^2.2.0"
ember-cli-htmlbars@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/ember-cli-htmlbars/-/ember-cli-htmlbars-6.0.1.tgz#5487831d477e61682bc867fd138808269e5d2152"
integrity sha512-IDsl9uty+MXtMfp/BUTEc/Q36EmlHYj8ZdPekcoRa8hmdsigHnK4iokfaB7dJFktlf6luruei+imv7JrJrBQPQ==
dependencies:
"@ember/edition-utils" "^1.2.0"
babel-plugin-ember-template-compilation "^1.0.0"
babel-plugin-htmlbars-inline-precompile "^5.3.0"
broccoli-debug "^0.6.5"
broccoli-persistent-filter "^3.1.2"
broccoli-plugin "^4.0.3"
ember-cli-version-checker "^5.1.2"
fs-tree-diff "^2.0.1"
hash-for-dep "^1.5.1"
heimdalljs-logger "^0.1.10"
json-stable-stringify "^1.0.1"
semver "^7.3.4"
silent-error "^1.1.1"
strip-bom "^4.0.0"
walk-sync "^2.2.0"
ember-cli-inject-live-reload@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/ember-cli-inject-live-reload/-/ember-cli-inject-live-reload-2.0.2.tgz#95edb543b386239d35959e5ea9579f5382976ac7"
@ -6845,6 +6873,22 @@ ember-cli-typescript@^4.0.0, ember-cli-typescript@^4.1.0:
stagehand "^1.0.0"
walk-sync "^2.2.0"
ember-cli-typescript@^5.0.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/ember-cli-typescript/-/ember-cli-typescript-5.1.0.tgz#460eb848564e29d64f2b36b2a75bbe98172b72a4"
integrity sha512-wEZfJPkjqFEZAxOYkiXsDrJ1HY75e/6FoGhQFg8oNFJeGYpIS/3W0tgyl1aRkSEEN1NRlWocDubJ4aZikT+RTA==
dependencies:
ansi-to-html "^0.6.15"
broccoli-stew "^3.0.0"
debug "^4.0.0"
execa "^4.0.0"
fs-extra "^9.0.1"
resolve "^1.5.0"
rsvp "^4.8.1"
semver "^7.3.2"
stagehand "^1.0.0"
walk-sync "^2.2.0"
ember-cli-uglify@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/ember-cli-uglify/-/ember-cli-uglify-3.0.0.tgz#8819665b2cc5fe70e3ba9fe7a94645209bc42fd6"
@ -7247,6 +7291,16 @@ ember-intl@^5.5.1:
mkdirp "^1.0.4"
silent-error "^1.1.1"
ember-keyboard@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/ember-keyboard/-/ember-keyboard-7.0.1.tgz#6cf336efd4ea6cb69ec93d20fb0b819bd7241a9d"
integrity sha512-MKK9/3yzn30ekmFAQO7z+okCQa7Z5wCSI5m7lR3EL2dMIeRd/9eeLhbQNCU00Slx+GjwsGyCEWPqIQmekFJxpQ==
dependencies:
ember-cli-babel "^7.26.6"
ember-cli-htmlbars "^6.0.1"
ember-modifier "^2.1.2 || ^3.0.0"
ember-modifier-manager-polyfill "^1.2.0"
ember-load-initializers@^2.1.1:
version "2.1.2"
resolved "https://registry.yarnpkg.com/ember-load-initializers/-/ember-load-initializers-2.1.2.tgz#8a47a656c1f64f9b10cecdb4e22a9d52ad9c7efa"
@ -7305,6 +7359,17 @@ 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.0.0":
version "3.2.7"
resolved "https://registry.yarnpkg.com/ember-modifier/-/ember-modifier-3.2.7.tgz#f2d35b7c867cbfc549e1acd8d8903c5ecd02ea4b"
integrity sha512-ezcPQhH8jUfcJQbbHji4/ZG/h0yyj1jRDknfYue/ypQS8fM8LrGcCMo0rjDZLzL1Vd11InjNs3BD7BdxFlzGoA==
dependencies:
ember-cli-babel "^7.26.6"
ember-cli-normalize-entity-name "^1.0.0"
ember-cli-string-utils "^1.1.0"
ember-cli-typescript "^5.0.0"
ember-compatibility-helpers "^1.2.5"
ember-named-blocks-polyfill@^0.2.3:
version "0.2.4"
resolved "https://registry.yarnpkg.com/ember-named-blocks-polyfill/-/ember-named-blocks-polyfill-0.2.4.tgz#f5f30711ee89244927b55aae7fa9630edaadc974"