ui: HealthCheck Search/Sort/Filtering (#9314)
* 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
This commit is contained in:
parent
f727023b18
commit
23adad684b
|
@ -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}}
|
||||
|
@ -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."}}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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]'),
|
||||
}),
|
||||
};
|
||||
};
|
|
@ -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>
|
|
@ -1,5 +1,5 @@
|
|||
<form
|
||||
class="consul-node-list filter-bar"
|
||||
class="consul-node-search-bar filter-bar"
|
||||
...attributes
|
||||
>
|
||||
<div class="search">
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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 [];
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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('Checks.@each.Kind', (item, i, arr) => item.Kind === 'service') ServiceChecks;
|
||||
@filter('Checks.@each.Kind', (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() {
|
||||
|
|
|
@ -1,12 +1,30 @@
|
|||
import Route from 'consul-ui/routing/route';
|
||||
|
||||
export default class HealthchecksRoute extends Route {
|
||||
queryParams = {
|
||||
sortBy: 'sort',
|
||||
status: 'status',
|
||||
kind: 'kind',
|
||||
check: 'check',
|
||||
searchproperty: {
|
||||
as: 'searchproperty',
|
||||
empty: [['Name', 'Service', 'CheckID', 'Notes', 'Output', 'ServiceTags']],
|
||||
},
|
||||
search: {
|
||||
as: 'filter',
|
||||
replace: true,
|
||||
},
|
||||
};
|
||||
|
||||
model() {
|
||||
const parent = this.routeName
|
||||
.split('.')
|
||||
.slice(0, -1)
|
||||
.join('.');
|
||||
return this.modelFor(parent);
|
||||
return {
|
||||
...this.modelFor(parent),
|
||||
searchProperties: this.queryParams.searchproperty.empty[0],
|
||||
};
|
||||
}
|
||||
|
||||
setupController(controller, model) {
|
||||
|
|
|
@ -29,7 +29,7 @@ export default class InstanceRoute extends Route {
|
|||
// the proxy itself is just a normal service model
|
||||
proxy = await this.data.source(
|
||||
uri =>
|
||||
uri`/${nspace}/${dc}/service-instance/${proxyParams.id}/${proxyParams.node}/${proxyParams.name}`
|
||||
uri`/${nspace}/${dc}/proxy-service-instance/${proxyParams.id}/${proxyParams.node}/${proxyParams.name}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,30 @@
|
|||
import Route from 'consul-ui/routing/route';
|
||||
|
||||
export default class HealthchecksRoute extends Route {
|
||||
queryParams = {
|
||||
sortBy: 'sort',
|
||||
status: 'status',
|
||||
kind: 'kind',
|
||||
check: 'check',
|
||||
searchproperty: {
|
||||
as: 'searchproperty',
|
||||
empty: [['Name', 'Node', 'CheckID', 'Notes', 'Output', 'ServiceTags']],
|
||||
},
|
||||
search: {
|
||||
as: 'filter',
|
||||
replace: true,
|
||||
},
|
||||
};
|
||||
|
||||
model() {
|
||||
const parent = this.routeName
|
||||
.split('.')
|
||||
.slice(0, -1)
|
||||
.join('.');
|
||||
return this.modelFor(parent);
|
||||
return {
|
||||
...this.modelFor(parent),
|
||||
searchProperties: this.queryParams.searchproperty.empty[0],
|
||||
};
|
||||
}
|
||||
|
||||
setupController(controller, model) {
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
const asArray = function(arr) {
|
||||
return Array.isArray(arr) ? arr : arr.toArray();
|
||||
};
|
||||
export default {
|
||||
Name: (item, value) => {
|
||||
return item.Name.toLowerCase().indexOf(value.toLowerCase()) !== -1;
|
||||
},
|
||||
Node: (item, value) => {
|
||||
return item.Node.toLowerCase().indexOf(value.toLowerCase()) !== -1;
|
||||
},
|
||||
Service: (item, value) => {
|
||||
const lower = value.toLowerCase();
|
||||
return (
|
||||
item.ServiceName.toLowerCase().indexOf(lower) !== -1 ||
|
||||
item.ServiceID.toLowerCase().indexOf(lower) !== -1
|
||||
);
|
||||
},
|
||||
CheckID: (item, value) => (item.CheckID || '').toLowerCase().indexOf(value.toLowerCase()) !== -1,
|
||||
Notes: (item, value) =>
|
||||
item.Notes.toString()
|
||||
.toLowerCase()
|
||||
.indexOf(value.toLowerCase()) !== -1,
|
||||
Output: (item, value) =>
|
||||
item.Output.toString()
|
||||
.toLowerCase()
|
||||
.indexOf(value.toLowerCase()) !== -1,
|
||||
ServiceTags: (item, value) => {
|
||||
return asArray(item.ServiceTags || []).some(
|
||||
item => item.toLowerCase().indexOf(value.toLowerCase()) !== -1
|
||||
);
|
||||
},
|
||||
};
|
|
@ -26,6 +26,9 @@ export default class HttpService extends Service {
|
|||
@service('repository/service-instance')
|
||||
'service-instance';
|
||||
|
||||
@service('repository/service-instance')
|
||||
'proxy-service-instance';
|
||||
|
||||
@service('repository/service-instance')
|
||||
'service-instances';
|
||||
|
||||
|
@ -192,6 +195,11 @@ export default class HttpService extends Service {
|
|||
find = configuration =>
|
||||
repo.findBySlug(rest[0], rest[1], rest[2], dc, nspace, configuration);
|
||||
break;
|
||||
case 'proxy-service-instance':
|
||||
// id, node, service
|
||||
find = configuration =>
|
||||
repo.findProxyBySlug(rest[0], rest[1], rest[2], dc, nspace, configuration);
|
||||
break;
|
||||
case 'proxy-instance':
|
||||
// id, node, service
|
||||
find = configuration =>
|
||||
|
|
|
@ -4,6 +4,7 @@ import { andOr } from 'consul-ui/utils/filter';
|
|||
import acl from 'consul-ui/filter/predicates/acl';
|
||||
import service from 'consul-ui/filter/predicates/service';
|
||||
import serviceInstance from 'consul-ui/filter/predicates/service-instance';
|
||||
import healthCheck from 'consul-ui/filter/predicates/health-check';
|
||||
import node from 'consul-ui/filter/predicates/node';
|
||||
import kv from 'consul-ui/filter/predicates/kv';
|
||||
import intention from 'consul-ui/filter/predicates/intention';
|
||||
|
@ -14,6 +15,7 @@ const predicates = {
|
|||
acl: andOr(acl),
|
||||
service: andOr(service),
|
||||
['service-instance']: andOr(serviceInstance),
|
||||
['health-check']: andOr(healthCheck),
|
||||
node: andOr(node),
|
||||
kv: andOr(kv),
|
||||
intention: andOr(intention),
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
import RepositoryService from 'consul-ui/services/repository';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { set, get } from '@ember/object';
|
||||
|
||||
const modelName = 'service-instance';
|
||||
export default class ServiceInstanceService extends RepositoryService {
|
||||
@service('repository/proxy') proxyRepo;
|
||||
getModelName() {
|
||||
return modelName;
|
||||
}
|
||||
|
||||
findByService(slug, dc, nspace, configuration = {}) {
|
||||
async findByService(slug, dc, nspace, configuration = {}) {
|
||||
const query = {
|
||||
dc: dc,
|
||||
ns: nspace,
|
||||
|
@ -18,7 +22,7 @@ export default class ServiceInstanceService extends RepositoryService {
|
|||
return this.store.query(this.getModelName(), query);
|
||||
}
|
||||
|
||||
findBySlug(serviceId, node, service, dc, nspace, configuration = {}) {
|
||||
async findBySlug(serviceId, node, service, dc, nspace, configuration = {}) {
|
||||
const query = {
|
||||
dc: dc,
|
||||
ns: nspace,
|
||||
|
@ -32,4 +36,27 @@ export default class ServiceInstanceService extends RepositoryService {
|
|||
}
|
||||
return this.store.queryRecord(this.getModelName(), query);
|
||||
}
|
||||
|
||||
async findProxyBySlug(serviceId, node, service, dc, nspace, configuration = {}) {
|
||||
const instance = await this.findBySlug(...arguments);
|
||||
let proxy = this.store.peekRecord('proxy', instance.uid);
|
||||
// if(typeof proxy === 'undefined') {
|
||||
// await proxyRepo.create({})
|
||||
// }
|
||||
|
||||
// Copy over all the things to the ProxyServiceInstance
|
||||
['Service', 'Node'].forEach(prop => {
|
||||
set(proxy, prop, instance[prop]);
|
||||
});
|
||||
['Checks'].forEach(prop => {
|
||||
instance[prop].forEach(item => {
|
||||
if (typeof item !== 'undefined') {
|
||||
proxy[prop].addFragment(item.copy());
|
||||
}
|
||||
});
|
||||
});
|
||||
// delete the ServiceInstance record as we now have a ProxyServiceInstance
|
||||
instance.unloadRecord();
|
||||
return proxy;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import setHelpers from 'mnemonist/set';
|
|||
import intention from 'consul-ui/search/predicates/intention';
|
||||
import upstreamInstance from 'consul-ui/search/predicates/upstream-instance';
|
||||
import serviceInstance from 'consul-ui/search/predicates/service-instance';
|
||||
import healthCheck from 'consul-ui/search/predicates/health-check';
|
||||
import acl from 'consul-ui/search/predicates/acl';
|
||||
import service from 'consul-ui/search/predicates/service';
|
||||
import node from 'consul-ui/search/predicates/node';
|
||||
|
@ -47,6 +48,7 @@ const predicates = {
|
|||
service: search(service),
|
||||
['service-instance']: search(serviceInstance),
|
||||
['upstream-instance']: upstreamInstance(),
|
||||
['health-check']: search(healthCheck),
|
||||
node: search(node),
|
||||
kv: search(kv),
|
||||
acl: search(acl),
|
||||
|
|
|
@ -4,7 +4,7 @@ import serviceInstance from 'consul-ui/sort/comparators/service-instance';
|
|||
import upstreamInstance from 'consul-ui/sort/comparators/upstream-instance';
|
||||
import acl from 'consul-ui/sort/comparators/acl';
|
||||
import kv from 'consul-ui/sort/comparators/kv';
|
||||
import check from 'consul-ui/sort/comparators/check';
|
||||
import healthCheck from 'consul-ui/sort/comparators/health-check';
|
||||
import intention from 'consul-ui/sort/comparators/intention';
|
||||
import token from 'consul-ui/sort/comparators/token';
|
||||
import role from 'consul-ui/sort/comparators/role';
|
||||
|
@ -31,9 +31,9 @@ const comparators = {
|
|||
service: service(options),
|
||||
['service-instance']: serviceInstance(options),
|
||||
['upstream-instance']: upstreamInstance(options),
|
||||
['health-check']: healthCheck(options),
|
||||
acl: acl(options),
|
||||
kv: kv(options),
|
||||
check: check(options),
|
||||
intention: intention(options),
|
||||
token: token(options),
|
||||
role: role(options),
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export default () => key => {
|
||||
export default ({ properties }) => (key = 'Status:asc') => {
|
||||
if (key.startsWith('Status:')) {
|
||||
return function(itemA, itemB) {
|
||||
const [, dir] = key.split(':');
|
||||
|
@ -38,5 +38,5 @@ export default () => key => {
|
|||
return 0;
|
||||
};
|
||||
}
|
||||
return key;
|
||||
return properties(['Name', 'Kind'])(key);
|
||||
};
|
|
@ -1,4 +1,4 @@
|
|||
export default ({ properties }) => (key = 'Name:asc') => {
|
||||
export default ({ properties }) => (key = 'Status:asc') => {
|
||||
if (key.startsWith('Status:')) {
|
||||
return function(serviceA, serviceB) {
|
||||
const [, dir] = key.split(':');
|
||||
|
|
|
@ -9,7 +9,8 @@ html[data-route$='edit'] .app-view > header + div > *:first-child {
|
|||
/* if it is a filter bar and the thing after the filter bar is a p then it also */
|
||||
/* needs a top margun :S */
|
||||
%app-view-content [role='tabpanel'] > *:first-child:not(.filter-bar):not(table),
|
||||
%app-view-content [role='tabpanel'] > .filter-bar + p {
|
||||
%app-view-content [role='tabpanel'] > .filter-bar + p,
|
||||
%app-view-content [role='tabpanel'] .consul-health-check-list {
|
||||
margin-top: 1.25em;
|
||||
}
|
||||
.consul-upstream-instance-list,
|
||||
|
|
|
@ -1,15 +1,58 @@
|
|||
<div id="health-checks" class="tab-section">
|
||||
<div role="tabpanel">
|
||||
{{#if (gt item.Checks.length 0) }}
|
||||
<Consul::HealthCheck::List @items={{sort-by (comparator 'check' 'Status:asc') item.Checks}} />
|
||||
{{else}}
|
||||
<EmptyState>
|
||||
<BlockSlot @name="body">
|
||||
<p>
|
||||
This node has no health checks.
|
||||
</p>
|
||||
</BlockSlot>
|
||||
</EmptyState>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{#let (hash
|
||||
statuses=(if status (split status ',') undefined)
|
||||
kinds=(if kind (split kind ',') undefined)
|
||||
checks=(if check (split check ',') undefined)
|
||||
searchproperties=(if (not-eq searchproperty undefined)
|
||||
(split searchproperty ',')
|
||||
searchProperties
|
||||
)
|
||||
) as |filters|}}
|
||||
{{#let (or sortBy "Status:asc") as |sort|}}
|
||||
<div class="tab-section">
|
||||
<div role="tabpanel">
|
||||
{{#if (gt item.Checks.length 0) }}
|
||||
<input type="checkbox" id="toolbar-toggle" />
|
||||
<Consul::HealthCheck::SearchBar
|
||||
|
||||
@search={{search}}
|
||||
@onsearch={{action (mut search) value="target.value"}}
|
||||
@searchproperties={{searchProperties}}
|
||||
|
||||
@sort={{sort}}
|
||||
@onsort={{action (mut sortBy) value="target.selected"}}
|
||||
|
||||
@filter={{filters}}
|
||||
@onfilter={{hash
|
||||
searchproperty=(action (mut searchproperty) value="target.selectedItems")
|
||||
status=(action (mut status) value="target.selectedItems")
|
||||
kind=(action (mut kind) value="target.selectedItems")
|
||||
check=(action (mut check) value="target.selectedItems")
|
||||
}}
|
||||
/>
|
||||
{{/if}}
|
||||
<DataCollection
|
||||
@type="health-check"
|
||||
@sort={{sort}}
|
||||
@filters={{filters}}
|
||||
@search={{search}}
|
||||
@items={{item.Checks}}
|
||||
as |collection|>
|
||||
<collection.Collection>
|
||||
<Consul::HealthCheck::List
|
||||
@items={{collection.items}}
|
||||
/>
|
||||
</collection.Collection>
|
||||
<collection.Empty>
|
||||
<EmptyState>
|
||||
<BlockSlot @name="body">
|
||||
<p>
|
||||
This node has no health checks{{#if (gt item.Checks.length 0)}} matching that search{{/if}}.
|
||||
</p>
|
||||
</BlockSlot>
|
||||
</EmptyState>
|
||||
</collection.Empty>
|
||||
</DataCollection>
|
||||
</div>
|
||||
</div>
|
||||
{{/let}}
|
||||
{{/let}}
|
|
@ -1,22 +1,60 @@
|
|||
{{#let (hash
|
||||
statuses=(if status (split status ',') undefined)
|
||||
kinds=(if kind (split kind ',') undefined)
|
||||
checks=(if check (split check ',') undefined)
|
||||
searchproperties=(if (not-eq searchproperty undefined)
|
||||
(split searchproperty ',')
|
||||
searchProperties
|
||||
)
|
||||
) as |filters|}}
|
||||
{{#let (or sortBy "Status:asc") as |sort|}}
|
||||
<div class="tab-section">
|
||||
<div role="tabpanel">
|
||||
{{#let (append item.Checks (or proxy.Checks (array))) as |checks|}}
|
||||
{{#if (gt checks.length 0) }}
|
||||
<section data-test-checks>
|
||||
<Consul::HealthCheck::List
|
||||
@items={{sort-by (comparator 'check' 'Status:asc') checks}}
|
||||
@exposed={{proxyMeta.ServiceProxy.Expose.Checks}}
|
||||
/>
|
||||
</section>
|
||||
{{else}}
|
||||
<EmptyState>
|
||||
<BlockSlot @name="body">
|
||||
<p>
|
||||
This instance has no health checks.
|
||||
</p>
|
||||
</BlockSlot>
|
||||
</EmptyState>
|
||||
|
||||
{{#if (gt item.MeshChecks.length 0) }}
|
||||
<input type="checkbox" id="toolbar-toggle" />
|
||||
<Consul::HealthCheck::SearchBar
|
||||
@search={{search}}
|
||||
@onsearch={{action (mut search) value="target.value"}}
|
||||
@searchproperties={{searchProperties}}
|
||||
|
||||
@sort={{sort}}
|
||||
@onsort={{action (mut sortBy) value="target.selected"}}
|
||||
|
||||
@filter={{filters}}
|
||||
@onfilter={{hash
|
||||
searchproperty=(action (mut searchproperty) value="target.selectedItems")
|
||||
status=(action (mut status) value="target.selectedItems")
|
||||
kind=(action (mut kind) value="target.selectedItems")
|
||||
check=(action (mut check) value="target.selectedItems")
|
||||
}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{/let}}
|
||||
|
||||
<DataCollection
|
||||
@type="health-check"
|
||||
@sort={{sort}}
|
||||
@filters={{filters}}
|
||||
@search={{search}}
|
||||
@items={{item.MeshChecks}}
|
||||
as |collection|>
|
||||
<collection.Collection>
|
||||
<Consul::HealthCheck::List
|
||||
@items={{collection.items}}
|
||||
/>
|
||||
</collection.Collection>
|
||||
<collection.Empty>
|
||||
<EmptyState>
|
||||
<BlockSlot @name="body">
|
||||
<p>
|
||||
This instance has no health checks{{#if (gt item.MeshChecks.length 0)}} matching that search{{/if}}.
|
||||
</p>
|
||||
</BlockSlot>
|
||||
</EmptyState>
|
||||
</collection.Empty>
|
||||
</DataCollection>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{{/let}}
|
||||
{{/let}}
|
||||
|
|
|
@ -28,18 +28,10 @@ test('findByDatacenter returns the correct data for list endpoint', function(ass
|
|||
return service.findAllByDatacenter(dc);
|
||||
},
|
||||
function performAssertion(actual, expected) {
|
||||
assert.deepEqual(
|
||||
actual,
|
||||
expected(function(payload) {
|
||||
return payload.map(item =>
|
||||
Object.assign({}, item, {
|
||||
SyncTime: now,
|
||||
Datacenter: dc,
|
||||
uid: `["${nspace}","${dc}","${item.ID}"]`,
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
actual.forEach(item => {
|
||||
assert.equal(item.uid, `["${nspace}","${dc}","${item.ID}"]`);
|
||||
assert.equal(item.Datacenter, dc);
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
@ -55,22 +47,8 @@ test('findBySlug returns the correct data for item endpoint', function(assert) {
|
|||
return service.findBySlug(id, dc);
|
||||
},
|
||||
function(actual, expected) {
|
||||
assert.deepEqual(
|
||||
actual,
|
||||
expected(function(payload) {
|
||||
const item = payload;
|
||||
return Object.assign({}, item, {
|
||||
Datacenter: dc,
|
||||
uid: `["${nspace}","${dc}","${item.ID}"]`,
|
||||
meta: {
|
||||
cacheControl: undefined,
|
||||
cursor: undefined,
|
||||
dc: dc,
|
||||
nspace: nspace,
|
||||
},
|
||||
});
|
||||
})
|
||||
);
|
||||
assert.equal(actual.uid, `["${nspace}","${dc}","${actual.ID}"]`);
|
||||
assert.equal(actual.Datacenter, dc);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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 consulHealthCheckListFactory from 'consul-ui/components/consul/health-check/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';
|
||||
|
@ -95,6 +96,7 @@ const morePopoverMenu = morePopoverMenuFactory(clickable);
|
|||
const popoverSelect = popoverSelectFactory(clickable, collection);
|
||||
const emptyState = emptyStateFactory(isPresent);
|
||||
|
||||
const consulHealthCheckList = consulHealthCheckListFactory(collection, text);
|
||||
const consulUpstreamInstanceList = consulUpstreamInstanceListFactory(collection, text);
|
||||
const consulIntentionList = consulIntentionListFactory(
|
||||
collection,
|
||||
|
@ -162,10 +164,30 @@ export default {
|
|||
service(visitable, attribute, collection, text, consulIntentionList, catalogToolbar, tabgroup)
|
||||
),
|
||||
instance: create(
|
||||
instance(visitable, alias, attribute, collection, text, tabgroup, consulUpstreamInstanceList)
|
||||
instance(
|
||||
visitable,
|
||||
alias,
|
||||
attribute,
|
||||
collection,
|
||||
text,
|
||||
tabgroup,
|
||||
consulUpstreamInstanceList,
|
||||
consulHealthCheckList
|
||||
)
|
||||
),
|
||||
nodes: create(nodes(visitable, text, clickable, attribute, collection, popoverSelect)),
|
||||
node: create(node(visitable, deletable, clickable, attribute, collection, tabgroup, text)),
|
||||
node: create(
|
||||
node(
|
||||
visitable,
|
||||
deletable,
|
||||
clickable,
|
||||
attribute,
|
||||
collection,
|
||||
tabgroup,
|
||||
text,
|
||||
consulHealthCheckList
|
||||
)
|
||||
),
|
||||
kvs: create(kvs(visitable, creatable, consulKvList)),
|
||||
kv: create(kv(visitable, attribute, submitable, deletable, cancelable, clickable)),
|
||||
acls: create(acls(visitable, deletable, creatable, clickable, attribute, collection, aclFilter)),
|
||||
|
|
|
@ -1,4 +1,13 @@
|
|||
export default function(visitable, deletable, clickable, attribute, collection, tabs, text) {
|
||||
export default function(
|
||||
visitable,
|
||||
deletable,
|
||||
clickable,
|
||||
attribute,
|
||||
collection,
|
||||
tabs,
|
||||
text,
|
||||
healthChecks
|
||||
) {
|
||||
return {
|
||||
visit: visitable('/:dc/nodes/:node'),
|
||||
tabs: tabs('tab', [
|
||||
|
@ -8,9 +17,7 @@ export default function(visitable, deletable, clickable, attribute, collection,
|
|||
'lock-sessions',
|
||||
'metadata',
|
||||
]),
|
||||
healthchecks: collection('[data-test-node-healthcheck]', {
|
||||
name: attribute('data-test-node-healthcheck'),
|
||||
}),
|
||||
healthChecks: healthChecks(),
|
||||
services: collection('.consul-service-instance-list > ul > li:not(:first-child)', {
|
||||
name: text('[data-test-service-name]'),
|
||||
port: attribute('data-test-service-port', '[data-test-service-port]'),
|
||||
|
|
|
@ -1,17 +1,26 @@
|
|||
export default function(visitable, alias, attribute, collection, text, tabs, upstreams) {
|
||||
export default function(
|
||||
visitable,
|
||||
alias,
|
||||
attribute,
|
||||
collection,
|
||||
text,
|
||||
tabs,
|
||||
upstreams,
|
||||
healthChecks
|
||||
) {
|
||||
return {
|
||||
visit: visitable('/:dc/services/:service/instances/:node/:id'),
|
||||
externalSource: attribute('data-test-external-source', '[data-test-external-source]', {
|
||||
scope: '.title',
|
||||
}),
|
||||
tabs: tabs('tab', ['health-checks', 'upstreams', 'exposed-paths', 'addresses', 'tags-&-meta']),
|
||||
checks: collection('[data-test-checks] li'),
|
||||
checks: alias('healthChecks.item'),
|
||||
healthChecks: healthChecks(),
|
||||
upstreams: alias('upstreamInstances.item'),
|
||||
upstreamInstances: upstreams(),
|
||||
exposedPaths: collection('[data-test-proxy-exposed-paths] > tbody tr', {
|
||||
combinedAddress: text('[data-test-combined-address]'),
|
||||
}),
|
||||
proxyChecks: collection('[data-test-proxy-checks] li'),
|
||||
addresses: collection('#addresses [data-test-tabular-row]', {
|
||||
address: text('[data-test-address]'),
|
||||
}),
|
||||
|
|
Loading…
Reference in New Issue