ui: Misc changes for Catalog area (#9414)

* Use DataLoader errors for Service Detail and Service Instance

* uiCfg > config use the repo-like async interface where possible

* Clean up node show

* Make sure you can put `=` in dev cookie values

* Never default to default

* Tweak chain variable

* Remove env service

* Pass chain through to the template for the tempalte to clean it up

* Delete controller tests

* Remove cleanup in Nodes show as this is still being used in another tab

* Use dc.Local
This commit is contained in:
John Cowen 2020-12-17 16:35:01 +00:00 committed by GitHub
parent 325bca338b
commit ae049feeab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 210 additions and 245 deletions

View File

@ -1,6 +0,0 @@
{{#if (eq @type 'update')}}
{{#if (eq @status 'warning') }}
This service has been deregistered and no longer exists in the catalog.
{{else}}
{{/if}}
{{/if}}

View File

@ -1,28 +0,0 @@
import { inject as service } from '@ember/service';
import Controller from '@ember/controller';
import { get, action } from '@ember/object';
export default class InstanceController extends Controller {
@service('flashMessages')
notify;
@action
error(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.proxy].forEach(function(item) {
if (item && typeof item.close === 'function') {
item.close();
}
});
}
}
}
}

View File

@ -1,27 +0,0 @@
import { inject as service } from '@ember/service';
import Controller from '@ember/controller';
import { get, action } from '@ember/object';
export default class ShowController extends Controller {
@service('flashMessages') notify;
@action
error(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.proxies].forEach(function(item) {
if (item && typeof item.close === 'function') {
item.close();
}
});
}
}
}

View File

