Skip to content

Commit

Permalink
Add filters
Browse files Browse the repository at this point in the history
  • Loading branch information
jkettmann committed Dec 8, 2022
1 parent 8ae036c commit 49c8b63
Show file tree
Hide file tree
Showing 19 changed files with 517 additions and 72 deletions.
5 changes: 3 additions & 2 deletions api/issues.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { axios } from "./axios";
import type { Issue } from "./issues.types";
import type { Issue, IssueFilters } from "./issues.types";
import type { Page } from "@typings/page.types";

const ENDPOINT = "/issue";

export async function getIssues(
page: number,
filters: IssueFilters,
options?: { signal?: AbortSignal }
) {
const { data } = await axios.get<Page<Issue>>(ENDPOINT, {
params: { page },
params: { page, ...filters },
signal: options?.signal,
});
return data;
Expand Down
12 changes: 12 additions & 0 deletions api/issues.types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
export enum IssueStatus {
open = "open",
resolved = "resolved",
}

export enum IssueLevel {
info = "info",
warning = "warning",
Expand All @@ -11,5 +16,12 @@ export type Issue = {
message: string;
stack: string;
level: IssueLevel;
status: IssueStatus;
numEvents: number;
};

export type IssueFilters = {
level?: IssueLevel;
status?: IssueStatus;
project?: string;
};
18 changes: 10 additions & 8 deletions features/issues/api/use-get-issues.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,34 @@ 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";
import type { Issue, IssueFilters } from "@api/issues.types";
import { useFilters } from "../hooks/use-filters";

const QUERY_KEY = "issues";

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

export function useGetIssues(page: number) {
const { filters } = useFilters();
const query = useQuery<Page<Issue>, Error>(
getQueryKey(page),
({ signal }) => getIssues(page, { signal }),
getQueryKey(page, filters),
({ signal }) => getIssues(page, filters, { 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, { signal })
queryClient.prefetchQuery(getQueryKey(page + 1, filters), ({ signal }) =>
getIssues(page + 1, filters, { signal })
);
}
}, [query.data, page, queryClient]);
}, [query.data, page, filters, queryClient]);
return query;
}
31 changes: 31 additions & 0 deletions features/issues/components/filters/filters.styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import styled from "styled-components";
import { breakpoint } from "@styles/theme";

export const Container = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
margin-block: 1rem;
gap: 1rem;
width: 100%;
@media (min-width: ${breakpoint("desktop")}) {
flex-direction: row;
justify-content: flex-end;
order: initial;
gap: 3rem;
flex-wrap: wrap;
}
`;

export const RightContainer = styled.div`
margin-bottom: 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
order: -1;
@media (min-width: ${breakpoint("desktop")}) {
flex-direction: row;
gap: 3rem;
order: initial;
}
`;
162 changes: 162 additions & 0 deletions features/issues/components/filters/filters.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import React, {
useState,
useEffect,
useCallback,
useRef,
useContext,
} from "react";
import { useRouter } from "next/router";
import { useWindowSize } from "react-use";
import { Select, Option, Input } from "@features/ui";
import { NavigationContext } from "@features/layout";
import { useFilters } from "../../hooks/use-filters";
import { IssueLevel, IssueStatus } from "@api/issues.types";
import { useProjects } from "@features/projects";
import * as S from "./filters.styled";

export function Filters() {
const { handleFilters, filters } = useFilters();
const { data: projects } = useProjects();
const router = useRouter();
const routerQueryProjectName =
(router.query.projectName as string)?.toLowerCase() || undefined;
const [inputValue, setInputValue] = useState<string>("");
const projectNames = projects?.map((project) => project.name.toLowerCase());
const isFirst = useRef(true);
const { width } = useWindowSize();
const isMobileScreen = width <= 1023;
const { isMobileMenuOpen } = useContext(NavigationContext);
const handleChange = (input: string) => {
setInputValue(input);

if (inputValue?.length < 2) {
handleProjectName(undefined);
return;
}

const name = projectNames?.find((name) =>
name?.toLowerCase().includes(inputValue.toLowerCase())
);

if (name) {
handleProjectName(name);
}
};

const handleLevel = (level?: string) => {
if (level) {
level = level.toLowerCase();
}
handleFilters({ level: level as IssueLevel });
};

const handleStatus = (status?: string) => {
if (status === "Unresolved") {
status = "open";
}
if (status) {
status = status.toLowerCase();
}
handleFilters({ status: status as IssueStatus });
};

const handleProjectName = useCallback(
(projectName?: string) =>
handleFilters({ project: projectName?.toLowerCase() }),
[handleFilters]
);

useEffect(() => {
const newObj: { [key: string]: string } = {
...filters,
};

Object.keys(newObj).forEach((key) => {
if (newObj[key] === undefined) {
delete newObj[key];
}
});

const url = {
pathname: router.pathname,
query: {
page: router.query.page || 1,
...newObj,
},
};

if (routerQueryProjectName && isFirst) {
handleProjectName(routerQueryProjectName);
setInputValue(routerQueryProjectName || "");
isFirst.current = false;
}

router.push(url, undefined, { shallow: false });
}, [filters.level, filters.status, filters.project, router.query.page]);

return (
<S.Container>
<Select
placeholder="Status"
defaultValue="Status"
width={isMobileScreen ? "97%" : "8rem"}
data-cy="filter-by-status"
style={{
...(isMobileMenuOpen && {
opacity: 0,
}),
}}
>
<Option value={undefined} handleCallback={handleStatus}>
--None--
</Option>
<Option value="Unresolved" handleCallback={handleStatus}>
Unresolved
</Option>
<Option value="Resolved" handleCallback={handleStatus}>
Resolved
</Option>
</Select>

<Select
placeholder="Level"
defaultValue="Level"
width={isMobileScreen ? "97%" : "8rem"}
data-cy="filter-by-level"
style={{
...(isMobileMenuOpen && {
opacity: 0,
}),
}}
>
<Option value={undefined} handleCallback={handleLevel}>
--None--
</Option>
<Option value="Error" handleCallback={handleLevel}>
Error
</Option>
<Option value="Warning" handleCallback={handleLevel}>
Warning
</Option>
<Option value="Info" handleCallback={handleLevel}>
Info
</Option>
</Select>

<Input
handleChange={handleChange}
value={inputValue}
label="project name"
placeholder="Project Name"
iconSrc="/icons/search-icon.svg"
data-cy="filter-by-project"
style={{
...(isMobileScreen && { width: "94%", marginRight: "3rem" }),
...(isMobileMenuOpen && {
opacity: 0,
}),
}}
/>
</S.Container>
);
}
1 change: 1 addition & 0 deletions features/issues/components/filters/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./filters";
86 changes: 45 additions & 41 deletions features/issues/components/issue-list/issue-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ProjectLanguage } from "@api/projects.types";
import { useProjects } from "@features/projects";
import { useGetIssues } from "../../api";
import { IssueRow } from "./issue-row";
import { Filters } from "../filters";

const Container = styled.div`
background: white;
Expand Down Expand Up @@ -98,46 +99,49 @@ export function IssueList() {
const { items, meta } = issuesPage.data || {};

return (
<Container>
<Table>
<thead>
<HeaderRow>
<HeaderCell>Issue</HeaderCell>
<HeaderCell>Level</HeaderCell>
<HeaderCell>Events</HeaderCell>
<HeaderCell>Users</HeaderCell>
</HeaderRow>
</thead>
<tbody>
{(items || []).map((issue) => (
<IssueRow
key={issue.id}
issue={issue}
projectLanguage={projectIdToLanguage[issue.projectId]}
/>
))}
</tbody>
</Table>
<PaginationContainer>
<div>
<PaginationButton
onClick={() => navigateToPage(page - 1)}
disabled={page === 1}
>
Previous
</PaginationButton>
<PaginationButton
onClick={() => navigateToPage(page + 1)}
disabled={page === meta?.totalPages}
>
Next
</PaginationButton>
</div>
<PageInfo>
Page <PageNumber>{meta?.currentPage}</PageNumber> of{" "}
<PageNumber>{meta?.totalPages}</PageNumber>
</PageInfo>
</PaginationContainer>
</Container>
<>
<Filters />
<Container>
<Table>
<thead>
<HeaderRow>
<HeaderCell>Issue</HeaderCell>
<HeaderCell>Level</HeaderCell>
<HeaderCell>Events</HeaderCell>
<HeaderCell>Users</HeaderCell>
</HeaderRow>
</thead>
<tbody>
{(items || []).map((issue) => (
<IssueRow
key={issue.id}
issue={issue}
projectLanguage={projectIdToLanguage[issue.projectId]}
/>
))}
</tbody>
</Table>
<PaginationContainer>
<div>
<PaginationButton
onClick={() => navigateToPage(page - 1)}
disabled={page === 1}
>
Previous
</PaginationButton>
<PaginationButton
onClick={() => navigateToPage(page + 1)}
disabled={page === meta?.totalPages}
>
Next
</PaginationButton>
</div>
<PageInfo>
Page <PageNumber>{meta?.currentPage}</PageNumber> of{" "}
<PageNumber>{meta?.totalPages}</PageNumber>
</PageInfo>
</PaginationContainer>
</Container>
</>
);
}
Loading

0 comments on commit 49c8b63

Please sign in to comment.