mirror of
https://github.com/status-im/consul.git
synced 2025-02-16 07:38:22 +00:00
* Add data layer for discovery chain (model/adapter/serializer/repo) * Add routing plus template for routing tab * Add extra deps - consul-api-double upgrade plus ngraph for graphing * Add discovery-chain and related components and helpers: 1. discovery-chain to orchestrate/view controller 2. route-card, splitter-card, resolver card to represent the 3 different node types. 3. route-match helper for easy formatting of route rules 4. dom-position to figure out where things are in order to draw lines 5. svg-curve, simple wrapper around svg's <path d=""> attribute format. 6. data-structs service. This isn't super required but we are using other data-structures provided by other third party npm modules in other yet to be merged PRs. All of these types of things will live here for easy access/injection/changability 7. Some additions to our css-var 'polyfill' for a couple of extra needed rules * Related CSS for discovery chain 1. We add a %card base component here, eventually this will go into our base folder and %stats-card will also use it for a base component. 2. New icon for failovers * ui: Discovery Chain Continued (#6939) 1. Add in the things we use for the animations 2 Use IntersectionObserver so we know when the tab is visible, otherwise the dom-position helper won't work as the dom elements don't have any display. 3. Add some base work for animations and use them a little 4. Try to detect if a resolver is a redirect. Right now this works for datacenters and namespaces, but it can't work for services and subsets - we are awaiting backend support for doing this properly. 5. Add a fake 'this service has no routes' route that says 'Default' 6. redirect icon 7. Add CSS.escape polyfill for Edge
289 lines
9.1 KiB
JavaScript
289 lines
9.1 KiB
JavaScript
import Component from '@ember/component';
|
|
import { inject as service } from '@ember/service';
|
|
import { set, get, computed } from '@ember/object';
|
|
import { next } from '@ember/runloop';
|
|
|
|
const getNodesByType = function(nodes = {}, type) {
|
|
return Object.values(nodes).filter(item => item.Type === type);
|
|
};
|
|
|
|
const targetsToFailover = function(targets, a) {
|
|
let type;
|
|
const Targets = targets.map(function(b) {
|
|
// FIXME: this isn't going to work past namespace for services
|
|
// with dots in the name
|
|
const [aRev, bRev] = [a, b].map(item => item.split('.').reverse());
|
|
const types = ['Datacenter', 'Namespace', 'Service', 'Subset'];
|
|
return bRev.find(function(item, i) {
|
|
const res = item !== aRev[i];
|
|
if (res) {
|
|
type = types[i];
|
|
}
|
|
return res;
|
|
});
|
|
});
|
|
return {
|
|
Type: type,
|
|
Targets: Targets,
|
|
};
|
|
};
|
|
const getNodeResolvers = function(nodes = {}) {
|
|
const failovers = getFailovers(nodes);
|
|
const resolvers = {};
|
|
Object.keys(nodes).forEach(function(key) {
|
|
const node = nodes[key];
|
|
if (node.Type === 'resolver' && !failovers.includes(key.split(':').pop())) {
|
|
resolvers[node.Name] = node;
|
|
}
|
|
});
|
|
return resolvers;
|
|
};
|
|
|
|
const getTargetResolvers = function(dc, nspace = 'default', targets = [], nodes = {}) {
|
|
const resolvers = {};
|
|
Object.values(targets).forEach(item => {
|
|
let node = nodes[item.ID];
|
|
if (node) {
|
|
if (typeof resolvers[item.Service] === 'undefined') {
|
|
resolvers[item.Service] = {
|
|
ID: item.ID,
|
|
Name: item.Service,
|
|
Children: [],
|
|
Failover: null,
|
|
Redirect: null,
|
|
};
|
|
}
|
|
const resolver = resolvers[item.Service];
|
|
let failoverable = resolver;
|
|
if (item.ServiceSubset) {
|
|
failoverable = item;
|
|
// FIXME: Sometimes we have set the resolvers ID to the ID of the
|
|
// subset this just shifts the subset of the front of the URL for the moment
|
|
const temp = item.ID.split('.');
|
|
temp.shift();
|
|
resolver.ID = temp.join('.');
|
|
resolver.Children.push(item);
|
|
}
|
|
if (typeof node.Resolver.Failover !== 'undefined') {
|
|
// FIXME: Figure out if we can get rid of this
|
|
/* eslint ember/no-side-effects: "warn" */
|
|
set(failoverable, 'Failover', targetsToFailover(node.Resolver.Failover.Targets, item.ID));
|
|
} else {
|
|
const res = targetsToFailover([node.Resolver.Target], `service.${nspace}.${dc}`);
|
|
if (res.Type === 'Datacenter' || res.Type === 'Namespace') {
|
|
resolver.Children.push(item);
|
|
set(failoverable, 'Redirect', true);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
return Object.values(resolvers);
|
|
};
|
|
const getFailovers = function(nodes = {}) {
|
|
const failovers = [];
|
|
Object.values(nodes)
|
|
.filter(item => item.Type === 'resolver')
|
|
.forEach(function(item) {
|
|
(get(item, 'Resolver.Failover.Targets') || []).forEach(failover => {
|
|
failovers.push(failover);
|
|
});
|
|
});
|
|
return failovers;
|
|
};
|
|
|
|
export default Component.extend({
|
|
dom: service('dom'),
|
|
ticker: service('ticker'),
|
|
dataStructs: service('data-structs'),
|
|
classNames: ['discovery-chain'],
|
|
classNameBindings: ['active'],
|
|
isDisplayed: false,
|
|
selectedId: '',
|
|
x: 0,
|
|
y: 0,
|
|
tooltip: '',
|
|
activeTooltip: false,
|
|
init: function() {
|
|
this._super(...arguments);
|
|
this._listeners = this.dom.listeners();
|
|
this._viewportlistener = this.dom.listeners();
|
|
},
|
|
didInsertElement: function() {
|
|
this._super(...arguments);
|
|
this._viewportlistener.add(
|
|
this.dom.isInViewport(this.element, bool => {
|
|
set(this, 'isDisplayed', bool);
|
|
if (this.isDisplayed) {
|
|
this.addPathListeners();
|
|
} else {
|
|
this.ticker.destroy(this);
|
|
}
|
|
})
|
|
);
|
|
},
|
|
didReceiveAttrs: function() {
|
|
this._super(...arguments);
|
|
if (this.element) {
|
|
this.addPathListeners();
|
|
}
|
|
},
|
|
willDestroyElement: function() {
|
|
this._super(...arguments);
|
|
this._listeners.remove();
|
|
this._viewportlistener.remove();
|
|
this.ticker.destroy(this);
|
|
},
|
|
splitters: computed('chain.Nodes', function() {
|
|
return getNodesByType(get(this, 'chain.Nodes'), 'splitter').map(function(item) {
|
|
set(item, 'ID', `splitter:${item.Name}`);
|
|
return item;
|
|
});
|
|
}),
|
|
routers: computed('chain.Nodes', function() {
|
|
// Right now there should only ever be one 'Router'.
|
|
return getNodesByType(get(this, 'chain.Nodes'), 'router');
|
|
}),
|
|
routes: computed('chain', 'routers', function() {
|
|
const routes = get(this, 'routers').reduce(function(prev, item) {
|
|
return prev.concat(
|
|
item.Routes.map(function(route, i) {
|
|
return {
|
|
...route,
|
|
ID: `route:${item.Name}-${JSON.stringify(route.Definition.Match.HTTP)}`,
|
|
};
|
|
})
|
|
);
|
|
}, []);
|
|
if (routes.length === 0) {
|
|
let nextNode = `resolver:${this.chain.ServiceName}.${this.chain.Namespace}.${this.chain.Datacenter}`;
|
|
const splitterID = `splitter:${this.chain.ServiceName}`;
|
|
if (typeof this.chain.Nodes[splitterID] !== 'undefined') {
|
|
nextNode = splitterID;
|
|
}
|
|
routes.push({
|
|
Default: true,
|
|
ID: `route:${this.chain.ServiceName}`,
|
|
Name: this.chain.ServiceName,
|
|
Definition: {
|
|
Match: {
|
|
HTTP: {
|
|
PathPrefix: '/',
|
|
},
|
|
},
|
|
},
|
|
NextNode: nextNode,
|
|
});
|
|
}
|
|
return routes;
|
|
}),
|
|
nodeResolvers: computed('chain.Nodes', function() {
|
|
return getNodeResolvers(get(this, 'chain.Nodes'));
|
|
}),
|
|
resolvers: computed('nodeResolvers.[]', 'chain.Targets', function() {
|
|
return getTargetResolvers(
|
|
this.chain.Datacenter,
|
|
this.chain.Namespace,
|
|
get(this, 'chain.Targets'),
|
|
this.nodeResolvers
|
|
);
|
|
}),
|
|
graph: computed('chain.Nodes', function() {
|
|
const graph = this.dataStructs.graph();
|
|
Object.entries(get(this, 'chain.Nodes')).forEach(function([key, item]) {
|
|
switch (item.Type) {
|
|
case 'splitter':
|
|
item.Splits.forEach(function(splitter) {
|
|
graph.addLink(`splitter:${item.Name}`, splitter.NextNode);
|
|
});
|
|
break;
|
|
case 'router':
|
|
item.Routes.forEach(function(route, i) {
|
|
graph.addLink(
|
|
`route:${item.Name}-${JSON.stringify(route.Definition.Match.HTTP)}`,
|
|
route.NextNode
|
|
);
|
|
});
|
|
break;
|
|
}
|
|
});
|
|
return graph;
|
|
}),
|
|
selected: computed('selectedId', 'graph', function() {
|
|
if (this.selectedId === '' || !this.dom.element(`#${this.selectedId}`)) {
|
|
return {};
|
|
}
|
|
const getTypeFromId = function(id) {
|
|
return id.split(':').shift();
|
|
};
|
|
const id = this.selectedId;
|
|
const type = getTypeFromId(id);
|
|
const nodes = [id];
|
|
const edges = [];
|
|
this.graph.forEachLinkedNode(id, (linkedNode, link) => {
|
|
nodes.push(linkedNode.id);
|
|
edges.push(`${link.fromId}>${link.toId}`);
|
|
this.graph.forEachLinkedNode(linkedNode.id, (linkedNode, link) => {
|
|
const nodeType = getTypeFromId(linkedNode.id);
|
|
if (type !== nodeType && type !== 'splitter' && nodeType !== 'splitter') {
|
|
nodes.push(linkedNode.id);
|
|
edges.push(`${link.fromId}>${link.toId}`);
|
|
}
|
|
});
|
|
});
|
|
return {
|
|
nodes: nodes.map(item => `#${CSS.escape(item)}`),
|
|
edges: edges.map(item => `#${CSS.escape(item)}`),
|
|
};
|
|
}),
|
|
width: computed('isDisplayed', 'chain.{Nodes,Targets}', function() {
|
|
return this.element.offsetWidth;
|
|
}),
|
|
height: computed('isDisplayed', 'chain.{Nodes,Targets}', function() {
|
|
return this.element.offsetHeight;
|
|
}),
|
|
// TODO(octane): ember has trouble adding mouse events to svg elements whilst giving
|
|
// the developer access to the mouse event therefore we just use JS to add our events
|
|
// revisit this post Octane
|
|
addPathListeners: function() {
|
|
// FIXME: Figure out if we can remove this next
|
|
next(() => {
|
|
this._listeners.remove();
|
|
[...this.dom.elements('path.split', this.element)].forEach(item => {
|
|
this._listeners.add(item, {
|
|
mouseover: e => this.actions.showSplit.apply(this, [e]),
|
|
mouseout: e => this.actions.hideSplit.apply(this, [e]),
|
|
});
|
|
});
|
|
});
|
|
// TODO: currently don't think there is a way to listen
|
|
// for an element being removed inside a component, possibly
|
|
// using IntersectionObserver. It's a tiny detail, but we just always
|
|
// remove the tooltip on component update as its so tiny, ideal
|
|
// the tooltip would stay if there was no change to the <path>
|
|
// set(this, 'activeTooltip', false);
|
|
},
|
|
actions: {
|
|
showSplit: function(e) {
|
|
this.setProperties({
|
|
x: e.offsetX,
|
|
y: e.offsetY - 5,
|
|
tooltip: e.target.dataset.percentage,
|
|
activeTooltip: true,
|
|
});
|
|
},
|
|
hideSplit: function(e = null) {
|
|
set(this, 'activeTooltip', false);
|
|
},
|
|
click: function(e) {
|
|
const id = e.currentTarget.getAttribute('id');
|
|
if (id === this.selectedId) {
|
|
set(this, 'active', false);
|
|
set(this, 'selectedId', '');
|
|
} else {
|
|
set(this, 'active', true);
|
|
set(this, 'selectedId', id);
|
|
}
|
|
},
|
|
},
|
|
});
|