@ -7,7 +7,7 @@ export default class InstanceRoute extends Route {
async model(params, transition) { async model(params, transition) {
const dc = this.modelFor('dc').dc.Name; const dc = this.modelFor('dc').dc.Name;
const nspace = this.modelFor('nspace').nspace.substr(1) || 'default'; const nspace = this.modelFor('nspace').nspace.substr(1);
const item = await this.data.source( const item = await this.data.source(
uri => uri`/${nspace}/${dc}/service-instance/${params.id}/${params.node}/${params.name}` uri => uri`/${nspace}/${dc}/service-instance/${params.id}/${params.node}/${params.name}`

View File

@ -11,9 +11,10 @@ export default class ShowRoute extends Route {
const nspace = this.modelFor('nspace').nspace.substr(1); const nspace = this.modelFor('nspace').nspace.substr(1);
const slug = params.name; const slug = params.name;
let chain;
let proxies = []; let proxies = [];
const urls = this.config.get().dashboard_url_templates; const urls = await this.config.findByPath('dashboard_url_templates');
const items = await this.data.source( const items = await this.data.source(
uri => uri`/${nspace}/${dc.Name}/service-instances/for-service/${params.name}` uri => uri`/${nspace}/${dc.Name}/service-instances/for-service/${params.name}`
); );
@ -30,9 +31,7 @@ export default class ShowRoute extends Route {
// use that endpoint here. Eventually if we have an endpoint specific to // use that endpoint here. Eventually if we have an endpoint specific to
// a dc that gives us more DC specific info we can use that instead // a dc that gives us more DC specific info we can use that instead
// higher up the routing hierarchy instead. // higher up the routing hierarchy instead.
let chain = this.data.source( chain = this.data.source(uri => uri`/${nspace}/${dc.Name}/discovery-chain/${params.name}`);
uri => uri`/${nspace}/${dc.Name}/discovery-chain/${params.name}`
);
[chain, proxies] = await Promise.all([chain, proxies]); [chain, proxies] = await Promise.all([chain, proxies]);
// we close the chain for now, if you enter the routing tab before the // we close the chain for now, if you enter the routing tab before the
// EventSource comes around to request again, this one will just be // EventSource comes around to request again, this one will just be
@ -45,6 +44,7 @@ export default class ShowRoute extends Route {
slug, slug,
items, items,
urls, urls,
chain,
proxies, proxies,
}; };
} }

View File

@ -4,7 +4,6 @@ import { get, action } from '@ember/object';
export default class TopologyRoute extends Route { export default class TopologyRoute extends Route {
@service('ui-config') config; @service('ui-config') config;
@service('env') env;
@service('data-source/service') data; @service('data-source/service') data;
@service('repository/intention') repo; @service('repository/intention') repo;
@ -32,19 +31,20 @@ export default class TopologyRoute extends Route {
const nspace = get(model, 'nspace'); const nspace = get(model, 'nspace');
const item = get(model, 'items.firstObject'); const item = get(model, 'items.firstObject');
if (get(item, 'IsMeshOrigin')) { let kind = get(item, 'Service.Kind');
let kind = get(item, 'Service.Kind'); if (typeof kind === 'undefined') {
if (typeof kind === 'undefined') { kind = '';
kind = '';
}
model.topology = await this.data.source(
uri => uri`/${nspace}/${dc.Name}/topology/${model.slug}/${kind}`
);
} }
const topology = await this.data.source(
uri => uri`/${nspace}/${dc.Name}/topology/${model.slug}/${kind}`
);
let hasMetricsProvider = await this.config.findByPath('metrics_provider');
hasMetricsProvider = !!hasMetricsProvider;
return { return {
...model, ...model,
hasMetricsProvider: !!this.config.get().metrics_provider, topology,
isRemoteDC: this.env.var('CONSUL_DATACENTER_LOCAL') !== this.modelFor('dc').dc.Name, hasMetricsProvider,
}; };
} }

View File

@ -5,7 +5,7 @@ import RepositoryService from 'consul-ui/services/repository';
// metrics provider // metrics provider
export default class MetricsService extends RepositoryService { export default class MetricsService extends RepositoryService {
@service('ui-config') cfg; @service('ui-config') config;
@service('env') env; @service('env') env;
@service('client/http') client; @service('client/http') client;
@ -13,16 +13,16 @@ export default class MetricsService extends RepositoryService {
init() { init() {
super.init(...arguments); super.init(...arguments);
const uiCfg = this.cfg.get(); const config = this.config.get();
// Inject whether or not the proxy is enabled as an option into the opaque // Inject whether or not the proxy is enabled as an option into the opaque
// JSON options the user provided. // JSON options the user provided.
const opts = uiCfg.metrics_provider_options || {}; const opts = config.metrics_provider_options || {};
opts.metrics_proxy_enabled = uiCfg.metrics_proxy_enabled; opts.metrics_proxy_enabled = config.metrics_proxy_enabled;
// Inject a convenience function for dialing through the metrics proxy. // Inject a convenience function for dialing through the metrics proxy.
opts.fetch = (path, params) => opts.fetch = (path, params) =>
this.client.fetchWithToken(`/v1/internal/ui/metrics-proxy${path}`, params); this.client.fetchWithToken(`/v1/internal/ui/metrics-proxy${path}`, params);
// Inject the base app URL // Inject the base app URL
const provider = uiCfg.metrics_provider || 'prometheus'; const provider = config.metrics_provider || 'prometheus';
try { try {
this.provider = window.consul.getMetricsProvider(provider, opts); this.provider = window.consul.getMetricsProvider(provider, opts);

View File

@ -1,17 +1,17 @@
{{page-title item.Node}} {{page-title item.Node}}
<DataLoader as |api|> <DataLoader as |loader|>
<BlockSlot @name="data"> <BlockSlot @name="data">
<EventSource @src={{item}} @onerror={{action loader.dispatchError}} />
<EventSource @src={{tomography}} /> <EventSource @src={{tomography}} />
<EventSource @src={{item}} @onerror={{queue (action api.dispatchError)}} />
</BlockSlot> </BlockSlot>
<BlockSlot @name="error"> <BlockSlot @name="error">
<AppError @error={{api.error}} /> <AppError @error={{loader.error}} />
</BlockSlot> </BlockSlot>
<BlockSlot @name="disconnected" as |Notification|> <BlockSlot @name="disconnected" as |Notification|>
{{#if (eq api.error.status "404")}} {{#if (eq loader.error.status "404")}}
<Notification @sticky={{true}}> <Notification @sticky={{true}}>
<p data-notification role="alert" class="warning notification-update"> <p data-notification role="alert" class="warning notification-update">
<strong>Warning!</strong> <strong>Warning!</strong>

View File

@ -21,12 +21,6 @@
as |sort filters items|}} as |sort filters items|}}
<AppView> <AppView>
<BlockSlot @name="notification" as |status type|>
<Consul::Service::Notifications
@type={{type}}
@status={{status}}
/>
</BlockSlot>
<BlockSlot @name="header"> <BlockSlot @name="header">
<h1> <h1>
Services <em>{{format-number items.length}} total</em> Services <em>{{format-number items.length}} total</em>

View File

@ -1,64 +1,90 @@
{{page-title item.Service.ID}} {{page-title item.Service.ID}}
<EventSource @src={{item}} @onerror={{action "error"}} /> <DataLoader as |loader|>
<EventSource @src={{proxy}} />
<EventSource @src={{proxyMeta}} /> <BlockSlot @name="data">
<AppView> <EventSource @src={{item}} @onerror={{action loader.dispatchError}} />
<BlockSlot @name="notification" as |status type|> {{#if (not loader.error)}}
<Consul::Service::Notifications <EventSource @src={{proxy}} />
@type={{type}} <EventSource @src={{proxyMeta}} />
@status={{status}} {{/if}}
/>
</BlockSlot> </BlockSlot>
<BlockSlot @name="breadcrumbs">
<ol> <BlockSlot @name="error">
<li><a data-test-back href={{href-to 'dc.services'}}>All Services</a></li> <AppError @error={{loader.error}} />
<li><a data-test-back href={{href-to 'dc.services.show'}}>Service ({{item.Service.Service}})</a></li>
</ol>
</BlockSlot> </BlockSlot>
<BlockSlot @name="header">
<h1> <BlockSlot @name="disconnected" as |Notification|>
{{ item.Service.ID }} {{#if (eq loader.error.status "404")}}
</h1> <Notification @sticky={{true}}>
<Consul::ExternalSource @item={{item}} /> <p data-notification role="alert" class="warning notification-update">
<Consul::Kind @item={{item}} @withInfo={{true}} /> <strong>Warning!</strong>
This service has been deregistered and no longer exists in the catalog.
</p>
</Notification>
{{else}}
<Notification @sticky={{true}}>
<p data-notification role="alert" class="warning notification-update">
<strong>Warning!</strong>
An error was returned whilst loading this data, refresh to try again.
</p>
</Notification>
{{/if}}
</BlockSlot> </BlockSlot>
<BlockSlot @name="nav">
<dl> <BlockSlot @name="loaded">
<dt>Service Name</dt> <AppView>
<dd><a href="{{href-to 'dc.services.show' item.Service.Service}}">{{item.Service.Service}}</a></dd> <BlockSlot @name="breadcrumbs">
</dl> <ol>
<dl> <li><a data-test-back href={{href-to 'dc.services'}}>All Services</a></li>
<dt>Node Name</dt> <li><a data-test-back href={{href-to 'dc.services.show'}}>Service ({{item.Service.Service}})</a></li>
<dd><a href="{{href-to 'dc.nodes.show' item.Node.Node}}">{{item.Node.Node}}</a></dd> </ol>
</dl> </BlockSlot>
<BlockSlot @name="header">
<h1>
{{ item.Service.ID }}
</h1>
<Consul::ExternalSource @item={{item}} />
<Consul::Kind @item={{item}} @withInfo={{true}} />
</BlockSlot>
<BlockSlot @name="nav">
<dl>
<dt>Service Name</dt>
<dd><a href="{{href-to 'dc.services.show' item.Service.Service}}">{{item.Service.Service}}</a></dd>
</dl>
<dl>
<dt>Node Name</dt>
<dd><a href="{{href-to 'dc.nodes.show' item.Node.Node}}">{{item.Node.Node}}</a></dd>
</dl>
</BlockSlot>
<BlockSlot @name="actions">
{{#let (or item.Service.Address item.Node.Address) as |address|}}
<CopyButton @value={{address}} @name="Address">{{address}}</CopyButton>
{{/let}}
</BlockSlot>
<BlockSlot @name="content">
<TabNav @items={{
compact
(array
(hash label="Health Checks" href=(href-to "dc.services.instance.healthchecks") selected=(is-href "dc.services.instance.healthchecks"))
(if
(eq item.Service.Kind 'mesh-gateway')
(hash label="Addresses" href=(href-to "dc.services.instance.addresses") selected=(is-href "dc.services.instance.addresses")) ""
)
(if proxy
(hash label="Upstreams" href=(href-to "dc.services.instance.upstreams") selected=(is-href "dc.services.instance.upstreams"))
)
(if proxy
(hash label="Exposed Paths" href=(href-to "dc.services.instance.exposedpaths") selected=(is-href "dc.services.instance.exposedpaths"))
)
(hash label="Tags & Meta" href=(href-to "dc.services.instance.metadata") selected=(is-href "dc.services.instance.metadata"))
)
}}/>
<Outlet
@name={{routeName}}
as |o|>
{{outlet}}
</Outlet>
</BlockSlot>
</AppView>
</BlockSlot> </BlockSlot>
<BlockSlot @name="actions"> </DataLoader>
{{#let (or item.Service.Address item.Node.Address) as |address|}}
<CopyButton @value={{address}} @name="Address">{{address}}</CopyButton>
{{/let}}
</BlockSlot>
<BlockSlot @name="content">
<TabNav @items={{
compact
(array
(hash label="Health Checks" href=(href-to "dc.services.instance.healthchecks") selected=(is-href "dc.services.instance.healthchecks"))
(if
(eq item.Service.Kind 'mesh-gateway')
(hash label="Addresses" href=(href-to "dc.services.instance.addresses") selected=(is-href "dc.services.instance.addresses")) ""
)
(if proxy
(hash label="Upstreams" href=(href-to "dc.services.instance.upstreams") selected=(is-href "dc.services.instance.upstreams"))
)
(if proxy
(hash label="Exposed Paths" href=(href-to "dc.services.instance.exposedpaths") selected=(is-href "dc.services.instance.exposedpaths"))
)
(hash label="Tags & Meta" href=(href-to "dc.services.instance.metadata") selected=(is-href "dc.services.instance.metadata"))
)
}}/>
<Outlet
@name={{routeName}}
as |o|>
{{outlet}}
</Outlet>
</BlockSlot>
</AppView>

View File

@ -1,73 +1,100 @@
<EventSource @src={{items}} @onerror={{action "error"}} />
<EventSource @src={{proxies}} />
{{#let items.firstObject as |item|}} {{#let items.firstObject as |item|}}
{{page-title item.Service.Service}} {{page-title item.Service.Service}}
<AppView> <DataLoader as |loader|>
<BlockSlot @name="notification" as |status type|>
<Consul::Service::Notifications <BlockSlot @name="data">
@type={{type}} <EventSource @src={{items}} @onerror={{action loader.dispatchError}} />
@status={{status}} {{#if (not loader.error)}}
/> <EventSource @src={{proxies}} />
</BlockSlot> <EventSource @src={{chain}} />
<BlockSlot @name="breadcrumbs">
<ol>
<li><a data-test-back href={{href-to 'dc.services'}}>All Services</a></li>
</ol>
</BlockSlot>
<BlockSlot @name="header">
<h1>
{{item.Service.Service}}
</h1>
<Consul::ExternalSource @item={{item.Service}} />
<Consul::Kind @item={{item.Service}} @withInfo={{true}} />
</BlockSlot>
<BlockSlot @name="nav">
{{#if (not-eq item.Service.Kind 'mesh-gateway')}}
<TabNav @items={{
compact
(array
(if (and dc.MeshEnabled item.IsMeshOrigin (or (gt proxies.length 0) (eq item.Service.Kind 'ingress-gateway')))
(hash label="Topology" href=(href-to "dc.services.show.topology") selected=(is-href "dc.services.show.topology"))
'')
(if (eq item.Service.Kind 'terminating-gateway')
(hash label="Linked Services" href=(href-to "dc.services.show.services") selected=(is-href "dc.services.show.services"))
'')
(if (eq item.Service.Kind 'ingress-gateway')
(hash label="Upstreams" href=(href-to "dc.services.show.upstreams") selected=(is-href "dc.services.show.upstreams"))
'')
(hash label="Instances" href=(href-to "dc.services.show.instances") selected=(is-href "dc.services.show.instances"))
(if (not-eq item.Service.Kind 'terminating-gateway')
(hash label="Intentions" href=(href-to "dc.services.show.intentions") selected=(is-href "dc.services.show.intentions"))
'')
(if (and dc.MeshEnabled item.IsOrigin)
(hash label="Routing" href=(href-to "dc.services.show.routing") selected=(is-href "dc.services.show.routing"))
'')
(if (not item.Service.Kind)
(hash label="Tags" href=(href-to "dc.services.show.tags") selected=(is-href "dc.services.show.tags"))
'')
)
}}/>
{{/if}}
</BlockSlot>
<BlockSlot @name="actions">
{{#if urls.service}}
<a href={{render-template urls.service (hash
Datacenter=dc.Name
Service=(hash Name=item.Service.Service)
)}}
target="_blank"
rel="noopener noreferrer"
data-test-dashboard-anchor>
Open Dashboard
</a>
{{/if}} {{/if}}
</BlockSlot> </BlockSlot>
<BlockSlot @name="content">
<Outlet <BlockSlot @name="error">
@name={{routeName}} <AppError @error={{loader.error}} />
as |o|>
{{outlet}}
</Outlet>
</BlockSlot> </BlockSlot>
</AppView>
<BlockSlot @name="disconnected" as |Notification|>
{{#if (eq loader.error.status "404")}}
<Notification @sticky={{true}}>
<p data-notification role="alert" class="warning notification-update">
<strong>Warning!</strong>
This service has been deregistered and no longer exists in the catalog.
</p>
</Notification>
{{else}}
<Notification @sticky={{true}}>
<p data-notification role="alert" class="warning notification-update">
<strong>Warning!</strong>
An error was returned whilst loading this data, refresh to try again.
</p>
</Notification>
{{/if}}
</BlockSlot>
<BlockSlot @name="loaded">
<AppView>
<BlockSlot @name="breadcrumbs">
<ol>
<li><a data-test-back href={{href-to 'dc.services'}}>All Services</a></li>
</ol>
</BlockSlot>
<BlockSlot @name="header">
<h1>
{{item.Service.Service}}
</h1>
<Consul::ExternalSource @item={{item.Service}} />
<Consul::Kind @item={{item.Service}} @withInfo={{true}} />
</BlockSlot>
<BlockSlot @name="nav">
{{#if (not-eq item.Service.Kind 'mesh-gateway')}}
<TabNav @items={{
compact
(array
(if (and dc.MeshEnabled item.IsMeshOrigin (or (gt proxies.length 0) (eq item.Service.Kind 'ingress-gateway')))
(hash label="Topology" href=(href-to "dc.services.show.topology") selected=(is-href "dc.services.show.topology"))
'')
(if (eq item.Service.Kind 'terminating-gateway')
(hash label="Linked Services" href=(href-to "dc.services.show.services") selected=(is-href "dc.services.show.services"))
'')
(if (eq item.Service.Kind 'ingress-gateway')
(hash label="Upstreams" href=(href-to "dc.services.show.upstreams") selected=(is-href "dc.services.show.upstreams"))
'')
(hash label="Instances" href=(href-to "dc.services.show.instances") selected=(is-href "dc.services.show.instances"))
(if (not-eq item.Service.Kind 'terminating-gateway')
(hash label="Intentions" href=(href-to "dc.services.show.intentions") selected=(is-href "dc.services.show.intentions"))
'')
(if (and dc.MeshEnabled item.IsOrigin)
(hash label="Routing" href=(href-to "dc.services.show.routing") selected=(is-href "dc.services.show.routing"))
'')
(if (not item.Service.Kind)
(hash label="Tags" href=(href-to "dc.services.show.tags") selected=(is-href "dc.services.show.tags"))
'')
)
}}/>
{{/if}}
</BlockSlot>
<BlockSlot @name="actions">
{{#if urls.service}}
<a href={{render-template urls.service (hash
Datacenter=dc.Name
Service=(hash Name=item.Service.Service)
)}}
target="_blank"
rel="noopener noreferrer"
data-test-dashboard-anchor>
Open Dashboard
</a>
{{/if}}
</BlockSlot>
<BlockSlot @name="content">
<Outlet
@name={{routeName}}
as |o|>
{{outlet}}
</Outlet>
</BlockSlot>
</AppView>
</BlockSlot>
</DataLoader>
{{/let}} {{/let}}

View File

@ -32,7 +32,7 @@
Datacenter=dc.Name Datacenter=dc.Name
Service=items.firstObject Service=items.firstObject
)}} )}}
@isRemoteDC={{isRemoteDC}} @isRemoteDC={{not dc.Local}}
@hasMetricsProvider={{hasMetricsProvider}} @hasMetricsProvider={{hasMetricsProvider}}
@oncreate={{route-action 'createIntention'}} @oncreate={{route-action 'createIntention'}}
/> />

View File

@ -23,7 +23,10 @@ export default function(config = {}, win = window, doc = document) {
return str return str
.split(';') .split(';')
.filter(item => item !== '') .filter(item => item !== '')
.map(item => item.trim().split('=')); .map(item => {
const [key, ...rest] = item.trim().split('=');
return [key, rest.join('=')];
});
}; };
const user = function(str) { const user = function(str) {
const item = win.localStorage.getItem(str); const item = win.localStorage.getItem(str);

View File

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

View File

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