Show only pinned projects on node view

And style them visually similar to the landing page.
This commit is contained in:
Rūdolfs Ošiņš
2024-02-22 05:33:36 +00:00
parent 693f8991a0
commit 975d1823a2
7 changed files with 176 additions and 128 deletions
+9 -1
View File
@@ -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,
};
+8 -2
View File
@@ -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"),
+3
View File
@@ -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);
}
+8
View File
@@ -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"
+5 -1
View File
@@ -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");
+129 -97
View File
@@ -1,49 +1,28 @@
<script lang="ts">
import type { BaseUrl } from "@httpd-client";
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 { 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 AppLayout from "@app/App/AppLayout.svelte";
import CopyableId from "@app/components/CopyableId.svelte";
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;
display: flex;
flex-direction: column;
justify-content: center;
gap: 3.5rem;
}
.header {
display: flex;
gap: 0.5rem;
flex-direction: column;
gap: 1rem;
margin-bottom: 2rem;
}
.title {
.wrapper {
padding: 3rem;
max-width: 72rem;
margin: 0 auto;
width: 100%;
display: flex;
font-size: var(--font-size-x-large);
font-weight: var(--font-weight-bold);
flex-direction: column;
}
.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;
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;
gap: 1rem;
flex-direction: column;
}
.more {
min-height: 3rem;
display: flex;
align-items: center;
justify-content: center;
}
.project-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
gap: 1rem;
}
.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>
+14 -27
View File
@@ -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) {