Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 32 additions & 16 deletions src/lib/components/GHBadge.svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script lang="ts">
import type { ClassValue } from "svelte/elements";
import {
CircleCheck,
CircleDot,
Expand All @@ -22,38 +23,45 @@
};

type Props = {
mode?: "regular" | "minimal";
type: PropsObj["type"];
status: PropsObj["status"];
class?: ClassValue;
};

let { type, status }: Props = $props();
let { mode = "regular", type, status, class: className = undefined }: Props = $props();

let icon = $state<typeof Icon>();
let label = $state("");
let color = $state("");
let textColor = $state("");
let bgColor = $state("");

switch (type) {
case "pull":
switch (status) {
case "draft":
icon = GitPullRequestDraft;
label = "Draft";
color = "bg-neutral-500";
textColor = "text-neutral-500";
bgColor = "bg-neutral-500";
break;
case "open":
icon = GitPullRequestArrow;
label = "Open";
color = "bg-green-600";
textColor = "text-green-600";
bgColor = "bg-green-600";
break;
case "merged":
icon = GitMerge;
label = "Merged";
color = "bg-purple-500";
textColor = "text-purple-500";
bgColor = "bg-purple-500";
break;
case "closed":
icon = GitPullRequestClosed;
label = "Closed";
color = "bg-red-500";
textColor = "text-red-500";
bgColor = "bg-red-500";
break;
}
break;
Expand All @@ -62,26 +70,34 @@
case "open":
icon = CircleDot;
label = "Open";
color = "bg-green-600";
textColor = "text-green-600";
bgColor = "bg-green-600";
break;
case "closed":
icon = CircleSlash;
label = "Closed";
color = "bg-neutral-500";
textColor = "text-neutral-500";
bgColor = "bg-neutral-500";
break;
case "solved":
icon = CircleCheck;
label = "Solved";
color = "bg-purple-500";
textColor = "text-purple-500";
bgColor = "bg-purple-500";
break;
}
}
</script>

<div class="flex items-center rounded-full px-4 py-2 text-white {color}">
{#if icon}
{@const SvelteComponent = icon}
<SvelteComponent class="mr-2 size-5" />
{/if}
<span class="font-semibold">{label}</span>
</div>
{#if mode === "regular"}
<div class={["flex items-center rounded-full px-4 py-2 text-white", bgColor, className]}>
{#if icon}
{@const SvelteComponent = icon}
<SvelteComponent class="mr-2 size-5" />
{/if}
<span class="font-semibold">{label}</span>
</div>
{:else if icon}
{@const Component = icon}
<Component class={["size-6", textColor, className]} />
{/if}
122 changes: 115 additions & 7 deletions src/lib/server/github-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,28 @@ export type GitHubRelease = Awaited<
ReturnType<InstanceType<typeof Octokit>["rest"]["repos"]["listReleases"]>
>["data"][number];

type KeyType = "releases" | "descriptions" | "issue" | "pr";
export type Member = Awaited<
ReturnType<InstanceType<typeof Octokit>["rest"]["orgs"]["listMembers"]>
>["data"][number];

type OwnerKeyType = "members";

type RepoKeyType = "releases" | "descriptions" | "issue" | "issues" | "pr" | "prs";

export type ItemDetails = {
comments: Awaited<ReturnType<Issues["listComments"]>>["data"];
};

export type Issue = Awaited<ReturnType<Issues["get"]>>["data"];
export type IssueDetails = ItemDetails & {
info: Awaited<ReturnType<Issues["get"]>>["data"];
info: Issue;
linkedPrs: LinkedItem[];
};

export type PullRequest = Awaited<ReturnType<Pulls["get"]>>["data"];
export type ListedPullRequest = Awaited<ReturnType<Pulls["list"]>>["data"][number];
export type PullRequestDetails = ItemDetails & {
info: Awaited<ReturnType<Pulls["get"]>>["data"];
info: PullRequest;
commits: Awaited<ReturnType<Pulls["listCommits"]>>["data"];
files: Awaited<ReturnType<Pulls["listFiles"]>>["data"];
linkedIssues: LinkedItem[];
Expand Down Expand Up @@ -70,6 +79,10 @@ const FULL_DETAILS_TTL = 60 * 60 * 2; // 2 hours
* The TTL of the cached descriptions, in seconds.
*/
const DESCRIPTIONS_TTL = 60 * 60 * 24 * 10; // 10 days
/**
* The TTL of organization members, in seconds.
*/
const MEMBERS_TTL = 60 * 60 * 24 * 2; // 2 days
/**
* The TTL for non-deprecated packages, in seconds
*/
Expand Down Expand Up @@ -102,6 +115,22 @@ export class GitHubCache {
});
}

/**
* Generates a Redis key from the passed info.
*
* @param owner the GitHub repository owner
* @param type the kind of cache to use
* @param args the optional additional values to append
* at the end of the key; every element will be interpolated
* in a string
* @returns the pure computed key
* @private
*/
#getOwnerKey(owner: string, type: OwnerKeyType, ...args: unknown[]) {
const strArgs = args.map(a => `:${a}`).join("");
return `owner:${owner}:${type}${strArgs}`;
}

/**
* Generates a Redis key from the passed info.
*
Expand All @@ -114,8 +143,8 @@ export class GitHubCache {
* @returns the pure computed key
* @private
*/
#getRepoKey(owner: string, repo: string, type: KeyType, ...args: unknown[]) {
const strArgs = args.map(a => `:${a}`);
#getRepoKey(owner: string, repo: string, type: RepoKeyType, ...args: unknown[]) {
const strArgs = args.map(a => `:${a}`).join("");
return `repo:${owner}/${repo}:${type}${strArgs}`;
}

Expand All @@ -130,7 +159,7 @@ export class GitHubCache {
* @private
*/
#getPackageKey(packageName: string, ...args: unknown[]) {
const strArgs = args.map(a => `:${a}`);
const strArgs = args.map(a => `:${a}`).join("");
return `package:${packageName}${strArgs}`;
}

Expand Down Expand Up @@ -200,7 +229,7 @@ export class GitHubCache {
owner: string,
repo: string,
id: number,
type: ExtractStrict<KeyType, "issue" | "pr"> | undefined = undefined
type: ExtractStrict<RepoKeyType, "issue" | "pr"> | undefined = undefined
) {
// Known type we assume the existence of
switch (type) {
Expand Down Expand Up @@ -612,6 +641,85 @@ export class GitHubCache {
);
}

/**
* Get the list of members for a given organization.
*
* @param owner the GitHub organization name
* @returns a list of members, or `undefined` if not existing
*/
async getOrganizationMembers(owner: string) {
return await this.#processCached<Member[]>()(
this.#getOwnerKey(owner, "members"),
async () => {
try {
const { data: members } = await this.#octokit.rest.orgs.listPublicMembers({
org: owner,
per_page
});
return members;
} catch {
return [] as Member[];
}
},
members => members,
MEMBERS_TTL
);
}

/**
* Get all the issues for a given GitHub repository.
*
* @param owner the GitHub repository owner
* @param repo the GitHub repository name
* @returns a list of issues, empty if not existing
*/
async getAllIssues(owner: string, repo: string) {
return await this.#processCached<Issue[]>()(
this.#getRepoKey(owner, repo, "issues"),
async () => {
try {
const { data: issues } = await this.#octokit.rest.issues.listForRepo({
owner,
repo,
per_page
});
return issues;
} catch {
return [] as Issue[];
}
},
issues => issues,
FULL_DETAILS_TTL
);
}

