2
0
mirror of https://github.com/status-im/consul.git synced 2025-01-12 06:44:41 +00:00

ui: Add peer searching and sorting ()

* ui: Add peer searching and sorting

Initial name search and sort only, more to come here

* Remove old peerings::search component

* Use @model peers

* ui: Peer listing with dc/ns/partition/name based unique IDs and polling deletion ()

* ui: Add peer repo with listing datasource

* ui: Use data-loader component to use the data-source

* ui: Remove ember-data REST things and Route.model hook

* 10 second not 1 second poll

* Fill out Datacenter and Partition

* route > routeName

* Faker randomised mocks for peering endpoint

* ui: Adds initial peer detail page plus address tab ()
This commit is contained in:
John Cowen 2022-07-04 11:31:58 +01:00 committed by GitHub
parent ea33bc249c
commit 27e50ae925
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 543 additions and 313 deletions
ui
packages/consul-ui
app
abilities
adapters
components
consul/peer
address/list
search-bar
peerings/search
controllers/dc/peers
models
routes/dc/peers
search/predicates
serializers
services
sort/comparators
styles
templates/dc/peers
mock-api/v1
package.json
tests/unit/abilities
vendor/consul-ui
yarn.lock

@ -0,0 +1,19 @@
import BaseAbility from 'consul-ui/abilities/base';
export default class PeerAbility extends BaseAbility {
resource = 'operator';
segmented = false;
get isLinkable() {
return this.canDelete;
}
get canDelete() {
// TODO: Need to confirm these states
return ![
'DELETING',
'TERMINATED',
'UNDEFINED'
].includes(this.item.State);
}
}

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

@ -0,0 +1,15 @@
<ListCollection
class="consul-peer-address-list"
...attributes
@items={{@items}}
as |item|>
<BlockSlot @name="header">
<p>
{{item}}
<CopyButton
@value={{item}}
@name="Address"
/>
</p>
</BlockSlot>
</ListCollection>

@ -0,0 +1,30 @@
# Consul::Peer::SearchBar
Searchbar tailored for searching Peers. Follows our more generic
'*::SearchBar' component interface.
```hbs preview-template
<Consul::Peer::SearchBar
@search={{this.search}}
@onsearch={{fn (mut this.search) value="target.value"}}
@sort={{hash
value='Name:asc'
change=(noop)
}}
@filter={{hash
searchproperty=(hash
value=(array)
change=(noop)
default=(array)
)
}}
/>
```
## See
- [Template Source Code](./index.hbs)
---

@ -0,0 +1,98 @@
<SearchBar
class="consul-peer-search-bar"
...attributes
@filter={{@filter}}
>
<:status as |search|>
{{#let
(t (concat "components.consul.peer.search-bar." search.status.key)
default=(array
(concat "common.search." search.status.key)
(concat "common.consul." search.status.key)
)
)
(t (concat "components.consul.peer.search-bar." search.status.value)
default=(array
(concat "common.search." search.status.value)
(concat "common.consul." search.status.value)
(concat "common.brand." search.status.value)
)
)
as |key value|}}
<search.RemoveFilter
aria-label={{t "common.ui.remove" item=(concat key " " value)}}
>
<dl>
<dt>{{key}}</dt>
<dd>{{value}}</dd>
</dl>
</search.RemoveFilter>
{{/let}}
</:status>
<:search as |search|>
<search.Search
@onsearch={{action @onsearch}}
@value={{@search}}
@placeholder={{t "common.search.search"}}
>
<search.Select
class="type-search-properties"
@position="right"
@onchange={{action @filter.searchproperty.change}}
@multiple={{true}}
@required={{true}}
as |components|>
<BlockSlot @name="selected">
<span>
{{t "common.search.searchproperty"}}
</span>
</BlockSlot>
<BlockSlot @name="options">
{{#let components.Optgroup components.Option as |Optgroup Option|}}
{{#each @filter.searchproperty.default as |prop|}}
<Option @value={{prop}} @selected={{includes prop @filter.searchproperty.value}}>
{{t (concat "common.consul." (lowercase prop))}}
</Option>
{{/each}}
{{/let}}
</BlockSlot>
</search.Select>
</search.Search>
</:search>
<:sort as |search|>
<search.Select
class="type-sort"
data-test-sort-control
@position="right"
@onchange={{action @sort.change}}
@multiple={{false}}
@required={{true}}
as |components|>
<BlockSlot @name="selected">
<span>
{{#let (from-entries (array
(array "Name:asc" (t "common.sort.alpha.asc"))
(array "Name:desc" (t "common.sort.alpha.desc"))
))
as |selectable|
}}
{{get selectable @sort.value}}
{{/let}}
</span>
</BlockSlot>
<BlockSlot @name="options">
{{#let components.Optgroup components.Option as |Optgroup Option|}}
<Optgroup @label={{t "common.consul.name"}}>
<Option @value="Name:asc" @selected={{eq "Name:asc" @sort.value}}>{{t "common.sort.alpha.asc"}}</Option>
<Option @value="Name:desc" @selected={{eq "Name:desc" @sort.value}}>{{t "common.sort.alpha.desc"}}</Option>
</Optgroup>
{{/let}}
</BlockSlot>
</search.Select>
</:sort>
</SearchBar>

@ -1,28 +0,0 @@
<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>

@ -1,36 +0,0 @@
.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;
}
}

@ -1,29 +0,0 @@
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;
}
}

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

