diff --git a/.changelog/24723.txt b/.changelog/24723.txt new file mode 100644 index 00000000000..b788768b7c8 --- /dev/null +++ b/.changelog/24723.txt @@ -0,0 +1,3 @@ +```release-note:improvement +ui: add leadership status for servers in other regions +``` diff --git a/ui/app/models/agent.js b/ui/app/models/agent.js index 306e642ad92..fbf9f009857 100644 --- a/ui/app/models/agent.js +++ b/ui/app/models/agent.js @@ -7,10 +7,10 @@ import { inject as service } from '@ember/service'; import { computed } from '@ember/object'; import Model from '@ember-data/model'; import { attr } from '@ember-data/model'; -import classic from 'ember-classic-decorator'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; import formatHost from 'nomad-ui/utils/format-host'; -@classic export default class Agent extends Model { @service system; @@ -29,9 +29,12 @@ export default class Agent extends Model { return formatHost(address, rpcPort); } - @computed('rpcAddr', 'system.leader.rpcAddr') - get isLeader() { - return this.get('system.leader.rpcAddr') === this.rpcAddr; + @tracked isLeader = false; + + @action async checkForLeadership() { + const leaders = await this.system.leaders; + this.isLeader = leaders.includes(this.rpcAddr); + return this.isLeader; } @computed('tags.build') diff --git a/ui/app/routes/clients.js b/ui/app/routes/clients.js index e74f9bdc0e4..afda9aba99e 100644 --- a/ui/app/routes/clients.js +++ b/ui/app/routes/clients.js @@ -13,11 +13,6 @@ import classic from 'ember-classic-decorator'; @classic export default class ClientsRoute extends Route.extend(WithForbiddenState) { @service store; - @service system; - - beforeModel() { - return this.get('system.leader'); - } model() { return RSVP.hash({ diff --git a/ui/app/routes/servers.js b/ui/app/routes/servers.js index 3dcb8d733c8..f11aa28657a 100644 --- a/ui/app/routes/servers.js +++ b/ui/app/routes/servers.js @@ -15,14 +15,16 @@ export default class ServersRoute extends Route.extend(WithForbiddenState) { @service store; @service system; - beforeModel() { - return this.get('system.leader'); + async beforeModel() { + await this.system.leaders; } - model() { + async model() { + const agents = await this.store.findAll('agent'); + await Promise.all(agents.map((agent) => agent.checkForLeadership())); return RSVP.hash({ nodes: this.store.findAll('node'), - agents: this.store.findAll('agent'), + agents, }).catch(notifyForbidden(this)); } } diff --git a/ui/app/services/system.js b/ui/app/services/system.js index f88163b4bb0..864d61e5e9c 100644 --- a/ui/app/services/system.js +++ b/ui/app/services/system.js @@ -12,27 +12,23 @@ import { namespace } from '../adapters/application'; import jsonWithDefault from '../utils/json-with-default'; import classic from 'ember-classic-decorator'; import { task } from 'ember-concurrency'; - @classic export default class SystemService extends Service { @service token; @service store; - @computed('activeRegion') - get leader() { - const token = this.token; - - return PromiseObject.create({ - promise: token - .authorizedRequest(`/${namespace}/status/leader`) - .then((res) => res.json()) - .then((rpcAddr) => ({ rpcAddr })) - .then((leader) => { - // Dirty self so leader can be used as a dependent key - this.notifyPropertyChange('leader.rpcAddr'); - return leader; - }), - }); + /** + * Iterates over all regions and returns a list of leaders' rpcAddrs + */ + @computed('regions.[]') + get leaders() { + return Promise.all( + this.regions.map((region) => { + return this.token + .authorizedRequest(`/${namespace}/status/leader?region=${region}`) + .then((res) => res.json()); + }) + ); } @computed diff --git a/ui/app/styles/core/table.scss b/ui/app/styles/core/table.scss index 549fdd11637..3dbb8d91c7b 100644 --- a/ui/app/styles/core/table.scss +++ b/ui/app/styles/core/table.scss @@ -110,7 +110,8 @@ white-space: nowrap; } - &.node-status-badges { + &.node-status-badges, + &.server-status-badges { .hds-badge__text { white-space: nowrap; } diff --git a/ui/app/templates/components/server-agent-row.hbs b/ui/app/templates/components/server-agent-row.hbs index 302ace102b2..5cf4e892d91 100644 --- a/ui/app/templates/components/server-agent-row.hbs +++ b/ui/app/templates/components/server-agent-row.hbs @@ -4,7 +4,7 @@ ~}} - - - + + {{this.agent.address}} {{this.agent.serfPort}} diff --git a/ui/mirage/config.js b/ui/mirage/config.js index 3d3a9f001b5..d97ef3580ba 100644 --- a/ui/mirage/config.js +++ b/ui/mirage/config.js @@ -13,8 +13,14 @@ import { copy } from 'ember-copy'; import formatHost from 'nomad-ui/utils/format-host'; import faker from 'nomad-ui/mirage/faker'; -export function findLeader(schema) { - const agent = schema.agents.first(); +export function findLeader(schema, region = null) { + let agent; + let agents = schema.agents.all().models; + if (region) { + agent = agents.find((agent) => agent.member?.Tags?.region === region); + } else { + agent = agents[0]; + } return formatHost(agent.member.Address, agent.member.Tags.port); } @@ -741,8 +747,9 @@ export default function () { return logEncode(logFrames, logFrames.length - 1); }); - this.get('/status/leader', function (schema) { - return JSON.stringify(findLeader(schema)); + this.get('/status/leader', function (schema, { queryParams: { region } }) { + let leader = JSON.stringify(findLeader(schema, region)); + return leader; }); this.get('/acl/tokens', function ({ tokens }, req) { diff --git a/ui/mirage/factories/agent.js b/ui/mirage/factories/agent.js index 1686eab1dd8..70d9aa0bba4 100644 --- a/ui/mirage/factories/agent.js +++ b/ui/mirage/factories/agent.js @@ -90,5 +90,6 @@ function generateTags(serfPort) { rpcPortCandidate === serfPort ? rpcPortCandidate + 1 : rpcPortCandidate, dc: faker.helpers.randomize(DATACENTERS), build: faker.helpers.randomize(AGENT_BUILDS), + region: 'global', }; } diff --git a/ui/mirage/scenarios/default.js b/ui/mirage/scenarios/default.js index 6a069657f18..d613d31c431 100644 --- a/ui/mirage/scenarios/default.js +++ b/ui/mirage/scenarios/default.js @@ -92,6 +92,9 @@ function smallCluster(server) { server.create('feature', { name: 'Dynamic Application Sizing' }); server.create('feature', { name: 'Sentinel Policies' }); server.createList('agent', 3, 'withConsulLink', 'withVaultLink'); + if (withRegions) { + server.db.agents[0].member.Tags.region = server.db.regions[0].id; + } server.createList('node-pool', 2); server.createList('node', 5); server.create( diff --git a/ui/tests/acceptance/servers-list-test.js b/ui/tests/acceptance/servers-list-test.js index defdcd033f7..993ae7d995e 100644 --- a/ui/tests/acceptance/servers-list-test.js +++ b/ui/tests/acceptance/servers-list-test.js @@ -35,6 +35,10 @@ module('Acceptance | servers list', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); + hooks.beforeEach(function () { + server.create('region', { id: 'global' }); + }); + test('it passes an accessibility audit', async function (assert) { minimumSetup(); await ServersList.visit(); @@ -51,7 +55,6 @@ module('Acceptance | servers list', function (hooks) { const sortedAgents = server.db.agents.sort(agentSort(leader)).reverse(); await ServersList.visit(); - await percySnapshot(assert); assert.equal( @@ -116,4 +119,31 @@ module('Acceptance | servers list', function (hooks) { await ServersList.error.seekHelp(); assert.equal(currentURL(), '/settings/tokens'); }); + + test('multiple regions should each show leadership values', async function (assert) { + server.createList('node-pool', 1); + server.createList('node', 1); + server.create('region', { id: 'global' }); + server.create('region', { id: 'galactic' }); + server.createList('agent', 3); + server.db.agents[0].member.Tags.region = 'global'; + server.db.agents[1].member.Tags.region = 'galactic'; + server.db.agents[2].member.Tags.region = 'galactic'; + await ServersList.visit(); + assert.equal( + ServersList.servers.objectAt(0).leader, + 'True (galactic)', + 'Leadership is shown for the galactic region' + ); + assert.equal( + ServersList.servers.objectAt(1).leader, + 'True (global)', + 'Leadership is shown for the global region' + ); + assert.equal( + ServersList.servers.objectAt(2).leader, + 'False', + 'Non-leader servers are shown' + ); + }); });