[ui] task logs in sidebar (#14612)
* button styles * Further styles including global toggle adjustment * sidebar funcs and header * Functioning task logs in high-level sidebars * same-lineify the show tasks toggle * Changelog * Full-height sidebar calc in css, plz drop soon container queries * Active status and query params for allocations page * Reactive shouldShowLogs getter and added to client and task group pages * Higher order func passing, thanks @DingoEatingFuzz * Non-service job types get allocation params passed * Keyframe animation for task log sidebar * Acceptance test * A few more sub-row tests * Lintfix
This commit is contained in:
parent
c29c4bd66c
commit
eca0e7bf56
3
.changelog/14612.txt
Normal file
3
.changelog/14612.txt
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:improvement
|
||||||
|
ui: adds a sidebar to show in-page logs for a given task, accessible via job, client, or task group routes
|
||||||
|
```
|
|
@ -10,7 +10,7 @@ export default class AllocationServiceSidebarComponent extends Component {
|
||||||
}
|
}
|
||||||
keyCommands = [
|
keyCommands = [
|
||||||
{
|
{
|
||||||
label: 'Close Evaluations Sidebar',
|
label: 'Close Service Sidebar',
|
||||||
pattern: ['Escape'],
|
pattern: ['Escape'],
|
||||||
action: () => this.args.fns.closeSidebar(),
|
action: () => this.args.fns.closeSidebar(),
|
||||||
},
|
},
|
||||||
|
|
|
@ -22,6 +22,7 @@ export default class StreamingFile extends Component.extend(WindowResizable) {
|
||||||
isStreaming = true;
|
isStreaming = true;
|
||||||
logger = null;
|
logger = null;
|
||||||
follow = true;
|
follow = true;
|
||||||
|
shouldFillHeight = true;
|
||||||
|
|
||||||
// Internal bookkeeping to avoid multiple scroll events on one frame
|
// Internal bookkeeping to avoid multiple scroll events on one frame
|
||||||
requestFrame = true;
|
requestFrame = true;
|
||||||
|
@ -89,7 +90,9 @@ export default class StreamingFile extends Component.extend(WindowResizable) {
|
||||||
|
|
||||||
didInsertElement() {
|
didInsertElement() {
|
||||||
super.didInsertElement(...arguments);
|
super.didInsertElement(...arguments);
|
||||||
|
if (this.shouldFillHeight) {
|
||||||
this.fillAvailableHeight();
|
this.fillAvailableHeight();
|
||||||
|
}
|
||||||
|
|
||||||
this.set('_scrollHandler', this.scrollHandler.bind(this));
|
this.set('_scrollHandler', this.scrollHandler.bind(this));
|
||||||
this.element.addEventListener('scroll', this._scrollHandler);
|
this.element.addEventListener('scroll', this._scrollHandler);
|
||||||
|
@ -105,8 +108,10 @@ export default class StreamingFile extends Component.extend(WindowResizable) {
|
||||||
}
|
}
|
||||||
|
|
||||||
windowResizeHandler() {
|
windowResizeHandler() {
|
||||||
|
if (this.shouldFillHeight) {
|
||||||
once(this, this.fillAvailableHeight);
|
once(this, this.fillAvailableHeight);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fillAvailableHeight() {
|
fillAvailableHeight() {
|
||||||
// This math is arbitrary and far from bulletproof, but the UX
|
// This math is arbitrary and far from bulletproof, but the UX
|
||||||
|
|
44
ui/app/components/task-context-sidebar.hbs
Normal file
44
ui/app/components/task-context-sidebar.hbs
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
<Portal @target="log-sidebar-portal">
|
||||||
|
<div
|
||||||
|
class="sidebar task-context-sidebar has-subnav {{if this.isSideBarOpen "open"}}"
|
||||||
|
{{on-click-outside
|
||||||
|
@fns.closeSidebar
|
||||||
|
capture=true
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{{#if @task}}
|
||||||
|
{{keyboard-commands this.keyCommands}}
|
||||||
|
<header>
|
||||||
|
<h1 class="title">
|
||||||
|
{{@task.name}}
|
||||||
|
<span class="state {{@task.state}}">
|
||||||
|
{{@task.state}}
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
<LinkTo
|
||||||
|
class="link"
|
||||||
|
title={{@task.name}}
|
||||||
|
@route="allocations.allocation.task"
|
||||||
|
@models={{array @task.allocation @task}}
|
||||||
|
>
|
||||||
|
Go to Task page
|
||||||
|
</LinkTo>
|
||||||
|
<button
|
||||||
|
class="button close is-borderless"
|
||||||
|
type="button"
|
||||||
|
{{on "click" @fns.closeSidebar}}
|
||||||
|
>
|
||||||
|
{{x-icon "cancel"}}
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<TaskLog
|
||||||
|
@allocation={{@task.allocation}}
|
||||||
|
@task={{@task.name}}
|
||||||
|
@shouldFillHeight={{false}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</Portal>
|
16
ui/app/components/task-context-sidebar.js
Normal file
16
ui/app/components/task-context-sidebar.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
// @ts-check
|
||||||
|
import Component from '@glimmer/component';
|
||||||
|
|
||||||
|
export default class TaskContextSidebarComponent extends Component {
|
||||||
|
get isSideBarOpen() {
|
||||||
|
return !!this.args.task;
|
||||||
|
}
|
||||||
|
|
||||||
|
keyCommands = [
|
||||||
|
{
|
||||||
|
label: 'Close Task Logs Sidebar',
|
||||||
|
pattern: ['Escape'],
|
||||||
|
action: () => this.args.fns.closeSidebar(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
|
@ -35,6 +35,8 @@ export default class TaskLog extends Component {
|
||||||
isStreaming = true;
|
isStreaming = true;
|
||||||
streamMode = 'streaming';
|
streamMode = 'streaming';
|
||||||
|
|
||||||
|
shouldFillHeight = true;
|
||||||
|
|
||||||
@alias('userSettings.logMode') mode;
|
@alias('userSettings.logMode') mode;
|
||||||
|
|
||||||
@computed('allocation.{id,node.httpAddr}', 'useServer')
|
@computed('allocation.{id,node.httpAddr}', 'useServer')
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
<tr class="task-sub-row"
|
<tr class="task-sub-row {{if @active "is-active"}}"
|
||||||
{{keyboard-shortcut
|
{{keyboard-shortcut
|
||||||
enumerated=true
|
enumerated=true
|
||||||
action=(action "gotoTask" this.task.allocation this.task)
|
action=(action "gotoTask" this.task.allocation this.task)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<td colspan={{@namespan}}>
|
<td colspan={{@namespan}}>
|
||||||
/
|
<div class="name-grid">
|
||||||
<LinkTo @route="allocations.allocation.task" @models={{array this.task.allocation this.task}}>
|
<LinkTo title={{this.task.name}} class="task-name" @route="allocations.allocation.task" @models={{array this.task.allocation this.task}}>{{this.task.name}}</LinkTo>
|
||||||
{{this.task.name}}
|
<button type="button" class="logs-sidebar-trigger button is-borderless is-inline is-compact" onclick={{action "handleTaskLogsClick" this.task}}>
|
||||||
</LinkTo>
|
<FlightIcon @name="logs" />View Logs
|
||||||
{{!-- TODO: in-page logs --}}
|
</button>
|
||||||
{{!-- <FlightIcon @name="logs" /> --}}
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td data-test-cpu class="is-1 has-text-centered">
|
<td data-test-cpu class="is-1 has-text-centered">
|
||||||
{{#if this.task.isRunning}}
|
{{#if this.task.isRunning}}
|
||||||
|
@ -77,3 +77,12 @@
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
{{yield}}
|
{{yield}}
|
||||||
|
|
||||||
|
{{#if this.shouldShowLogs}}
|
||||||
|
<TaskContextSidebar
|
||||||
|
@task={{this.task}}
|
||||||
|
@fns={{hash
|
||||||
|
closeSidebar=this.closeSidebar
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
|
|
@ -71,4 +71,22 @@ export default class TaskSubRowComponent extends Component {
|
||||||
} while (this.enablePolling);
|
} while (this.enablePolling);
|
||||||
}).drop())
|
}).drop())
|
||||||
fetchStats;
|
fetchStats;
|
||||||
|
|
||||||
|
//#region Logs Sidebar
|
||||||
|
|
||||||
|
@alias('args.active') shouldShowLogs;
|
||||||
|
|
||||||
|
@action handleTaskLogsClick(task) {
|
||||||
|
if (this.args.onSetActiveTask) {
|
||||||
|
this.args.onSetActiveTask(task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action closeSidebar() {
|
||||||
|
if (this.args.onSetActiveTask) {
|
||||||
|
this.args.onSetActiveTask(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion Logs Sidebar
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,6 +47,7 @@ export default class ClientController extends Controller.extend(
|
||||||
{
|
{
|
||||||
qpStatus: 'status',
|
qpStatus: 'status',
|
||||||
},
|
},
|
||||||
|
'activeTask',
|
||||||
];
|
];
|
||||||
|
|
||||||
// Set in the route
|
// Set in the route
|
||||||
|
@ -57,6 +58,7 @@ export default class ClientController extends Controller.extend(
|
||||||
qpStatus = '';
|
qpStatus = '';
|
||||||
currentPage = 1;
|
currentPage = 1;
|
||||||
pageSize = 8;
|
pageSize = 8;
|
||||||
|
activeTask = null;
|
||||||
|
|
||||||
sortProperty = 'modifyIndex';
|
sortProperty = 'modifyIndex';
|
||||||
sortDescending = true;
|
sortDescending = true;
|
||||||
|
@ -266,4 +268,13 @@ export default class ClientController extends Controller.extend(
|
||||||
setFacetQueryParam(queryParam, selection) {
|
setFacetQueryParam(queryParam, selection) {
|
||||||
this.set(queryParam, serialize(selection));
|
this.set(queryParam, serialize(selection));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
setActiveTaskQueryParam(task) {
|
||||||
|
if (task) {
|
||||||
|
this.set('activeTask', `${task.allocation.id}-${task.name}`);
|
||||||
|
} else {
|
||||||
|
this.set('activeTask', null);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,6 +41,7 @@ export default class AllocationsController extends Controller.extend(
|
||||||
{
|
{
|
||||||
qpTaskGroup: 'taskGroup',
|
qpTaskGroup: 'taskGroup',
|
||||||
},
|
},
|
||||||
|
'activeTask',
|
||||||
];
|
];
|
||||||
|
|
||||||
qpStatus = '';
|
qpStatus = '';
|
||||||
|
@ -48,6 +49,7 @@ export default class AllocationsController extends Controller.extend(
|
||||||
qpTaskGroup = '';
|
qpTaskGroup = '';
|
||||||
currentPage = 1;
|
currentPage = 1;
|
||||||
pageSize = 25;
|
pageSize = 25;
|
||||||
|
activeTask = null;
|
||||||
|
|
||||||
sortProperty = 'modifyIndex';
|
sortProperty = 'modifyIndex';
|
||||||
sortDescending = true;
|
sortDescending = true;
|
||||||
|
@ -159,4 +161,13 @@ export default class AllocationsController extends Controller.extend(
|
||||||
setFacetQueryParam(queryParam, selection) {
|
setFacetQueryParam(queryParam, selection) {
|
||||||
this.set(queryParam, serialize(selection));
|
this.set(queryParam, serialize(selection));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
setActiveTaskQueryParam(task) {
|
||||||
|
if (task) {
|
||||||
|
this.set('activeTask', `${task.allocation.id}-${task.name}`);
|
||||||
|
} else {
|
||||||
|
this.set('activeTask', null);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { alias } from '@ember/object/computed';
|
||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
import WithNamespaceResetting from 'nomad-ui/mixins/with-namespace-resetting';
|
import WithNamespaceResetting from 'nomad-ui/mixins/with-namespace-resetting';
|
||||||
import classic from 'ember-classic-decorator';
|
import classic from 'ember-classic-decorator';
|
||||||
|
import { action } from '@ember/object';
|
||||||
@classic
|
@classic
|
||||||
export default class IndexController extends Controller.extend(
|
export default class IndexController extends Controller.extend(
|
||||||
WithNamespaceResetting
|
WithNamespaceResetting
|
||||||
|
@ -20,6 +20,7 @@ export default class IndexController extends Controller.extend(
|
||||||
{
|
{
|
||||||
sortDescending: 'desc',
|
sortDescending: 'desc',
|
||||||
},
|
},
|
||||||
|
'activeTask',
|
||||||
];
|
];
|
||||||
|
|
||||||
currentPage = 1;
|
currentPage = 1;
|
||||||
|
@ -28,4 +29,14 @@ export default class IndexController extends Controller.extend(
|
||||||
|
|
||||||
sortProperty = 'name';
|
sortProperty = 'name';
|
||||||
sortDescending = false;
|
sortDescending = false;
|
||||||
|
activeTask = null;
|
||||||
|
|
||||||
|
@action
|
||||||
|
setActiveTaskQueryParam(task) {
|
||||||
|
if (task) {
|
||||||
|
this.set('activeTask', `${task.allocation.id}-${task.name}`);
|
||||||
|
} else {
|
||||||
|
this.set('activeTask', null);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,7 @@ export default class TaskGroupController extends Controller.extend(
|
||||||
{
|
{
|
||||||
qpClient: 'client',
|
qpClient: 'client',
|
||||||
},
|
},
|
||||||
|
'activeTask',
|
||||||
];
|
];
|
||||||
|
|
||||||
currentPage = 1;
|
currentPage = 1;
|
||||||
|
@ -52,6 +53,7 @@ export default class TaskGroupController extends Controller.extend(
|
||||||
qpClient = '';
|
qpClient = '';
|
||||||
sortProperty = 'modifyIndex';
|
sortProperty = 'modifyIndex';
|
||||||
sortDescending = true;
|
sortDescending = true;
|
||||||
|
activeTask = null;
|
||||||
|
|
||||||
@computed
|
@computed
|
||||||
get searchProps() {
|
get searchProps() {
|
||||||
|
@ -186,4 +188,13 @@ export default class TaskGroupController extends Controller.extend(
|
||||||
args: ['jobs.job.task-group', job, name],
|
args: ['jobs.job.task-group', job, name],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
setActiveTaskQueryParam(task) {
|
||||||
|
if (task) {
|
||||||
|
this.set('activeTask', `${task.allocation.id}-${task.name}`);
|
||||||
|
} else {
|
||||||
|
this.set('activeTask', null);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,9 @@
|
||||||
.is-padded {
|
.is-padded {
|
||||||
padding: 0em 0em 0em 1em;
|
padding: 0em 0em 0em 1em;
|
||||||
}
|
}
|
||||||
|
.is-one-line {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-fixed-width {
|
.is-fixed-width {
|
||||||
|
|
|
@ -56,3 +56,78 @@ $subNavOffset: 49px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
width: 100px;
|
width: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.task-context-sidebar {
|
||||||
|
animation-name: slideFromRight;
|
||||||
|
animation-duration: 150ms;
|
||||||
|
animation-fill-mode: both;
|
||||||
|
|
||||||
|
header {
|
||||||
|
display: grid;
|
||||||
|
justify-content: left;
|
||||||
|
grid-template-columns: 1fr auto auto;
|
||||||
|
gap: 2rem;
|
||||||
|
border-bottom: 1px solid $grey-blue;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
height: 50px;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin-bottom: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: normal;
|
||||||
|
margin-left: 1rem;
|
||||||
|
text-transform: capitalize;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
height: 1rem;
|
||||||
|
width: 1rem;
|
||||||
|
margin-right: 5px;
|
||||||
|
border-radius: 4px;
|
||||||
|
position: relative;
|
||||||
|
top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.running:before {
|
||||||
|
background-color: $green;
|
||||||
|
}
|
||||||
|
&.dead:before {
|
||||||
|
background-color: $red;
|
||||||
|
}
|
||||||
|
&.pending:before {
|
||||||
|
background-color: $grey-lighter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instead of trying to calculate on the fly with JS, let's use vh and offset nav and headers above.
|
||||||
|
// We can make this a LOT more streamlined when CSS Container Queries are available.
|
||||||
|
$sidebarTopOffset: 161px;
|
||||||
|
$sidebarInnerPadding: 48px;
|
||||||
|
$sidebarHeaderOffset: 74px;
|
||||||
|
$cliHeaderOffset: 54.5px;
|
||||||
|
.cli-window {
|
||||||
|
height: calc(
|
||||||
|
100vh - $sidebarTopOffset - $sidebarInnerPadding - $sidebarHeaderOffset -
|
||||||
|
$cliHeaderOffset
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideFromRight {
|
||||||
|
from {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,16 +1,39 @@
|
||||||
table tbody .task-sub-row {
|
table tbody .task-sub-row {
|
||||||
td {
|
td {
|
||||||
border-top: 2px solid white;
|
border-top: 2px solid white;
|
||||||
|
|
||||||
|
.name-grid {
|
||||||
|
display: inline-grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
margin-left: 4rem;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
.task-name {
|
||||||
|
display: block;
|
||||||
|
width: 150px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
&:before {
|
||||||
|
color: black;
|
||||||
|
content: '/';
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
td:nth-child(1) {
|
|
||||||
padding-left: 4rem;
|
|
||||||
a {
|
|
||||||
margin-right: 1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
svg.flight-icon {
|
.logs-sidebar-trigger {
|
||||||
|
color: $blue;
|
||||||
|
text-decoration: underline;
|
||||||
|
font-weight: normal;
|
||||||
|
svg {
|
||||||
|
color: black;
|
||||||
|
margin-right: 5px;
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 3px;
|
top: 3px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -31,7 +31,9 @@ $size: 12px;
|
||||||
.toggler {
|
.toggler {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: relative;
|
position: relative;
|
||||||
vertical-align: middle;
|
vertical-align: baseline;
|
||||||
|
position: relative;
|
||||||
|
top: 1px;
|
||||||
width: $size * 2;
|
width: $size * 2;
|
||||||
height: $size;
|
height: $size;
|
||||||
border-radius: $size;
|
border-radius: $size;
|
||||||
|
|
|
@ -26,6 +26,8 @@
|
||||||
|
|
||||||
<KeyboardShortcutsModal />
|
<KeyboardShortcutsModal />
|
||||||
|
|
||||||
|
<PortalTarget @name="log-sidebar-portal" />
|
||||||
|
|
||||||
{{#if this.error}}
|
{{#if this.error}}
|
||||||
<div class="error-container">
|
<div class="error-container">
|
||||||
<div data-test-error class="error-message">
|
<div data-test-error class="error-message">
|
||||||
|
|
|
@ -497,9 +497,8 @@
|
||||||
@class="is-padded"
|
@class="is-padded"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span class="is-padded">
|
<span class="is-padded is-one-line">
|
||||||
<Toggle
|
<Toggle
|
||||||
class="button is-borderless is-inline"
|
|
||||||
@isActive={{this.showSubTasks}}
|
@isActive={{this.showSubTasks}}
|
||||||
@onToggle={{this.toggleShowSubTasks}}
|
@onToggle={{this.toggleShowSubTasks}}
|
||||||
title="Show tasks of allocations"
|
title="Show tasks of allocations"
|
||||||
|
@ -568,7 +567,7 @@
|
||||||
/>
|
/>
|
||||||
{{#if this.showSubTasks}}
|
{{#if this.showSubTasks}}
|
||||||
{{#each row.model.states as |task|}}
|
{{#each row.model.states as |task|}}
|
||||||
<TaskSubRow @namespan="8" @taskState={{task}} />
|
<TaskSubRow @namespan="8" @taskState={{task}} @active={{eq this.activeTask (concat task.allocation.id "-" task.name)}} @onSetActiveTask={{action 'setActiveTaskQueryParam'}} />
|
||||||
{{/each}}
|
{{/each}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</t.body>
|
</t.body>
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
"job-page/parts/latest-deployment" job=@job handleError=this.handleError
|
"job-page/parts/latest-deployment" job=@job handleError=this.handleError
|
||||||
)
|
)
|
||||||
TaskGroups=(component "job-page/parts/task-groups" job=@job)
|
TaskGroups=(component "job-page/parts/task-groups" job=@job)
|
||||||
RecentAllocations=(component "job-page/parts/recent-allocations" job=@job)
|
RecentAllocations=(component "job-page/parts/recent-allocations" job=@job activeTask=@activeTask setActiveTaskQueryParam=@setActiveTaskQueryParam)
|
||||||
Meta=(component "job-page/parts/meta" job=@job)
|
Meta=(component "job-page/parts/meta" job=@job)
|
||||||
DasRecommendations=(component
|
DasRecommendations=(component
|
||||||
"job-page/parts/das-recommendations" job=@job
|
"job-page/parts/das-recommendations" job=@job
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<jobPage.ui.Summary />
|
<jobPage.ui.Summary />
|
||||||
<jobPage.ui.PlacementFailures />
|
<jobPage.ui.PlacementFailures />
|
||||||
<jobPage.ui.TaskGroups @sortProperty={{@sortProperty}} @sortDescending={{@sortDescending}} />
|
<jobPage.ui.TaskGroups @sortProperty={{@sortProperty}} @sortDescending={{@sortDescending}} />
|
||||||
<jobPage.ui.RecentAllocations />
|
<jobPage.ui.RecentAllocations @activeTask={{@activeTask}} @setActiveTaskQueryParam={{@setActiveTaskQueryParam}} />
|
||||||
<jobPage.ui.Meta />
|
<jobPage.ui.Meta />
|
||||||
</jobPage.ui.Body>
|
</jobPage.ui.Body>
|
||||||
</JobPage>
|
</JobPage>
|
|
@ -21,7 +21,7 @@
|
||||||
<jobPage.ui.Summary @forceCollapsed={{@job.hasClientStatus}} />
|
<jobPage.ui.Summary @forceCollapsed={{@job.hasClientStatus}} />
|
||||||
<jobPage.ui.PlacementFailures />
|
<jobPage.ui.PlacementFailures />
|
||||||
<jobPage.ui.TaskGroups @sortProperty={{@sortProperty}} @sortDescending={{@sortDescending}} />
|
<jobPage.ui.TaskGroups @sortProperty={{@sortProperty}} @sortDescending={{@sortDescending}} />
|
||||||
<jobPage.ui.RecentAllocations />
|
<jobPage.ui.RecentAllocations @activeTask={{@activeTask}} @setActiveTaskQueryParam={{@setActiveTaskQueryParam}} />
|
||||||
<div class="boxed-section">
|
<div class="boxed-section">
|
||||||
{{#if @job.meta}}
|
{{#if @job.meta}}
|
||||||
<jobPage.ui.Meta />
|
<jobPage.ui.Meta />
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
Recent Allocations
|
Recent Allocations
|
||||||
<span class="pull-right is-padded">
|
<span class="pull-right is-padded">
|
||||||
<Toggle
|
<Toggle
|
||||||
class="button is-borderless is-inline"
|
|
||||||
@isActive={{this.showSubTasks}}
|
@isActive={{this.showSubTasks}}
|
||||||
@onToggle={{this.toggleShowSubTasks}}
|
@onToggle={{this.toggleShowSubTasks}}
|
||||||
title="Show tasks of allocations"
|
title="Show tasks of allocations"
|
||||||
|
@ -69,7 +68,7 @@
|
||||||
|
|
||||||
{{#if this.showSubTasks}}
|
{{#if this.showSubTasks}}
|
||||||
{{#each row.model.states as |task|}}
|
{{#each row.model.states as |task|}}
|
||||||
<TaskSubRow @namespan="9" @taskState={{task}} />
|
<TaskSubRow @namespan="9" @taskState={{task}} @active={{eq @activeTask (concat task.allocation.id "-" task.name)}} @onSetActiveTask={{@setActiveTaskQueryParam}} />
|
||||||
{{/each}}
|
{{/each}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</t.body>
|
</t.body>
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
<jobPage.ui.Summary @forceCollapsed={{@job.hasClientStatus}} />
|
<jobPage.ui.Summary @forceCollapsed={{@job.hasClientStatus}} />
|
||||||
<jobPage.ui.PlacementFailures />
|
<jobPage.ui.PlacementFailures />
|
||||||
<jobPage.ui.TaskGroups @sortProperty={{@sortProperty}} @sortDescending={{@sortDescending}} />
|
<jobPage.ui.TaskGroups @sortProperty={{@sortProperty}} @sortDescending={{@sortDescending}} />
|
||||||
<jobPage.ui.RecentAllocations />
|
<jobPage.ui.RecentAllocations @activeTask={{@activeTask}} @setActiveTaskQueryParam={{@setActiveTaskQueryParam}} />
|
||||||
<jobPage.ui.Meta />
|
<jobPage.ui.Meta />
|
||||||
</jobPage.ui.Body>
|
</jobPage.ui.Body>
|
||||||
</JobPage>
|
</JobPage>
|
|
@ -8,7 +8,7 @@
|
||||||
<jobPage.ui.PlacementFailures />
|
<jobPage.ui.PlacementFailures />
|
||||||
<jobPage.ui.LatestDeployment />
|
<jobPage.ui.LatestDeployment />
|
||||||
<jobPage.ui.TaskGroups @sortProperty={{@sortProperty}} @sortDescending={{@sortDescending}} />
|
<jobPage.ui.TaskGroups @sortProperty={{@sortProperty}} @sortDescending={{@sortDescending}} />
|
||||||
<jobPage.ui.RecentAllocations />
|
<jobPage.ui.RecentAllocations @activeTask={{@activeTask}} @setActiveTaskQueryParam={{@setActiveTaskQueryParam}} />
|
||||||
<jobPage.ui.Meta />
|
<jobPage.ui.Meta />
|
||||||
</jobPage.ui.Body>
|
</jobPage.ui.Body>
|
||||||
</JobPage>
|
</JobPage>
|
|
@ -7,7 +7,7 @@
|
||||||
<jobPage.ui.Summary @forceCollapsed="true" />
|
<jobPage.ui.Summary @forceCollapsed="true" />
|
||||||
<jobPage.ui.PlacementFailures />
|
<jobPage.ui.PlacementFailures />
|
||||||
<jobPage.ui.TaskGroups @sortProperty={{@sortProperty}} @sortDescending={{@sortDescending}} />
|
<jobPage.ui.TaskGroups @sortProperty={{@sortProperty}} @sortDescending={{@sortDescending}} />
|
||||||
<jobPage.ui.RecentAllocations />
|
<jobPage.ui.RecentAllocations @activeTask={{@activeTask}} @setActiveTaskQueryParam={{@setActiveTaskQueryParam}} />
|
||||||
<jobPage.ui.Meta />
|
<jobPage.ui.Meta />
|
||||||
</jobPage.ui.Body>
|
</jobPage.ui.Body>
|
||||||
</JobPage>
|
</JobPage>
|
|
@ -8,7 +8,7 @@
|
||||||
<jobPage.ui.Summary @forceCollapsed="true" />
|
<jobPage.ui.Summary @forceCollapsed="true" />
|
||||||
<jobPage.ui.PlacementFailures />
|
<jobPage.ui.PlacementFailures />
|
||||||
<jobPage.ui.TaskGroups @sortProperty={{@sortProperty}} @sortDescending={{@sortDescending}} />
|
<jobPage.ui.TaskGroups @sortProperty={{@sortProperty}} @sortDescending={{@sortDescending}} />
|
||||||
<jobPage.ui.RecentAllocations />
|
<jobPage.ui.RecentAllocations @activeTask={{@activeTask}} @setActiveTaskQueryParam={{@setActiveTaskQueryParam}} />
|
||||||
<jobPage.ui.Meta />
|
<jobPage.ui.Meta />
|
||||||
</jobPage.ui.Body>
|
</jobPage.ui.Body>
|
||||||
</JobPage>
|
</JobPage>
|
|
@ -25,5 +25,5 @@
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div data-test-log-box class="boxed-section-body is-dark is-full-bleed">
|
<div data-test-log-box class="boxed-section-body is-dark is-full-bleed">
|
||||||
<StreamingFile @logger={{this.logger}} @mode={{this.streamMode}} @isStreaming={{this.isStreaming}} />
|
<StreamingFile @logger={{this.logger}} @mode={{this.streamMode}} @isStreaming={{this.isStreaming}} @shouldFillHeight={{this.shouldFillHeight}} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -71,7 +71,7 @@
|
||||||
@context="job"
|
@context="job"
|
||||||
@onClick={{action "gotoAllocation" row.model}} />
|
@onClick={{action "gotoAllocation" row.model}} />
|
||||||
{{#each row.model.states as |task|}}
|
{{#each row.model.states as |task|}}
|
||||||
<TaskSubRow @namespan="9" @taskState={{task}} />
|
<TaskSubRow @active={{eq this.activeTask (concat task.allocation.id "-" task.name)}} @onSetActiveTask={{action 'setActiveTaskQueryParam'}} @namespan="9" @taskState={{task}} />
|
||||||
{{/each}}
|
{{/each}}
|
||||||
|
|
||||||
</t.body>
|
</t.body>
|
||||||
|
|
|
@ -5,4 +5,6 @@
|
||||||
sortProperty=this.sortProperty
|
sortProperty=this.sortProperty
|
||||||
sortDescending=this.sortDescending
|
sortDescending=this.sortDescending
|
||||||
currentPage=this.currentPage
|
currentPage=this.currentPage
|
||||||
|
activeTask=this.activeTask
|
||||||
|
setActiveTaskQueryParam=this.setActiveTaskQueryParam
|
||||||
}}
|
}}
|
|
@ -149,9 +149,8 @@
|
||||||
@class="is-padded"
|
@class="is-padded"
|
||||||
@inputClass="is-compact"
|
@inputClass="is-compact"
|
||||||
/>
|
/>
|
||||||
<span class="is-padded">
|
<span class="is-padded is-one-line">
|
||||||
<Toggle
|
<Toggle
|
||||||
class="button is-borderless is-inline"
|
|
||||||
@isActive={{this.showSubTasks}}
|
@isActive={{this.showSubTasks}}
|
||||||
@onToggle={{this.toggleShowSubTasks}}
|
@onToggle={{this.toggleShowSubTasks}}
|
||||||
title="Show tasks of allocations"
|
title="Show tasks of allocations"
|
||||||
|
@ -218,7 +217,7 @@
|
||||||
/>
|
/>
|
||||||
{{#if this.showSubTasks}}
|
{{#if this.showSubTasks}}
|
||||||
{{#each row.model.states as |task|}}
|
{{#each row.model.states as |task|}}
|
||||||
<TaskSubRow @namespan="8" @taskState={{task}} />
|
<TaskSubRow @namespan="8" @taskState={{task}} @active={{eq this.activeTask (concat task.allocation.id "-" task.name)}} @onSetActiveTask={{action 'setActiveTaskQueryParam'}} />
|
||||||
{{/each}}
|
{{/each}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</t.body>
|
</t.body>
|
||||||
|
|
|
@ -1,23 +1,27 @@
|
||||||
/* eslint-disable qunit/require-expect */
|
/* eslint-disable qunit/require-expect */
|
||||||
import { currentURL } from '@ember/test-helpers';
|
import { click, currentURL } from '@ember/test-helpers';
|
||||||
import { run } from '@ember/runloop';
|
import { run } from '@ember/runloop';
|
||||||
import { module, test } from 'qunit';
|
import { module, test } from 'qunit';
|
||||||
import { setupApplicationTest } from 'ember-qunit';
|
import { setupApplicationTest } from 'ember-qunit';
|
||||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||||
import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit';
|
import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit';
|
||||||
import TaskLogs from 'nomad-ui/tests/pages/allocations/task/logs';
|
import TaskLogs from 'nomad-ui/tests/pages/allocations/task/logs';
|
||||||
|
import percySnapshot from '@percy/ember';
|
||||||
|
import faker from 'nomad-ui/mirage/faker';
|
||||||
|
|
||||||
let allocation;
|
let allocation;
|
||||||
let task;
|
let task;
|
||||||
|
let job;
|
||||||
|
|
||||||
module('Acceptance | task logs', function (hooks) {
|
module('Acceptance | task logs', function (hooks) {
|
||||||
setupApplicationTest(hooks);
|
setupApplicationTest(hooks);
|
||||||
setupMirage(hooks);
|
setupMirage(hooks);
|
||||||
|
|
||||||
hooks.beforeEach(async function () {
|
hooks.beforeEach(async function () {
|
||||||
|
faker.seed(1);
|
||||||
server.create('agent');
|
server.create('agent');
|
||||||
server.create('node', 'forceIPv4');
|
server.create('node', 'forceIPv4');
|
||||||
const job = server.create('job', { createAllocations: false });
|
job = server.create('job', { createAllocations: false });
|
||||||
|
|
||||||
allocation = server.create('allocation', {
|
allocation = server.create('allocation', {
|
||||||
jobId: job.id,
|
jobId: job.id,
|
||||||
|
@ -26,14 +30,15 @@ module('Acceptance | task logs', function (hooks) {
|
||||||
task = server.db.taskStates.where({ allocationId: allocation.id })[0];
|
task = server.db.taskStates.where({ allocationId: allocation.id })[0];
|
||||||
|
|
||||||
run.later(run, run.cancelTimers, 1000);
|
run.later(run, run.cancelTimers, 1000);
|
||||||
await TaskLogs.visit({ id: allocation.id, name: task.name });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it passes an accessibility audit', async function (assert) {
|
test('it passes an accessibility audit', async function (assert) {
|
||||||
|
await TaskLogs.visit({ id: allocation.id, name: task.name });
|
||||||
await a11yAudit(assert);
|
await a11yAudit(assert);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('/allocation/:id/:task_name/logs should have a log component', async function (assert) {
|
test('/allocation/:id/:task_name/logs should have a log component', async function (assert) {
|
||||||
|
await TaskLogs.visit({ id: allocation.id, name: task.name });
|
||||||
assert.equal(
|
assert.equal(
|
||||||
currentURL(),
|
currentURL(),
|
||||||
`/allocations/${allocation.id}/${task.name}/logs`,
|
`/allocations/${allocation.id}/${task.name}/logs`,
|
||||||
|
@ -44,6 +49,7 @@ module('Acceptance | task logs', function (hooks) {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the stdout log immediately starts streaming', async function (assert) {
|
test('the stdout log immediately starts streaming', async function (assert) {
|
||||||
|
await TaskLogs.visit({ id: allocation.id, name: task.name });
|
||||||
const node = server.db.nodes.find(allocation.nodeId);
|
const node = server.db.nodes.find(allocation.nodeId);
|
||||||
const logUrlRegex = new RegExp(
|
const logUrlRegex = new RegExp(
|
||||||
`${node.httpAddr}/v1/client/fs/logs/${allocation.id}`
|
`${node.httpAddr}/v1/client/fs/logs/${allocation.id}`
|
||||||
|
@ -55,4 +61,31 @@ module('Acceptance | task logs', function (hooks) {
|
||||||
'Log requests were made'
|
'Log requests were made'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('logs are accessible in a sidebar context', async function (assert) {
|
||||||
|
await TaskLogs.visitParentJob({
|
||||||
|
id: job.id,
|
||||||
|
allocationId: allocation.id,
|
||||||
|
name: task.name,
|
||||||
|
});
|
||||||
|
assert.notOk(TaskLogs.sidebarIsPresent, 'Sidebar is not present');
|
||||||
|
|
||||||
|
run.later(() => {
|
||||||
|
run.cancelTimers();
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
await click('button.logs-sidebar-trigger');
|
||||||
|
|
||||||
|
assert.ok(TaskLogs.sidebarIsPresent, 'Sidebar is present');
|
||||||
|
assert
|
||||||
|
.dom('.task-context-sidebar h1.title')
|
||||||
|
.includesText(task.name, 'Sidebar title is correct');
|
||||||
|
assert
|
||||||
|
.dom('.task-context-sidebar h1.title')
|
||||||
|
.includesText(task.state, 'Task state is correctly displayed');
|
||||||
|
await percySnapshot(assert);
|
||||||
|
|
||||||
|
await click('.sidebar button.close');
|
||||||
|
assert.notOk(TaskLogs.sidebarIsPresent, 'Sidebar is not present');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -51,10 +51,28 @@ const mockTask = {
|
||||||
module('Integration | Component | task-sub-row', function (hooks) {
|
module('Integration | Component | task-sub-row', function (hooks) {
|
||||||
setupRenderingTest(hooks);
|
setupRenderingTest(hooks);
|
||||||
test('it renders', async function (assert) {
|
test('it renders', async function (assert) {
|
||||||
assert.expect(2);
|
assert.expect(6);
|
||||||
this.set('task', mockTask);
|
this.set('task', mockTask);
|
||||||
await render(hbs`<TaskSubRow @taskState={{this.task}} />`);
|
await render(hbs`<TaskSubRow @taskState={{this.task}} />`);
|
||||||
assert.dom(this.element).hasText(`/ ${mockTask.name}`);
|
assert.ok(
|
||||||
|
this.element.textContent.includes(`${mockTask.name}`),
|
||||||
|
'Task name is rendered'
|
||||||
|
);
|
||||||
|
assert.dom('.task-sub-row').doesNotHaveClass('is-active');
|
||||||
|
|
||||||
|
await render(hbs`<TaskSubRow @taskState={{this.task}} @active={{true}} />`);
|
||||||
|
assert.dom('.task-sub-row').hasClass('is-active');
|
||||||
|
|
||||||
|
await render(
|
||||||
|
hbs`<TaskSubRow @taskState={{this.task}} @active={{true}} @namespan={{5}} />`
|
||||||
|
);
|
||||||
|
assert.dom('.task-sub-row td:nth-child(1)').hasAttribute('colspan', '5');
|
||||||
|
|
||||||
|
await render(
|
||||||
|
hbs`<TaskSubRow @taskState={{this.task}} @active={{true}} @namespan={{9}} />`
|
||||||
|
);
|
||||||
|
assert.dom('.task-sub-row td:nth-child(1)').hasAttribute('colspan', '9');
|
||||||
|
|
||||||
await componentA11yAudit(this.element, assert);
|
await componentA11yAudit(this.element, assert);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,6 +2,8 @@ import { create, isPresent, visitable } from 'ember-cli-page-object';
|
||||||
|
|
||||||
export default create({
|
export default create({
|
||||||
visit: visitable('/allocations/:id/:name/logs'),
|
visit: visitable('/allocations/:id/:name/logs'),
|
||||||
|
visitParentJob: visitable('/jobs/:id/allocations'),
|
||||||
|
|
||||||
hasTaskLog: isPresent('[data-test-task-log]'),
|
hasTaskLog: isPresent('[data-test-task-log]'),
|
||||||
|
sidebarIsPresent: isPresent('.sidebar.task-context-sidebar'),
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue