Radish alpha
r
rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5
Radicle web interface
Radicle
Git
Show job cobs on repo source and patches
Rūdolfs Ošiņš committed 8 days ago
commit 64f91fa41d3af6c4ce57ee0391c437547140c801
parent 7cef061
9 files changed +265 -21
modified http-client/index.ts
@@ -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,
modified http-client/lib/repo.ts
@@ -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,
modified src/components/HoverPopover.svelte
@@ -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>
added src/components/JobCob.svelte
@@ -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}
modified src/views/repos/Cob/CobCommitTeaser.svelte
@@ -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>
modified src/views/repos/Cob/Revision.svelte
@@ -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>
modified src/views/repos/Commit.svelte
@@ -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"}:
modified src/views/repos/Commit/CommitAuthorship.svelte
@@ -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>
modified src/views/repos/Source/Header.svelte
@@ -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,22 +129,32 @@
      {repo}
      {selectedBranch} />
  {/if}
-
  <CommitButton
-
    variant={commitButtonVariant}
-
    styleMinWidth="0"
-
    hideSummaryOnMobile
-
    repoId={repo.rid}
-
    commit={lastCommit}
-
    baseUrl={node} />
-
  {#if !onCanonical}
-
    <Link route={baseRoute}>
-
      <Button
-
        variant="not-selected"
-
        styleBorderRadius="0 var(--border-radius-sm) var(--border-radius-sm) 0">
-
        <Icon name="close" />
-
      </Button>
-
    </Link>
-
  {/if}
+
  <div class="global-flex-item" style:gap="1px">
+
    <CommitButton
+
      variant={commitButtonVariant}
+
      styleMinWidth="0"
+
      hideSummaryOnMobile
+
      repoId={repo.rid}
+
      commit={lastCommit}
+
      baseUrl={node} />
+
    {#if !onCanonical}
+
      <Link route={baseRoute}>
+
        <Button
+
          variant="not-selected"
+
          styleBorderRadius="0 var(--border-radius-sm) var(--border-radius-sm) 0">
+
          <Icon name="close" />
+
        </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>