ui: Initial Server Status Overview Page (#12599)

pull/12669/head
John Cowen 2022-04-04 09:45:03 +01:00 committed by GitHub
parent 61af7947f9
commit 18f55be3c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 525 additions and 47 deletions

View File

@ -1,6 +0,0 @@
import BaseAbility from './base';
export default class RaftAbility extends BaseAbility {
resource = 'operator';
segmented = false;
}

View File

@ -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');
}
}

View File

@ -16,7 +16,7 @@
margin-bottom: calc(var(--padding-y) / 2); margin-bottom: calc(var(--padding-y) / 2);
} }
%consul-server-card.voting-status-leader dd { %consul-server-card.voting-status-leader dd {
margin-left: calc(var(--tile-size) + var(--padding-x)); margin-left: calc(var(--tile-size) + 1rem); /* 16px */
} }

View File

@ -7,9 +7,11 @@
<ul> <ul>
{{#each @items as |item|}} {{#each @items as |item|}}
<li> <li>
<a href={{href-to 'dc.nodes.show' item.Name}}>
<Consul::Server::Card <Consul::Server::Card
@item={{item}} @item={{item}}
/> />
</a>
</li> </li>
{{/each}} {{/each}}
</ul> </ul>

View File

@ -30,7 +30,7 @@
border-color: rgb(var(--tone-gray-999) / 10%); border-color: rgb(var(--tone-gray-999) / 10%);
} }
%with-leader-tile::after { %with-leader-tile::after {
--icon-name: icon-star-circle; --icon-name: icon-star-fill;
--icon-size: icon-700; --icon-size: icon-700;
color: rgb(var(--strawberry-500)); color: rgb(var(--strawberry-500));
} }

View File

@ -14,6 +14,9 @@ export default class Datacenter extends Model {
@attr('string') Leader; @attr('string') Leader;
@attr() Voters; // [] @attr() Voters; // []
@attr() Servers; // [] the API uses {} but we reshape that on the frontend @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') Local;
@attr('boolean') Primary; @attr('boolean') Primary;

View File

@ -108,21 +108,56 @@ export default class DcService extends RepositoryService {
GET /v1/operator/autopilot/state?${{ dc }} GET /v1/operator/autopilot/state?${{ dc }}
X-Request-ID: ${uri} X-Request-ID: ${uri}
`)( `)(
(headers, body, cache) => ({ (headers, body, cache) => {
// turn servers into an array instead of a map/object
const servers = Object.values(body.Servers);
const grouped = [];
return {
meta: { meta: {
version: 2, version: 2,
uri: uri, uri: uri,
interval: 30 * SECONDS
}, },
body: cache( body: cache(
{ {
...body, ...body,
// turn servers into an array instead of a map/object // all servers
Servers: Object.values(body.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` uri => uri`${MODEL_NAME}:///${''}/${''}/${dc}/datacenter`
) )
}) }
}
); );
} }

View File

@ -10,13 +10,13 @@
} }
%visually-unhidden, %visually-unhidden,
%unvisually-hidden { %unvisually-hidden {
position: static; position: static !important;
clip: unset; clip: unset !important;
overflow: visible; overflow: visible !important;
width: auto; width: auto !important;
height: auto; height: auto !important;
margin: 0; margin: 0 !important;
padding: 0; padding: 0 !important;
} }
%visually-hidden-text { %visually-hidden-text {
text-indent: -9000px; text-indent: -9000px;

View File

@ -15,6 +15,7 @@
--decor-border-400: 4px solid; --decor-border-400: 4px solid;
/* box-shadowing*/ /* box-shadowing*/
--decor-elevation-000: none;
--decor-elevation-100: 0 3px 2px rgb(var(--black) / 6%); --decor-elevation-100: 0 3px 2px rgb(var(--black) / 6%);
--decor-elevation-200: 0 2px 4px rgb(var(--black) / 10%); --decor-elevation-200: 0 2px 4px rgb(var(--black) / 10%);
--decor-elevation-300: 0 5px 1px -2px rgb(var(--black) / 12%); --decor-elevation-300: 0 5px 1px -2px rgb(var(--black) / 12%);

View File

@ -3,3 +3,4 @@
@import 'routes/dc/kv/index'; @import 'routes/dc/kv/index';
@import 'routes/dc/acls/index'; @import 'routes/dc/acls/index';
@import 'routes/dc/intentions/index'; @import 'routes/dc/intentions/index';
@import 'routes/dc/overview/serverstatus';

View File

@ -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);
}

View File

