Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 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
6 changes: 5 additions & 1 deletion frontend/src/components/Admin/AddUser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,11 @@ const AddUser = ({ isOpen, onClose }: AddUserProps) => {
},
onError: (err: ApiError) => {
const errDetail = (err.body as any)?.detail
showToast("Something went wrong.", `${errDetail}`, "error")
let errorMessage = "Something went wrong."
if (Array.isArray(errDetail) && errDetail.length > 0) {
errorMessage = errDetail[0].msg
}
showToast("Error", errorMessage, "error")
Comment thread
alejsdev marked this conversation as resolved.
Outdated
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["users"] })
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/components/Admin/EditUser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,11 @@ const EditUser = ({ user, isOpen, onClose }: EditUserProps) => {
},
onError: (err: ApiError) => {
const errDetail = (err.body as any)?.detail
showToast("Something went wrong.", `${errDetail}`, "error")
let errorMessage = "Something went wrong."
if (Array.isArray(errDetail) && errDetail.length > 0) {
errorMessage = errDetail[0].msg
}
showToast("Error", errorMessage, "error")
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["users"] })
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/components/Items/AddItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@ const AddItem = ({ isOpen, onClose }: AddItemProps) => {
},
onError: (err: ApiError) => {
const errDetail = (err.body as any)?.detail
showToast("Something went wrong.", `${errDetail}`, "error")
let errorMessage = "Something went wrong."
if (Array.isArray(errDetail) && errDetail.length > 0) {
errorMessage = errDetail[0].msg
}
showToast("Error", errorMessage, "error")
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["items"] })
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/components/Items/EditItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,11 @@ const EditItem = ({ item, isOpen, onClose }: EditItemProps) => {
},
onError: (err: ApiError) => {
const errDetail = (err.body as any)?.detail
showToast("Something went wrong.", `${errDetail}`, "error")
let errorMessage = "Something went wrong."
if (Array.isArray(errDetail) && errDetail.length > 0) {
errorMessage = errDetail[0].msg
}
showToast("Error", errorMessage, "error")
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["items"] })
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/UserSettings/ChangePassword.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const ChangePassword = () => {
},
onError: (err: ApiError) => {
const errDetail = (err.body as any)?.detail
showToast("Something went wrong.", `${errDetail}`, "error")
showToast("Something went wrong.", errDetail, "error")
Comment thread
alejsdev marked this conversation as resolved.
Outdated
},
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const DeleteConfirmation = ({ isOpen, onClose }: DeleteProps) => {
},
onError: (err: ApiError) => {
const errDetail = (err.body as any)?.detail
showToast("Something went wrong.", `${errDetail}`, "error")
showToast("Something went wrong.", errDetail, "error")
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["currentUser"] })
Expand Down
14 changes: 9 additions & 5 deletions frontend/src/components/UserSettings/UserInformation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,14 @@ const UserInformation = () => {
},
onError: (err: ApiError) => {
const errDetail = (err.body as any)?.detail
showToast("Something went wrong.", `${errDetail}`, "error")
let errorMessage = "Something went wrong."
if (Array.isArray(errDetail) && errDetail.length > 0) {
errorMessage = errDetail[0].msg
}
showToast("Error", errorMessage, "error")
},
onSettled: () => {
// TODO: can we do just one call now?
queryClient.invalidateQueries({ queryKey: ["users"] })
queryClient.invalidateQueries({ queryKey: ["currentUser"] })
queryClient.invalidateQueries()
Comment thread
alejsdev marked this conversation as resolved.
},
})

Expand Down Expand Up @@ -104,6 +106,8 @@ const UserInformation = () => {
size="md"
py={2}
color={!currentUser?.full_name ? "ui.dim" : "inherit"}
isTruncated
maxWidth="250px"
>
{currentUser?.full_name || "N/A"}
</Text>
Expand All @@ -125,7 +129,7 @@ const UserInformation = () => {
w="auto"
/>
) : (
<Text size="md" py={2}>
<Text size="md" py={2} isTruncated maxWidth="250px">
{currentUser?.email}
</Text>
)}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/hooks/useAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ const useAuth = () => {
errDetail = err.message
}

showToast("Something went wrong.", `${errDetail}`, "error")
showToast("Something went wrong.", errDetail, "error")
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["users"] })
Expand Down
190 changes: 122 additions & 68 deletions frontend/src/routes/_layout/admin.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
Badge,
Box,
Button,
Container,
Flex,
Heading,
Expand All @@ -13,90 +14,65 @@ import {
Thead,
Tr,
} from "@chakra-ui/react"
import { useQueryClient, useSuspenseQuery } from "@tanstack/react-query"
import { createFileRoute } from "@tanstack/react-router"
import { useQuery, useQueryClient } from "@tanstack/react-query"
import { createFileRoute, useNavigate } from "@tanstack/react-router"
import { useEffect } from "react"
import { z } from "zod"

import { Suspense } from "react"
import { type UserPublic, UsersService } from "../../client"
import AddUser from "../../components/Admin/AddUser"
import ActionsMenu from "../../components/Common/ActionsMenu"
import Navbar from "../../components/Common/Navbar"

const usersSearchSchema = z.object({
page: z.number().catch(1),
})

