Skip to content
Open
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
2 changes: 2 additions & 0 deletions .env.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
NEXT_PUBLIC_API_BASE_URL=https://prolog-api.profy.dev/v2
NEXT_PUBLIC_API_TOKEN=my-access-token
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ yarn-error.log*

.vscode
.eslintcache
.env

# Cypress test videos
cypress/videos/
29 changes: 29 additions & 0 deletions api/axios.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import assert from "assert";
import Axios, { AxiosRequestConfig } from "axios";

assert(
process.env.NEXT_PUBLIC_API_BASE_URL,
"env variable not set: NEXT_PUBLIC_API_BASE_URL"
);

assert(
process.env.NEXT_PUBLIC_API_TOKEN,
"env variable not set: NEXT_PUBLIC_API_TOKEN"
);

function authRequestInterceptor(config: AxiosRequestConfig) {
if (!config.headers) {
config.headers = {};
}

// assertion that env variable exists was already done outside the function
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
config.headers.authorization = process.env.NEXT_PUBLIC_API_TOKEN!;
return config;
}

export const axios = Axios.create({
baseURL: process.env.NEXT_PUBLIC_API_BASE_URL,
});

axios.interceptors.request.use(authRequestInterceptor);
28 changes: 28 additions & 0 deletions api/issues.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { axios } from "./axios";
import type { Issue } from "./issues.types";
import type { Page } from "@typings/page.types";

type IssueFilters = {
status?: "open" | "resolved";
};

const ENDPOINT = "/issue";

export async function getIssues(
page: number,
filters: IssueFilters = {},
options?: { signal?: AbortSignal }
) {
const { data } = await axios.get<Page<Issue>>(ENDPOINT, {
params: { page, ...filters },
signal: options?.signal,
});
return data;
}

export async function resolveIssue(issueId: string) {
const { data } = await axios.patch(`${ENDPOINT}/${issueId}`, {
status: "resolved",
});
return data;
}
File renamed without changes.
9 changes: 9 additions & 0 deletions api/projects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { axios } from "./axios";
import type { Project } from "./projects.types";

const ENDPOINT = "/project";

export async function getProjects() {
const { data } = await axios.get<Project[]>(ENDPOINT);
return data;
}
File renamed without changes.
7 changes: 7 additions & 0 deletions api/query-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { QueryClient } from "@tanstack/react-query";

const defaultQueryConfig = { staleTime: 60000 };

export const queryClient = new QueryClient({
defaultOptions: { queries: defaultQueryConfig },
});
2 changes: 2 additions & 0 deletions features/issues/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./use-get-issues";
export * from "./use-resolve-issue";
33 changes: 33 additions & 0 deletions features/issues/api/use-get-issues.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { useEffect } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { getIssues } from "@api/issues";
import type { Page } from "@typings/page.types";
import type { Issue } from "@api/issues.types";

const QUERY_KEY = "issues";

export function getQueryKey(page?: number) {
if (page === undefined) {
return [QUERY_KEY];
}
return [QUERY_KEY, page];
}

export function useGetIssues(page: number) {
const query = useQuery<Page<Issue>, Error>(
getQueryKey(page),
({ signal }) => getIssues(page, { status: "open" }, { signal }),
{ keepPreviousData: true }
);

// Prefetch the next page!
const queryClient = useQueryClient();
useEffect(() => {
if (query.data?.meta.hasNextPage) {
queryClient.prefetchQuery(getQueryKey(page + 1), ({ signal }) =>
getIssues(page + 1, { status: "open" }, { signal })
);
}
}, [query.data, page, queryClient]);
return query;
}
37 changes: 0 additions & 37 deletions features/issues/api/use-issues.tsx

This file was deleted.

62 changes: 62 additions & 0 deletions features/issues/api/use-resolve-issue.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useRef } from "react";
import { resolveIssue } from "@api/issues";
import * as GetIssues from "./use-get-issues";
import type { Issue } from "@api/issues.types";

export function useResolveIssue(page: number) {
const queryClient = useQueryClient();
const ongoingMutationCount = useRef(0);
return useMutation((issueId) => resolveIssue(issueId), {
onMutate: async (issueId: string) => {
ongoingMutationCount.current += 1;

await queryClient.cancelQueries(GetIssues.getQueryKey());

const currentPage = queryClient.getQueryData<{ items: Issue[] }>(
GetIssues.getQueryKey(page)
);
const nextPage = queryClient.getQueryData<{ items: Issue[] }>(
GetIssues.getQueryKey(page + 1)
);

if (!currentPage) {
return;
}

const newItems = currentPage.items.filter(({ id }) => id !== issueId);

if (nextPage?.items.length) {
const lastIssueOnPage = currentPage.items[currentPage.items.length - 1];
const indexOnNextPage = nextPage.items.findIndex(
(issue) => issue.id === lastIssueOnPage.id
);
const nextIssue = nextPage.items[indexOnNextPage + 1];
if (nextIssue) {
newItems.push(nextIssue);
}
}

queryClient.setQueryData(GetIssues.getQueryKey(page), {
...currentPage,
items: newItems,
});

return { currentIssuesPage: currentPage };
},
onError: (err, issueId, context) => {
if (context?.currentIssuesPage) {
queryClient.setQueryData(
GetIssues.getQueryKey(page),
context.currentIssuesPage
);
}
},
onSettled: () => {
ongoingMutationCount.current -= 1;
if (ongoingMutationCount.current === 0) {
queryClient.invalidateQueries(GetIssues.getQueryKey());
}
},
});
}
48 changes: 11 additions & 37 deletions features/issues/components/issue-list/issue-list.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useRouter } from "next/router";
import { useState } from "react";
import styled from "styled-components";
import { useIssues } from "@features/issues";
import { ProjectLanguage, useProjects } from "@features/projects";
import { ProjectLanguage } from "@api/projects.types";
import { color, space, textFont } from "@styles/theme";
import { IssueRow } from "./issue-row";
import { useGetIssues, useResolveIssue } from "../../api";

