diff --git a/ui/packages/consul-ui/app/abilities/raft.js b/ui/packages/consul-ui/app/abilities/raft.js deleted file mode 100644 index 169619ad84..0000000000 --- a/ui/packages/consul-ui/app/abilities/raft.js +++ /dev/null @@ -1,6 +0,0 @@ -import BaseAbility from './base'; - -export default class RaftAbility extends BaseAbility { - resource = 'operator'; - segmented = false; -} diff --git a/ui/packages/consul-ui/app/abilities/zone.js b/ui/packages/consul-ui/app/abilities/zone.js new file mode 100644 index 0000000000..a976bd9e6d --- /dev/null +++ b/ui/packages/consul-ui/app/abilities/zone.js @@ -0,0 +1,10 @@ +import BaseAbility from './base'; +import { inject as service } from '@ember/service'; + +export default class ZoneAbility extends BaseAbility { + @service('env') env; + + get canRead() { + return this.env.var('CONSUL_NSPACES_ENABLED'); + } +} diff --git a/ui/packages/consul-ui/app/components/consul/server/card/layout.scss b/ui/packages/consul-ui/app/components/consul/server/card/layout.scss index 8ca116a8fe..a1c679bd4b 100644 --- a/ui/packages/consul-ui/app/components/consul/server/card/layout.scss +++ b/ui/packages/consul-ui/app/components/consul/server/card/layout.scss @@ -16,7 +16,7 @@ margin-bottom: calc(var(--padding-y) / 2); } %consul-server-card.voting-status-leader dd { - margin-left: calc(var(--tile-size) + var(--padding-x)); + margin-left: calc(var(--tile-size) + 1rem); /* 16px */ } diff --git a/ui/packages/consul-ui/app/components/consul/server/list/index.hbs b/ui/packages/consul-ui/app/components/consul/server/list/index.hbs index 4f9d1b50a2..2144fdd9f1 100644 --- a/ui/packages/consul-ui/app/components/consul/server/list/index.hbs +++ b/ui/packages/consul-ui/app/components/consul/server/list/index.hbs @@ -7,9 +7,11 @@ diff --git a/ui/packages/consul-ui/app/components/tile/index.scss b/ui/packages/consul-ui/app/components/tile/index.scss index 7754657eb7..55ceb7b400 100644 --- a/ui/packages/consul-ui/app/components/tile/index.scss +++ b/ui/packages/consul-ui/app/components/tile/index.scss @@ -30,7 +30,7 @@ border-color: rgb(var(--tone-gray-999) / 10%); } %with-leader-tile::after { - --icon-name: icon-star-circle; + --icon-name: icon-star-fill; --icon-size: icon-700; color: rgb(var(--strawberry-500)); } diff --git a/ui/packages/consul-ui/app/models/dc.js b/ui/packages/consul-ui/app/models/dc.js index da0e06551b..48250722b6 100644 --- a/ui/packages/consul-ui/app/models/dc.js +++ b/ui/packages/consul-ui/app/models/dc.js @@ -14,6 +14,9 @@ export default class Datacenter extends Model { @attr('string') Leader; @attr() Voters; // [] @attr() Servers; // [] the API uses {} but we reshape that on the frontend + @attr() RedundancyZones; + @attr() Default; // added by the frontend, {Servers: []} any server that isn't in a zone + @attr() ReadReplicas; // @attr('boolean') Local; @attr('boolean') Primary; diff --git a/ui/packages/consul-ui/app/services/repository/dc.js b/ui/packages/consul-ui/app/services/repository/dc.js index 517c932ba6..f47dfc683b 100644 --- a/ui/packages/consul-ui/app/services/repository/dc.js +++ b/ui/packages/consul-ui/app/services/repository/dc.js @@ -108,21 +108,56 @@ export default class DcService extends RepositoryService { GET /v1/operator/autopilot/state?${{ dc }} X-Request-ID: ${uri} `)( - (headers, body, cache) => ({ - meta: { - version: 2, - uri: uri, - interval: 30 * SECONDS - }, - body: cache( - { - ...body, - // turn servers into an array instead of a map/object - Servers: Object.values(body.Servers) + (headers, body, cache) => { + // turn servers into an array instead of a map/object + const servers = Object.values(body.Servers); + const grouped = []; + return { + meta: { + version: 2, + uri: uri, }, - uri => uri`${MODEL_NAME}:///${''}/${''}/${dc}/datacenter` - ) - }) + body: cache( + { + ...body, + // all servers + Servers: servers, + RedundancyZones: Object.entries(body.RedundancyZones || {}).map(([key, value]) => { + const zone = { + ...value, + Name: key, + Healthy: true, + // convert the string[] to Server[] + Servers: value.Servers.reduce((prev, item) => { + const server = body.Servers[item]; + // TODO: It is not currently clear whether we should be + // taking ReadReplicas out of the RedundancyZones when we + // encounter one in a Zone once this is cleared up either + // way we can either remove this comment or make any + // necessary amends here + if(!server.ReadReplica) { + // keep a record of things + grouped.push(server.ID); + prev.push(server); + } + return prev; + }, []), + } + return zone; + }), + ReadReplicas: (body.ReadReplicas || []).map(item => { + // keep a record of things + grouped.push(item); + return body.Servers[item]; + }), + Default: { + Servers: servers.filter(item => !grouped.includes(item.ID)) + } + }, + uri => uri`${MODEL_NAME}:///${''}/${''}/${dc}/datacenter` + ) + } + } ); } diff --git a/ui/packages/consul-ui/app/styles/base/decoration/base-placeholders.scss b/ui/packages/consul-ui/app/styles/base/decoration/base-placeholders.scss index 095755d002..8e64f496bd 100644 --- a/ui/packages/consul-ui/app/styles/base/decoration/base-placeholders.scss +++ b/ui/packages/consul-ui/app/styles/base/decoration/base-placeholders.scss @@ -10,13 +10,13 @@ } %visually-unhidden, %unvisually-hidden { - position: static; - clip: unset; - overflow: visible; - width: auto; - height: auto; - margin: 0; - padding: 0; + position: static !important; + clip: unset !important; + overflow: visible !important; + width: auto !important; + height: auto !important; + margin: 0 !important; + padding: 0 !important; } %visually-hidden-text { text-indent: -9000px; diff --git a/ui/packages/consul-ui/app/styles/base/decoration/base-variables.scss b/ui/packages/consul-ui/app/styles/base/decoration/base-variables.scss index e97f8df127..d2ee83711c 100644 --- a/ui/packages/consul-ui/app/styles/base/decoration/base-variables.scss +++ b/ui/packages/consul-ui/app/styles/base/decoration/base-variables.scss @@ -15,6 +15,7 @@ --decor-border-400: 4px solid; /* box-shadowing*/ + --decor-elevation-000: none; --decor-elevation-100: 0 3px 2px rgb(var(--black) / 6%); --decor-elevation-200: 0 2px 4px rgb(var(--black) / 10%); --decor-elevation-300: 0 5px 1px -2px rgb(var(--black) / 12%); diff --git a/ui/packages/consul-ui/app/styles/routes.scss b/ui/packages/consul-ui/app/styles/routes.scss index 34f12616b2..238790812a 100644 --- a/ui/packages/consul-ui/app/styles/routes.scss +++ b/ui/packages/consul-ui/app/styles/routes.scss @@ -3,3 +3,4 @@ @import 'routes/dc/kv/index'; @import 'routes/dc/acls/index'; @import 'routes/dc/intentions/index'; +@import 'routes/dc/overview/serverstatus'; diff --git a/ui/packages/consul-ui/app/styles/routes/dc/overview/serverstatus.scss b/ui/packages/consul-ui/app/styles/routes/dc/overview/serverstatus.scss new file mode 100644 index 0000000000..0e035f8a28 --- /dev/null +++ b/ui/packages/consul-ui/app/styles/routes/dc/overview/serverstatus.scss @@ -0,0 +1,135 @@ +section[data-route='dc.show.serverstatus'] { + @extend %serverstatus-route; +} +%serverstatus-route .server-failure-tolerance { + @extend %server-failure-tolerance; +} +%serverstatus-route .redundancy-zones { + @extend %redundancy-zones; +} +%redundancy-zones section { + @extend %redundancy-zone; +} + +/**/ + +%serverstatus-route h2, +%serverstatus-route h3 { + @extend %h200; +} + +%server-failure-tolerance { + @extend %panel; + box-shadow: var(--decor-elevation-000); + padding: var(--padding-y) var(--padding-x); + width: 770px; + display: flex; + flex-wrap: wrap; +} +%server-failure-tolerance > header { + width: 100%; + padding-bottom: 0.500rem; /* 8px */ + margin-bottom: 1rem; /* 16px */ + border-bottom: var(--decor-border-100); + border-color: rgb(var(--tone-border)); +} +%server-failure-tolerance header em { + @extend %pill-200; + font-size: 0.812rem; /* 13px */ + background-color: rgb(var(--tone-gray-200)); + + text-transform: uppercase; + font-style: normal; + +} +%server-failure-tolerance > section { + width: 50%; +} +%server-failure-tolerance > section, +%server-failure-tolerance dl { + display: flex; + flex-direction: column; +} +%server-failure-tolerance dl { + flex-grow: 1; + justify-content: space-between; +} +%server-failure-tolerance dd { + display: flex; + align-items: center; +} +%server-failure-tolerance dl.warning dd::before { + --icon-name: icon-alert-circle; + --icon-resolution: .5; + --icon-size: icon-800; + --icon-color: rgb(var(--tone-orange-400)); + content: ''; + margin-right: 0.500rem; /* 8px */ +} +%server-failure-tolerance section:first-of-type dl { + padding-right: 1.500rem; /* 24px */ +} +%server-failure-tolerance dt { + @extend %p2; + color: rgb(var(--tone-gray-700)); +} +%server-failure-tolerance dd { + font-size: var(--typo-size-250); + color: rgb(var(--tone-gray-999)); +} +%server-failure-tolerance header span::before { + --icon-name: icon-info; + --icon-size: icon-300; + --icon-color: rgb(var(--tone-gray-500)); + vertical-align: unset; + content: ''; +} + +%serverstatus-route section:not([class*='-tolerance']) h2 { + margin-top: 1.5rem; /* 24px */ + margin-bottom: 1.5rem; /* 24px */ +} +%serverstatus-route section:not([class*='-tolerance']) header { + margin-top: 18px; + margin-bottom: 18px; +} + + +%redundancy-zones h3 { + @extend %h300; +} +%redundancy-zone header { + display: flow-root; +} +%redundancy-zone header h3 { + float: left; + margin-right: 0.5rem; /* 8px */ +} + +%redundancy-zone header dl { + @extend %horizontal-kv-list; + @extend %pill-500; +} +%redundancy-zone header dt { + @extend %visually-unhidden; +} +%redundancy-zone header dl:not(.warning) { + background-color: rgb(var(--tone-gray-100)); +} +%redundancy-zone header dl.warning { + background-color: rgb(var(--tone-orange-100)); + color: rgb(var(--tone-orange-800)); +} +%redundancy-zone header dl.warning::before { + --icon-name: icon-alert-circle; + --icon-size: icon-000; + margin-right: 0.312rem; /* 5px */ + content: ''; +} +%redundancy-zone header dt::after { + content: ':'; + display: inline-block; + vertical-align: revert; + background-color: var(--transparent); +} + diff --git a/ui/packages/consul-ui/app/templates/application.hbs b/ui/packages/consul-ui/app/templates/application.hbs index d84c790570..d1c8a3a234 100644 --- a/ui/packages/consul-ui/app/templates/application.hbs +++ b/ui/packages/consul-ui/app/templates/application.hbs @@ -47,6 +47,9 @@ as |source|> {{! redirect if we aren't on a URL with dc information }} {{#if (eq route.currentName 'index')}} +{{! until we get to the dc route we don't know any permissions }} +{{! as we don't know the dc, any inital permission based }} +{{! redirects are in the dc.show route}} {{did-insert (route-action 'replaceWith' 'dc.show' (hash dc=(env 'CONSUL_DATACENTER_LOCAL') diff --git a/ui/packages/consul-ui/app/templates/dc/show.hbs b/ui/packages/consul-ui/app/templates/dc/show.hbs index 090152e1e3..6089703a7e 100644 --- a/ui/packages/consul-ui/app/templates/dc/show.hbs +++ b/ui/packages/consul-ui/app/templates/dc/show.hbs @@ -9,7 +9,8 @@ as |route|> - + +{{#if false}} href=(href-to "dc.show.serverstatus") selected=(is-href "dc.show.serverstatus") ) +(if false (hash - label=(compute (fn route.t 'health.title')) - href=(href-to 'dc.show.health') - selected=(is-href 'dc.show.health') + label=(compute (fn route.t 'cataloghealth.title')) + href=(href-to 'dc.show.cataloghealth') + selected=(is-href 'dc.show.cataloghealth') ) -(if (and (can 'read license') (not (is 'hcp'))) +'') +(if (can 'read license') (hash label=(compute (fn route.t 'license.title')) href=(href-to 'dc.show.license') @@ -32,6 +35,15 @@ as |route|> ) '') }}/> +{{/if}} + + + + {{outlet}} + diff --git a/ui/packages/consul-ui/app/templates/dc/show/index.hbs b/ui/packages/consul-ui/app/templates/dc/show/index.hbs new file mode 100644 index 0000000000..96d39860a1 --- /dev/null +++ b/ui/packages/consul-ui/app/templates/dc/show/index.hbs @@ -0,0 +1,6 @@ + + {{did-insert (route-action 'replaceWith' (if (can 'access overview') 'dc.show.serverstatus' 'dc.services.index'))}} + + diff --git a/ui/packages/consul-ui/app/templates/dc/show/serverstatus.hbs b/ui/packages/consul-ui/app/templates/dc/show/serverstatus.hbs new file mode 100644 index 0000000000..211289c721 --- /dev/null +++ b/ui/packages/consul-ui/app/templates/dc/show/serverstatus.hbs @@ -0,0 +1,240 @@ + + + +{{#let + loader.data +as |item|}} + + + + + + {{#if (eq loader.error.status "404")}} + + + Warning! + + +

+ This service has been deregistered and no longer exists in the catalog. +

+
+
+ {{else if (eq loader.error.status "403")}} + + + Error! + + +

+ You no longer have access to this service +

+
+
+ {{else}} + + + Warning! + + +

+ An error was returned whilst loading this data, refresh to try again. +

+
+
+ {{/if}} +
+ + +
+ +
+ +
+

+ {{compute (fn route.t 'tolerance.header')}} +

+
+ +
+
+

+ {{compute (fn route.t 'tolerance.immediate.header')}} +

+
+
+
+ {{compute (fn route.t 'tolerance.immediate.body')}} +
+
+ {{item.FailureTolerance}} +
+
+
+ +
+
+

+ {{compute (fn route.t 'tolerance.optimistic.header')}} + {{#if (not (can 'read zones'))}} + + {{t 'common.ui.enterprisefeature'}} + + {{/if}} + 30 seconds between server failures, Consul can restore the Immediate Fault Tolerance by replacing failed active voters with healthy back-up voters when using redundancy zones.'}} + > + +

+
+
+
+ {{compute (fn route.t 'tolerance.optimistic.body')}} +
+
+ {{item.OptimisticFailureTolerance}} +
+
+ +
+ +
+ + {{#if (gt item.RedundancyZones.length 0)}} +
+
+

+ {{pluralize (t 'common.consul.redundancyzone')}} +

+
+ + {{#each item.RedundancyZones as |item|}} + {{#if (gt item.Servers.length 0) }} +
+
+

+ {{item.Name}} +

+
+
{{t 'common.consul.failuretolerance'}}
+
{{item.FailureTolerance}}
+
+
+ +
+ {{/if}} + {{/each}} + + {{#if (gt item.Default.Servers.length 0)}} +
+
+

+ {{compute (fn route.t 'unassigned')}} +

+
+ +
+ {{/if}} + +
+ {{else}} +
+
+

+ {{compute (fn route.t 'servers')}} +

+
+ +
+ {{/if}} + + {{#if (gt item.ReadReplicas.length 0)}} +
+
+

+ {{pluralize (t 'common.consul.readreplica')}} +

+
+ + +
+ {{/if}} + +
+
+{{/let}} +
+
+ diff --git a/ui/packages/consul-ui/mock-api/v1/operator/autopilot/state b/ui/packages/consul-ui/mock-api/v1/operator/autopilot/state index e3dbc500ae..1cd442d09e 100644 --- a/ui/packages/consul-ui/mock-api/v1/operator/autopilot/state +++ b/ui/packages/consul-ui/mock-api/v1/operator/autopilot/state @@ -1,7 +1,8 @@ ${[0].map(_ => { - const servers = range(env('CONSUL_SERVER_COUNT', 3)).map(_ => fake.random.uuid()); + const zones = range(env('CONSUL_ZONE_COUNT', 3)).map(_ => fake.hacker.noun()); + const servers = range(env('CONSUL_SERVER_COUNT', 15)).map(_ => fake.random.uuid()); const failureTolerance = Math.ceil(servers.length / 2); - const optimisticTolerance = failureTolerance; // <== same for now + const optimisticTolerance = 0; const leader = fake.random.number({min: 0, max: servers.length - 1}); return ` { @@ -18,10 +19,10 @@ ${[0].map(_ => { "LastContact": "0s", "LastTerm": 2, "LastIndex": 91, - "Healthy": true, + "Healthy": ${fake.random.boolean()}, "StableSince": "2022-02-02T11:59:01.0708146Z", "ReadReplica": false, - "Status": "${i === leader ? `leader` : `voter`}", + "Status": "${i === leader ? `leader` : fake.helpers.randomize(['non-voter', 'voter', 'staging'])}", "Meta": { "consul-network-segment": "" }, @@ -30,8 +31,26 @@ ${[0].map(_ => { `)}}, "Leader": "${servers[leader]}", "Voters": [ +${servers.map(item => `"${item}"`)} + ], +${ env('CONSUL_ZONES_ENABLE', false) ? ` + "RedundancyZones": {${zones.map((item, i) => ` + "${item}": { + "Servers": [ +${servers.map(item => `"${item}"`)} + ], + "Voters": [ +${servers.map(item => `"${item}"`)} + ], + "FailureTolerance": ${i} + } + `)} + }, + "ReadReplicas": [ ${servers.map(item => `"${item}"`)} - ] + ], +` : ``} + "Upgrade": {} } `; })} diff --git a/ui/packages/consul-ui/tests/unit/abilities/-test.js b/ui/packages/consul-ui/tests/unit/abilities/-test.js index e3ac70d9f6..3ac3cf0724 100644 --- a/ui/packages/consul-ui/tests/unit/abilities/-test.js +++ b/ui/packages/consul-ui/tests/unit/abilities/-test.js @@ -52,6 +52,9 @@ module('Unit | Ability | *', function(hooks) { // TODO: We currently hardcode KVs to always be true assert.equal(true, ability[`can${perm}`], `Expected ${item}.can${perm} to be true`); return; + case 'zone': + // Zone permissions depend on NSPACES_ENABLED + return; } assert.equal( bool, diff --git a/ui/packages/consul-ui/translations/common/en-us.yaml b/ui/packages/consul-ui/translations/common/en-us.yaml index e5700effe7..5365996abf 100644 --- a/ui/packages/consul-ui/translations/common/en-us.yaml +++ b/ui/packages/consul-ui/translations/common/en-us.yaml @@ -14,6 +14,7 @@ ui: name: Name creation: Creation maxttl: Max TTL + enterprisefeature: Enterprise feature consul: name: Name passing: Passing @@ -41,6 +42,9 @@ consul: destinationname: Destination Name sourcename: Source Name displayname: Display Name + failuretolerance: Fault tolerance + readreplica: Read replica + redundancyzone: Redundancy zone search: search: Search searchproperty: Search Across diff --git a/ui/packages/consul-ui/translations/routes/en-us.yaml b/ui/packages/consul-ui/translations/routes/en-us.yaml index 82c60e4a86..59dd94ff15 100644 --- a/ui/packages/consul-ui/translations/routes/en-us.yaml +++ b/ui/packages/consul-ui/translations/routes/en-us.yaml @@ -3,10 +3,20 @@ dc: title: Cluster Overview serverstatus: title: Server status - health: + unassigned: Unassigned Zones + tolerance: + header: Server fault tolerance + immediate: + header: Immediate + body: the number of healthy active voting servers that can fail at once without causing an outage + optimistic: + header: Optimistic + body: the number of healthy active and back-up voting servers that can fail gradually without causing an outage + cataloghealth: title: Health license: title: License + nodes: show: healthchecks: diff --git a/ui/packages/consul-ui/vendor/consul-ui/routes.js b/ui/packages/consul-ui/vendor/consul-ui/routes.js index c79057c3bc..99b3cfda5d 100644 --- a/ui/packages/consul-ui/vendor/consul-ui/routes.js +++ b/ui/packages/consul-ui/vendor/consul-ui/routes.js @@ -13,18 +13,17 @@ show: { _options: { path: '/overview', - redirect: './serverstatus', abilities: ['access overview'] }, serverstatus: { _options: { path: '/server-status', - abilities: ['access overview', 'read raft'] + abilities: ['access overview', 'read zones'] }, }, - health: { + cataloghealth: { _options: { - path: '/health', + path: '/catalog-health', abilities: ['access overview'] }, }, @@ -417,6 +416,7 @@ }, index: { _options: { path: '/' }, + // root index redirects are currently dealt with in application.hbs }, settings: { _options: {