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:
parent
19a2c065f4
commit
d3c351d2d2
|
@ -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
|
||||
```
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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
|
||||
};
|
||||
`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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=" - "
|
||||
}}
|
||||
|
|
|
@ -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 }}
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -87,7 +87,7 @@ export default function browseFilesystem({
|
|||
`${pathWithLeadingSlash} - ${getTitleComponent({
|
||||
allocation: this.allocation,
|
||||
task: this.task,
|
||||
})} - Nomad`
|
||||
})} - Mirage - Nomad`
|
||||
);
|
||||
assert.equal(
|
||||
FS.breadcrumbsText,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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`
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue