mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-11-15 11:20:36 +08:00
Consolidate and optimize data fetching for ProjectCard
We consolidate the code that fetches the data for `ProjectCard` in one module, adjacent to the `ProjectCard` component. We also consolidate all the data into one interface to simplify the component API. With the data loading in one place we can eliminate duplicate code and a handful of helper functions. The consolidation also allows us to discover some optimizations to fetching: We parallelize requests and we use the commit detail endpoint instead of the commit list endpoint. For the latter to pay off, we need to deploy caching to the API.
This commit is contained in:
@@ -1,32 +1,25 @@
|
||||
<script lang="ts">
|
||||
import type { BaseUrl } from "@httpd-client";
|
||||
|
||||
import type { WeeklyActivity } from "@app/lib/commit";
|
||||
import { formatTimestamp, twemoji } from "@app/lib/utils";
|
||||
|
||||
import ActivityDiagram from "@app/components/ActivityDiagram.svelte";
|
||||
import IconSmall from "@app/components/IconSmall.svelte";
|
||||
import Link from "@app/components/Link.svelte";
|
||||
|
||||
import type { ProjectInfo } from "./ProjectCard";
|
||||
|
||||
export let compact = false;
|
||||
|
||||
export let activity: WeeklyActivity[];
|
||||
export let description: string;
|
||||
export let baseUrl: BaseUrl;
|
||||
|
||||
export let numberOfIssues: number;
|
||||
export let numberOfPatches: number;
|
||||
export let projectInfo: ProjectInfo;
|
||||
|
||||
export let isDelegate: boolean;
|
||||
export let isSeeding: boolean;
|
||||
export let isPrivate: boolean;
|
||||
|
||||
export let lastUpdatedTimestamp: number;
|
||||
|
||||
$: lastUpdated = formatTimestamp(lastUpdatedTimestamp);
|
||||
|
||||
export let id: string;
|
||||
export let name: string;
|
||||
$: project = projectInfo.project;
|
||||
$: baseUrl = projectInfo.baseUrl;
|
||||
$: isPrivate = project.visibility?.type === "private";
|
||||
$: lastUpdated = formatTimestamp(
|
||||
projectInfo.lastCommit.commit.committer.time,
|
||||
);
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@@ -138,21 +131,21 @@
|
||||
<Link
|
||||
route={{
|
||||
resource: "project.source",
|
||||
project: id,
|
||||
project: project.id,
|
||||
node: baseUrl,
|
||||
}}>
|
||||
<div class="project-card" class:compact>
|
||||
<div class="activity">
|
||||
<div class="fadeout-overlay" />
|
||||
<ActivityDiagram
|
||||
{id}
|
||||
id={project.id}
|
||||
viewBoxHeight={200}
|
||||
styleColor="var(--color-foreground-primary"
|
||||
{activity} />
|
||||
activity={projectInfo.activity} />
|
||||
</div>
|
||||
<div class="title">
|
||||
<div class="headline-and-badges">
|
||||
<h4 use:twemoji>{name}</h4>
|
||||
<h4 use:twemoji>{project.name}</h4>
|
||||
<div class="badges">
|
||||
{#if isPrivate}
|
||||
<div
|
||||
@@ -180,14 +173,14 @@
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<p class="txt-small" use:twemoji>{description}</p>
|
||||
<p class="txt-small" use:twemoji>{project.description}</p>
|
||||
</div>
|
||||
<div class="stats-row txt-tiny" style:color="var(--color-foreground-dim)">
|
||||
<IconSmall name="issue" />
|
||||
{numberOfIssues} ·
|
||||
{project.issues.open} ·
|
||||
<IconSmall name="patch" />
|
||||
<span style:overflow="hidden" style:text-overflow="ellipsis">
|
||||
{numberOfPatches} · Updated {lastUpdated}
|
||||
{project.patches.open} · Updated {lastUpdated}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { loadProjectActivity, type WeeklyActivity } from "@app/lib/commit";
|
||||
import {
|
||||
HttpdClient,
|
||||
type BaseUrl,
|
||||
type Commit,
|
||||
type Project,
|
||||
} from "@httpd-client";
|
||||
|
||||
export interface ProjectInfo {
|
||||
project: Project;
|
||||
baseUrl: BaseUrl;
|
||||
activity: WeeklyActivity[];
|
||||
lastCommit: Commit;
|
||||
}
|
||||
|
||||
export async function fetchProjectInfos(
|
||||
baseUrl: BaseUrl,
|
||||
show: "all" | "pinned",
|
||||
): Promise<ProjectInfo[]> {
|
||||
const api = new HttpdClient(baseUrl);
|
||||
const projects = await api.project.getAll({ show });
|
||||
const info = await Promise.all(
|
||||
projects.map(async project => {
|
||||
const [activity, lastCommit] = await Promise.all([
|
||||
loadProjectActivity(project.id, baseUrl),
|
||||
api.project.getCommitBySha(project.id, project.head),
|
||||
]);
|
||||
return { project, activity, lastCommit, baseUrl };
|
||||
}),
|
||||
);
|
||||
|
||||
return info.sort((a, b) => {
|
||||
const aLastCommit = a.lastCommit.commit.committer.time;
|
||||
const bLastCommit = b.lastCommit.commit.committer.time;
|
||||
|
||||
return bLastCommit - aLastCommit;
|
||||
});
|
||||
}
|
||||
@@ -116,10 +116,3 @@ export async function loadProjectActivity(id: string, baseUrl: BaseUrl) {
|
||||
|
||||
return groupCommitsByWeek(commits.activity);
|
||||
}
|
||||
|
||||
export async function fetchLastCommit(id: string, baseUrl: BaseUrl) {
|
||||
const api = new HttpdClient(baseUrl);
|
||||
const res = await api.project.getAllCommits(id, { perPage: 1 });
|
||||
|
||||
return res.commits[0];
|
||||
}
|
||||
|
||||
@@ -2,22 +2,12 @@ import type { BaseUrl, Project } from "@httpd-client";
|
||||
|
||||
import { HttpdClient } from "@httpd-client";
|
||||
import { isFulfilled } from "@app/lib/utils";
|
||||
import {
|
||||
fetchLastCommit,
|
||||
loadProjectActivity,
|
||||
type WeeklyActivity,
|
||||
} from "./commit";
|
||||
|
||||
export interface ProjectBaseUrl {
|
||||
project: Project;
|
||||
baseUrl: BaseUrl;
|
||||
}
|
||||
|
||||
export interface ProjectWithListingData extends ProjectBaseUrl {
|
||||
activity: WeeklyActivity[];
|
||||
lastCommit: Awaited<ReturnType<typeof fetchLastCommit>>;
|
||||
}
|
||||
|
||||
export async function getProjectsFromNodes(
|
||||
params: { id: string; baseUrl: BaseUrl }[],
|
||||
): Promise<ProjectBaseUrl[]> {
|
||||
@@ -34,32 +24,6 @@ export async function getProjectsFromNodes(
|
||||
return results.filter(isFulfilled).map(r => r.value);
|
||||
}
|
||||
|
||||
export async function getProjectListingData(id: string, baseUrl: BaseUrl) {
|
||||
const activity = await loadProjectActivity(id, baseUrl);
|
||||
const lastCommit = await fetchLastCommit(id, baseUrl);
|
||||
|
||||
return { activity, lastCommit };
|
||||
}
|
||||
|
||||
export async function getProjectsListingData(projects: ProjectBaseUrl[]) {
|
||||
const result = await Promise.all(
|
||||
projects.map(async ({ project, baseUrl }) => {
|
||||
const { activity, lastCommit } = await getProjectListingData(
|
||||
project.id,
|
||||
baseUrl,
|
||||
);
|
||||
return { project, activity, lastCommit, baseUrl };
|
||||
}),
|
||||
);
|
||||
|
||||
return result.sort((a, b) => {
|
||||
const aLastCommit = a.lastCommit?.commit.committer.time ?? 0;
|
||||
const bLastCommit = b.lastCommit?.commit.committer.time ?? 0;
|
||||
|
||||
return bLastCommit - aLastCommit;
|
||||
});
|
||||
}
|
||||
|
||||
export async function queryProject(
|
||||
baseUrl: BaseUrl,
|
||||
projectId: string,
|
||||
|
||||
+20
-46
@@ -1,17 +1,13 @@
|
||||
<script lang="ts">
|
||||
import type { BaseUrl } from "@httpd-client";
|
||||
import type { ProjectWithListingData } from "@app/lib/projects";
|
||||
import type { ComponentProps } from "svelte";
|
||||
|
||||
import storedWritable from "@efstajas/svelte-stored-writable";
|
||||
import { HttpdClient } from "@httpd-client";
|
||||
import { derived } from "svelte/store";
|
||||
import { literal, union } from "zod";
|
||||
|
||||
import { api, httpdStore } from "@app/lib/httpd";
|
||||
import { deduplicateStore } from "@app/lib/deduplicateStore";
|
||||
import { baseUrlToString } from "@app/lib/utils";
|
||||
import { getProjectsListingData } from "@app/lib/projects";
|
||||
import { handleError } from "@app/views/home/error";
|
||||
import { isDelegate } from "@app/lib/roles";
|
||||
import { preferredSeeds } from "@app/lib/seeds";
|
||||
@@ -25,6 +21,10 @@
|
||||
import HomepageSection from "./components/HomepageSection.svelte";
|
||||
import NewProjectButton from "./components/NewProjectButton.svelte";
|
||||
import PreferredSeedDropdown from "./components/PreferredSeedDropdown.svelte";
|
||||
import {
|
||||
fetchProjectInfos,
|
||||
type ProjectInfo,
|
||||
} from "@app/components/ProjectCard";
|
||||
|
||||
const selectedSeed = deduplicateStore(
|
||||
derived(preferredSeeds, $ => $?.selected),
|
||||
@@ -40,30 +40,17 @@
|
||||
);
|
||||
|
||||
let localProjects:
|
||||
| ProjectWithListingData[]
|
||||
| ProjectInfo[]
|
||||
| ComponentProps<ErrorMessage>["error"]
|
||||
| undefined;
|
||||
let preferredSeedProjects:
|
||||
| ProjectWithListingData[]
|
||||
| ProjectInfo[]
|
||||
| ComponentProps<ErrorMessage>["error"]
|
||||
| undefined;
|
||||
|
||||
async function fetchProjects(baseUrl: BaseUrl, show: "all" | "pinned") {
|
||||
const api = new HttpdClient(baseUrl);
|
||||
|
||||
const projects = (await api.project.getAll({ perPage: 30, show })).map(
|
||||
project => ({
|
||||
project,
|
||||
baseUrl,
|
||||
}),
|
||||
);
|
||||
|
||||
return await getProjectsListingData(projects);
|
||||
}
|
||||
|
||||
async function loadLocalProjects() {
|
||||
localProjects = undefined;
|
||||
localProjects = await fetchProjects(api.baseUrl, "all").catch(
|
||||
localProjects = await fetchProjectInfos(api.baseUrl, "all").catch(
|
||||
error => error,
|
||||
);
|
||||
}
|
||||
@@ -72,9 +59,10 @@
|
||||
preferredSeedProjects = undefined;
|
||||
|
||||
if (!$selectedSeed) return;
|
||||
preferredSeedProjects = await fetchProjects($selectedSeed, "pinned").catch(
|
||||
error => error,
|
||||
);
|
||||
preferredSeedProjects = await fetchProjectInfos(
|
||||
$selectedSeed,
|
||||
"pinned",
|
||||
).catch(error => error);
|
||||
}
|
||||
|
||||
function isSeeding(projectId: string) {
|
||||
@@ -176,19 +164,12 @@
|
||||
</svelte:fragment>
|
||||
<div class="project-grid">
|
||||
{#if filteredLocalProjects && !(filteredLocalProjects instanceof Error)}
|
||||
{#each filteredLocalProjects as { project, baseUrl, activity, lastCommit }}
|
||||
{#each filteredLocalProjects as projectInfo}
|
||||
<ProjectCard
|
||||
id={project.id}
|
||||
name={project.name}
|
||||
description={project.description}
|
||||
numberOfIssues={project.issues.open}
|
||||
numberOfPatches={project.patches.open}
|
||||
isPrivate={project.visibility?.type === "private"}
|
||||
{projectInfo}
|
||||
isSeeding={true}
|
||||
isDelegate={isDelegate(nodeId, project.delegates) ?? false}
|
||||
lastUpdatedTimestamp={lastCommit.commit.committer.time}
|
||||
{activity}
|
||||
{baseUrl} />
|
||||
isDelegate={isDelegate(nodeId, projectInfo.project.delegates) ??
|
||||
false} />
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
@@ -228,19 +209,12 @@
|
||||
</svelte:fragment>
|
||||
<div class="project-grid">
|
||||
{#if preferredSeedProjects && !(preferredSeedProjects instanceof Error)}
|
||||
{#each preferredSeedProjects as { project, baseUrl, activity, lastCommit }}
|
||||
{#each preferredSeedProjects as projectInfo}
|
||||
<ProjectCard
|
||||
id={project.id}
|
||||
name={project.name}
|
||||
description={project.description}
|
||||
numberOfIssues={project.issues.open}
|
||||
numberOfPatches={project.patches.open}
|
||||
isPrivate={project.visibility?.type === "private"}
|
||||
isSeeding={isSeeding(project.id)}
|
||||
isDelegate={isDelegate(nodeId, project.delegates) ?? false}
|
||||
lastUpdatedTimestamp={lastCommit.commit.committer.time}
|
||||
{activity}
|
||||
{baseUrl} />
|
||||
{projectInfo}
|
||||
isSeeding={isSeeding(projectInfo.project.id)}
|
||||
isDelegate={isDelegate(nodeId, projectInfo.project.delegates) ??
|
||||
false} />
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type { BaseUrl, Policy, Scope } from "@httpd-client";
|
||||
import type { ProjectWithListingData } from "@app/lib/projects";
|
||||
|
||||
import capitalize from "lodash/capitalize";
|
||||
|
||||
@@ -14,11 +13,12 @@
|
||||
import ProjectCard from "@app/components/ProjectCard.svelte";
|
||||
import ScopePolicyExplainer from "@app/components/ScopePolicyExplainer.svelte";
|
||||
import HoverPopover from "@app/components/HoverPopover.svelte";
|
||||
import type { ProjectInfo } from "@app/components/ProjectCard";
|
||||
|
||||
export let baseUrl: BaseUrl;
|
||||
export let nid: string;
|
||||
export let externalAddresses: string[];
|
||||
export let projects: ProjectWithListingData[] = [];
|
||||
export let projectInfos: ProjectInfo[];
|
||||
export let version: string;
|
||||
export let policy: Policy | undefined = undefined;
|
||||
export let scope: Scope | undefined = undefined;
|
||||
@@ -135,7 +135,7 @@
|
||||
|
||||
<div class="subtitle">
|
||||
<div class="pinned txt-semibold">
|
||||
{projects.length}
|
||||
{projectInfos.length}
|
||||
{isLocal(baseUrl.hostname) ? "" : "pinned"} projects
|
||||
</div>
|
||||
|
||||
@@ -188,20 +188,14 @@
|
||||
</div>
|
||||
|
||||
<div class="project-grid">
|
||||
{#each projects as { project, baseUrl, activity, lastCommit }}
|
||||
{#each projectInfos as projectInfo}
|
||||
<ProjectCard
|
||||
id={project.id}
|
||||
name={project.name}
|
||||
description={project.description}
|
||||
numberOfIssues={project.issues.open}
|
||||
numberOfPatches={project.patches.open}
|
||||
isPrivate={project.visibility?.type === "private"}
|
||||
{projectInfo}
|
||||
isSeeding={false}
|
||||
isDelegate={isDelegate(session?.publicKey, project.delegates) ??
|
||||
false}
|
||||
lastUpdatedTimestamp={lastCommit.commit.committer.time}
|
||||
{activity}
|
||||
{baseUrl} />
|
||||
isDelegate={isDelegate(
|
||||
session?.publicKey,
|
||||
projectInfo.project.delegates,
|
||||
) ?? false} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+11
-20
@@ -1,12 +1,14 @@
|
||||
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 { baseUrlToString, isLocal } from "@app/lib/utils";
|
||||
import { getProjectsListingData } from "@app/lib/projects";
|
||||
import { handleError } from "@app/views/nodes/error";
|
||||
import {
|
||||
fetchProjectInfos,
|
||||
type ProjectInfo,
|
||||
} from "@app/components/ProjectCard";
|
||||
|
||||
export interface NodesRouteParams {
|
||||
baseUrl: BaseUrl;
|
||||
@@ -25,26 +27,12 @@ export interface NodesLoadedRoute {
|
||||
version: string;
|
||||
externalAddresses: string[];
|
||||
nid: string;
|
||||
projects: ProjectWithListingData[];
|
||||
projectInfos: ProjectInfo[];
|
||||
policy?: Policy;
|
||||
scope?: Scope;
|
||||
};
|
||||
}
|
||||
|
||||
async function loadProjects(
|
||||
baseUrl: BaseUrl,
|
||||
): Promise<ProjectWithListingData[]> {
|
||||
const api = new HttpdClient(baseUrl);
|
||||
const projects = await api.project.getAll({
|
||||
show: isLocal(baseUrl.hostname) ? "all" : "pinned",
|
||||
});
|
||||
const results = await getProjectsListingData(
|
||||
projects.map(p => ({ project: p, baseUrl })),
|
||||
);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
export function nodePath(baseUrl: BaseUrl) {
|
||||
const port = baseUrl.port ?? config.nodes.defaultHttpdPort;
|
||||
|
||||
@@ -60,9 +48,12 @@ export async function loadNodeRoute(
|
||||
): Promise<NodesLoadedRoute | NotFoundRoute | ErrorRoute> {
|
||||
const api = new HttpdClient(params.baseUrl);
|
||||
try {
|
||||
const [node, projects] = await Promise.all([
|
||||
const [node, projectInfos] = await Promise.all([
|
||||
api.getNode(),
|
||||
loadProjects(params.baseUrl),
|
||||
fetchProjectInfos(
|
||||
params.baseUrl,
|
||||
isLocal(params.baseUrl.hostname) ? "all" : "pinned",
|
||||
),
|
||||
]);
|
||||
return {
|
||||
resource: "nodes",
|
||||
@@ -71,7 +62,7 @@ export async function loadNodeRoute(
|
||||
nid: node.id,
|
||||
externalAddresses: node.config?.externalAddresses ?? [],
|
||||
version: node.version,
|
||||
projects: projects,
|
||||
projectInfos: projectInfos,
|
||||
policy: node.config?.policy,
|
||||
scope: node.config?.scope,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user