Skip to content

Commit

Permalink
Merge pull request #3564 from hashicorp/f-ui-log-streaming
Browse files Browse the repository at this point in the history
UI: Log streaming
  • Loading branch information
DingoEatingFuzz committed Nov 29, 2017
2 parents e0d657f + 981c34b commit e369a9a
Show file tree
Hide file tree
Showing 34 changed files with 961 additions and 17 deletions.
2 changes: 1 addition & 1 deletion command/agent/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ func (s *HTTPServer) registerHandlers(enableDebug bool) {
s.mux.HandleFunc("/v1/acl/token", s.wrap(s.ACLTokenSpecificRequest))
s.mux.HandleFunc("/v1/acl/token/", s.wrap(s.ACLTokenSpecificRequest))

s.mux.HandleFunc("/v1/client/fs/", s.wrap(s.FsRequest))
s.mux.Handle("/v1/client/fs/", wrapCORS(s.wrap(s.FsRequest)))
s.mux.HandleFunc("/v1/client/gc", s.wrap(s.ClientGCRequest))
s.mux.Handle("/v1/client/stats", wrapCORS(s.wrap(s.ClientStatsRequest)))
s.mux.Handle("/v1/client/allocation/", wrapCORS(s.wrap(s.ClientAllocRequest)))
Expand Down
6 changes: 6 additions & 0 deletions ui/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,11 @@ module.exports = {
'linebreak-style': ['error', 'unix'],
quotes: ['error', 'single', 'avoid-escape'],
semi: ['error', 'always'],
'no-constant-condition': [
'error',
{
checkLoops: false,
},
],
},
};
8 changes: 3 additions & 5 deletions ui/app/components/job-versions-stream.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,9 @@ export default Component.extend({
meta.showDate = true;
} else {
const previousVersion = versions.objectAt(index - 1);
if (
moment(previousVersion.get('submitTime'))
.startOf('day')
.diff(moment(version.get('submitTime')).startOf('day'), 'days') > 0
) {
const previousStart = moment(previousVersion.get('submitTime')).startOf('day');
const currentStart = moment(version.get('submitTime')).startOf('day');
if (previousStart.diff(currentStart, 'days') > 0) {
meta.showDate = true;
}
}
Expand Down
103 changes: 103 additions & 0 deletions ui/app/components/task-log.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import Ember from 'ember';
import { task } from 'ember-concurrency';
import { logger } from 'nomad-ui/utils/classes/log';
import WindowResizable from 'nomad-ui/mixins/window-resizable';

const { Component, computed, inject, run } = Ember;

export default Component.extend(WindowResizable, {
token: inject.service(),

classNames: ['boxed-section', 'task-log'],

allocation: null,
task: null,

didReceiveAttrs() {
if (this.get('allocation') && this.get('task')) {
this.send('toggleStream');
}
},

didInsertElement() {
this.fillAvailableHeight();
},

windowResizeHandler() {
run.once(this, this.fillAvailableHeight);
},

fillAvailableHeight() {
// This math is arbitrary and far from bulletproof, but the UX
// of having the log window fill available height is worth the hack.
const cliWindow = this.$('.cli-window');
cliWindow.height(window.innerHeight - cliWindow.offset().top - 25);
},

mode: 'stdout',

logUrl: computed('allocation.id', 'allocation.node.httpAddr', function() {
const address = this.get('allocation.node.httpAddr');
const allocation = this.get('allocation.id');

return `//${address}/v1/client/fs/logs/${allocation}`;
}),

logParams: computed('task', 'mode', function() {
return {
task: this.get('task'),
type: this.get('mode'),
};
}),

logger: logger('logUrl', 'logParams', function() {
const token = this.get('token');
return token.authorizedRequest.bind(token);
}),

head: task(function*() {
yield this.get('logger.gotoHead').perform();
run.scheduleOnce('afterRender', () => {
this.$('.cli-window').scrollTop(0);
});
}),

tail: task(function*() {
yield this.get('logger.gotoTail').perform();
run.scheduleOnce('afterRender', () => {
const cliWindow = this.$('.cli-window');
cliWindow.scrollTop(cliWindow[0].scrollHeight);
});
}),

stream: task(function*() {
this.get('logger').on('tick', () => {
run.scheduleOnce('afterRender', () => {
const cliWindow = this.$('.cli-window');
cliWindow.scrollTop(cliWindow[0].scrollHeight);
});
});

yield this.get('logger').startStreaming();
this.get('logger').off('tick');
}),

willDestroy() {
this.get('logger').stop();
},

actions: {
setMode(mode) {
this.get('logger').stop();
this.set('mode', mode);
this.get('stream').perform();
},
toggleStream() {
if (this.get('logger.isStreaming')) {
this.get('logger').stop();
} else {
this.get('stream').perform();
}
},
},
});
4 changes: 2 additions & 2 deletions ui/app/controllers/allocations/allocation/task/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ const { Controller, computed } = Ember;
export default Controller.extend({
network: computed.alias('model.resources.networks.firstObject'),
ports: computed('network.reservedPorts.[]', 'network.dynamicPorts.[]', function() {
return this.get('network.reservedPorts')
return (this.get('network.reservedPorts') || [])
.map(port => ({
name: port.Label,
port: port.Value,
isDynamic: false,
}))
.concat(
this.get('network.dynamicPorts').map(port => ({
(this.get('network.dynamicPorts') || []).map(port => ({
name: port.Label,
port: port.Value,
isDynamic: true,
Expand Down
10 changes: 10 additions & 0 deletions ui/app/routes/allocations/allocation/task/logs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Ember from 'ember';

const { Route } = Ember;

export default Route.extend({
model() {
const task = this._super(...arguments);
return task.get('allocation.node').then(() => task);
},
});
2 changes: 1 addition & 1 deletion ui/app/services/token.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Ember from 'ember';
import fetch from 'fetch';
import fetch from 'nomad-ui/utils/fetch';

const { Service, computed, assign } = Ember;

Expand Down
1 change: 1 addition & 0 deletions ui/app/styles/components.scss
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
@import "./components/badge";
@import "./components/boxed-section";
@import "./components/breadcrumbs";
@import "./components/cli-window";
@import "./components/ember-power-select";
@import "./components/empty-message";
@import "./components/error-container";
Expand Down
4 changes: 4 additions & 0 deletions ui/app/styles/components/boxed-section.scss
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
.boxed-section {
margin-bottom: 1.5em;

&:last-child {
margin-bottom: 0;
}

.boxed-section-head,
.boxed-section-foot {
padding: 0.75em 1.5em;
Expand Down
11 changes: 11 additions & 0 deletions ui/app/styles/components/cli-window.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.cli-window {
background: transparent;
color: $white;

height: 500px;
overflow: auto;

.is-light {
color: $text;
}
}
27 changes: 26 additions & 1 deletion ui/app/styles/core/buttons.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,24 @@ $button-box-shadow-standard: 0 2px 0 0 rgba($grey, 0.2);
font-weight: $weight-bold;
box-shadow: $button-box-shadow-standard;
border: 1px solid transparent;
text-decoration: none;

&:hover,
&.is-hovered {
text-decoration: none;
}

&:active,
&.is-active,
&:focus,
&.is-focused {
box-shadow: $button-box-shadow-standard;
text-decoration: none;
}

&.is-inverted.is-outlined {
box-shadow: none;
background: transparent;
}

&.is-compact {
Expand Down Expand Up @@ -90,12 +98,29 @@ $button-box-shadow-standard: 0 2px 0 0 rgba($grey, 0.2);

&:active,
&.is-active {
background-color: rgba($color-invert, 0.2);
background-color: rgba($color-invert, 0.1);
border-color: $color-invert;
color: $color-invert;
box-shadow: none;
}
}
}
}

// When an icon in a button should be treated like text,
// override the default Bulma behavior
.icon.is-text {
&:first-child:not(:last-child) {
margin-left: 0;
margin-right: 0;
}
&:last-child:not(:first-child) {
margin-left: 0;
margin-right: 0;
}
&:first-child:last-child {
margin-left: 0;
margin-right: 0;
}
}
}
5 changes: 3 additions & 2 deletions ui/app/styles/core/icon.scss
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ $icon-dimensions-large: 2rem;
vertical-align: text-top;
height: $icon-dimensions;
width: $icon-dimensions;
fill: lighten($text, 25%);
fill: $text;

&.is-small {
&.is-small,
&.is-text {
height: $icon-dimensions-small;
width: $icon-dimensions-small;
}
Expand Down
15 changes: 15 additions & 0 deletions ui/app/templates/allocations/allocation/task/logs.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{{#global-header class="page-header"}}
<span class="breadcrumb">Allocations</span>
{{#link-to "allocations.allocation" model.allocation class="breadcrumb"}}
{{model.allocation.shortId}}
{{/link-to}}
{{#link-to "allocations.allocation.task" model.allocation model class="breadcrumb"}}
{{model.name}}
{{/link-to}}
{{/global-header}}
{{#gutter-menu class="page-body"}}
{{partial "allocations/allocation/task/subnav"}}
<section class="section">
{{task-log allocation=model.allocation task=model.name}}
</section>
{{/gutter-menu}}
1 change: 1 addition & 0 deletions ui/app/templates/allocations/allocation/task/subnav.hbs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<div class="tabs is-subnav">
<ul>
<li>{{#link-to "allocations.allocation.task.index" model.allocation model activeClass="is-active"}}Overview{{/link-to}}</li>
<li>{{#link-to "allocations.allocation.task.logs" model.allocation model activeClass="is-active"}}Logs{{/link-to}}</li>
</ul>
</div>
16 changes: 16 additions & 0 deletions ui/app/templates/components/task-log.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<div class="boxed-section-head">
<span>
<button class="button {{if (eq mode "stdout") "is-info"}} action-stdout" {{action "setMode" "stdout"}}>stdout</button>
<button class="button {{if (eq mode "stderr") "is-danger"}} action-stderr" {{action "setMode" "stderr"}}>stderr</button>
</span>
<span class="pull-right">
<button class="button is-white action-head" onclick={{perform head}}>Head</button>
<button class="button is-white action-tail" onclick={{perform tail}}>Tail</button>
<button class="button is-white action-toggle-stream" onclick={{action "toggleStream"}}>
{{x-icon (if logger.isStreaming "media-pause" "media-play") class="is-text"}}
</button>
</span>
</div>
<div class="boxed-section-body is-dark is-full-bleed">
<pre class="cli-window"><code>{{logger.output}}</code></pre>
</div>
33 changes: 33 additions & 0 deletions ui/app/utils/classes/abstract-logger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import Ember from 'ember';
import queryString from 'npm:query-string';

const { Mixin, computed, assign } = Ember;
const MAX_OUTPUT_LENGTH = 50000;

export default Mixin.create({
url: '',
params: computed(() => ({})),
logFetch() {
Ember.assert(
'Loggers need a logFetch method, which should have an interface like window.fetch'
);
},

endOffset: null,

offsetParams: computed('endOffset', function() {
const endOffset = this.get('endOffset');
return endOffset
? { origin: 'start', offset: endOffset }
: { origin: 'end', offset: MAX_OUTPUT_LENGTH };
}),

additionalParams: computed(() => ({})),

fullUrl: computed('url', 'params', 'offsetParams', 'additionalParams', function() {
const queryParams = queryString.stringify(
assign({}, this.get('params'), this.get('offsetParams'), this.get('additionalParams'))
);
return `${this.get('url')}?${queryParams}`;
}),
});
Loading

0 comments on commit e369a9a

Please sign in to comment.