[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
|
@ -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 = [
|
||||
{
|
||||
label: 'Close Evaluations Sidebar',
|
||||
label: 'Close Service Sidebar',
|
||||
pattern: ['Escape'],
|
||||
action: () => this.args.fns.closeSidebar(),
|
||||
},
|
||||
|
|
|
@ -22,6 +22,7 @@ export default class StreamingFile extends Component.extend(WindowResizable) {
|
|||
isStreaming = true;
|
||||
logger = null;
|
||||
follow = true;
|
||||
shouldFillHeight = true;
|
||||
|
||||
// Internal bookkeeping to avoid multiple scroll events on one frame
|
||||
requestFrame = true;
|
||||
|
@ -89,7 +90,9 @@ export default class StreamingFile extends Component.extend(WindowResizable) {
|
|||
|
||||
didInsertElement() {
|
||||
super.didInsertElement(...arguments);
|
||||
if (this.shouldFillHeight) {
|
||||
this.fillAvailableHeight();
|
||||
}
|
||||
|
||||
this.set('_scrollHandler', this.scrollHandler.bind(this));
|
||||
this.element.addEventListener('scroll', this._scrollHandler);
|
||||
|
@ -105,8 +108,10 @@ export default class StreamingFile extends Component.extend(WindowResizable) {
|
|||
}
|
||||
|
||||
windowResizeHandler() {
|
||||
if (this.shouldFillHeight) {
|
||||
once(this, this.fillAvailableHeight);
|
||||
}
|
||||
}
|
||||
|
||||
fillAvailableHeight() {
|
||||
// This math is arbitrary and far from bulletproof, but the UX
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
streamMode = 'streaming';
|
||||
|
||||
shouldFillHeight = true;
|
||||
|
||||
@alias('userSettings.logMode') mode;
|
||||
|
||||
@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
|
||||
enumerated=true
|
||||
action=(action "gotoTask" this.task.allocation this.task)
|
||||
}}
|
||||
>
|
||||
<td colspan={{@namespan}}>
|
||||
/
|
||||
<LinkTo @route="allocations.allocation.task" @models={{array this.task.allocation this.task}}>
|
||||
{{this.task.name}}
|
||||
</LinkTo>
|
||||
{{!-- TODO: in-page logs --}}
|
||||
{{!-- <FlightIcon @name="logs" /> --}}
|
||||
<div class="name-grid">
|
||||
<LinkTo title={{this.task.name}} class="task-name" @route="allocations.allocation.task" @models={{array this.task.allocation this.task}}>{{this.task.name}}</LinkTo>
|
||||
<button type="button" class="logs-sidebar-trigger button is-borderless is-inline is-compact" onclick={{action "handleTaskLogsClick" this.task}}>
|
||||
<FlightIcon @name="logs" />View Logs
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td data-test-cpu class="is-1 has-text-centered">
|
||||
{{#if this.task.isRunning}}
|
||||
|
@ -77,3 +77,12 @@
|
|||
</tr>
|
||||
|
||||
{{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);
|
||||
}).drop())
|
||||
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',
|
||||
},
|
||||
'activeTask',
|
||||
];
|
||||
|
||||
// Set in the route
|
||||
|
@ -57,6 +58,7 @@ export default class ClientController extends Controller.extend(
|
|||
qpStatus = '';
|
||||
currentPage = 1;
|
||||
pageSize = 8;
|
||||
activeTask = null;
|
||||
|
||||
sortProperty = 'modifyIndex';
|
||||
sortDescending = true;
|
||||
|
@ -266,4 +268,13 @@ export default class ClientController extends Controller.extend(
|
|||
setFacetQueryParam(queryParam, 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',
|
||||
},
|
||||
'activeTask',
|
||||
];
|
||||
|
||||
qpStatus = '';
|
||||
|
@ -48,6 +49,7 @@ export default class AllocationsController extends Controller.extend(
|
|||
qpTaskGroup = '';
|
||||
currentPage = 1;
|
||||
pageSize = 25;
|
||||
activeTask = null;
|
||||
|
||||
sortProperty = 'modifyIndex';
|
||||
sortDescending = true;
|
||||
|
@ -159,4 +161,13 @@ export default class AllocationsController extends Controller.extend(
|
|||
setFacetQueryParam(queryParam, 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 WithNamespaceResetting from 'nomad-ui/mixins/with-namespace-resetting';
|
||||
import classic from 'ember-classic-decorator';
|
||||
|
||||
import { action } from '@ember/object';
|
||||
@classic
|
||||
export default class IndexController extends Controller.extend(
|
||||
WithNamespaceResetting
|
||||
|
@ -20,6 +20,7 @@ export default class IndexController extends Controller.extend(
|
|||
{
|
||||
sortDescending: 'desc',
|
||||
},
|
||||
'activeTask',
|
||||
];
|
||||
|
||||
currentPage = 1;
|
||||
|
@ -28,4 +29,14 @@ export default class IndexController extends Controller.extend(
|
|||
|
||||
sortProperty = 'name';
|
||||
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',
|
||||
},
|
||||
'activeTask',
|
||||
];
|
||||
|
||||
currentPage = 1;
|
||||
|
@ -52,6 +53,7 @@ export default class TaskGroupController extends Controller.extend(
|
|||
qpClient = '';
|
||||
sortProperty = 'modifyIndex';
|
||||
sortDescending = true;
|
||||
activeTask = null;
|
||||
|
||||
@computed
|
||||
get searchProps() {
|
||||
|
@ -186,4 +188,13 @@ export default class TaskGroupController extends Controller.extend(
|
|||
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 {
|
||||
padding: 0em 0em 0em 1em;
|
||||
}
|
||||
.is-one-line {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.is-fixed-width {
|
||||
|
|
|
@ -56,3 +56,78 @@ $subNavOffset: 49px;
|
|||
padding: 10px;
|
||||
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 {
|
||||
td {
|
||||
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;
|
||||
top: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,7 +31,9 @@ $size: 12px;
|
|||
.toggler {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
vertical-align: baseline;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
width: $size * 2;
|
||||
height: $size;
|
||||
border-radius: $size;
|
||||
|
|
|
@ -26,6 +26,8 @@
|
|||
|
||||
<KeyboardShortcutsModal />
|
||||
|
||||
<PortalTarget @name="log-sidebar-portal" />
|
||||
|
||||
{{#if this.error}}
|
||||
<div class="error-container">
|
||||
<div data-test-error class="error-message">
|
||||
|
|
|
@ -497,9 +497,8 @@
|
|||
@class="is-padded"
|
||||
/>
|
||||
|
||||
<span class="is-padded">
|
||||
<span class="is-padded is-one-line">
|
||||
<Toggle
|
||||
class="button is-borderless is-inline"
|
||||
@isActive={{this.showSubTasks}}
|
||||
@onToggle={{this.toggleShowSubTasks}}
|
||||
title="Show tasks of allocations"
|
||||
|
@ -568,7 +567,7 @@
|
|||
/>
|
||||
{{#if this.showSubTasks}}
|
||||
{{#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}}
|
||||
{{/if}}
|
||||
</t.body>
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
"job-page/parts/latest-deployment" job=@job handleError=this.handleError
|
||||
)
|
||||
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)
|
||||
DasRecommendations=(component
|
||||
"job-page/parts/das-recommendations" job=@job
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<jobPage.ui.Summary />
|
||||
<jobPage.ui.PlacementFailures />
|
||||
<jobPage.ui.TaskGroups @sortProperty={{@sortProperty}} @sortDescending={{@sortDescending}} />
|
||||
<jobPage.ui.RecentAllocations />
|
||||
<jobPage.ui.RecentAllocations @activeTask={{@activeTask}} @setActiveTaskQueryParam={{@setActiveTaskQueryParam}} />
|
||||
<jobPage.ui.Meta />
|
||||
</jobPage.ui.Body>
|
||||
</JobPage>
|
|
@ -21,7 +21,7 @@
|
|||
<jobPage.ui.Summary @forceCollapsed={{@job.hasClientStatus}} />
|
||||
<jobPage.ui.PlacementFailures />
|
||||
<jobPage.ui.TaskGroups @sortProperty={{@sortProperty}} @sortDescending={{@sortDescending}} />
|
||||
<jobPage.ui.RecentAllocations />
|
||||
<jobPage.ui.RecentAllocations @activeTask={{@activeTask}} @setActiveTaskQueryParam={{@setActiveTaskQueryParam}} />
|
||||
<div class="boxed-section">
|
||||
{{#if @job.meta}}
|
||||
<jobPage.ui.Meta />
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
Recent Allocations
|
||||
<span class="pull-right is-padded">
|
||||
<Toggle
|
||||
class="button is-borderless is-inline"
|
||||
@isActive={{this.showSubTasks}}
|
||||
@onToggle={{this.toggleShowSubTasks}}
|
||||
title="Show tasks of allocations"
|
||||
|
@ -69,7 +68,7 @@
|
|||
|
||||
{{#if this.showSubTasks}}
|
||||
{{#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}}
|
||||
{{/if}}
|
||||
</t.body>
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
<jobPage.ui.Summary @forceCollapsed={{@job.hasClientStatus}} />
|
||||
<jobPage.ui.PlacementFailures />
|
||||
<jobPage.ui.TaskGroups @sortProperty={{@sortProperty}} @sortDescending={{@sortDescending}} />
|
||||
<jobPage.ui.RecentAllocations />
|
||||
<jobPage.ui.RecentAllocations @activeTask={{@activeTask}} @setActiveTaskQueryParam={{@setActiveTaskQueryParam}} />
|
||||
<jobPage.ui.Meta />
|
||||
</jobPage.ui.Body>
|
||||
</JobPage>
|
|
@ -8,7 +8,7 @@
|
|||
<jobPage.ui.PlacementFailures />
|
||||
<jobPage.ui.LatestDeployment />
|
||||
<jobPage.ui.TaskGroups @sortProperty={{@sortProperty}} @sortDescending={{@sortDescending}} />
|
||||
<jobPage.ui.RecentAllocations />
|
||||
<jobPage.ui.RecentAllocations @activeTask={{@activeTask}} @setActiveTaskQueryParam={{@setActiveTaskQueryParam}} />
|
||||
<jobPage.ui.Meta />
|
||||
</jobPage.ui.Body>
|
||||
</JobPage>
|
|
@ -7,7 +7,7 @@
|
|||
<jobPage.ui.Summary @forceCollapsed="true" />
|
||||
<jobPage.ui.PlacementFailures />
|
||||
<jobPage.ui.TaskGroups @sortProperty={{@sortProperty}} @sortDescending={{@sortDescending}} />
|
||||
<jobPage.ui.RecentAllocations />
|
||||
<jobPage.ui.RecentAllocations @activeTask={{@activeTask}} @setActiveTaskQueryParam={{@setActiveTaskQueryParam}} />
|
||||
<jobPage.ui.Meta />
|
||||
</jobPage.ui.Body>
|
||||
</JobPage>
|
|
@ -8,7 +8,7 @@
|
|||
<jobPage.ui.Summary @forceCollapsed="true" />
|
||||
<jobPage.ui.PlacementFailures />
|
||||
<jobPage.ui.TaskGroups @sortProperty={{@sortProperty}} @sortDescending={{@sortDescending}} />
|
||||
<jobPage.ui.RecentAllocations />
|
||||
<jobPage.ui.RecentAllocations @activeTask={{@activeTask}} @setActiveTaskQueryParam={{@setActiveTaskQueryParam}} />
|
||||
<jobPage.ui.Meta />
|
||||
</jobPage.ui.Body>
|
||||
</JobPage>
|
|
@ -25,5 +25,5 @@
|
|||
</span>
|
||||
</div>
|
||||
<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>
|
||||
|
|
|
@ -71,7 +71,7 @@
|
|||
@context="job"
|
||||
@onClick={{action "gotoAllocation" row.model}} />
|
||||
{{#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}}
|
||||
|
||||
</t.body>
|
||||
|
|
|
@ -5,4 +5,6 @@
|
|||
sortProperty=this.sortProperty
|
||||
sortDescending=this.sortDescending
|
||||
currentPage=this.currentPage
|
||||
activeTask=this.activeTask
|
||||
setActiveTaskQueryParam=this.setActiveTaskQueryParam
|
||||
}}
|
|
@ -149,9 +149,8 @@
|
|||
@class="is-padded"
|
||||
@inputClass="is-compact"
|
||||
/>
|
||||
<span class="is-padded">
|
||||
<span class="is-padded is-one-line">
|
||||
<Toggle
|
||||
class="button is-borderless is-inline"
|
||||
@isActive={{this.showSubTasks}}
|
||||
@onToggle={{this.toggleShowSubTasks}}
|
||||
title="Show tasks of allocations"
|
||||
|
@ -218,7 +217,7 @@
|
|||
/>
|
||||
{{#if this.showSubTasks}}
|
||||
{{#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}}
|
||||
{{/if}}
|
||||
</t.body>
|
||||
|
|
|
@ -1,23 +1,27 @@
|
|||
/* eslint-disable qunit/require-expect */
|
||||
import { currentURL } from '@ember/test-helpers';
|
||||
import { click, currentURL } from '@ember/test-helpers';
|
||||
import { run } from '@ember/runloop';
|
||||
import { module, test } from 'qunit';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit';
|
||||
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 task;
|
||||
let job;
|
||||
|
||||
module('Acceptance | task logs', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.beforeEach(async function () {
|
||||
faker.seed(1);
|
||||
server.create('agent');
|
||||
server.create('node', 'forceIPv4');
|
||||
const job = server.create('job', { createAllocations: false });
|
||||
job = server.create('job', { createAllocations: false });
|
||||
|
||||
allocation = server.create('allocation', {
|
||||
jobId: job.id,
|
||||
|
@ -26,14 +30,15 @@ module('Acceptance | task logs', function (hooks) {
|
|||
task = server.db.taskStates.where({ allocationId: allocation.id })[0];
|
||||
|
||||
run.later(run, run.cancelTimers, 1000);
|
||||
await TaskLogs.visit({ id: allocation.id, name: task.name });
|
||||
});
|
||||
|
||||
test('it passes an accessibility audit', async function (assert) {
|
||||
await TaskLogs.visit({ id: allocation.id, name: task.name });
|
||||
await a11yAudit(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(
|
||||
currentURL(),
|
||||
`/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) {
|
||||
await TaskLogs.visit({ id: allocation.id, name: task.name });
|
||||
const node = server.db.nodes.find(allocation.nodeId);
|
||||
const logUrlRegex = new RegExp(
|
||||
`${node.httpAddr}/v1/client/fs/logs/${allocation.id}`
|
||||
|
@ -55,4 +61,31 @@ module('Acceptance | task logs', function (hooks) {
|
|||
'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) {
|
||||
setupRenderingTest(hooks);
|
||||
test('it renders', async function (assert) {
|
||||
assert.expect(2);
|
||||
assert.expect(6);
|
||||
this.set('task', mockTask);
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,6 +2,8 @@ import { create, isPresent, visitable } from 'ember-cli-page-object';
|
|||
|
||||
export default create({
|
||||
visit: visitable('/allocations/:id/:name/logs'),
|
||||
visitParentJob: visitable('/jobs/:id/allocations'),
|
||||
|
||||
hasTaskLog: isPresent('[data-test-task-log]'),
|
||||
sidebarIsPresent: isPresent('.sidebar.task-context-sidebar'),
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue