From d559072e48384e2f2ce07632b6042f53cc6e2228 Mon Sep 17 00:00:00 2001 From: Phil Renaud Date: Fri, 30 Jun 2023 17:07:57 -0400 Subject: [PATCH] [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 --- .changelog/17754.txt | 3 + ui/app/components/job-editor.js | 11 +++ ui/app/components/task-log.js | 8 ++ ui/app/modifiers/code-mirror.js | 2 + ui/app/styles/components/boxed-section.scss | 15 +++- ui/app/styles/components/cli-window.scss | 4 + .../templates/components/job-editor/edit.hbs | 11 +++ .../templates/components/job-editor/read.hbs | 17 ++++- .../templates/components/streaming-file.hbs | 2 +- ui/app/templates/components/task-log.hbs | 14 +++- ui/tests/acceptance/task-logs-test.js | 76 ++++++++++++++++++- 11 files changed, 157 insertions(+), 6 deletions(-) create mode 100644 .changelog/17754.txt diff --git a/.changelog/17754.txt b/.changelog/17754.txt new file mode 100644 index 000000000..9cad98303 --- /dev/null +++ b/.changelog/17754.txt @@ -0,0 +1,3 @@ +```release-note:improvement +ui: adds a toggle and localStorage property to Word Wrap logs and job definitions +``` diff --git a/ui/app/components/job-editor.js b/ui/app/components/job-editor.js index c03b4666e..fc91c2bf3 100644 --- a/ui/app/components/job-editor.js +++ b/ui/app/components/job-editor.js @@ -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, }; } } diff --git a/ui/app/components/task-log.js b/ui/app/components/task-log.js index f017a5d93..63f403db9 100644 --- a/ui/app/components/task-log.js +++ b/ui/app/components/task-log.js @@ -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; + } } diff --git a/ui/app/modifiers/code-mirror.js b/ui/app/modifiers/code-mirror.js index 380ce75ee..ed65ef8fe 100644 --- a/ui/app/modifiers/code-mirror.js +++ b/ui/app/modifiers/code-mirror.js @@ -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) { diff --git a/ui/app/styles/components/boxed-section.scss b/ui/app/styles/components/boxed-section.scss index 329f2adac..3de8bfaea 100644 --- a/ui/app/styles/components/boxed-section.scss +++ b/ui/app/styles/components/boxed-section.scss @@ -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 { diff --git a/ui/app/styles/components/cli-window.scss b/ui/app/styles/components/cli-window.scss index c3deeb986..dde745da1 100644 --- a/ui/app/styles/components/cli-window.scss +++ b/ui/app/styles/components/cli-window.scss @@ -12,6 +12,10 @@ code { height: 100%; + + &.wrapped { + white-space: pre-wrap; + } } .is-light { diff --git a/ui/app/templates/components/job-editor/edit.hbs b/ui/app/templates/components/job-editor/edit.hbs index cdcab2af5..30d6dd41a 100644 --- a/ui/app/templates/components/job-editor/edit.hbs +++ b/ui/app/templates/components/job-editor/edit.hbs @@ -8,6 +8,15 @@ Job Definition {{#if @data.cancelable}}
+ + + Word Wrap + +
@@ -77,6 +87,7 @@ onUpdate=@fns.onUpdate type="hclVariables" mode="ruby" + lineWrapping=@data.shouldWrap }} > diff --git a/ui/app/templates/components/job-editor/read.hbs b/ui/app/templates/components/job-editor/read.hbs index 6a33c929c..2375b3671 100644 --- a/ui/app/templates/components/job-editor/read.hbs +++ b/ui/app/templates/components/job-editor/read.hbs @@ -7,6 +7,16 @@
Job Definition
+ + + Word Wrap + + +
+ -
+ +
{{#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 }} />
diff --git a/ui/app/templates/components/streaming-file.hbs b/ui/app/templates/components/streaming-file.hbs index 83ea4d800..96d8b951f 100644 --- a/ui/app/templates/components/streaming-file.hbs +++ b/ui/app/templates/components/streaming-file.hbs @@ -3,4 +3,4 @@ SPDX-License-Identifier: MPL-2.0 ~}} -{{this.logger.output}} \ No newline at end of file +{{this.logger.output}} \ No newline at end of file diff --git a/ui/app/templates/components/task-log.hbs b/ui/app/templates/components/task-log.hbs index 48beefd53..d417afabf 100644 --- a/ui/app/templates/components/task-log.hbs +++ b/ui/app/templates/components/task-log.hbs @@ -16,12 +16,22 @@ {{/if}} -
+
+ + + Word Wrap + +
- +
diff --git a/ui/tests/acceptance/task-logs-test.js b/ui/tests/acceptance/task-logs-test.js index 45432adae..78c34bac5 100644 --- a/ui/tests/acceptance/task-logs-test.js +++ b/ui/tests/acceptance/task-logs-test.js @@ -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,