Show job cobs on repo source and patches

This commit is contained in:
Rūdolfs Ošiņš
2026-04-21 17:29:39 +02:00
parent 7cef0618ce
commit 64f91fa41d
9 changed files with 265 additions and 21 deletions
+2
View File
@@ -2,6 +2,7 @@ import type { BaseUrl } from "./lib/fetcher.js";
import type {
Blob,
DiffResponse,
Job,
Remote,
Repo,
RepoListQuery,
@@ -71,6 +72,7 @@ export type {
HunkLine,
Issue,
IssueState,
Job,
LifecycleState,
Merge,
Patch,
+38
View File
@@ -125,6 +125,29 @@ const diffResponseSchema = object({
files: record(string(), diffBlobSchema),
});
const statusSchema = union([
literal("started"),
literal("failed"),
literal("succeeded"),
]);
const runSchema = object({
runId: string(),
node: authorSchema,
status: statusSchema,
log: string(),
});
const jobSchema = object({
jobId: string(),
commit: string(),
runs: array(runSchema),
});
export type Job = z.infer<typeof jobSchema>;
const jobsSchema = array(jobSchema) satisfies ZodSchema<Job[]>;
export type RepoListQuery = {
page?: number;
perPage?: number;
@@ -340,6 +363,21 @@ export class Client {
);
}
public async getJobsByCommit(
rid: string,
commit: string,
options?: RequestOptions,
): Promise<Job[]> {
return this.#fetcher.fetchOk(
{
method: "GET",
path: `repos/${rid}/xyz.radworks.job/${commit}`,
options,
},
jobsSchema,
);
}
public async getIssueById(
rid: string,
issueId: string,
+8 -1
View File
@@ -3,6 +3,8 @@
export let stylePopoverPositionBottom: string | undefined = undefined;
export let stylePopoverPositionLeft: string | undefined = undefined;
export let stylePopoverPositionTop: string | undefined = undefined;
export let canMouseOver: boolean = false;
let visible: boolean = false;
@@ -37,10 +39,15 @@
{#if visible}
<div style:position="absolute">
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="popover"
on:mouseenter={() =>
canMouseOver ? setVisible(true) : setVisible(false)}
on:mouseleave={() => (canMouseOver ? setVisible(false) : undefined)}
style:left={stylePopoverPositionLeft}
style:bottom={stylePopoverPositionBottom}>
style:bottom={stylePopoverPositionBottom}
style:top={stylePopoverPositionTop}>
<slot name="popover" />
</div>
</div>
+143
View File
@@ -0,0 +1,143 @@
<script context="module" lang="ts">
import type { BaseUrl, Job } from "@http-client";
import { LRUCache } from "lru-cache";
import { HttpdClient } from "@http-client";
const inFlightJobs = new LRUCache<string, Promise<Job[]>>({ max: 50 });
function fetchJobs(
baseUrl: BaseUrl,
rid: string,
commit: string,
): Promise<Job[]> {
const key = `${baseUrl.scheme}://${baseUrl.hostname}:${baseUrl.port}/${rid}/${commit}`;
let promise = inFlightJobs.get(key);
if (!promise) {
promise = new HttpdClient(baseUrl).repo.getJobsByCommit(rid, commit);
promise.catch(() => inFlightJobs.delete(key));
inFlightJobs.set(key, promise);
}
return promise;
}
</script>
<script lang="ts">
import HoverPopover from "@app/components/HoverPopover.svelte";
import Icon from "@app/components/Icon.svelte";
import NodeId from "@app/components/NodeId.svelte";
import Badge from "./Badge.svelte";
import ExternalLink from "./ExternalLink.svelte";
import Loading from "./Loading.svelte";
export let baseUrl: BaseUrl;
export let commit: string;
export let rid: string;
export let stylePopoverPositionBottom: string | undefined = undefined;
export let stylePopoverPositionLeft: string | undefined = undefined;
export let stylePopoverPositionTop: string | undefined = undefined;
$: jobsPromise = fetchJobs(baseUrl, rid, commit);
type JobStatus = "succeeded" | "failed" | "mixed";
function computeStatus(jobs: Job[]): JobStatus | undefined {
if (jobs.length === 0) return undefined;
const allSucceeded = jobs.every(j =>
j.runs.every(r => r.status === "succeeded"),
);
const allFailed = jobs.every(j => j.runs.every(r => r.status === "failed"));
if (allSucceeded) return "succeeded";
if (allFailed) return "failed";
return "mixed";
}
</script>
<style>
.status {
display: inline-flex;
align-items: center;
gap: 0.25rem;
white-space: nowrap;
font: var(--txt-body-m-regular);
}
.status.succeeded {
color: var(--color-text-open);
}
.status.failed {
color: var(--color-feedback-error-text);
}
.status.mixed {
color: var(--color-text-tertiary);
}
.popover-grid {
display: grid;
grid-template-columns: auto auto auto;
align-items: center;
gap: 0.5rem;
font: var(--txt-body-m-regular);
}
</style>
{#await jobsPromise}
<div style:padding-left="0.5rem">
<Loading small noDelay />
</div>
{:then jobs}
{@const jobStatus = computeStatus(jobs)}
{#if jobStatus}
<HoverPopover
canMouseOver
{stylePopoverPositionBottom}
{stylePopoverPositionLeft}
{stylePopoverPositionTop}>
<div
class="global-flex-item"
slot="toggle"
style:cursor="default"
role="status">
{#if jobStatus === "succeeded"}
<span class="status succeeded" title="All CI jobs passed">
<Icon name="checkmark" /> All passed
</span>
{:else if jobStatus === "failed"}
<span class="status failed" title="All CI jobs failed">
<Icon name="close" /> All failed
</span>
{:else}
<span class="status mixed" title="CI jobs have mixed results">
<Icon name="warning" /> Mixed
</span>
{/if}
</div>
<div slot="popover" class="popover-grid">
{#each jobs as job (job.jobId)}
{#each job.runs as run (run.runId)}
{#if run.status === "started"}
<Badge variant="foreground" title="Job started">
<Icon name="hourglass" /> Started
</Badge>
{:else if run.status === "failed"}
<Badge variant="negative" title="Job failed">
<Icon name="close" /> Failed
</Badge>
{:else if run.status === "succeeded"}
<Badge variant="positive" title="Job passed">
<Icon name="checkmark" /> Passed
</Badge>
{/if}
<NodeId {baseUrl} nodeId={run.node.id} alias={run.node.alias} />
<ExternalLink href={run.log}>logs</ExternalLink>
{/each}
{/each}
</div>
</HoverPopover>
{/if}
{:catch}
<!-- Silently ignore errors from old nodes without the jobs endpoint. -->
{/await}
+15 -2
View File
@@ -1,5 +1,6 @@
<script lang="ts">
import type { BaseUrl, CommitHeader } from "@http-client";
import type { Snippet } from "svelte";
import { twemoji } from "@app/lib/utils";
@@ -14,6 +15,7 @@
export let baseUrl: BaseUrl;
export let commit: CommitHeader;
export let repoId: string;
export let children: Snippet | undefined = undefined;
let commitMessageVisible = false;
</script>
@@ -44,6 +46,11 @@
margin-left: auto;
height: 21px;
}
.authorship {
display: flex;
align-items: center;
gap: 0.5rem;
}
.summary:hover {
text-decoration: underline;
}
@@ -86,15 +93,21 @@
<pre>{commit.description.trim()}</pre>
</div>
{/if}
<div class="global-hide-on-small-desktop-up">
<div class="authorship global-hide-on-small-desktop-up">
<CompactCommitAuthorship {commit}>
<Id id={commit.id} />
</CompactCommitAuthorship>
{#if children}
{@render children()}
{/if}
</div>
</div>
<div class="right">
<div style="display: flex; gap: 0.5rem; height: 21px; align-items: center;">
<div class="global-hide-on-mobile-down">
<div class="authorship global-hide-on-mobile-down">
{#if children}
{@render children()}
{/if}
<CompactCommitAuthorship {commit}>
<Id id={commit.id} />
</CompactCommitAuthorship>
+14 -2
View File
@@ -24,6 +24,7 @@
import ExpandButton from "@app/components/ExpandButton.svelte";
import IconButton from "@app/components/IconButton.svelte";
import Icon from "@app/components/Icon.svelte";
import JobCob from "@app/components/JobCob.svelte";
import Link from "@app/components/Link.svelte";
import Loading from "@app/components/Loading.svelte";
import Markdown from "@app/components/Markdown.svelte";
@@ -436,10 +437,21 @@
{/if}
{#if response?.commits}
<div class="commits">
{#each response.commits.toReversed() as commit}
{#each response.commits.toReversed() as commit, index}
<div class="commit" style:position="relative">
<div class="commit-dot"></div>
<CobCommitTeaser {commit} {baseUrl} {repoId} />
<CobCommitTeaser {commit} {baseUrl} {repoId}>
{#if response.commits.length - 1 === index}
<div class="global-flex-item" style:margin-right="0.25rem">
<JobCob
{baseUrl}
rid={repoId}
commit={commit.id}
stylePopoverPositionBottom="2rem"
stylePopoverPositionLeft="0" />
</div>
{/if}
</CobCommitTeaser>
</div>
{/each}
</div>
+8
View File
@@ -11,6 +11,7 @@
import Icon from "@app/components/Icon.svelte";
import Id from "@app/components/Id.svelte";
import InlineTitle from "@app/views/repos/components/InlineTitle.svelte";
import JobCob from "@app/components/JobCob.svelte";
import Layout from "./Layout.svelte";
import Link from "@app/components/Link.svelte";
import Separator from "./Separator.svelte";
@@ -119,6 +120,13 @@
</span>
<CommitAuthorship {header}>
<Id id={header.id} ariaLabel="commit-id" />
<JobCob
slot="after-timestamp"
{baseUrl}
rid={repo.rid}
commit={header.id}
stylePopoverPositionTop="0.5rem"
stylePopoverPositionLeft="0" />
</CommitAuthorship>
<span class="txt-body-m-regular">
{header.parents.length === 1 ? "Parent" : "Parents"}:
@@ -45,6 +45,7 @@
<span title={absoluteTimestamp(header.committer.time)}>
{formatTimestamp(header.committer.time)}
</span>
<slot name="after-timestamp" />
{:else}
<div class="person">
<img class="avatar" alt="avatar" src={gravatarURL(header.author.email)} />
@@ -63,5 +64,6 @@
<span title={absoluteTimestamp(header.committer.time)}>
{formatTimestamp(header.committer.time)}
</span>
<slot name="after-timestamp" />
{/if}
</span>
+19
View File
@@ -13,6 +13,7 @@
import Button from "@app/components/Button.svelte";
import CommitButton from "../components/CommitButton.svelte";
import Icon from "@app/components/Icon.svelte";
import JobCob from "@app/components/JobCob.svelte";
import Link from "@app/components/Link.svelte";
import PeerBranchSelector from "./PeerBranchSelector.svelte";
@@ -128,6 +129,7 @@
{repo}
{selectedBranch} />
{/if}
<div class="global-flex-item" style:gap="1px">
<CommitButton
variant={commitButtonVariant}
styleMinWidth="0"
@@ -144,6 +146,15 @@
</Button>
</Link>
{/if}
<div style:margin-left="0.5rem">
<JobCob
baseUrl={node}
rid={repo.rid}
commit={lastCommit.id}
stylePopoverPositionTop="0.5rem"
stylePopoverPositionLeft="0" />
</div>
</div>
</div>
<div class="header">
@@ -209,5 +220,13 @@
</Button>
</Link>
{/if}
<div style:margin-left="0.5rem">
<JobCob
baseUrl={node}
rid={repo.rid}
commit={lastCommit.id}
stylePopoverPositionTop="0.5rem"
stylePopoverPositionLeft="0" />
</div>
</div>
</div>