Radish alpha
r
rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5
Radicle web interface
Radicle
Git
Show only pinned projects on node view
Merged rudolfs opened 2 years ago

And style them visually similar to the landing page.

check check-visual check-unit-test check-httpd-api-unit-test check-e2e check-build

πŸ‘‰ Preview
πŸ‘‰ Workflow runs
πŸ‘‰ Branch on GitHub

9 files changed +203 -144 d86ff195 β†’ 975d1823
modified httpd-client/index.ts
@@ -6,7 +6,13 @@ import type {
  Tree,
  DiffResponse,
} from "./lib/project.js";
-
import type { SuccessResponse, CodeLocation, Range } from "./lib/shared.js";
+
import type {
+
  SuccessResponse,
+
  CodeLocation,
+
  Range,
+
  Policy,
+
  Scope,
+
} from "./lib/shared.js";
import type { Comment, Embed } from "./lib/project/comment.js";
import type {
  Commit,
@@ -62,11 +68,13 @@ export type {
  Patch,
  PatchState,
  PatchUpdateAction,
+
  Policy,
  Project,
  Range,
  Remote,
  Review,
  Revision,
+
  Scope,
  Tree,
  Verdict,
};
modified httpd-client/lib/shared.ts
@@ -10,6 +10,9 @@ export const successResponseSchema = object({
  success: literal(true),
}) satisfies ZodSchema<SuccessResponse>;

+
const policySchema = union([literal("allow"), literal("block")]);
+
const scopeSchema = union([literal("followed"), literal("all")]);
+

export const nodeConfigSchema = object({
  alias: string(),
  peers: union([
@@ -38,10 +41,13 @@ export const nodeConfigSchema = object({
      }),
    }),
  }),
-
  policy: union([literal("allow"), literal("block")]),
-
  scope: union([literal("followed"), literal("all")]),
+
  policy: policySchema,
+
  scope: scopeSchema,
});

+
export type Policy = z.infer<typeof policySchema>;
+
export type Scope = z.infer<typeof scopeSchema>;
+

