mirror of https://github.com/hashicorp/consul
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 testspull/13574/head
parent
a0b94d9a3a
commit
5de75550d3
@ -0,0 +1,9 @@
|
||||
import JSONAPIAdapter from '@ember-data/adapter/json-api';
|
||||
|
||||
export default class PeerAdapter extends JSONAPIAdapter {
|
||||
namespace = 'v1';
|
||||
|
||||
pathForType(_modelName) {
|
||||
return 'peerings';
|
||||
}
|
||||
}
|
@ -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 data-test-bucket-item={{item.type}}>
|
||||
{{item.item}}
|
||||
</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>
|
||||
{{/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}}
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
.consul-intention-list-table__meta-info {
|
||||
display: flex;
|
||||
|
||||
.consul-intention-list-table__meta-info__peer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
@ -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}}
|
@ -0,0 +1,8 @@
|
||||
.consul-node-peer-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.consul-node-peer-info__name {
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
@ -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}}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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}}
|
@ -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}}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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>
|
@ -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;
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
{{#if this.count}}
|
||||
<div {{tooltip this.tooltipText}}>
|
||||
{{this.text}}
|
||||
</div>
|
||||
{{/if}}
|
@ -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 });
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
{{yield (hash
|
||||
fns=(hash
|
||||
start=this.start
|
||||
stop=this.stop
|
||||
)
|
||||
)}}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
@ -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');
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
export default {
|
||||
Name: item => item.Name,
|
||||
Tags: item => item.Tags || [],
|
||||
PeerName: item => item.PeerName,
|
||||
};
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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];
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
.peers__list__peer-detail {
|
||||
display: flex;
|
||||
align-content: center;
|
||||
overflow-x: scroll;
|
||||
gap: 18px;
|
||||
}
|
@ -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>
|
@ -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
|
||||
}
|
||||
]
|
@ -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"
|
||||
|
@ -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"
|
@ -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);
|
||||
});
|
||||
}
|
Loading…
Reference in new issue