mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-10-18 19:30:27 +08:00
Show job cobs on repo source and patches
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user