Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds searching and filtering for nodes on topology view #14913

Merged
merged 5 commits into from
Oct 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/14913.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
ui: adds searching and filtering to the topology page
```
145 changes: 140 additions & 5 deletions ui/app/controllers/topology.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,158 @@
/* 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';
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', {
maximumFractionDigits: 2,
});

@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 = '';
qpState = '';
qpVersion = '';
qpClass = '';
qpDatacenter = '';

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('model.nodes', '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('model.nodes', '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('model.nodes', '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;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because we want to be able to filter nodes now, I've renamed what was filteredNodes to pre09Nodes. This modifies the code added from #9733 but I think in a healthy way. cc @DingoEatingFuzz


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) ||
node.datacenter.includes(searchTerm) ||
node.nodeClass.includes(searchTerm))
);
});
}

@computed('model.nodes.@each.datacenter')
get datacenters() {
Expand Down Expand Up @@ -156,9 +291,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;
}
}
}
1 change: 1 addition & 0 deletions ui/app/templates/components/topo-viz.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -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}}>
<FlexMasonry
Expand Down
2 changes: 2 additions & 0 deletions ui/app/templates/components/topo-viz/node.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
<strong>{{@node.node.name}}</strong>
<span class="bumper-left">{{this.count}} Allocs</span>
<span class="bumper-left is-faded">{{format-scheduled-bytes @node.memory start="MiB"}}, {{format-scheduled-hertz @node.cpu}}</span>
<span class="bumper-left is-faded">{{@node.node.status}}</span>
<span class="bumper-left is-faded">{{@node.node.version}}</span>
</p>
{{/unless}}
<svg
Expand Down
60 changes: 54 additions & 6 deletions ui/app/templates/topology.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,26 @@
{{#if this.isForbidden}}
<ForbiddenMessage />
{{else}}
{{#if this.filteredNodes}}
{{#if this.pre09Nodes}}
<div class="notification is-warning">
<div data-test-filtered-nodes-warning class="columns">
<div class="column">
<h3 data-test-title class="title is-4">
Some Clients Were Filtered
</h3>
<p data-test-message>
{{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
</p>
</div>
<div class="column is-centered is-minimum">
<button
data-test-dismiss
class="button is-warning"
onclick={{action (mut this.filteredNodes) null}}
onclick={{action (mut this.pre09Nodes) null}}
type="button"
>
Okay
Expand Down Expand Up @@ -462,12 +462,60 @@
</div>
</div>
<div class="column">
<div class="toolbar">
<div class="toolbar-item">
{{#if this.model.nodes.length}}
<SearchBox
@inputClass="node-search"
@searchTerm={{mut this.searchTerm}}
@placeholder="Search clients..."
/>
{{/if}}
</div>
<div class="toolbar-item is-right-aligned is-mobile-full-width">
<div class="button-bar">
<MultiSelectDropdown
data-test-datacenter-facet
@label="Datacenter"
@options={{this.optionsDatacenter}}
@selection={{this.selectionDatacenter}}
@onSelect={{action this.setFacetQueryParam "qpDatacenter"}}
/>
<MultiSelectDropdown
data-test-class-facet
@label="Class"
@options={{this.optionsClass}}
@selection={{this.selectionClass}}
@onSelect={{action this.setFacetQueryParam "qpClass"}}
/>
<MultiSelectDropdown
data-test-state-facet
@label="State"
@options={{this.optionsState}}
@selection={{this.selectionState}}
@onSelect={{action this.setFacetQueryParam "qpState"}}
/>
<MultiSelectDropdown
data-test-version-facet
@label="Version"
@options={{this.optionsVersion}}
@selection={{this.selectionVersion}}
@onSelect={{action this.setFacetQueryParam "qpVersion"}}
/>
</div>
</div>
</div>
<TopoViz
@nodes={{this.model.nodes}}
@nodes={{this.filteredNodes}}
@allocations={{this.model.allocations}}
@onAllocationSelect={{action this.setAllocation}}
@onNodeSelect={{action this.setNode}}
@onDataError={{action this.handleTopoVizDataError}}
@filters={{hash
search=this.searchTerm
clientState=this.selectionState
clientVersion=this.selectionVersion
}}
/>
</div>
</div>
Expand Down
26 changes: 25 additions & 1 deletion ui/tests/acceptance/topology-test.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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 });
});
});
8 changes: 8 additions & 0 deletions ui/tests/pages/topology.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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]'),
Expand Down