mirror of
https://github.com/status-im/consul.git
synced 2025-02-21 18:08:23 +00:00
* add peers route * add peers to nav * use regular app ui patterns peers template * use empty state in peers UI * mock `v1/peerings` request * implement custom adapter/serializer for `peers`-model * index request for peerings on peers route * update peers list to show as proper list * Use tailwind for easier styling * Unique ids in peerings response mock-api * Add styling peerings list * Allow creating empty tooltip To make it easier to iterate over a set of items where some items should not display a tooltip and others should. * Add tooltip Peerings:Badge * Add undefined peering state badge * Remove imported/exported services count peering This won't be included in the initial version of the API response * Implement Peerings::Search * Make it possible to filter peerings by name * Install ember-keyboard For idiomatic handling of key-presses. * Clear peering search input when pressing `Escape` * use peers.index instead of peers for peerings listing * Allow to include peered services in services-query * update services mock to add peerName * add Consul::Peer component To surface peering information on a resource * add PeerName as attribute to service model * surface peering information in service list * Add tooltip to Consul::Peer * Make services searchable by peer-name * Allow passing optional query-params to href-to * Add peer query-param to dc.services.show * Pass peer as query-param services listing * support option peer route-param * set peer-name undefined in services serializer when empty * update peer route-param when navigating to peered service * request sercice with peer-name if need be * make sure to reset peer route-param when leaving service.show * componentize services.peer-info * surface peer info services.show * make sure to reset peer route-param in main nav * fix services breadcrumb services.intentions we need to reset peer route-param here to not break the app * surface peer when querying for it on service api call * query for peer info service-instance api calls * surface peer info service-instance.show * Camelize peer attributes to match rest of app * Refactor peers.index to reflect camelized attributes for peer * Remove unused query-params services.show * make logo href reset peer route-param * Cleanup optional peer param query service-instance * Use replace decorator instead of serializer for empty peerName * make sure to only send peer info when correct qp is passed * Always send qp for querying peers services request * rename with-imports to with-peers * Use css for peer-icon * Refactor bucket-list component to surface peer-info * Remove Consul::Peer component This info is now displayed via the bucket-list component * Fix bucket-list component to surface service again * Update bucket-list docs to reflect peer-info addition * Remove tailwind related styles * Remove consul-tailwind package We won't be using tailwind for now * Fix typo badge scss * Add with-import handling mock-api nodes * Add peerName to node attributes * include peers when querying nodes * reflect api updates node list mock * Create consul::node::peer-info component * Surface peer-info in nodes list * Mock peer response for node request * Make it possible to add peer-name to node request * Update peer route-param when linking to node * Reset peers route-param when leaving nodes.show We need to reset the route-param to not introduce a bug - otherwise subsequent node show request would request with the old peer query-param * Add sourcePeer intentions api mock * add SourcePeer attr to intentions model * Surface peering info on intentions list * Request peered intentions differently intentions.edit * Handle peer info in intentions/exact mock * Surface peering info intention view * Add randomized peer data topology mock * Surface peer info topology view * fix service/peer-info styling We aren't using tailwind anymore - we need to create a custom scss file * Update peerings api mocks * Update peerings::badge with updated styling * cleanup intentions/exact mock * Create watcher component to declaratively register polling * Poll peers in background when on peers route * use existing colors for peering-badge * Add test for requesting service with `with-peers`-query * add imported/exported count to peers model * update mock-api to surface exported/imported count on peers * Show exported/imported peers count on peers list * Use translations for service import/export UI peers * Make sure to ask for nodes with peers * Add match-url step for easier url testing of service urls * Add test for peer-name on peered services * Add test for service navigation peered service * Implement feature-flag handling * Enable peering feature in test and development * Redirect peers to services.index when feature-flag is disabled * Only query for peers when feature is enabled * Only show peers in nav when feature is enabled * Componentize peering service count detail * Handle non-state Peerings::Badge * Use Peerings::ServiceCount in peerings list * Only send peer query for peered service-instances. * Add step to visit url directly * add test for accessing peered service directly * Remove unused service import peers.index * Only query for peer when peer provided node-adapter * fix tests
363 lines
11 KiB
JavaScript
363 lines
11 KiB
JavaScript
import { env } from 'consul-ui/env';
|
|
const OPTIONAL = {};
|
|
if (env('CONSUL_PARTITIONS_ENABLED')) {
|
|
OPTIONAL.partition = /^_([a-zA-Z0-9]([a-zA-Z0-9-]{0,62}[a-zA-Z0-9])?)$/;
|
|
}
|
|
|
|
if (env('CONSUL_NSPACES_ENABLED')) {
|
|
OPTIONAL.nspace = /^~([a-zA-Z0-9]([a-zA-Z0-9-]{0,62}[a-zA-Z0-9])?)$/;
|
|
}
|
|
|
|
OPTIONAL.peer = /^:([a-zA-Z0-9]([a-zA-Z0-9-]{0,62}[a-zA-Z0-9])?)$/;
|
|
|
|
const trailingSlashRe = /\/$/;
|
|
|
|
// see below re: ember double slashes
|
|
// const moreThan1SlashRe = /\/{2,}/g;
|
|
|
|
const _uuid = function() {
|
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
|
|
const r = (Math.random() * 16) | 0;
|
|
return (c === 'x' ? r : (r & 3) | 8).toString(16);
|
|
});
|
|
};
|
|
|
|
// let popstateFired = false;
|
|
/**
|
|
* Register a callback to be invoked whenever the browser history changes,
|
|
* including using forward and back buttons.
|
|
*/
|
|
const route = function(e) {
|
|
const path = e.state.path;
|
|
const url = this.getURLForTransition(path);
|
|
// Ignore initial page load popstate event in Chrome
|
|
// if (!popstateFired) {
|
|
// popstateFired = true;
|
|
// if (url === this._previousURL) {
|
|
// return;
|
|
// }
|
|
// }
|
|
if (url === this._previousURL) {
|
|
if (path === this._previousPath) {
|
|
return;
|
|
}
|
|
this._previousPath = e.state.path;
|
|
// async
|
|
this.container.lookup('route:application').refresh();
|
|
}
|
|
if (typeof this.callback === 'function') {
|
|
// TODO: Can we use `settled` or similar to make this `route` method async?
|
|
// not async
|
|
this.callback(url);
|
|
}
|
|
// used for webkit workaround
|
|
this._previousURL = url;
|
|
this._previousPath = e.state.path;
|
|
};
|
|
export default class FSMWithOptionalLocation {
|
|
// extend FSMLocation
|
|
implementation = 'fsm-with-optional';
|
|
|
|
baseURL = '';
|
|
/**
|
|
* Set from router:main._setupLocation (-internals/routing/lib/system/router)
|
|
* Will be pre-pended to path upon state change
|
|
*/
|
|
rootURL = '/';
|
|
|
|
/**
|
|
* Path is the 'application path' i.e. the path/URL with no root/base URLs
|
|
* but potentially with optional parameters (these are remove when getURL is called)
|
|
*/
|
|
path = '/';
|
|
|
|
/**
|
|
* Sneaky undocumented property used in ember's main router used to skip any
|
|
* setup of location from the main router. We currently don't need this but
|
|
* document it here incase we ever do.
|
|
*/
|
|
cancelRouterSetup = false;
|
|
|
|
/**
|
|
* Used to store our 'optional' segments should we have any
|
|
*/
|
|
optional = {};
|
|
|
|
static create() {
|
|
return new this(...arguments);
|
|
}
|
|
|
|
constructor(owner, doc, env) {
|
|
this.container = Object.entries(owner)[0][1];
|
|
|
|
// add the route/state change handler
|
|
this.route = route.bind(this);
|
|
|
|
this.doc = typeof doc === 'undefined' ? this.container.lookup('service:-document') : doc;
|
|
this.env = typeof env === 'undefined' ? this.container.lookup('service:env') : env;
|
|
|
|
const base = this.doc.querySelector('base[href]');
|
|
if (base !== null) {
|
|
this.baseURL = base.getAttribute('href');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @internal
|
|
* Called from router:main._setupLocation (-internals/routing/lib/system/router)
|
|
* Used to set state on first call to setURL
|
|
*/
|
|
initState() {
|
|
this.location = this.location || this.doc.defaultView.location;
|
|
this.machine = this.machine || this.doc.defaultView.history;
|
|
this.doc.defaultView.addEventListener('popstate', this.route);
|
|
|
|
const state = this.machine.state;
|
|
const url = this.getURL();
|
|
const href = this.formatURL(url);
|
|
|
|
if (state && state.path === href) {
|
|
// preserve existing state
|
|
// used for webkit workaround, since there will be no initial popstate event
|
|
this._previousPath = href;
|
|
this._previousURL = url;
|
|
} else {
|
|
this.dispatch('replace', href);
|
|
}
|
|
}
|
|
|
|
getURLFrom(url) {
|
|
// remove trailing slashes if they exist
|
|
url = url || this.location.pathname;
|
|
this.rootURL = this.rootURL.replace(trailingSlashRe, '');
|
|
this.baseURL = this.baseURL.replace(trailingSlashRe, '');
|
|
// remove baseURL and rootURL from start of path
|
|
return url
|
|
.replace(new RegExp(`^${this.baseURL}(?=/|$)`), '')
|
|
.replace(new RegExp(`^${this.rootURL}(?=/|$)`), '');
|
|
// ember default locations remove double slashes here e.g. '//'
|
|
// .replace(moreThan1SlashRe, '/'); // remove extra slashes
|
|
}
|
|
|
|
getURLForTransition(url) {
|
|
this.optional = {};
|
|
url = this.getURLFrom(url)
|
|
.split('/')
|
|
.filter((item, i) => {
|
|
if (i < 3) {
|
|
let found = false;
|
|
Object.entries(OPTIONAL).reduce((prev, [key, re]) => {
|
|
const res = re.exec(item);
|
|
if (res !== null) {
|
|
prev[key] = {
|
|
value: item,
|
|
match: res[1],
|
|
};
|
|
found = true;
|
|
}
|
|
return prev;
|
|
}, this.optional);
|
|
return !found;
|
|
}
|
|
return true;
|
|
})
|
|
.join('/');
|
|
return url;
|
|
}
|
|
|
|
optionalParams() {
|
|
let optional = this.optional || {};
|
|
return ['partition', 'nspace', 'peer'].reduce((prev, item) => {
|
|
let value = '';
|
|
if (typeof optional[item] !== 'undefined') {
|
|
value = optional[item].match;
|
|
}
|
|
prev[item] = value;
|
|
return prev;
|
|
}, {});
|
|
}
|
|
|
|
// public entrypoints for app hrefs/URLs
|
|
|
|
// visit and transitionTo can't be async/await as they return promise-like
|
|
// non-promises that get re-wrapped by the addition of async/await
|
|
visit() {
|
|
return this.transitionTo(...arguments);
|
|
}
|
|
|
|
/**
|
|
* Turns a routeName into a full URL string for anchor hrefs etc.
|
|
*/
|
|
hrefTo(routeName, params, hash) {
|
|
if (typeof hash.dc !== 'undefined') {
|
|
delete hash.dc;
|
|
}
|
|
if (typeof hash.nspace !== 'undefined') {
|
|
hash.nspace = `~${hash.nspace}`;
|
|
}
|
|
if (typeof hash.partition !== 'undefined') {
|
|
hash.partition = `_${hash.partition}`;
|
|
}
|
|
if (typeof hash.peer !== 'undefined') {
|
|
hash.peer = `:${hash.peer}`;
|
|
}
|
|
|
|
if (typeof this.router === 'undefined') {
|
|
this.router = this.container.lookup('router:main');
|
|
}
|
|
let withOptional = true;
|
|
switch (true) {
|
|
case routeName === 'settings':
|
|
case routeName.startsWith('docs.'):
|
|
withOptional = false;
|
|
break;
|
|
}
|
|
if (this.router.currentRouteName.startsWith('docs.')) {
|
|
// If we are in docs, then add a default dc as there won't be one in the
|
|
// URL
|
|
params.unshift(env('CONSUL_DATACENTER_PRIMARY'));
|
|
if (routeName.startsWith('dc')) {
|
|
// if its an app URL replace it with debugging instead of linking
|
|
return `console://${routeName} <= ${JSON.stringify(params)}`;
|
|
}
|
|
}
|
|
const router = this.router._routerMicrolib;
|
|
let url;
|
|
try {
|
|
url = router.generate(routeName, ...params, {
|
|
queryParams: {},
|
|
});
|
|
} catch (e) {
|
|
// if the previous generation throws due to params not being available
|
|
// its probably due to the view wanting to re-render even though we are
|
|
// leaving the view and the router has already moved the state to old
|
|
// state so try again with the old state to avoid errors
|
|
params = Object.values(router.oldState.params).reduce((prev, item) => {
|
|
return prev.concat(Object.keys(item).length > 0 ? item : []);
|
|
}, []);
|
|
url = router.generate(routeName, ...params);
|
|
}
|
|
return this.formatURL(url, hash, withOptional);
|
|
}
|
|
|
|
/**
|
|
* Takes a full browser URL including rootURL and optional (a full href) and
|
|
* performs an ember transition/refresh and browser location update using that
|
|
*/
|
|
transitionTo(url) {
|
|
if (this.router.currentRouteName.startsWith('docs') && url.startsWith('console://')) {
|
|
console.info(`location.transitionTo: ${url.substr(10)}`);
|
|
return true;
|
|
}
|
|
const previousOptional = Object.entries(this.optionalParams());
|
|
const transitionURL = this.getURLForTransition(url);
|
|
if (this._previousURL === transitionURL) {
|
|
// probably an optional parameter change as the Ember URLs are the same
|
|
// whereas the entire URL is different
|
|
this.dispatch('push', url);
|
|
return Promise.resolve();
|
|
// this.setURL(url);
|
|
} else {
|
|
const currentOptional = this.optionalParams();
|
|
if (previousOptional.some(([key, value]) => currentOptional[key] !== value)) {
|
|
// an optional parameter change and a normal param change as the Ember
|
|
// URLs are different and we know the optional params changed
|
|
// TODO: Consider changing the above previousURL === transitionURL to
|
|
// use the same 'check the optionalParams' approach
|
|
this.dispatch('push', url);
|
|
}
|
|
// use ember to transition, which will eventually come around to use location.setURL
|
|
return this.container.lookup('router:main').transitionTo(transitionURL);
|
|
}
|
|
}
|
|
|
|
//
|
|
|
|
// Ember location interface
|
|
|
|
/**
|
|
* Returns the current `location.pathname` without `rootURL` or `baseURL`
|
|
*/
|
|
getURL() {
|
|
const search = this.location.search || '';
|
|
let hash = '';
|
|
if (typeof this.location.hash !== 'undefined') {
|
|
hash = this.location.hash.substr(0);
|
|
}
|
|
const url = this.getURLForTransition(this.location.pathname);
|
|
return `${url}${search}${hash}`;
|
|
}
|
|
|
|
formatURL(url, optional, withOptional = true) {
|
|
if (url !== '') {
|
|
// remove trailing slashes if they exists
|
|
this.rootURL = this.rootURL.replace(trailingSlashRe, '');
|
|
this.baseURL = this.baseURL.replace(trailingSlashRe, '');
|
|
} else if (this.baseURL[0] === '/' && this.rootURL[0] === '/') {
|
|
// if baseURL and rootURL both start with a slash
|
|
// ... remove trailing slash from baseURL if it exists
|
|
this.baseURL = this.baseURL.replace(trailingSlashRe, '');
|
|
}
|
|
|
|
if (withOptional) {
|
|
const temp = url.split('/');
|
|
if (Object.keys(optional || {}).length === 0) {
|
|
optional = undefined;
|
|
}
|
|
optional = Object.values(optional || this.optional || {});
|
|
optional = optional.filter(item => Boolean(item)).map(item => item.value || item, []);
|
|
temp.splice(...[1, 0].concat(optional));
|
|
url = temp.join('/');
|
|
}
|
|
|
|
return `${this.baseURL}${this.rootURL}${url}`;
|
|
}
|
|
/**
|
|
* Change URL takes an ember application URL
|
|
*/
|
|
changeURL(type, path) {
|
|
this.path = path;
|
|
const state = this.machine.state;
|
|
path = this.formatURL(path);
|
|
|
|
if (!state || state.path !== path) {
|
|
this.dispatch(type, path);
|
|
}
|
|
}
|
|
|
|
setURL(path) {
|
|
// this.optional = {};
|
|
this.changeURL('push', path);
|
|
}
|
|
|
|
replaceURL(path) {
|
|
this.changeURL('replace', path);
|
|
}
|
|
|
|
onUpdateURL(callback) {
|
|
this.callback = callback;
|
|
}
|
|
|
|
//
|
|
|
|
/**
|
|
* Dispatch takes a full actual browser URL with all the rootURL and optional
|
|
* params if they exist
|
|
*/
|
|
dispatch(event, path) {
|
|
const state = {
|
|
path: path,
|
|
uuid: _uuid(),
|
|
};
|
|
this.machine[`${event}State`](state, null, path);
|
|
// popstate listeners only run from a browser action not when a state change
|
|
// is called directly, so manually call the popstate listener.
|
|
// https://developer.mozilla.org/en-US/docs/Web/API/Window/popstate_event#the_history_stack
|
|
this.route({ state: state });
|
|
}
|
|
|
|
willDestroy() {
|
|
this.doc.defaultView.removeEventListener('popstate', this.route);
|
|
}
|
|
}
|