Use local-storage service to manage localStorage

Use local-storage service, prototyped here https://github.com/LevelbossMike/local-storage-service, to manage local storage usage in an octane way. Does not write to local storage in tests by default and is easy to stub out.
This commit is contained in:
wenincode 2022-10-18 09:40:47 -06:00
parent 63c4d670d9
commit c450183b4c
7 changed files with 214 additions and 49 deletions

View File

@ -1,34 +1,27 @@
import Component from '@glimmer/component'; import Component from '@glimmer/component';
import { action } from '@ember/object'; import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking'; import { storageFor } from '../../../../services/local-storage';
const DISMISSED_VALUE = 'true';
export default class AgentlessNotice extends Component { export default class AgentlessNotice extends Component {
storageKey = 'consul-nodes-agentless-notice-dismissed'; storageKey = 'nodes-agentless-dismissed';
@tracked hasDismissedNotice = false; @storageFor('notices') notices;
constructor(owner, args) { constructor(owner, args) {
super(owner, args); super(owner, args);
if (this.args.postfix) { if (this.args.postfix) {
this.storageKey = `consul-nodes-agentless-notice-dismissed-${this.args.postfix}`; this.storageKey = `nodes-agentless-dismissed-${this.args.postfix}`;
}
if (window.localStorage.getItem(this.storageKey) === DISMISSED_VALUE) {
this.hasDismissedNotice = true;
} }
} }
get isVisible() { get isVisible() {
const { items, filteredItems } = this.args; const { items, filteredItems } = this.args;
return !this.hasDismissedNotice && items.length > filteredItems.length; return !this.notices.state.includes(this.storageKey) && items.length > filteredItems.length;
} }
@action @action
dismissAgentlessNotice() { dismissAgentlessNotice() {
window.localStorage.setItem(this.storageKey, DISMISSED_VALUE); this.notices.add(this.storageKey);
this.hasDismissedNotice = true;
} }
} }

View File

@ -0,0 +1,126 @@
import Service from '@ember/service';
import { getOwner } from '@ember/application';
import ENV from 'consul-ui/config/environment';
export function storageFor(key) {
return function () {
return {
get() {
const owner = getOwner(this);
const localStorageService = owner.lookup('service:localStorage');
return localStorageService.getBucket(key);
},
};
};
}
/**
* An in-memory stub of window.localStorage. Ideally this would
* implement the [Storage](https://developer.mozilla.org/en-US/docs/Web/API/Storage)-interface that localStorage implements
* as well.
*
* We use this implementation during testing to not pollute `window.localStorage`
*/
class MemoryStorage {
constructor() {
this.data = new Map();
}
getItem(key) {
return this.data.get(key);
}
setItem(key, value) {
return this.data.set(key, value.toString());
}
/**
* A function to seed data into MemoryStorage. This expects an object to be
* passed. The passed values will be persisted as a string - i.e. the values
* passed will call their `toString()`-method before writing to storage. You need
* to take this into account when you want to persist complex values, like arrays
* or objects:
*
* Example:
*
* ```js
* const storage = new MemoryStorage();
* storage.seed({ notices: ['notice-a', 'notice-b']});
*
* storage.getItem('notices') // => 'notice-a,notice-b'
*
* // won't work
* storage.seed({
* user: { name: 'Tomster' }
* })
*
* storage.getItem('user') // => '[object Object]'
*
* // this works
* storage.seed({
* . user: JSON.stringify({name: 'Tomster'})
* })
*
* storage.getItem('user') // => '{ "name": "Tomster" }'
* ```
* @param {object} data - the data to seed
*/
seed(data) {
const newData = new Map();
const keys = Object.keys(data);
keys.forEach((key) => {
newData.set(key, data[key].toString());
});
this.data = newData;
}
}
/**
* There might be better ways to do this but this is good enough for now.
* During testing we want to use MemoryStorage not window.localStorage.
*/
function initStorage() {
if (ENV.environment === 'test') {
return new MemoryStorage();
} else {
return window.localStorage;
}
}
/**
* A service that wraps access to local-storage. We wrap
* local-storage to not pollute local-storage during testing.
*/
export default class LocalStorageService extends Service {
constructor() {
super(...arguments);
this.storage = initStorage();
this.buckets = new Map();
}
getBucket(key) {
const bucket = this.buckets.get(key);
if (bucket) {
return bucket;
} else {
return this._setupBucket(key);
}
}
_setupBucket(key) {
const owner = getOwner(this);
const Klass = owner.factoryFor(`storage:${key}`).class;
const storage = new Klass(key, this.storage);
this.buckets.set(key, storage);
return storage;
}
}

