Skip to content

Commit

Permalink
ui: HealthCheck Search/Sort/Filtering (#9314)
Browse files Browse the repository at this point in the history
* Adds model layer changes around HealthChecks

1. Makes a HealthCheck model fragment and uses it in ServiceInstances and
Nodes
2. Manually adds a relationship between a ServiceInstance and its
potential ServiceInstanceProxy
3. Misc changes related to the above such as an Exposed property on
MeshChecks, MeshChecks itself

* Add a potential temporary endpoint to distinguish ProxyServiceInstance

* Fix up Node search bar class

* Add search/sort/filter logic

* Fixup Service default sort key

* Add Healthcheck search/sort/filtering

* Tweak CSS add a default Type of 'Serf' when type is blank

* Fix up tests and new test support

* Add ability to search on Service/Node name depending on where you are

* Fixup CheckID search predicate

* Use computed for DataCollection to use caching

* Alpha sort the Type menu

* Temporary fix for new non-changing style Ember Proxys

* Only special case EventSource proxies
  • Loading branch information
johncowen authored Dec 7, 2020
1 parent 1a3dd32 commit 2061bff
Show file tree
Hide file tree
Showing 29 changed files with 557 additions and 93 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<h3>{{item.Name}}</h3>
</header>
<dl>
{{#if (eq item.ServiceName "")}}
{{#if (eq item.Kind "node")}}
<dt>NodeName</dt>
<dd>{{item.Node}}</dd>
{{else}}
Expand All @@ -24,9 +24,9 @@
</dl>
<dl>
<dt>Type</dt>
<dd>
{{item.Type}}
{{#if (and @exposed (contains item.Type (array 'http' 'grpc')))}}
<dd data-health-check-type>
{{or item.Type 'serf'}}
{{#if item.Exposed}}
<em
data-test-exposed="true"
{{tooltip "Expose.checks is set to true, so all registered HTTP and gRPC check paths are exposed through Envoy for the Consul agent."}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
width: 50%;
}
%healthcheck-output dl:last-of-type {
margin-top: 1em;
margin-bottom: 0;
}
%healthcheck-output dl:last-of-type dt {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export default (collection, text) => (scope = '.consul-health-check-list') => {
return {
scope,
item: collection('li', {
name: text('header h3'),
type: text('[data-health-check-type]'),
exposed: text('[data-test-exposed]'),
}),
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
<form
class="consul-health-check-search-bar filter-bar"
...attributes
>
<div class="search">
<FreetextFilter
@onsearch={{action @onsearch}}
@value={{@search}}
@placeholder="Search"
>
<PopoverSelect
class="type-search-properties"
@position="right"
@onchange={{action @onfilter.searchproperty}}
@multiple={{true}}
as |components|>
<BlockSlot @name="selected">
<span>
Search across
</span>
</BlockSlot>
<BlockSlot @name="options">
{{#let components.Optgroup components.Option as |Optgroup Option|}}
{{#each @searchproperties as |prop|}}
<Option @value={{prop}} @selected={{contains prop @filter.searchproperties}}>{{prop}}</Option>
{{/each}}
{{/let}}
</BlockSlot>
</PopoverSelect>
</FreetextFilter>
</div>
<div class="filters">
<PopoverSelect
class="type-status"
@position="left"
@onchange={{action @onfilter.status}}
@multiple={{true}}
as |components|>
<BlockSlot @name="selected">
<span>
Health Status
</span>
</BlockSlot>
<BlockSlot @name="options">
{{#let components.Optgroup components.Option as |Optgroup Option|}}
<Option class="value-passing" @value="passing" @selected={{contains 'passing' @filter.statuses}}>Passing</Option>
<Option class="value-warning" @value="warning" @selected={{contains 'warning' @filter.statuses}}>Warning</Option>
<Option class="value-critical" @value="critical" @selected={{contains 'critical' @filter.statuses}}>Failing</Option>
<Option class="value-empty" @value="empty" @selected={{contains 'empty' @filter.statuses}}>No checks</Option>
{{/let}}
</BlockSlot>
</PopoverSelect>

<PopoverSelect
class="type-kind"
@position="left"
@onchange={{action @onfilter.kind}}
@multiple={{true}}
as |components|>
<BlockSlot @name="selected">
<span>
Kind
</span>
</BlockSlot>
<BlockSlot @name="options">
{{#let components.Optgroup components.Option as |Optgroup Option|}}
<Option @value="service" @selected={{contains 'service' @filter.kinds}}>Service Check</Option>
<Option @value="node" @selected={{contains 'node' @filter.kinds}}>Node Check</Option>
{{/let}}
</BlockSlot>
</PopoverSelect>

<PopoverSelect
class="type-check"
@position="left"
@onchange={{action @onfilter.check}}
@multiple={{true}}
as |components|>
<BlockSlot @name="selected">
<span>
Type
</span>
</BlockSlot>
<BlockSlot @name="options">
{{#let components.Optgroup components.Option as |Optgroup Option|}}
<Option @value="alias" @selected={{contains 'alias' @filter.checks}}>alias</Option>
<Option @value="docker" @selected={{contains 'docker' @filter.checks}}>Docker</Option>
<Option @value="grpc" @selected={{contains 'grpc' @filter.checks}}>gRPC</Option>
<Option @value="http" @selected={{contains 'http' @filter.checks}}>HTTP</Option>
<Option @value="serf" @selected={{contains 'serf' @filter.checks}}>Serf</Option>
<Option @value="tcp" @selected={{contains 'tcp' @filter.checks}}>TCP</Option>
<Option @value="ttl" @selected={{contains 'ttl' @filter.checks}}>TTL</Option>
{{/let}}
</BlockSlot>
</PopoverSelect>

</div>
<div class="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 "Name:asc" "A to Z")
(array "Name:desc" "Z to A")
(array "Status:asc" "Unhealthy to Healthy")
(array "Status:desc" "Healthy to Unhealthy")
(array "Kind:asc" "Service to Node")
(array "Kind:desc" "Node to Service")
))
as |selectable|
}}
{{get selectable @sort}}
{{/let}}
</span>
</BlockSlot>
<BlockSlot @name="options">
{{#let components.Optgroup components.Option as |Optgroup Option|}}
<Optgroup @label="Health Status">
<Option @value="Status:asc" @selected={{eq "Status:asc" @sort}}>Unhealthy to Healthy</Option>
<Option @value="Status:desc" @selected={{eq "Status:desc" @sort}}>Healthy to Unhealthy</Option>
</Optgroup>
<Optgroup @label="Check Name">
<Option @value="Name:asc" @selected={{eq "Name:asc" @sort}}>A to Z</Option>
<Option @value="Name:desc" @selected={{eq "Name:desc" @sort}}>Z to A</Option>
</Optgroup>
<Optgroup @label="Check Type">
<Option @value="Kind:asc" @selected={{eq "Kind:asc" @sort}}>Service to Node</Option>
<Option @value="Kind:desc" @selected={{eq "Kind:desc" @sort}}>Node to Service</Option>
</Optgroup>
{{/let}}
</BlockSlot>
</PopoverSelect>
</div>
</form>
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<form
class="consul-node-list filter-bar"
class="consul-node-search-bar filter-bar"
...attributes
>
<div class="search">
Expand Down
22 changes: 19 additions & 3 deletions ui/packages/consul-ui/app/components/data-collection/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
import { computed } from '@ember/object';
import { sort } from '@ember/object/computed';
import { defineProperty } from '@ember/object';

Expand All @@ -12,6 +13,18 @@ export default class DataCollectionComponent extends Component {
return this.args.type;
}

@computed('args.items', 'args.items.content')
get content() {
// TODO: Temporary little hack to ensure we detect DataSource proxy
// objects but not any other special Ember Proxy object like ember-data
// things. Remove this once we no longer need the Proxies
if (this.args.items.dispatchEvent === 'function') {
return this.args.items.content;
}
return this.args.items;
}

@computed('comparator', 'searched')
get items() {
// the ember sort computed accepts either:
// 1. The name of a property (as a string) returning an array properties to sort by
Expand All @@ -24,6 +37,7 @@ export default class DataCollectionComponent extends Component {
return this.sorted;
}

@computed('type', 'filtered', 'args.filters.searchproperties', 'args.search')
get searched() {
if (typeof this.args.search === 'undefined') {
return this.filtered;
Expand All @@ -36,17 +50,19 @@ export default class DataCollectionComponent extends Component {
return this.filtered.filter(predicate(this.args.search, options));
}

@computed('type', 'content', 'args.filters')
get filtered() {
if (typeof this.args.filters === 'undefined') {
return this.args.items;
return this.content;
}
const predicate = this.filter.predicate(this.type);
if (typeof predicate === 'undefined') {
return this.args.items;
return this.content;
}
return this.args.items.filter(predicate(this.args.filters));
return this.content.filter(predicate(this.args.filters));
}

@computed('type', 'args.sort')
get comparator() {
if (typeof this.args.sort === 'undefined') {
return [];
Expand Down
21 changes: 21 additions & 0 deletions ui/packages/consul-ui/app/filter/predicates/health-check.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export default {
statuses: {
passing: (item, value) => item.Status === value,
warning: (item, value) => item.Status === value,
critical: (item, value) => item.Status === value,
},
kinds: {
service: (item, value) => item.Kind === value,
node: (item, value) => item.Kind === value,
},
checks: {
serf: (item, value) => item.Type === '',
script: (item, value) => item.Type === value,
http: (item, value) => item.Type === value,
tcp: (item, value) => item.Type === value,
ttl: (item, value) => item.Type === value,
docker: (item, value) => item.Type === value,
grpc: (item, value) => item.Type === value,
alias: (item, value) => item.Type === value,
},
};
41 changes: 41 additions & 0 deletions ui/packages/consul-ui/app/models/health-check.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import Fragment from 'ember-data-model-fragments/fragment';
import { array } from 'ember-data-model-fragments/attributes';
import { attr } from '@ember-data/model';
import { computed } from '@ember/object';

export const schema = {
Status: {
allowedValues: ['passing', 'warning', 'critical'],
},
Type: {
allowedValues: ['', 'script', 'http', 'tcp', 'ttl', 'docker', 'grpc', 'alias'],
},
};

export default class HealthCheck extends Fragment {
@attr('string') Name;
@attr('string') CheckID;
@attr('string') Type;
@attr('string') Status;
@attr('string') Notes;
@attr('string') Output;
@attr('string') ServiceName;
@attr('string') ServiceID;
@attr('string') Node;
@array('string') ServiceTags;
@attr() Definition; // {}

// Exposed is only set correct if this Check is accessed via instance.MeshChecks
// essentially this is a lazy MeshHealthCheckModel
@attr('boolean') Exposed;

@computed('ServiceID')
get Kind() {
return this.ServiceID === '' ? 'node' : 'service';
}

@computed('Type')
get Exposable() {
return ['http', 'grpc'].includes(this.Type);
}
}
3 changes: 2 additions & 1 deletion ui/packages/consul-ui/app/models/node.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Model, { attr } from '@ember-data/model';
import { computed } from '@ember/object';
import { fragmentArray } from 'ember-data-model-fragments/attributes';

export const PRIMARY_KEY = 'uid';
export const SLUG_KEY = 'ID';
Expand All @@ -18,7 +19,7 @@ export default class Node extends Model {
@attr() Meta; // {}
@attr() TaggedAddresses; // {lan, wan}
@attr() Services; // ServiceInstances[]
@attr() Checks; // Checks[]
@fragmentArray('health-check') Checks;

@computed('Checks.[]', 'ChecksCritical', 'ChecksPassing', 'ChecksWarning')
get Status() {
Expand Down
3 changes: 2 additions & 1 deletion ui/packages/consul-ui/app/models/proxy.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import Model, { attr } from '@ember-data/model';
import ServiceInstanceModel from './service-instance';

export const PRIMARY_KEY = 'uid';
export const SLUG_KEY = 'Node,ServiceID';

// TODO: This should be changed to ProxyInstance
export default class Proxy extends Model {
export default class Proxy extends ServiceInstanceModel {
@attr('string') uid;
@attr('string') ID;

Expand Down
36 changes: 32 additions & 4 deletions ui/packages/consul-ui/app/models/service-instance.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Model, { attr, belongsTo } from '@ember-data/model';
import { computed } from '@ember/object';
import { fragmentArray } from 'ember-data-model-fragments/attributes';
import { computed, get, set } from '@ember/object';
import { or, filter, alias } from '@ember/object/computed';

export const PRIMARY_KEY = 'uid';
Expand All @@ -15,7 +16,7 @@ export default class ServiceInstance extends Model {
@attr() Proxy;
@attr() Node;
@attr() Service;
@attr() Checks;
@fragmentArray('health-check') Checks;
@attr('number') SyncTime;
@attr() meta;

Expand All @@ -29,8 +30,35 @@ export default class ServiceInstance extends Model {
@alias('Service.Tags') Tags;
@alias('Service.Meta') Meta;
@alias('Service.Namespace') Namespace;
@filter('Checks.[]', (item, i, arr) => item.ServiceID !== '') ServiceChecks;
@filter('Checks.[]', (item, i, arr) => item.ServiceID === '') NodeChecks;

@filter('[email protected]', (item, i, arr) => item.Kind === 'service') ServiceChecks;
@filter('[email protected]', (item, i, arr) => item.Kind === 'node') NodeChecks;

// MeshChecks are a concatenation of Checks for the Instance and Checks for
// the ProxyInstance. Checks is an ember-data-model-fragment, so we can't just
// concat it, we have to loop through all the items in order to merge
@computed('Checks', 'ProxyInstance.Checks', 'ProxyInstance.ServiceProxy.Expose.Checks')
get MeshChecks() {
return (get(this, 'Checks') || [])
.map(item => {
set(
item,
'Exposed',
get(this, 'ProxyInstance.ServiceProxy.Expose.Checks') && get(item, 'Exposable')
);
return item;
})
.concat(
(get(this, 'ProxyInstance.Checks') || []).map(item => {
set(
item,
'Exposed',
get(this, 'ProxyInstance.ServiceProxy.Expose.Checks') && get(item, 'Exposable')
);
return item;
})
);
}

@computed('Service.Meta')
get ExternalSources() {
Expand Down
Loading

0 comments on commit 2061bff

Please sign in to comment.