mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-03-26 13:43:02 -04:00
Some checks are pending
/ release (push) Waiting to run
testing-integration / test-unit (push) Waiting to run
testing-integration / test-sqlite (push) Waiting to run
testing-integration / test-mariadb (v10.6) (push) Waiting to run
testing-integration / test-mariadb (v11.8) (push) Waiting to run
testing / backend-checks (push) Waiting to run
testing / frontend-checks (push) Waiting to run
testing / test-unit (push) Blocked by required conditions
testing / test-e2e (push) Blocked by required conditions
testing / test-remote-cacher (redis) (push) Blocked by required conditions
testing / test-remote-cacher (valkey) (push) Blocked by required conditions
testing / test-remote-cacher (garnet) (push) Blocked by required conditions
testing / test-remote-cacher (redict) (push) Blocked by required conditions
testing / test-mysql (push) Blocked by required conditions
testing / test-pgsql (push) Blocked by required conditions
testing / test-sqlite (push) Blocked by required conditions
testing / security-check (push) Blocked by required conditions
The `RepoActionView` Vue component attempts to do almost everything in the action log view in one Vue component, including data management & fetching new logs, rendering the list of jobs, rendering the list of steps, rendering the logs, etc. As part of #9768, this view is expected to receive some significant new features to display nested jobs within steps. Before that work commences, I'm refactoring the component. This refactor step moves the rendering of the step header, expansion of the step, and rendering of the logs, into a smaller component `ActionJobStep`. Tests for the functionality of the new component are added, and some tests have been moved from `RepoActionView` where they only touched log rendering. ## Checklist The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org). ### Tests - I added test coverage for Go changes... - [ ] in their respective `*_test.go` for unit tests. - [ ] in the `tests/integration` directory if it involves interactions with a live Forgejo server. - I added test coverage for JavaScript changes... - [x] in `web_src/js/*.test.js` if it can be unit tested. - [ ] in `tests/e2e/*.test.e2e.js` if it requires interactions with a live Forgejo server (see also the [developer guide for JavaScript testing](https://codeberg.org/forgejo/forgejo/src/branch/forgejo/tests/e2e/README.md#end-to-end-tests)). ### Documentation - [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change. - [x] I did not document these changes and I do not expect someone else to do it. ### Release notes - [x] I do not want this change to show in the release notes. - [ ] I want the title to show in the release notes with a link to this pull request. - [ ] I want the content of the `release-notes/<pull request number>.md` to be be used for the release notes instead of the title. Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/10366 Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org> Co-authored-by: Mathieu Fenniak <mathieu@fenniak.net> Co-committed-by: Mathieu Fenniak <mathieu@fenniak.net>
238 lines
6.7 KiB
Vue
238 lines
6.7 KiB
Vue
<script>
|
|
import {SvgIcon} from '../svg.js';
|
|
import ActionRunStatus from './ActionRunStatus.vue';
|
|
import {toggleElem} from '../utils/dom.js';
|
|
import {formatDatetime} from '../utils/time.js';
|
|
import {renderAnsi} from '../render/ansi.js';
|
|
|
|
export default {
|
|
name: 'ActionJobStep',
|
|
components: {
|
|
SvgIcon,
|
|
ActionRunStatus,
|
|
},
|
|
props: {
|
|
stepId: {
|
|
type: Number,
|
|
required: true,
|
|
},
|
|
status: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
runStatus: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
expanded: {
|
|
type: Boolean,
|
|
required: true,
|
|
},
|
|
isExpandable: {
|
|
type: Function,
|
|
required: true,
|
|
},
|
|
isDone: {
|
|
type: Function,
|
|
required: true,
|
|
},
|
|
cursor: {
|
|
type: Number,
|
|
required: false,
|
|
default: null,
|
|
},
|
|
summary: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
duration: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
timeVisibleTimestamp: {
|
|
type: Boolean,
|
|
required: true,
|
|
},
|
|
timeVisibleSeconds: {
|
|
type: Boolean,
|
|
required: true,
|
|
},
|
|
},
|
|
emits: ['toggle'],
|
|
|
|
data() {
|
|
return {
|
|
lineNumberOffset: 0,
|
|
};
|
|
},
|
|
|
|
methods: {
|
|
createLogLine(line, startTime, group) {
|
|
const lineNo = line.index - this.lineNumberOffset;
|
|
const div = document.createElement('div');
|
|
div.classList.add('job-log-line');
|
|
div.setAttribute('id', `jobstep-${this.stepId}-${lineNo}`);
|
|
div._jobLogTime = line.timestamp;
|
|
|
|
const lineNumber = document.createElement('a');
|
|
lineNumber.classList.add('line-num', 'muted');
|
|
lineNumber.textContent = lineNo;
|
|
lineNumber.setAttribute('href', `#jobstep-${this.stepId}-${lineNo}`);
|
|
div.append(lineNumber);
|
|
|
|
// for "Show timestamps"
|
|
const logTimeStamp = document.createElement('span');
|
|
logTimeStamp.className = 'log-time-stamp';
|
|
const date = new Date(parseFloat(line.timestamp * 1000));
|
|
const timeStamp = formatDatetime(date);
|
|
logTimeStamp.textContent = timeStamp;
|
|
toggleElem(logTimeStamp, this.timeVisibleTimestamp);
|
|
// for "Show seconds"
|
|
const logTimeSeconds = document.createElement('span');
|
|
logTimeSeconds.className = 'log-time-seconds';
|
|
const seconds = Math.floor(parseFloat(line.timestamp) - parseFloat(startTime));
|
|
logTimeSeconds.textContent = `${seconds}s`;
|
|
toggleElem(logTimeSeconds, this.timeVisibleSeconds);
|
|
|
|
let logMessage = document.createElement('span');
|
|
logMessage.innerHTML = renderAnsi(line.message);
|
|
// If the input to renderAnsi is not empty and the output is empty we can
|
|
// assume the input was only ANSI escape codes that have been removed. In
|
|
// that case we should not display this message
|
|
if (line.message !== '' && logMessage.innerHTML === '') {
|
|
this.lineNumberOffset++;
|
|
return [];
|
|
}
|
|
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);
|
|
|
|
return div;
|
|
},
|
|
|
|
appendLogs(logLines, startTime) {
|
|
this.lineNumberOffset = 0;
|
|
|
|
const groupStack = [];
|
|
const container = this.$refs.logsContainer;
|
|
for (const line of logLines) {
|
|
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, group,
|
|
);
|
|
logLine.setAttribute('data-group', group.index);
|
|
el.append(logLine);
|
|
|
|
const list = document.createElement('div');
|
|
list.classList.add('job-log-list', '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, group));
|
|
}
|
|
}
|
|
},
|
|
|
|
// show/hide the step logs for a group
|
|
toggleGroupLogs(event) {
|
|
const line = event.target.parentElement;
|
|
const list = line.nextSibling;
|
|
list.classList.toggle('hidden', event.newState !== 'open');
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
<template>
|
|
<div
|
|
class="job-step-summary"
|
|
tabindex="0"
|
|
@click.stop="isExpandable(status) && $emit('toggle')"
|
|
@keyup.enter.stop="isExpandable(status) && $emit('toggle')"
|
|
@keyup.space.stop="isExpandable(status) && $emit('toggle')"
|
|
:class="[expanded ? 'selected' : '', isExpandable(status) && 'step-expandable']"
|
|
>
|
|
<!-- If the job is done and the job step log is loaded for the first time, show the loading icon
|
|
currentJobStepsStates[i].cursor === null means the log is loaded for the first time
|
|
-->
|
|
<SvgIcon
|
|
v-if="isDone(runStatus) && expanded && cursor === null"
|
|
name="octicon-sync"
|
|
class="tw-mr-2 job-status-rotate"
|
|
/>
|
|
<SvgIcon
|
|
v-else
|
|
:name="expanded ? 'octicon-chevron-down': 'octicon-chevron-right'"
|
|
:class="['tw-mr-2', !isExpandable(status) && 'tw-invisible']"
|
|
/>
|
|
<ActionRunStatus :status="status" class="tw-mr-2"/>
|
|
|
|
<span class="step-summary-msg gt-ellipsis">{{ summary }}</span>
|
|
<span class="step-summary-duration">{{ duration }}</span>
|
|
</div>
|
|
|
|
<!-- the log elements could be a lot, do not use v-if to destroy/reconstruct the DOM,
|
|
use native DOM elements for "log line" to improve performance, Vue is not suitable for managing so many reactive elements. -->
|
|
<div class="job-step-logs" ref="logsContainer" v-show="expanded"/>
|
|
</template>
|
|
<style scoped>
|
|
|
|
.job-step-summary {
|
|
padding: 5px 10px;
|
|
display: flex;
|
|
align-items: center;
|
|
border-radius: var(--border-radius);
|
|
}
|
|
|
|
.job-step-summary.step-expandable {
|
|
cursor: pointer;
|
|
}
|
|
|
|
.job-step-summary.step-expandable:hover {
|
|
color: var(--color-console-fg);
|
|
background: var(--color-console-hover-bg);
|
|
}
|
|
|
|
.job-step-summary .step-summary-msg {
|
|
flex: 1;
|
|
}
|
|
|
|
.job-step-summary .step-summary-duration {
|
|
margin-left: 16px;
|
|
}
|
|
|
|
.job-step-summary.selected {
|
|
color: var(--color-console-fg);
|
|
background-color: var(--color-console-active-bg);
|
|
position: sticky;
|
|
top: 60px;
|
|
}
|
|
</style>
|
|
<style>
|
|
/* some elements are not managed by vue, so we need to use global style */
|
|
|
|
</style>
|