ui: Move routes to use data-sources (#8321)

* Add uri identifiers to all data source things and make them the same

1. Add uri identitifer to data-source service
2. Make <EventSource /> and <DataSource /> as close as possible
3. Add extra `.closed` method to get a list of inactive/closed/closing
data-sources from elsewhere

* Make the connections cleanup the least worst connection when required

* Pass the uri/request id through all the things

* Better user erroring

* Make event sources close on error

* Allow <DataLoader /> data slot to be configurable

* Allow the <DataWriter /> removed state to be configurable

* Don't error if meta is undefined

* Stitch together all the repositories into the data-source/sink

* Use data.source over repositories

* Add missing  <EventSource /> components

* Fix up the views/templates

* Disable all the old route based blocking query things

* We still need the repo for the mixin for the moment

* Don't default to default, default != ''
This commit is contained in:
John Cowen 2020-07-17 14:42:45 +01:00 committed by GitHub
parent 0b6a098aca
commit 8cb402eae7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
54 changed files with 490 additions and 283 deletions

View file

@ -1,9 +1,10 @@
import Adapter from './application'; import Adapter from './application';
// TODO: Update to use this.formatDatacenter() // TODO: Update to use this.formatDatacenter()
export default Adapter.extend({ export default Adapter.extend({
requestForQuery: function(request, { dc, index }) { requestForQuery: function(request, { dc, index, uri }) {
return request` return request`
GET /v1/coordinate/nodes?${{ dc }} GET /v1/coordinate/nodes?${{ dc }}
X-Request-ID: ${uri}
${{ index }} ${{ index }}
`; `;

View file

@ -2,12 +2,13 @@ import Adapter from './application';
// TODO: Update to use this.formatDatacenter() // TODO: Update to use this.formatDatacenter()
export default Adapter.extend({ export default Adapter.extend({
requestForQueryRecord: function(request, { dc, ns, index, id }) { requestForQueryRecord: function(request, { dc, ns, index, id, uri }) {
if (typeof id === 'undefined') { if (typeof id === 'undefined') {
throw new Error('You must specify an id'); throw new Error('You must specify an id');
} }
return request` return request`
GET /v1/discovery-chain/${id}?${{ dc }} GET /v1/discovery-chain/${id}?${{ dc }}
X-Request-ID: ${uri}
${{ ${{
...this.formatNspace(ns), ...this.formatNspace(ns),

View file

@ -6,9 +6,10 @@ import { SLUG_KEY } from 'consul-ui/models/intention';
// TODO: Update to use this.formatDatacenter() // TODO: Update to use this.formatDatacenter()
export default Adapter.extend({ export default Adapter.extend({
requestForQuery: function(request, { dc, filter, index }) { requestForQuery: function(request, { dc, filter, index, uri }) {
return request` return request`
GET /v1/connect/intentions?${{ dc }} GET /v1/connect/intentions?${{ dc }}
X-Request-ID: ${uri}
${{ ${{
index, index,

View file

@ -1,19 +1,21 @@
import Adapter from './application'; import Adapter from './application';
// TODO: Update to use this.formatDatacenter() // TODO: Update to use this.formatDatacenter()
export default Adapter.extend({ export default Adapter.extend({
requestForQuery: function(request, { dc, index, id }) { requestForQuery: function(request, { dc, index, id, uri }) {
return request` return request`
GET /v1/internal/ui/nodes?${{ dc }} GET /v1/internal/ui/nodes?${{ dc }}
X-Request-ID: ${uri}
${{ index }} ${{ index }}
`; `;
}, },
requestForQueryRecord: function(request, { dc, index, id }) { requestForQueryRecord: function(request, { dc, index, id, uri }) {
if (typeof id === 'undefined') { if (typeof id === 'undefined') {
throw new Error('You must specify an id'); throw new Error('You must specify an id');
} }
return request` return request`
GET /v1/internal/ui/node/${id}?${{ dc }} GET /v1/internal/ui/node/${id}?${{ dc }}
X-Request-ID: ${uri}
${{ index }} ${{ index }}
`; `;

View file

@ -3,9 +3,10 @@ import { SLUG_KEY } from 'consul-ui/models/nspace';
// namespaces aren't categorized by datacenter, therefore no dc // namespaces aren't categorized by datacenter, therefore no dc
export default Adapter.extend({ export default Adapter.extend({
requestForQuery: function(request, { index }) { requestForQuery: function(request, { index, uri }) {
return request` return request`
GET /v1/namespaces GET /v1/namespaces
X-Request-ID: ${uri}
${{ index }} ${{ index }}
`; `;

View file

@ -12,9 +12,10 @@ if (env('CONSUL_NSPACES_ENABLED')) {
} }
export default Adapter.extend({ export default Adapter.extend({
env: service('env'), env: service('env'),
requestForQuery: function(request, { dc, ns, index }) { requestForQuery: function(request, { dc, ns, index, uri }) {
return request` return request`
GET /v1/internal/ui/oidc-auth-methods?${{ dc }} GET /v1/internal/ui/oidc-auth-methods?${{ dc }}
X-Request-ID: ${uri}
${{ ${{
index, index,

View file

@ -1,12 +1,13 @@
import Adapter from './application'; import Adapter from './application';
// TODO: Update to use this.formatDatacenter() // TODO: Update to use this.formatDatacenter()
export default Adapter.extend({ export default Adapter.extend({
requestForQuery: function(request, { dc, ns, index, id }) { requestForQuery: function(request, { dc, ns, index, id, uri }) {
if (typeof id === 'undefined') { if (typeof id === 'undefined') {
throw new Error('You must specify an id'); throw new Error('You must specify an id');
} }
return request` return request`
GET /v1/catalog/connect/${id}?${{ dc }} GET /v1/catalog/connect/${id}?${{ dc }}
X-Request-ID: ${uri}
${{ ${{
...this.formatNspace(ns), ...this.formatNspace(ns),

View file

@ -1,10 +1,11 @@
import Adapter from './application'; import Adapter from './application';
// TODO: Update to use this.formatDatacenter() // TODO: Update to use this.formatDatacenter()
export default Adapter.extend({ export default Adapter.extend({
requestForQuery: function(request, { dc, ns, index, gateway }) { requestForQuery: function(request, { dc, ns, index, gateway, uri }) {
if (typeof gateway !== 'undefined') { if (typeof gateway !== 'undefined') {
return request` return request`
GET /v1/internal/ui/gateway-services-nodes/${gateway}?${{ dc }} GET /v1/internal/ui/gateway-services-nodes/${gateway}?${{ dc }}
X-Request-ID: ${uri}
${{ ${{
...this.formatNspace(ns), ...this.formatNspace(ns),
@ -14,6 +15,7 @@ export default Adapter.extend({
} else { } else {
return request` return request`
GET /v1/internal/ui/services?${{ dc }} GET /v1/internal/ui/services?${{ dc }}
X-Request-ID: ${uri}
${{ ${{
...this.formatNspace(ns), ...this.formatNspace(ns),
@ -22,12 +24,13 @@ export default Adapter.extend({
`; `;
} }
}, },
requestForQueryRecord: function(request, { dc, ns, index, id }) { requestForQueryRecord: function(request, { dc, ns, index, id, uri }) {
if (typeof id === 'undefined') { if (typeof id === 'undefined') {
throw new Error('You must specify an id'); throw new Error('You must specify an id');
} }
return request` return request`
GET /v1/health/service/${id}?${{ dc }} GET /v1/health/service/${id}?${{ dc }}
X-Request-ID: ${uri}
${{ ${{
...this.formatNspace(ns), ...this.formatNspace(ns),

View file

@ -6,12 +6,13 @@ import { NSPACE_KEY } from 'consul-ui/models/nspace';
// TODO: Update to use this.formatDatacenter() // TODO: Update to use this.formatDatacenter()
export default Adapter.extend({ export default Adapter.extend({
requestForQuery: function(request, { dc, ns, index, id }) { requestForQuery: function(request, { dc, ns, index, id, uri }) {
if (typeof id === 'undefined') { if (typeof id === 'undefined') {
throw new Error('You must specify an id'); throw new Error('You must specify an id');
} }
return request` return request`
GET /v1/session/node/${id}?${{ dc }} GET /v1/session/node/${id}?${{ dc }}
X-Request-ID: ${uri}
${{ ${{
...this.formatNspace(ns), ...this.formatNspace(ns),

View file

@ -9,23 +9,28 @@
{{#let (hash {{#let (hash
data=data data=data
error=error error=error
dispatchError=(queue (action (mut error) value="error.errors.firstObject") (action dispatch "ERROR"))
) as |api|}} ) as |api|}}
{{! if we didn't specify any data}} {{#yield-slot name="data"}}
{{#if (not items)}} {{yield api}}
{{! try and load the data if we aren't in an error state}} {{else}}
<State @notMatches={{array "error" "disconnected"}}> {{! if we didn't specify any data}}
{{! but only if we only asked for a single load and we are in loading state}} {{#if (not items)}}
{{#if (or (not once) (state-matches state "loading"))}} {{! try and load the data if we aren't in an error state}}
<DataSource <State @notMatches={{array "error" "disconnected"}}>
@open={{open}} {{! but only if we only asked for a single load and we are in loading state}}
@src={{src}} {{#if (and src (or (not once) (state-matches state "loading")))}}
@onchange={{queue (action "change" value="data") (action dispatch "SUCCESS")}} <DataSource
@onerror={{queue (action (mut error) value="error.errors.firstObject") (action dispatch "ERROR")}} @open={{open}}
/> @src={{src}}
{{/if}} @onchange={{queue (action "change" value="data") (action dispatch "SUCCESS")}}
</State> @onerror={{api.dispatchError}}
{{/if}} />
{{/if}}
</State>
{{/if}}
{{/yield-slot}}
<State @matches="loading"> <State @matches="loading">
{{#yield-slot name="loading"}} {{#yield-slot name="loading"}}

View file

@ -22,7 +22,7 @@ export default Component.extend(Slotted, {
}, },
actions: { actions: {
isLoaded: function() { isLoaded: function() {
return typeof this.items !== 'undefined'; return typeof this.items !== 'undefined' || typeof this.src === 'undefined';
}, },
change: function(data) { change: function(data) {
set(this, 'data', this.onchange(data)); set(this, 'data', this.onchange(data));

View file

@ -91,7 +91,10 @@ export default Component.extend({
); );
const error = err => { const error = err => {
try { try {
this.onerror(err); const error = get(err, 'error.errors.firstObject');
if (get(error || {}, 'status') !== '429') {
this.onerror(err);
}
this.logger.execute(err); this.logger.execute(err);
} catch (err) { } catch (err) {
this.logger.execute(err); this.logger.execute(err);
@ -107,9 +110,7 @@ export default Component.extend({
} }
}, },
error: e => { error: e => {
if (get(e, 'error.errors.firstObject.status') !== '429') { error(e);
error(e);
}
}, },
}); });
replace(this, '_remove', remove); replace(this, '_remove', remove);

View file

@ -32,16 +32,16 @@
</State> </State>
<State @matches="removed"> <State @matches="removed">
<Notification @after={{queue (action dispatch "RESET") (action ondelete)}}> {{#yield-slot name="removed" params=(block-params (component 'notification' after=(queue (action dispatch "RESET") (action ondelete))))}}
{{#yield-slot name="removed"}} {{yield api}}
{{yield api}} {{else}}
{{else}} <Notification @after={{queue (action dispatch "RESET") (action ondelete)}}>
<p data-notification role="alert" class="success notification-delete"> <p data-notification role="alert" class="success notification-delete">
<strong>Success!</strong> <strong>Success!</strong>
Your {{type}} has been deleted. Your {{type}} has been deleted.
</p> </p>
{{/yield-slot}} </Notification>
</Notification> {{/yield-slot}}
</State> </State>
<State @matches="persisted"> <State @matches="persisted">

View file

@ -0,0 +1,3 @@
{{yield (hash
close=(action "close")
)}}

View file

@ -1,10 +1,24 @@
import Component from '@ember/component'; import Component from '@ember/component';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import { get, set } from '@ember/object';
const replace = function(
obj,
prop,
value,
destroy = (prev = null, value) => (typeof prev === 'function' ? prev() : null)
) {
const prev = obj[prop];
if (prev !== value) {
destroy(prev, value);
}
return set(obj, prop, value);
};
export default Component.extend({ export default Component.extend({
tagName: '', tagName: '',
dom: service('dom'), dom: service('dom'),
logger: service('logger'), logger: service('logger'),
data: service('data-source/service'),
closeOnDestroy: true, closeOnDestroy: true,
onerror: function(e) { onerror: function(e) {
this.logger.execute(e.error); this.logger.execute(e.error);
@ -14,25 +28,64 @@ export default Component.extend({
this._listeners = this.dom.listeners(); this._listeners = this.dom.listeners();
}, },
willDestroyElement: function() { willDestroyElement: function() {
if (this.closeOnDestroy && typeof (this.src || {}).close === 'function') { if (this.closeOnDestroy) {
this.src.close(); this.actions.close.apply(this, []);
this.src.willDestroy();
} }
this._listeners.remove(); this._listeners.remove();
this._super(...arguments); this._super(...arguments);
}, },
didReceiveAttrs: function() { didReceiveAttrs: function() {
this._listeners.remove(); this._super(...arguments);
if (typeof (this.src || {}).addEventListener === 'function') { // only close and reopen if the uri changes
this._listeners.add(this.src, { // otherwise this will fire whenever the proxies data changes
error: e => { if (get(this, 'src.configuration.uri') !== get(this, 'source.configuration.uri')) {
try { this.actions.open.apply(this, []);
this.onerror(e);
} catch (err) {
this.logger.execute(e.error);
}
},
});
} }
}, },
actions: {
open: function() {
replace(this, 'source', this.data.open(this.src, this), (prev, source) => {
// Makes sure any previous source (if different) is ALWAYS closed
if (typeof prev !== 'undefined') {
this.data.close(prev, this);
}
});
replace(this, 'proxy', this.src, (prev, proxy) => {
// Makes sure any previous proxy (if different) is ALWAYS closed
if (typeof prev !== 'undefined') {
prev.destroy();
}
});
const error = err => {
try {
const error = get(err, 'error.errors.firstObject');
if (get(error || {}, 'status') !== '429') {
this.onerror(err);
}
this.logger.execute(err);
} catch (err) {
this.logger.execute(err);
}
};
// set up the listeners (which auto cleanup on component destruction)
// we only need errors here as this only uses proxies which
// automatically update their data
const remove = this._listeners.add(this.source, {
error: e => {
error(e);
},
});
replace(this, '_remove', remove);
},
close: function() {
if (typeof this.source !== 'undefined') {
this.data.close(this.source, this);
replace(this, '_remove', undefined);
set(this, 'source', undefined);
}
if (typeof this.proxy !== 'undefined') {
this.proxy.destroy();
}
},
},
}); });

View file

@ -1,30 +0,0 @@
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
import { get } from '@ember/object';
import { alias } from '@ember/object/computed';
export default Controller.extend({
dom: service('dom'),
notify: service('flashMessages'),
items: alias('item.Services'),
actions: {
error: function(e) {
if (e.target.readyState === 1) {
// OPEN
if (get(e, 'error.errors.firstObject.status') === '404') {
this.notify.add({
destroyOnClick: false,
sticky: true,
type: 'warning',
action: 'update',
});
[e.target, this.tomography, this.sessions].forEach(function(item) {
if (item && typeof item.close === 'function') {
item.close();
}
});
}
}
},
},
});

View file

@ -1,9 +1,7 @@
import Controller from '@ember/controller'; import Controller from '@ember/controller';
import { alias } from '@ember/object/computed';
import { get, computed } from '@ember/object'; import { get, computed } from '@ember/object';
export default Controller.extend({ export default Controller.extend({
items: alias('item.Services'),
queryParams: { queryParams: {
search: { search: {
as: 'filter', as: 'filter',

9
ui-v2/app/helpers/uri.js Normal file
View file

@ -0,0 +1,9 @@
import Helper from '@ember/component/helper';
import { inject as service } from '@ember/service';
export default Helper.extend({
encoder: service('encoder'),
compute(params, hash) {
return this.encoder.uriJoin(params);
},
});

View file

@ -4,20 +4,7 @@ export function initialize(container) {
if (env('CONSUL_UI_DISABLE_REALTIME')) { if (env('CONSUL_UI_DISABLE_REALTIME')) {
return; return;
} }
['node', 'coordinate', 'session', 'service', 'proxy', 'discovery-chain', 'intention'] []
.concat(env('CONSUL_NSPACES_ENABLED') ? ['nspace/enabled'] : [])
.map(function(item) {
// create repositories that return a promise resolving to an EventSource
return {
service: `repository/${item}/event-source`,
extend: 'repository/type/event-source',
// Inject our original respository that is used by this class
// within the callable of the EventSource
services: {
content: `repository/${item}`,
},
};
})
.concat( .concat(
['policy', 'role'].map(function(item) { ['policy', 'role'].map(function(item) {
// create repositories that return a promise resolving to an EventSource // create repositories that return a promise resolving to an EventSource
@ -33,50 +20,6 @@ export function initialize(container) {
}) })
) )
.concat([ .concat([
// These are the routes where we overwrite the 'default'
// repo service. Default repos are repos that return a promise resolving to
// an ember-data record or recordset
{
route: 'dc/nodes/index',
services: {
repo: 'repository/node/event-source',
},
},
{
route: 'dc/nodes/show',
services: {
repo: 'repository/node/event-source',
coordinateRepo: 'repository/coordinate/event-source',
sessionRepo: 'repository/session/event-source',
},
},
{
route: 'dc/services/index',
services: {
repo: 'repository/service/event-source',
},
},
{
route: 'dc/services/show',
services: {
repo: 'repository/service/event-source',
chainRepo: 'repository/discovery-chain/event-source',
intentionRepo: 'repository/intention/event-source',
},
},
{
route: 'dc/services/instance',
services: {
repo: 'repository/service/event-source',
proxyRepo: 'repository/proxy/event-source',
},
},
{
route: 'dc/intentions/index',
services: {
repo: 'repository/intention/event-source',
},
},
{ {
service: 'form', service: 'form',
services: { services: {
@ -85,18 +28,6 @@ export function initialize(container) {
}, },
}, },
]) ])
.concat(
env('CONSUL_NSPACES_ENABLED')
? [
{
route: 'dc/nspaces/index',
services: {
repo: 'repository/nspace/enabled/event-source',
},
},
]
: []
)
.forEach(function(definition) { .forEach(function(definition) {
if (typeof definition.extend !== 'undefined') { if (typeof definition.extend !== 'undefined') {
// Create the class instances that we need // Create the class instances that we need
@ -111,9 +42,6 @@ export function initialize(container) {
// but hardcode this for the moment // but hardcode this for the moment
if (typeof definition.route !== 'undefined') { if (typeof definition.route !== 'undefined') {
container.inject(`route:${definition.route}`, name, `service:${servicePath}`); container.inject(`route:${definition.route}`, name, `service:${servicePath}`);
if (env('CONSUL_NSPACES_ENABLED') && definition.route.startsWith('dc/')) {
container.inject(`route:nspace/${definition.route}`, name, `service:${servicePath}`);
}
} else { } else {
container.inject(`service:${definition.service}`, name, `service:${servicePath}`); container.inject(`service:${definition.service}`, name, `service:${servicePath}`);
} }

View file

@ -4,6 +4,7 @@ import { hash } from 'rsvp';
export default Route.extend({ export default Route.extend({
repo: service('repository/node'), repo: service('repository/node'),
data: service('data-source/service'),
queryParams: { queryParams: {
search: { search: {
as: 'filter', as: 'filter',
@ -12,8 +13,9 @@ export default Route.extend({
}, },
model: function(params) { model: function(params) {
const dc = this.modelFor('dc').dc.Name; const dc = this.modelFor('dc').dc.Name;
const nspace = '*';
return hash({ return hash({
items: this.repo.findAllByDatacenter(dc, this.modelFor('nspace').nspace.substr(1)), items: this.data.source(uri => uri`/${nspace}/${dc}/nodes`),
leader: this.repo.findByLeader(dc), leader: this.repo.findByLeader(dc),
}); });
}, },

View file

@ -3,20 +3,20 @@ import { inject as service } from '@ember/service';
import { hash } from 'rsvp'; import { hash } from 'rsvp';
export default Route.extend({ export default Route.extend({
repo: service('repository/node'), data: service('data-source/service'),
sessionRepo: service('repository/session'),
coordinateRepo: service('repository/coordinate'),
model: function(params) { model: function(params) {
const dc = this.modelFor('dc').dc.Name; const dc = this.modelFor('dc').dc.Name;
const nspace = this.modelFor('nspace').nspace.substr(1); const nspace = this.modelFor('nspace').nspace.substr(1);
const name = params.name; const name = params.name;
return hash({ return hash({
item: this.repo.findBySlug(name, dc, nspace), dc: dc,
nspace: nspace,
item: this.data.source(uri => uri`/${nspace}/${dc}/node/${name}`),
}).then(model => { }).then(model => {
return hash({ return hash({
...model, ...model,
sessions: this.sessionRepo.findByNode(name, dc, nspace), tomography: this.data.source(uri => uri`/${nspace}/${dc}/coordinates/for-node/${name}`),
tomography: this.coordinateRepo.findAllByNode(name, dc), sessions: this.data.source(uri => uri`/${nspace}/${dc}/sessions/for-node/${name}`),
}); });
}); });
}, },

View file

@ -4,6 +4,7 @@ import { hash } from 'rsvp';
import WithNspaceActions from 'consul-ui/mixins/nspace/with-actions'; import WithNspaceActions from 'consul-ui/mixins/nspace/with-actions';
export default Route.extend(WithNspaceActions, { export default Route.extend(WithNspaceActions, {
data: service('data-source/service'),
repo: service('repository/nspace'), repo: service('repository/nspace'),
queryParams: { queryParams: {
search: { search: {
@ -13,7 +14,7 @@ export default Route.extend(WithNspaceActions, {
}, },
model: function(params) { model: function(params) {
return hash({ return hash({
items: this.repo.findAll(), items: this.data.source(uri => uri`/*/*/namespaces`),
isLoading: false, isLoading: false,
}); });
}, },

View file

@ -3,7 +3,7 @@ import { inject as service } from '@ember/service';
import { hash } from 'rsvp'; import { hash } from 'rsvp';
export default Route.extend({ export default Route.extend({
repo: service('repository/service'), data: service('data-source/service'),
queryParams: { queryParams: {
search: { search: {
as: 'filter', as: 'filter',
@ -29,12 +29,13 @@ export default Route.extend({
.trim(); .trim();
} }
} }
const nspace = this.modelFor('nspace').nspace.substr(1);
const dc = this.modelFor('dc').dc.Name;
return hash({ return hash({
nspace: nspace,
dc: dc,
terms: terms !== '' ? terms.split('\n') : [], terms: terms !== '' ? terms.split('\n') : [],
items: this.repo.findAllByDatacenter( items: this.data.source(uri => uri`/${nspace}/${dc}/services`),
this.modelFor('dc').dc.Name,
this.modelFor('nspace').nspace.substr(1)
),
}); });
}, },
setupController: function(controller, model) { setupController: function(controller, model) {

View file

@ -4,38 +4,46 @@ import { hash } from 'rsvp';
import { get } from '@ember/object'; import { get } from '@ember/object';
export default Route.extend({ export default Route.extend({
repo: service('repository/service'), data: service('data-source/service'),
proxyRepo: service('repository/proxy'),
model: function(params) { model: function(params) {
const dc = this.modelFor('dc').dc.Name; const dc = this.modelFor('dc').dc.Name;
const nspace = this.modelFor('nspace').nspace.substr(1); const nspace = this.modelFor('nspace').nspace.substr(1) || 'default';
return hash({ return hash({
dc: dc, dc: dc,
nspace: nspace || 'default', nspace: nspace,
item: this.repo.findInstanceBySlug(params.id, params.node, params.name, dc, nspace), item: this.data.source(
uri => uri`/${nspace}/${dc}/service-instance/${params.id}/${params.node}/${params.name}`
),
}).then(model => { }).then(model => {
// this will not be run in a blocking loop, but this is ok as // this will not be run in a blocking loop, but this is ok as
// its highly unlikely that a service will suddenly change to being a // its highly unlikely that a service will suddenly change to being a
// connect-proxy or vice versa so leave as is for now // connect-proxy or vice versa so leave as is for now
return hash({ return hash({
...model,
proxyMeta: proxyMeta:
// proxies and mesh-gateways can't have proxies themselves so don't even look // proxies and mesh-gateways can't have proxies themselves so don't even look
['connect-proxy', 'mesh-gateway'].includes(get(model.item, 'Kind')) ['connect-proxy', 'mesh-gateway'].includes(get(model.item, 'Kind'))
? null ? null
: this.proxyRepo.findInstanceBySlug(params.id, params.node, params.name, dc, nspace), : this.data.source(
...model, uri =>
uri`/${nspace}/${dc}/proxy-instance/${params.id}/${params.node}/${params.name}`
),
}).then(model => { }).then(model => {
if (typeof get(model, 'proxyMeta.ServiceID') === 'undefined') { if (typeof get(model, 'proxyMeta.ServiceID') === 'undefined') {
return model; return model;
} }
const proxyName = get(model, 'proxyMeta.ServiceName'); const proxy = {
const proxyID = get(model, 'proxyMeta.ServiceID'); id: get(model, 'proxyMeta.ServiceID'),
const proxyNode = get(model, 'proxyMeta.Node'); node: get(model, 'proxyMeta.Node'),
name: get(model, 'proxyMeta.ServiceName'),
};
return hash({ return hash({
...model,
// Proxies have identical dc/nspace as their parent instance // Proxies have identical dc/nspace as their parent instance
// No need to use Proxy's dc/nspace response // No need to use Proxy's dc/nspace response
proxy: this.repo.findInstanceBySlug(proxyID, proxyNode, proxyName, dc, nspace), proxy: this.data.source(
...model, uri => uri`/${nspace}/${dc}/service-instance/${proxy.id}/${proxy.node}/${proxy.name}`
),
}); });
}); });
}); });

View file

@ -4,9 +4,7 @@ import { hash } from 'rsvp';
import { get } from '@ember/object'; import { get } from '@ember/object';
export default Route.extend({ export default Route.extend({
repo: service('repository/service'), data: service('data-source/service'),
chainRepo: service('repository/discovery-chain'),
proxyRepo: service('repository/proxy'),
settings: service('settings'), settings: service('settings'),
model: function(params, transition = {}) { model: function(params, transition = {}) {
const dc = this.modelFor('dc').dc.Name; const dc = this.modelFor('dc').dc.Name;
@ -14,8 +12,8 @@ export default Route.extend({
return hash({ return hash({
slug: params.name, slug: params.name,
dc: dc, dc: dc,
nspace: nspace || 'default', nspace: nspace,
item: this.repo.findBySlug(params.name, dc, nspace), item: this.data.source(uri => uri`/${nspace}/${dc}/service/${params.name}`),
urls: this.settings.findBySlug('urls'), urls: this.settings.findBySlug('urls'),
proxies: [], proxies: [],
}) })
@ -25,16 +23,20 @@ export default Route.extend({
) )
? model ? model
: hash({ : hash({
chain: this.chainRepo.findBySlug(params.name, dc, nspace),
proxies: this.proxyRepo.findAllBySlug(params.name, dc, nspace),
...model, ...model,
chain: this.data.source(uri => uri`/${nspace}/${dc}/discovery-chain/${params.name}`),
proxies: this.data.source(
uri => uri`/${nspace}/${dc}/proxies/for-service/${params.name}`
),
}); });
}) })
.then(model => { .then(model => {
return ['ingress-gateway', 'terminating-gateway'].includes(get(model, 'item.Service.Kind')) return ['ingress-gateway', 'terminating-gateway'].includes(get(model, 'item.Service.Kind'))
? hash({ ? hash({
gatewayServices: this.repo.findGatewayBySlug(params.name, dc, nspace),
...model, ...model,
gatewayServices: this.data.source(
uri => uri`/${nspace}/${dc}/gateways/for-service/${params.name}`
),
}) })
: model; : model;
}); });

View file

@ -3,6 +3,8 @@ import Service, { inject as service } from '@ember/service';
export default Service.extend({ export default Service.extend({
dom: service('dom'), dom: service('dom'),
env: service('env'), env: service('env'),
data: service('data-source/service'),
sources: service('repository/type/event-source'),
init: function() { init: function() {
this._super(...arguments); this._super(...arguments);
this._listeners = this.dom.listeners(); this._listeners = this.dom.listeners();
@ -42,21 +44,41 @@ export default Service.extend({
} }
return Promise.resolve(e); return Promise.resolve(e);
}, },
purge: function() { purge: function(statusCode = 0) {
[...this.connections].forEach(function(connection) { [...this.connections].forEach(function(connection) {
// Cancelled // Cancelled
connection.abort(0); connection.abort(statusCode);
}); });
this.connections = new Set(); this.connections = new Set();
}, },
acquire: function(request) { acquire: function(request) {
this.connections.add(request); if (this.connections.size >= this.env.var('CONSUL_HTTP_MAX_CONNECTIONS')) {
if (this.connections.size > this.env.var('CONSUL_HTTP_MAX_CONNECTIONS')) { const closed = this.data.closed();
const connection = this.connections.values().next().value; let connection = [...this.connections].find(item => {
this.connections.delete(connection); const id = item.headers()['x-request-id'];
// Too Many Requests if (id) {
connection.abort(429); return closed.includes(item.headers()['x-request-id']);
}
return false;
});
if (typeof connection === 'undefined') {
// all connections are being used on the page
// if the new one is a blocking query then cancel the oldest connection
if (request.headers()['content-type'] === 'text/event-stream') {
connection = this.connections.values().next().value;
}
// otherwise wait for a connection to become available
}
// cancel the connection
if (typeof connection !== 'undefined') {
// if its a shared blocking query cancel everything
// listening to it
this.release(connection);
// Too Many Requests
connection.abort(429);
}
} }
this.connections.add(request);
}, },
release: function(request) { release: function(request) {
this.connections.delete(request); this.connections.delete(request);

View file

@ -68,7 +68,7 @@ const parseBody = function(strs, ...values) {
return [body, ...values]; return [body, ...values];
}; };
const CLIENT_HEADERS = [CACHE_CONTROL]; const CLIENT_HEADERS = [CACHE_CONTROL, 'X-Request-ID'];
export default Service.extend({ export default Service.extend({
dom: service('dom'), dom: service('dom'),
connections: service('client/connections'), connections: service('client/connections'),

View file

@ -12,7 +12,10 @@ export default Service.extend({
return xhr(options); return xhr(options);
}, },
request: function(params) { request: function(params) {
const request = new Request(params.method, params.url, { body: params.data || {} }); const request = new Request(params.method, params.url, {
['x-request-id']: params.clientHeaders['x-request-id'],
body: params.data || {},
});
const options = { const options = {
...params, ...params,
beforeSend: function(xhr) { beforeSend: function(xhr) {
@ -51,6 +54,7 @@ export default Service.extend({
}; };
request.fetch = () => { request.fetch = () => {
this.xhr(options); this.xhr(options);
return request;
}; };
return request; return request;
}, },

View file

@ -4,6 +4,7 @@ import { setProperties } from '@ember/object';
export default Service.extend({ export default Service.extend({
settings: service('settings'), settings: service('settings'),
intention: service('repository/intention'), intention: service('repository/intention'),
session: service('repository/session'),
prepare: function(sink, data, instance) { prepare: function(sink, data, instance) {
return setProperties(instance, data); return setProperties(instance, data);
}, },

View file

@ -3,7 +3,17 @@ import { get } from '@ember/object';
export default Service.extend({ export default Service.extend({
datacenters: service('repository/dc'), datacenters: service('repository/dc'),
nodes: service('repository/node'),
node: service('repository/node'),
gateways: service('repository/service'),
services: service('repository/service'), services: service('repository/service'),
service: service('repository/service'),
['service-instance']: service('repository/service'),
proxies: service('repository/proxy'),
['proxy-instance']: service('repository/proxy'),
['discovery-chain']: service('repository/discovery-chain'),
coordinates: service('repository/coordinate'),
sessions: service('repository/session'),
namespaces: service('repository/nspace'), namespaces: service('repository/nspace'),
intentions: service('repository/intention'), intentions: service('repository/intention'),
intention: service('repository/intention'), intention: service('repository/intention'),
@ -12,17 +22,15 @@ export default Service.extend({
policies: service('repository/policy'), policies: service('repository/policy'),
policy: service('repository/policy'), policy: service('repository/policy'),
roles: service('repository/role'), roles: service('repository/role'),
oidc: service('repository/oidc-provider'), oidc: service('repository/oidc-provider'),
type: service('data-source/protocols/http/blocking'), type: service('data-source/protocols/http/blocking'),
source: function(src, configuration) { source: function(src, configuration) {
// TODO: Consider adding/requiring nspace, dc, model, action, ...rest // TODO: Consider adding/requiring 'action': nspace, dc, model, action, ...rest
const [, nspace, dc, model, ...rest] = src.split('/'); const [, nspace, dc, model, ...rest] = src.split('/').map(decodeURIComponent);
// TODO: Consider throwing if we have an empty nspace or dc // nspaces can be filled, blank or *
// we are going to use '*' for 'all' when we need that // so we might get urls like //dc/services
// and an empty value is the same as 'default'
// reasoning for potentially doing it here is, uri's should
// always be complete, they should never have things like '///model'
let find; let find;
const repo = this[model]; const repo = this[model];
if (repo.shouldReconcile(src)) { if (repo.shouldReconcile(src)) {
@ -41,32 +49,72 @@ export default Service.extend({
case 'namespaces': case 'namespaces':
find = configuration => repo.findAll(configuration); find = configuration => repo.findAll(configuration);
break; break;
case 'token':
find = configuration => repo.self(rest[1], dc);
break;
case 'services': case 'services':
case 'nodes':
case 'roles': case 'roles':
case 'policies': case 'policies':
find = configuration => repo.findAllByDatacenter(dc, nspace, configuration); find = configuration => repo.findAllByDatacenter(dc, nspace, configuration);
break; break;
case 'policy':
find = configuration => repo.findBySlug(rest[0], dc, nspace, configuration);
break;
case 'intentions': case 'intentions':
[method, ...slug] = rest; [method, ...slug] = rest;
switch (method) { switch (method) {
case 'for-service': case 'for-service':
// TODO: Are we going to need to encode/decode here...? find = configuration => repo.findByService(slug, dc, nspace, configuration);
find = configuration => repo.findByService(slug.join('/'), dc, nspace, configuration);
break; break;
default: default:
find = configuration => repo.findAllByDatacenter(dc, nspace, configuration); find = configuration => repo.findAllByDatacenter(dc, nspace, configuration);
break; break;
} }
break; break;
case 'coordinates':
[method, ...slug] = rest;
switch (method) {
case 'for-node':
find = configuration => repo.findAllByNode(slug, dc, configuration);
break;
}
break;
case 'proxies':
[method, ...slug] = rest;
switch (method) {
case 'for-service':
find = configuration => repo.findAllBySlug(slug, dc, nspace, configuration);
break;
}
break;
case 'gateways':
[method, ...slug] = rest;
switch (method) {
case 'for-service':
find = configuration => repo.findGatewayBySlug(slug, dc, nspace, configuration);
break;
}
break;
case 'sessions':
[method, ...slug] = rest;
switch (method) {
case 'for-node':
find = configuration => repo.findByNode(slug, dc, nspace, configuration);
break;
}
break;
case 'token':
find = configuration => repo.self(rest[1], dc);
break;
case 'service':
case 'discovery-chain':
case 'node':
find = configuration => repo.findBySlug(rest[0], dc, nspace, configuration);
break;
case 'service-instance':
case 'proxy-instance':
// id, node, service
find = configuration =>
repo.findInstanceBySlug(rest[0], rest[1], rest[2], dc, nspace, configuration);
break;
case 'policy':
case 'intention': case 'intention':
// TODO: Are we going to need to encode/decode here...? slug = rest[0];
slug = rest.join('/');
if (slug) { if (slug) {
find = configuration => repo.findBySlug(slug, dc, nspace, configuration); find = configuration => repo.findBySlug(slug, dc, nspace, configuration);
} else { } else {

View file

@ -18,7 +18,7 @@ export default Service.extend({
return find(configuration) return find(configuration)
.then(maybeCall(close, ifNotBlocking(this.settings))) .then(maybeCall(close, ifNotBlocking(this.settings)))
.then(function(res) { .then(function(res) {
if (typeof get(res, 'meta.cursor') === 'undefined') { if (typeof get(res || {}, 'meta.cursor') === 'undefined') {
close(); close();
} }
return res; return res;

View file

@ -11,6 +11,7 @@ export default Service.extend({
}, },
{ {
key: src, key: src,
uri: configuration.uri,
} }
); );
}, },

View file

@ -1,4 +1,5 @@
import Service, { inject as service } from '@ember/service'; import Service, { inject as service } from '@ember/service';
import { proxy } from 'consul-ui/utils/dom/event-source';
import MultiMap from 'mnemonist/multi-map'; import MultiMap from 'mnemonist/multi-map';
@ -10,9 +11,9 @@ let cache = null;
let sources = null; let sources = null;
// keeps a count of currently in use EventSources // keeps a count of currently in use EventSources
let usage = null; let usage = null;
export default Service.extend({ export default Service.extend({
dom: service('dom'), dom: service('dom'),
encoder: service('encoder'),
consul: service('data-source/protocols/http'), consul: service('data-source/protocols/http'),
settings: service('data-source/protocols/local-storage'), settings: service('data-source/protocols/local-storage'),
@ -33,29 +34,68 @@ export default Service.extend({
}); });
cache = null; cache = null;
sources = null; sources = null;
usage.clear();
usage = null; usage = null;
}, },
source: function(cb, attrs) {
const src = cb(this.encoder.uriTag());
return new Promise((resolve, reject) => {
const ref = {};
const source = this.open(src, ref, true);
source.configuration.ref = ref;
const remove = this._listeners.add(source, {
message: e => {
remove();
// the source only gets wrapped in the proxy
// after the first message
// but the proxy itself is resolve to the route
resolve(proxy(e.target, e.data));
},
error: e => {
remove();
this.close(source, ref);
reject(e.error);
},
});
if (typeof source.getCurrentEvent() !== 'undefined') {
source.dispatchEvent(source.getCurrentEvent());
}
});
},
unwrap: function(src, ref) {
const source = src._source;
usage.set(source, ref);
usage.remove(source, source.configuration.ref);
delete source.configuration.ref;
return source;
},
open: function(uri, ref, open = false) { open: function(uri, ref, open = false) {
if (typeof uri !== 'string') {
return this.unwrap(uri, ref);
}
let source; let source;
// Check the cache for an EventSource that is already being used // Check the cache for an EventSource that is already being used
// for this uri. If we don't have one, set one up. // for this uri. If we don't have one, set one up.
if (uri.indexOf('://') === -1) { if (uri.indexOf('://') === -1) {
uri = `consul://${uri}`; uri = `consul://${uri}`;
} }
let [providerName, pathname] = uri.split('://');
const provider = this[providerName];
if (!sources.has(uri)) { if (!sources.has(uri)) {
let [providerName, pathname] = uri.split('://');
const provider = this[providerName];
let configuration = {}; let configuration = {};
if (cache.has(uri)) { if (cache.has(uri)) {
configuration = cache.get(uri); configuration = cache.get(uri);
} }
configuration.uri = uri;
source = provider.source(pathname, configuration); source = provider.source(pathname, configuration);
this._listeners.add(source, { const remove = this._listeners.add(source, {
close: e => { close: e => {
// a close could be fired either by:
// 1. A non-blocking query leaving the page
// 2. A non-blocking query responding
// 3. A blocking query responding when is in a closing state
// 3. A non-blocking query or a blocking query being cancelled
const source = e.target; const source = e.target;
source.removeEventListener('close', close);
const event = source.getCurrentEvent(); const event = source.getCurrentEvent();
const cursor = source.configuration.cursor; const cursor = source.configuration.cursor;
// only cache data if we have any // only cache data if we have any
@ -67,20 +107,25 @@ export default Service.extend({
} }
// the data is cached delete the EventSource // the data is cached delete the EventSource
if (!usage.has(source)) { if (!usage.has(source)) {
// A non-blocking query could close but still be on the page
sources.delete(uri); sources.delete(uri);
} }
remove();
}, },
}); });
sources.set(uri, source); sources.set(uri, source);
} else { } else {
source = sources.get(uri); source = sources.get(uri);
// bump to the end of the list
sources.delete(uri);
sources.set(uri, source);
} }
// only open if its not already being used // only open if its not already being used
// in the case of blocking queries being disabled // in the case of blocking queries being disabled
// you may want to specifically force an open // you may want to specifically force an open
// if blocking queries are enabled then opening an already // if blocking queries are enabled then opening an already
// open blocking query does nothing // open blocking query does nothing
if (!usage.has(source) || open) { if (!usage.has(source) || source.readyState > 1 || open) {
source.open(); source.open();
} }
// set/increase the usage counter // set/increase the usage counter
@ -88,6 +133,8 @@ export default Service.extend({
return source; return source;
}, },
close: function(source, ref) { close: function(source, ref) {
// this close is called when the source has either left the page
// or in the case of a proxied source, it errors
if (source) { if (source) {
// decrease the usage counter // decrease the usage counter
usage.remove(source, ref); usage.remove(source, ref);
@ -95,7 +142,22 @@ export default Service.extend({
// close it (data caching is dealt with by the above 'close' event listener) // close it (data caching is dealt with by the above 'close' event listener)
if (!usage.has(source)) { if (!usage.has(source)) {
source.close(); source.close();
if (source.readyState === 2) {
// in the case that a non-blocking query is on the page
// and it has already responded and has therefore been cached
// but not removed itself from sources
// delete from sources
sources.delete(source.configuration.uri);
}
} }
} }
}, },
closed: function() {
// anything that is closed or closing
return [...sources.entries()]
.filter(([key, item]) => {
return item.readyState > 1;
})
.map(item => item[0]);
},
}); });

View file

@ -0,0 +1,27 @@
import Service from '@ember/service';
import atob from 'consul-ui/utils/atob';
import btoa from 'consul-ui/utils/btoa';
export default Service.extend({
uriComponent: encodeURIComponent,
atob: function() {
return atob(...arguments);
},
btoa: function() {
return btoa(...arguments);
},
uriJoin: function() {
return this.joiner(this.uriComponent, '/', '')(...arguments);
},
uriTag: function() {
return this.tag(this.uriJoin.bind(this));
},
joiner: (encoder, joiner = '', defaultValue = '') => (values, strs) =>
(strs || Array(values.length).fill(joiner)).reduce(
(prev, item, i) => `${prev}${item}${encoder(values[i] || defaultValue)}`,
''
),
tag: function(join) {
return (strs, ...values) => join(values, strs);
},
});

View file

@ -49,6 +49,7 @@ export default Service.extend({
}; };
if (typeof configuration.cursor !== 'undefined') { if (typeof configuration.cursor !== 'undefined') {
query.index = configuration.cursor; query.index = configuration.cursor;
query.uri = configuration.uri;
} }
return this.store.query(this.getModelName(), query); return this.store.query(this.getModelName(), query);
}, },
@ -60,6 +61,7 @@ export default Service.extend({
}; };
if (typeof configuration.cursor !== 'undefined') { if (typeof configuration.cursor !== 'undefined') {
query.index = configuration.cursor; query.index = configuration.cursor;
query.uri = configuration.uri;
} }
return this.store.queryRecord(this.getModelName(), query); return this.store.queryRecord(this.getModelName(), query);
}, },

View file

@ -18,6 +18,7 @@ export default RepositoryService.extend({
}; };
if (typeof configuration.cursor !== 'undefined') { if (typeof configuration.cursor !== 'undefined') {
query.index = configuration.cursor; query.index = configuration.cursor;
query.uri = configuration.uri;
} }
return this.store.query(this.getModelName(), query); return this.store.query(this.getModelName(), query);
}, },
@ -25,7 +26,10 @@ export default RepositoryService.extend({
return this.findAllByDatacenter(dc, configuration).then(function(coordinates) { return this.findAllByDatacenter(dc, configuration).then(function(coordinates) {
let results = {}; let results = {};
if (get(coordinates, 'length') > 1) { if (get(coordinates, 'length') > 1) {
results = tomography(node, coordinates.map(item => get(item, 'data'))); results = tomography(
node,
coordinates.map(item => get(item, 'data'))
);
} }
results.meta = get(coordinates, 'meta'); results.meta = get(coordinates, 'meta');
return results; return results;

View file

@ -29,6 +29,7 @@ export default RepositoryService.extend({
}; };
if (typeof configuration.cursor !== 'undefined') { if (typeof configuration.cursor !== 'undefined') {
query.index = configuration.cursor; query.index = configuration.cursor;
query.uri = configuration.uri;
} }
return this.store.query(this.getModelName(), { return this.store.query(this.getModelName(), {
...query, ...query,

View file

@ -16,6 +16,7 @@ export default RepositoryService.extend({
const query = {}; const query = {};
if (typeof configuration.cursor !== 'undefined') { if (typeof configuration.cursor !== 'undefined') {
query.index = configuration.cursor; query.index = configuration.cursor;
query.uri = configuration.uri;
} }
return this.store.query(this.getModelName(), query); return this.store.query(this.getModelName(), query);
}, },

View file

@ -21,6 +21,7 @@ export default RepositoryService.extend({
const query = {}; const query = {};
if (typeof configuration.cursor !== 'undefined') { if (typeof configuration.cursor !== 'undefined') {
query.index = configuration.cursor; query.index = configuration.cursor;
query.uri = configuration.uri;
} }
return this.store.query(this.getModelName(), query); return this.store.query(this.getModelName(), query);
}, },

View file

@ -17,6 +17,7 @@ export default RepositoryService.extend({
}; };
if (typeof configuration.cursor !== 'undefined') { if (typeof configuration.cursor !== 'undefined') {
query.index = configuration.cursor; query.index = configuration.cursor;
query.uri = configuration.uri;
} }
return this.store.query(this.getModelName(), query); return this.store.query(this.getModelName(), query);
}, },

View file

@ -84,6 +84,7 @@ export default RepositoryService.extend({
}; };
if (typeof configuration.cursor !== 'undefined') { if (typeof configuration.cursor !== 'undefined') {
query.index = configuration.cursor; query.index = configuration.cursor;
query.uri = configuration.uri;
} }
return this.store.query(this.getModelName(), query); return this.store.query(this.getModelName(), query);
}, },

View file

@ -15,6 +15,7 @@ export default RepositoryService.extend({
}; };
if (typeof configuration.cursor !== 'undefined') { if (typeof configuration.cursor !== 'undefined') {
query.index = configuration.cursor; query.index = configuration.cursor;
query.uri = configuration.uri;
} }
return this.store.query(this.getModelName(), query); return this.store.query(this.getModelName(), query);
}, },

View file

@ -1,39 +1,70 @@
{{title item.Node}} {{title item.Node}}
<EventSource @src={{item}} @onerror={{action "error"}} /> <DataLoader as |api|>
<EventSource @src={{sessions}} />
<EventSource @src={{tomography}} /> <BlockSlot @name="data">
<AppView @class="node show"> <EventSource @src={{sessions}} />
<BlockSlot @name="notification" as |status type|> <EventSource @src={{tomography}} />
{{!TODO: Move sessions to its own folder within nodes }} <EventSource @src={{item}} @onerror={{queue (action api.dispatchError)}} />
{{partial 'dc/nodes/notifications'}} </BlockSlot>
</BlockSlot>
<BlockSlot @name="breadcrumbs"> <BlockSlot @name="error">
<ol> <AppError @error={{api.error}} />
<li><a data-test-back href={{href-to 'dc.nodes'}}>All Nodes</a></li> </BlockSlot>
</ol>
</BlockSlot> <BlockSlot @name="disconnected" as |Notification|>
<BlockSlot @name="header"> {{#if (eq api.error.status "404")}}
<h1> <Notification @sticky={{true}}>
{{ item.Node }} <p data-notification role="alert" class="warning notification-update">
</h1> <strong>Warning!</strong>
<label for="toolbar-toggle"></label> This node no longer exists in the catalog.
</BlockSlot> </p>
<BlockSlot @name="nav"> </Notification>
<TabNav @items={{ {{else}}
compact <Notification @sticky={{true}}>
(array <p data-notification role="alert" class="warning notification-update">
(hash label="Health Checks" href=(href-to "dc.nodes.show.healthchecks") selected=(is-href "dc.nodes.show.healthchecks")) <strong>Warning!</strong>
(hash label="Service Instances" href=(href-to "dc.nodes.show.services") selected=(is-href "dc.nodes.show.services")) An error was returned whilst loading this data, refresh to try again.
(if tomography.distances (hash label="Round Trip Time" href=(href-to "dc.nodes.show.rtt") selected=(is-href "dc.nodes.show.rtt")) '') </p>
(hash label="Lock Sessions" href=(href-to "dc.nodes.show.sessions") selected=(is-href "dc.nodes.show.sessions")) </Notification>
(hash label="Metadata" href=(href-to "dc.nodes.show.metadata") selected=(is-href "dc.nodes.show.metadata")) {{/if}}
) </BlockSlot>
}}/>
</BlockSlot> <BlockSlot @name="loaded">
<BlockSlot @name="actions"> <AppView @class="node show">
<CopyButton @value={{item.Address}} @name="Address">{{item.Address}}</CopyButton> <BlockSlot @name="notification" as |status type|>
</BlockSlot> {{!TODO: Move sessions to its own folder within nodes }}
<BlockSlot @name="content"> {{partial 'dc/nodes/notifications'}}
{{outlet}} </BlockSlot>
</BlockSlot> <BlockSlot @name="breadcrumbs">
</AppView> <ol>
<li><a data-test-back href={{href-to 'dc.nodes'}}>All Nodes</a></li>
</ol>
</BlockSlot>
<BlockSlot @name="header">
<h1>
{{ item.Node }}
</h1>
<label for="toolbar-toggle"></label>
</BlockSlot>
<BlockSlot @name="nav">
<TabNav @items={{
compact
(array
(hash label="Health Checks" href=(href-to "dc.nodes.show.healthchecks") selected=(is-href "dc.nodes.show.healthchecks"))
(hash label="Service Instances" href=(href-to "dc.nodes.show.services") selected=(is-href "dc.nodes.show.services"))
(if tomography.distances (hash label="Round Trip Time" href=(href-to "dc.nodes.show.rtt") selected=(is-href "dc.nodes.show.rtt")) '')
(hash label="Lock Sessions" href=(href-to "dc.nodes.show.sessions") selected=(is-href "dc.nodes.show.sessions"))
(hash label="Metadata" href=(href-to "dc.nodes.show.metadata") selected=(is-href "dc.nodes.show.metadata"))
)
}}/>
</BlockSlot>
<BlockSlot @name="actions">
<CopyButton @value={{item.Address}} @name="Address">{{item.Address}}</CopyButton>
</BlockSlot>
<BlockSlot @name="content">
{{outlet}}
</BlockSlot>
</AppView>
</BlockSlot>
</DataLoader>

View file

@ -1,3 +1,4 @@
{{#let item.Services as |items|}}
<div id="services" class="tab-section"> <div id="services" class="tab-section">
<div role="tabpanel"> <div role="tabpanel">
{{#if (gt items.length 0) }} {{#if (gt items.length 0) }}
@ -24,3 +25,4 @@
</ChangeableSet> </ChangeableSet>
</div> </div>
</div> </div>
{{/let}}

View file

@ -39,14 +39,14 @@
<td> <td>
<ConfirmationDialog @message="Are you sure you want to invalidate this session?"> <ConfirmationDialog @message="Are you sure you want to invalidate this session?">
<BlockSlot @name="action" as |confirm|> <BlockSlot @name="action" as |confirm|>
<button data-test-delete type="button" class="type-delete" {{action confirm 'invalidateSession' item}}>Invalidate</button> <button data-test-delete type="button" class="type-delete" onclick={{queue (action confirm 'invalidateSession' item) (refresh-route)}}>Invalidate</button>
</BlockSlot> </BlockSlot>
<BlockSlot @name="dialog" as |execute cancel message|> <BlockSlot @name="dialog" as |execute cancel message|>
<p> <p>
{{message}} {{message}}
</p> </p>
<button type="button" class="type-delete" {{action execute}}>Confirm Invalidate</button> <button type="button" class="type-delete" {{action execute}}>Confirm Invalidate</button>
<button type="button" class="type-cancel" {{ action cancel}}>Cancel</button> <button type="button" class="type-cancel" {{action cancel}}>Cancel</button>
</BlockSlot> </BlockSlot>
</ConfirmationDialog> </ConfirmationDialog>
</td> </td>

View file

@ -1,6 +1,7 @@
{{title item.ID}} {{title item.ID}}
<EventSource @src={{item}} @onerror={{action "error"}} /> <EventSource @src={{item}} @onerror={{action "error"}} />
<EventSource @src={{proxy}} /> <EventSource @src={{proxy}} />
<EventSource @src={{proxyMeta}} />
<AppView @class="instance show"> <AppView @class="instance show">
<BlockSlot @name="notification" as |status type|> <BlockSlot @name="notification" as |status type|>
{{partial 'dc/services/notifications'}} {{partial 'dc/services/notifications'}}

View file

@ -2,6 +2,8 @@
<EventSource @src={{item}} @onerror={{action "error"}} /> <EventSource @src={{item}} @onerror={{action "error"}} />
<EventSource @src={{chain}} /> <EventSource @src={{chain}} />
<EventSource @src={{intentions}} /> <EventSource @src={{intentions}} />
<EventSource @src={{proxies}} />
<EventSource @src={{gatewayServices}} />
<AppView @class="service show"> <AppView @class="service show">
<BlockSlot @name="notification" as |status type|> <BlockSlot @name="notification" as |status type|>
{{partial 'dc/services/notifications'}} {{partial 'dc/services/notifications'}}

View file

@ -57,6 +57,7 @@ export default function(
// close after the dispatch so we can tell if it was an error whilst closed or not // close after the dispatch so we can tell if it was an error whilst closed or not
// but make sure its before the promise tick // but make sure its before the promise tick
this.readyState = 2; // CLOSE this.readyState = 2; // CLOSE
this.dispatchEvent({ type: 'close' });
}) })
.then(() => { .then(() => {
// This only gets called when the promise chain completely finishes // This only gets called when the promise chain completely finishes

View file

@ -7,10 +7,10 @@ module('Integration | Adapter | coordinate', function(hooks) {
const adapter = this.owner.lookup('adapter:coordinate'); const adapter = this.owner.lookup('adapter:coordinate');
const client = this.owner.lookup('service:client/http'); const client = this.owner.lookup('service:client/http');
const expected = `GET /v1/coordinate/nodes?dc=${dc}`; const expected = `GET /v1/coordinate/nodes?dc=${dc}`;
const actual = adapter.requestForQuery(client.url, { const actual = adapter.requestForQuery(client.requestParams.bind(client), {
dc: dc, dc: dc,
}); });
assert.equal(actual, expected); assert.equal(`${actual.method} ${actual.url}`, expected);
}); });
test('requestForQuery returns the correct body', function(assert) { test('requestForQuery returns the correct body', function(assert) {
const adapter = this.owner.lookup('adapter:coordinate'); const adapter = this.owner.lookup('adapter:coordinate');

View file

@ -11,11 +11,11 @@ module('Integration | Adapter | discovery-chain', function(hooks) {
const adapter = this.owner.lookup('adapter:discovery-chain'); const adapter = this.owner.lookup('adapter:discovery-chain');
const client = this.owner.lookup('service:client/http'); const client = this.owner.lookup('service:client/http');
const expected = `GET /v1/discovery-chain/${id}?dc=${dc}`; const expected = `GET /v1/discovery-chain/${id}?dc=${dc}`;
const actual = adapter.requestForQueryRecord(client.url, { const actual = adapter.requestForQueryRecord(client.requestParams.bind(client), {
dc: dc, dc: dc,
id: id, id: id,
}); });
assert.equal(actual, expected); assert.equal(`${actual.method} ${actual.url}`, expected);
}); });
test("requestForQueryRecord throws if you don't specify an id", function(assert) { test("requestForQueryRecord throws if you don't specify an id", function(assert) {
const adapter = this.owner.lookup('adapter:discovery-chain'); const adapter = this.owner.lookup('adapter:discovery-chain');

View file

@ -8,10 +8,10 @@ module('Integration | Adapter | intention', function(hooks) {
const adapter = this.owner.lookup('adapter:intention'); const adapter = this.owner.lookup('adapter:intention');
const client = this.owner.lookup('service:client/http'); const client = this.owner.lookup('service:client/http');
const expected = `GET /v1/connect/intentions?dc=${dc}`; const expected = `GET /v1/connect/intentions?dc=${dc}`;
const actual = adapter.requestForQuery(client.url, { const actual = adapter.requestForQuery(client.requestParams.bind(client), {
dc: dc, dc: dc,
}); });
assert.equal(actual, expected); assert.equal(`${actual.method} ${actual.url}`, expected);
}); });
test('requestForQueryRecord returns the correct url', function(assert) { test('requestForQueryRecord returns the correct url', function(assert) {
const adapter = this.owner.lookup('adapter:intention'); const adapter = this.owner.lookup('adapter:intention');

View file

@ -8,20 +8,20 @@ module('Integration | Adapter | node', function(hooks) {
const adapter = this.owner.lookup('adapter:node'); const adapter = this.owner.lookup('adapter:node');
const client = this.owner.lookup('service:client/http'); const client = this.owner.lookup('service:client/http');
const expected = `GET /v1/internal/ui/nodes?dc=${dc}`; const expected = `GET /v1/internal/ui/nodes?dc=${dc}`;
const actual = adapter.requestForQuery(client.url, { const actual = adapter.requestForQuery(client.requestParams.bind(client), {
dc: dc, dc: dc,
}); });
assert.equal(actual, expected); assert.equal(`${actual.method} ${actual.url}`, expected);
}); });
test('requestForQueryRecord returns the correct url', function(assert) { test('requestForQueryRecord returns the correct url', function(assert) {
const adapter = this.owner.lookup('adapter:node'); const adapter = this.owner.lookup('adapter:node');
const client = this.owner.lookup('service:client/http'); const client = this.owner.lookup('service:client/http');
const expected = `GET /v1/internal/ui/node/${id}?dc=${dc}`; const expected = `GET /v1/internal/ui/node/${id}?dc=${dc}`;
const actual = adapter.requestForQueryRecord(client.url, { const actual = adapter.requestForQueryRecord(client.requestParams.bind(client), {
dc: dc, dc: dc,
id: id, id: id,
}); });
assert.equal(actual, expected); assert.equal(`${actual.method} ${actual.url}`, expected);
}); });
test("requestForQueryRecord throws if you don't specify an id", function(assert) { test("requestForQueryRecord throws if you don't specify an id", function(assert) {
const adapter = this.owner.lookup('adapter:node'); const adapter = this.owner.lookup('adapter:node');

View file

@ -9,8 +9,8 @@ module('Integration | Adapter | nspace', function(hooks) {
const adapter = this.owner.lookup('adapter:nspace'); const adapter = this.owner.lookup('adapter:nspace');
const client = this.owner.lookup('service:client/http'); const client = this.owner.lookup('service:client/http');
const expected = `GET /v1/namespaces`; const expected = `GET /v1/namespaces`;
const actual = adapter.requestForQuery(client.url, {}); const actual = adapter.requestForQuery(client.requestParams.bind(client), {});
assert.equal(actual, expected); assert.equal(`${actual.method} ${actual.url}`, expected);
}); });
test('requestForQueryRecord returns the correct url/method', function(assert) { test('requestForQueryRecord returns the correct url/method', function(assert) {
const adapter = this.owner.lookup('adapter:nspace'); const adapter = this.owner.lookup('adapter:nspace');

View file

@ -1,12 +1,12 @@
import { module, test } from 'qunit'; import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit'; import { setupTest } from 'ember-qunit';
module('Unit | Controller | dc/nodes/show', function(hooks) { module('Unit | Service | encoder', function(hooks) {
setupTest(hooks); setupTest(hooks);
// Replace this with your real tests. // Replace this with your real tests.
test('it exists', function(assert) { test('it exists', function(assert) {
let controller = this.owner.lookup('controller:dc/nodes/show'); let service = this.owner.lookup('service:encoder');
assert.ok(controller); assert.ok(service);
}); });
}); });