Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/render errors in modal #57

Merged
merged 16 commits into from
Jun 6, 2024
Merged
Show file tree
Hide file tree
Changes from 15 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
2 changes: 1 addition & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"version": "0.2",
"words": ["devpool", "supabase"]
"words": ["devpool", "ratelimit", "supabase", "UBIQUIBOT"]
}
4 changes: 3 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
SUPABASE_URL=
SUPABASE_ANON_KEY=
SUPABASE_ANON_KEY=
UBIQUIBOT_GITHUB_USERNAME=
UBIQUIBOT_GITHUB_PASSWORD=
19 changes: 15 additions & 4 deletions cypress.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,21 @@ export default defineConfig({
},
viewportHeight: 900,
viewportWidth: 1440,
env: {
GITHUB_USERNAME: process.env.UBIQUIBOT_GITHUB_USERNAME,
GITHUB_PASSWORD: process.env.UBIQUIBOT_GITHUB_PASSWORD,
},
env: readEnvironmentVariables(),
watchForFileChanges: false,
video: true,
});

function readEnvironmentVariables() {
const UBIQUIBOT_GITHUB_USERNAME = process.env["UBIQUIBOT_GITHUB_USERNAME"];
const UBIQUIBOT_GITHUB_PASSWORD = process.env["UBIQUIBOT_GITHUB_PASSWORD"];

if (!UBIQUIBOT_GITHUB_USERNAME) {
throw new Error("Please provide `UBIQUIBOT_GITHUB_USERNAME` environment variable");
}

if (!UBIQUIBOT_GITHUB_PASSWORD) {
throw new Error("Please provide `UBIQUIBOT_GITHUB_PASSWORD` environment variable");
}
return { UBIQUIBOT_GITHUB_USERNAME, UBIQUIBOT_GITHUB_PASSWORD };
}
24 changes: 22 additions & 2 deletions cypress/e2e/devpool.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,11 @@ describe("DevPool", () => {
cy.get("#filter").should("not.be.visible");
cy.get("#github-login-button").click();
cy.origin("https://github.com/login", () => {
cy.get("#login_field").type(Cypress.env("GITHUB_USERNAME"));
cy.get("#password").type(Cypress.env("GITHUB_PASSWORD"));
const username = Cypress.env("UBIQUIBOT_GITHUB_USERNAME");
const password = Cypress.env("UBIQUIBOT_GITHUB_PASSWORD");

cy.get("#login_field").type(username);
cy.get("#password").type(password, { parseSpecialCharSequences: false });
cy.get(".position-relative > .btn").click();
// This part of the test can sometimes fail if the endpoint for OAuth is hit too many times, asking the user to
// authorize the app again. It should not happen in a normal testing scenario since it's only hit once, but more
Expand All @@ -205,4 +208,21 @@ describe("DevPool", () => {
cy.get("#authenticated").should("exist");
cy.get("#filter").should("be.visible");
});

describe("Display error modal", () => {
it("should display an error modal when fetching issue previews fails on page load", () => {
cy.intercept("GET", "https://api.github.com/repos/ubiquity/devpool-directory/issues*", {
statusCode: 500,
body: "Internal Server Error",
}).as("getPublicIssues");

cy.visit("/");

cy.wait("@getPublicIssues");

cy.get(".preview-header").should("be.visible");
cy.get(".preview-header").should("contain", "Something went wrong");
cy.get(".preview-body-inner").should("contain", "HttpError: Internal Server Error");
});
});
});
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"format:prettier": "prettier --write .",
"prepare": "husky install",
"cypress:open": "cypress open",
"cypress:run": "cypress run"
"cypress:run": "cypress run",
"cypress:headed": "cypress run --headed"
},
"keywords": [
"typescript",
Expand Down
3 changes: 2 additions & 1 deletion src/home/fetch-github/fetch-avatar.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Octokit } from "@octokit/rest";
import { getGitHubAccessToken } from "../getters/get-github-access-token";
import { getImageFromCache, saveImageToCache } from "../getters/get-indexed-db";
import { renderErrorInModal } from "../rendering/display-popup-modal";
import { organizationImageCache } from "./fetch-issues-full";

export async function fetchAvatar(orgName: string) {
Expand Down Expand Up @@ -38,7 +39,7 @@ export async function fetchAvatar(orgName: string) {
organizationImageCache.set(orgName, blob);
}
} catch (error) {
console.error(`Failed to fetch avatar for organization ${orgName}: ${error}`);
renderErrorInModal(error as Error, `Failed to fetch avatar for organization ${orgName}: ${error}`);
const {
data: { avatar_url: avatarUrl },
} = await octokit.rest.users.getByUsername({ username: orgName });
Expand Down
82 changes: 29 additions & 53 deletions src/home/fetch-github/fetch-issues-preview.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { RequestError } from "@octokit/request-error";
import { Octokit } from "@octokit/rest";
import { getGitHubAccessToken, getGitHubUserName } from "../getters/get-github-access-token";
import { GitHubIssue } from "../github-types";
import { displayPopupMessage } from "../rendering/display-popup-modal";
import { gitHubLoginButton } from "../rendering/render-github-login-button";
import { handleRateLimit } from "./handle-rate-limit";
import { TaskNoFull } from "./preview-to-full-mapping";
import { getGitHubUser } from "../getters/get-github-user";
import { RequestError } from "@octokit/request-error";

async function checkPrivateRepoAccess(): Promise<boolean> {
const octokit = new Octokit({ auth: await getGitHubAccessToken() });
Expand Down Expand Up @@ -58,20 +59,7 @@ export async function fetchIssuePreviews(): Promise<TaskNoFull[]> {

// Fetch issues from the private repository only if the user has access
if (hasPrivateRepoAccess) {
const { data: privateResponse } = await octokit.issues.listForRepo({
owner: "ubiquity",
repo: "devpool-directory-private",
state: "open",
});
const privateIssues = privateResponse.filter((issue: GitHubIssue) => !issue.pull_request);

// Mark private issues
const privateIssuesWithFlag = privateIssues.map((issue) => {
return issue;
});

// Combine public and private issues
freshIssues = [...privateIssuesWithFlag, ...publicIssues];
await fetchPrivateIssues(publicIssues);
} else {
// If user doesn't have access, only load issues from the public repository
freshIssues = publicIssues;
Expand All @@ -80,7 +68,15 @@ export async function fetchIssuePreviews(): Promise<TaskNoFull[]> {
if (error instanceof RequestError && error.status === 403) {
await handleRateLimit(octokit, error);
} else {
console.error("Error fetching issue previews:", error);
// renderErrorInModal(error as Error, "You have been rate limited. Please login to increase your limits."); // @DEV: user another method to render the modal not as an error
displayPopupMessage({
modalHeader: "GitHub API rate limit exceeded.",
modalBody: "You have been rate limited. Please login to increase your limits.",
isError: false,
});
gitHubLoginButton?.classList.add("highlight");
// throw error;
// console.error("You have been rate limited. Please login to increase your limits. ", error);
}
}

Expand All @@ -92,45 +88,25 @@ export async function fetchIssuePreviews(): Promise<TaskNoFull[]> {
})) as TaskNoFull[];

return tasks;
}

function rateLimitModal(message: string) {
displayPopupMessage(`GitHub API rate limit exceeded.`, message);
}

type RateLimit = {
reset: number | null;
user: boolean;
};

export async function handleRateLimit(octokit?: Octokit, error?: RequestError) {
const rate: RateLimit = {
reset: null,
user: false,
};

if (error?.response?.headers["x-ratelimit-reset"]) {
rate.reset = parseInt(error.response.headers["x-ratelimit-reset"]);
}
async function fetchPrivateIssues(publicIssues: GitHubIssue[]) {
const { data: privateResponse } = await octokit.issues.listForRepo({
owner: "ubiquity",
repo: "devpool-directory-private",
state: "open",
});
const privateIssues = privateResponse.filter((issue: GitHubIssue) => !issue.pull_request);

if (octokit) {
try {
const core = await octokit.rest.rateLimit.get();
const remaining = core.data.resources.core.remaining;
const reset = core.data.resources.core.reset;
// Mark private issues
const privateIssuesWithFlag = privateIssues.map((issue) => {
return issue;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just realized this is unnecessary in its current form.

});

rate.reset = !rate.reset && remaining === 0 ? reset : rate.reset;
rate.user = (await getGitHubUser()) ? true : false;
} catch (err) {
console.error("Error handling GitHub rate limit", err);
}
// Combine public and private issues
freshIssues = [...privateIssuesWithFlag, ...publicIssues];
}
}

const resetParsed = rate.reset && new Date(rate.reset * 1000).toLocaleTimeString();

if (!rate.user) {
rateLimitModal(`You have been rate limited. Please log in to GitHub to increase your GitHub API limits, otherwise you can try again at ${resetParsed}.`);
} else {
rateLimitModal(`You have been rate limited. Please try again at ${resetParsed}.`);
}
export function rateLimitModal(message: string) {
displayPopupMessage({ modalHeader: `GitHub API rate limit exceeded.`, modalBody: message, isError: false });
}
42 changes: 42 additions & 0 deletions src/home/fetch-github/handle-rate-limit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { RequestError } from "@octokit/request-error";
import { Octokit } from "@octokit/rest";
import { getGitHubUser } from "../getters/get-github-user";
import { renderErrorInModal } from "../rendering/display-popup-modal";
import { rateLimitModal } from "./fetch-issues-preview";

type RateLimit = {
reset: number | null;
user: boolean;
};

export async function handleRateLimit(octokit?: Octokit, error?: RequestError) {
const rate: RateLimit = {
reset: null,
user: false,
};

if (error?.response?.headers["x-ratelimit-reset"]) {
rate.reset = parseInt(error.response.headers["x-ratelimit-reset"]);
}

if (octokit) {
try {
const core = await octokit.rest.rateLimit.get();
const remaining = core.data.resources.core.remaining;
const reset = core.data.resources.core.reset;

rate.reset = !rate.reset && remaining === 0 ? reset : rate.reset;
rate.user = (await getGitHubUser()) ? true : false;
} catch (err) {
renderErrorInModal(err as Error, "Error handling GitHub rate limit");
}
}

const resetParsed = rate.reset && new Date(rate.reset * 1000).toLocaleTimeString();

if (!rate.user) {
rateLimitModal(`You have been rate limited. Please log in to GitHub to increase your GitHub API limits, otherwise you can try again at ${resetParsed}.`);
} else {
rateLimitModal(`You have been rate limited. Please try again at ${resetParsed}.`);
}
}
8 changes: 6 additions & 2 deletions src/home/getters/get-github-user.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { RequestError } from "@octokit/request-error";
import { Octokit } from "@octokit/rest";
import { handleRateLimit } from "../fetch-github/handle-rate-limit";
import { GitHubUser, GitHubUserResponse } from "../github-types";
import { OAuthToken } from "./get-github-access-token";
import { getLocalStore } from "./get-local-store";
import { handleRateLimit } from "../fetch-github/fetch-issues-preview";
import { RequestError } from "@octokit/request-error";
declare const SUPABASE_STORAGE_KEY: string; // @DEV: passed in at build time check build/esbuild-build.ts

export async function getGitHubUser(): Promise<GitHubUser | null> {
Expand Down Expand Up @@ -44,6 +44,10 @@ async function getNewGitHubUser(providerToken: string | null): Promise<GitHubUse
if (error instanceof RequestError && error.status === 403) {
await handleRateLimit(providerToken ? octokit : undefined, error);
}
// renderErrorInModal(error as Error, "You have been logged out. Please login again."); // @DEV: user another method to render the modal not as an error
0x4007 marked this conversation as resolved.
Show resolved Hide resolved
// gitHubLoginButton?.classList.add("highlight");
// throw error;
console.warn("You have been logged out. Please login again.", error);
}
return null;
}
3 changes: 2 additions & 1 deletion src/home/getters/get-local-store.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { TaskStorageItems } from "../github-types";
import { renderErrorInModal } from "../rendering/display-popup-modal";
import { OAuthToken } from "./get-github-access-token";

export function getLocalStore(key: string): TaskStorageItems | OAuthToken | null {
Expand All @@ -9,7 +10,7 @@ export function getLocalStore(key: string): TaskStorageItems | OAuthToken | null

return value; // as OAuthToken;
} catch (error) {
console.error(error);
renderErrorInModal(error as Error, "Failed to parse cached issues from local storage");
}
}
return null;
Expand Down
31 changes: 18 additions & 13 deletions src/home/home.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,23 @@ import { initiateDevRelTracking } from "./devrel-tracker";
import { fetchAndDisplayPreviewsFromCache } from "./fetch-github/fetch-and-display-previews";
import { fetchIssuesFull } from "./fetch-github/fetch-issues-full";
import { readyToolbar } from "./ready-toolbar";
import { renderErrorInModal } from "./rendering/display-popup-modal";
import { generateSortingToolbar } from "./sorting/generate-sorting-buttons";
import { TaskManager } from "./task-manager";

// All unhandled errors are caught and displayed in a modal
window.addEventListener("error", (event: ErrorEvent) => renderErrorInModal(event.error));

// All unhandled promise rejections are caught and displayed in a modal
window.addEventListener("unhandledrejection", (event: PromiseRejectionEvent) => {
renderErrorInModal(event.reason as Error);
event.preventDefault();
});

initiateDevRelTracking();
generateSortingToolbar();
renderServiceMessage();

grid(document.getElementById("grid") as HTMLElement, () => document.body.classList.add("grid-loaded")); // @DEV: display grid background
const container = document.getElementById("issues-container") as HTMLDivElement;

Expand All @@ -18,21 +29,14 @@ if (!container) {
}

export const taskManager = new TaskManager(container);
// window["taskManager"] = taskManager;

void (async function home() {
try {
void authentication();
void readyToolbar();
const previews = await fetchAndDisplayPreviewsFromCache();
const fullTasks = await fetchIssuesFull(previews);
taskManager.syncTasks(fullTasks);
console.trace({ fullTasks });
await taskManager.writeToStorage();
return fullTasks;
} catch (error) {
console.error(error);
}
void authentication();
void readyToolbar();
const previews = await fetchAndDisplayPreviewsFromCache();
const fullTasks = await fetchIssuesFull(previews);
taskManager.syncTasks(fullTasks);
await taskManager.writeToStorage();

if ("serviceWorker" in navigator) {
window.addEventListener("load", () => {
Expand All @@ -46,6 +50,7 @@ void (async function home() {
);
});
}
return fullTasks;
})();

function renderServiceMessage() {
Expand Down
5 changes: 3 additions & 2 deletions src/home/rendering/display-github-user-information.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { isOrgMemberWithoutScope } from "../getters/get-github-access-token";
import { GitHubUser } from "../github-types";
import { renderErrorInModal } from "./display-popup-modal";
import { getSupabase, renderAugmentAccessButton } from "./render-github-login-button";

export async function displayGitHubUserInformation(gitHubUser: GitHubUser) {
Expand All @@ -25,8 +26,8 @@ export async function displayGitHubUserInformation(gitHubUser: GitHubUser) {
const supabase = getSupabase();
const { error } = await supabase.auth.signOut();
if (error) {
console.error("Error logging out:", error);
alert(error);
renderErrorInModal(error, "Error logging out");
alert("Error logging out");
}
window.location.reload();
});
Expand Down
Loading