Merge pull request #14971 from hashicorp/ui/feature/agentless-nodes-banner
ui: agentless nodes notice banner banner
This commit is contained in:
commit
b790131289
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:feature
|
||||||
|
ui: Display notice banner on nodes index page if synthetic nodes are being filtered.
|
||||||
|
```
|
|
@ -0,0 +1,28 @@
|
||||||
|
{{#if isVisible}}
|
||||||
|
<Notice data-test-node-agentless-notice @type='info' class='agentless-node-notice' as |notice|>
|
||||||
|
<notice.Header>
|
||||||
|
<h3>
|
||||||
|
{{t 'routes.dc.nodes.index.agentless.notice.header'}}
|
||||||
|
</h3>
|
||||||
|
<button type='button' aria-label='Dismiss notice' {{on 'click' this.dismissAgentlessNotice}}>
|
||||||
|
<FlightIcon @name='x' />
|
||||||
|
</button>
|
||||||
|
</notice.Header>
|
||||||
|
<notice.Body>
|
||||||
|
<p>
|
||||||
|
{{t 'routes.dc.nodes.index.agentless.notice.body'}}
|
||||||
|
</p>
|
||||||
|
</notice.Body>
|
||||||
|
<notice.Footer>
|
||||||
|
<p class='docs-link'>
|
||||||
|
<a
|
||||||
|
href='{{env "CONSUL_DOCS_DEVELOPER_URL"}}/connect/dataplane'
|
||||||
|
target='_blank'
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
>
|
||||||
|
{{t 'routes.dc.nodes.index.agentless.notice.footer'}}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</notice.Footer>
|
||||||
|
</Notice>
|
||||||
|
{{/if}}
|
|
@ -0,0 +1,27 @@
|
||||||
|
import Component from '@glimmer/component';
|
||||||
|
import { action } from '@ember/object';
|
||||||
|
import { storageFor } from '../../../../services/local-storage';
|
||||||
|
|
||||||
|
export default class AgentlessNotice extends Component {
|
||||||
|
storageKey = 'nodes-agentless-dismissed';
|
||||||
|
@storageFor('notices') notices;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
|
||||||
|
if (this.args.postfix) {
|
||||||
|
this.storageKey = `nodes-agentless-dismissed-${this.args.postfix}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get isVisible() {
|
||||||
|
const { items, filteredItems } = this.args;
|
||||||
|
|
||||||
|
return !this.notices.state.includes(this.storageKey) && items.length > filteredItems.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
dismissAgentlessNotice() {
|
||||||
|
this.notices.add(this.storageKey);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
.agentless-node-notice header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -110,3 +110,4 @@
|
||||||
@import 'consul-ui/components/consul/peer/info';
|
@import 'consul-ui/components/consul/peer/info';
|
||||||
@import 'consul-ui/components/consul/peer/form';
|
@import 'consul-ui/components/consul/peer/form';
|
||||||
@import 'consul-ui/components/consul/hcp/home';
|
@import 'consul-ui/components/consul/hcp/home';
|
||||||
|
@import 'consul-ui/components/consul/node/agentless-notice';
|
||||||
|
|
|
@ -60,6 +60,7 @@
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</BlockSlot>
|
</BlockSlot>
|
||||||
<BlockSlot @name='content'>
|
<BlockSlot @name='content'>
|
||||||
|
<Consul::Node::AgentlessNotice @items={{items}} @filteredItems={{filtered}} @postfix={{concat route.params.partition route.params.dc}} />
|
||||||
<DataCollection
|
<DataCollection
|
||||||
@type='node'
|
@type='node'
|
||||||
@sort={{sort.value}}
|
@sort={{sort.value}}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
import { module, test } from 'qunit';
|
||||||
|
import { setupRenderingTest } from 'ember-qunit';
|
||||||
|
import hbs from 'htmlbars-inline-precompile';
|
||||||
|
import { click, render } from '@ember/test-helpers';
|
||||||
|
|
||||||
|
module('Integration | Component | consul node agentless-notice', function (hooks) {
|
||||||
|
setupRenderingTest(hooks);
|
||||||
|
|
||||||
|
test('it does not display the notice if the filtered nodes are the same as the regular nodes', async function (assert) {
|
||||||
|
this.set('nodes', [
|
||||||
|
{
|
||||||
|
Meta: {
|
||||||
|
'synthetic-node': false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.set('filteredNodes', [
|
||||||
|
{
|
||||||
|
Meta: {
|
||||||
|
'synthetic-node': false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await render(
|
||||||
|
hbs`<Consul::Node::AgentlessNotice @items={{this.nodes}} @filteredItems={{this.filteredNodes}} />`
|
||||||
|
);
|
||||||
|
assert
|
||||||
|
.dom('[data-test-node-agentless-notice]')
|
||||||
|
.doesNotExist(
|
||||||
|
'The agentless notice should not display if the items are the same as the filtered items'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it does display the notice when the filtered items are smaller then the regular items', async function (assert) {
|
||||||
|
this.set('nodes', [
|
||||||
|
{
|
||||||
|
Meta: {
|
||||||
|
'synthetic-node': true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.set('filteredNodes', []);
|
||||||
|
|
||||||
|
await render(
|
||||||
|
hbs`<Consul::Node::AgentlessNotice @items={{this.nodes}} @filteredItems={{this.filteredNodes}} />`
|
||||||
|
);
|
||||||
|
|
||||||
|
assert
|
||||||
|
.dom('[data-test-node-agentless-notice]')
|
||||||
|
.exists(
|
||||||
|
'The agentless notice should display if their are less items then the filtered items'
|
||||||
|
);
|
||||||
|
|
||||||
|
await click('button');
|
||||||
|
assert
|
||||||
|
.dom('[data-test-node-agentless-notice]')
|
||||||
|
.doesNotExist('The agentless notice be dismissed');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it does not display if the localstorage key is already set to true', async function (assert) {
|
||||||
|
this.set('nodes', [
|
||||||
|
{
|
||||||
|
Meta: {
|
||||||
|
'synthetic-node': false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
this.set('filteredNodes', []);
|
||||||
|
|
||||||
|
const localStorage = this.owner.lookup('service:local-storage');
|
||||||
|
localStorage.storage.seed({
|
||||||
|
notices: ['nodes-agentless-dismissed-partition'],
|
||||||
|
});
|
||||||
|
|
||||||
|
await render(
|
||||||
|
hbs`<Consul::Node::AgentlessNotice @items={{this.nodes}} @filteredItems={{this.filteredNodes}} @postfix="partition" />`
|
||||||
|
);
|
||||||
|
|
||||||
|
assert
|
||||||
|
.dom('[data-test-node-agentless-notice]')
|
||||||
|
.doesNotExist(
|
||||||
|
'The agentless notice should not display if the dismissal has already been stored in local storage'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -55,6 +55,11 @@ dc:
|
||||||
|
|
||||||
nodes:
|
nodes:
|
||||||
index:
|
index:
|
||||||
|
agentless:
|
||||||
|
notice:
|
||||||
|
header: Consul 1.14 removes client nodes from Kubernetes Service Mesh deployments
|
||||||
|
body: Kubernetes clusters do not require client nodes to run as of Consul 1.14. Kubernetes nodes will not appear in this view.
|
||||||
|
footer: View documentation on Consul Dataplane
|
||||||
empty:
|
empty:
|
||||||
header: |
|
header: |
|
||||||
{items, select,
|
{items, select,
|
||||||
|
|
35
ui/yarn.lock
35
ui/yarn.lock
|
@ -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"
|
||||||
|
|
Loading…
Reference in New Issue