Skip to content

Commit

Permalink
[ui] Service Discovery: Allocation Service fly-out (#14389)
Browse files Browse the repository at this point in the history
* Bones of a new flyout section

* Basic sidebar behaviour and style edits

* Concept of a refID for service fragments to disambiguate task and group

* A11y audit etc

* Moves health check aggregation to serviceFragment model and retains history

* Has to be a getter

* flyout populated

* Sidebar styling

* Sidebar table and details added

* Mirage fixture

* Active status and table styles

* Unit test mock updated

* Acceptance tests for alloc services table and flyout

* Chart styles closer to mock

* Without a paused test

* Consul and Nomad icons in services table

* Alloc services test updates in light of new column changes

* without using an inherited scenario
  • Loading branch information
philrenaud committed Sep 7, 2022
1 parent 9f0d9c9 commit e68f07e
Show file tree
Hide file tree
Showing 15 changed files with 785 additions and 325 deletions.
130 changes: 130 additions & 0 deletions ui/app/components/allocation-service-sidebar.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
<div
class="sidebar has-subnav service-sidebar {{if this.isSideBarOpen "open"}}"
{{on-click-outside
@fns.closeSidebar
capture=true
}}
>
{{#if @service}}
{{keyboard-commands this.keyCommands}}
<header class="detail-header">
<h1 class="title">
{{@service.name}}
<span class="aggregate-status">
{{#if (eq this.aggregateStatus 'Unhealthy')}}
<FlightIcon @name="x-square-fill" @color="#c84034" />
Unhealthy
{{else}}
<FlightIcon @name="check-square-fill" @color="#25ba81" />
Healthy
{{/if}}
</span>
</h1>
<button
data-test-close-service-sidebar
class="button is-borderless"
type="button"
{{on "click" @fns.closeSidebar}}
>
{{x-icon "cancel"}}
</button>
</header>

<div class="boxed-section is-small">
<div
class="boxed-section-body inline-definitions"
>
<span class="label">
Service Details
</span>

<div>
<span class="pair">
<span class="term">
Allocation
</span>
<LinkTo
@route="allocations.allocation"
@model={{@allocation}}
@query={{hash service=""}}
>
{{@allocation.shortId}}
</LinkTo>
</span>
<span class="pair">
<span class="term">
IP Address &amp; Port
</span>
<a
href="http://{{this.address}}"
target="_blank"
rel="noopener noreferrer"
>
{{this.address}}
</a>
</span>
{{#if @service.tags.length}}
<span class="pair">
<span class="term">
Tags
</span>
{{join ", " @service.tags}}
</span>
{{/if}}
<span class="pair">
<span class="term">
Client
</span>
<LinkTo
@route="clients.client"
@model={{@allocation.node}}
>
{{@allocation.node.shortId}}
</LinkTo>
</span>

</div>
</div>
</div>
{{#if @service.mostRecentChecks.length}}
<ListTable class="health-checks" @source={{@service.mostRecentChecks}} as |t|>
<t.head>
<th>
Name
</th>
<th>
Status
</th>
<td>
Output
</td>
</t.head>
<t.body as |row|>
<tr data-service-health={{row.model.Status}}>
<td class="name">
<span title={{row.model.Check}}>{{row.model.Check}}</span>
</td>
<td class="status">
<span>
{{#if (eq row.model.Status "success")}}
<FlightIcon @name="check-square-fill" @color="#25ba81" />
Healthy
{{else if (eq row.model.Status "failure")}}
<FlightIcon @name="x-square-fill" @color="#c84034" />
Unhealthy
{{else if (eq row.model.Status "pending")}}
Pending
{{/if}}
</span>
</td>
<td class="service-output">
<code>
{{row.model.Output}}
</code>
</td>
</tr>
</t.body>
</ListTable>
{{/if}}
{{/if}}
</div>
41 changes: 41 additions & 0 deletions ui/app/components/allocation-service-sidebar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';

export default class AllocationServiceSidebarComponent extends Component {
@service store;

get isSideBarOpen() {
return !!this.args.service;
}
keyCommands = [
{
label: 'Close Evaluations Sidebar',
pattern: ['Escape'],
action: () => this.args.fns.closeSidebar(),
},
];

get service() {
return this.store.query('service-fragment', { refID: this.args.serviceID });
}

get address() {
const port = this.args.allocation?.allocatedResources?.ports?.findBy(
'label',
this.args.service.portLabel
);
if (port) {
return `${port.hostIp}:${port.value}`;
} else {
return null;
}
}

get aggregateStatus() {
return this.args.service?.mostRecentChecks?.any(
(check) => check.Status === 'failure'
)
? 'Unhealthy'
: 'Healthy';
}
}
15 changes: 6 additions & 9 deletions ui/app/components/service-status-bar.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,19 @@ import classic from 'ember-classic-decorator';
export default class ServiceStatusBar extends DistributionBar {
layoutName = 'components/distribution-bar';

services = null;
name = null;
status = null;

'data-test-service-status-bar' = true;

@computed('services.{}', 'name')
@computed('status.{failure,pending,success}')
get data() {
const service = this.services && this.services.get(this.name);

if (!service) {
if (!this.status) {
return [];
}

const pending = service.pending || 0;
const failing = service.failure || 0;
const success = service.success || 0;
const pending = this.status.pending || 0;
const failing = this.status.failure || 0;
const success = this.status.success || 0;

const [grey, red, green] = ['queued', 'failed', 'complete'];

Expand Down
88 changes: 57 additions & 31 deletions ui/app/controllers/allocations/allocation/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { watchRecord } from 'nomad-ui/utils/properties/watch';
import messageForError from 'nomad-ui/utils/message-from-adapter-error';
import classic from 'ember-classic-decorator';
import { union } from '@ember/object/computed';
import { tracked } from '@glimmer/tracking';

@classic
export default class IndexController extends Controller.extend(Sortable) {
Expand All @@ -25,6 +26,9 @@ export default class IndexController extends Controller.extend(Sortable) {
{
sortDescending: 'desc',
},
{
activeServiceID: 'service',
},
];

sortProperty = 'name';
Expand Down Expand Up @@ -55,7 +59,7 @@ export default class IndexController extends Controller.extend(Sortable) {
@computed('tasks.@each.services')
get taskServices() {
return this.get('tasks')
.map((t) => ((t && t.get('services')) || []).toArray())
.map((t) => ((t && t.services) || []).toArray())
.flat()
.compact();
}
Expand All @@ -67,38 +71,33 @@ export default class IndexController extends Controller.extend(Sortable) {

@union('taskServices', 'groupServices') services;

@computed('model.healthChecks.{}')
get serviceHealthStatuses() {
if (!this.model.healthChecks) return null;

let result = new Map();
Object.values(this.model.healthChecks)?.forEach((service) => {
const isTask = !!service.Task;
const groupName = service.Group.split('.')[1].split('[')[0];
const currentServiceStatus = service.Status;

const currentServiceName = isTask
? service.Task.concat(`-${service.Service}`)
: groupName.concat(`-${service.Service}`);
const serviceStatuses = result.get(currentServiceName);
if (serviceStatuses) {
if (serviceStatuses[currentServiceStatus]) {
result.set(currentServiceName, {
...serviceStatuses,
[currentServiceStatus]: serviceStatuses[currentServiceStatus]++,
});
} else {
result.set(currentServiceName, {
...serviceStatuses,
[currentServiceStatus]: 1,
});
}
} else {
result.set(currentServiceName, { [currentServiceStatus]: 1 });
@computed('model.healthChecks.{}', 'services')
get servicesWithHealthChecks() {
return this.services.map((service) => {
if (this.model.healthChecks) {
const healthChecks = Object.values(this.model.healthChecks)?.filter(
(check) => {
const refPrefix =
check.Task || check.Group.split('.')[1].split('[')[0];
const currentServiceName = `${refPrefix}-${check.Service}`;
return currentServiceName === service.refID;
}
);
// Only append those healthchecks whose timestamps are not already found in service.healthChecks
healthChecks.forEach((check) => {
if (
!service.healthChecks.find(
(sc) =>
sc.Check === check.Check && sc.Timestamp === check.Timestamp
)
) {
service.healthChecks.pushObject(check);
service.healthChecks = [...service.healthChecks.slice(-10)];
}
});
}
return service;
});

return result;
}

onDismiss() {
Expand Down Expand Up @@ -165,4 +164,31 @@ export default class IndexController extends Controller.extend(Sortable) {
taskClick(allocation, task, event) {
lazyClick([() => this.send('gotoTask', allocation, task), event]);
}

//#region Services

@tracked activeServiceID = null;

@action handleServiceClick(service) {
this.set('activeServiceID', service.refID);
}

@computed('activeServiceID', 'services')
get activeService() {
return this.services.findBy('refID', this.activeServiceID);
}

@action closeSidebar() {
this.set('activeServiceID', null);
}

keyCommands = [
{
label: 'Close Evaluations Sidebar',
pattern: ['Escape'],
action: () => this.closeSidebar(),
},
];

//#endregion Services
}
31 changes: 30 additions & 1 deletion ui/app/models/service-fragment.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { attr } from '@ember-data/model';
import Fragment from 'ember-data-model-fragments/fragment';
import { fragment } from 'ember-data-model-fragments/attributes';
import { computed } from '@ember/object';
import classic from 'ember-classic-decorator';

@classic
export default class Service extends Fragment {
@attr('string') name;
@attr('string') portLabel;
Expand All @@ -11,8 +14,34 @@ export default class Service extends Fragment {
@fragment('consul-connect') connect;
@attr() groupName;
@attr() taskName;

get refID() {
return `${this.groupName || this.taskName}-${this.name}`;
}
@attr({ defaultValue: () => [] }) healthChecks;

@computed('healthChecks.[]')
get mostRecentChecks() {
// Get unique check names, then get the most recent one
return this.get('healthChecks')
.mapBy('Check')
.uniq()
.map((name) => {
return this.get('healthChecks')
.sortBy('Timestamp')
.reverse()
.find((x) => x.Check === name);
})
.sortBy('Check');
}

@computed('mostRecentChecks.[]')
get mostRecentCheckStatus() {
// Get unique check names, then get the most recent one
return this.get('mostRecentChecks')
.mapBy('Status')
.reduce((acc, curr) => {
acc[curr] = (acc[curr] || 0) + 1;
return acc;
}, {});
}
}
1 change: 0 additions & 1 deletion ui/app/serializers/task-group.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ export default class TaskGroup extends ApplicationSerializer {
service.GroupName = hash.Name;
});
}

// Provide EphemeralDisk to each task
hash.Tasks.forEach((task) => {
task.EphemeralDisk = copy(hash.EphemeralDisk);
Expand Down
Loading

0 comments on commit e68f07e

Please sign in to comment.