Skip to content

Commit 49c8b63

Browse files
committed
Add filters
1 parent 8ae036c commit 49c8b63

File tree

19 files changed

+517
-72
lines changed

19 files changed

+517
-72
lines changed

api/issues.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import { axios } from "./axios";
2-
import type { Issue } from "./issues.types";
2+
import type { Issue, IssueFilters } from "./issues.types";
33
import type { Page } from "@typings/page.types";
44

55
const ENDPOINT = "/issue";
66

77
export async function getIssues(
88
page: number,
9+
filters: IssueFilters,
910
options?: { signal?: AbortSignal }
1011
) {
1112
const { data } = await axios.get<Page<Issue>>(ENDPOINT, {
12-
params: { page },
13+
params: { page, ...filters },
1314
signal: options?.signal,
1415
});
1516
return data;

api/issues.types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
export enum IssueStatus {
2+
open = "open",
3+
resolved = "resolved",
4+
}
5+
16
export enum IssueLevel {
27
info = "info",
38
warning = "warning",
@@ -11,5 +16,12 @@ export type Issue = {
1116
message: string;
1217
stack: string;
1318
level: IssueLevel;
19+
status: IssueStatus;
1420
numEvents: number;
1521
};
22+
23+
export type IssueFilters = {
24+
level?: IssueLevel;
25+
status?: IssueStatus;
26+
project?: string;
27+
};

features/issues/api/use-get-issues.tsx

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,34 @@ import { useEffect } from "react";
22
import { useQuery, useQueryClient } from "@tanstack/react-query";
33
import { getIssues } from "@api/issues";
44
import type { Page } from "@typings/page.types";
5-
import type { Issue } from "@api/issues.types";
5+
import type { Issue, IssueFilters } from "@api/issues.types";
6+
import { useFilters } from "../hooks/use-filters";
67

78
const QUERY_KEY = "issues";
89

9-
export function getQueryKey(page?: number) {
10+
export function getQueryKey(page?: number, filters?: IssueFilters) {
1011
if (page === undefined) {
1112
return [QUERY_KEY];
1213
}
13-
return [QUERY_KEY, page];
14+
return [QUERY_KEY, page, filters];
1415
}
1516

1617
export function useGetIssues(page: number) {
18+
const { filters } = useFilters();
1719
const query = useQuery<Page<Issue>, Error>(
18-
getQueryKey(page),
19-
({ signal }) => getIssues(page, { signal }),
20+
getQueryKey(page, filters),
21+
({ signal }) => getIssues(page, filters, { signal }),
2022
{ keepPreviousData: true }
2123
);
2224

2325
// Prefetch the next page!
2426
const queryClient = useQueryClient();
2527
useEffect(() => {
2628
if (query.data?.meta.hasNextPage) {
27-
queryClient.prefetchQuery(getQueryKey(page + 1), ({ signal }) =>
28-
getIssues(page + 1, { signal })
29+
queryClient.prefetchQuery(getQueryKey(page + 1, filters), ({ signal }) =>
30+
getIssues(page + 1, filters, { signal })
2931
);
3032
}
31-
}, [query.data, page, queryClient]);
33+
}, [query.data, page, filters, queryClient]);
3234
return query;
3335
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import styled from "styled-components";
2+
import { breakpoint } from "@styles/theme";
3+
4+
export const Container = styled.div`
5+
display: flex;
6+
flex-direction: column;
7+
justify-content: center;
8+
margin-block: 1rem;
9+
gap: 1rem;
10+
width: 100%;
11+
@media (min-width: ${breakpoint("desktop")}) {
12+
flex-direction: row;
13+
justify-content: flex-end;
14+
order: initial;
15+
gap: 3rem;
16+
flex-wrap: wrap;
17+
}
18+
`;
19+
20+
export const RightContainer = styled.div`
21+
margin-bottom: 1rem;
22+
display: flex;
23+
flex-direction: column;
24+
gap: 1rem;
25+
order: -1;
26+
@media (min-width: ${breakpoint("desktop")}) {
27+
flex-direction: row;
28+
gap: 3rem;
29+
order: initial;
30+
}
31+
`;
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import React, {
2+
useState,
3+
useEffect,
4+
useCallback,
5+
useRef,
6+
useContext,
7+
} from "react";
8+
import { useRouter } from "next/router";
9+
import { useWindowSize } from "react-use";
10+
import { Select, Option, Input } from "@features/ui";
11+
import { NavigationContext } from "@features/layout";
12+
import { useFilters } from "../../hooks/use-filters";
13+
import { IssueLevel, IssueStatus } from "@api/issues.types";
14+
import { useProjects } from "@features/projects";
15+
import * as S from "./filters.styled";
16+
17+
export function Filters() {
18+
const { handleFilters, filters } = useFilters();
19+
const { data: projects } = useProjects();
20+
const router = useRouter();
21+
const routerQueryProjectName =
22+
(router.query.projectName as string)?.toLowerCase() || undefined;
23+
const [inputValue, setInputValue] = useState<string>("");
24+
const projectNames = projects?.map((project) => project.name.toLowerCase());
25+
const isFirst = useRef(true);
26+
const { width } = useWindowSize();
27+
const isMobileScreen = width <= 1023;
28+
const { isMobileMenuOpen } = useContext(NavigationContext);
29+
const handleChange = (input: string) => {
30+
setInputValue(input);
31+
32+
if (inputValue?.length < 2) {
33+
handleProjectName(undefined);
34+
return;
35+
}
36+
37+
const name = projectNames?.find((name) =>
38+
name?.toLowerCase().includes(inputValue.toLowerCase())
39+
);
40+
41+
if (name) {
42+
handleProjectName(name);
43+
}
44+
};
45+
46+
const handleLevel = (level?: string) => {
47+
if (level) {
48+
level = level.toLowerCase();
49+
}
50+
handleFilters({ level: level as IssueLevel });
51+
};
52+
53+
const handleStatus = (status?: string) => {
54+
if (status === "Unresolved") {
55+
status = "open";
56+
}
57+
if (status) {
58+
status = status.toLowerCase();
59+
}
60+
handleFilters({ status: status as IssueStatus });
61+
};
62+
63+
const handleProjectName = useCallback(
64+
(projectName?: string) =>
65+
handleFilters({ project: projectName?.toLowerCase() }),
66+
[handleFilters]
67+
);
68+
69+
useEffect(() => {
70+
const newObj: { [key: string]: string } = {
71+
...filters,
72+
};
73+
74+
Object.keys(newObj).forEach((key) => {
75+
if (newObj[key] === undefined) {
76+
delete newObj[key];
77+
}
78+
});
79+
80+
const url = {
81+
pathname: router.pathname,
82+
query: {
83+
page: router.query.page || 1,
84+
...newObj,
85+
},
86+
};
87+
88+
if (routerQueryProjectName && isFirst) {
89+
handleProjectName(routerQueryProjectName);
90+
setInputValue(routerQueryProjectName || "");
91+
isFirst.current = false;
92+
}
93+
94+
router.push(url, undefined, { shallow: false });
95+
}, [filters.level, filters.status, filters.project, router.query.page]);
96+
97+
return (
98+
<S.Container>
99+
<Select
100+
placeholder="Status"
101+
defaultValue="Status"
102+
width={isMobileScreen ? "97%" : "8rem"}
103+
data-cy="filter-by-status"
104+
style={{
105+
...(isMobileMenuOpen && {
106+
opacity: 0,
107+
}),
108+
}}
109+
>
110+
<Option value={undefined} handleCallback={handleStatus}>
111+
--None--
112+
</Option>
113+
<Option value="Unresolved" handleCallback={handleStatus}>
114+
Unresolved
115+
</Option>
116+
<Option value="Resolved" handleCallback={handleStatus}>
117+
Resolved
118+
</Option>
119+
</Select>
120+
121+
<Select
122+
placeholder="Level"
123+
defaultValue="Level"
124+
width={isMobileScreen ? "97%" : "8rem"}
125+
data-cy="filter-by-level"
126+
style={{
127+
...(isMobileMenuOpen && {
128+
opacity: 0,
129+
}),
130+
}}
131+
>
132+
<Option value={undefined} handleCallback={handleLevel}>
133+
--None--
134+
</Option>
135+
<Option value="Error" handleCallback={handleLevel}>
136+
Error
137+
</Option>
138+
<Option value="Warning" handleCallback={handleLevel}>
139+
Warning
140+
</Option>
141+
<Option value="Info" handleCallback={handleLevel}>
142+
Info
143+
</Option>
144+
</Select>
145+
146+
<Input
147+
handleChange={handleChange}
148+
value={inputValue}
149+
label="project name"
150+
placeholder="Project Name"
151+
iconSrc="/icons/search-icon.svg"
152+
data-cy="filter-by-project"
153+
style={{
154+
...(isMobileScreen && { width: "94%", marginRight: "3rem" }),
155+
...(isMobileMenuOpen && {
156+
opacity: 0,
157+
}),
158+
}}
159+
/>
160+
</S.Container>
161+
);
162+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./filters";

features/issues/components/issue-list/issue-list.tsx

Lines changed: 45 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { ProjectLanguage } from "@api/projects.types";
55
import { useProjects } from "@features/projects";
66
import { useGetIssues } from "../../api";
77
import { IssueRow } from "./issue-row";
8+
import { Filters } from "../filters";
89

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

100101
return (
101-
<Container>
102-
<Table>
103-
<thead>
104-
<HeaderRow>
105-
<HeaderCell>Issue</HeaderCell>
106-
<HeaderCell>Level</HeaderCell>
107-
<HeaderCell>Events</HeaderCell>
108-
<HeaderCell>Users</HeaderCell>
109-
</HeaderRow>
110-
</thead>
111-
<tbody>
112-
{(items || []).map((issue) => (
113-
<IssueRow
114-
key={issue.id}
115-
issue={issue}
116-
projectLanguage={projectIdToLanguage[issue.projectId]}
117-
/>
118-
))}
119-
</tbody>
120-
</Table>
121-
<PaginationContainer>
122-
<div>
123-
<PaginationButton
124-
onClick={() => navigateToPage(page - 1)}
125-
disabled={page === 1}
126-
>
127-
Previous
128-
</PaginationButton>
129-
<PaginationButton
130-
onClick={() => navigateToPage(page + 1)}
131-
disabled={page === meta?.totalPages}
132-
>
133-
Next
134-
</PaginationButton>
135-
</div>
136-
<PageInfo>
137-
Page <PageNumber>{meta?.currentPage}</PageNumber> of{" "}
138-
<PageNumber>{meta?.totalPages}</PageNumber>
139-
</PageInfo>
140-
</PaginationContainer>
141-
</Container>
102+
<>
103+
<Filters />
104+
<Container>
105+
<Table>
106+
<thead>
107+
<HeaderRow>
108+
<HeaderCell>Issue</HeaderCell>
109+
<HeaderCell>Level</HeaderCell>
110+
<HeaderCell>Events</HeaderCell>
111+
<HeaderCell>Users</HeaderCell>
112+
</HeaderRow>
113+
</thead>
114+
<tbody>
115+
{(items || []).map((issue) => (
116+
<IssueRow
117+
key={issue.id}
118+
issue={issue}
119+
projectLanguage={projectIdToLanguage[issue.projectId]}
120+
/>
121+
))}
122+
</tbody>
123+
</Table>
124+
<PaginationContainer>
125+
<div>
126+
<PaginationButton
127+
onClick={() => navigateToPage(page - 1)}
128+
disabled={page === 1}
129+
>
130+
Previous
131+
</PaginationButton>
132+
<PaginationButton
133+
onClick={() => navigateToPage(page + 1)}
134+
disabled={page === meta?.totalPages}
135+
>
136+
Next
137+
</PaginationButton>
138+
</div>
139+
<PageInfo>
140+
Page <PageNumber>{meta?.currentPage}</PageNumber> of{" "}
141+
<PageNumber>{meta?.totalPages}</PageNumber>
142+
</PageInfo>
143+
</PaginationContainer>
144+
</Container>
145+
</>
142146
);
143147
}

0 commit comments

Comments
 (0)