From d71fc9565c1110785ea94f5a55833e4632be523e Mon Sep 17 00:00:00 2001 From: Phil Renaud Date: Thu, 2 Feb 2023 16:29:04 -0500 Subject: [PATCH] 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 --- .changelog/16006.txt | 3 ++ nomad/structs/config/ui.go | 46 +++++++++++++++++++ nomad/structs/config/ui_test.go | 6 +++ ui/app/components/global-header.js | 12 +++++ ui/app/styles/core/navbar.scss | 9 ++++ ui/app/templates/application.hbs | 1 + ui/app/templates/components/global-header.hbs | 5 ++ ui/mirage/factories/agent.js | 22 +++++++-- ui/tests/acceptance/allocation-detail-test.js | 5 +- ui/tests/acceptance/behaviors/fs.js | 2 +- ui/tests/acceptance/client-detail-test.js | 2 +- ui/tests/acceptance/clients-list-test.js | 2 +- ui/tests/acceptance/exec-test.js | 2 +- ui/tests/acceptance/regions-test.js | 4 +- ui/tests/acceptance/server-detail-test.js | 2 +- ui/tests/acceptance/servers-list-test.js | 2 +- ui/tests/acceptance/task-detail-test.js | 2 +- ui/tests/acceptance/task-group-detail-test.js | 2 +- ui/tests/acceptance/task-logs-test.js | 2 +- ui/tests/acceptance/token-test.js | 2 +- website/content/docs/configuration/ui.mdx | 20 ++++++++ 21 files changed, 135 insertions(+), 18 deletions(-) create mode 100644 .changelog/16006.txt 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