-
Notifications
You must be signed in to change notification settings - Fork 2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
UI: Client and Server Monitor #8177
Changes from 19 commits
2eceffa
cc8aed8
9e59488
f02beee
0a4cde3
47969c4
88e7e7e
2c85e69
f896a62
d0a0ffd
a4a8c9e
c3b4999
a0fce03
9e5cd53
9294a7a
7185757
53027ba
a0c6cc2
5bb3c43
619aea8
34e8e1f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import AbstractAbility from './abstract'; | ||
import { computed, get } from '@ember/object'; | ||
import { or } from '@ember/object/computed'; | ||
|
||
export default class Client extends AbstractAbility { | ||
@or('bypassAuthorization', 'selfTokenIsManagement', 'policiesIncludeAgentReadOrWrite') | ||
canRead; | ||
|
||
@computed('token.selfTokenPolicies.[]') | ||
get policiesIncludeAgentReadOrWrite() { | ||
const policies = (this.get('token.selfTokenPolicies') || []) | ||
.toArray() | ||
.map(policy => get(policy, 'rulesJSON.Agent.Policy')) | ||
.compact(); | ||
|
||
return policies.some(policy => policy === 'read' || policy === 'write'); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import { inject as service } from '@ember/service'; | ||
import Component from '@ember/component'; | ||
import { computed } from '@ember/object'; | ||
import { assert } from '@ember/debug'; | ||
import { tagName } from '@ember-decorators/component'; | ||
import Log from 'nomad-ui/utils/classes/log'; | ||
|
||
const LEVELS = ['error', 'warn', 'info', 'debug', 'trace']; | ||
|
||
@tagName('') | ||
export default class AgentMonitor extends Component { | ||
@service token; | ||
|
||
client = null; | ||
server = null; | ||
level = LEVELS[2]; | ||
onLevelChange() {} | ||
|
||
levels = LEVELS; | ||
monitorUrl = '/v1/agent/monitor'; | ||
isStreaming = true; | ||
logger = null; | ||
|
||
@computed('level', 'client.id', 'server.id') | ||
get monitorParams() { | ||
assert( | ||
'Provide a client OR a server to AgentMonitor, not both.', | ||
this.server != null || this.client != null | ||
); | ||
|
||
const type = this.server ? 'server_id' : 'client_id'; | ||
const id = this.server ? this.server.id : this.client && this.client.id; | ||
|
||
return { | ||
log_level: this.level, | ||
[type]: id, | ||
}; | ||
} | ||
|
||
didInsertElement() { | ||
this.updateLogger(); | ||
} | ||
|
||
updateLogger() { | ||
let currentTail = this.logger ? this.logger.tail : ''; | ||
if (currentTail) { | ||
currentTail += `\n...changing log level to ${this.level}...\n\n`; | ||
} | ||
this.set( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe that using |
||
'logger', | ||
Log.create({ | ||
logFetch: url => this.token.authorizedRequest(url), | ||
params: this.monitorParams, | ||
url: this.monitorUrl, | ||
tail: currentTail, | ||
}) | ||
); | ||
} | ||
|
||
setLevel(level) { | ||
this.logger.stop(); | ||
this.set('level', level); | ||
this.onLevelChange(level); | ||
this.updateLogger(); | ||
} | ||
|
||
toggleStream() { | ||
this.set('streamMode', 'streaming'); | ||
this.toggleProperty('isStreaming'); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import Component from '@ember/component'; | ||
import { tagName } from '@ember-decorators/component'; | ||
|
||
@tagName('') | ||
export default class ClientSubnav extends Component {} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import Component from '@ember/component'; | ||
import { tagName } from '@ember-decorators/component'; | ||
|
||
@tagName('') | ||
export default class ForbiddenMessage extends Component {} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import Component from '@ember/component'; | ||
import { tagName } from '@ember-decorators/component'; | ||
|
||
@tagName('') | ||
export default class ServerSubnav extends Component {} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,8 @@ import WindowResizable from 'nomad-ui/mixins/window-resizable'; | |
import { classNames, tagName } from '@ember-decorators/component'; | ||
import classic from 'ember-classic-decorator'; | ||
|
||
const A_KEY = 65; | ||
|
||
@classic | ||
@tagName('pre') | ||
@classNames('cli-window') | ||
|
@@ -14,6 +16,10 @@ export default class StreamingFile extends Component.extend(WindowResizable) { | |
mode = 'streaming'; // head, tail, streaming | ||
isStreaming = true; | ||
logger = null; | ||
follow = true; | ||
|
||
// Internal bookkeeping to avoid multiple scroll events on one frame | ||
requestFrame = true; | ||
|
||
didReceiveAttrs() { | ||
if (!this.logger) { | ||
|
@@ -26,12 +32,15 @@ export default class StreamingFile extends Component.extend(WindowResizable) { | |
performTask() { | ||
switch (this.mode) { | ||
case 'head': | ||
this.set('follow', false); | ||
this.head.perform(); | ||
break; | ||
case 'tail': | ||
this.set('follow', true); | ||
this.tail.perform(); | ||
break; | ||
case 'streaming': | ||
this.set('follow', true); | ||
if (this.isStreaming) { | ||
this.stream.perform(); | ||
} else { | ||
|
@@ -41,8 +50,42 @@ export default class StreamingFile extends Component.extend(WindowResizable) { | |
} | ||
} | ||
|
||
scrollHandler() { | ||
const cli = this.element; | ||
window.requestAnimationFrame(() => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be wrapped in an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Haaaaaaaa yep. Thank you for catching this. |
||
// If the scroll position is close enough to the bottom, autoscroll to the bottom | ||
this.set('follow', cli.scrollHeight - cli.scrollTop - cli.clientHeight < 20); | ||
this.requestFrame = true; | ||
}); | ||
this.requestFrame = false; | ||
} | ||
|
||
keyDownHandler(e) { | ||
// Rebind select-all shortcut to only select the text in the | ||
// streaming file output. | ||
if ((e.metaKey || e.ctrlKey) && e.keyCode === A_KEY) { | ||
e.preventDefault(); | ||
const selection = window.getSelection(); | ||
selection.removeAllRanges(); | ||
const range = document.createRange(); | ||
range.selectNode(this.element); | ||
selection.addRange(range); | ||
} | ||
} | ||
|
||
didInsertElement() { | ||
this.fillAvailableHeight(); | ||
|
||
this.set('_scrollHandler', this.scrollHandler.bind(this)); | ||
this.element.addEventListener('scroll', this._scrollHandler); | ||
|
||
this.set('_keyDownHandler', this.keyDownHandler.bind(this)); | ||
document.addEventListener('keydown', this._keyDownHandler); | ||
} | ||
|
||
willDestroyElement() { | ||
this.element.removeEventListener('scroll', this._scrollHandler); | ||
document.removeEventListener('keydown', this._keyDownHandler); | ||
} | ||
|
||
windowResizeHandler() { | ||
|
@@ -69,24 +112,16 @@ export default class StreamingFile extends Component.extend(WindowResizable) { | |
|
||
@task(function*() { | ||
yield this.get('logger.gotoTail').perform(); | ||
run.scheduleOnce('afterRender', this, this.synchronizeScrollPosition, [true]); | ||
}) | ||
tail; | ||
|
||
synchronizeScrollPosition(force = false) { | ||
const cliWindow = this.element; | ||
if (cliWindow.scrollHeight - cliWindow.scrollTop < 10 || force) { | ||
// If the window is approximately scrolled to the bottom, follow the log | ||
cliWindow.scrollTop = cliWindow.scrollHeight; | ||
synchronizeScrollPosition() { | ||
if (this.follow) { | ||
this.element.scrollTop = this.element.scrollHeight; | ||
} | ||
} | ||
|
||
@task(function*() { | ||
// Force the scroll position to the bottom of the window when starting streaming | ||
this.logger.one('tick', () => { | ||
run.scheduleOnce('afterRender', this, this.synchronizeScrollPosition, [true]); | ||
}); | ||
|
||
// Follow the log if the scroll position is near the bottom of the cli window | ||
this.logger.on('tick', this, 'scheduleScrollSynchronization'); | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import Controller from '@ember/controller'; | ||
import classic from 'ember-classic-decorator'; | ||
|
||
@classic | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this can be de- |
||
export default class ClientMonitorController extends Controller { | ||
queryParams = [{ level: 'level' }]; | ||
|
||
level = 'info'; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,31 +1,5 @@ | ||
import { alias } from '@ember/object/computed'; | ||
import Controller from '@ember/controller'; | ||
import Sortable from 'nomad-ui/mixins/sortable'; | ||
|
||
export default class ServersController extends Controller.extend(Sortable) { | ||
@alias('model.nodes') nodes; | ||
@alias('model.agents') agents; | ||
|
||
queryParams = [ | ||
{ | ||
currentPage: 'page', | ||
}, | ||
{ | ||
sortProperty: 'sort', | ||
}, | ||
{ | ||
sortDescending: 'desc', | ||
}, | ||
]; | ||
|
||
currentPage = 1; | ||
pageSize = 8; | ||
|
||
sortProperty = 'isLeader'; | ||
sortDescending = true; | ||
|
||
export default class ServersController extends Controller { | ||
isForbidden = false; | ||
|
||
@alias('agents') listToSort; | ||
@alias('listSorted') sortedAgents; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,32 @@ | ||
import { alias } from '@ember/object/computed'; | ||
import Controller, { inject as controller } from '@ember/controller'; | ||
import Sortable from 'nomad-ui/mixins/sortable'; | ||
|
||
export default class IndexController extends Controller { | ||
export default class IndexController extends Controller.extend(Sortable) { | ||
@controller('servers') serversController; | ||
@alias('serversController.isForbidden') isForbidden; | ||
|
||
@alias('model.nodes') nodes; | ||
@alias('model.agents') agents; | ||
|
||
queryParams = [ | ||
{ | ||
currentPage: 'page', | ||
}, | ||
{ | ||
sortProperty: 'sort', | ||
}, | ||
{ | ||
sortDescending: 'desc', | ||
}, | ||
]; | ||
|
||
currentPage = 1; | ||
pageSize = 8; | ||
|
||
sortProperty = 'isLeader'; | ||
sortDescending = true; | ||
|
||
@alias('agents') listToSort; | ||
@alias('listSorted') sortedAgents; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import Controller from '@ember/controller'; | ||
import classic from 'ember-classic-decorator'; | ||
|
||
@classic | ||
export default class ServerMonitorController extends Controller { | ||
queryParams = [{ level: 'level' }]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It doesn’t really matter but I think this could be |
||
|
||
level = 'info'; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this also use
get
instead ofthis.get
? I’m semi-surprised it doesn’t warn about this but maybe that’s becauseAbstractAbility
is already@classic
. (ETA I have since found it’s because I didn’t include the ESLint rules 😞)