Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Show job cobs on repo source and patches
Rūdolfs Ošiņš committed 29 days ago
commit 64f91fa41d3af6c4ce57ee0391c437547140c801
parent 7cef0618ce08bc7bb49393a019577306231230ad
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>