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
This commit is contained in:
parent
9ff49034e7
commit
7c36d749f5
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
{{did-insert this.connect}}
|
||||
{{will-destroy this.disconnect}}
|
||||
<section
|
||||
{{did-insert (fn this.attributeChanged 'element')}}
|
||||
class="outlet"
|
||||
data-outlet={{@name}}
|
||||
data-route={{this.route}}
|
||||
data-route={{this.routeName}}
|
||||
data-state={{this.state.name}}
|
||||
data-transition={{concat this.previousState.name ' ' this.state.name}}
|
||||
{{on 'transitionend' this.transitionEnd}}
|
||||
>
|
||||
{{yield (hash
|
||||
state=this.state
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
{{did-insert this.connect}}
|
||||
{{will-destroy this.disconnect}}
|
||||
{{yield (hash
|
||||
model=model
|
||||
)}}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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],
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {}
|
||||
}
|
|
@ -1,3 +1,6 @@
|
|||
<Route
|
||||
@name={{routeName}}
|
||||
>
|
||||
<HeadLayout />
|
||||
{{page-title 'Consul' separator=' - '}}
|
||||
|
||||
|
@ -35,3 +38,4 @@ as |source|>
|
|||
<Consul::Loader class="view-loader" />
|
||||
</HashicorpConsul>
|
||||
{{/if}}
|
||||
</Route>
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
<Route
|
||||
@name={{routeName}}
|
||||
as |route|>
|
||||
<Outlet
|
||||
@name={{routeName}}
|
||||
@model={{route.model}}
|
||||
as |o|>
|
||||
{{outlet}}
|
||||
</Outlet>
|
||||
</Route>
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
<Route
|
||||
@name={{routeName}}
|
||||
>
|
||||
{{page-title 'Services'}}
|
||||
|
||||
<EventSource @src={{items}} />
|
||||
|
@ -108,3 +111,4 @@ as |sort filters items|}}
|
|||
</AppView>
|
||||
|
||||
{{/let}}
|
||||
</Route>
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
<Route
|
||||
@name={{routeName}}
|
||||
as |route|>
|
||||
{{#let items.firstObject as |item|}}
|
||||
{{page-title item.Service.Service}}
|
||||
<DataLoader as |loader|>
|
||||
|
@ -112,3 +115,4 @@
|
|||
</BlockSlot>
|
||||
</DataLoader>
|
||||
{{/let}}
|
||||
</Route>
|
|
@ -1,3 +1,6 @@
|
|||
<Route
|
||||
@name={{routeName}}
|
||||
>
|
||||
<div class="tab-section">
|
||||
{{#let
|
||||
|
||||
|
@ -66,3 +69,4 @@ as |sort filters items|}}
|
|||
</DataCollection>
|
||||
{{/let}}
|
||||
</div>
|
||||
</Route>
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue