Radish alpha
r
rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5
Radicle web interface
Radicle
Git
Add rudimentary "Browse all repos on node" view
Merged did:key:z6Mkhuez...D9KR opened 8 months ago

Seemed to me there was no way to browse all the repositories being seeded on a node. On the massive public nodes this is not that useful a feature since there are too many and too jumbled an assortment, but for a smaller personal node, this feature might be valuable to make the explorer’s contents a bit easier to explore.

Went with the most unintrusive implementation of this idea for now; just enough that it “does the job” (you can access this view, and paginate the results) but does not have any of the bells and whistles it should have (search/filtering, sorting, page query parameters support).

As part of this work, since the repos section gets much more complex, I extracted the repos part of the page into its own component. Otherwise, it is just very simply allowing the parameters to the fetchRepoInfos call to be manipulated from the page.


I’ve deployed this version of the explorer to my own server, to see the results live.

https://app.seed.eldridge.cam/nodes/seed.eldridge.cam/

2 files changed +165 -65 5cf65724 97dac7e2
added src/views/nodes/ReposView.svelte
@@ -0,0 +1,163 @@
+
<script lang="ts">
+
  import type { BaseUrl, NodeStats } from "@http-client";
+

+
  import * as router from "@app/lib/router";
+
  import { baseUrlToString } from "@app/lib/utils";
+
  import { fetchRepoInfos } from "@app/components/RepoCard";
+
  import { handleError } from "@app/views/nodes/error";
+

+
  import Loading from "@app/components/Loading.svelte";
+
  import Placeholder from "@app/components/Placeholder.svelte";
+
  import RepoCard from "@app/components/RepoCard.svelte";
+

+
  export let baseUrl: BaseUrl;
+
  export let stats: NodeStats;
+

+
  let listState: "pinned" | "all" = "pinned";
+

+
  $: page = 0;
+
  $: perPage = listState === "pinned" ? stats.repos.total : 24;
+
  $: totalPages = Math.ceil(stats.repos.total / perPage);
+

+
  function showPinned() {
+
    listState = "pinned";
+
    page = 0;
+
  }
+
  function showAll() {
+
    listState = "all";
+
  }
+
</script>
+

+
<style>
+
  .subtitle,
+
  .pagination {
+
    font-size: var(--font-size-small);
+
    color: var(--color-foreground-dim);
+
  }
+
  .pagination {
+
    display: flex;
+
    gap: 0.25rem;
+
    margin-left: auto;
+
  }
+
  .repos {
+
    margin-top: 0;
+
  }
+
  .repo-grid {
+
    display: grid;
+
    grid-template-columns: repeat(auto-fill, minmax(21rem, 1fr));
+
    gap: 1rem;
+
  }
+
  .empty-state {
+
    display: flex;
+
    flex-direction: column;
+
    align-items: center;
+
    justify-content: center;
+
    gap: 0.5rem;
+
    height: 35vh;
+
    font-size: var(--font-size-small);
+
  }
+
  .text-button {
+
    background: none;
+
    border: none;
+
    font: inherit;
+
    color: inherit;
+
    margin: 0;
+
    padding: 0;
+
  }
+
  .text-button:not(:disabled) {
+
    cursor: pointer;
+
  }
+
  .text-button:hover:not(:disabled) {
+
    text-decoration: underline;
+
  }
+
  .current-page {
+
    text-decoration: underline;
+
  }
+
  .footer {
+
    display: flex;
+
    gap: 0.5rem 1rem;
+
    margin-top: 1rem;
+
    flex-wrap: wrap-reverse;
+
  }
+

+
  @media (max-width: 719.98px) {
+
    .repos {
+
      margin-top: 3rem;
+
    }
+
  }
+
</style>
+

+
<div class="repos">
+
  {#await fetchRepoInfos(baseUrl, { show: listState, perPage, page })}
+
    <div style:height="35vh">
+
      <Loading small center />
+
    </div>
+
  {:then repoInfos}
+
    {#if repoInfos.length > 0}
+
      <div class="repo-grid">
+
        {#each repoInfos as repoInfo}
+
          <RepoCard {baseUrl} {repoInfo} />
+
        {/each}
+
      </div>
+
      <div class="footer">
+
        {#if listState === "pinned"}
+
          <div class="subtitle">
+
            {repoInfos.length}
+
            pinned {repoInfos.length === 1 ? "repository" : "repositories"} ·
+
            <button class="text-button" on:click={showAll}>Browse all</button>
+
          </div>
+
        {:else}
+
          <div class="subtitle">
+
            {stats.repos.total.toLocaleString()}
+
            seeded {stats.repos.total === 1 ? "repository" : "repositories"} ·
+
            <button class="text-button" on:click={showPinned}>
+
              See pinned
+
            </button>
+
          </div>
+

+
          <div class="pagination">
+
            {#if page !== 0}
+
              <button class="text-button" on:click={() => (page = page - 1)}>
+
                Previous
+
              </button>
+
              ·
+
            {/if}
+

+
            {#each Array.from({ length: Math.min(totalPages, 7) }) as _, i}
+
              {@const startPage = Math.max(page - 3, 0)}
+
              {@const pageNumber = startPage + i}
+
              <button
+
                class="text-button"
+
                class:current-page={page === pageNumber}
+
                on:click={() => (page = pageNumber)}
+
                disabled={page === pageNumber}>
+
                {pageNumber + 1}
+
              </button>
+
            {/each}
+

+
            {#if page !== totalPages - 1}
+
              ·
+
              <button class="text-button" on:click={() => (page = page + 1)}>
+
                Next
+
              </button>
+
            {/if}
+
          </div>
+
        {/if}
+
      </div>
+
    {:else}
+
      <div class="empty-state">
+
        {#if listState === "pinned"}
+
          <Placeholder
+
            iconName="desert"
+
            caption="This node doesn't have any pinned repositories." />
+
        {:else}
+
          <Placeholder
+
            iconName="desert"
+
            caption="This node doesn't seed any repositories." />
+
        {/if}
+
      </div>
+
    {/if}
+
  {:catch error}
+
    {router.push(handleError(error, baseUrlToString(baseUrl)))}
+
  {/await}
+
</div>
modified src/views/nodes/View.svelte
@@ -1,12 +1,8 @@
<script lang="ts">
  import type { BaseUrl, Node, NodeStats } from "@http-client";

-
  import * as router from "@app/lib/router";
  import dompurify from "dompurify";
  import { markdown } from "@app/lib/markdown";
-
  import { baseUrlToString } from "@app/lib/utils";
-
  import { fetchRepoInfos } from "@app/components/RepoCard";
-
  import { handleError } from "@app/views/nodes/error";

  import Settings from "@app/App/Settings.svelte";

@@ -16,11 +12,9 @@
  import Icon from "@app/components/Icon.svelte";
  import IconButton from "@app/components/IconButton.svelte";
  import Link from "@app/components/Link.svelte";
-
  import Loading from "@app/components/Loading.svelte";
  import MobileFooter from "@app/App/MobileFooter.svelte";
-
  import Placeholder from "@app/components/Placeholder.svelte";
  import Popover from "@app/components/Popover.svelte";
-
  import RepoCard from "@app/components/RepoCard.svelte";
+
  import ReposView from "./ReposView.svelte";

  import PolicyExplainer from "./PolicyExplainer.svelte";
  import SeedSelector from "./SeedSelector.svelte";
@@ -138,32 +132,6 @@
    height: 2rem;
  }

-
  .subtitle {
-
    font-size: var(--font-size-small);
-
    color: var(--color-foreground-dim);
-
    display: flex;
-
    align-items: center;
-
    gap: 0.5rem;
-
    width: 100%;
-
    margin-top: 1rem;
-
  }
-
  .repos {
-
    margin-top: 0;
-
  }
-
  .repo-grid {
-
    display: grid;
-
    grid-template-columns: repeat(auto-fill, minmax(21rem, 1fr));
-
    gap: 1rem;
-
  }
-
  .empty-state {
-
    display: flex;
-
    flex-direction: column;
-
    align-items: center;
-
    justify-content: center;
-
    gap: 0.5rem;
-
    height: 35vh;
-
    font-size: var(--font-size-small);
-
  }
  .box {
    font-size: var(--font-size-small);
    line-height: 1.625rem;
@@ -220,9 +188,6 @@
    .container {
      padding: 0;
    }
-
    .repos {
-
      margin-top: 3rem;
-
    }
    .mobile-footer {
      margin-top: auto;
      display: grid;
@@ -418,35 +383,7 @@
            </div>
          {/if}

-
          <div class="repos">
-
            {#await fetchRepoInfos( baseUrl, { show: "pinned", perPage: stats.repos.total }, )}
-
              <div style:height="35vh">
-
                <Loading small center />
-
              </div>
-
            {:then repoInfos}
-
              {#if repoInfos.length > 0}
-
                <div class="repo-grid">
-
                  {#each repoInfos as repoInfo}
-
                    <RepoCard {baseUrl} {repoInfo} />
-
                  {/each}
-
                </div>
-
                <div class="subtitle">
-
                  {repoInfos.length}
-
                  pinned {repoInfos.length === 1
-
                    ? "repository"
-
                    : "repositories"}
-
                </div>
-
              {:else}
-
                <div class="empty-state">
-
                  <Placeholder
-
                    iconName="desert"
-
                    caption="This node doesn't have any pinned repositories." />
-
                </div>
-
              {/if}
-
            {:catch error}
-
              {router.push(handleError(error, baseUrlToString(baseUrl)))}
-
            {/await}
-
          </div>
+
          <ReposView {baseUrl} {stats} />
        </div>
      </div>
    </div>