const Container = styled.div`
background: white;
Expand Down Expand Up @@ -62,39 +62,12 @@ const PageNumber = styled.span`
`;

export function IssueList() {
const router = useRouter();
const page = Number(router.query.page || 1);
const navigateToPage = (newPage: number) =>
router.push({
pathname: router.pathname,
query: { page: newPage },
});
const [page, setPage] = useState(1);

const issuesPage = useIssues(page);
const projects = useProjects();
const issuePage = useGetIssues(page);
const resolveIssue = useResolveIssue(page);

if (projects.isLoading || issuesPage.isLoading) {
return <div>Loading</div>;
}

if (projects.isError) {
console.error(projects.error);
return <div>Error loading projects: {projects.error.message}</div>;
}

if (issuesPage.isError) {
console.error(issuesPage.error);
return <div>Error loading issues: {issuesPage.error.message}</div>;
}

const projectIdToLanguage = (projects.data || []).reduce(
(prev, project) => ({
...prev,
[project.id]: project.language,
}),
{} as Record<string, ProjectLanguage>
);
const { items, meta } = issuesPage.data || {};
const { items, meta } = issuePage.data || {};

return (
<Container>
Expand All @@ -112,21 +85,22 @@ export function IssueList() {
<IssueRow
key={issue.id}
issue={issue}
projectLanguage={projectIdToLanguage[issue.projectId]}
projectLanguage={ProjectLanguage.react}
resolveIssue={() => resolveIssue.mutate(issue.id)}
/>
))}
</tbody>
</Table>
<PaginationContainer>
<div>
<PaginationButton
onClick={() => navigateToPage(page - 1)}
onClick={() => setPage(page - 1)}
disabled={page === 1}
>
Previous
</PaginationButton>
<PaginationButton
onClick={() => navigateToPage(page + 1)}
onClick={() => setPage(page + 1)}
disabled={page === meta?.totalPages}
>
Next
Expand Down
40 changes: 36 additions & 4 deletions features/issues/components/issue-list/issue-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import styled from "styled-components";
import capitalize from "lodash/capitalize";
import { color, space, textFont } from "@styles/theme";
import { Badge, BadgeColor, BadgeSize } from "@features/ui";
import { IssueLevel } from "../../types/issue.types";
import { ProjectLanguage } from "@features/projects";
import type { Issue } from "../../types/issue.types";
import { ProjectLanguage } from "@api/projects.types";
import { IssueLevel } from "@api/issues.types";
import type { Issue } from "@api/issues.types";

type IssueRowProps = {
projectLanguage: ProjectLanguage;
issue: Issue;
resolveIssue: () => void;
};

const levelColors = {
Expand Down Expand Up @@ -47,9 +48,30 @@ const ErrorType = styled.span`
${textFont("sm", "medium")}
`;

export function IssueRow({ projectLanguage, issue }: IssueRowProps) {
const ResolveButton = styled.button`
width: 1.5rem;
height: 1.5rem;
font-size: 0.8rem;
display: flex;
align-items: center;
justify-content: center;
color: #aaa;
border: 1px solid #aaa;
background: none;
border-radius: 50%;
padding: none;
margin: none;
cursor: pointer;
`;

export function IssueRow({
projectLanguage,
issue,
resolveIssue,
}: IssueRowProps) {
const { name, message, stack, level, numEvents } = issue;
const firstLineOfStackTrace = stack.split("\n")[1];

return (
<Row>
<IssueCell>
Expand All @@ -72,6 +94,16 @@ export function IssueRow({ projectLanguage, issue }: IssueRowProps) {
</Cell>
<Cell>{numEvents}</Cell>
<Cell>{numEvents}</Cell>
<Cell>
<ResolveButton onClick={resolveIssue}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M20.285 2l-11.285 11.567-5.286-5.011-3.714 3.716 9 8.728 15-15.285z"
/>
</svg>
</ResolveButton>
</Cell>
</Row>
);
}
3 changes: 1 addition & 2 deletions features/issues/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
export * from "./api/use-issues";
export * from "./api";
export * from "./components/issue-list";
export * from "./types/issue.types";
1 change: 1 addition & 0 deletions features/projects/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./use-projects";
Loading