Add rudimentary "Browse all repos on node" view

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.
This commit is contained in:
Cameron Eldridge
2025-09-15 16:02:35 +02:00
committed by Rūdolfs Ošiņš
parent 5cf657249f
commit 97dac7e28f
2 changed files with 170 additions and 65 deletions
+168
View File
@@ -0,0 +1,168 @@
<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;
}
@media (max-width: 1010.98px) {
.repos {
margin-top: 3rem;
}
.footer {
flex-direction: column;
}
.pagination {
margin-left: 0;
}
}
</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>
+2 -65
View File
@@ -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>