diff --git a/.changelog/11366.txt b/.changelog/11366.txt new file mode 100644 index 000000000000..d304935aa19f --- /dev/null +++ b/.changelog/11366.txt @@ -0,0 +1,3 @@ +```release-note:improvement +ui: Display the Nomad version in the Servers and Clients tables and allow filtering and sorting +``` diff --git a/ui/app/controllers/clients/index.js b/ui/app/controllers/clients/index.js index 14c9195d3f24..f801bc8fe25e 100644 --- a/ui/app/controllers/clients/index.js +++ b/ui/app/controllers/clients/index.js @@ -12,7 +12,7 @@ import classic from 'ember-classic-decorator'; @classic export default class IndexController extends Controller.extend( - SortableFactory(['id', 'name', 'compositeStatus', 'datacenter']), + SortableFactory(['id', 'name', 'compositeStatus', 'datacenter', 'version']), Searchable ) { @service userSettings; @@ -43,6 +43,9 @@ export default class IndexController extends Controller.extend( { qpDatacenter: 'dc', }, + { + qpVersion: 'version', + }, { qpVolume: 'volume', }, @@ -62,11 +65,13 @@ export default class IndexController extends Controller.extend( qpClass = ''; qpState = ''; qpDatacenter = ''; + qpVersion = ''; qpVolume = ''; @selection('qpClass') selectionClass; @selection('qpState') selectionState; @selection('qpDatacenter') selectionDatacenter; + @selection('qpVersion') selectionVersion; @selection('qpVolume') selectionVolume; @computed('nodes.[]', 'selectionClass') @@ -108,6 +113,19 @@ export default class IndexController extends Controller.extend( return datacenters.sort().map(dc => ({ key: dc, label: dc })); } + @computed('nodes.[]', 'selectionVersion') + get optionsVersion() { + const versions = Array.from(new Set(this.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 })); + } + @computed('nodes.[]', 'selectionVolume') get optionsVolume() { const flatten = (acc, val) => acc.concat(val.toArray()); @@ -128,6 +146,7 @@ export default class IndexController extends Controller.extend( 'selectionClass', 'selectionState', 'selectionDatacenter', + 'selectionVersion', 'selectionVolume' ) get filteredNodes() { @@ -135,6 +154,7 @@ export default class IndexController extends Controller.extend( selectionClass: classes, selectionState: states, selectionDatacenter: datacenters, + selectionVersion: versions, selectionVolume: volumes, } = this; @@ -148,6 +168,7 @@ export default class IndexController extends Controller.extend( if (classes.length && !classes.includes(node.get('nodeClass'))) return false; if (statuses.length && !statuses.includes(node.get('status'))) return false; if (datacenters.length && !datacenters.includes(node.get('datacenter'))) return false; + if (versions.length && !versions.includes(node.get('version'))) return false; if (volumes.length && !node.hostVolumes.find(volume => volumes.includes(volume.name))) return false; diff --git a/ui/app/models/agent.js b/ui/app/models/agent.js index be70e5467b3b..5b66ed356a4e 100644 --- a/ui/app/models/agent.js +++ b/ui/app/models/agent.js @@ -28,4 +28,9 @@ export default class Agent extends Model { get isLeader() { return this.get('system.leader.rpcAddr') === this.rpcAddr; } + + @computed('tags.build') + get version() { + return this.tags?.build || ''; + } } diff --git a/ui/app/models/node.js b/ui/app/models/node.js index 8d63616c31c3..abb0958a2c88 100644 --- a/ui/app/models/node.js +++ b/ui/app/models/node.js @@ -21,6 +21,7 @@ export default class Node extends Model { @attr('string') statusDescription; @shortUUIDProperty('id') shortId; @attr('number') modifyIndex; + @attr('string') version; // Available from single response @attr('string') httpAddr; diff --git a/ui/app/templates/clients/index.hbs b/ui/app/templates/clients/index.hbs index d3d5c9ff076e..91d889569e7c 100644 --- a/ui/app/templates/clients/index.hbs +++ b/ui/app/templates/clients/index.hbs @@ -32,6 +32,12 @@ @options={{this.optionsDatacenter}} @selection={{this.selectionDatacenter}} @onSelect={{action this.setFacetQueryParam "qpDatacenter"}} /> + ID Name State - Address + Address Datacenter + Version # Volumes # Allocs diff --git a/ui/app/templates/components/client-node-row.hbs b/ui/app/templates/components/client-node-row.hbs index 3e6435af3c2f..1f341b0ac061 100644 --- a/ui/app/templates/components/client-node-row.hbs +++ b/ui/app/templates/components/client-node-row.hbs @@ -12,8 +12,9 @@ {{this.node.compositeStatus}} -{{this.node.httpAddr}} +{{this.node.httpAddr}} {{this.node.datacenter}} +{{this.node.version}} {{if this.node.hostVolumes.length this.node.hostVolumes.length}} {{#if this.node.allocations.isPending}} diff --git a/ui/app/templates/components/server-agent-row.hbs b/ui/app/templates/components/server-agent-row.hbs index ea1353f07bd2..5c16fc3153ce 100644 --- a/ui/app/templates/components/server-agent-row.hbs +++ b/ui/app/templates/components/server-agent-row.hbs @@ -1,6 +1,7 @@ {{this.agent.name}} {{this.agent.status}} {{if this.agent.isLeader "True" "False"}} -{{this.agent.address}} +{{this.agent.address}} {{this.agent.serfPort}} {{this.agent.datacenter}} +{{this.agent.version}} diff --git a/ui/app/templates/servers/index.hbs b/ui/app/templates/servers/index.hbs index 8173b9100dad..1daed7b45616 100644 --- a/ui/app/templates/servers/index.hbs +++ b/ui/app/templates/servers/index.hbs @@ -16,9 +16,10 @@ Name Status Leader - Address + Address port Datacenter + Version diff --git a/ui/mirage/factories/agent.js b/ui/mirage/factories/agent.js index cf985d95ba49..47f96df6c504 100644 --- a/ui/mirage/factories/agent.js +++ b/ui/mirage/factories/agent.js @@ -5,6 +5,7 @@ import { DATACENTERS } from '../common'; const UUIDS = provide(100, faker.random.uuid.bind(faker.random)); const AGENT_STATUSES = ['alive', 'leaving', 'left', 'failed']; +const AGENT_BUILDS = ['1.1.0-beta', '1.0.2-alpha+ent', ...provide(5, faker.system.semver)]; export default Factory.extend({ id: i => (i / 100 >= 1 ? `${UUIDS[i]}-${i}` : UUIDS[i]), @@ -29,6 +30,10 @@ export default Factory.extend({ Tags: generateTags(serfPort), }; }, + + version() { + return this.member.Tags?.build || ''; + }, }); function generateName() { @@ -44,5 +49,6 @@ function generateTags(serfPort) { return { port: rpcPortCandidate === serfPort ? rpcPortCandidate + 1 : rpcPortCandidate, dc: faker.helpers.randomize(DATACENTERS), + build: faker.helpers.randomize(AGENT_BUILDS), }; } diff --git a/ui/mirage/factories/node.js b/ui/mirage/factories/node.js index f670db32cee5..14a269ea2d23 100644 --- a/ui/mirage/factories/node.js +++ b/ui/mirage/factories/node.js @@ -7,6 +7,7 @@ import moment from 'moment'; const UUIDS = provide(100, faker.random.uuid.bind(faker.random)); const NODE_STATUSES = ['initializing', 'ready', 'down']; const NODE_CLASSES = provide(7, faker.company.bsBuzz.bind(faker.company)); +const NODE_VERSIONS = ['1.1.0-beta', '1.0.2-alpha+ent', ...provide(5, faker.system.semver)]; const REF_DATE = new Date(); export default Factory.extend({ @@ -22,6 +23,7 @@ export default Factory.extend({ createIndex: i => i, modifyIndex: () => faker.random.number({ min: 10, max: 2000 }), + version: () => faker.helpers.randomize(NODE_VERSIONS), httpAddr() { return this.name.split('@')[1]; diff --git a/ui/tests/acceptance/clients-list-test.js b/ui/tests/acceptance/clients-list-test.js index 3e6ea8e82403..dd76d5a8cc00 100644 --- a/ui/tests/acceptance/clients-list-test.js +++ b/ui/tests/acceptance/clients-list-test.js @@ -66,6 +66,7 @@ module('Acceptance | clients list', function(hooks) { ); assert.equal(nodeRow.address, node.httpAddr); assert.equal(nodeRow.datacenter, node.datacenter, 'Datacenter'); + assert.equal(nodeRow.version, node.version, 'Version'); assert.equal(nodeRow.allocations, allocations.length, '# Allocations'); }); @@ -146,7 +147,11 @@ module('Acceptance | clients list', function(hooks) { assert.equal(ClientsList.nodes[1].compositeStatus.text, 'initializing'); assert.equal(ClientsList.nodes[2].compositeStatus.text, 'down'); - assert.equal(ClientsList.nodes[2].compositeStatus.text, 'down', 'down takes priority over ineligible'); + assert.equal( + ClientsList.nodes[2].compositeStatus.text, + 'down', + 'down takes priority over ineligible' + ); assert.equal(ClientsList.nodes[4].compositeStatus.text, 'ineligible'); assert.ok(ClientsList.nodes[4].compositeStatus.isWarning, 'expected warning class'); @@ -299,6 +304,22 @@ module('Acceptance | clients list', function(hooks) { filter: (node, selection) => selection.includes(node.datacenter), }); + testFacet('Versions', { + facet: ClientsList.facets.version, + paramName: 'version', + expectedOptions(nodes) { + return Array.from(new Set(nodes.mapBy('version'))).sort(); + }, + async beforeEach() { + server.create('agent'); + server.createList('node', 2, { version: '0.12.0' }); + server.createList('node', 2, { version: '1.1.0-beta1' }); + server.createList('node', 2, { version: '1.2.0+ent' }); + await ClientsList.visit(); + }, + filter: (node, selection) => selection.includes(node.version), + }); + testFacet('Volumes', { facet: ClientsList.facets.volume, paramName: 'volume', diff --git a/ui/tests/acceptance/servers-list-test.js b/ui/tests/acceptance/servers-list-test.js index d4b30c580ab7..05ac3f327e79 100644 --- a/ui/tests/acceptance/servers-list-test.js +++ b/ui/tests/acceptance/servers-list-test.js @@ -63,6 +63,7 @@ module('Acceptance | servers list', function(hooks) { assert.equal(agentRow.address, agent.member.Address, 'Address'); assert.equal(agentRow.serfPort, agent.member.Port, 'Serf Port'); assert.equal(agentRow.datacenter, agent.member.Tags.dc, 'Datacenter'); + assert.equal(agentRow.version, agent.version, 'Version'); }); test('each server should link to the server detail page', async function(assert) { diff --git a/ui/tests/pages/clients/list.js b/ui/tests/pages/clients/list.js index 652a777dc4ed..f00c36a28901 100644 --- a/ui/tests/pages/clients/list.js +++ b/ui/tests/pages/clients/list.js @@ -49,6 +49,7 @@ export default create({ address: text('[data-test-client-address]'), datacenter: text('[data-test-client-datacenter]'), + version: text('[data-test-client-version]'), allocations: text('[data-test-client-allocations]'), clickRow: clickable(), @@ -75,6 +76,7 @@ export default create({ class: multiFacet('[data-test-class-facet]'), state: multiFacet('[data-test-state-facet]'), datacenter: multiFacet('[data-test-datacenter-facet]'), + version: multiFacet('[data-test-version-facet]'), volume: multiFacet('[data-test-volume-facet]'), }, }); diff --git a/ui/tests/pages/servers/list.js b/ui/tests/pages/servers/list.js index 9f8081e6bca1..cc884d4e1d62 100644 --- a/ui/tests/pages/servers/list.js +++ b/ui/tests/pages/servers/list.js @@ -11,6 +11,7 @@ export default create({ address: text('[data-test-server-address]'), serfPort: text('[data-test-server-port]'), datacenter: text('[data-test-server-datacenter]'), + version: text('[data-test-server-version]'), clickRow: clickable(), clickName: clickable('[data-test-server-name] a'),