diff --git a/src/lib/components/GHBadge.svelte b/src/lib/components/GHBadge.svelte
index 795331ff..e509ab4e 100644
--- a/src/lib/components/GHBadge.svelte
+++ b/src/lib/components/GHBadge.svelte
@@ -1,4 +1,5 @@
-
- {#if icon}
- {@const SvelteComponent = icon}
-
- {/if}
- {label}
-
+{#if mode === "regular"}
+
+ {#if icon}
+ {@const SvelteComponent = icon}
+
+ {/if}
+ {label}
+
+{:else if icon}
+ {@const Component = icon}
+
+{/if}
diff --git a/src/lib/server/github-cache.ts b/src/lib/server/github-cache.ts
index b09f8c09..c6e6ab67 100644
--- a/src/lib/server/github-cache.ts
+++ b/src/lib/server/github-cache.ts
@@ -17,19 +17,28 @@ export type GitHubRelease = Awaited<
ReturnType["rest"]["repos"]["listReleases"]>
>["data"][number];
-type KeyType = "releases" | "descriptions" | "issue" | "pr";
+export type Member = Awaited<
+ ReturnType["rest"]["orgs"]["listMembers"]>
+>["data"][number];
+
+type OwnerKeyType = "members";
+
+type RepoKeyType = "releases" | "descriptions" | "issue" | "issues" | "pr" | "prs";
export type ItemDetails = {
comments: Awaited>["data"];
};
+export type Issue = Awaited>["data"];
export type IssueDetails = ItemDetails & {
- info: Awaited>["data"];
+ info: Issue;
linkedPrs: LinkedItem[];
};
+export type PullRequest = Awaited>["data"];
+export type ListedPullRequest = Awaited>["data"][number];
export type PullRequestDetails = ItemDetails & {
- info: Awaited>["data"];
+ info: PullRequest;
commits: Awaited>["data"];
files: Awaited>["data"];
linkedIssues: LinkedItem[];
@@ -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
*/
@@ -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.
*
@@ -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}`;
}
@@ -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}`;
}
@@ -200,7 +229,7 @@ export class GitHubCache {
owner: string,
repo: string,
id: number,
- type: ExtractStrict | undefined = undefined
+ type: ExtractStrict | undefined = undefined
) {
// Known type we assume the existence of
switch (type) {
@@ -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()(
+ 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()(
+ 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()(
+ 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.
*
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
index 6945a199..b1ae7d85 100644
--- a/src/routes/+layout.svelte
+++ b/src/routes/+layout.svelte
@@ -127,7 +127,7 @@
{#if !page.route.id?.startsWith("/blog")}
- {#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)}
-