-
Notifications
You must be signed in to change notification settings - Fork 0
feat: implement AG Grid for user management with header and filters #1
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,27 +1,12 @@ | ||
| import './index.css' | ||
|
|
||
| import './index.css'; | ||
| import { UsersGrid } from './features/users-grid/components/UsersGrid'; | ||
| function App() { | ||
|
|
||
| return ( | ||
| <> | ||
| <div className="bg-green-500 text-white p-4 mt-4 rounded-lg shadow-md"> | ||
| <p className="text-lg">User Management System</p> | ||
| </div> | ||
| <div className="bg-red-500 text-white p-4 mt-4 rounded-lg shadow-md"> | ||
| <button className="bg-white text-red-500 px-4 py-2 rounded hover:bg-gray-100">Login</button> | ||
| </div> | ||
| <div className="bg-yellow-500 text-black p-4 mt-4 rounded-lg shadow-md"> | ||
| <input className="border border-gray-300 p-2 rounded w-full" placeholder="Enter username" /> | ||
| </div> | ||
| <div className="bg-purple-500 text-white p-4 mt-4 rounded-lg shadow-md"> | ||
| <ul className="list-disc list-inside"> | ||
| <li>Manage Users</li> | ||
| <li>View Profiles</li> | ||
| <li>Edit Settings</li> | ||
| </ul> | ||
| </div> | ||
| </> | ||
| ) | ||
| <div className="flex flex-col gap-8 p-8"> | ||
| <UsersGrid /> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| export default App |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import clsx from "clsx"; | ||
|
|
||
| type ButtonProps = { | ||
| label: string; | ||
| onClick: () => void; | ||
| isActive?: boolean; | ||
| }; | ||
|
|
||
| export function Button({ label, onClick, isActive }: ButtonProps) { | ||
| const base = "px-4 py-2 text-sm rounded transition font-medium"; | ||
|
|
||
| return ( | ||
| <button | ||
| onClick={onClick} | ||
| className={clsx( | ||
| base, | ||
| isActive | ||
| ? "bg-green-600 text-white" | ||
| : "bg-gray-200 text-gray-700 hover:bg-gray-300", | ||
| )} | ||
| > | ||
| {label} | ||
| </button> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| import { describe, it, expect, vi } from "vitest"; | ||
| import { render, screen } from "@testing-library/react"; | ||
|
|
||
| import { UsersGrid } from "./UsersGrid"; | ||
| import type { UserProps } from "../types/users.types"; | ||
|
|
||
| const mockUsers: UserProps[] = [ | ||
| { | ||
| id: 1, | ||
| firstName: "John", | ||
| lastName: "Doe", | ||
| maidenName: "Smith", | ||
| age: 30, | ||
| gender: "male", | ||
| email: "john@test.com", | ||
| phone: "1234567890", | ||
| username: "john doe", | ||
| birthDate: "1994-01-01", | ||
| bloodGroup: "O+", | ||
| height: 180, | ||
| weight: 75, | ||
| eyeColor: "Blue", | ||
| isActive: true, | ||
| }, | ||
| ]; | ||
|
|
||
| vi.mock("../hooks/useUsersGrid", () => ({ | ||
| useUsersApi: () => ({ | ||
| users: mockUsers, | ||
| isLoading: false, | ||
| }), | ||
| })); | ||
|
|
||
| vi.mock("ag-grid-react", () => ({ | ||
| AgGridReact: () => ( | ||
| <div> | ||
| <div>First Name</div> | ||
| <div>John</div> | ||
| </div> | ||
| ), | ||
| })); | ||
|
|
||
| const renderComponent = () => render(<UsersGrid />); | ||
|
|
||
| describe("UsersGrid", () => { | ||
| it("renders the grid heading", () => { | ||
| renderComponent(); | ||
|
|
||
| expect( | ||
| screen.getByRole("heading", { | ||
| name: /user management grid/i, | ||
| }), | ||
| ).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("renders column headers", () => { | ||
| renderComponent(); | ||
|
|
||
| expect(screen.getByText("First Name")).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("renders user data in the grid", () => { | ||
| renderComponent(); | ||
|
|
||
| expect(screen.getByText("John")).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("renders filter toggle buttons", () => { | ||
| renderComponent(); | ||
|
|
||
| expect( | ||
| screen.getByRole("button", { name: /show filters/i }), | ||
| ).toBeInTheDocument(); | ||
|
|
||
| expect( | ||
| screen.getByRole("button", { name: /hide filters/i }), | ||
| ).toBeInTheDocument(); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| import { useState } from "react"; | ||
| import { AgGridReact } from "ag-grid-react"; | ||
| import { ModuleRegistry, AllCommunityModule } from "ag-grid-community"; | ||
| import { SetFilterModule } from "ag-grid-enterprise"; | ||
| import { useUsersApi } from "../hooks/useUsersGrid"; | ||
| import type { UserProps } from "../types/users.types"; | ||
|
|
||
| import { | ||
| DEFAULT_PAGINATION_PAGE_SIZE, | ||
| PAGINATION_PAGE_SIZE_OPTIONS, | ||
| } from "../constants/users.constants"; | ||
|
|
||
| import "ag-grid-community/styles/ag-theme-alpine.css"; | ||
| import { getUserColumnDefs, getDefaultColDef } from "../utils/userColumnDefs"; | ||
| import { Button } from "./Button"; | ||
|
|
||
| ModuleRegistry.registerModules([AllCommunityModule, SetFilterModule]); | ||
|
|
||
| export function UsersGrid() { | ||
| const { users: userData, isLoading } = useUsersApi(); | ||
| const [showFilters, setShowFilters] = useState(false); | ||
|
|
||
| const columnDefs = getUserColumnDefs(showFilters); | ||
| const defaultColDef = getDefaultColDef(showFilters); | ||
|
|
||
| const filterButtons = [ | ||
| { | ||
| label: "Show Filters", | ||
| isActive: showFilters, | ||
| onClick: () => setShowFilters(true), | ||
| }, | ||
| { | ||
| label: "Hide Filters", | ||
| isActive: !showFilters, | ||
| onClick: () => setShowFilters(false), | ||
| }, | ||
| ]; | ||
|
|
||
| return ( | ||
| <div className="h-screen flex flex-col bg-gray-50"> | ||
| {/* Header */} | ||
| <div className="flex items-center justify-between px-6 py-4 bg-white border-b shadow-sm"> | ||
| <h2 className="text-xl font-bold tracking-wide text-gray-800"> | ||
| 👥 User Management Grid | ||
| </h2> | ||
|
|
||
| <div className="flex gap-3"> | ||
| {filterButtons.map(({ label, onClick, isActive }) => ( | ||
| <Button | ||
| key={label} | ||
| label={label} | ||
| onClick={onClick} | ||
| isActive={isActive} | ||
| /> | ||
| ))} | ||
| </div> | ||
| </div> | ||
|
|
||
| {/* Grid Wrapper */} | ||
| <div className="flex-1 p-4 overflow-x-auto"> | ||
| <AgGridReact<UserProps> | ||
| className="ag-theme-alpine min-w-300 rounded-lg border shadow-sm bg-violet-500" | ||
| containerStyle={{ height: 600 }} | ||
| rowData={userData} | ||
| columnDefs={columnDefs} | ||
| defaultColDef={defaultColDef} | ||
| loading={isLoading} | ||
| pagination | ||
| paginationPageSize={DEFAULT_PAGINATION_PAGE_SIZE} | ||
| paginationPageSizeSelector={PAGINATION_PAGE_SIZE_OPTIONS} | ||
| /> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| export const USERS_API_BASE_URL = 'https://dummyjson.com'; | ||
|
|
||
| export const DEFAULT_PAGINATION_PAGE_SIZE = 10; | ||
| export const PAGINATION_PAGE_SIZE_OPTIONS = [10, 20, 50, 100]; | ||
|
|
||
| export const Gender = { | ||
| Male: 'male', | ||
| Female: 'female', | ||
| } as const; | ||
|
|
||
| export type Gender = (typeof Gender)[keyof typeof Gender]; | ||
|
|
||
| export const GENDER_OPTIONS = [Gender.Male, Gender.Female]; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| import { useEffect, useState } from 'react'; | ||
| import type { UserProps } from '../types/users.types'; | ||
| import { USERS_API_BASE_URL } from '../constants/users.constants'; | ||
| import axios from 'axios'; | ||
|
|
||
| export function useUsersApi() { | ||
| const [users, setUsers] = useState<UserProps[]>([]); | ||
| const [isLoading, setIsLoading] = useState(false); | ||
|
|
||
| useEffect(function () { | ||
| let isMounted = true; | ||
| const abortController = new AbortController(); | ||
|
|
||
| async function fetchUsersOnMount() { | ||
| setIsLoading(true); | ||
|
|
||
| try { | ||
|
Comment on lines
+14
to
+17
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Severity: 🟠 Major Errors from the request are currently swallowed; add a try {
...
} catch (error) {
if (isMounted) console.error('Failed to load users', error);
} |
||
| const response = await axios.get<{ users: UserProps[] }>(`${USERS_API_BASE_URL}/users`, { | ||
| signal: abortController.signal, | ||
| }); | ||
|
|
||
| if (isMounted) { | ||
| const mappedUsers = response.data.users.map(function (user: UserProps) { | ||
| return { | ||
| ...user, | ||
| isActive: user.age < 50, | ||
| }; | ||
| }); | ||
|
|
||
| setUsers(mappedUsers); | ||
| } | ||
| } catch (error) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Severity: 🟠 Major Expose an } catch (error: unknown) {
if (isMounted) {
setError(error);
}
} |
||
| if (isMounted && !axios.isCancel(error)) { | ||
| console.error('Failed to load users', error); | ||
| } | ||
| } finally { | ||
| if (isMounted) { | ||
| setIsLoading(false); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| fetchUsersOnMount(); | ||
|
|
||
| return function () { | ||
| isMounted = false; | ||
| abortController.abort(); | ||
| }; | ||
| }, []); | ||
|
|
||
| return { | ||
| users, | ||
| isLoading, | ||
| }; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Severity: 🟠 Major
Add a small unit test for the error path (e.g., axios rejects) to ensure
isLoadingresets and the newerrorstate is set, preventing regressions to “empty grid with no signal.”