From 184b928b978ec843fa66329874ce7b16805de61d Mon Sep 17 00:00:00 2001 From: Phil Renaud Date: Mon, 17 Oct 2022 14:23:19 -0400 Subject: [PATCH 1/5] Adds searching and filtering for nodes on topology view --- ui/app/controllers/topology.js | 138 +++++++++++++++++- ui/app/templates/components/topo-viz.hbs | 1 + ui/app/templates/components/topo-viz/node.hbs | 2 + ui/app/templates/topology.hbs | 60 +++++++- 4 files changed, 190 insertions(+), 11 deletions(-) diff --git a/ui/app/controllers/topology.js b/ui/app/controllers/topology.js index c3866eb5ea4c..8dfe22be1ccf 100644 --- a/ui/app/controllers/topology.js +++ b/ui/app/controllers/topology.js @@ -5,6 +5,13 @@ import { inject as service } from '@ember/service'; import { tracked } from '@glimmer/tracking'; import classic from 'ember-classic-decorator'; import { reduceBytes, reduceHertz } from 'nomad-ui/utils/units'; +import { + serialize, + deserializedQueryParam as selection, +} from 'nomad-ui/utils/qp-serialize'; +import { scheduleOnce } from '@ember/runloop'; +import intersection from 'lodash.intersection'; +import Searchable from 'nomad-ui/mixins/searchable'; const sumAggregator = (sum, value) => sum + (value || 0); const formatter = new Intl.NumberFormat(window.navigator.locale || 'en', { @@ -12,12 +19,133 @@ const formatter = new Intl.NumberFormat(window.navigator.locale || 'en', { }); @classic -export default class TopologyControllers extends Controller { +export default class TopologyControllers extends Controller.extend(Searchable) { @service userSettings; + queryParams = [ + { + searchTerm: 'search', + }, + { + qpState: 'status', + }, + { + qpVersion: 'version', + }, + { + qpClass: 'class', + }, + { + qpDatacenter: 'dc', + }, + ]; + + @tracked searchTerm = ''; + + setFacetQueryParam(queryParam, selection) { + this.set(queryParam, serialize(selection)); + } + + @selection('qpState') selectionState; + @selection('qpClass') selectionClass; + @selection('qpDatacenter') selectionDatacenter; + @selection('qpVersion') selectionVersion; + + @computed + get optionsState() { + return [ + { key: 'initializing', label: 'Initializing' }, + { key: 'ready', label: 'Ready' }, + { key: 'down', label: 'Down' }, + { key: 'ineligible', label: 'Ineligible' }, + { key: 'draining', label: 'Draining' }, + { key: 'disconnected', label: 'Disconnected' }, + ]; + } + + @computed('nodes.[]', 'selectionClass') + get optionsClass() { + const classes = Array.from(new Set(this.model.nodes.mapBy('nodeClass'))) + .compact() + .without(''); + + // Remove any invalid node classes from the query param/selection + scheduleOnce('actions', () => { + // eslint-disable-next-line ember/no-side-effects + this.set( + 'qpClass', + serialize(intersection(classes, this.selectionClass)) + ); + }); + + return classes.sort().map((dc) => ({ key: dc, label: dc })); + } + + @computed('nodes.[]', 'selectionDatacenter') + get optionsDatacenter() { + const datacenters = Array.from( + new Set(this.model.nodes.mapBy('datacenter')) + ).compact(); + + // Remove any invalid datacenters from the query param/selection + scheduleOnce('actions', () => { + // eslint-disable-next-line ember/no-side-effects + this.set( + 'qpDatacenter', + serialize(intersection(datacenters, this.selectionDatacenter)) + ); + }); + + return datacenters.sort().map((dc) => ({ key: dc, label: dc })); + } + + @computed('nodes.[]', 'selectionVersion') + get optionsVersion() { + const versions = Array.from( + new Set(this.model.nodes.mapBy('version')) + ).compact(); + + // Remove any invalid versions from the query param/selection + scheduleOnce('actions', () => { + // eslint-disable-next-line ember/no-side-effects + this.set( + 'qpVersion', + serialize(intersection(versions, this.selectionVersion)) + ); + }); + + return versions.sort().map((v) => ({ key: v, label: v })); + } + @alias('userSettings.showTopoVizPollingNotice') showPollingNotice; - @tracked filteredNodes = null; + @tracked pre09Nodes = null; + + get filteredNodes() { + const { nodes } = this.model; + return nodes.filter((node) => { + const { + searchTerm, + selectionState, + selectionVersion, + selectionDatacenter, + selectionClass, + } = this; + return ( + (selectionState.length ? selectionState.includes(node.status) : true) && + (selectionVersion.length + ? selectionVersion.includes(node.version) + : true) && + (selectionDatacenter.length + ? selectionDatacenter.includes(node.datacenter) + : true) && + (selectionClass.length + ? selectionClass.includes(node.nodeClass) + : true) && + node.name.includes(searchTerm) + ); + }); + } @computed('model.nodes.@each.datacenter') get datacenters() { @@ -156,9 +284,9 @@ export default class TopologyControllers extends Controller { @action handleTopoVizDataError(errors) { - const filteredNodesError = errors.findBy('type', 'filtered-nodes'); - if (filteredNodesError) { - this.filteredNodes = filteredNodesError.context; + const pre09NodesError = errors.findBy('type', 'filtered-nodes'); + if (pre09NodesError) { + this.pre09Nodes = pre09NodesError.context; } } } diff --git a/ui/app/templates/components/topo-viz.hbs b/ui/app/templates/components/topo-viz.hbs index baf84c9a7320..f457afc1f543 100644 --- a/ui/app/templates/components/topo-viz.hbs +++ b/ui/app/templates/components/topo-viz.hbs @@ -2,6 +2,7 @@ data-test-topo-viz class="topo-viz {{if this.isSingleColumn "is-single-column"}}" {{did-insert this.buildTopology}} + {{did-update this.buildTopology @nodes}} {{did-insert this.captureElement}} {{window-resize this.determineViewportColumns}}> {{@node.node.name}} {{this.count}} Allocs {{format-scheduled-bytes @node.memory start="MiB"}}, {{format-scheduled-hertz @node.cpu}} + {{@node.node.status}} + {{@node.node.version}}

{{/unless}} {{else}} - {{#if this.filteredNodes}} + {{#if this.pre09Nodes}}
@@ -13,10 +13,10 @@ Some Clients Were Filtered

- {{this.filteredNodes.length}} - {{if (eq this.filteredNodes.length 1) "client was" "clients were"}} + {{this.pre09Nodes.length}} + {{if (eq this.pre09Nodes.length 1) "client was" "clients were"}} filtered from the topology visualization. This is most likely due to the - {{pluralize "client" this.filteredNodes.length}} + {{pluralize "client" this.pre09Nodes.length}} running a version of Nomad

@@ -24,7 +24,7 @@
+
+
+ {{#if this.model.nodes.length}} + + {{/if}} +
+
+
+ + + + +
+
+
From a1f347e2a7e54b96f3e013d9cbc9d5ea41ec4f7e Mon Sep 17 00:00:00 2001 From: Phil Renaud Date: Mon, 17 Oct 2022 14:30:56 -0400 Subject: [PATCH 2/5] Lintfix and changelog --- .changelog/14913.txt | 3 +++ ui/app/controllers/topology.js | 7 ++++--- 2 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 .changelog/14913.txt diff --git a/.changelog/14913.txt b/.changelog/14913.txt new file mode 100644 index 000000000000..511b00a8c75a --- /dev/null +++ b/.changelog/14913.txt @@ -0,0 +1,3 @@ +```release-note:improvement +ui: adds searching and filtering to the topology page +``` diff --git a/ui/app/controllers/topology.js b/ui/app/controllers/topology.js index 8dfe22be1ccf..f12e9450a200 100644 --- a/ui/app/controllers/topology.js +++ b/ui/app/controllers/topology.js @@ -1,3 +1,4 @@ +/* eslint-disable ember/no-incorrect-calls-with-inline-anonymous-functions */ import Controller from '@ember/controller'; import { computed, action } from '@ember/object'; import { alias } from '@ember/object/computed'; @@ -63,7 +64,7 @@ export default class TopologyControllers extends Controller.extend(Searchable) { ]; } - @computed('nodes.[]', 'selectionClass') + @computed('model.nodes', 'nodes.[]', 'selectionClass') get optionsClass() { const classes = Array.from(new Set(this.model.nodes.mapBy('nodeClass'))) .compact() @@ -81,7 +82,7 @@ export default class TopologyControllers extends Controller.extend(Searchable) { return classes.sort().map((dc) => ({ key: dc, label: dc })); } - @computed('nodes.[]', 'selectionDatacenter') + @computed('model.nodes', 'nodes.[]', 'selectionDatacenter') get optionsDatacenter() { const datacenters = Array.from( new Set(this.model.nodes.mapBy('datacenter')) @@ -99,7 +100,7 @@ export default class TopologyControllers extends Controller.extend(Searchable) { return datacenters.sort().map((dc) => ({ key: dc, label: dc })); } - @computed('nodes.[]', 'selectionVersion') + @computed('model.nodes', 'nodes.[]', 'selectionVersion') get optionsVersion() { const versions = Array.from( new Set(this.model.nodes.mapBy('version')) From 56bd954fa3bfecf4c8a42e51d006f6b9630cf995 Mon Sep 17 00:00:00 2001 From: Phil Renaud Date: Mon, 17 Oct 2022 16:14:57 -0400 Subject: [PATCH 3/5] Acceptance tests for topology search and filter --- ui/app/templates/topology.hbs | 2 +- ui/tests/acceptance/topology-test.js | 26 +++++++++++++++++++++++++- ui/tests/pages/topology.js | 8 ++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/ui/app/templates/topology.hbs b/ui/app/templates/topology.hbs index 5cad3df1eb5b..6d3ab76f27ab 100644 --- a/ui/app/templates/topology.hbs +++ b/ui/app/templates/topology.hbs @@ -466,8 +466,8 @@
{{#if this.model.nodes.length}} {{/if}} diff --git a/ui/tests/acceptance/topology-test.js b/ui/tests/acceptance/topology-test.js index 2d664beccadc..87fbca0100d3 100644 --- a/ui/tests/acceptance/topology-test.js +++ b/ui/tests/acceptance/topology-test.js @@ -1,6 +1,6 @@ /* eslint-disable qunit/require-expect */ import { get } from '@ember/object'; -import { currentURL } from '@ember/test-helpers'; +import { currentURL, typeIn, click } from '@ember/test-helpers'; import { module, test } from 'qunit'; import { setupApplicationTest } from 'ember-qunit'; import { setupMirage } from 'ember-cli-mirage/test-support'; @@ -311,4 +311,28 @@ module('Acceptance | topology', function (hooks) { assert.ok(Topology.filteredNodesWarning.isPresent); assert.ok(Topology.filteredNodesWarning.message.startsWith('1')); }); + + test('Filtering and Querying reduces the number of nodes shown', async function (assert) { + server.createList('node', 10); + server.createList('node', 2, { + nodeClass: 'foo-bar-baz', + }); + server.createList('allocation', 5); + + await Topology.visit(); + assert.dom('[data-test-topo-viz-node]').exists({ count: 12 }); + + await typeIn('input.node-search', server.schema.nodes.first().name); + assert.dom('[data-test-topo-viz-node]').exists({ count: 1 }); + await typeIn('input.node-search', server.schema.nodes.first().name); + assert.dom('[data-test-topo-viz-node]').doesNotExist(); + await click('[title="Clear search"]'); + assert.dom('[data-test-topo-viz-node]').exists({ count: 12 }); + + await Topology.facets.class.toggle(); + await Topology.facets.class.options + .findOneBy('label', 'foo-bar-baz') + .toggle(); + assert.dom('[data-test-topo-viz-node]').exists({ count: 2 }); + }); }); diff --git a/ui/tests/pages/topology.js b/ui/tests/pages/topology.js index c62dfeef0502..6442a4d2575d 100644 --- a/ui/tests/pages/topology.js +++ b/ui/tests/pages/topology.js @@ -8,6 +8,7 @@ import { visitable, } from 'ember-cli-page-object'; +import { multiFacet } from 'nomad-ui/tests/pages/components/facet'; import TopoViz from 'nomad-ui/tests/pages/components/topo-viz'; import notification from 'nomad-ui/tests/pages/components/notification'; @@ -19,6 +20,13 @@ export default create({ viz: TopoViz('[data-test-topo-viz]'), + facets: { + datacenter: multiFacet('[data-test-datacenter-facet]'), + class: multiFacet('[data-test-class-facet]'), + state: multiFacet('[data-test-state-facet]'), + version: multiFacet('[data-test-version-facet]'), + }, + clusterInfoPanel: { scope: '[data-test-info-panel]', nodeCount: text('[data-test-node-count]'), From 6ab4613b8ec53fc667bbcfcf75dbcfcb3ee8893a Mon Sep 17 00:00:00 2001 From: Phil Renaud Date: Tue, 18 Oct 2022 10:38:52 -0400 Subject: [PATCH 4/5] Search terms also apply to class and dc on topo page --- ui/app/controllers/topology.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ui/app/controllers/topology.js b/ui/app/controllers/topology.js index f12e9450a200..f078d5e3b7d0 100644 --- a/ui/app/controllers/topology.js +++ b/ui/app/controllers/topology.js @@ -143,7 +143,9 @@ export default class TopologyControllers extends Controller.extend(Searchable) { (selectionClass.length ? selectionClass.includes(node.nodeClass) : true) && - node.name.includes(searchTerm) + (node.name.includes(searchTerm) || + node.datacenter.includes(searchTerm) || + node.nodeClass.includes(searchTerm)) ); }); } From a6331cf8694bbb9291ed534bda3825ce60fe4e60 Mon Sep 17 00:00:00 2001 From: Phil Renaud Date: Wed, 19 Oct 2022 13:09:30 -0400 Subject: [PATCH 5/5] Initialize queryparam values so as to not break history state --- ui/app/controllers/topology.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ui/app/controllers/topology.js b/ui/app/controllers/topology.js index f078d5e3b7d0..901dcaa398cb 100644 --- a/ui/app/controllers/topology.js +++ b/ui/app/controllers/topology.js @@ -42,6 +42,10 @@ export default class TopologyControllers extends Controller.extend(Searchable) { ]; @tracked searchTerm = ''; + qpState = ''; + qpVersion = ''; + qpClass = ''; + qpDatacenter = ''; setFacetQueryParam(queryParam, selection) { this.set(queryParam, serialize(selection));