ui: Upstream Instance Search and Sort (#9172)

* ui: Add predicate, comparator and necessary files for the search/sort

* Implement search and sort for upstream instance list

* ui: Tweak CSS so its all part of the component

* Remove the old proxy test attribute
This commit is contained in:
John Cowen 2020-11-12 18:45:11 +00:00 committed by GitHub
parent a5bd1ba323
commit f68d989d84
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 239 additions and 65 deletions

View File

@ -1,52 +1,66 @@
<ul data-test-proxy-upstreams class="consul-upstream-instance-list">
{{#each @items as |item|}}
<li>
<div class="header">
<p data-test-destination-name>
{{item.DestinationName}}
</p>
</div>
<div class="detail">
{{#if (env 'CONSUL_NSPACES_ENABLED')}}
{{#if (not-eq item.DestinationType 'prepared_query')}}
<dl class="nspace">
<dt>
<Tooltip>
Namespace
</Tooltip>
</dt>
<dd>
{{or item.DestinationNamespace 'default'}}
</dd>
</dl>
{{/if}}
<div
class="consul-upstream-instance-list"
...attributes
>
{{#if (gt this.items.length 0)}}
<ul>
{{#each this.items as |item|}}
<li>
<div class="header">
<p>
{{item.DestinationName}}
</p>
</div>
<div class="detail">
{{#if (env 'CONSUL_NSPACES_ENABLED')}}
{{#if (not-eq item.DestinationType 'prepared_query')}}
<dl class="nspace">
<dt>
<Tooltip>
Namespace
</Tooltip>
</dt>
<dd>
{{or item.DestinationNamespace 'default'}}
</dd>
</dl>
{{/if}}
{{/if}}
{{#if (and (not-eq item.Datacenter @dc) (not-eq item.Datacenter ""))}}
<dl class="datacenter">
<dt>
<Tooltip>
Datacenter
</Tooltip>
</dt>
<dd>
{{item.Datacenter}}
</dd>
</dl>
{{/if}}
{{#if (gt item.LocalBindPort 0)}}
{{#let (concat (or item.LocalBindAddress '127.0.0.1') ':' item.LocalBindPort) as |combinedAddress|}}
<dl class="local-bind-address">
<dt>
<span>
Address
</span>
</dt>
<dd>
<CopyButton
@value={{combinedAddress}}
@name="Address"
/>
{{combinedAddress}}
</dd>
</dl>
{{/let}}
{{/if}}
</div>
</li>
{{/each}}
</ul>
{{else}}
{{yield api to="empty"}}
{{/if}}
{{#if (and (not-eq item.Datacenter @dc) (not-eq item.Datacenter ""))}}
<dl class="datacenter">
<dt>
<Tooltip>
Datacenter
</Tooltip>
</dt>
<dd>
{{item.Datacenter}}
</dd>
</dl>
{{/if}}
{{#if (gt item.LocalBindPort 0)}}
{{#let (concat (or item.LocalBindAddress '127.0.0.1') ':' item.LocalBindPort) as |combinedAddress|}}
<span>
<CopyButton
@value={{combinedAddress}}
@name="Address"
/>
<span>
{{combinedAddress}}
</span>
</span>
{{/let}}
{{/if}}
</div>
</li>
{{/each}}
</ul>
</div>

View File

@ -0,0 +1,24 @@
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
import { sort } from '@ember/object/computed';
export default class ConsulUpstreamInstanceList extends Component {
@service('sort') sort;
@service('search') search;
@sort('searched', 'comparator') sorted;
get items() {
return this.sorted;
}
get searched() {
if (typeof this.args.search === 'undefined') {
return this.args.items;
}
const predicate = this.search.predicate('upstream-instance');
return this.args.items.filter(predicate(this.args.search));
}
get comparator() {
return this.sort.comparator('upstream-instance')(this.args.sort);
}
}

View File

@ -1,6 +1,14 @@
.consul-upstream-instance-list > li {
@extend %composite-row;
.consul-upstream-instance-list {
li {
@extend %composite-row;
}
dl {
@extend %icon-definition;
}
dl.datacenter dt::before {
@extend %with-user-organization-mask, %as-pseudo;
}
dl.nspace dt::before {
@extend %with-folder-outline-mask, %as-pseudo;
}
}
.consul-upstream-instance-list > ul {
border-top: 1px solid $gray-200;
}

View File

@ -0,0 +1,11 @@
export default (collection, text) => (scope = '.consul-upstream-instance-list') => {
return {
scope,
item: collection('li', {
name: text('.header p'),
nspace: text('.nspace dd'),
datacenter: text('.datacenter dd'),
localAddress: text('.local-address dd'),
}),
};
};

View File

@ -0,0 +1,41 @@
<form
class="consul-upstream-instance-search-bar filter-bar"
...attributes
>
<FreetextFilter
@onsearch={{action @onsearch}}
@value={{@search}}
@placeholder="Search"
/>
<div class="sort">
{{#let (or @sort 'DestinationName:asc') as |sort|}}
<PopoverSelect
class="type-sort"
data-test-sort-control
@position="right"
@onchange={{action @onsort}}
@multiple={{false}}
as |components|>
<BlockSlot @name="selected">
<span>
{{#let (from-entries (array
(array "DestinationName:asc" "A to Z")
(array "DestinationName:desc" "Z to A")
))
as |selectable|}}
{{get selectable sort}}
{{/let}}
</span>
</BlockSlot>
<BlockSlot @name="options">
{{#let components.Optgroup components.Option as |Optgroup Option|}}
<Optgroup @label="Service Name">
<Option @value="DestinationName:asc" @selected={{eq "DestinationName:asc" sort}}>A to Z</Option>
<Option @value="DestinationName:desc" @selected={{eq "DestinationName:desc" sort}}>Z to A</Option>
</Optgroup>
{{/let}}
</BlockSlot>
</PopoverSelect>
{{/let}}
</div>
</form>

View File

@ -0,0 +1,11 @@
import Controller from '@ember/controller';
export default class DcServicesInstanceUpstreamsController extends Controller {
queryParams = {
sortBy: 'sort',
search: {
as: 'filter',
replace: true,
},
};
}

View File

@ -1,6 +1,14 @@
import Route from 'consul-ui/routing/route';
export default class UpstreamsRoute extends Route {
queryParams = {
sortBy: 'sort',
search: {
as: 'filter',
replace: true,
},
};
model() {
const parent = this.routeName
.split('.')

View File

@ -0,0 +1,12 @@
export default () => term => item => {
const lowerTerm = term.toLowerCase();
return Object.entries(item)
.filter(([key, value]) => key !== 'DestinationType')
.some(
([key, value]) =>
value
.toString()
.toLowerCase()
.indexOf(lowerTerm) !== -1
);
};

View File

@ -1,6 +1,8 @@
import Service from '@ember/service';
import intention from 'consul-ui/search/predicates/intention';
import upstreamInstance from 'consul-ui/search/predicates/upstream-instance';
import token from 'consul-ui/search/filters/token';
import policy from 'consul-ui/search/filters/policy';
import role from 'consul-ui/search/filters/role';
@ -29,6 +31,7 @@ const searchables = {
};
const predicates = {
intention: intention(),
['upstream-instance']: upstreamInstance(),
};
export default class SearchService extends Service {
searchable(name) {

View File

@ -1,6 +1,7 @@
import Service from '@ember/service';
import service from 'consul-ui/sort/comparators/service';
import serviceInstance from 'consul-ui/sort/comparators/service-instance';
import upstreamInstance from 'consul-ui/sort/comparators/upstream-instance';
import kv from 'consul-ui/sort/comparators/kv';
import check from 'consul-ui/sort/comparators/check';
import intention from 'consul-ui/sort/comparators/intention';
@ -13,6 +14,7 @@ import node from 'consul-ui/sort/comparators/node';
const comparators = {
service: service(),
serviceInstance: serviceInstance(),
['upstream-instance']: upstreamInstance(),
kv: kv(),
check: check(),
intention: intention(),

View File

@ -0,0 +1,7 @@
const directionify = arr => {
return arr.reduce((prev, item) => prev.concat([`${item}:asc`, `${item}:desc`]), []);
};
export default () => key => {
const comparables = directionify(['DestinationName']);
return [comparables.find(item => item === key) || comparables[0]];
};

View File

@ -56,10 +56,12 @@
@import 'consul-ui/components/modal-dialog';
@import 'consul-ui/components/consul/discovery-chain';
@import 'consul-ui/components/consul/upstream-instance/list';
@import 'consul-ui/components/consul/exposed-path/list';
@import 'consul-ui/components/consul/external-source';
@import 'consul-ui/components/consul/kind';
@import 'consul-ui/components/consul/intention';
@import 'consul-ui/components/role-selector';
@import 'consul-ui/components/topology-metrics';
@import 'consul-ui/components/topology-metrics/popover';

View File

@ -1,6 +1,5 @@
@import './composite-row/index';
.consul-upstream-instance-list > li,
.list-collection > ul > li:not(:first-child) {
@extend %composite-row;
}
@ -33,8 +32,7 @@
.consul-service-instance-list .detail {
overflow-x: visible !important;
}
.consul-intention-permission-list > ul,
.consul-upstream-instance-list > ul {
.consul-intention-permission-list > ul {
border-top: 1px solid $gray-200;
}
.consul-service-instance-list .port dt,

View File

@ -1,7 +1,37 @@
<div class="tab-section">
<div role="tabpanel">
{{#let (or sortBy "DestinationName:asc") as |sort|}}
{{#if (gt proxy.Service.Proxy.Upstreams.length 0)}}
<Consul::UpstreamInstance::List @items={{proxy.Service.Proxy.Upstreams}} @dc={{dc}} @nspace={{nspace}} />
<input type="checkbox" id="toolbar-toggle" />
<Consul::UpstreamInstance::SearchBar
@search={{search}}
@onsearch={{action (mut search) value="target.value"}}
@sort={{sort}}
@onsort={{action (mut sortBy) value="target.selected"}}
/>
<Consul::UpstreamInstance::List
@search={{search}}
@sort={{sort}}
@items={{proxy.Service.Proxy.Upstreams}}
@dc={{dc}}
@nspace={{nspace}}
>
<:empty>
<EmptyState>
<BlockSlot @name="body">
<p>
{{#if search.length}}
No upstreams where found matching that search.
{{else}}
This service has no upstreams.
{{/if}}
</p>
</BlockSlot>
</EmptyState>
</:empty>
</Consul::UpstreamInstance::List>
{{/if}}
{{/let}}
</div>
</div>

View File

@ -40,6 +40,7 @@ import popoverSelectFactory from 'consul-ui/components/popover-select/pageobject
import morePopoverMenuFactory from 'consul-ui/components/more-popover-menu/pageobject';
import tokenListFactory from 'consul-ui/components/token-list/pageobject';
import consulUpstreamInstanceListFactory from 'consul-ui/components/consul/upstream-instance/list/pageobject';
import consulTokenListFactory from 'consul-ui/components/consul/token/list/pageobject';
import consulRoleListFactory from 'consul-ui/components/consul/role/list/pageobject';
import consulPolicyListFactory from 'consul-ui/components/consul/policy/list/pageobject';
@ -94,6 +95,7 @@ const morePopoverMenu = morePopoverMenuFactory(clickable);
const popoverSelect = popoverSelectFactory(clickable, collection);
const emptyState = emptyStateFactory(isPresent);
const consulUpstreamInstanceList = consulUpstreamInstanceListFactory(collection, text);
const consulIntentionList = consulIntentionListFactory(
collection,
clickable,
@ -159,7 +161,9 @@ export default {
service: create(
service(visitable, attribute, collection, text, consulIntentionList, catalogToolbar, tabgroup)
),
instance: create(instance(visitable, attribute, collection, text, tabgroup)),
instance: create(
instance(visitable, alias, attribute, collection, text, tabgroup, consulUpstreamInstanceList)
),
nodes: create(nodes(visitable, text, clickable, attribute, collection, popoverSelect)),
node: create(node(visitable, deletable, clickable, attribute, collection, tabgroup, text)),
kvs: create(kvs(visitable, creatable, consulKvList)),

View File

@ -1,4 +1,4 @@
export default function(visitable, attribute, collection, text, tabs) {
export default function(visitable, alias, attribute, collection, text, tabs, upstreams) {
return {
visit: visitable('/:dc/services/:service/instances/:node/:id'),
externalSource: attribute('data-test-external-source', '[data-test-external-source]', {
@ -6,9 +6,8 @@ export default function(visitable, attribute, collection, text, tabs) {
}),
tabs: tabs('tab', ['health-checks', 'upstreams', 'exposed-paths', 'addresses', 'tags-&-meta']),
checks: collection('[data-test-checks] li'),
upstreams: collection('[data-test-proxy-upstreams] > li', {
name: text('[data-test-destination-name]'),
}),
upstreams: alias('upstreamInstances.item'),
upstreamInstances: upstreams(),
exposedPaths: collection('[data-test-proxy-exposed-paths] > tbody tr', {
combinedAddress: text('[data-test-combined-address]'),
}),