/**
* Get all the pull requests for a given GitHub repository.
*
* @param owner the GitHub repository owner
* @param repo the GitHub repository name
* @returns a list of pull requests, empty if not existing
*/
async getAllPRs(owner: string, repo: string) {
return await this.#processCached<ListedPullRequest[]>()(
this.#getRepoKey(owner, repo, "prs"),
async () => {
try {
const { data: prs } = await this.#octokit.rest.pulls.list({
owner,
repo,
per_page
});
return prs;
} catch {
return [] as ListedPullRequest[];
}
},
prs => prs,
FULL_DETAILS_TTL
);
}

/**
* Get the deprecation state of a package from its name.
*
Expand Down
2 changes: 1 addition & 1 deletion src/routes/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@
<!-- Navigation -->
{#if !page.route.id?.startsWith("/blog")}
<ul class="ml-6 hidden sm:flex">
{#each [{ link: "/packages", title: "Packages" }, { link: "/blog", title: "Blog" }] as { link, title } (link)}
{#each [{ link: "/packages", title: "Packages" }, { link: "/tracker", title: "Tracker" }, { link: "/blog", title: "Blog" }] as { link, title } (link)}
{@const disabled = page.url.pathname.startsWith(link)}
<li>
<Button
Expand Down
9 changes: 9 additions & 0 deletions src/routes/tracker/+page.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { redirect } from "@sveltejs/kit";
import { uniqueRepos } from "$lib/repositories";

export function load({ url }) {
// Load the first repo of our list
const firstRepo = uniqueRepos[0];
if (!firstRepo) redirect(307, "/");
redirect(307, `${url.pathname}/${firstRepo.owner}/${firstRepo.name}`);
}
11 changes: 11 additions & 0 deletions src/routes/tracker/[org]/+page.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { redirect } from "@sveltejs/kit";
import { uniqueRepos } from "$lib/repositories";

export function load({ params }) {
// Load the first repo that has the org
const matchingRepo = uniqueRepos.find(
({ owner }) => owner.localeCompare(params.org, undefined, { sensitivity: "base" }) === 0
);
if (!matchingRepo) redirect(307, "/");
redirect(307, `/tracker/${matchingRepo.owner}/${matchingRepo.name}`);
}
64 changes: 64 additions & 0 deletions src/routes/tracker/[org]/[repo]/+layout.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<script lang="ts">
import { page } from "$app/state";
import { Menu } from "@lucide/svelte";
import { uniqueRepos } from "$lib/repositories";
import { Button } from "$lib/components/ui/button";
import * as Sheet from "$lib/components/ui/sheet";
import RepoSidePanel from "./RepoSidePanel.svelte";

let { children } = $props();
</script>

{#snippet repoList()}
<ul class="space-y-1">
{#each uniqueRepos as repo (repo)}
{@const active = repo.owner === page.params.org && repo.name === page.params.repo}
<li>
{#if active}
<span class="font-bold underline underline-offset-2">{repo.owner}/<wbr />{repo.name}</span
>
{:else}
<a
href="../{repo.owner}/{repo.name}"
class="hover:underline hover:text-primary underline-offset-2"
>
{repo.owner}/<wbr />{repo.name}
</a>
{/if}
</li>
{/each}
</ul>
{/snippet}

<div class="relative flex gap-8">
<div class="min-w-0">
{@render children()}
</div>

<Sheet.Root>
<Sheet.Trigger>
{#snippet child({ props })}
<Button
{...props}
variant="secondary"
class="absolute right-0 mt-12 ml-auto lg:hidden sm:mt-16"
>
<Menu />
<span class="sr-only">Menu</span>
</Button>
{/snippet}
</Sheet.Trigger>
<Sheet.Content class="overflow-y-auto">
<Sheet.Header>
<Sheet.Title>Repositories</Sheet.Title>
</Sheet.Header>
<RepoSidePanel headless title="Repositories" class="my-8">
{@render repoList()}
</RepoSidePanel>
</Sheet.Content>
</Sheet.Root>

<RepoSidePanel title="Repositories" class="mt-43 shrink-0 hidden h-fit w-80 lg:flex flex-col">
{@render repoList()}
</RepoSidePanel>
</div>
Loading
Loading