[ui] Text wrap long lines of code and logs (#17754)
* Text and code wrapping as a localStorage var * task-log uses wrapping and kb shortcut * Word wrap keyboard labels * Wrapper as a toggle not a button * Changelog and fixed an extra space trailing log lines * Moves toggle to inside * Acceptance tests for ww and toggle click
This commit is contained in:
parent
e7cc7f2123
commit
d559072e48
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:improvement
|
||||||
|
ui: adds a toggle and localStorage property to Word Wrap logs and job definitions
|
||||||
|
```
|
|
@ -86,6 +86,7 @@ export default class JobEditor extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
@localStorageProperty('nomadMessageJobPlan', true) shouldShowPlanMessage;
|
@localStorageProperty('nomadMessageJobPlan', true) shouldShowPlanMessage;
|
||||||
|
@localStorageProperty('nomadShouldWrapCode', false) shouldWrapCode;
|
||||||
|
|
||||||
@action
|
@action
|
||||||
dismissPlanMessage() {
|
dismissPlanMessage() {
|
||||||
|
@ -184,6 +185,14 @@ export default class JobEditor extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle the wrapping of the job's definition or definition variables.
|
||||||
|
*/
|
||||||
|
@action
|
||||||
|
toggleWrap() {
|
||||||
|
this.shouldWrapCode = !this.shouldWrapCode;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read the content of an uploaded job specification file and update the job's definition.
|
* Read the content of an uploaded job specification file and update the job's definition.
|
||||||
*
|
*
|
||||||
|
@ -279,6 +288,7 @@ export default class JobEditor extends Component {
|
||||||
planOutput: this.planOutput,
|
planOutput: this.planOutput,
|
||||||
shouldShowPlanMessage: this.shouldShowPlanMessage,
|
shouldShowPlanMessage: this.shouldShowPlanMessage,
|
||||||
view: this.args.view,
|
view: this.args.view,
|
||||||
|
shouldWrap: this.shouldWrapCode,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -295,6 +305,7 @@ export default class JobEditor extends Component {
|
||||||
onSelect: this.args.onSelect,
|
onSelect: this.args.onSelect,
|
||||||
onUpdate: this.updateCode,
|
onUpdate: this.updateCode,
|
||||||
onUpload: this.uploadJobSpec,
|
onUpload: this.uploadJobSpec,
|
||||||
|
onToggleWrap: this.toggleWrap,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { logger } from 'nomad-ui/utils/classes/log';
|
||||||
import timeout from 'nomad-ui/utils/timeout';
|
import timeout from 'nomad-ui/utils/timeout';
|
||||||
import { classNames } from '@ember-decorators/component';
|
import { classNames } from '@ember-decorators/component';
|
||||||
import classic from 'ember-classic-decorator';
|
import classic from 'ember-classic-decorator';
|
||||||
|
import localStorageProperty from 'nomad-ui/utils/properties/local-storage';
|
||||||
|
|
||||||
class MockAbortController {
|
class MockAbortController {
|
||||||
abort() {
|
abort() {
|
||||||
|
@ -42,6 +43,8 @@ export default class TaskLog extends Component {
|
||||||
|
|
||||||
shouldFillHeight = true;
|
shouldFillHeight = true;
|
||||||
|
|
||||||
|
@localStorageProperty('nomadShouldWrapCode', false) wrapped;
|
||||||
|
|
||||||
@alias('userSettings.logMode') mode;
|
@alias('userSettings.logMode') mode;
|
||||||
|
|
||||||
@computed('allocation.{id,node.httpAddr}', 'useServer')
|
@computed('allocation.{id,node.httpAddr}', 'useServer')
|
||||||
|
@ -123,4 +126,9 @@ export default class TaskLog extends Component {
|
||||||
failoverToServer() {
|
failoverToServer() {
|
||||||
this.set('useServer', true);
|
this.set('useServer', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action toggleWrap() {
|
||||||
|
this.toggleProperty('wrapped');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@ export default class CodeMirrorModifier extends Modifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
didUpdateArguments() {
|
didUpdateArguments() {
|
||||||
|
this._editor.setOption('lineWrapping', this.args.named.lineWrapping);
|
||||||
this._editor.setOption('readOnly', this.args.named.readOnly);
|
this._editor.setOption('readOnly', this.args.named.readOnly);
|
||||||
if (!this.args.named.content) {
|
if (!this.args.named.content) {
|
||||||
return;
|
return;
|
||||||
|
@ -66,6 +67,7 @@ export default class CodeMirrorModifier extends Modifier {
|
||||||
value: this.args.named.content || '',
|
value: this.args.named.content || '',
|
||||||
viewportMargin: this.args.named.viewportMargin || '',
|
viewportMargin: this.args.named.viewportMargin || '',
|
||||||
screenReaderLabel: this.args.named.screenReaderLabel || '',
|
screenReaderLabel: this.args.named.screenReaderLabel || '',
|
||||||
|
lineWrapping: this.args.named.lineWrapping || false,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.autofocus) {
|
if (this.autofocus) {
|
||||||
|
|
|
@ -70,9 +70,22 @@
|
||||||
border-top-right-radius: 0;
|
border-top-right-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.button,
|
||||||
|
.is-inline-block {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-toggle {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
top: 0.2em;
|
||||||
|
margin-left: $control-padding-horizontal;
|
||||||
|
margin-right: $control-padding-horizontal;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.task-log-head .header-toggle {
|
||||||
|
top: 0.5em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.boxed-section-body {
|
.boxed-section-body {
|
||||||
|
|
|
@ -12,6 +12,10 @@
|
||||||
|
|
||||||
code {
|
code {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
|
&.wrapped {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-light {
|
.is-light {
|
||||||
|
|
|
@ -8,6 +8,15 @@
|
||||||
Job Definition
|
Job Definition
|
||||||
{{#if @data.cancelable}}
|
{{#if @data.cancelable}}
|
||||||
<div class="pull-right" style="display: flex">
|
<div class="pull-right" style="display: flex">
|
||||||
|
<span class="header-toggle">
|
||||||
|
<Hds::Form::Toggle::Field
|
||||||
|
{{keyboard-shortcut label="Toggle word wrap" action=(action @fns.onToggleWrap) pattern=(array "w" "w") menuLevel=true }}
|
||||||
|
checked={{@data.shouldWrap}}
|
||||||
|
{{on "change" @fns.onToggleWrap}}
|
||||||
|
as |F|>
|
||||||
|
<F.Label>Word Wrap</F.Label>
|
||||||
|
</Hds::Form::Toggle::Field>
|
||||||
|
</span>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
@condition={{unless @data.hasSpecification true false}}
|
@condition={{unless @data.hasSpecification true false}}
|
||||||
@isFullText={{true}}
|
@isFullText={{true}}
|
||||||
|
@ -57,6 +66,7 @@
|
||||||
theme="hashi"
|
theme="hashi"
|
||||||
onUpdate=@fns.onUpdate
|
onUpdate=@fns.onUpdate
|
||||||
mode=(if (eq @data.format "json") "javascript" "ruby")
|
mode=(if (eq @data.format "json") "javascript" "ruby")
|
||||||
|
lineWrapping=@data.shouldWrap
|
||||||
}}
|
}}
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -77,6 +87,7 @@
|
||||||
onUpdate=@fns.onUpdate
|
onUpdate=@fns.onUpdate
|
||||||
type="hclVariables"
|
type="hclVariables"
|
||||||
mode="ruby"
|
mode="ruby"
|
||||||
|
lineWrapping=@data.shouldWrap
|
||||||
}}
|
}}
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -7,6 +7,16 @@
|
||||||
<div class="boxed-section-head">
|
<div class="boxed-section-head">
|
||||||
Job Definition
|
Job Definition
|
||||||
<div class="pull-right" style="display: flex">
|
<div class="pull-right" style="display: flex">
|
||||||
|
<span class="header-toggle">
|
||||||
|
<Hds::Form::Toggle::Field
|
||||||
|
{{keyboard-shortcut label="Toggle word wrap" action=(action @fns.onToggleWrap) pattern=(array "w" "w") menuLevel=true }}
|
||||||
|
checked={{@data.shouldWrap}}
|
||||||
|
{{on "change" @fns.onToggleWrap}}
|
||||||
|
as |F|>
|
||||||
|
<F.Label>Word Wrap</F.Label>
|
||||||
|
</Hds::Form::Toggle::Field>
|
||||||
|
</span>
|
||||||
|
|
||||||
<Tooltip @condition={{unless @data.hasSpecification true false}} @isFullText={{true}} @text="A jobspec file was not submitted when this job was run. You can still view and edit the expanded JSON format.">
|
<Tooltip @condition={{unless @data.hasSpecification true false}} @isFullText={{true}} @text="A jobspec file was not submitted when this job was run. You can still view and edit the expanded JSON format.">
|
||||||
<div class="job-definition-select {{unless @data.hasSpecification " disabled"}}" data-test-select={{@data.view}}>
|
<div class="job-definition-select {{unless @data.hasSpecification " disabled"}}" data-test-select={{@data.view}}>
|
||||||
<button
|
<button
|
||||||
|
@ -26,6 +36,7 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="button is-light is-compact"
|
class="button is-light is-compact"
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -34,6 +45,7 @@
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="boxed-section-body is-full-bleed">
|
<div class="boxed-section-body is-full-bleed">
|
||||||
|
@ -46,6 +58,7 @@
|
||||||
readOnly=true
|
readOnly=true
|
||||||
screenReaderLabel="Job specification"
|
screenReaderLabel="Job specification"
|
||||||
theme="hashi-read-only"
|
theme="hashi-read-only"
|
||||||
|
lineWrapping=@data.shouldWrap
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{{else}}
|
{{else}}
|
||||||
|
@ -56,6 +69,7 @@
|
||||||
theme="hashi-read-only"
|
theme="hashi-read-only"
|
||||||
readOnly=true
|
readOnly=true
|
||||||
screenReaderLabel="JSON Viewer"
|
screenReaderLabel="JSON Viewer"
|
||||||
|
lineWrapping=@data.shouldWrap
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
@ -75,6 +89,7 @@
|
||||||
mode="ruby"
|
mode="ruby"
|
||||||
theme="hashi-read-only"
|
theme="hashi-read-only"
|
||||||
readOnly=true
|
readOnly=true
|
||||||
|
lineWrapping=@data.shouldWrap
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,4 +3,4 @@
|
||||||
SPDX-License-Identifier: MPL-2.0
|
SPDX-License-Identifier: MPL-2.0
|
||||||
~}}
|
~}}
|
||||||
|
|
||||||
<code data-test-output>{{this.logger.output}}</code>
|
<code data-test-output class={{if @wrapped "wrapped"}}>{{this.logger.output}}</code>
|
|
@ -16,12 +16,22 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
<div class="boxed-section-head">
|
<div class="boxed-section-head task-log-head">
|
||||||
<span>
|
<span>
|
||||||
<button data-test-log-action="stdout" class="button {{if (eq this.mode "stdout") "is-info"}}" {{action "setMode" "stdout"}} type="button">stdout</button>
|
<button data-test-log-action="stdout" class="button {{if (eq this.mode "stdout") "is-info"}}" {{action "setMode" "stdout"}} type="button">stdout</button>
|
||||||
<button data-test-log-action="stderr" class="button {{if (eq this.mode "stderr") "is-danger"}}" {{action "setMode" "stderr"}} type="button">stderr</button>
|
<button data-test-log-action="stderr" class="button {{if (eq this.mode "stderr") "is-danger"}}" {{action "setMode" "stderr"}} type="button">stderr</button>
|
||||||
</span>
|
</span>
|
||||||
<span class="pull-right">
|
<span class="pull-right">
|
||||||
|
<span class="header-toggle">
|
||||||
|
<Hds::Form::Toggle::Field
|
||||||
|
{{keyboard-shortcut label="Toggle word wrap" action=(action "toggleWrap") pattern=(array "w" "w") menuLevel=true }}
|
||||||
|
checked={{this.wrapped}}
|
||||||
|
{{on "change" this.toggleWrap}}
|
||||||
|
data-test-word-wrap-toggle
|
||||||
|
as |F|>
|
||||||
|
<F.Label>Word Wrap</F.Label>
|
||||||
|
</Hds::Form::Toggle::Field>
|
||||||
|
</span>
|
||||||
<button data-test-log-action="head" class="button is-white" onclick={{action "gotoHead"}} type="button">Head</button>
|
<button data-test-log-action="head" class="button is-white" onclick={{action "gotoHead"}} type="button">Head</button>
|
||||||
<button data-test-log-action="tail" class="button is-white" onclick={{action "gotoTail"}} type="button">Tail</button>
|
<button data-test-log-action="tail" class="button is-white" onclick={{action "gotoTail"}} type="button">Tail</button>
|
||||||
<button data-test-log-action="toggle-stream" class="button is-white" onclick={{action "toggleStream"}} type="button" title="{{if this.logger.isStreaming "Stop" "Start"}} log streaming">
|
<button data-test-log-action="toggle-stream" class="button is-white" onclick={{action "toggleStream"}} type="button" title="{{if this.logger.isStreaming "Stop" "Start"}} log streaming">
|
||||||
|
@ -30,5 +40,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}} @shouldFillHeight={{this.shouldFillHeight}} />
|
<StreamingFile @logger={{this.logger}} @mode={{this.streamMode}} @isStreaming={{this.isStreaming}} @shouldFillHeight={{this.shouldFillHeight}} @wrapped={{this.wrapped}} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,7 +4,12 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/* eslint-disable qunit/require-expect */
|
/* eslint-disable qunit/require-expect */
|
||||||
import { click, currentURL, findAll } from '@ember/test-helpers';
|
import {
|
||||||
|
click,
|
||||||
|
currentURL,
|
||||||
|
findAll,
|
||||||
|
triggerKeyEvent,
|
||||||
|
} 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';
|
||||||
|
@ -69,6 +74,75 @@ module('Acceptance | task logs', function (hooks) {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('logs can be word-wrapped', async function (assert) {
|
||||||
|
await TaskLogs.visit({ id: allocation.id, name: task.name });
|
||||||
|
|
||||||
|
assert.dom('[data-test-word-wrap-toggle]').isNotChecked();
|
||||||
|
assert.dom('[data-test-output]').doesNotHaveClass('wrapped');
|
||||||
|
|
||||||
|
run.later(() => {
|
||||||
|
run.cancelTimers();
|
||||||
|
}, 100);
|
||||||
|
await click('[data-test-word-wrap-toggle]');
|
||||||
|
assert.dom('[data-test-word-wrap-toggle]').isChecked();
|
||||||
|
assert.dom('[data-test-output]').hasClass('wrapped');
|
||||||
|
|
||||||
|
run.later(() => {
|
||||||
|
run.cancelTimers();
|
||||||
|
}, 100);
|
||||||
|
await click('[data-test-word-wrap-toggle]');
|
||||||
|
assert.dom('[data-test-word-wrap-toggle]').isNotChecked();
|
||||||
|
assert.dom('[data-test-output]').doesNotHaveClass('wrapped');
|
||||||
|
|
||||||
|
window.localStorage.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('logs in sidebar can be word-wrapped', async function (assert) {
|
||||||
|
await TaskLogs.visitParentJob({
|
||||||
|
id: job.id,
|
||||||
|
allocationId: allocation.id,
|
||||||
|
name: task.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
run.later(() => {
|
||||||
|
run.cancelTimers();
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
const taskRow = [
|
||||||
|
...findAll('.task-sub-row').filter((row) => {
|
||||||
|
return row.textContent.includes(task.name);
|
||||||
|
}),
|
||||||
|
][0];
|
||||||
|
|
||||||
|
await click(taskRow.querySelector('button.logs-sidebar-trigger'));
|
||||||
|
|
||||||
|
assert.dom('[data-test-word-wrap-toggle]').isNotChecked();
|
||||||
|
assert.dom('[data-test-output]').doesNotHaveClass('wrapped');
|
||||||
|
|
||||||
|
run.later(() => {
|
||||||
|
run.cancelTimers();
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
// type "ww" to trigger word wrap
|
||||||
|
const W_KEY = 87;
|
||||||
|
triggerKeyEvent('.sidebar', 'keydown', W_KEY);
|
||||||
|
await triggerKeyEvent('.sidebar', 'keydown', W_KEY);
|
||||||
|
|
||||||
|
assert.dom('[data-test-word-wrap-toggle]').isChecked();
|
||||||
|
assert.dom('[data-test-output]').hasClass('wrapped');
|
||||||
|
|
||||||
|
run.later(() => {
|
||||||
|
run.cancelTimers();
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
triggerKeyEvent('.sidebar', 'keydown', W_KEY);
|
||||||
|
await triggerKeyEvent('.sidebar', 'keydown', W_KEY);
|
||||||
|
assert.dom('[data-test-word-wrap-toggle]').isNotChecked();
|
||||||
|
assert.dom('[data-test-output]').doesNotHaveClass('wrapped');
|
||||||
|
|
||||||
|
window.localStorage.clear();
|
||||||
|
});
|
||||||
|
|
||||||
test('logs are accessible in a sidebar context', async function (assert) {
|
test('logs are accessible in a sidebar context', async function (assert) {
|
||||||
await TaskLogs.visitParentJob({
|
await TaskLogs.visitParentJob({
|
||||||
id: job.id,
|
id: job.id,
|
||||||
|
|
Loading…
Reference in New Issue