@ -1,17 +1,15 @@
import Route from '@ember/routing/route';
import { action } from '@ember/object';
import Route from 'consul-ui/routing/route';
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');
}
queryParams = {
sortBy: 'sort',
searchproperty: {
as: 'searchproperty',
empty: [['Name']],
},
search: {
as: 'filter',
replace: true,
},
};
}

@ -0,0 +1,3 @@
export default {
Name: item => item.Name,
};

@ -1,21 +0,0 @@
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,73 @@
import RepositoryService from 'consul-ui/services/repository';
import dataSource from 'consul-ui/decorators/data-source';
export default class PeerService extends RepositoryService {
getModelName() {
return 'peer';
}
@dataSource('/:partition/:ns/:dc/peers')
async fetchAll({ dc, ns, partition }, { uri }, request) {
return (await request`
GET /v1/peerings
${{
partition,
}}
`)(
(headers, body, cache) => {
return {
meta: {
version: 2,
interval: 10000,
uri: uri,
},
body: body.map(item => {
return cache(
{
...item,
Datacenter: dc,
Partition: partition,
},
uri => uri`peer:///${partition}/${ns}/${dc}/peer/${item.Name}`
);
})
};
});
}
@dataSource('/:partition/:ns/:dc/peer/:name')
async fetchOne({partition, ns, dc, name}, { uri }, request) {
if (name === '') {
return this.create({
Datacenter: dc,
Namespace: '',
Partition: partition,
});
}
return (await request`
GET /v1/peering/${name}
${{
partition,
}}
`)((headers, body, cache) => {
return {
meta: {
version: 2,
interval: 10000,
uri: uri,
},
body: cache(
{
...body,
Datacenter: dc,
Partition: partition,
},
uri => uri`peer:///${partition}/${ns}/${dc}/peer/${body.Name}`
)
};
});
}
}