export const Route = createFileRoute("/_layout/admin")({
component: Admin,
validateSearch: (search) => usersSearchSchema.parse(search),
})

const MembersTableBody = () => {
const PER_PAGE = 5

function getUsersQueryOptions({ page }: { page: number }) {
return {
queryFn: () =>
UsersService.readUsers({ skip: (page - 1) * PER_PAGE, limit: PER_PAGE }),
queryKey: ["users", { page }],
}
}

function UsersTable() {
const queryClient = useQueryClient()
const currentUser = queryClient.getQueryData<UserPublic>(["currentUser"])
const { page } = Route.useSearch()
const navigate = useNavigate({ from: Route.fullPath })
const setPage = (page: number) =>
navigate({ search: (prev) => ({ ...prev, page }) })

const { data: users } = useSuspenseQuery({
queryKey: ["users"],
queryFn: () => UsersService.readUsers({}),
const {
data: users,
isPending,
isPlaceholderData,
} = useQuery({
...getUsersQueryOptions({ page }),
placeholderData: (prevData) => prevData,
})

return (
<Tbody>
{users.data.map((user) => (
<Tr key={user.id}>
<Td color={!user.full_name ? "ui.dim" : "inherit"}>
{user.full_name || "N/A"}
{currentUser?.id === user.id && (
<Badge ml="1" colorScheme="teal">
You
</Badge>
)}
</Td>
<Td>{user.email}</Td>
<Td>{user.is_superuser ? "Superuser" : "User"}</Td>
<Td>
<Flex gap={2}>
<Box
w="2"
h="2"
borderRadius="50%"
bg={user.is_active ? "ui.success" : "ui.danger"}
alignSelf="center"
/>
{user.is_active ? "Active" : "Inactive"}
</Flex>
</Td>
<Td>
<ActionsMenu
type="User"
value={user}
disabled={currentUser?.id === user.id ? true : false}
/>
</Td>
</Tr>
))}
</Tbody>
)
}
const hasNextPage = !isPlaceholderData && users?.data.length === PER_PAGE
const hasPreviousPage = page > 1

const MembersBodySkeleton = () => {
return (
<Tbody>
<Tr>
{new Array(5).fill(null).map((_, index) => (
<Td key={index}>
<SkeletonText noOfLines={1} paddingBlock="16px" />
</Td>
))}
</Tr>
</Tbody>
)
}
useEffect(() => {
if (hasNextPage) {
queryClient.prefetchQuery(getUsersQueryOptions({ page: page + 1 }))
}
}, [page, queryClient, hasNextPage])

function Admin() {
return (
<Container maxW="full">
<Heading size="lg" textAlign={{ base: "center", md: "left" }} pt={12}>
User Management
</Heading>
<Navbar type={"User"} addModalAs={AddUser} />
<>
<TableContainer>
<Table fontSize="md" size={{ base: "sm", md: "md" }}>
<Table size={{ base: "sm", md: "md" }}>
<Thead>
<Tr>
<Th width="20%">Full name</Th>
Expand All @@ -106,11 +82,89 @@ function Admin() {
<Th width="10%">Actions</Th>
</Tr>
</Thead>
<Suspense fallback={<MembersBodySkeleton />}>
<MembersTableBody />
</Suspense>
{isPending ? (
<Tbody>
<Tr>
{new Array(4).fill(null).map((_, index) => (
<Td key={index}>
<SkeletonText noOfLines={1} paddingBlock="16px" />
</Td>
))}
</Tr>
</Tbody>
) : (
<Tbody>
{users?.data.map((user) => (
<Tr key={user.id}>
<Td
color={!user.full_name ? "ui.dim" : "inherit"}
isTruncated
maxWidth="150px"
>
{user.full_name || "N/A"}
{currentUser?.id === user.id && (
<Badge ml="1" colorScheme="teal">
You
</Badge>
)}
</Td>
<Td isTruncated maxWidth="150px">
{user.email}
</Td>
<Td>{user.is_superuser ? "Superuser" : "User"}</Td>
<Td>
<Flex gap={2}>
<Box
w="2"
h="2"
borderRadius="50%"
bg={user.is_active ? "ui.success" : "ui.danger"}
alignSelf="center"
/>
{user.is_active ? "Active" : "Inactive"}
</Flex>
</Td>
<Td>
<ActionsMenu
type="User"
value={user}
disabled={currentUser?.id === user.id ? true : false}
/>
</Td>
</Tr>
))}
</Tbody>
)}
</Table>
</TableContainer>
<Flex
gap={4}
alignItems="center"
mt={4}
direction="row"
justifyContent="flex-end"
>
<Button onClick={() => setPage(page - 1)} isDisabled={!hasPreviousPage}>
Previous
</Button>
<span>Page {page}</span>
<Button isDisabled={!hasNextPage} onClick={() => setPage(page + 1)}>
Next
</Button>
</Flex>
</>
)
}

function Admin() {
return (
<Container maxW="full">
<Heading size="lg" textAlign={{ base: "center", md: "left" }} pt={12}>
Users Management
</Heading>

<Navbar type={"User"} addModalAs={AddUser} />
<UsersTable />
</Container>
)
}
Loading