diff --git a/public/actions-run.css b/public/actions-run.css new file mode 100644 index 0000000..8709868 --- /dev/null +++ b/public/actions-run.css @@ -0,0 +1,462 @@ +.page-content.repository.github-actions-run, +.full.height:has(> .action-view-right):has(.github-actions-run) { + padding-top: 0; + margin-top: 0 !important; + .action-view-container.ui.container { + width: 100%; + margin: 0; + padding: 16px 32px 32px; + } + .action-view-header.action-view-header { + margin: 0 0 16px; + } + .actions-run-back { + display: inline-block; + color: var(--color-text-light-1); + font-size: 14px; + line-height: 21px; + margin-bottom: 8px; + } + .action-info-summary { + gap: 16px; + align-items: center; + } + .action-info-summary-title { + gap: 8px; + min-width: 0; + } + .action-info-summary-title-text { + font-size: 20px; + line-height: 28px; + font-weight: 600; + margin: 0; + overflow-wrap: anywhere; + } + .actions-run-index { + font-weight: 400; + color: var(--color-text-light-1); + } + .action-info-summary-actions { + flex-shrink: 0; + display: flex; + gap: 8px; + } + .action-info-summary-actions .ui.button { + height: 32px; + min-height: 32px; + padding: 5px 12px; + margin: 0; + font-size: 14px; + } + .action-summary { + display: none; + font-size: 12px; + line-height: 18px; + margin-left: 28px; + } + .action-summary + .action-summary { + display: none; + } + .action-view-body { + display: grid; + grid-template-columns: 280px minmax(0, 1fr); + gap: 48px; + align-items: start; + } + .action-view-left.action-view-left.action-view-left { + overflow: visible; + margin: 0; + width: auto; + min-width: 0; + max-width: none; + } + .actions-summary-link, + .actions-workflow-source { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 8px; + margin: 0; + border-radius: var(--border-radius); + font-size: 14px; + line-height: 20px; + color: var(--color-text); + } + .actions-summary-link { + position: relative; + font-weight: 400; + margin-bottom: 8px; + } + .actions-summary-link[aria-current="page"] { + font-weight: 600; + background: var(--github-control-transparent-bgColor-active); + } + .actions-summary-link[aria-current="page"]::before { + display: block; + content: ""; + position: absolute; + left: -8px; + top: 4px; + height: 24px; + width: 4px; + background: var(--color-primary); + border-radius: var(--border-radius); + } + .actions-jobs-heading { + font-size: 12px; + line-height: 18px; + color: var(--color-text-light-1); + margin: 0 0 8px; + padding: 8px 0 16px; + border-bottom: 1px solid var(--color-light-border); + } + .actions-details-heading { + font-size: 12px; + line-height: 18px; + color: var(--color-text-light-1); + margin: 16px 0 8px; + padding: 16px 8px 0; + border-top: 1px solid var(--color-light-border); + } + .actions-workflow-source > svg, + .actions-summary-link > svg { + flex-shrink: 0; + color: var(--color-text-light-1); + } + .job-brief-item.job-brief-item.job-brief-item { + margin: 0 0 4px; + padding: 6px 8px; + min-height: 32px; + font-size: 14px; + line-height: 20px; + } + .job-brief-item .step-summary-duration { + font-size: 12px; + } + .actions-summary-link, + .actions-workflow-source, + .job-brief-item.job-brief-item { + text-decoration: none; + &:hover { + text-decoration: none; + color: var(--color-text); + background: var(--github-control-transparent-bgColor-hover); + } + &:focus-visible { + outline: 2px solid var(--color-primary); + outline-offset: -2px; + } + } + .actions-summary-link[aria-current="page"]:hover, + .job-brief-item.job-brief-item.selected:hover { + background: var(--github-control-transparent-bgColor-active); + } + .actions-job-card { + text-decoration: none; + &:hover { + text-decoration: none; + color: var(--color-primary); + } + &:focus-visible { + outline: 2px solid var(--color-primary); + outline-offset: 2px; + } + &:active { + background: var(--github-control-transparent-bgColor-active); + } + } + .job-step-summary:focus-visible { + outline: 2px solid var(--color-primary); + outline-offset: -2px; + } + .job-brief-item.selected { + background: var(--github-control-transparent-bgColor-active); + } + .action-view-right { + min-width: 0; + width: auto; + margin: 0; + border: 1px solid var(--color-light-border); + box-shadow: none; + border-radius: var(--border-radius); + min-height: calc(100dvh - 260px); + background: var(--color-menu); + } + .job-info-header { + height: 80px !important; + padding: 16px !important; + background: var(--color-menu); + border-bottom: 1px solid var(--color-light-border); + } + .job-info-header-title { + color: var(--color-primary) !important; + font-size: 16px; + line-height: 28px; + font-weight: 600; + } + .job-info-header-right { + display: flex; + align-items: center; + gap: 8px; + } + .job-info-header-left { + min-width: 0; + } + .job-info-header-detail { + margin: 0 !important; + color: var(--color-text-light-1); + font-size: 12px; + line-height: 18px; + } + .job-step-container { + padding: 8px; + } + .job-step-section { + margin: 0 !important; + } + .job-step-summary.job-step-summary { + height: 40px; + min-height: 40px; + padding: 8px !important; + gap: 8px; + color: var(--color-text-light-1); + font: 14px/21px var(--fonts-proportional); + border-radius: var(--border-radius); + } + .job-step-summary .octicon-check-circle-fill { + color: var(--color-text-light-1) !important; + } + .job-step-summary:hover { + background: var(--github-control-transparent-bgColor-hover); + } + .job-step-summary.selected { + top: 80px; + background: var(--color-box-header) !important; + } + .job-step-summary .step-summary-duration { + font: 12px/18px var(--fonts-proportional); + color: var(--color-text-light-1); + } + .job-step-logs { + padding: 8px 0; + background: var(--color-console-bg); + overflow: auto; + } + .job-log-line { + font: 12px/20px var(--fonts-monospace); + } + .actions-log-search { + display: flex; + align-items: center; + gap: 8px; + } + .actions-log-search input { + width: 256px; + max-width: 100%; + height: 32px; + padding: 5px 12px; + border: 1px solid var(--color-light-border); + border-radius: var(--border-radius); + background: var(--color-body); + color: var(--color-text); + font-size: 14px; + box-shadow: none; + } + .actions-log-search span { + font-size: 12px; + color: var(--color-text-light-1); + } + .job-log-line[hidden] { + display: none; + } + .actions-run-overview { + min-width: 0; + grid-column: 2; + grid-row: 1; + } + .actions-run-facts { + display: flex; + flex-wrap: wrap; + gap: 24px 48px; + padding: 20px; + border: 1px solid var(--color-light-border); + border-radius: var(--border-radius); + margin-bottom: 16px; + } + .actions-fact-label { + display: block; + width: 100%; + color: var(--color-text-light-1); + font-size: 12px; + line-height: 18px; + margin-bottom: 6px; + } + .actions-run-trigger { + flex: 0 1 280px; + display: flex; + align-content: start; + align-items: center; + flex-wrap: wrap; + gap: 0 8px; + min-width: 0; + } + .actions-run-trigger > a { + font-size: 14px; + color: var(--color-text); + } + .actions-run-trigger .actions-summary-commit { + font-size: 12px; + font-family: var(--fonts-monospace); + text-decoration: underline; + } + .actions-run-trigger .actions-summary-branch { + padding: 0 6px; + font-size: 12px; + background: var(--color-primary-alpha-10); + color: var(--color-primary); + border-radius: var(--border-radius); + } + .actions-run-fact strong { + font-size: 14px; + line-height: 21px; + text-transform: capitalize; + } + .actions-workflow-card { + padding: 20px; + border: 1px solid var(--color-light-border); + border-radius: var(--border-radius); + background: var(--color-menu); + } + .actions-workflow-title { + font-size: 16px; + line-height: 24px; + font-weight: 600; + color: var(--color-text); + } + .actions-job-cards { + display: flex; + flex-wrap: wrap; + gap: 24px; + padding: 48px 0; + } + .actions-job-card { + display: flex; + align-items: center; + gap: 8px; + min-width: 200px; + max-width: 100%; + padding: 12px; + border: 1px solid var(--color-light-border); + border-radius: var(--border-radius); + background: var(--color-box-header); + color: var(--color-text); + } + .actions-job-card strong { + overflow-wrap: anywhere; + font-size: 14px; + } + .actions-job-duration { + margin-left: auto; + font-size: 12px; + color: var(--color-text-light-1); + } + &:not(.actions-summary-mode) .actions-run-overview { + display: none; + } + &.actions-summary-mode .action-view-right { + display: none; + } + &.actions-summary-mode .job-brief-item.selected { + font-weight: 400; + background: transparent; + &:hover { + background: var(--github-control-transparent-bgColor-hover); + } + } + &.actions-summary-mode .job-brief-item.selected::after { + display: none; + } + @media (max-width: 1011.98px) { + .action-view-body { + grid-template-columns: 224px minmax(0, 1fr); + gap: 24px; + } + } + @media (max-width: 767.98px) { + .action-view-container.ui.container { + padding: 16px; + } + .action-info-summary { + flex-wrap: wrap; + gap: 8px; + } + .action-info-summary-actions { + flex-wrap: wrap; + } + .action-view-header.action-view-header { + margin-bottom: 24px; + } + .action-view-body { + display: flex; + flex-direction: column; + gap: 16px; + } + .action-view-left.action-view-left.action-view-left { + width: 100%; + } + .job-brief-list { + display: flex; + flex-wrap: wrap; + gap: 4px; + } + .job-brief-item.job-brief-item.job-brief-item { + flex: 1; + } + .actions-jobs-heading { + padding: 4px 0 8px; + } + .actions-workflow-source, + .actions-details-heading { + display: none; + } + .action-view-right, + .actions-run-overview { + width: 100%; + } + .actions-run-facts { + gap: 16px; + padding: 16px; + } + .actions-run-trigger { + flex-basis: 100%; + } + .actions-workflow-card { + padding: 16px; + } + .actions-job-cards { + padding: 24px 0; + gap: 12px; + } + .actions-job-card { + width: 100%; + } + .actions-log-search input { + width: 170px; + } + .actions-log-search span { + display: none; + } + .job-step-summary.job-step-summary { + font-size: 12px; + } + } +} + +/* Forgejo teleports the job panel outside the run mount in fullscreen mode. */ +.full.height:has(.github-actions-run) > .action-view-right { + position: fixed; + inset: 0; + z-index: 1000; + overflow: auto; + border-radius: 0; +} diff --git a/public/assets/js/actions-run.js b/public/assets/js/actions-run.js new file mode 100644 index 0000000..ff31fe6 --- /dev/null +++ b/public/assets/js/actions-run.js @@ -0,0 +1,156 @@ +(() => { + const mount = document.querySelector("#repo-action-view"); + const page = document.querySelector(".github-actions-run"); + if (!mount || !page) return; + const initial = JSON.parse(mount.dataset.initialPostResponse); + const run = initial.state.run; + let fingerprint = ""; + let isSummary = location.hash === "#summary"; + const make = (tag, className, text) => { + const node = document.createElement(tag); + node.className = className; + if (text !== undefined) node.textContent = text; + return node; + }; + function render() { + const header = mount.querySelector(".action-view-header"); + const sidebar = mount.querySelector(".action-view-left"); + const right = mount.querySelector(".action-view-right"); + const body = mount.querySelector(".action-view-body"); + if (!header || !sidebar || !right || !body) return; + page.classList.toggle("actions-summary-mode", isSummary); + if (!header.querySelector(".actions-run-back")) { + const back = make("a", "actions-run-back", "← " + mount.dataset.workflowName); + back.href = mount.dataset.workflowUrl; + header.prepend(back); + } + const title = header.querySelector(".action-info-summary-title-text"); + if (title && !title.querySelector(".actions-run-index")) + title.append(make("span", "actions-run-index", " #" + mount.dataset.runIndex)); + if (!sidebar.querySelector(".actions-summary-link")) { + const summary = make("a", "actions-summary-link", "Summary"); + summary.href = run.link + "#summary"; + const homeIcon = document.querySelector("#actions-nav-icons")?.content.querySelector('[data-icon="home"] svg'); + if (homeIcon) summary.prepend(homeIcon.cloneNode(true)); + sidebar.prepend(summary); + const jobs = make("h3", "actions-jobs-heading", "All jobs"); + summary.after(jobs); + const source = make("a", "actions-workflow-source", "Workflow file"); + source.href = mount.dataset.workflowSourceUrl; + const fileIcon = document.querySelector("#actions-nav-icons")?.content.querySelector('[data-icon="file"] svg'); + if (fileIcon) source.prepend(fileIcon.cloneNode(true)); + sidebar.append(make("h3", "actions-details-heading", "Run details"), source); + } + sidebar.querySelector(".actions-summary-link").setAttribute("aria-current", isSummary ? "page" : "false"); + const tools = right.querySelector(".job-info-header-right"); + if (tools && !tools.querySelector(".actions-log-search")) { + const search = make("div", "actions-log-search"); + const input = make("input", ""); + input.type = "search"; + input.placeholder = "Search loaded logs"; + input.setAttribute("aria-label", "Search loaded logs"); + const result = make("span", ""); + result.setAttribute("aria-live", "polite"); + search.append(input, result); + tools.prepend(search); + } + filterLogs(); + const jobs = [...sidebar.querySelectorAll(".job-brief-item")]; + const status = header.querySelector(".action-info-summary-title > span")?.getAttribute("aria-label") || run.status; + const next = JSON.stringify([ + status, + jobs.map(job => [job.href, job.textContent, job.querySelector("svg")?.getAttribute("class")]), + ]); + if (next === fingerprint && body.querySelector(".actions-run-overview")) return; + fingerprint = next; + const overview = make("section", "actions-run-overview"); + overview.setAttribute("aria-label", "Run summary"); + const facts = make("div", "actions-run-facts"); + const triggered = make("div", "actions-run-trigger"); + triggered.append(make("span", "actions-fact-label", "Triggered by")); + const author = make("a", "", run.commit.pusher.displayName); + author.href = run.commit.pusher.link; + triggered.append(author); + const commit = make("a", "actions-summary-commit", run.commit.shortSHA.slice(0, 7)); + commit.href = run.commit.link; + triggered.append(commit); + if (run.commit.branch) { + const branch = make("a", "actions-summary-branch", run.commit.branch.name); + branch.href = run.commit.branch.link; + triggered.append(branch); + } + facts.append(triggered); + for (const [name, value] of [ + ["Status", status], + ["Jobs", String(jobs.length)], + ]) { + const fact = make("div", "actions-run-fact"); + fact.append(make("span", "actions-fact-label", name), make("strong", "", value)); + facts.append(fact); + } + overview.append(facts); + const workflow = make("section", "actions-workflow-card"); + const heading = make("a", "actions-workflow-title", mount.dataset.workflowName); + heading.href = mount.dataset.workflowSourceUrl; + workflow.append(heading); + const cards = make("div", "actions-job-cards"); + for (const job of jobs) { + const card = make("a", "actions-job-card"); + card.href = job.href; + const icon = job.querySelector(".job-brief-item-left > span"); + if (icon) card.append(icon.cloneNode(true)); + card.append(make("strong", "", job.querySelector(".job-brief-name")?.textContent.trim())); + card.append( + make("span", "actions-job-duration", job.querySelector(".step-summary-duration")?.textContent.trim()) + ); + cards.append(card); + } + workflow.append(cards); + overview.append(workflow); + body.querySelector(".actions-run-overview")?.remove(); + body.append(overview); + } + new MutationObserver(() => { + filterLogs(); + render(); + }).observe(page.parentElement, { childList: true, subtree: true }); + function filterLogs() { + const panel = document.querySelector(".action-view-right"); + const input = panel?.querySelector(".actions-log-search input"); + if (!input) return; + const query = input.value.trim().toLowerCase(); + let matches = 0; + for (const line of panel.querySelectorAll(".job-log-line")) { + line.hidden = !!query && !line.textContent.toLowerCase().includes(query); + if (!line.hidden) matches++; + } + const result = input.nextElementSibling; + const text = query ? `${matches} ${matches === 1 ? "match" : "matches"}` : ""; + if (result.textContent !== text) result.textContent = text; + } + document.addEventListener("input", event => { + if (event.target.matches(".actions-log-search input")) filterLogs(); + }); + mount.addEventListener("click", event => { + if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return; + const link = event.target.closest("a"); + if (link?.matches(".actions-summary-link")) { + event.preventDefault(); + isSummary = true; + if (location.hash !== "#summary") history.pushState({}, "", run.link + "#summary"); + render(); + } else if (link?.matches(".job-brief-item") && !event.target.closest(".link-action")) { + isSummary = false; + render(); + } + }); + window.addEventListener("hashchange", () => { + isSummary = location.hash === "#summary"; + render(); + }); + window.addEventListener("popstate", () => { + isSummary = location.hash === "#summary"; + render(); + }); + render(); +})(); diff --git a/templates/repo/actions/view.tmpl b/templates/repo/actions/view.tmpl new file mode 100644 index 0000000..f4368bf --- /dev/null +++ b/templates/repo/actions/view.tmpl @@ -0,0 +1,49 @@ +{{template "base/head" .}} + +
+ + +
+
+
+ + + +{{template "base/footer" .}}