diff --git a/.changelog/16006.txt b/.changelog/16006.txt
new file mode 100644
index 00000000000..1b1c1142535
--- /dev/null
+++ b/.changelog/16006.txt
@@ -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
+```
diff --git a/nomad/structs/config/ui.go b/nomad/structs/config/ui.go
index 1626544fb4a..4e8757ef51a 100644
--- a/nomad/structs/config/ui.go
+++ b/nomad/structs/config/ui.go
@@ -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
+}
diff --git a/nomad/structs/config/ui_test.go b/nomad/structs/config/ui_test.go
index d310403b530..337ab3ca1c4 100644
--- a/nomad/structs/config/ui_test.go
+++ b/nomad/structs/config/ui_test.go
@@ -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{},
},
},
}
diff --git a/ui/app/components/global-header.js b/ui/app/components/global-header.js
index 613a757ec37..58709074b63 100644
--- a/ui/app/components/global-header.js
+++ b/ui/app/components/global-header.js
@@ -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
+ };
+ `
+ );
+ }
}
diff --git a/ui/app/styles/core/navbar.scss b/ui/app/styles/core/navbar.scss
index 52f8c0632d4..62bf9b41015 100644
--- a/ui/app/styles/core/navbar.scss
+++ b/ui/app/styles/core/navbar.scss
@@ -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;
+ }
}
diff --git a/ui/app/templates/application.hbs b/ui/app/templates/application.hbs
index edf8c1c3f90..fdd437bac7c 100644
--- a/ui/app/templates/application.hbs
+++ b/ui/app/templates/application.hbs
@@ -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=" - "
}}
diff --git a/ui/app/templates/components/global-header.hbs b/ui/app/templates/components/global-header.hbs
index 9624c274ea2..6ba50748bd4 100644
--- a/ui/app/templates/components/global-header.hbs
+++ b/ui/app/templates/components/global-header.hbs
@@ -12,6 +12,11 @@
+ {{#if this.system.agent.config.UI.Label}}
+
+ {{this.system.agent.config.UI.Label.Text}}
+
+ {{/if}}
{{#if this.system.fuzzySearchEnabled}}
{{! template-lint-disable simple-unless }}
diff --git a/ui/mirage/factories/agent.js b/ui/mirage/factories/agent.js
index e2840f428d3..1241d23610f 100644
--- a/ui/mirage/factories/agent.js
+++ b/ui/mirage/factories/agent.js
@@ -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),
};
diff --git a/ui/tests/acceptance/allocation-detail-test.js b/ui/tests/acceptance/allocation-detail-test.js
index 8d3ee103628..3503e45d0c0 100644
--- a/ui/tests/acceptance/allocation-detail-test.js
+++ b/ui/tests/acceptance/allocation-detail-test.js
@@ -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(
diff --git a/ui/tests/acceptance/behaviors/fs.js b/ui/tests/acceptance/behaviors/fs.js
index c6a1f4a57d7..28c3803b7b1 100644
--- a/ui/tests/acceptance/behaviors/fs.js
+++ b/ui/tests/acceptance/behaviors/fs.js
@@ -87,7 +87,7 @@ export default function browseFilesystem({
`${pathWithLeadingSlash} - ${getTitleComponent({
allocation: this.allocation,
task: this.task,
- })} - Nomad`
+ })} - Mirage - Nomad`
);
assert.equal(
FS.breadcrumbsText,
diff --git a/ui/tests/acceptance/client-detail-test.js b/ui/tests/acceptance/client-detail-test.js
index 3823f55c050..f9e89a9d900 100644
--- a/ui/tests/acceptance/client-detail-test.js
+++ b/ui/tests/acceptance/client-detail-test.js
@@ -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,
diff --git a/ui/tests/acceptance/clients-list-test.js b/ui/tests/acceptance/clients-list-test.js
index 2a0c31544a4..8a51d6db4d6 100644
--- a/ui/tests/acceptance/clients-list-test.js
+++ b/ui/tests/acceptance/clients-list-test.js
@@ -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) {
diff --git a/ui/tests/acceptance/exec-test.js b/ui/tests/acceptance/exec-test.js
index a16ae80a764..67dc9dfd64e 100644
--- a/ui/tests/acceptance/exec-test.js
+++ b/ui/tests/acceptance/exec-test.js
@@ -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);
diff --git a/ui/tests/acceptance/regions-test.js b/ui/tests/acceptance/regions-test.js
index d81af2c0851..d1ca54f912c 100644
--- a/ui/tests/acceptance/regions-test.js
+++ b/ui/tests/acceptance/regions-test.js
@@ -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) {
diff --git a/ui/tests/acceptance/server-detail-test.js b/ui/tests/acceptance/server-detail-test.js
index ef8b761f5ff..2d6704c2d5d 100644
--- a/ui/tests/acceptance/server-detail-test.js
+++ b/ui/tests/acceptance/server-detail-test.js
@@ -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) {
diff --git a/ui/tests/acceptance/servers-list-test.js b/ui/tests/acceptance/servers-list-test.js
index 36800db0f6e..e7a0f292ed1 100644
--- a/ui/tests/acceptance/servers-list-test.js
+++ b/ui/tests/acceptance/servers-list-test.js
@@ -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) {
diff --git a/ui/tests/acceptance/task-detail-test.js b/ui/tests/acceptance/task-detail-test.js
index 02a06e58bb5..6e4a22d09ba 100644
--- a/ui/tests/acceptance/task-detail-test.js
+++ b/ui/tests/acceptance/task-detail-test.js
@@ -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) {
diff --git a/ui/tests/acceptance/task-group-detail-test.js b/ui/tests/acceptance/task-group-detail-test.js
index b984a539f5e..074a2fdd77e 100644
--- a/ui/tests/acceptance/task-group-detail-test.js
+++ b/ui/tests/acceptance/task-group-detail-test.js
@@ -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`
);
});
diff --git a/ui/tests/acceptance/task-logs-test.js b/ui/tests/acceptance/task-logs-test.js
index 5d12a92e732..41165108ed8 100644
--- a/ui/tests/acceptance/task-logs-test.js
+++ b/ui/tests/acceptance/task-logs-test.js
@@ -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) {
diff --git a/ui/tests/acceptance/token-test.js b/ui/tests/acceptance/token-test.js
index c7c0cfbc2fe..6c6d4ff3aa4 100644
--- a/ui/tests/acceptance/token-test.js
+++ b/ui/tests/acceptance/token-test.js
@@ -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(
diff --git a/website/content/docs/configuration/ui.mdx b/website/content/docs/configuration/ui.mdx
index 9525aab1164..54839f8f092 100644
--- a/website/content/docs/configuration/ui.mdx
+++ b/website/content/docs/configuration/ui.mdx
@@ -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` ([Vault]: nil)
- Configures integrations
between the Nomad web UI and the Vault web UI.
+- `label` ([Label]: nil)
- 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