@ -47,6 +47,9 @@ as |source|>
{{! redirect if we aren't on a URL with dc information }} {{! redirect if we aren't on a URL with dc information }}
{{#if (eq route.currentName 'index')}} {{#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' {{did-insert (route-action 'replaceWith' 'dc.show'
(hash (hash
dc=(env 'CONSUL_DATACENTER_LOCAL') dc=(env 'CONSUL_DATACENTER_LOCAL')

View File

@ -9,7 +9,8 @@ as |route|>
</BlockSlot> </BlockSlot>
<BlockSlot @name="toolbar"> <BlockSlot @name="toolbar">
</BlockSlot> </BlockSlot>
<BlockSlot @name="content"> <BlockSlot @name="nav">
{{#if false}}
<TabNav @items={{ <TabNav @items={{
compact compact
(array (array
@ -18,12 +19,14 @@ as |route|>
href=(href-to "dc.show.serverstatus") href=(href-to "dc.show.serverstatus")
selected=(is-href "dc.show.serverstatus") selected=(is-href "dc.show.serverstatus")
) )
(if false
(hash (hash
label=(compute (fn route.t 'health.title')) label=(compute (fn route.t 'cataloghealth.title'))
href=(href-to 'dc.show.health') href=(href-to 'dc.show.cataloghealth')
selected=(is-href 'dc.show.health') selected=(is-href 'dc.show.cataloghealth')
) )
(if (and (can 'read license') (not (is 'hcp'))) '')
(if (can 'read license')
(hash (hash
label=(compute (fn route.t 'license.title')) label=(compute (fn route.t 'license.title'))
href=(href-to 'dc.show.license') href=(href-to 'dc.show.license')
@ -32,6 +35,15 @@ as |route|>
) )
'') '')
}}/> }}/>
{{/if}}
</BlockSlot>
<BlockSlot @name="content">
<Outlet
@name={{routeName}}
@model={{route.model}}
as |o|>
{{outlet}}
</Outlet>
</BlockSlot> </BlockSlot>
</AppView> </AppView>

View File

@ -0,0 +1,6 @@
<Route
@name={{routeName}}
as |route|>
{{did-insert (route-action 'replaceWith' (if (can 'access overview') 'dc.show.serverstatus' 'dc.services.index'))}}
</Route>

View File

@ -0,0 +1,240 @@
<Route
@name={{routeName}}
as |route|>
<DataLoader
@src={{
uri '/${partition}/${nspace}/${dc}/datacenter'
(hash
partition=route.params.partition
nspace=route.params.nspace
dc=route.params.dc
)
}}
as |loader|>
{{#let
loader.data
as |item|}}
<BlockSlot @name="error">
<ErrorState
@error={{loader.error}}
@login={{route.model.app.login.open}}
/>
</BlockSlot>
<BlockSlot @name="disconnected" as |after|>
{{#if (eq loader.error.status "404")}}
<Notice
{{notification
sticky=true
}}
class="notification-update"
@type="warning"
as |notice|>
<notice.Header>
<strong>Warning!</strong>
</notice.Header>
<notice.Body>
<p>
This service has been deregistered and no longer exists in the catalog.
</p>
</notice.Body>
</Notice>
{{else if (eq loader.error.status "403")}}
<Notice
{{notification
sticky=true
}}
class="notification-update"
@type="error"
as |notice|>
<notice.Header>
<strong>Error!</strong>
</notice.Header>
<notice.Body>
<p>
You no longer have access to this service
</p>
</notice.Body>
</Notice>
{{else}}
<Notice
{{notification
sticky=true
}}
class="notification-update"
@type="warning"
as |notice|>
<notice.Header>
<strong>Warning!</strong>
</notice.Header>
<notice.Body>
<p>
An error was returned whilst loading this data, refresh to try again.
</p>
</notice.Body>
</Notice>
{{/if}}
</BlockSlot>
<BlockSlot @name="loaded">
<div class="tab-section">
<section
class={{class-map
'server-failure-tolerance'
}}
>
<header>
<h2>
{{compute (fn route.t 'tolerance.header')}}
</h2>
</header>
<section
class={{class-map
(array 'immediate-tolerance')
}}
>
<header>
<h3>
{{compute (fn route.t 'tolerance.immediate.header')}}
</h3>
</header>
<dl
class={{class-map
(array 'warning' (and
(eq item.FailureTolerance 0)
(eq item.OptimisticFailureTolerance 0)
))
}}
>
<dt>
{{compute (fn route.t 'tolerance.immediate.body')}}
</dt>
<dd>
{{item.FailureTolerance}}
</dd>
</dl>
</section>
<section
class={{class-map
(array 'optimistic-tolerance')
}}
>
<header>
<h3>
{{compute (fn route.t 'tolerance.optimistic.header')}}
{{#if (not (can 'read zones'))}}
<em>
{{t 'common.ui.enterprisefeature'}}
</em>
{{/if}}
<span
{{tooltip 'With > 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.'}}
>
</span>
</h3>
</header>
<dl
class={{class-map
(array 'warning' (eq item.OptimisticFailureTolerance 0))
}}
>
<dt>
{{compute (fn route.t 'tolerance.optimistic.body')}}
</dt>
<dd>
{{item.OptimisticFailureTolerance}}
</dd>
</dl>
</section>
</section>
{{#if (gt item.RedundancyZones.length 0)}}
<section
class={{class-map
'redundancy-zones'
}}
>
<header>
<h2>
{{pluralize (t 'common.consul.redundancyzone')}}
</h2>
</header>
{{#each item.RedundancyZones as |item|}}
{{#if (gt item.Servers.length 0) }}
<section>
<header>
<h3>
{{item.Name}}
</h3>
<dl
class={{class-map
(array 'warning' (eq item.FailureTolerance 0))
}}
>
<dt
>{{t 'common.consul.failuretolerance'}}</dt>
<dd>{{item.FailureTolerance}}</dd>
</dl>
</header>
<Consul::Server::List
@items={{item.Servers}}
/>
</section>
{{/if}}
{{/each}}
{{#if (gt item.Default.Servers.length 0)}}
<section>
<header>
<h3>
{{compute (fn route.t 'unassigned')}}
</h3>
</header>
<Consul::Server::List
@items={{item.Default.Servers}}
/>
</section>
{{/if}}
</section>
{{else}}
<section>
<header>
<h2>
{{compute (fn route.t 'servers')}}
</h2>
</header>
<Consul::Server::List
@items={{item.Default.Servers}}
/>
</section>
{{/if}}
{{#if (gt item.ReadReplicas.length 0)}}
<section>
<header>
<h2>
{{pluralize (t 'common.consul.readreplica')}}
</h2>
</header>
<Consul::Server::List
@items={{item.ReadReplicas}}
/>
</section>
{{/if}}
</div>
</BlockSlot>
{{/let}}
</DataLoader>
</Route>

View File

@ -1,7 +1,8 @@
${[0].map(_ => { ${[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 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}); const leader = fake.random.number({min: 0, max: servers.length - 1});
return ` return `
{ {
@ -18,10 +19,10 @@ ${[0].map(_ => {
"LastContact": "0s", "LastContact": "0s",
"LastTerm": 2, "LastTerm": 2,
"LastIndex": 91, "LastIndex": 91,
"Healthy": true, "Healthy": ${fake.random.boolean()},
"StableSince": "2022-02-02T11:59:01.0708146Z", "StableSince": "2022-02-02T11:59:01.0708146Z",
"ReadReplica": false, "ReadReplica": false,
"Status": "${i === leader ? `leader` : `voter`}", "Status": "${i === leader ? `leader` : fake.helpers.randomize(['non-voter', 'voter', 'staging'])}",
"Meta": { "Meta": {
"consul-network-segment": "" "consul-network-segment": ""
}, },
@ -30,8 +31,26 @@ ${[0].map(_ => {
`)}}, `)}},
"Leader": "${servers[leader]}", "Leader": "${servers[leader]}",
"Voters": [ "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}"`)} ${servers.map(item => `"${item}"`)}
] ],
` : ``}
"Upgrade": {}
} }
`; `;
})} })}

View File

@ -52,6 +52,9 @@ module('Unit | Ability | *', function(hooks) {
// TODO: We currently hardcode KVs to always be true // TODO: We currently hardcode KVs to always be true
assert.equal(true, ability[`can${perm}`], `Expected ${item}.can${perm} to be true`); assert.equal(true, ability[`can${perm}`], `Expected ${item}.can${perm} to be true`);
return; return;
case 'zone':
// Zone permissions depend on NSPACES_ENABLED
return;
} }
assert.equal( assert.equal(
bool, bool,

View File

@ -14,6 +14,7 @@ ui:
name: Name name: Name
creation: Creation creation: Creation
maxttl: Max TTL maxttl: Max TTL
enterprisefeature: Enterprise feature
consul: consul:
name: Name name: Name
passing: Passing passing: Passing
@ -41,6 +42,9 @@ consul:
destinationname: Destination Name destinationname: Destination Name
sourcename: Source Name sourcename: Source Name
displayname: Display Name displayname: Display Name
failuretolerance: Fault tolerance
readreplica: Read replica
redundancyzone: Redundancy zone
search: search:
search: Search search: Search
searchproperty: Search Across searchproperty: Search Across

View File

@ -3,10 +3,20 @@ dc:
title: Cluster Overview title: Cluster Overview
serverstatus: serverstatus:
title: Server status 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 title: Health
license: license:
title: License title: License
nodes: nodes:
show: show:
healthchecks: healthchecks:

View File

@ -13,18 +13,17 @@
show: { show: {
_options: { _options: {
path: '/overview', path: '/overview',
redirect: './serverstatus',
abilities: ['access overview'] abilities: ['access overview']
}, },
serverstatus: { serverstatus: {
_options: { _options: {
path: '/server-status', path: '/server-status',
abilities: ['access overview', 'read raft'] abilities: ['access overview', 'read zones']
}, },
}, },
health: { cataloghealth: {
_options: { _options: {
path: '/health', path: '/catalog-health',
abilities: ['access overview'] abilities: ['access overview']
}, },
}, },
@ -417,6 +416,7 @@
}, },
index: { index: {
_options: { path: '/' }, _options: { path: '/' },
// root index redirects are currently dealt with in application.hbs
}, },
settings: { settings: {
_options: { _options: {