Label for the Web UI (#16006)

* Demoable state

* Demo mirage color

* Label as a block with foreground and background colours

* Test mock updates

* Go test updated

* Documentation update for label support
This commit is contained in:
Phil Renaud 2023-02-02 16:29:04 -05:00 committed by GitHub
parent 19a2c065f4
commit d3c351d2d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 135 additions and 18 deletions

3
.changelog/16006.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
ui: Added a ui.label block to agent config, letting operators set a visual label and color for their Nomad instance
```

View File

@ -13,6 +13,9 @@ type UIConfig struct {
// Vault configures deep links for Vault UI
Vault *VaultUIConfig `hcl:"vault"`
// Label configures UI label styles
Label *LabelUIConfig `hcl:"label"`
}
// ConsulUIConfig configures deep links to this cluster's Consul
@ -30,6 +33,13 @@ type VaultUIConfig struct {
BaseUIURL string `hcl:"ui_url"`
}
// Label configures UI label styles
type LabelUIConfig struct {
Text string `hcl:"text"`
BackgroundColor string `hcl:"background_color"`
TextColor string `hcl:"text_color"`
}
// DefaultUIConfig returns the canonical defaults for the Nomad
// `ui` configuration.
func DefaultUIConfig() *UIConfig {
@ -37,6 +47,7 @@ func DefaultUIConfig() *UIConfig {
Enabled: true,
Consul: &ConsulUIConfig{},
Vault: &VaultUIConfig{},
Label: &LabelUIConfig{},
}
}
@ -69,6 +80,7 @@ func (old *UIConfig) Merge(other *UIConfig) *UIConfig {
result.Enabled = other.Enabled
result.Consul = result.Consul.Merge(other.Consul)
result.Vault = result.Vault.Merge(other.Vault)
result.Label = result.Label.Merge(other.Label)
return result
}
@ -128,3 +140,37 @@ func (old *VaultUIConfig) Merge(other *VaultUIConfig) *VaultUIConfig {
}
return result
}
// Copy returns a copy of this Label UI config.
func (old *LabelUIConfig) Copy() *LabelUIConfig {
if old == nil {
return nil
}
nc := new(LabelUIConfig)
*nc = *old
return nc
}
// Merge returns a new Label UI configuration by merging another Label UI
// configuration into this one
func (old *LabelUIConfig) Merge(other *LabelUIConfig) *LabelUIConfig {
result := old.Copy()
if result == nil {
result = &LabelUIConfig{}
}
if other == nil {
return result
}
if other.Text != "" {
result.Text = other.Text
}
if other.BackgroundColor != "" {
result.BackgroundColor = other.BackgroundColor
}
if other.TextColor != "" {
result.TextColor = other.TextColor
}
return result
}

View File

@ -18,6 +18,11 @@ func TestUIConfig_Merge(t *testing.T) {
Vault: &VaultUIConfig{
BaseUIURL: "http://vault.example.com:8200",
},
Label: &LabelUIConfig{
Text: "Example Cluster",
BackgroundColor: "blue",
TextColor: "#fff",
},
}
testCases := []struct {
@ -64,6 +69,7 @@ func TestUIConfig_Merge(t *testing.T) {
BaseUIURL: "http://consul-other.example.com:8500",
},
Vault: &VaultUIConfig{},
Label: &LabelUIConfig{},
},
},
}

View File

@ -2,6 +2,7 @@ import Component from '@ember/component';
import classic from 'ember-classic-decorator';
import { inject as service } from '@ember/service';
import { attributeBindings } from '@ember-decorators/component';
import { htmlSafe } from '@ember/template';
@classic
@attributeBindings('data-test-global-header')
@ -22,4 +23,15 @@ export default class GlobalHeader extends Component {
this.system.agent?.get('config.ACL.Enabled') === true
);
}
get labelStyles() {
return htmlSafe(
`
color: ${this.system.agent.get('config')?.UI?.Label?.TextColor};
background-color: ${
this.system.agent.get('config')?.UI?.Label?.BackgroundColor
};
`
);
}
}

View File

@ -174,4 +174,13 @@
border-top-color: white;
}
}
.custom-label {
border-radius: 1rem;
padding: 0.25rem 1rem;
background: black;
color: white;
display: grid;
align-self: center;
}
}

View File

@ -1,5 +1,6 @@
{{page-title
(if this.system.shouldShowRegions (concat this.system.activeRegion " - "))
(if this.system.agent.config.UI.Label.Text (concat this.system.agent.config.UI.Label.Text " - "))
"Nomad"
separator=" - "
}}

View File

@ -12,6 +12,11 @@
<LinkTo @route="jobs" class="navbar-item is-logo" aria-label="Home">
<NomadLogo />
</LinkTo>
{{#if this.system.agent.config.UI.Label}}
<div class="custom-label" style={{this.labelStyles}}>
{{this.system.agent.config.UI.Label.Text}}
</div>
{{/if}}
</div>
{{#if this.system.fuzzySearchEnabled}}
{{! template-lint-disable simple-unless }}

View File

@ -5,19 +5,28 @@ 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)];
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]),
id: (i) => (i / 100 >= 1 ? `${UUIDS[i]}-${i}` : UUIDS[i]),
name: () => generateName(),
config: {
UI: {
Enabled: true,
Label: {
TextColor: 'white',
BackgroundColor: 'hotpink',
Text: 'Mirage',
},
},
ACL: {
Enabled: true
Enabled: true,
},
Version: {
Version: '1.1.0',
@ -59,7 +68,9 @@ export default Factory.extend({
});
function generateName() {
return `nomad@${faker.random.boolean() ? faker.internet.ip() : faker.internet.ipv6()}`;
return `nomad@${
faker.random.boolean() ? faker.internet.ip() : faker.internet.ipv6()
}`;
}
function generateAddress(name) {
@ -69,7 +80,8 @@ function generateAddress(name) {
function generateTags(serfPort) {
const rpcPortCandidate = faker.random.number({ min: 4000, max: 4999 });
return {
port: rpcPortCandidate === serfPort ? rpcPortCandidate + 1 : rpcPortCandidate,
port:
rpcPortCandidate === serfPort ? rpcPortCandidate + 1 : rpcPortCandidate,
dc: faker.helpers.randomize(DATACENTERS),
build: faker.helpers.randomize(AGENT_BUILDS),
};

View File

@ -78,7 +78,10 @@ module('Acceptance | allocation detail', function (hooks) {
);
assert.ok(Allocation.execButton.isPresent);
assert.equal(document.title, `Allocation ${allocation.name} - Nomad`);
assert.equal(
document.title,
`Allocation ${allocation.name} - Mirage - Nomad`
);
await Allocation.details.visitJob();
assert.equal(

View File

@ -87,7 +87,7 @@ export default function browseFilesystem({
`${pathWithLeadingSlash} - ${getTitleComponent({
allocation: this.allocation,
task: this.task,
})} - Nomad`
})} - Mirage - Nomad`
);
assert.equal(
FS.breadcrumbsText,

View File

@ -62,7 +62,7 @@ module('Acceptance | client detail', function (hooks) {
test('/clients/:id should have a breadcrumb trail linking back to clients', async function (assert) {
await ClientDetail.visit({ id: node.id });
assert.equal(document.title, `Client ${node.name} - Nomad`);
assert.equal(document.title, `Client ${node.name} - Mirage - Nomad`);
assert.equal(
Layout.breadcrumbFor('clients.index').text,

View File

@ -51,7 +51,7 @@ module('Acceptance | clients list', function (hooks) {
);
});
assert.equal(document.title, 'Clients - Nomad');
assert.equal(document.title, 'Clients - Mirage - Nomad');
});
test('each client record should show high-level info of the client', async function (assert) {

View File

@ -67,7 +67,7 @@ module('Acceptance | exec', function (hooks) {
region: 'region-2',
});
assert.equal(document.title, 'Exec - region-2 - Nomad');
assert.equal(document.title, 'Exec - region-2 - Mirage - Nomad');
assert.equal(Exec.header.region.text, this.job.region);
assert.equal(Exec.header.namespace.text, this.job.namespace);

View File

@ -35,7 +35,7 @@ module('Acceptance | regions (only one)', function (hooks) {
await JobsList.visit();
assert.notOk(Layout.navbar.regionSwitcher.isPresent, 'No region switcher');
assert.equal(document.title, 'Jobs - Nomad');
assert.equal(document.title, 'Jobs - Mirage - Nomad');
});
test('when the only region is not named "global", the region switcher still is not shown', async function (assert) {
@ -100,7 +100,7 @@ module('Acceptance | regions (many)', function (hooks) {
Layout.navbar.regionSwitcher.isPresent,
'Region switcher is shown'
);
assert.equal(document.title, 'Jobs - global - Nomad');
assert.equal(document.title, 'Jobs - global - Mirage - Nomad');
});
test('when on the default region, pages do not include the region query param', async function (assert) {

View File

@ -25,7 +25,7 @@ module('Acceptance | server detail', function (hooks) {
test('visiting /servers/:server_name', async function (assert) {
assert.equal(currentURL(), `/servers/${encodeURIComponent(agent.name)}`);
assert.equal(document.title, `Server ${agent.name} - Nomad`);
assert.equal(document.title, `Server ${agent.name} - Mirage - Nomad`);
});
test('when the server is the leader, the title shows a leader badge', async function (assert) {

View File

@ -61,7 +61,7 @@ module('Acceptance | servers list', function (hooks) {
);
});
assert.equal(document.title, 'Servers - Nomad');
assert.equal(document.title, 'Servers - Mirage - Nomad');
});
test('each server should show high-level info of the server', async function (assert) {

View File

@ -65,7 +65,7 @@ module('Acceptance | task detail', function (hooks) {
assert.equal(Task.lifecycle, lifecycleName);
assert.equal(document.title, `Task ${task.name} - Nomad`);
assert.equal(document.title, `Task ${task.name} - Mirage - Nomad`);
});
test('breadcrumbs match jobs / job / task group / allocation / task', async function (assert) {

View File

@ -123,7 +123,7 @@ module('Acceptance | task group detail', function (hooks) {
assert.equal(
document.title,
`Task group ${taskGroup.name} - Job ${job.name} - Nomad`
`Task group ${taskGroup.name} - Job ${job.name} - Mirage - Nomad`
);
});

View File

@ -46,7 +46,7 @@ module('Acceptance | task logs', function (hooks) {
'No redirect'
);
assert.ok(TaskLogs.hasTaskLog, 'Task log component found');
assert.equal(document.title, `Task ${task.name} logs - Nomad`);
assert.equal(document.title, `Task ${task.name} logs - Mirage - Nomad`);
});
test('the stdout log immediately starts streaming', async function (assert) {

View File

@ -51,7 +51,7 @@ module('Acceptance | tokens', function (hooks) {
null,
'No token secret set'
);
assert.equal(document.title, 'Authorization - Nomad');
assert.equal(document.title, 'Authorization - Mirage - Nomad');
await Tokens.secret(secretId).submit();
assert.equal(

View File

@ -23,6 +23,12 @@ ui {
vault {
ui_url = "https://vault.example.com:8200/ui"
}
label {
text = "Staging Cluster"
background_color = "yellow"
text_color = "#000000"
}
}
```
@ -39,6 +45,9 @@ and the configuration is individual to each agent.
- `vault` <code>([Vault]: nil)</code> - Configures integrations
between the Nomad web UI and the Vault web UI.
- `label` <code>([Label]: nil)</code> - Configures a user-defined
label to display in the Nomad Web UI header.
## `consul` Parameters
- `ui_url` `(string: "")` - Specifies the full base URL to a Consul
@ -61,9 +70,20 @@ and the configuration is individual to each agent.
`ui.vault.ui_url` is the URL you'll visit in your browser. If
this field is omitted, this integration will be disabled.
## `label` Parameters
- `text` `(string: "")` - Specifies the text of the label that will be
displayed in the header of the Web UI.
- `background_color` `(string: "")` - The background color of the label to
be displayed. The Web UI will default to a black background.
- `text_color` `(string: "")` - The text color of the label to be displayed.
The Web UI will default to white text.
[web UI]: /nomad/tutorials/web-ui
[Consul]: /nomad/docs/configuration/ui#consul-parameters
[Vault]: /nomad/docs/configuration/ui#vault-parameters
[Label]: /nomad/docs/configuration/ui#label-parameters
[`consul.address`]: /nomad/docs/configuration/consul#address
[`vault.address`]: /nomad/docs/configuration/vault#address