mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-03-26 16:23:04 -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
Continuing refactor work to split functionality out of `RepoActionView` and into more testable, more manageable sub-components. #9768 will eventually result in some updates to this view for new functionality, and before more complexity is added I'd like to clean it up to a more maintainable state. Previous refactor step: #10366 ## 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/10537 Reviewed-by: Gusted <gusted@noreply.codeberg.org> Co-authored-by: Mathieu Fenniak <mathieu@fenniak.net> Co-committed-by: Mathieu Fenniak <mathieu@fenniak.net>
304 lines
8 KiB
Vue
304 lines
8 KiB
Vue
<!--
|
|
Copyright 2025 The Forgejo Authors. All rights reserved.
|
|
SPDX-License-Identifier: GPL-3.0-or-later
|
|
-->
|
|
|
|
<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');
|
|
},
|
|
|
|
scrollIntoView(lineID) {
|
|
const logLine = this.$refs.logsContainer.querySelector(lineID);
|
|
if (!logLine) {
|
|
return;
|
|
}
|
|
logLine.querySelector('.line-num').scrollIntoView();
|
|
},
|
|
},
|
|
};
|
|
</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;
|
|
}
|
|
|
|
.job-step-logs {
|
|
font-family: var(--fonts-monospace);
|
|
margin: 8px 0;
|
|
font-size: 12px;
|
|
}
|
|
|
|
</style>
|
|
<style>
|
|
/* some elements are not managed by vue, so we need to use global style */
|
|
|
|
.job-step-logs .job-log-line {
|
|
display: flex;
|
|
}
|
|
|
|
.job-step-logs .job-log-line .log-msg {
|
|
flex: 1;
|
|
word-break: break-all;
|
|
white-space: break-spaces;
|
|
margin-left: 10px;
|
|
overflow-wrap: anywhere;
|
|
color: var(--color-console-fg);
|
|
}
|
|
|
|
.job-log-line:hover,
|
|
.job-log-line:target {
|
|
background-color: var(--color-console-hover-bg);
|
|
}
|
|
|
|
.job-log-line:target {
|
|
scroll-margin-top: 95px;
|
|
}
|
|
|
|
/* class names 'log-time-seconds' and 'log-time-stamp' are used in the method toggleTimeDisplay */
|
|
.job-log-line .line-num, .log-time-seconds {
|
|
width: 48px;
|
|
color: var(--color-text-light-3);
|
|
text-align: right;
|
|
user-select: none;
|
|
}
|
|
|
|
.job-log-line:target > .line-num {
|
|
color: var(--color-primary);
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.log-time-seconds {
|
|
padding-right: 2px;
|
|
}
|
|
|
|
.job-log-line .log-time,
|
|
.log-time-stamp {
|
|
color: var(--color-text-light-3);
|
|
margin-left: 10px;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
</style>
|