@ -15,6 +15,7 @@ import role from 'consul-ui/search/predicates/role';
import policy from 'consul-ui/search/predicates/policy';
import authMethod from 'consul-ui/search/predicates/auth-method';
import nspace from 'consul-ui/search/predicates/nspace';
import peer from 'consul-ui/search/predicates/peer';
const predicates = {
intention: intention,
@ -30,6 +31,7 @@ const predicates = {
role: role,
policy: policy,
nspace: nspace,
peer: peer,
};
export default class SearchService extends Service {

@ -10,6 +10,7 @@ import role from 'consul-ui/sort/comparators/role';
import policy from 'consul-ui/sort/comparators/policy';
import authMethod from 'consul-ui/sort/comparators/auth-method';
import nspace from 'consul-ui/sort/comparators/nspace';
import peer from 'consul-ui/sort/comparators/peer';
import node from 'consul-ui/sort/comparators/node';
// returns an array of Property:asc, Property:desc etc etc
@ -39,6 +40,7 @@ const comparators = {
role: role(options),
policy: policy(options),
nspace: nspace(options),
peer: peer(options),
node: node(options),
};
export default class SortService extends Service {

@ -0,0 +1,3 @@
export default ({ properties }) => key => {
return properties(['Name'])(key);
};

@ -105,7 +105,6 @@
@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';

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

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

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

@ -1,8 +1,43 @@
<Route @name={{route}} as |route|>
<Watcher @watch={{this.model.loadPeers}} as |w|>
{{did-insert w.fns.start}}
{{will-destroy w.fns.stop}}
</Watcher>
<Route @name={{routeName}} as |route|>
<DataLoader
@src={{
uri '/${partition}/${nspace}/${dc}/peers'
(hash
partition=route.params.partition
nspace=route.params.nspace
dc=route.params.dc
)}}
as |loader|>
<BlockSlot @name="error">
<AppError
@error={{loader.error}}
@login={{route.model.app.login.open}}
/>
</BlockSlot>
<BlockSlot @name="loaded">
{{#let
(hash
value=(or sortBy "Name:asc")
change=(action (mut sortBy) value="target.selected")
)
(hash
searchproperty=(hash
value=(if (not-eq searchproperty undefined)
(split searchproperty ',')
searchProperties
)
change=(action (mut searchproperty) value="target.selectedItems")
default=searchProperties
)
)
loader.data
as |sort filters items|}}
<AppView>
<BlockSlot @name="header">
<h1>
@ -10,52 +45,103 @@
</h1>
</BlockSlot>
<BlockSlot @name="toolbar">
<Peerings::Search @search={{this.filter}} @onSearch={{this.handleSearchChanged}} />
{{#if (gt items.length 0)}}
<Consul::Peer::SearchBar
@search={{search}}
@onsearch={{action (mut search) value="target.value"}}
@sort={{sort}}
@filter={{filters}}
/>
{{/if}}
</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}}
<DataCollection
@type="peer"
@sort={{sort.value}}
@filters={{filters}}
@search={{search}}
@items={{items}}
as |collection|>
<collection.Collection>
<ListCollection
@items={{collection.items}}
@linkable="linkable peer"
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>
<BlockSlot @name="actions" as |Actions|>
{{#if (can 'delete peer' item=item)}}
<Actions as |Action|>
{{#if true}}
<Action data-test-edit-action @href={{href-to 'dc.peers.edit' item.Name}}>
<BlockSlot @name="label">
View
</BlockSlot>
</Action>
{{/if}}
</Actions>
{{/if}}
</BlockSlot>
</ListCollection>
</collection.Collection>
<collection.Empty>
{{!-- 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>
{{#if (gt items.length 0)}}
No peers found
{{else}}
Welcome to Peers
{{/if}}
</h2>
</BlockSlot>
<BlockSlot @name="body">
{{#if (gt items.length 0)}}
No peers where found matching that search, or you may not have access to view the peers you are searching for.
{{else}}
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.
{{/if}}
</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>
</collection.Empty>
</DataCollection>
</BlockSlot>
</AppView>
{{/let}}
</BlockSlot>
</DataLoader>
</Route>

@ -0,0 +1,25 @@
{
"ID": "${fake.random.uuid()}",
"Name": "${location.pathname.get(2)}",
"State": "${fake.helpers.randomize([
'ACTIVE',
'PENDING',
'ESTABLISHING',
'FAILING',
'DELETING',
'TERMINATED',
'UNDEFINED'
])}",
"PeerID": "${fake.random.uuid()}",
"PeerServerName": "${fake.internet.domainName()}",
"PeerServerAddresses": [
${
range(
env('CONSUL_PEER_ADDRESS_COUNT', Math.floor((Math.random() * 10) + 1))
).map(i => `
"${fake.internet.ip()}:${fake.random.number({min: 0, max: 65535})}"
`)}
],
"CreateIndex": 89,
"ModifyIndex": 89
}

@ -0,0 +1,3 @@
{
"PeeringToken": "eyJDQSI6bnVsbCwiU2V"
}

@ -1,65 +1,26 @@
[
${
range(
env('CONSUL_PEER_COUNT', Math.floor((Math.random() * 10) + 1))
).map(i => `
{
"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,
"ID": "${fake.random.uuid()}-${i}",
"Name": "${fake.hacker.noun()}-peer-${i}",
"State": "${fake.helpers.randomize([
'ACTIVE',
'PENDING',
'ESTABLISHING',
'FAILING',
'DELETING',
'TERMINATED',
'UNDEFINED'
])}",
"ImportedServiceCount": ${fake.random.number({min: 0, max: 4000})},
"ExportedServiceCount": ${fake.random.number({min: 0, max: 4000})},
"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
}
`)}
]

@ -123,7 +123,6 @@
"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",

@ -48,6 +48,11 @@ module('Unit | Ability | *', function(hooks) {
ID: bool ? 'not-default' : 'default',
};
break;
case 'peer':
ability.item = {
State: bool ? 'ACTIVE' : 'DELETING',
};
break;
case 'kv':
// TODO: We currently hardcode KVs to always be true
assert.equal(true, ability[`can${perm}`], `Expected ${item}.can${perm} to be true`);

@ -41,6 +41,16 @@
path: '/',
},
},
edit: {
_options: {
path: '/:name'
},
addresses: {
_options: {
path: '/addresses',
},
},
},
},
services: {
_options: { path: '/services' },

@ -2944,13 +2944,6 @@ 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"
@ -6654,27 +6647,6 @@ 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"
@ -6873,22 +6845,6 @@ 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"
@ -7291,16 +7247,6 @@ 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"
@ -7359,17 +7305,6 @@ 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"