diff --git a/httpd-client/index.ts b/httpd-client/index.ts index a68a3761f..83d22854d 100644 --- a/httpd-client/index.ts +++ b/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, }; diff --git a/httpd-client/lib/shared.ts b/httpd-client/lib/shared.ts index 10a87cbc4..503e44f80 100644 --- a/httpd-client/lib/shared.ts +++ b/httpd-client/lib/shared.ts @@ -10,6 +10,9 @@ export const successResponseSchema = object({ success: literal(true), }) satisfies ZodSchema; +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; +export type Scope = z.infer; + export const rangeSchema = union([ object({ type: literal("lines"), diff --git a/public/typography.css b/public/typography.css index c3f5d5d7a..a771557bd 100644 --- a/public/typography.css +++ b/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); } diff --git a/src/components/IconSmall.svelte b/src/components/IconSmall.svelte index a06423184..452fe1cf6 100644 --- a/src/components/IconSmall.svelte +++ b/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"} + + {:else if name === "issue"} - 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 { - 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 @@
-
- {hostname} -
+
{hostname}
{#each externalAddresses as address} @@ -143,45 +132,88 @@
-
- {#each projects as { project, activity, lastCommit } (project.id)} +
+
+ {projects.length} + {isLocal(baseUrl.hostname) ? "" : "pinned"} projects +
+ + {#if !isLocal(baseUrl.hostname)} + +
+ + + +
+ +
+ These are pinned projects that were configured to be highlighted + on this node. +
+
+ {/if} + + {#if policy && scope} +
+ + Seeding Policy: + {capitalize(policy)} + + + + + Scope: + {capitalize(scope)} + + + +
+ + + +
+ +
+ {#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} +
+
+
+
+ {/if} +
+ +
+ {#each projects as { project, baseUrl, activity, lastCommit }} + false} + lastUpdatedTimestamp={lastCommit.commit.committer.time} + {activity} + {baseUrl} /> {/each}
- - {#if loadingProjects} -
- -
- {/if} - - {#if showMoreButton} -
- -
- {/if} - - {#if error} - - {/if}
diff --git a/src/views/nodes/router.ts b/src/views/nodes/router.ts index b8c967670..2b95e6017 100644 --- a/src/views/nodes/router.ts +++ b/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 { 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 { 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) {