mirror of
https://github.com/kemko/nomad.git
synced 2026-01-01 16:05:42 +03:00
ui: display Nomad version in the Clients and Servers table (#11366)
This commit is contained in:
3
.changelog/11366.txt
Normal file
3
.changelog/11366.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
```release-note:improvement
|
||||||
|
ui: Display the Nomad version in the Servers and Clients tables and allow filtering and sorting
|
||||||
|
```
|
||||||
@@ -12,7 +12,7 @@ import classic from 'ember-classic-decorator';
|
|||||||
|
|
||||||
@classic
|
@classic
|
||||||
export default class IndexController extends Controller.extend(
|
export default class IndexController extends Controller.extend(
|
||||||
SortableFactory(['id', 'name', 'compositeStatus', 'datacenter']),
|
SortableFactory(['id', 'name', 'compositeStatus', 'datacenter', 'version']),
|
||||||
Searchable
|
Searchable
|
||||||
) {
|
) {
|
||||||
@service userSettings;
|
@service userSettings;
|
||||||
@@ -43,6 +43,9 @@ export default class IndexController extends Controller.extend(
|
|||||||
{
|
{
|
||||||
qpDatacenter: 'dc',
|
qpDatacenter: 'dc',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
qpVersion: 'version',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
qpVolume: 'volume',
|
qpVolume: 'volume',
|
||||||
},
|
},
|
||||||
@@ -62,11 +65,13 @@ export default class IndexController extends Controller.extend(
|
|||||||
qpClass = '';
|
qpClass = '';
|
||||||
qpState = '';
|
qpState = '';
|
||||||
qpDatacenter = '';
|
qpDatacenter = '';
|
||||||
|
qpVersion = '';
|
||||||
qpVolume = '';
|
qpVolume = '';
|
||||||
|
|
||||||
@selection('qpClass') selectionClass;
|
@selection('qpClass') selectionClass;
|
||||||
@selection('qpState') selectionState;
|
@selection('qpState') selectionState;
|
||||||
@selection('qpDatacenter') selectionDatacenter;
|
@selection('qpDatacenter') selectionDatacenter;
|
||||||
|
@selection('qpVersion') selectionVersion;
|
||||||
@selection('qpVolume') selectionVolume;
|
@selection('qpVolume') selectionVolume;
|
||||||
|
|
||||||
@computed('nodes.[]', 'selectionClass')
|
@computed('nodes.[]', 'selectionClass')
|
||||||
@@ -108,6 +113,19 @@ export default class IndexController extends Controller.extend(
|
|||||||
return datacenters.sort().map(dc => ({ key: dc, label: dc }));
|
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')
|
@computed('nodes.[]', 'selectionVolume')
|
||||||
get optionsVolume() {
|
get optionsVolume() {
|
||||||
const flatten = (acc, val) => acc.concat(val.toArray());
|
const flatten = (acc, val) => acc.concat(val.toArray());
|
||||||
@@ -128,6 +146,7 @@ export default class IndexController extends Controller.extend(
|
|||||||
'selectionClass',
|
'selectionClass',
|
||||||
'selectionState',
|
'selectionState',
|
||||||
'selectionDatacenter',
|
'selectionDatacenter',
|
||||||
|
'selectionVersion',
|
||||||
'selectionVolume'
|
'selectionVolume'
|
||||||
)
|
)
|
||||||
get filteredNodes() {
|
get filteredNodes() {
|
||||||
@@ -135,6 +154,7 @@ export default class IndexController extends Controller.extend(
|
|||||||
selectionClass: classes,
|
selectionClass: classes,
|
||||||
selectionState: states,
|
selectionState: states,
|
||||||
selectionDatacenter: datacenters,
|
selectionDatacenter: datacenters,
|
||||||
|
selectionVersion: versions,
|
||||||
selectionVolume: volumes,
|
selectionVolume: volumes,
|
||||||
} = this;
|
} = this;
|
||||||
|
|
||||||
@@ -148,6 +168,7 @@ export default class IndexController extends Controller.extend(
|
|||||||
if (classes.length && !classes.includes(node.get('nodeClass'))) return false;
|
if (classes.length && !classes.includes(node.get('nodeClass'))) return false;
|
||||||
if (statuses.length && !statuses.includes(node.get('status'))) return false;
|
if (statuses.length && !statuses.includes(node.get('status'))) return false;
|
||||||
if (datacenters.length && !datacenters.includes(node.get('datacenter'))) 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)))
|
if (volumes.length && !node.hostVolumes.find(volume => volumes.includes(volume.name)))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
|||||||
@@ -28,4 +28,9 @@ export default class Agent extends Model {
|
|||||||
get isLeader() {
|
get isLeader() {
|
||||||
return this.get('system.leader.rpcAddr') === this.rpcAddr;
|
return this.get('system.leader.rpcAddr') === this.rpcAddr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@computed('tags.build')
|
||||||
|
get version() {
|
||||||
|
return this.tags?.build || '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export default class Node extends Model {
|
|||||||
@attr('string') statusDescription;
|
@attr('string') statusDescription;
|
||||||
@shortUUIDProperty('id') shortId;
|
@shortUUIDProperty('id') shortId;
|
||||||
@attr('number') modifyIndex;
|
@attr('number') modifyIndex;
|
||||||
|
@attr('string') version;
|
||||||
|
|
||||||
// Available from single response
|
// Available from single response
|
||||||
@attr('string') httpAddr;
|
@attr('string') httpAddr;
|
||||||
|
|||||||
@@ -32,6 +32,12 @@
|
|||||||
@options={{this.optionsDatacenter}}
|
@options={{this.optionsDatacenter}}
|
||||||
@selection={{this.selectionDatacenter}}
|
@selection={{this.selectionDatacenter}}
|
||||||
@onSelect={{action this.setFacetQueryParam "qpDatacenter"}} />
|
@onSelect={{action this.setFacetQueryParam "qpDatacenter"}} />
|
||||||
|
<MultiSelectDropdown
|
||||||
|
data-test-version-facet
|
||||||
|
@label="Version"
|
||||||
|
@options={{this.optionsVersion}}
|
||||||
|
@selection={{this.selectionVersion}}
|
||||||
|
@onSelect={{action this.setFacetQueryParam "qpVersion"}} />
|
||||||
<MultiSelectDropdown
|
<MultiSelectDropdown
|
||||||
data-test-volume-facet
|
data-test-volume-facet
|
||||||
@label="Volume"
|
@label="Volume"
|
||||||
@@ -56,8 +62,9 @@
|
|||||||
<t.sort-by @prop="id">ID</t.sort-by>
|
<t.sort-by @prop="id">ID</t.sort-by>
|
||||||
<t.sort-by @class="is-200px is-truncatable" @prop="name">Name</t.sort-by>
|
<t.sort-by @class="is-200px is-truncatable" @prop="name">Name</t.sort-by>
|
||||||
<t.sort-by @prop="compositeStatus">State</t.sort-by>
|
<t.sort-by @prop="compositeStatus">State</t.sort-by>
|
||||||
<th>Address</th>
|
<th class="is-200px is-truncatable">Address</th>
|
||||||
<t.sort-by @prop="datacenter">Datacenter</t.sort-by>
|
<t.sort-by @prop="datacenter">Datacenter</t.sort-by>
|
||||||
|
<t.sort-by @prop="version">Version</t.sort-by>
|
||||||
<th># Volumes</th>
|
<th># Volumes</th>
|
||||||
<th># Allocs</th>
|
<th># Allocs</th>
|
||||||
</t.head>
|
</t.head>
|
||||||
|
|||||||
@@ -12,8 +12,9 @@
|
|||||||
<span class="{{this.compositeStatusClass}}">{{this.node.compositeStatus}}</span>
|
<span class="{{this.compositeStatusClass}}">{{this.node.compositeStatus}}</span>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td data-test-client-address>{{this.node.httpAddr}}</td>
|
<td data-test-client-address class="is-200px is-truncatable">{{this.node.httpAddr}}</td>
|
||||||
<td data-test-client-datacenter>{{this.node.datacenter}}</td>
|
<td data-test-client-datacenter>{{this.node.datacenter}}</td>
|
||||||
|
<td data-test-client-version>{{this.node.version}}</td>
|
||||||
<td data-test-client-volumes>{{if this.node.hostVolumes.length this.node.hostVolumes.length}}</td>
|
<td data-test-client-volumes>{{if this.node.hostVolumes.length this.node.hostVolumes.length}}</td>
|
||||||
<td data-test-client-allocations>
|
<td data-test-client-allocations>
|
||||||
{{#if this.node.allocations.isPending}}
|
{{#if this.node.allocations.isPending}}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<td data-test-server-name><LinkTo @route="servers.server" @model={{this.agent.id}} class="is-primary">{{this.agent.name}}</LinkTo></td>
|
<td data-test-server-name><LinkTo @route="servers.server" @model={{this.agent.id}} class="is-primary">{{this.agent.name}}</LinkTo></td>
|
||||||
<td data-test-server-status>{{this.agent.status}}</td>
|
<td data-test-server-status>{{this.agent.status}}</td>
|
||||||
<td data-test-server-is-leader>{{if this.agent.isLeader "True" "False"}}</td>
|
<td data-test-server-is-leader>{{if this.agent.isLeader "True" "False"}}</td>
|
||||||
<td data-test-server-address>{{this.agent.address}}</td>
|
<td data-test-server-address class="is-200px is-truncatable">{{this.agent.address}}</td>
|
||||||
<td data-test-server-port>{{this.agent.serfPort}}</td>
|
<td data-test-server-port>{{this.agent.serfPort}}</td>
|
||||||
<td data-test-server-datacenter>{{this.agent.datacenter}}</td>
|
<td data-test-server-datacenter>{{this.agent.datacenter}}</td>
|
||||||
|
<td data-test-server-version>{{this.agent.version}}</td>
|
||||||
|
|||||||
@@ -16,9 +16,10 @@
|
|||||||
<t.sort-by @prop="name">Name</t.sort-by>
|
<t.sort-by @prop="name">Name</t.sort-by>
|
||||||
<t.sort-by @prop="status">Status</t.sort-by>
|
<t.sort-by @prop="status">Status</t.sort-by>
|
||||||
<t.sort-by @prop="isLeader">Leader</t.sort-by>
|
<t.sort-by @prop="isLeader">Leader</t.sort-by>
|
||||||
<t.sort-by @prop="address">Address</t.sort-by>
|
<t.sort-by @class="is-200px is-truncatable" @prop="address">Address</t.sort-by>
|
||||||
<t.sort-by @prop="serfPort">port</t.sort-by>
|
<t.sort-by @prop="serfPort">port</t.sort-by>
|
||||||
<t.sort-by @prop="datacenter">Datacenter</t.sort-by>
|
<t.sort-by @prop="datacenter">Datacenter</t.sort-by>
|
||||||
|
<t.sort-by @prop="version">Version</t.sort-by>
|
||||||
</t.head>
|
</t.head>
|
||||||
<t.body as |row|>
|
<t.body as |row|>
|
||||||
<ServerAgentRow data-test-server-agent-row @agent={{row.model}} />
|
<ServerAgentRow data-test-server-agent-row @agent={{row.model}} />
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { DATACENTERS } from '../common';
|
|||||||
|
|
||||||
const UUIDS = provide(100, faker.random.uuid.bind(faker.random));
|
const UUIDS = provide(100, faker.random.uuid.bind(faker.random));
|
||||||
const AGENT_STATUSES = ['alive', 'leaving', 'left', 'failed'];
|
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({
|
export default Factory.extend({
|
||||||
id: i => (i / 100 >= 1 ? `${UUIDS[i]}-${i}` : UUIDS[i]),
|
id: i => (i / 100 >= 1 ? `${UUIDS[i]}-${i}` : UUIDS[i]),
|
||||||
@@ -29,6 +30,10 @@ export default Factory.extend({
|
|||||||
Tags: generateTags(serfPort),
|
Tags: generateTags(serfPort),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
version() {
|
||||||
|
return this.member.Tags?.build || '';
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function generateName() {
|
function generateName() {
|
||||||
@@ -44,5 +49,6 @@ function generateTags(serfPort) {
|
|||||||
return {
|
return {
|
||||||
port: rpcPortCandidate === serfPort ? rpcPortCandidate + 1 : rpcPortCandidate,
|
port: rpcPortCandidate === serfPort ? rpcPortCandidate + 1 : rpcPortCandidate,
|
||||||
dc: faker.helpers.randomize(DATACENTERS),
|
dc: faker.helpers.randomize(DATACENTERS),
|
||||||
|
build: faker.helpers.randomize(AGENT_BUILDS),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import moment from 'moment';
|
|||||||
const UUIDS = provide(100, faker.random.uuid.bind(faker.random));
|
const UUIDS = provide(100, faker.random.uuid.bind(faker.random));
|
||||||
const NODE_STATUSES = ['initializing', 'ready', 'down'];
|
const NODE_STATUSES = ['initializing', 'ready', 'down'];
|
||||||
const NODE_CLASSES = provide(7, faker.company.bsBuzz.bind(faker.company));
|
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();
|
const REF_DATE = new Date();
|
||||||
|
|
||||||
export default Factory.extend({
|
export default Factory.extend({
|
||||||
@@ -22,6 +23,7 @@ export default Factory.extend({
|
|||||||
|
|
||||||
createIndex: i => i,
|
createIndex: i => i,
|
||||||
modifyIndex: () => faker.random.number({ min: 10, max: 2000 }),
|
modifyIndex: () => faker.random.number({ min: 10, max: 2000 }),
|
||||||
|
version: () => faker.helpers.randomize(NODE_VERSIONS),
|
||||||
|
|
||||||
httpAddr() {
|
httpAddr() {
|
||||||
return this.name.split('@')[1];
|
return this.name.split('@')[1];
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ module('Acceptance | clients list', function(hooks) {
|
|||||||
);
|
);
|
||||||
assert.equal(nodeRow.address, node.httpAddr);
|
assert.equal(nodeRow.address, node.httpAddr);
|
||||||
assert.equal(nodeRow.datacenter, node.datacenter, 'Datacenter');
|
assert.equal(nodeRow.datacenter, node.datacenter, 'Datacenter');
|
||||||
|
assert.equal(nodeRow.version, node.version, 'Version');
|
||||||
assert.equal(nodeRow.allocations, allocations.length, '# Allocations');
|
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[1].compositeStatus.text, 'initializing');
|
||||||
assert.equal(ClientsList.nodes[2].compositeStatus.text, 'down');
|
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.equal(ClientsList.nodes[4].compositeStatus.text, 'ineligible');
|
||||||
assert.ok(ClientsList.nodes[4].compositeStatus.isWarning, 'expected warning class');
|
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),
|
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', {
|
testFacet('Volumes', {
|
||||||
facet: ClientsList.facets.volume,
|
facet: ClientsList.facets.volume,
|
||||||
paramName: 'volume',
|
paramName: 'volume',
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ module('Acceptance | servers list', function(hooks) {
|
|||||||
assert.equal(agentRow.address, agent.member.Address, 'Address');
|
assert.equal(agentRow.address, agent.member.Address, 'Address');
|
||||||
assert.equal(agentRow.serfPort, agent.member.Port, 'Serf Port');
|
assert.equal(agentRow.serfPort, agent.member.Port, 'Serf Port');
|
||||||
assert.equal(agentRow.datacenter, agent.member.Tags.dc, 'Datacenter');
|
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) {
|
test('each server should link to the server detail page', async function(assert) {
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export default create({
|
|||||||
|
|
||||||
address: text('[data-test-client-address]'),
|
address: text('[data-test-client-address]'),
|
||||||
datacenter: text('[data-test-client-datacenter]'),
|
datacenter: text('[data-test-client-datacenter]'),
|
||||||
|
version: text('[data-test-client-version]'),
|
||||||
allocations: text('[data-test-client-allocations]'),
|
allocations: text('[data-test-client-allocations]'),
|
||||||
|
|
||||||
clickRow: clickable(),
|
clickRow: clickable(),
|
||||||
@@ -75,6 +76,7 @@ export default create({
|
|||||||
class: multiFacet('[data-test-class-facet]'),
|
class: multiFacet('[data-test-class-facet]'),
|
||||||
state: multiFacet('[data-test-state-facet]'),
|
state: multiFacet('[data-test-state-facet]'),
|
||||||
datacenter: multiFacet('[data-test-datacenter-facet]'),
|
datacenter: multiFacet('[data-test-datacenter-facet]'),
|
||||||
|
version: multiFacet('[data-test-version-facet]'),
|
||||||
volume: multiFacet('[data-test-volume-facet]'),
|
volume: multiFacet('[data-test-volume-facet]'),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export default create({
|
|||||||
address: text('[data-test-server-address]'),
|
address: text('[data-test-server-address]'),
|
||||||
serfPort: text('[data-test-server-port]'),
|
serfPort: text('[data-test-server-port]'),
|
||||||
datacenter: text('[data-test-server-datacenter]'),
|
datacenter: text('[data-test-server-datacenter]'),
|
||||||
|
version: text('[data-test-server-version]'),
|
||||||
|
|
||||||
clickRow: clickable(),
|
clickRow: clickable(),
|
||||||
clickName: clickable('[data-test-server-name] a'),
|
clickName: clickable('[data-test-server-name] a'),
|
||||||
|
|||||||
Reference in New Issue
Block a user