From 7dc1a91edfe37734edcbfebc9070eaa0ddb6b603 Mon Sep 17 00:00:00 2001 From: John Cowen Date: Wed, 9 Dec 2020 09:22:01 +0000 Subject: [PATCH] ui: document-attrs helper (#9336) This commit adds a {{document-attrs}} helper, specifically for adding attributes to the root documentElement, which in our case is always --- .../app/components/app-view/index.hbs | 5 +- .../consul-ui/app/helpers/document-attrs.js | 96 +++++++++++++++++++ .../app/instance-initializers/nspace.js | 16 ---- .../styles/components/app-view/layout.scss | 4 - ui/packages/consul-ui/app/styles/layout.scss | 14 ++- .../consul-ui/app/templates/application.hbs | 6 ++ .../consul-ui/app/templates/dc/kv/index.hbs | 4 +- .../tests/unit/helpers/document-attrs-test.js | 41 ++++++++ 8 files changed, 158 insertions(+), 28 deletions(-) create mode 100644 ui/packages/consul-ui/app/helpers/document-attrs.js create mode 100644 ui/packages/consul-ui/tests/unit/helpers/document-attrs-test.js diff --git a/ui/packages/consul-ui/app/components/app-view/index.hbs b/ui/packages/consul-ui/app/components/app-view/index.hbs index ae87ddec6..783cd8e6f 100644 --- a/ui/packages/consul-ui/app/components/app-view/index.hbs +++ b/ui/packages/consul-ui/app/components/app-view/index.hbs @@ -52,7 +52,10 @@
{{#if authorized}} {{/if}}
diff --git a/ui/packages/consul-ui/app/helpers/document-attrs.js b/ui/packages/consul-ui/app/helpers/document-attrs.js new file mode 100644 index 000000000..4f91fe14d --- /dev/null +++ b/ui/packages/consul-ui/app/helpers/document-attrs.js @@ -0,0 +1,96 @@ +import Helper from '@ember/component/helper'; +import { inject as service } from '@ember/service'; +import { runInDebug } from '@ember/debug'; +import MultiMap from 'mnemonist/multi-map'; + +// keep a record or attrs +const attrs = new Map(); + +// keep a record of hashes privately +const wm = new WeakMap(); + +export default class DocumentAttrsHelper extends Helper { + @service('-document') document; + + compute(params, hash) { + this.synchronize(this.document.documentElement, hash); + } + + willDestroy() { + this.synchronize(this.document.documentElement); + wm.delete(this); + } + + synchronize(root, hash) { + const prev = wm.get(this); + if (prev) { + // if this helper was already setting a property then remove them from + // our book keeping + Object.entries(prev).forEach(([key, value]) => { + let map = attrs.get(key); + + if (typeof map !== 'undefined') { + [...new Set(value.split(' '))].map(val => map.remove(val, this)); + } + }); + } + if (hash) { + // if we are setting more properties add them to our book keeping + wm.set(this, hash); + [...Object.entries(hash)].forEach(([key, value]) => { + let values = attrs.get(key); + if (typeof values === 'undefined') { + values = new MultiMap(Set); + attrs.set(key, values); + } + [...new Set(value.split(' '))].map(val => { + if (values.count(val) === 0) { + values.set(val, null); + } + values.set(val, this); + }); + }); + } + [...attrs.entries()].forEach(([attr, values]) => { + let type = 'attr'; + if (attr === 'class') { + type = attr; + } else if (attr.startsWith('data-')) { + type = 'data'; + } + // go through our list of properties and synchronize the DOM + // properties with our properties + [...values.keys()].forEach(value => { + if (values.count(value) === 1) { + switch (type) { + case 'class': + root.classList.remove(value); + break; + case 'data': + default: + runInDebug(() => { + throw new Error(`${type} is not implemented yet`); + }); + } + values.delete(value); + // remove the property if it has no values + if (values.size === 0) { + attrs.delete(attr); + } + } else { + switch (type) { + case 'class': + root.classList.add(value); + break; + case 'data': + default: + runInDebug(() => { + throw new Error(`${type} is not implemented yet`); + }); + } + } + }); + }); + return attrs; + } +} diff --git a/ui/packages/consul-ui/app/instance-initializers/nspace.js b/ui/packages/consul-ui/app/instance-initializers/nspace.js index f3039f874..8bd693841 100644 --- a/ui/packages/consul-ui/app/instance-initializers/nspace.js +++ b/ui/packages/consul-ui/app/instance-initializers/nspace.js @@ -101,22 +101,6 @@ export function initialize(container) { } register(container, route, item); }); - - // tell the view we have nspaces enabled - container - .lookup('service:dom') - .root() - .classList.add('has-nspaces'); - } - // TODO: This needs to live in its own initializer, either: - // 1. Make it be about adding classes to the root dom node - // 2. Make it be about config and things to do on initialization re: config - // If we go with 1 then we need to move both this and the above nspaces class - if (env('CONSUL_ACLS_ENABLED')) { - container - .lookup('service:dom') - .root() - .classList.add('has-acls'); } } diff --git a/ui/packages/consul-ui/app/styles/components/app-view/layout.scss b/ui/packages/consul-ui/app/styles/components/app-view/layout.scss index e7bf50502..358cd64e2 100644 --- a/ui/packages/consul-ui/app/styles/components/app-view/layout.scss +++ b/ui/packages/consul-ui/app/styles/components/app-view/layout.scss @@ -12,10 +12,6 @@ align-items: flex-start; } /* units */ -%app-view { - margin-top: 50px; -} - %app-view-title { padding-bottom: 0.2em; } diff --git a/ui/packages/consul-ui/app/styles/layout.scss b/ui/packages/consul-ui/app/styles/layout.scss index 46cf52cdb..99f8c7d0f 100644 --- a/ui/packages/consul-ui/app/styles/layout.scss +++ b/ui/packages/consul-ui/app/styles/layout.scss @@ -1,5 +1,14 @@ @import 'layouts/index'; +.app-view { + margin-top: 50px; +} +@media #{$--lt-spacious-page-header} { + html:not(.with-breadcrumbs) .app-view { + margin-top: 10px; + } +} + /* all forms have a margin below the header */ html[data-route$='create'] .app-view > header + div > *:first-child, html[data-route$='edit'] .app-view > header + div > *:first-child { @@ -66,11 +75,6 @@ html[data-route$='edit'] main { @extend %content-container-restricted; } -@media #{$--lt-spacious-page-header} { - html[data-route$='.index']:not([data-route^='dc.kv']) .app-view { - margin-top: 10px; - } -} @media #{$--lt-spacious-page-header} { .actions button.copy-btn { margin-top: -56px; diff --git a/ui/packages/consul-ui/app/templates/application.hbs b/ui/packages/consul-ui/app/templates/application.hbs index c19a304e2..6fb5c3880 100644 --- a/ui/packages/consul-ui/app/templates/application.hbs +++ b/ui/packages/consul-ui/app/templates/application.hbs @@ -1,5 +1,11 @@ {{page-title 'Consul' separator=' - '}} +{{#if (env 'CONSUL_ACLS_ENABLED')}} + {{document-attrs class="has-acls"}} +{{/if}} +{{#if (env 'CONSUL_NSPACES_ENABLED')}} + {{document-attrs class="has-nspaces"}} +{{/if}} {{#if (not-eq router.currentRouteName 'application')}} + {{#if (not-eq parent.Key '/') }}
    - {{#if (not-eq parent.Key '/') }}
  1. Key / Values
  2. - {{/if}} {{#each (slice 0 -2 (split parent.Key '/')) as |breadcrumb index|}}
  3. {{breadcrumb}}
  4. {{/each}}
+ {{/if}}

{{#if (eq parent.Key '/')}} diff --git a/ui/packages/consul-ui/tests/unit/helpers/document-attrs-test.js b/ui/packages/consul-ui/tests/unit/helpers/document-attrs-test.js new file mode 100644 index 000000000..7d2f2ac22 --- /dev/null +++ b/ui/packages/consul-ui/tests/unit/helpers/document-attrs-test.js @@ -0,0 +1,41 @@ +import { module, test } from 'qunit'; +import Helper from 'consul-ui/helpers/document-attrs'; + +const root = { + classList: { + add: () => {}, + remove: () => {}, + }, +}; +module('Unit | Helper | document-attrs', function() { + test('synchronize adds and removes values correctly', function(assert) { + let attrs, actual; + // add first helper + const a = new Helper(); + attrs = a.synchronize(root, { + class: 'a b a a a a', + }); + actual = [...attrs.get('class').keys()]; + assert.deepEqual(actual, ['a', 'b'], 'keys are adding correctly'); + const b = new Helper(); + // add second helper + attrs = b.synchronize(root, { + class: 'z a a a a', + }); + actual = [...attrs.get('class').keys()]; + assert.deepEqual(actual, ['a', 'b', 'z'], 'more keys are added correctly'); + // remove second helper + b.synchronize(root); + actual = [...attrs.get('class').keys()]; + assert.deepEqual(actual, ['a', 'b'], 'keys are removed, leaving keys that need to remain'); + // remove first helper + a.synchronize(root); + assert.ok( + typeof attrs.get('class') === 'undefined', + 'property is completely removed once its empty' + ); + assert.throws(() => { + a.synchronize(root, { data: 'a' }); + }, `throws an error if the attrs isn't class`); + }); +});