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 @@
{{#each @items as |item|}}
-
-
+
+
+
{{/each}}
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: {