ui: Add peer searching and sorting (#13634)

* 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 (#13648)

* 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 (#13651)
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

View File

@ -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);
}
}

View File

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

View File

@ -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>

View File

@ -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)
---

View File

@ -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>

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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,
queryParams = {
sortBy: 'sort',
searchproperty: {
as: 'searchproperty',
empty: [['Name']],
},
search: {
as: 'filter',
replace: true,
},
};
});
}
@action loadPeers() {
return this.store.findAll('peer');
}
}

View File

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

View File

@ -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);
}
}

View File

@ -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}`
)
};
});
}
}

View File

@ -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 {

View File

@ -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 {

View File

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

View File

@ -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';

View File

@ -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>

View File

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

View File

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

View File

@ -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,11 +45,32 @@
</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|>
<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>
@ -25,20 +81,46 @@
<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>
{{else}}
</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}}>
<EmptyState
@login={{route.model.app.login.open}}
>
<BlockSlot @name="header">
<h2>Welcome to Peers</h2>
<h2>
{{#if (gt items.length 0)}}
No peers found
{{else}}
Welcome to Peers
{{/if}}
</h2>
</BlockSlot>
<BlockSlot @name="body">
<p>
{{#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.
</p>
{{/if}}
</BlockSlot>
<BlockSlot @name="actions">
<li class="docs-link">
@ -54,8 +136,12 @@
</li>
</BlockSlot>
</EmptyState>
{{/if}}
</collection.Empty>
</DataCollection>
</BlockSlot>
</AppView>
{{/let}}
</BlockSlot>
</DataLoader>
</Route>

View File

@ -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
}

View File

@ -0,0 +1 @@
{}

View File

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

View File

@ -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
}
`)}
]

View File

@ -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",

View File

@ -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`);

View File

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

View File

@ -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"