[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('nomadShouldWrapCode', false) shouldWrapCode;
|
||||
|
||||
@action
|
||||
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.
|
||||
*
|
||||
|
@ -279,6 +288,7 @@ export default class JobEditor extends Component {
|
|||
planOutput: this.planOutput,
|
||||
shouldShowPlanMessage: this.shouldShowPlanMessage,
|
||||
view: this.args.view,
|
||||
shouldWrap: this.shouldWrapCode,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -295,6 +305,7 @@ export default class JobEditor extends Component {
|
|||
onSelect: this.args.onSelect,
|
||||
onUpdate: this.updateCode,
|
||||
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 { classNames } from '@ember-decorators/component';
|
||||
import classic from 'ember-classic-decorator';
|
||||
import localStorageProperty from 'nomad-ui/utils/properties/local-storage';
|
||||
|
||||
class MockAbortController {
|
||||
abort() {
|
||||
|
@ -42,6 +43,8 @@ export default class TaskLog extends Component {
|
|||
|
||||
shouldFillHeight = true;
|
||||
|
||||
@localStorageProperty('nomadShouldWrapCode', false) wrapped;
|
||||
|
||||
@alias('userSettings.logMode') mode;
|
||||
|
||||
@computed('allocation.{id,node.httpAddr}', 'useServer')
|
||||
|
@ -123,4 +126,9 @@ export default class TaskLog extends Component {
|
|||
failoverToServer() {
|
||||
this.set('useServer', true);
|
||||
}
|
||||
|
||||
@action toggleWrap() {
|
||||
this.toggleProperty('wrapped');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ export default class CodeMirrorModifier extends Modifier {
|
|||
}
|
||||
|
||||
didUpdateArguments() {
|
||||
this._editor.setOption('lineWrapping', this.args.named.lineWrapping);
|
||||
this._editor.setOption('readOnly', this.args.named.readOnly);
|
||||
if (!this.args.named.content) {
|
||||
return;
|
||||
|
@ -66,6 +67,7 @@ export default class CodeMirrorModifier extends Modifier {
|
|||
value: this.args.named.content || '',
|
||||
viewportMargin: this.args.named.viewportMargin || '',
|
||||
screenReaderLabel: this.args.named.screenReaderLabel || '',
|
||||
lineWrapping: this.args.named.lineWrapping || false,
|
||||
});
|
||||
|
||||
if (this.autofocus) {
|
||||
|
|
|
@ -70,9 +70,22 @@
|
|||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
.button {
|
||||
.button,
|
||||
.is-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 {
|
||||
|
|
|
@ -12,6 +12,10 @@
|
|||
|
||||
code {
|
||||
height: 100%;
|
||||
|
||||
&.wrapped {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.is-light {
|
||||
|
|
|
@ -8,6 +8,15 @@
|
|||
Job Definition
|
||||
{{#if @data.cancelable}}
|
||||
<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}}
|
||||
|
@ -57,6 +66,7 @@
|
|||
theme="hashi"
|
||||
onUpdate=@fns.onUpdate
|
||||
mode=(if (eq @data.format "json") "javascript" "ruby")
|
||||
lineWrapping=@data.shouldWrap
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
|
@ -77,6 +87,7 @@
|
|||
onUpdate=@fns.onUpdate
|
||||
type="hclVariables"
|
||||
mode="ruby"
|
||||
lineWrapping=@data.shouldWrap
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
|
|
|
@ -7,6 +7,16 @@
|
|||
<div class="boxed-section-head">
|
||||
Job Definition
|
||||
<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.">
|
||||
<div class="job-definition-select {{unless @data.hasSpecification " disabled"}}" data-test-select={{@data.view}}>
|
||||
<button
|
||||
|
@ -26,6 +36,7 @@
|
|||
</button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
<button
|
||||
class="button is-light is-compact"
|
||||
type="button"
|
||||
|
@ -34,7 +45,8 @@
|
|||
>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="boxed-section-body is-full-bleed">
|
||||
{{#if (eq @data.view "job-spec")}}
|
||||
|
@ -46,6 +58,7 @@
|
|||
readOnly=true
|
||||
screenReaderLabel="Job specification"
|
||||
theme="hashi-read-only"
|
||||
lineWrapping=@data.shouldWrap
|
||||
}}
|
||||
/>
|
||||
{{else}}
|
||||
|
@ -56,6 +69,7 @@
|
|||
theme="hashi-read-only"
|
||||
readOnly=true
|
||||
screenReaderLabel="JSON Viewer"
|
||||
lineWrapping=@data.shouldWrap
|
||||
}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
@ -75,6 +89,7 @@
|
|||
mode="ruby"
|
||||
theme="hashi-read-only"
|
||||
readOnly=true
|
||||
lineWrapping=@data.shouldWrap
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -3,4 +3,4 @@
|
|||
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>
|
||||
{{/if}}
|
||||
<div class="boxed-section-head">
|
||||
<div class="boxed-section-head task-log-head">
|
||||
<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="stderr" class="button {{if (eq this.mode "stderr") "is-danger"}}" {{action "setMode" "stderr"}} type="button">stderr</button>
|
||||
</span>
|
||||
<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="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">
|
||||
|
@ -30,5 +40,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}} @shouldFillHeight={{this.shouldFillHeight}} />
|
||||
<StreamingFile @logger={{this.logger}} @mode={{this.streamMode}} @isStreaming={{this.isStreaming}} @shouldFillHeight={{this.shouldFillHeight}} @wrapped={{this.wrapped}} />
|
||||
</div>
|
||||
|
|
|
@ -4,7 +4,12 @@
|
|||
*/
|
||||
|
||||
/* 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 { module, test } from '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) {
|
||||
await TaskLogs.visitParentJob({
|
||||
id: job.id,
|
||||
|
|
Loading…
Reference in New Issue