From ee2f71f71b3717d7a81e32e4f1eb381b68c0a2b9 Mon Sep 17 00:00:00 2001 From: Mai-Lapyst Date: Sat, 20 Apr 2024 07:02:58 +0200 Subject: [PATCH 1/2] Adds support for log-line groups Supports special rendering of `##[group]` and `##[endgroup]`. --- web_src/js/components/RepoActionView.vue | 100 +++++++++++++---------- 1 file changed, 56 insertions(+), 44 deletions(-) diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue index 378f72668..2303ae1e0 100644 --- a/web_src/js/components/RepoActionView.vue +++ b/web_src/js/components/RepoActionView.vue @@ -110,34 +110,6 @@ const sfc = { }, methods: { - // get the active container element, either the `job-step-logs` or the `job-log-list` in the `job-log-group` - getLogsContainer(idx) { - const el = this.$refs.logs[idx]; - return el._stepLogsActiveContainer ?? el; - }, - // begin a log group - beginLogGroup(idx) { - const el = this.$refs.logs[idx]; - - const elJobLogGroup = document.createElement('div'); - elJobLogGroup.classList.add('job-log-group'); - - const elJobLogGroupSummary = document.createElement('div'); - elJobLogGroupSummary.classList.add('job-log-group-summary'); - - const elJobLogList = document.createElement('div'); - elJobLogList.classList.add('job-log-list'); - - elJobLogGroup.append(elJobLogGroupSummary); - elJobLogGroup.append(elJobLogList); - el._stepLogsActiveContainer = elJobLogList; - }, - // end a log group - endLogGroup(idx) { - const el = this.$refs.logs[idx]; - el._stepLogsActiveContainer = null; - }, - // show/hide the step logs for a step toggleStepLogs(idx) { this.currentJobStepsStates[idx].expanded = !this.currentJobStepsStates[idx].expanded; @@ -153,8 +125,18 @@ const sfc = { approveRun() { POST(`${this.run.link}/approve`); }, + // show/hide the step logs for a group + toggleGroupLogs(event) { + const line = event.target.parentElement; + const list = line.nextSibling; + if (event.newState === 'open') { + list.classList.remove('hidden'); + } else { + list.classList.add('hidden'); + } + }, - createLogLine(line, startTime, stepIndex) { + createLogLine(line, startTime, stepIndex, group) { const div = document.createElement('div'); div.classList.add('job-log-line'); div.setAttribute('id', `jobstep-${stepIndex}-${line.index}`); @@ -180,9 +162,19 @@ const sfc = { logTimeSeconds.textContent = `${seconds}s`; toggleElem(logTimeSeconds, this.timeVisible['log-time-seconds']); - const logMessage = document.createElement('span'); - logMessage.className = 'log-msg'; + let logMessage = document.createElement('span'); logMessage.innerHTML = renderAnsi(line.message); + if (group.isHeader) { + const details = document.createElement('details'); + details.addEventListener('toggle', this.toggleGroupLogs); + const summary = document.createElement('summary'); + summary.append(logMessage); + details.append(summary); + logMessage = details; + } + logMessage.className = 'log-msg'; + logMessage.style.paddingLeft = `${group.depth}em`; + div.append(logTimeStamp); div.append(logMessage); div.append(logTimeSeconds); @@ -191,10 +183,38 @@ const sfc = { }, appendLogs(stepIndex, logLines, startTime) { + const groupStack = []; + const container = this.$refs.logs[stepIndex]; for (const line of logLines) { - // TODO: group support: ##[group]GroupTitle , ##[endgroup] - const el = this.getLogsContainer(stepIndex); - el.append(this.createLogLine(line, startTime, stepIndex)); + const el = groupStack.length > 0 ? groupStack[groupStack.length - 1] : container; + const group = { + depth: groupStack.length, + isHeader: false, + }; + if (line.message.startsWith('##[group]')) { + group.isHeader = true; + + const logLine = this.createLogLine( + { + ...line, + message: line.message.substring(9), + }, + startTime, stepIndex, group, + ); + logLine.setAttribute('data-group', group.index); + el.append(logLine); + + const list = document.createElement('div'); + list.classList.add('job-log-list'); + list.classList.add('hidden'); + list.setAttribute('data-group', group.index); + groupStack.push(list); + el.append(list); + } else if (line.message.startsWith('##[endgroup]')) { + groupStack.pop(); + } else { + el.append(this.createLogLine(line, startTime, stepIndex, group)); + } } }, @@ -872,15 +892,7 @@ export function initRepositoryActionView() { border-radius: 0; } -/* TODO: group support - -.job-log-group { - +.job-log-list.hidden { + display: none; } -.job-log-group-summary { - -} -.job-log-list { - -} */ From 66bbf75dd3a18825d8fffa0bee9e75e22ace5f91 Mon Sep 17 00:00:00 2001 From: Mai-Lapyst Date: Tue, 23 Apr 2024 15:55:39 +0200 Subject: [PATCH 2/2] Add frontend test --- web_src/js/components/RepoActionView.test.js | 105 +++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 web_src/js/components/RepoActionView.test.js diff --git a/web_src/js/components/RepoActionView.test.js b/web_src/js/components/RepoActionView.test.js new file mode 100644 index 000000000..8c4e1506b --- /dev/null +++ b/web_src/js/components/RepoActionView.test.js @@ -0,0 +1,105 @@ +import {mount, flushPromises} from '@vue/test-utils'; +import RepoActionView from './RepoActionView.vue'; + +test('processes ##[group] and ##[endgroup]', async () => { + Object.defineProperty(document.documentElement, 'lang', {value: 'en'}); + vi.spyOn(global, 'fetch').mockImplementation((url, opts) => { + const artifacts_value = { + artifacts: [], + }; + const stepsLog_value = [ + { + step: 0, + cursor: 0, + lines: [ + {index: 1, message: '##[group]Test group', timestamp: 0}, + {index: 2, message: 'A test line', timestamp: 0}, + {index: 3, message: '##[endgroup]', timestamp: 0}, + {index: 4, message: 'A line outside the group', timestamp: 0}, + ], + }, + ]; + const jobs_value = { + state: { + run: { + status: 'success', + commit: { + pusher: {}, + }, + }, + currentJob: { + steps: [ + { + summary: 'Test Job', + duration: '1s', + status: 'success', + }, + ], + }, + }, + logs: { + stepsLog: opts.body?.includes('"cursor":null') ? stepsLog_value : [], + }, + }; + + return Promise.resolve({ + ok: true, + json: vi.fn().mockResolvedValue( + url.endsWith('/artifacts') ? artifacts_value : jobs_value, + ), + }); + }); + + const wrapper = mount(RepoActionView, { + props: { + jobIndex: '1', + locale: { + approve: '', + cancel: '', + rerun: '', + artifactsTitle: '', + areYouSure: '', + confirmDeleteArtifact: '', + rerun_all: '', + showTimeStamps: '', + showLogSeconds: '', + showFullScreen: '', + downloadLogs: '', + status: { + unknown: '', + waiting: '', + running: '', + success: '', + failure: '', + cancelled: '', + skipped: '', + blocked: '', + }, + }, + }, + }); + await flushPromises(); + await wrapper.get('.job-step-summary').trigger('click'); + await flushPromises(); + + // Test if header was loaded correctly + expect(wrapper.get('.step-summary-msg').text()).toEqual('Test Job'); + + // Check if 3 lines where rendered + expect(wrapper.findAll('.job-log-line').length).toEqual(3); + + // Check if line 1 contains the group header + expect(wrapper.get('.job-log-line:nth-of-type(1) > details.log-msg').text()).toEqual('Test group'); + + // Check if right after the header line exists a log list + expect(wrapper.find('.job-log-line:nth-of-type(1) + .job-log-list.hidden').exists()).toBe(true); + + // Check if inside the loglist exist exactly one log line + expect(wrapper.findAll('.job-log-list > .job-log-line').length).toEqual(1); + + // Check if inside the loglist is an logline with our second logline + expect(wrapper.get('.job-log-list > .job-log-line > .log-msg').text()).toEqual('A test line'); + + // Check if after the log list exists another log line + expect(wrapper.get('.job-log-list + .job-log-line > .log-msg').text()).toEqual('A line outside the group'); +});