From 7c36d749f5d21248dc9c67ebd6a60417fbbb4add Mon Sep 17 00:00:00 2001 From: John Cowen Date: Mon, 8 Mar 2021 12:15:54 +0000 Subject: [PATCH] ui: Add Route component / routlet service (#9813) * Add Routlet service and Route Component * Add ember-assign-helper (already an indirect dependency) * Use EventListeners for is-href instead of observing * Don't include :active in '-intent' styles --- .../components/main-nav-horizontal/index.scss | 1 - .../components/main-nav-vertical/index.scss | 1 - .../consul-ui/app/components/outlet/index.hbs | 8 +- .../consul-ui/app/components/outlet/index.js | 100 +++----- .../consul-ui/app/components/route/index.hbs | 5 + .../consul-ui/app/components/route/index.js | 19 ++ ui/packages/consul-ui/app/helpers/is-href.js | 20 +- ui/packages/consul-ui/app/routes/dc.js | 21 +- .../consul-ui/app/routes/dc/services/index.js | 4 +- .../app/routes/dc/services/show/routing.js | 7 +- .../app/routes/dc/services/show/tags.js | 2 +- ui/packages/consul-ui/app/services/routlet.js | 101 ++++++++ .../consul-ui/app/templates/application.hbs | 4 + ui/packages/consul-ui/app/templates/dc.hbs | 13 +- .../app/templates/dc/services/index.hbs | 4 + .../app/templates/dc/services/show.hbs | 228 +++++++++--------- .../templates/dc/services/show/instances.hbs | 4 + ui/packages/consul-ui/package.json | 1 + 18 files changed, 338 insertions(+), 205 deletions(-) create mode 100644 ui/packages/consul-ui/app/components/route/index.hbs create mode 100644 ui/packages/consul-ui/app/components/route/index.js create mode 100644 ui/packages/consul-ui/app/services/routlet.js diff --git a/ui/packages/consul-ui/app/components/main-nav-horizontal/index.scss b/ui/packages/consul-ui/app/components/main-nav-horizontal/index.scss index 8eca9530f..45fef2c07 100644 --- a/ui/packages/consul-ui/app/components/main-nav-horizontal/index.scss +++ b/ui/packages/consul-ui/app/components/main-nav-horizontal/index.scss @@ -9,7 +9,6 @@ @extend %main-nav-horizontal-action; } %main-nav-horizontal .popover-menu [type='checkbox']:checked + label > *, -%main-nav-horizontal > ul > li > a:active, %main-nav-horizontal > ul > li.is-active > a, %main-nav-horizontal > ul > li.is-active > label > * { @extend %main-nav-horizontal-action-active; diff --git a/ui/packages/consul-ui/app/components/main-nav-vertical/index.scss b/ui/packages/consul-ui/app/components/main-nav-vertical/index.scss index 5d07a5fd4..9aeca69e9 100644 --- a/ui/packages/consul-ui/app/components/main-nav-vertical/index.scss +++ b/ui/packages/consul-ui/app/components/main-nav-vertical/index.scss @@ -5,7 +5,6 @@ %main-nav-vertical > ul > li > span { @extend %main-nav-vertical-action; } -%main-nav-vertical > ul > li > a:active, %main-nav-vertical > ul > li.is-active > a { @extend %main-nav-vertical-action-active; } diff --git a/ui/packages/consul-ui/app/components/outlet/index.hbs b/ui/packages/consul-ui/app/components/outlet/index.hbs index 32f2e1355..b4e65362f 100644 --- a/ui/packages/consul-ui/app/components/outlet/index.hbs +++ b/ui/packages/consul-ui/app/components/outlet/index.hbs @@ -1,11 +1,13 @@ - {{did-insert this.connect}} - {{will-destroy this.disconnect}} +{{did-insert this.connect}} +{{will-destroy this.disconnect}}
{{yield (hash state=this.state diff --git a/ui/packages/consul-ui/app/components/outlet/index.js b/ui/packages/consul-ui/app/components/outlet/index.js index c7de8dd4a..e86816221 100644 --- a/ui/packages/consul-ui/app/components/outlet/index.js +++ b/ui/packages/consul-ui/app/components/outlet/index.js @@ -12,56 +12,18 @@ class State { } } -class Outlets { - constructor() { - this.map = new Map(); - } - sort() { - this.sorted = [...this.map.keys()]; - this.sorted.sort((a, b) => { - const al = a.split('.').length; - const bl = b.split('.').length; - switch (true) { - case al > bl: - return -1; - case al < bl: - return 1; - default: - return 0; - } - }); - } - set(name, value) { - this.map.set(name, value); - this.sort(); - } - get(name) { - return this.map.get(name); - } - delete(name) { - this.map.delete(name); - this.sort(); - } - keys() { - return this.sorted; - } -} -const outlets = new Outlets(); - export default class Outlet extends Component { + @service('routlet') routlet; @service('router') router; - @service('dom') dom; - @tracked route; + @tracked element; + @tracked routeName; @tracked state; @tracked previousState; + @tracked endTransition; - constructor() { - super(...arguments); - if (this.args.name === 'application') { - this.setAppState('loading'); - this.setAppRoute(this.router.currentRouteName); - } + get model() { + return this.args.model || {}; } setAppRoute(name) { @@ -70,7 +32,7 @@ export default class Outlet extends Component { name = name.substr(nspace.length); } if (name !== 'loading') { - const doc = this.dom.root(); + const doc = this.element.ownerDocument.documentElement; if (doc.classList.contains('ember-loading')) { doc.classList.remove('ember-loading'); } @@ -80,31 +42,43 @@ export default class Outlet extends Component { } setAppState(state) { - this.dom.root().dataset.state = state; + const doc = this.element.ownerDocument.documentElement; + doc.dataset.state = state; } - setOutletRoutes(route) { - const keys = [...outlets.keys()]; - const pos = keys.indexOf(this.name); - const key = pos + 1; - const parent = outlets.get(keys[key]); - parent.route = this.args.name; + @action + attributeChanged(prop, value) { + switch (prop) { + case 'element': + this.element = value; + if (this.args.name === 'application') { + this.setAppState('loading'); + this.setAppRoute(this.router.currentRouteName); + } + break; + } + } - this.route = route; + @action transitionEnd($el) { + if (typeof this.endTransition === 'function') { + this.endTransition(); + } } @action startLoad(transition) { - const keys = [...outlets.keys()]; - - const outlet = - keys.find(item => { - return transition.to.name.indexOf(item) !== -1; - }) || 'application'; - + const outlet = this.routlet.findOutlet(transition.to.name) || 'application'; if (this.args.name === outlet) { this.previousState = this.state; this.state = new State('loading'); + this.endTransition = this.routlet.transition(); + // if we have no transition-duration set immediately end the transition + const duration = window + .getComputedStyle(this.element) + .getPropertyValue('transition-duration'); + if (parseFloat(duration) === 0) { + this.endTransition(); + } } if (this.args.name === 'application') { this.setAppState('loading'); @@ -114,8 +88,6 @@ export default class Outlet extends Component { @action endLoad(transition) { if (this.state.matches('loading')) { - this.setOutletRoutes(transition.to.name); - this.previousState = this.state; this.state = new State('idle'); } @@ -126,7 +98,7 @@ export default class Outlet extends Component { @action connect() { - outlets.set(this.args.name, this); + this.routlet.addOutlet(this.args.name, this); this.previousState = this.state = new State('idle'); this.router.on('routeWillChange', this.startLoad); this.router.on('routeDidChange', this.endLoad); @@ -134,7 +106,7 @@ export default class Outlet extends Component { @action disconnect() { - outlets.delete(this.args.name); + this.routlet.removeOutlet(this.args.name); this.router.off('routeWillChange', this.startLoad); this.router.off('routeDidChange', this.endLoad); } diff --git a/ui/packages/consul-ui/app/components/route/index.hbs b/ui/packages/consul-ui/app/components/route/index.hbs new file mode 100644 index 000000000..fa3b755ee --- /dev/null +++ b/ui/packages/consul-ui/app/components/route/index.hbs @@ -0,0 +1,5 @@ +{{did-insert this.connect}} +{{will-destroy this.disconnect}} +{{yield (hash + model=model +)}} \ No newline at end of file diff --git a/ui/packages/consul-ui/app/components/route/index.js b/ui/packages/consul-ui/app/components/route/index.js new file mode 100644 index 000000000..bfb388208 --- /dev/null +++ b/ui/packages/consul-ui/app/components/route/index.js @@ -0,0 +1,19 @@ +import Component from '@glimmer/component'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; +import { tracked } from '@glimmer/tracking'; + +export default class RouteComponent extends Component { + @service('routlet') routlet; + + @tracked model; + + @action + connect() { + this.routlet.addRoute(this.args.name, this); + } + @action + disconnect() { + this.routlet.removeRoute(this.args.name, this); + } +} diff --git a/ui/packages/consul-ui/app/helpers/is-href.js b/ui/packages/consul-ui/app/helpers/is-href.js index 202664bf6..8d53fa2a5 100644 --- a/ui/packages/consul-ui/app/helpers/is-href.js +++ b/ui/packages/consul-ui/app/helpers/is-href.js @@ -1,21 +1,31 @@ -/*eslint ember/no-observers: "warn"*/ -// TODO: Remove ^ import Helper from '@ember/component/helper'; import { inject as service } from '@ember/service'; -import { observes } from '@ember-decorators/object'; +import { action } from '@ember/object'; export default class IsHrefHelper extends Helper { @service('router') router; + init() { + super.init(...arguments); + this.router.on('routeWillChange', this.routeWillChange); + } compute([targetRouteName, ...rest]) { if (this.router.currentRouteName.startsWith('nspace.') && targetRouteName.startsWith('dc.')) { targetRouteName = `nspace.${targetRouteName}`; } + if (typeof this.next !== 'undefined' && this.next !== 'loading') { + return this.next.startsWith(targetRouteName); + } return this.router.isActive(...[targetRouteName, ...rest]); } - @observes('router.currentURL') - onURLChange() { + @action + routeWillChange(transition) { + this.next = transition.to.name.replace('.index', ''); this.recompute(); } + + willDestroy() { + this.router.off('routeWillChange', this.routeWillChange); + } } diff --git a/ui/packages/consul-ui/app/routes/dc.js b/ui/packages/consul-ui/app/routes/dc.js index 5fafb2926..dd99ba0a2 100644 --- a/ui/packages/consul-ui/app/routes/dc.js +++ b/ui/packages/consul-ui/app/routes/dc.js @@ -47,6 +47,18 @@ export default class DcRoute extends Route { dc: params.dc, nspace: get(nspace || {}, 'Name'), }); + // the model here is actually required for the entire application + // but we need to wait until we are in this route so we know what the dc + // and or nspace is if the below changes please revisit the comments + // in routes/application:model + // We do this here instead of in setupController to prevent timing issues + // in lower routes + this.controllerFor('application').setProperties({ + dc, + nspace, + token, + permissions, + }); return { dc, nspace, @@ -55,15 +67,6 @@ export default class DcRoute extends Route { }; } - setupController(controller, model) { - super.setupController(...arguments); - // the model here is actually required for the entire application - // but we need to wait until we are in this route so we know what the dc - // and or nspace is if the below changes please revists the comments - // in routes/application:model - this.controllerFor('application').setProperties(model); - } - // TODO: This will eventually be deprecated please see // https://deprecations.emberjs.com/v3.x/#toc_deprecate-router-events @action diff --git a/ui/packages/consul-ui/app/routes/dc/services/index.js b/ui/packages/consul-ui/app/routes/dc/services/index.js index 879621028..873225c78 100644 --- a/ui/packages/consul-ui/app/routes/dc/services/index.js +++ b/ui/packages/consul-ui/app/routes/dc/services/index.js @@ -22,11 +22,11 @@ export default class IndexRoute extends Route { async model(params, transition) { const nspace = this.modelFor('nspace').nspace.substr(1); const dc = this.modelFor('dc').dc.Name; - const items = await this.data.source(uri => uri`/${nspace}/${dc}/services`); + const items = this.data.source(uri => uri`/${nspace}/${dc}/services`); return { dc, nspace, - items, + items: await items, searchProperties: this.queryParams.searchproperty.empty[0], }; } diff --git a/ui/packages/consul-ui/app/routes/dc/services/show/routing.js b/ui/packages/consul-ui/app/routes/dc/services/show/routing.js index e70cacd61..a68248ed7 100644 --- a/ui/packages/consul-ui/app/routes/dc/services/show/routing.js +++ b/ui/packages/consul-ui/app/routes/dc/services/show/routing.js @@ -11,11 +11,12 @@ export default class RoutingRoute extends Route { .slice(0, -1) .join('.'); const model = this.modelFor(parent); + const chain = this.data.source( + uri => uri`/${model.nspace}/${model.dc.Name}/discovery-chain/${model.slug}` + ); return { ...model, - chain: await this.data.source( - uri => uri`/${model.nspace}/${model.dc.Name}/discovery-chain/${model.slug}` - ), + chain: await chain, }; } diff --git a/ui/packages/consul-ui/app/routes/dc/services/show/tags.js b/ui/packages/consul-ui/app/routes/dc/services/show/tags.js index 3c6b4e0f0..2e4ecd516 100644 --- a/ui/packages/consul-ui/app/routes/dc/services/show/tags.js +++ b/ui/packages/consul-ui/app/routes/dc/services/show/tags.js @@ -1,7 +1,7 @@ import Route from 'consul-ui/routing/route'; export default class TagsRoute extends Route { - model() { + async model() { const parent = this.routeName .split('.') .slice(0, -1) diff --git a/ui/packages/consul-ui/app/services/routlet.js b/ui/packages/consul-ui/app/services/routlet.js new file mode 100644 index 000000000..a66e0820f --- /dev/null +++ b/ui/packages/consul-ui/app/services/routlet.js @@ -0,0 +1,101 @@ +import Service from '@ember/service'; +import { schedule } from '@ember/runloop'; + +class Outlets { + constructor() { + this.map = new Map(); + this.sorted = []; + } + sort() { + this.sorted = [...this.map.keys()]; + this.sorted.sort((a, b) => { + if (a === 'application') { + return 1; + } + if (b === 'application') { + return -1; + } + const al = a.split('.').length; + const bl = b.split('.').length; + switch (true) { + case al > bl: + return -1; + case al < bl: + return 1; + default: + return 0; + } + }); + } + set(name, value) { + this.map.set(name, value); + // TODO: find, splice to insert at the correct index instead of sorting + // all the time + this.sort(); + } + get(name) { + return this.map.get(name); + } + delete(name) { + // TODO: find, splice to delete at the correct index instead of sorting + // all the time + this.map.delete(name); + this.sort(); + } + keys() { + return this.sorted; + } +} +const outlets = new Outlets(); +export default class RoutletService extends Service { + ready() { + return this._transition; + } + + transition() { + let endTransition; + this._transition = new Promise(resolve => { + endTransition = resolve; + }); + return endTransition; + } + + findOutlet(name) { + const keys = [...outlets.keys()]; + const key = keys.find(item => name.indexOf(item) !== -1); + return key; + } + + addOutlet(name, outlet) { + outlets.set(name, outlet); + } + + removeOutlet(name) { + outlets.delete(name); + } + + // modelFor gets the model for Outlet specified by `name`, not the Route + modelFor(name) { + const outlet = outlets.get(name); + if (typeof outlet !== 'undefined') { + return outlet.model || {}; + } + return {}; + } + + addRoute(name, route) { + const keys = [...outlets.keys()]; + const pos = keys.indexOf(name); + const key = pos + 1; + const outlet = outlets.get(keys[key]); + if (typeof outlet !== 'undefined') { + route.model = outlet.model; + // TODO: Try to avoid the double computation bug + schedule('afterRender', () => { + outlet.routeName = route.args.name; + }); + } + } + + removeRoute(name, route) {} +} diff --git a/ui/packages/consul-ui/app/templates/application.hbs b/ui/packages/consul-ui/app/templates/application.hbs index 8688ab200..0c810beda 100644 --- a/ui/packages/consul-ui/app/templates/application.hbs +++ b/ui/packages/consul-ui/app/templates/application.hbs @@ -1,3 +1,6 @@ + {{page-title 'Consul' separator=' - '}} @@ -35,3 +38,4 @@ as |source|> {{/if}} + diff --git a/ui/packages/consul-ui/app/templates/dc.hbs b/ui/packages/consul-ui/app/templates/dc.hbs index d20d940ec..a02351727 100644 --- a/ui/packages/consul-ui/app/templates/dc.hbs +++ b/ui/packages/consul-ui/app/templates/dc.hbs @@ -1,5 +1,10 @@ - - {{outlet}} - +as |route|> + + {{outlet}} + + diff --git a/ui/packages/consul-ui/app/templates/dc/services/index.hbs b/ui/packages/consul-ui/app/templates/dc/services/index.hbs index 892539567..205827a24 100644 --- a/ui/packages/consul-ui/app/templates/dc/services/index.hbs +++ b/ui/packages/consul-ui/app/templates/dc/services/index.hbs @@ -1,3 +1,6 @@ + {{page-title 'Services'}} @@ -108,3 +111,4 @@ as |sort filters items|}} {{/let}} + diff --git a/ui/packages/consul-ui/app/templates/dc/services/show.hbs b/ui/packages/consul-ui/app/templates/dc/services/show.hbs index 9aba298b3..8bcc10232 100644 --- a/ui/packages/consul-ui/app/templates/dc/services/show.hbs +++ b/ui/packages/consul-ui/app/templates/dc/services/show.hbs @@ -1,114 +1,118 @@ -{{#let items.firstObject as |item|}} -{{page-title item.Service.Service}} - + + {{#let items.firstObject as |item|}} + {{page-title item.Service.Service}} + - - - {{#if (not loader.error)}} - - - {{/if}} - - - - - - - - {{#if (eq loader.error.status "404")}} - - - - {{else if (eq loader.error.status "403")}} - - - - {{else}} - - - - {{/if}} - - - - - - - - -
    -
  1. All Services
  2. -
-
- -

- {{item.Service.Service}} -

- - -
- - {{#if (not-eq item.Service.Kind 'mesh-gateway')}} - + + + {{#if (not loader.error)}} + + {{/if}} - - - {{#if urls.service}} - - Open Dashboard - - {{/if}} - - - - {{outlet}} - - -
-
-
-{{/let}} \ No newline at end of file + + + + + + + + {{#if (eq loader.error.status "404")}} + + + + {{else if (eq loader.error.status "403")}} + + + + {{else}} + + + + {{/if}} + + + + + + + + +
    +
  1. All Services
  2. +
+
+ +

+ {{item.Service.Service}} +

+ + +
+ + {{#if (not-eq item.Service.Kind 'mesh-gateway')}} + + {{/if}} + + + {{#if urls.service}} + + Open Dashboard + + {{/if}} + + + + {{outlet}} + + +
+
+
+ {{/let}} + \ No newline at end of file diff --git a/ui/packages/consul-ui/app/templates/dc/services/show/instances.hbs b/ui/packages/consul-ui/app/templates/dc/services/show/instances.hbs index cb6c553ca..d1907e017 100644 --- a/ui/packages/consul-ui/app/templates/dc/services/show/instances.hbs +++ b/ui/packages/consul-ui/app/templates/dc/services/show/instances.hbs @@ -1,3 +1,6 @@ +
{{#let @@ -66,3 +69,4 @@ as |sort filters items|}} {{/let}}
+
diff --git a/ui/packages/consul-ui/package.json b/ui/packages/consul-ui/package.json index 957f1164c..7c762379b 100644 --- a/ui/packages/consul-ui/package.json +++ b/ui/packages/consul-ui/package.json @@ -87,6 +87,7 @@ "d3-selection": "^2.0.0", "d3-shape": "^2.0.0", "dayjs": "^1.9.3", + "ember-assign-helper": "^0.3.0", "ember-auto-import": "^1.5.3", "ember-can": "^3.0.0", "ember-changeset-conditional-validations": "^0.6.0",