export const rangeSchema = union([
  object({
    type: literal("lines"),
modified public/typography.css
@@ -125,6 +125,9 @@ p {
.txt-bold {
  font-weight: var(--font-weight-bold) !important;
}
+
.txt-semibold {
+
  font-weight: var(--font-weight-semibold) !important;
+
}
.txt-missing {
  color: var(--color-foreground-dim);
}
modified src/components/HoverPopover.svelte
@@ -3,7 +3,9 @@

  export let onShow: () => void = () => {};
  export let stylePopoverPositionLeft: string | undefined = undefined;
+
  export let stylePopoverPositionRight: string | undefined = undefined;
  export let stylePopoverPositionTop: string | undefined = undefined;
+
  export let stylePopoverPositionBottom: string | undefined = undefined;
  export let stylePopoverPadding: string | undefined = "1rem";

  let visible: boolean = false;
@@ -17,38 +19,44 @@
</script>

<style>
+
  .container {
+
    position: relative;
+
  }
  .popover {
    background: var(--color-background-float);
    border-radius: var(--border-radius-regular);
    border: 1px solid var(--color-border-hint);
    box-shadow: var(--elevation-low);
-
    position: relative;
-
    right: 1rem;
+
    position: absolute;
    z-index: 10;
  }
</style>

-
<div
-
  role="button"
-
  tabindex="0"
-
  on:mouseenter={() => setVisible(true)}
-
  on:mouseleave={() => setVisible(false)}>
-
  <slot name="toggle" />
+
<div class="container">
+
  <div
+
    role="button"
+
    tabindex="0"
+
    on:mouseenter={() => setVisible(true)}
+
    on:mouseleave={() => setVisible(false)}>
+
    <slot name="toggle" />

-
  {#if visible}
-
    <!-- If this component is used inside a button (see `NodeId`, for example)
+
    {#if visible}
+
      <!-- If this component is used inside a button (see `NodeId`, for example)
       we don’t want clicks in the popover to trigger button actions. So we
       stop propagation of click events. -->
-
    <!-- svelte-ignore a11y-click-events-have-key-events -->
-
    <!-- svelte-ignore a11y-no-static-element-interactions -->
-
    <div style:position="absolute" on:click|stopPropagation>
-
      <div
-
        class="popover"
-
        style:padding={stylePopoverPadding}
-
        style:left={stylePopoverPositionLeft}
-
        style:top={stylePopoverPositionTop}>
-
        <slot name="popover" />
+
      <!-- svelte-ignore a11y-click-events-have-key-events -->
+
      <!-- svelte-ignore a11y-no-static-element-interactions -->
+
      <div style:position="absolute" on:click|stopPropagation>
+
        <div
+
          class="popover"
+
          style:padding={stylePopoverPadding}
+
          style:left={stylePopoverPositionLeft}
+
          style:right={stylePopoverPositionRight}
+
          style:bottom={stylePopoverPositionBottom}
+
          style:top={stylePopoverPositionTop}>
+
          <slot name="popover" />
+
        </div>
      </div>
-
    </div>
-
  {/if}
+
    {/if}
+
  </div>
</div>
modified src/components/IconSmall.svelte
@@ -39,6 +39,7 @@
    | "globe"
    | "help"
    | "home"
+
    | "info"
    | "issue"
    | "key"
    | "link"
@@ -340,6 +341,13 @@
      fill-rule="evenodd"
      clip-rule="evenodd"
      d="M8.10975 3.42707C8.04691 3.37209 7.95308 3.37209 7.89025 3.42707L2.99592 7.70961C2.7881 7.89145 2.47222 7.87039 2.29038 7.66258C2.10853 7.45476 2.12959 7.13888 2.33741 6.95703L7.23174 2.67449C7.6716 2.28961 8.32839 2.28961 8.76825 2.67449L13.6626 6.95703C13.8704 7.13888 13.8915 7.45476 13.7096 7.66258C13.5278 7.87039 13.2119 7.89145 13.0041 7.70961L8.10975 3.42707ZM4.73737 7.50501C5.01074 7.54407 5.20069 7.79733 5.16164 8.0707L4.71239 11.2155C4.64067 11.7175 5.03022 12.1667 5.53734 12.1667H6.16666V10.6667C6.16666 9.65413 6.98748 8.83332 8 8.83332C9.01252 8.83332 9.83333 9.65413 9.83333 10.6667V12.1667H10.4627C10.9698 12.1667 11.3593 11.7175 11.2876 11.2155L10.8384 8.0707C10.7993 7.79733 10.9893 7.54407 11.2626 7.50501C11.536 7.46596 11.7893 7.65591 11.8283 7.92928L12.2776 11.074C12.4353 12.1785 11.5783 13.1667 10.4627 13.1667H5.53734C4.42167 13.1667 3.56466 12.1785 3.72244 11.074L4.17169 7.92928C4.21074 7.65591 4.46401 7.46596 4.73737 7.50501ZM8.83333 12.1667V10.6667C8.83333 10.2064 8.46023 9.83332 8 9.83332C7.53976 9.83332 7.16666 10.2064 7.16666 10.6667V12.1667H8.83333Z" />
+
  {:else if name === "info"}
+
    <path
+
      d="M9 10.8419C9 11.3865 8.54467 11.8419 8 11.8419C7.45533 11.8419 7 11.3865 7 10.8419C7 10.2972 7.45533 9.84188 8 9.84188C8.54467 9.84188 9 10.2972 9 10.8419Z" />
+
    <path
+
      fill-rule="evenodd"
+
      clip-rule="evenodd"
+
      d="M8 2.5C5.3534 2.5 2.5 4.52821 2.5 8C2.5 11.4718 5.3534 13.5 8 13.5C10.6466 13.5 13.5 11.4718 13.5 8C13.5 7.72386 13.7239 7.5 14 7.5C14.2761 7.5 14.5 7.72386 14.5 8C14.5 12.122 11.0956 14.5 8 14.5C4.90438 14.5 1.5 12.122 1.5 8C1.5 3.87804 4.90438 1.5 8 1.5C9.52918 1.5 10.6747 1.89561 11.4304 2.53262C12.1907 3.17354 12.5274 4.03904 12.4446 4.87775C12.3622 5.71281 11.8678 6.47921 11.0557 6.93153C10.3805 7.30768 9.51064 7.45531 8.5 7.28936V8.35417C8.5 8.63031 8.27614 8.85417 8 8.85417C7.72386 8.85417 7.5 8.63031 7.5 8.35417V6.66667C7.5 6.51033 7.57312 6.363 7.69763 6.26845C7.82214 6.17391 7.9837 6.14305 8.13429 6.18504C9.22008 6.48779 10.0333 6.35637 10.5691 6.05792C11.1056 5.75907 11.4005 5.27625 11.4495 4.77954C11.4981 4.28648 11.3084 3.73766 10.7859 3.2972C10.2588 2.85283 9.36507 2.5 8 2.5Z" />
  {:else if name === "issue"}
    <path
      fill-rule="evenodd"
modified src/components/Popover.svelte
@@ -37,6 +37,9 @@
</script>

<style>
+
  .container {
+
    position: relative;
+
  }
  .popover {
    background: var(--color-background-float);
    border-radius: var(--border-radius-regular);
@@ -50,7 +53,7 @@

<svelte:window on:click={clickOutside} on:touchstart={clickOutside} />

-
<div bind:this={thisComponent} style:position="relative">
+
<div bind:this={thisComponent} class="container">
  <slot name="toggle" {expanded} {toggle} />

  {#if expanded}
modified src/lib/router.ts
@@ -140,7 +140,11 @@ function setTitle(loadedRoute: LoadedRoute) {
  ) {
    title.push(...projectTitle(loadedRoute));
  } else if (loadedRoute.resource === "nodes") {
-
    title.push(loadedRoute.params.baseUrl.hostname);
+
    title.push(
+
      utils.isLocal(loadedRoute.params.baseUrl.hostname)
+
        ? "Local Node"
+
        : loadedRoute.params.baseUrl.hostname,
+
    );
  } else if (loadedRoute.resource === "session") {
    title.push("Authenticating");
    title.push("Radicle");
modified src/views/nodes/View.svelte
@@ -1,49 +1,28 @@
<script lang="ts">
-
  import type { BaseUrl } from "@httpd-client";
+
  import type { BaseUrl, Policy, Scope } from "@httpd-client";
+
  import type { ProjectWithListingData } from "@app/lib/projects";
+

+
  import capitalize from "lodash/capitalize";
+

+
  import { api, httpdStore } from "@app/lib/httpd";
+
  import { isDelegate } from "@app/lib/roles";
  import { isLocal, truncateId } from "@app/lib/utils";
-
  import { loadProjects } from "@app/views/nodes/router";
+

  import AppLayout from "@app/App/AppLayout.svelte";
-
  import ErrorMessage from "@app/components/ErrorMessage.svelte";
-
  import Loading from "@app/components/Loading.svelte";
-
  import ProjectCard from "@app/components/ProjectCard.svelte";
-
  import Button from "@app/components/Button.svelte";
  import CopyableId from "@app/components/CopyableId.svelte";
-
  import { isDelegate } from "@app/lib/roles";
-
  import { api, httpdStore } from "@app/lib/httpd";
-
  import type { ProjectWithListingData } from "@app/lib/projects";
+
  import IconSmall from "@app/components/IconSmall.svelte";
+
  import ProjectCard from "@app/components/ProjectCard.svelte";
+
  import HoverPopover from "@app/components/HoverPopover.svelte";

  export let baseUrl: BaseUrl;
  export let nid: string;
  export let externalAddresses: string[];
-
  export let projectCount: number;
-
  export let projectPageIndex: number;
  export let projects: ProjectWithListingData[] = [];
  export let version: string;
-

-
  let error: any;
-
  let loadingProjects = false;
-

-
  async function loadMore(): Promise<void> {
-
    loadingProjects = true;
-
    try {
-
      const result = await loadProjects(projectPageIndex, baseUrl);
-
      projectCount = result.total;
-
      projects = [...projects, ...result.projects];
-
      projectPageIndex += 1;
-
    } catch (err) {
-
      error = err;
-
    } finally {
-
      loadingProjects = false;
-
    }
-
  }
+
  export let policy: Policy | undefined = undefined;
+
  export let scope: Scope | undefined = undefined;

  $: hostname = isLocal(baseUrl.hostname) ? "Local Node" : baseUrl.hostname;
-
  $: showMoreButton =
-
    !loadingProjects &&
-
    !error &&
-
    projectCount &&
-
    projects.length < projectCount;
-

  $: session =
    $httpdStore.state === "authenticated" && isLocal(api.baseUrl.hostname)
      ? $httpdStore.session
@@ -57,22 +36,44 @@
    justify-content: center;
    padding: 3rem 0 5rem 0;
  }
-
  .wrapper {
-
    width: 720px;
+
  .header {
    display: flex;
+
    gap: 0.5rem;
    flex-direction: column;
-
    justify-content: center;
-
    gap: 3.5rem;
+
    margin-bottom: 2rem;
  }
-
  .header {
+
  .wrapper {
+
    padding: 3rem;
+
    max-width: 72rem;
+
    margin: 0 auto;
+
    width: 100%;
    display: flex;
    flex-direction: column;
-
    gap: 1rem;
  }
-
  .title {
+

+
  .separator {
+
    width: 1px;
+
    background-color: var(--color-fill-separator);
+
    display: flex;
+
    height: 1rem;
+
  }
+
  .subtitle {
+
    font-size: var(--font-size-small);
+
    display: flex;
+
    align-items: center;
+
    gap: 0.5rem;
+
    margin-bottom: 1rem;
+
    width: 100%;
+
  }
+
  .pinned {
    display: flex;
-
    font-size: var(--font-size-x-large);
-
    font-weight: var(--font-weight-bold);
+
    align-items: center;
+
  }
+
  .right {
+
    margin-left: auto;
+
    display: flex;
+
    gap: 0.5rem;
+
    align-items: center;
  }
  .info {
    display: flex;
@@ -83,27 +84,17 @@
    font-family: var(--font-family-monospace);
    font-size: var(--font-size-small);
  }
-
  .projects {
-
    display: flex;
+

+
  .project-grid {
+
    display: grid;
+
    grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
    gap: 1rem;
-
    flex-direction: column;
  }
-
  .more {
-
    min-height: 3rem;
-
    display: flex;
-
    align-items: center;
-
    justify-content: center;
+
  .popover {
+
    width: 18rem;
+
    color: var(--color-foreground-contrast);
  }
-

  @media (max-width: 720px) {
-
    .projects {
-
      gap: 1.5rem;
-
    }
-
    .wrapper {
-
      width: 100%;
-
      padding: 1rem 1.5rem 1.5rem 1.5rem;
-
      gap: 2rem;
-
    }
    .layout {
      width: 100%;
      display: flex;
@@ -120,9 +111,7 @@
  <div class="layout">
    <div class="wrapper">
      <div class="header">
-
        <div class="title">
-
          {hostname}
-
        </div>
+
        <div class="txt-large txt-bold">{hostname}</div>
        <div class="info">
          <div>
            {#each externalAddresses as address}
@@ -143,45 +132,88 @@
        </div>
      </div>

-
      <div class="projects">
-
        {#each projects as { project, activity, lastCommit } (project.id)}
+
      <div class="subtitle">
+
        <div class="pinned txt-semibold">
+
          {projects.length}
+
          {isLocal(baseUrl.hostname) ? "" : "pinned"} projects
+
        </div>
+

+
        {#if !isLocal(baseUrl.hostname)}
+
          <HoverPopover
+
            stylePopoverPositionLeft="-8rem"
+
            stylePopoverPositionBottom="1.5rem">
+
            <div slot="toggle">
+
              <span style:color="var(--color-fill-gray)">
+
                <IconSmall name="info" />
+
              </span>
+
            </div>
+

+
            <div slot="popover" class="popover">
+
              These are pinned projects that were configured to be highlighted
+
              on this node.
+
            </div>
+
          </HoverPopover>
+
        {/if}
+

+
        {#if policy && scope}
+
          <div class="right">
+
            <span>
+
              Seeding Policy: <span class="txt-semibold">
+
                {capitalize(policy)}
+
              </span>
+
            </span>
+
            <span class="separator" />
+
            <span>
+
              Scope:
+
              <span class="txt-semibold">{capitalize(scope)}</span>
+
            </span>
+
            <span style:color="var(--color-fill-gray)">
+
              <HoverPopover
+
                stylePopoverPositionRight="-1rem"
+
                stylePopoverPositionBottom="1.5rem">
+
                <div slot="toggle">
+
                  <span style:color="var(--color-fill-gray)">
+
                    <IconSmall name="info" />
+
                  </span>
+
                </div>
+

+
                <div slot="popover" class="popover">
+
                  {#if policy === "allow"}
+
                    All discovered repositories will get seeded,
+
                  {:else if policy === "block"}
+
                    Only repositories marked as such will get seeded,
+
                  {/if}
+
                  {#if scope === "all"}
+
                    and all changes in those repos, made by any peer, will be
+
                    synced.
+
                  {:else if scope === "followed"}
+
                    and only changes made by explicitly followed peers will be
+
                    synced.
+
                  {/if}
+
                </div>
+
              </HoverPopover>
+
            </span>
+
          </div>
+
        {/if}
+
      </div>
+

+
      <div class="project-grid">
+
        {#each projects as { project, baseUrl, activity, lastCommit }}
          <ProjectCard
-
            compact
            id={project.id}
            name={project.name}
            description={project.description}
-
            {activity}
-
            {baseUrl}
            numberOfIssues={project.issues.open}
            numberOfPatches={project.patches.open}
-
            lastUpdatedTimestamp={lastCommit.commit.committer.time}
            isPrivate={project.visibility?.type === "private"}
            isSeeding={false}
            isDelegate={isDelegate(session?.publicKey, project.delegates) ??
-
              false} />
+
              false}
+
            lastUpdatedTimestamp={lastCommit.commit.committer.time}
+
            {activity}
+
            {baseUrl} />
        {/each}
      </div>
-

-
      {#if loadingProjects}
-
        <div class="more">
-
          <Loading noDelay small />
-
        </div>
-
      {/if}
-

-
      {#if showMoreButton}
-
        <div class="more">
-
          <Button size="large" variant="outline" on:click={loadMore}>
-
            More
-
          </Button>
-
        </div>
-
      {/if}
-

-
      {#if error}
-
        <ErrorMessage
-
          title="Not able to load more projects from this node"
-
          description="You either loaded all remaining projects, or there was a network issue with this seed"
-
          {error} />
-
      {/if}
    </div>
  </div>
</AppLayout>
modified src/views/nodes/router.ts
@@ -1,10 +1,10 @@
-
import type { BaseUrl } from "@httpd-client";
+
import type { BaseUrl, Policy, Scope } from "@httpd-client";
import type { ErrorRoute, NotFoundRoute } from "@app/lib/router/definitions";
import type { ProjectWithListingData } from "@app/lib/projects";

import { HttpdClient } from "@httpd-client";
import { config } from "@app/lib/config";
-
import { baseUrlToUrl } from "@app/lib/utils";
+
import { baseUrlToUrl, isLocal } from "@app/lib/utils";
import { getProjectsListingData } from "@app/lib/projects";
import { handleError } from "@app/views/nodes/error";

@@ -22,39 +22,27 @@ export interface NodesLoadedRoute {
  resource: "nodes";
  params: {
    baseUrl: BaseUrl;
-
    projectPageIndex: number;
    version: string;
    externalAddresses: string[];
    nid: string;
    projects: ProjectWithListingData[];
-
    projectCount: number;
+
    policy?: Policy;
+
    scope?: Scope;
  };
}

-
const PROJECTS_PER_PAGE = 10;
-

-
export async function loadProjects(
-
  page: number,
+
async function loadProjects(
  baseUrl: BaseUrl,
-
): Promise<{
-
  total: number;
-
  projects: ProjectWithListingData[];
-
}> {
+
): Promise<ProjectWithListingData[]> {
  const api = new HttpdClient(baseUrl);
-

-
  const [nodeStats, projects] = await Promise.all([
-
    api.getStats(),
-
    api.project.getAll({ page, perPage: PROJECTS_PER_PAGE, show: "all" }),
-
  ]);
-

+
  const projects = await api.project.getAll({
+
    show: isLocal(baseUrl.hostname) ? "all" : "pinned",
+
  });
  const results = await getProjectsListingData(
    projects.map(p => ({ project: p, baseUrl })),
  );

-
  return {
-
    total: nodeStats.projects.count,
-
    projects: results,
-
  };
+
  return results;
}

export function nodePath(baseUrl: BaseUrl) {
@@ -72,21 +60,20 @@ export async function loadNodeRoute(
): Promise<NodesLoadedRoute | NotFoundRoute | ErrorRoute> {
  const api = new HttpdClient(params.baseUrl);
  try {
-
    const projectPageIndex = 0;
-
    const [node, { projects, total }] = await Promise.all([
+
    const [node, projects] = await Promise.all([
      api.getNode(),
-
      loadProjects(projectPageIndex, params.baseUrl),
+
      loadProjects(params.baseUrl),
    ]);
    return {
      resource: "nodes",
      params: {
-
        projectPageIndex: projectPageIndex + 1,
        baseUrl: params.baseUrl,
        nid: node.id,
        externalAddresses: node.config?.externalAddresses ?? [],
        version: node.version,
        projects: projects,
-
        projectCount: total,
+
        policy: node.config?.policy,
+
        scope: node.config?.scope,
      },
    };
  } catch (error: any) {