ui: Rearrange Service detail page to load Topology and Routing tabs separately (#9401)

This commit is contained in:
John Cowen 2020-12-16 09:18:29 +00:00 committed by GitHub
parent b9a9f395d6
commit d602b303e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 184 additions and 191 deletions

View File

@ -1,12 +1,10 @@
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import { alias } from '@ember/object/computed';
import Controller from '@ember/controller'; import Controller from '@ember/controller';
import { get, action } from '@ember/object'; import { get, action } from '@ember/object';
export default class ShowController extends Controller { export default class ShowController extends Controller {
@service('flashMessages') notify; @service('flashMessages') notify;
@alias('items.firstObject') item;
@action @action
error(e) { error(e) {
if (e.target.readyState === 1) { if (e.target.readyState === 1) {
@ -19,14 +17,7 @@ export default class ShowController extends Controller {
action: 'update', action: 'update',
}); });
} }
[ [e.target, this.proxies].forEach(function(item) {
e.target,
this.intentions,
this.chain,
this.proxies,
this.gatewayServices,
this.topology,
].forEach(function(item) {
if (item && typeof item.close === 'function') { if (item && typeof item.close === 'function') {
item.close(); item.close();
} }

View File

@ -1,6 +1,5 @@
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import Route from 'consul-ui/routing/route'; import Route from 'consul-ui/routing/route';
import { hash, Promise } from 'rsvp';
import { get, action } from '@ember/object'; import { get, action } from '@ember/object';
// TODO: We should potentially move all these nspace related things // TODO: We should potentially move all these nspace related things
@ -24,49 +23,35 @@ const findActiveNspace = function(nspaces, nspace) {
return found; return found;
}; };
export default class DcRoute extends Route { export default class DcRoute extends Route {
@service('repository/dc') @service('repository/dc') repo;
repo; @service('repository/nspace/disabled') nspacesRepo;
@service('settings') settingsRepo;
@service('repository/nspace/disabled') async model(params) {
nspacesRepo;
@service('settings')
settingsRepo;
model(params) {
const app = this.modelFor('application'); const app = this.modelFor('application');
return hash({
nspace: this.nspacesRepo.getActive(), let [token, nspace, dc] = await Promise.all([
token: this.settingsRepo.findBySlug('token'), this.settingsRepo.findBySlug('token'),
dc: this.repo.findBySlug(params.dc, app.dcs), this.nspacesRepo.getActive(),
}) this.repo.findBySlug(params.dc, app.dcs),
.then(function(model) { ]);
return hash({ // if there is only 1 namespace then use that
...model, // otherwise find the namespace object that corresponds
...{ // to the active one
// if there is only 1 namespace then use that nspace =
// otherwise find the namespace object that corresponds app.nspaces.length > 1 ? findActiveNspace(app.nspaces, nspace) : app.nspaces.firstObject;
// to the active one
nspace: let permissions;
app.nspaces.length > 1 if (get(token, 'SecretID')) {
? findActiveNspace(app.nspaces, model.nspace) // When disabled nspaces is [], so nspace is undefined
: app.nspaces.firstObject, permissions = await this.nspacesRepo.authorize(params.dc, get(nspace || {}, 'Name'));
}, }
}); return {
}) dc,
.then(model => { nspace,
if (get(model, 'token.SecretID')) { token,
return hash({ permissions,
...model, };
...{
// When disabled nspaces is [], so nspace is undefined
permissions: this.nspacesRepo.authorize(params.dc, get(model, 'nspace.Name')),
},
});
} else {
return model;
}
});
} }
setupController(controller, model) { setupController(controller, model) {

View File

@ -1,68 +1,51 @@
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import Route from 'consul-ui/routing/route'; import Route from 'consul-ui/routing/route';
import { get } from '@ember/object'; import { get } from '@ember/object';
import { action } from '@ember/object';
export default class ShowRoute extends Route { export default class ShowRoute extends Route {
@service('data-source/service') data; @service('data-source/service') data;
@service('repository/intention') repo;
@service('ui-config') config; @service('ui-config') config;
@action
async createIntention(source, destination) {
const model = this.repo.create({
Datacenter: source.Datacenter,
SourceName: source.Name,
SourceNS: source.Namespace || 'default',
DestinationName: destination.Name,
DestinationNS: destination.Namespace || 'default',
Action: 'allow',
});
await this.repo.persist(model);
this.refresh();
}
async model(params, transition) { async model(params, transition) {
const dc = this.modelFor('dc').dc.Name; const dc = this.modelFor('dc').dc;
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 = null;
let topology = null;
let proxies = []; let proxies = [];
const urls = this.config.get().dashboard_url_templates; const urls = this.config.get().dashboard_url_templates;
const items = await this.data.source( const items = await this.data.source(
uri => uri`/${nspace}/${dc}/service-instances/for-service/${params.name}` uri => uri`/${nspace}/${dc.Name}/service-instances/for-service/${params.name}`
); );
const item = get(items, 'firstObject'); const item = get(items, 'firstObject');
if (get(item, 'IsOrigin')) { if (get(item, 'IsOrigin')) {
chain = await this.data.source(uri => uri`/${nspace}/${dc}/discovery-chain/${params.name}`); proxies = this.data.source(
proxies = await this.data.source( uri => uri`/${nspace}/${dc.Name}/proxies/for-service/${params.name}`
uri => uri`/${nspace}/${dc}/proxies/for-service/${params.name}`
); );
// TODO: Temporary ping to see if a dc is MeshEnabled which we use in
if (get(item, 'IsMeshOrigin')) { // order to decide whether to show certain tabs in the template. This is
let kind = get(item, 'Service.Kind'); // a bit of a weird place to do this but we are trying to avoid wasting
if (typeof kind === 'undefined') { // HTTP requests and as disco chain is the most likely to be reused, we
kind = ''; // 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
topology = await this.data.source( // higher up the routing hierarchy instead.
uri => uri`/${nspace}/${dc}/topology/${params.name}/${kind}` let chain = this.data.source(
); uri => uri`/${nspace}/${dc.Name}/discovery-chain/${params.name}`
} );
[chain, proxies] = await Promise.all([chain, proxies]);
// 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
// reopened and reused
chain.close();
} }
return { return {
dc, dc,
nspace, nspace,
slug, slug,
items, items,
urls, urls,
chain,
proxies, proxies,
topology,
}; };
} }

View File

@ -2,21 +2,20 @@ import Route from '@ember/routing/route';
import { get } from '@ember/object'; import { get } from '@ember/object';
export default class IndexRoute extends Route { export default class IndexRoute extends Route {
afterModel(model, transition) { async afterModel(model, transition) {
const parent = this.routeName const parent = this.routeName
.split('.') .split('.')
.slice(0, -1) .slice(0, -1)
.join('.'); .join('.');
// the default selected tab depends on whether you have any healthchecks or not
// so check the length here.
let to = 'topology'; let to = 'topology';
const parentModel = this.modelFor(parent); const parentModel = this.modelFor(parent);
const hasProxy = get(parentModel, 'proxies.length') !== 0; const hasProxy = get(parentModel, 'proxies.length') !== 0;
const kind = get(parentModel, 'items.firstObject.Service.Kind'); const item = get(parentModel, 'items.firstObject');
const kind = get(item, 'Service.Kind');
const hasTopology = get(parentModel, 'dc.MeshEnabled') && get(item, 'IsMeshOrigin');
switch (kind) { switch (kind) {
case 'ingress-gateway': case 'ingress-gateway':
if (!get(parentModel, 'topology.Datacenter')) { if (!hasTopology) {
to = 'upstreams'; to = 'upstreams';
} }
break; break;
@ -27,11 +26,10 @@ export default class IndexRoute extends Route {
to = 'instances'; to = 'instances';
break; break;
default: default:
if (!hasProxy || !get(parentModel, 'topology.Datacenter')) { if (!hasProxy || !hasTopology) {
to = 'instances'; to = 'instances';
} }
} }
this.replaceWith(`${parent}.${to}`, parentModel); this.replaceWith(`${parent}.${to}`, parentModel);
} }
} }

View File

@ -15,7 +15,7 @@ export default class InstancesRoute extends Route {
}, },
}; };
model() { async model() {
const parent = this.routeName const parent = this.routeName
.split('.') .split('.')
.slice(0, -1) .slice(0, -1)

View File

@ -1,16 +1,25 @@
import Route from 'consul-ui/routing/route'; import Route from 'consul-ui/routing/route';
import { inject as service } from '@ember/service';
import { get } from '@ember/object'; import { get } from '@ember/object';
export default class RoutingRoute extends Route { export default class RoutingRoute extends Route {
model() { @service('data-source/service') data;
async model(params, transition) {
const parent = this.routeName const parent = this.routeName
.split('.') .split('.')
.slice(0, -1) .slice(0, -1)
.join('.'); .join('.');
return this.modelFor(parent); const model = this.modelFor(parent);
return {
...model,
chain: await this.data.source(
uri => uri`/${model.nspace}/${model.dc.Name}/discovery-chain/${model.slug}`
),
};
} }
afterModel(model, transition) { async afterModel(model, transition) {
if (!get(model, 'chain')) { if (!get(model, 'chain')) {
const parent = this.routeName const parent = this.routeName
.split('.') .split('.')

View File

@ -1,18 +1,48 @@
import Route from '@ember/routing/route'; import Route from '@ember/routing/route';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
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('env') env;
@service('data-source/service') data;
@service('repository/intention') repo;
model() { @action
async createIntention(source, destination) {
const model = this.repo.create({
Datacenter: source.Datacenter,
SourceName: source.Name,
SourceNS: source.Namespace || 'default',
DestinationName: destination.Name,
DestinationNS: destination.Namespace || 'default',
Action: 'allow',
});
await this.repo.persist(model);
this.refresh();
}
async model() {
const parent = this.routeName const parent = this.routeName
.split('.') .split('.')
.slice(0, -1) .slice(0, -1)
.join('.'); .join('.');
const model = this.modelFor(parent);
const dc = get(model, 'dc');
const nspace = get(model, 'nspace');
const item = get(model, 'items.firstObject');
if (get(item, 'IsMeshOrigin')) {
let kind = get(item, 'Service.Kind');
if (typeof kind === 'undefined') {
kind = '';
}
model.topology = await this.data.source(
uri => uri`/${nspace}/${dc.Name}/topology/${model.slug}/${kind}`
);
}
return { return {
...this.modelFor(parent), ...model,
hasMetricsProvider: !!this.config.get().metrics_provider, hasMetricsProvider: !!this.config.get().metrics_provider,
isRemoteDC: this.env.var('CONSUL_DATACENTER_LOCAL') !== this.modelFor('dc').dc.Name, isRemoteDC: this.env.var('CONSUL_DATACENTER_LOCAL') !== this.modelFor('dc').dc.Name,
}; };

View File

@ -12,18 +12,16 @@ export default class DcService extends RepositoryService {
return modelName; return modelName;
} }
findAll() { async findAll() {
return this.store.query(this.getModelName(), {}); return this.store.query(this.getModelName(), {});
} }
findBySlug(name, items) { async findBySlug(name, items) {
if (name != null) { if (name != null) {
const item = items.findBy('Name', name); const item = await items.findBy('Name', name);
if (item) { if (typeof item !== 'undefined') {
return this.settings.persist({ dc: get(item, 'Name') }).then(function() { await this.settings.persist({ dc: get(item, 'Name') });
// TODO: create a model return item;
return { Name: get(item, 'Name') };
});
} }
} }
const e = new Error('Page not found'); const e = new Error('Page not found');
@ -31,22 +29,21 @@ export default class DcService extends RepositoryService {
return Promise.reject({ errors: [e] }); return Promise.reject({ errors: [e] });
} }
getActive(name, items) { async getActive(name, items) {
const settings = this.settings; return Promise.all([name || this.settings.findBySlug('dc'), items || this.findAll()]).then(
return Promise.all([name || settings.findBySlug('dc'), items || this.findAll()]).then(
([name, items]) => { ([name, items]) => {
return this.findBySlug(name, items).catch(e => { return this.findBySlug(name, items).catch(async e => {
const item = const item =
items.findBy('Name', this.env.var('CONSUL_DATACENTER_LOCAL')) || items.findBy('Name', this.env.var('CONSUL_DATACENTER_LOCAL')) ||
get(items, 'firstObject'); get(items, 'firstObject');
settings.persist({ dc: get(item, 'Name') }); await this.settings.persist({ dc: get(item, 'Name') });
return item; return item;
}); });
} }
); );
} }
clearActive() { async clearActive() {
return this.settings.delete('dc'); return this.settings.delete('dc');
} }
} }

View File

@ -1,75 +1,73 @@
<EventSource @src={{items}} @onerror={{action "error"}} /> <EventSource @src={{items}} @onerror={{action "error"}} />
<EventSource @src={{chain}} />
<EventSource @src={{intentions}} />
<EventSource @src={{proxies}} /> <EventSource @src={{proxies}} />
<EventSource @src={{gatewayServices}} /> {{#let items.firstObject as |item|}}
<EventSource @src={{topology}} /> {{page-title item.Service.Service}}
{{page-title item.Service.Service}} <AppView>
<AppView> <BlockSlot @name="notification" as |status type|>
<BlockSlot @name="notification" as |status type|> <Consul::Service::Notifications
<Consul::Service::Notifications @type={{type}}
@type={{type}} @status={{status}}
@status={{status}} />
/> </BlockSlot>
</BlockSlot> <BlockSlot @name="breadcrumbs">
<BlockSlot @name="breadcrumbs"> <ol>
<ol> <li><a data-test-back href={{href-to 'dc.services'}}>All Services</a></li>
<li><a data-test-back href={{href-to 'dc.services'}}>All Services</a></li> </ol>
</ol> </BlockSlot>
</BlockSlot> <BlockSlot @name="header">
<BlockSlot @name="header"> <h1>
<h1> {{item.Service.Service}}
{{item.Service.Service}} </h1>
</h1> <Consul::ExternalSource @item={{item.Service}} />
<Consul::ExternalSource @item={{item.Service}} /> <Consul::Kind @item={{item.Service}} @withInfo={{true}} />
<Consul::Kind @item={{item.Service}} @withInfo={{true}} /> </BlockSlot>
</BlockSlot> <BlockSlot @name="nav">
<BlockSlot @name="nav"> {{#if (not-eq item.Service.Kind 'mesh-gateway')}}
{{#if (not-eq item.Service.Kind 'mesh-gateway')}} <TabNav @items={{
<TabNav @items={{ compact
compact (array
(array (if (and dc.MeshEnabled item.IsMeshOrigin (or (gt proxies.length 0) (eq item.Service.Kind 'ingress-gateway')))
(if (and topology.Datacenter (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"))
(hash label="Topology" href=(href-to "dc.services.show.topology") selected=(is-href "dc.services.show.topology")) '')
'') (if (eq item.Service.Kind 'terminating-gateway')
(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"))
(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')
(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="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"))
(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')
(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"))
(hash label="Intentions" href=(href-to "dc.services.show.intentions") selected=(is-href "dc.services.show.intentions")) '')
'') (if (and dc.MeshEnabled item.IsOrigin)
(if chain.Chain (hash label="Routing" href=(href-to "dc.services.show.routing") selected=(is-href "dc.services.show.routing"))
(hash label="Routing" href=(href-to "dc.services.show.routing") selected=(is-href "dc.services.show.routing")) '')
'') (if (not item.Service.Kind)
(if (not item.Service.Kind) (hash label="Tags" href=(href-to "dc.services.show.tags") selected=(is-href "dc.services.show.tags"))
(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
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"> <BlockSlot @name="actions">
<Outlet {{#if urls.service}}
@name={{routeName}} <a href={{render-template urls.service (hash
as |o|> Datacenter=dc.Name
{{outlet}} Service=(hash Name=item.Service.Service)
</Outlet> )}}
</BlockSlot> target="_blank"
</AppView> rel="noopener noreferrer"
data-test-dashboard-anchor>
Open Dashboard
</a>
{{/if}}
</BlockSlot>
<BlockSlot @name="content">
<Outlet
@name={{routeName}}
as |o|>
{{outlet}}
</Outlet>
</BlockSlot>
</AppView>
{{/let}}

View File

@ -1,3 +1,4 @@
<EventSource @src={{chain}} />
<div id="routing" class="tab-section"> <div id="routing" class="tab-section">
<div role="tabpanel"> <div role="tabpanel">
<Consul::DiscoveryChain <Consul::DiscoveryChain

View File

@ -1,3 +1,4 @@
<EventSource @src={{topology}} />
<div id="topology" class="tab-section"> <div id="topology" class="tab-section">
<div role="tabpanel"> <div role="tabpanel">
{{#if topology.FilteredByACLs}} {{#if topology.FilteredByACLs}}
@ -6,11 +7,11 @@
{{#if topology}} {{#if topology}}
<TopologyMetrics <TopologyMetrics
@nspace={{nspace}} @nspace={{nspace}}
@dc={{dc}} @dc={{dc.Name}}
@service={{items.firstObject}} @service={{items.firstObject}}
@topology={{topology}} @topology={{topology}}
@metricsHref={{render-template urls.service (hash @metricsHref={{render-template urls.service (hash
Datacenter=dc Datacenter=dc.Name
Service=items.firstObject Service=items.firstObject
)}} )}}
@isRemoteDC={{isRemoteDC}} @isRemoteDC={{isRemoteDC}}