[ui] Show ALL regions' leaders when viewing servers route (#24723)

* Looks up all regions' leaders when viewing servers route

* Tests for multi-region leadership badges and css same-line fix
This commit is contained in:
Phil Renaud
2025-01-07 12:35:04 -05:00
committed by GitHub
parent 1610f18500
commit ab39f198ff
11 changed files with 94 additions and 45 deletions

3
.changelog/24723.txt Normal file
View File

@@ -0,0 +1,3 @@
```release-note:improvement
ui: add leadership status for servers in other regions
```

View File

@@ -7,10 +7,10 @@ import { inject as service } from '@ember/service';
import { computed } from '@ember/object'; import { computed } from '@ember/object';
import Model from '@ember-data/model'; import Model from '@ember-data/model';
import { attr } 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'; import formatHost from 'nomad-ui/utils/format-host';
@classic
export default class Agent extends Model { export default class Agent extends Model {
@service system; @service system;
@@ -29,9 +29,12 @@ export default class Agent extends Model {
return formatHost(address, rpcPort); return formatHost(address, rpcPort);
} }
@computed('rpcAddr', 'system.leader.rpcAddr') @tracked isLeader = false;
get isLeader() {
return this.get('system.leader.rpcAddr') === this.rpcAddr; @action async checkForLeadership() {
const leaders = await this.system.leaders;
this.isLeader = leaders.includes(this.rpcAddr);
return this.isLeader;
} }
@computed('tags.build') @computed('tags.build')

View File

@@ -13,11 +13,6 @@ import classic from 'ember-classic-decorator';
@classic @classic
export default class ClientsRoute extends Route.extend(WithForbiddenState) { export default class ClientsRoute extends Route.extend(WithForbiddenState) {
@service store; @service store;
@service system;
beforeModel() {
return this.get('system.leader');
}
model() { model() {
return RSVP.hash({ return RSVP.hash({

View File

@@ -15,14 +15,16 @@ export default class ServersRoute extends Route.extend(WithForbiddenState) {
@service store; @service store;
@service system; @service system;
beforeModel() { async beforeModel() {
return this.get('system.leader'); 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({ return RSVP.hash({
nodes: this.store.findAll('node'), nodes: this.store.findAll('node'),
agents: this.store.findAll('agent'), agents,
}).catch(notifyForbidden(this)); }).catch(notifyForbidden(this));
} }
} }

View File

@@ -12,27 +12,23 @@ import { namespace } from '../adapters/application';
import jsonWithDefault from '../utils/json-with-default'; import jsonWithDefault from '../utils/json-with-default';
import classic from 'ember-classic-decorator'; import classic from 'ember-classic-decorator';
import { task } from 'ember-concurrency'; import { task } from 'ember-concurrency';
@classic @classic
export default class SystemService extends Service { export default class SystemService extends Service {
@service token; @service token;
@service store; @service store;
@computed('activeRegion') /**
get leader() { * Iterates over all regions and returns a list of leaders' rpcAddrs
const token = this.token; */
@computed('regions.[]')
return PromiseObject.create({ get leaders() {
promise: token return Promise.all(
.authorizedRequest(`/${namespace}/status/leader`) this.regions.map((region) => {
.then((res) => res.json()) return this.token
.then((rpcAddr) => ({ rpcAddr })) .authorizedRequest(`/${namespace}/status/leader?region=${region}`)
.then((leader) => { .then((res) => res.json());
// Dirty self so leader can be used as a dependent key })
this.notifyPropertyChange('leader.rpcAddr'); );
return leader;
}),
});
} }
@computed @computed

View File

@@ -110,7 +110,8 @@
white-space: nowrap; white-space: nowrap;
} }
&.node-status-badges { &.node-status-badges,
&.server-status-badges {
.hds-badge__text { .hds-badge__text {
white-space: nowrap; white-space: nowrap;
} }

View File

@@ -4,7 +4,7 @@
~}} ~}}
<td data-test-server-name <td data-test-server-name
{{keyboard-shortcut {{keyboard-shortcut
enumerated=true enumerated=true
action=(action this.goToAgent) action=(action this.goToAgent)
}} }}
@@ -16,14 +16,22 @@
@size="large" @size="large"
/> />
</span></td> </span></td>
<td data-test-server-is-leader> <td data-test-server-is-leader class="server-status-badges">
<Hds::Badge
<Hds::Badge @text={{if
@text={{if this.agent.isLeader "True" "False"}} this.agent.isLeader
@icon={{if this.agent.isLeader "check-circle" ""}} (if
@color={{if this.agent.isLeader "success" "neutral"}} this.agent.system.shouldShowRegions
@size="large" (concat "True" " (" this.agent.region ")")
/> "True"
)
"False"
}}
@icon={{if this.agent.isLeader "check-circle" ""}}
@color={{if this.agent.isLeader "success" "neutral"}}
@size="large"
class="no-wrap"
/>
</td> </td>
<td data-test-server-address class="is-200px is-truncatable">{{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>

View File

@@ -13,8 +13,14 @@ import { copy } from 'ember-copy';
import formatHost from 'nomad-ui/utils/format-host'; import formatHost from 'nomad-ui/utils/format-host';
import faker from 'nomad-ui/mirage/faker'; import faker from 'nomad-ui/mirage/faker';
export function findLeader(schema) { export function findLeader(schema, region = null) {
const agent = schema.agents.first(); 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); return formatHost(agent.member.Address, agent.member.Tags.port);
} }
@@ -741,8 +747,9 @@ export default function () {
return logEncode(logFrames, logFrames.length - 1); return logEncode(logFrames, logFrames.length - 1);
}); });
this.get('/status/leader', function (schema) { this.get('/status/leader', function (schema, { queryParams: { region } }) {
return JSON.stringify(findLeader(schema)); let leader = JSON.stringify(findLeader(schema, region));
return leader;
}); });
this.get('/acl/tokens', function ({ tokens }, req) { this.get('/acl/tokens', function ({ tokens }, req) {

View File

@@ -90,5 +90,6 @@ function generateTags(serfPort) {
rpcPortCandidate === serfPort ? rpcPortCandidate + 1 : rpcPortCandidate, rpcPortCandidate === serfPort ? rpcPortCandidate + 1 : rpcPortCandidate,
dc: faker.helpers.randomize(DATACENTERS), dc: faker.helpers.randomize(DATACENTERS),
build: faker.helpers.randomize(AGENT_BUILDS), build: faker.helpers.randomize(AGENT_BUILDS),
region: 'global',
}; };
} }

View File

@@ -92,6 +92,9 @@ function smallCluster(server) {
server.create('feature', { name: 'Dynamic Application Sizing' }); server.create('feature', { name: 'Dynamic Application Sizing' });
server.create('feature', { name: 'Sentinel Policies' }); server.create('feature', { name: 'Sentinel Policies' });
server.createList('agent', 3, 'withConsulLink', 'withVaultLink'); 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-pool', 2);
server.createList('node', 5); server.createList('node', 5);
server.create( server.create(

View File

@@ -35,6 +35,10 @@ module('Acceptance | servers list', function (hooks) {
setupApplicationTest(hooks); setupApplicationTest(hooks);
setupMirage(hooks); setupMirage(hooks);
hooks.beforeEach(function () {
server.create('region', { id: 'global' });
});
test('it passes an accessibility audit', async function (assert) { test('it passes an accessibility audit', async function (assert) {
minimumSetup(); minimumSetup();
await ServersList.visit(); await ServersList.visit();
@@ -51,7 +55,6 @@ module('Acceptance | servers list', function (hooks) {
const sortedAgents = server.db.agents.sort(agentSort(leader)).reverse(); const sortedAgents = server.db.agents.sort(agentSort(leader)).reverse();
await ServersList.visit(); await ServersList.visit();
await percySnapshot(assert); await percySnapshot(assert);
assert.equal( assert.equal(
@@ -116,4 +119,31 @@ module('Acceptance | servers list', function (hooks) {
await ServersList.error.seekHelp(); await ServersList.error.seekHelp();
assert.equal(currentURL(), '/settings/tokens'); 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'
);
});
}); });