View File

@ -0,0 +1,14 @@
export default class Storage {
constructor(key, storage) {
this.key = key;
this.storage = storage;
this.state = this.initState(this.key, this.storage);
}
initState() {
const { key, storage } = this;
return storage.getItem(key);
}
}

View File

@ -0,0 +1,24 @@
import { TrackedArray } from 'tracked-built-ins';
import Storage from './base';
export default class Notices extends Storage {
initState() {
const { key, storage } = this;
const persisted = storage.getItem(key);
if (persisted) {
return new TrackedArray(persisted.split(','));
} else {
return new TrackedArray();
}
}
add(value) {
const { key, storage, state } = this;
state.push(value);
storage.setItem(key, [...state]);
}
}

View File

@ -191,6 +191,7 @@
"text-encoding": "^0.7.0", "text-encoding": "^0.7.0",
"tippy.js": "^6.2.7", "tippy.js": "^6.2.7",
"torii": "^0.10.1", "torii": "^0.10.1",
"tracked-built-ins": "^3.1.0",
"unist-util-visit": "^2.0.3", "unist-util-visit": "^2.0.3",
"wayfarer": "^7.0.1", "wayfarer": "^7.0.1",
"webpack": "^5.74.0" "webpack": "^5.74.0"

View File

@ -2,16 +2,9 @@ import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit'; import { setupRenderingTest } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile'; import hbs from 'htmlbars-inline-precompile';
import { click, render } from '@ember/test-helpers'; import { click, render } from '@ember/test-helpers';
import sinon from 'sinon';
module('Integration | Component | consul node agentless-notice', function (hooks) { module('Integration | Component | consul node agentless-notice', function (hooks) {
setupRenderingTest(hooks); setupRenderingTest(hooks);
hooks.beforeEach(() => {
const localStore = {};
sinon.stub(window.localStorage, 'getItem').callsFake((key) => localStore[key]);
sinon.stub(window.localStorage, 'setItem').callsFake((key, value) => (localStore[key] = value));
});
test('it does not display the notice if the filtered nodes are the same as the regular nodes', async function (assert) { test('it does not display the notice if the filtered nodes are the same as the regular nodes', async function (assert) {
this.set('nodes', [ this.set('nodes', [
@ -33,7 +26,6 @@ module('Integration | Component | consul node agentless-notice', function (hooks
await render( await render(
hbs`<Consul::Node::AgentlessNotice @items={{this.nodes}} @filteredItems={{this.filteredNodes}} />` hbs`<Consul::Node::AgentlessNotice @items={{this.nodes}} @filteredItems={{this.filteredNodes}} />`
); );
assert.true(window.localStorage.getItem.called);
assert assert
.dom('[data-test-node-agentless-notice]') .dom('[data-test-node-agentless-notice]')
.doesNotExist( .doesNotExist(
@ -66,10 +58,6 @@ module('Integration | Component | consul node agentless-notice', function (hooks
assert assert
.dom('[data-test-node-agentless-notice]') .dom('[data-test-node-agentless-notice]')
.doesNotExist('The agentless notice be dismissed'); .doesNotExist('The agentless notice be dismissed');
assert.true(
window.localStorage.setItem.calledOnceWith('consul-nodes-agentless-notice-dismissed', 'true'),
"Set the key in localstorage to 'true'"
);
}); });
test('it does not display if the localstorage key is already set to true', async function (assert) { test('it does not display if the localstorage key is already set to true', async function (assert) {
@ -81,30 +69,21 @@ module('Integration | Component | consul node agentless-notice', function (hooks
}, },
]); ]);
this.set('filteredNodes', [ this.set('filteredNodes', []);
{
Meta: {
'synthetic-node': false,
},
},
]);
window.localStorage.setItem('consul-nodes-agentless-notice-dismissed-partition', 'true'); const localStorage = this.owner.lookup('service:local-storage');
localStorage.storage.seed({
notices: ['nodes-agentless-dismissed-partition'],
});
await render( await render(
hbs`<Consul::Node::AgentlessNotice @items={{this.nodes}} @filteredItems={{this.filteredNodes}} @postfix="partition" />` hbs`<Consul::Node::AgentlessNotice @items={{this.nodes}} @filteredItems={{this.filteredNodes}} @postfix="partition" />`
); );
assert.true(
window.localStorage.getItem.calledOnceWith(
'consul-nodes-agentless-notice-dismissed-partition'
)
);
assert assert
.dom('[data-test-node-agentless-notice]') .dom('[data-test-node-agentless-notice]')
.doesNotExist( .doesNotExist(
"The agentless notice should not display if the local storage key has already been set to 'true'" 'The agentless notice should not display if the dismissal has already been stored in local storage'
); );
}); });
}); });

View File

@ -7862,7 +7862,7 @@ ember-auto-import@^2.2.3, ember-auto-import@^2.4.1, ember-auto-import@^2.4.2:
typescript-memoize "^1.0.0-alpha.3" typescript-memoize "^1.0.0-alpha.3"
walk-sync "^3.0.0" walk-sync "^3.0.0"
ember-basic-dropdown@3.0.21, ember-basic-dropdown@^3.0.16: ember-basic-dropdown@^3.0.16:
version "3.0.21" version "3.0.21"
resolved "https://registry.yarnpkg.com/ember-basic-dropdown/-/ember-basic-dropdown-3.0.21.tgz#5711d071966919c9578d2d5ac2c6dcadbb5ea0e0" resolved "https://registry.yarnpkg.com/ember-basic-dropdown/-/ember-basic-dropdown-3.0.21.tgz#5711d071966919c9578d2d5ac2c6dcadbb5ea0e0"
integrity sha512-Wu9hJWyqorKo+ZT2PMSIO1BxAeAdaiIC2IjSic0+HcKjmMU47botvG0xbxlprimOWaS9vM+nHat6Pt3xPvcB0A== integrity sha512-Wu9hJWyqorKo+ZT2PMSIO1BxAeAdaiIC2IjSic0+HcKjmMU47botvG0xbxlprimOWaS9vM+nHat6Pt3xPvcB0A==
@ -7907,7 +7907,7 @@ ember-changeset-validations@~3.9.0:
ember-get-config "^0.2.4" ember-get-config "^0.2.4"
ember-validators "^2.0.0" ember-validators "^2.0.0"
ember-changeset@3.10.1, ember-changeset@^3.9.1: ember-changeset@^3.9.1:
version "3.10.1" version "3.10.1"
resolved "https://registry.yarnpkg.com/ember-changeset/-/ember-changeset-3.10.1.tgz#d6f06bc55f867a2c1ac7c5fd780776bd1e5a9b60" resolved "https://registry.yarnpkg.com/ember-changeset/-/ember-changeset-3.10.1.tgz#d6f06bc55f867a2c1ac7c5fd780776bd1e5a9b60"
integrity sha512-4FoGKRcKxixSr+NBQ+ZoiwwbJE0/fuZRULUp9M1RIHejYhst+U8/ni47SsphrMhoRAcZCeyl+JqlBMlwR7v50g== integrity sha512-4FoGKRcKxixSr+NBQ+ZoiwwbJE0/fuZRULUp9M1RIHejYhst+U8/ni47SsphrMhoRAcZCeyl+JqlBMlwR7v50g==
@ -7982,7 +7982,7 @@ ember-cli-babel@^6.0.0, ember-cli-babel@^6.0.0-beta.4, ember-cli-babel@^6.11.0,
ember-cli-version-checker "^2.1.2" ember-cli-version-checker "^2.1.2"
semver "^5.5.0" semver "^5.5.0"
ember-cli-babel@^7.12.0, ember-cli-babel@^7.13.2, ember-cli-babel@^7.26.1, ember-cli-babel@^7.26.11, ember-cli-babel@^7.26.3, ember-cli-babel@^7.26.5, ember-cli-babel@^7.4.0: ember-cli-babel@^7.12.0, ember-cli-babel@^7.13.2, ember-cli-babel@^7.26.1, ember-cli-babel@^7.26.10, ember-cli-babel@^7.26.11, ember-cli-babel@^7.26.3, ember-cli-babel@^7.26.5, ember-cli-babel@^7.4.0:
version "7.26.11" version "7.26.11"
resolved "https://registry.yarnpkg.com/ember-cli-babel/-/ember-cli-babel-7.26.11.tgz#50da0fe4dcd99aada499843940fec75076249a9f" resolved "https://registry.yarnpkg.com/ember-cli-babel/-/ember-cli-babel-7.26.11.tgz#50da0fe4dcd99aada499843940fec75076249a9f"
integrity sha512-JJYeYjiz/JTn34q7F5DSOjkkZqy8qwFOOxXfE6pe9yEJqWGu4qErKxlz8I22JoVEQ/aBUO+OcKTpmctvykM9YA== integrity sha512-JJYeYjiz/JTn34q7F5DSOjkkZqy8qwFOOxXfE6pe9yEJqWGu4qErKxlz8I22JoVEQ/aBUO+OcKTpmctvykM9YA==
@ -8448,6 +8448,22 @@ ember-cli-typescript@^5.0.0:
stagehand "^1.0.0" stagehand "^1.0.0"
walk-sync "^2.2.0" walk-sync "^2.2.0"
ember-cli-typescript@^5.1.0:
version "5.1.1"
resolved "https://registry.yarnpkg.com/ember-cli-typescript/-/ember-cli-typescript-5.1.1.tgz#cf561026f3e7bd05312c1c212acffa1c30d5fa0c"
integrity sha512-DbzATYWY8nbXwSxXqtK8YlqGJTcyFyL+eg6IGCc2ur0AMnq/H+o6Z9np9eGoq1sI+HwX7vBkOVoD3k0WurAwXg==
dependencies:
ansi-to-html "^0.6.15"
broccoli-stew "^3.0.0"
debug "^4.0.0"
execa "^4.0.0"
fs-extra "^9.0.1"
resolve "^1.5.0"
rsvp "^4.8.1"
semver "^7.3.2"
stagehand "^1.0.0"
walk-sync "^2.2.0"
ember-cli-version-checker@^2.1.0, ember-cli-version-checker@^2.1.2: ember-cli-version-checker@^2.1.0, ember-cli-version-checker@^2.1.2:
version "2.2.0" version "2.2.0"
resolved "https://registry.yarnpkg.com/ember-cli-version-checker/-/ember-cli-version-checker-2.2.0.tgz#47771b731fe0962705e27c8199a9e3825709f3b3" resolved "https://registry.yarnpkg.com/ember-cli-version-checker/-/ember-cli-version-checker-2.2.0.tgz#47771b731fe0962705e27c8199a9e3825709f3b3"
@ -9247,6 +9263,14 @@ ember-text-measurer@^0.6.0:
ember-cli-babel "^7.19.0" ember-cli-babel "^7.19.0"
ember-cli-htmlbars "^4.3.1" ember-cli-htmlbars "^4.3.1"
ember-tracked-storage-polyfill@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/ember-tracked-storage-polyfill/-/ember-tracked-storage-polyfill-1.0.0.tgz#84d307a1e4badc5f84dca681db2cfea9bdee8a77"
integrity sha512-eL7lZat68E6P/D7b9UoTB5bB5Oh/0aju0Z7PCMi3aTwhaydRaxloE7TGrTRYU+NdJuyNVZXeGyxFxn2frvd3TA==
dependencies:
ember-cli-babel "^7.26.3"
ember-cli-htmlbars "^5.7.1"
"ember-truth-helpers@^2.1.0 || ^3.0.0", ember-truth-helpers@^3.0.0: "ember-truth-helpers@^2.1.0 || ^3.0.0", ember-truth-helpers@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/ember-truth-helpers/-/ember-truth-helpers-3.0.0.tgz#86766bdca4ac9b86bce3d262dff2aabc4a0ea384" resolved "https://registry.yarnpkg.com/ember-truth-helpers/-/ember-truth-helpers-3.0.0.tgz#86766bdca4ac9b86bce3d262dff2aabc4a0ea384"
@ -16641,6 +16665,15 @@ tr46@~0.0.3:
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
tracked-built-ins@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/tracked-built-ins/-/tracked-built-ins-3.1.0.tgz#827703e8e8857e45ac449dfc41e8706e0d6da309"
integrity sha512-yPEZV1aYaw7xFWdoEluvdwNxIJIA834HaBQaMATjNAYPwd1fRqIJ46YnuRo6+9mRRWu6nM6sJqrVVa5H6UhFuw==
dependencies:
ember-cli-babel "^7.26.10"
ember-cli-typescript "^5.1.0"
ember-tracked-storage-polyfill "^1.0.0"
tracked-maps-and-sets@^2.1.0: tracked-maps-and-sets@^2.1.0:
version "2.2.1" version "2.2.1"
resolved "https://registry.yarnpkg.com/tracked-maps-and-sets/-/tracked-maps-and-sets-2.2.1.tgz#323dd40540c561e8b0ffdec8bf129c68ec5025f9" resolved "https://registry.yarnpkg.com/tracked-maps-and-sets/-/tracked-maps-and-sets-2.2.1.tgz#323dd40540c561e8b0ffdec8bf129c68ec5025f9"
@ -17177,7 +17210,7 @@ validate-peer-dependencies@^1.2.0:
resolve-package-path "^3.1.0" resolve-package-path "^3.1.0"
semver "^7.3.2" semver "^7.3.2"
validated-changeset@0.10.0, validated-changeset@~0.10.0: validated-changeset@~0.10.0:
version "0.10.0" version "0.10.0"
resolved "https://registry.yarnpkg.com/validated-changeset/-/validated-changeset-0.10.0.tgz#2e8188c089ab282c1b51fba3c289073f6bd14c8b" resolved "https://registry.yarnpkg.com/validated-changeset/-/validated-changeset-0.10.0.tgz#2e8188c089ab282c1b51fba3c289073f6bd14c8b"
integrity sha512-n8NB3ol6Tbi0O7bnq1wz81m5Wd1gfHw0HUcH4MatOfqO3DyXzWZV+bUaNq6wThXn20rMFB82C8pTNFSWbgXJLA== integrity sha512-n8NB3ol6Tbi0O7bnq1wz81m5Wd1gfHw0HUcH4MatOfqO3DyXzWZV+bUaNq6wThXn20rMFB82C8pTNFSWbgXJLA==
@ -17675,11 +17708,6 @@ xmlchars@^2.2.0:
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
xmlhttprequest-ssl@^1.6.3:
version "1.6.3"
resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.6.3.tgz#03b713873b01659dfa2c1c5d056065b27ddc2de6"
integrity sha512-3XfeQE/wNkvrIktn2Kf0869fC0BN6UpydVasGIeSm2B1Llihf7/0UfZM+eCkOw3P7bP4+qPgqhm7ZoxuJtFU0Q==
xtend@^4.0.0, xtend@^4.0.1, xtend@^4.0.2, xtend@~4.0.1: xtend@^4.0.0, xtend@^4.0.1, xtend@^4.0.2, xtend@~4.0.1:
version "4.0.2" version "4.0.2"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"