mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-13 17:05:47 +01:00
✨ Implement paging in manage users, implement search
This commit is contained in:
@@ -19,6 +19,7 @@ import {
|
|||||||
IconMail,
|
IconMail,
|
||||||
IconMailCheck,
|
IconMailCheck,
|
||||||
IconUser,
|
IconUser,
|
||||||
|
IconUserPlus,
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
@@ -59,7 +60,7 @@ const CreateNewUserPage = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const context = api.useContext();
|
const context = api.useContext();
|
||||||
const { mutateAsync, isSuccess, isLoading } = api.user.createUser.useMutation({
|
const { mutateAsync, isLoading } = api.user.createUser.useMutation({
|
||||||
onSettled: () => {
|
onSettled: () => {
|
||||||
void context.user.getAll.invalidate();
|
void context.user.getAll.invalidate();
|
||||||
},
|
},
|
||||||
@@ -173,10 +174,6 @@ const CreateNewUserPage = () => {
|
|||||||
<Flex justify="end" wrap="nowrap">
|
<Flex justify="end" wrap="nowrap">
|
||||||
<Button
|
<Button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
if (isSuccess) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
username: form.values.account.username,
|
username: form.values.account.username,
|
||||||
password: form.values.security.password,
|
password: form.values.security.password,
|
||||||
@@ -198,14 +195,26 @@ const CreateNewUserPage = () => {
|
|||||||
User has been created in the database. They can now log in.
|
User has been created in the database. They can now log in.
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
<Button
|
<Group>
|
||||||
component={Link}
|
<Button
|
||||||
leftIcon={<IconArrowLeft size="1rem" />}
|
onClick={() => {
|
||||||
variant="default"
|
form.reset();
|
||||||
href="/manage/users"
|
setActive(0);
|
||||||
>
|
}}
|
||||||
Go back to users
|
leftIcon={<IconUserPlus size="1rem" />}
|
||||||
</Button>
|
variant="default"
|
||||||
|
>
|
||||||
|
Create another
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
component={Link}
|
||||||
|
leftIcon={<IconArrowLeft size="1rem" />}
|
||||||
|
variant="default"
|
||||||
|
href="/manage/users"
|
||||||
|
>
|
||||||
|
Go back to users
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
</Stepper.Completed>
|
</Stepper.Completed>
|
||||||
</Stepper>
|
</Stepper>
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
ActionIcon,
|
ActionIcon,
|
||||||
Autocomplete,
|
Autocomplete,
|
||||||
Avatar,
|
Avatar,
|
||||||
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Flex,
|
Flex,
|
||||||
Group,
|
Group,
|
||||||
@@ -11,6 +12,7 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
Title,
|
Title,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
|
import { useDebouncedValue } from '@mantine/hooks';
|
||||||
import { openContextModal } from '@mantine/modals';
|
import { openContextModal } from '@mantine/modals';
|
||||||
import { IconPlus, IconTrash } from '@tabler/icons-react';
|
import { IconPlus, IconTrash } from '@tabler/icons-react';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
@@ -20,16 +22,13 @@ import { MainLayout } from '~/components/layout/admin/main-admin.layout';
|
|||||||
import { api } from '~/utils/api';
|
import { api } from '~/utils/api';
|
||||||
|
|
||||||
const ManageUsersPage = () => {
|
const ManageUsersPage = () => {
|
||||||
const { data, fetchNextPage, fetchPreviousPage } = api.user.getAll.useInfiniteQuery(
|
const [activePage, setActivePage] = useState(0);
|
||||||
{
|
const [nonDebouncedSearch, setNonDebouncedSearch] = useState<string | undefined>('');
|
||||||
limit: 10,
|
const [debouncedSearch] = useDebouncedValue<string | undefined>(nonDebouncedSearch, 200);
|
||||||
},
|
const { data } = api.user.getAll.useQuery({
|
||||||
{
|
page: activePage,
|
||||||
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
search: debouncedSearch,
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const [activePage, _] = useState(0);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
@@ -46,8 +45,13 @@ const ManageUsersPage = () => {
|
|||||||
<Flex columnGap={10} justify="end" mb="md">
|
<Flex columnGap={10} justify="end" mb="md">
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
placeholder="Filter"
|
placeholder="Filter"
|
||||||
data={['React', 'Angular', 'Svelte', 'Vue']}
|
data={
|
||||||
|
(data?.users.map((user) => user.name).filter((name) => name !== null) as string[]) ?? []
|
||||||
|
}
|
||||||
variant="filled"
|
variant="filled"
|
||||||
|
onChange={(value) => {
|
||||||
|
setNonDebouncedSearch(value);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
component={Link}
|
component={Link}
|
||||||
@@ -68,7 +72,7 @@ const ManageUsersPage = () => {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{data.pages[activePage].users.map((user, index) => (
|
{data.users.map((user, index) => (
|
||||||
<tr key={index}>
|
<tr key={index}>
|
||||||
<td>
|
<td>
|
||||||
<Group position="apart">
|
<Group position="apart">
|
||||||
@@ -98,13 +102,30 @@ const ManageUsersPage = () => {
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{debouncedSearch && debouncedSearch.length > 0 && (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={1}>
|
||||||
|
<Box p={15}>
|
||||||
|
<Text>Your search does not match any entries. Please adjust your filter.</Text>
|
||||||
|
</Box>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
</Table>
|
</Table>
|
||||||
<Pagination
|
<Pagination
|
||||||
total={data.pages.length}
|
total={data.countPages}
|
||||||
value={activePage + 1}
|
value={activePage + 1}
|
||||||
onNextPage={fetchNextPage}
|
onNextPage={() => {
|
||||||
onPreviousPage={fetchPreviousPage}
|
setActivePage((prev) => prev + 1);
|
||||||
|
}}
|
||||||
|
onPreviousPage={() => {
|
||||||
|
setActivePage((prev) => prev - 1);
|
||||||
|
}}
|
||||||
|
onChange={(targetPage) => {
|
||||||
|
setActivePage(targetPage - 1);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -161,23 +161,33 @@ export const userRouter = createTRPCRouter({
|
|||||||
getAll: publicProcedure
|
getAll: publicProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
limit: z.number().min(1).max(100).nullish(),
|
limit: z.number().min(1).max(100).default(10),
|
||||||
cursor: z.string().nullish(),
|
page: z.number().min(0),
|
||||||
|
search: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.transform((value) => (value === '' ? undefined : value)),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const limit = input.limit ?? 50;
|
const limit = input.limit;
|
||||||
const cursor = input.cursor;
|
|
||||||
const users = await ctx.prisma.user.findMany({
|
const users = await ctx.prisma.user.findMany({
|
||||||
take: limit + 1, // get an extra item at the end which we'll use as next cursor
|
take: limit + 1,
|
||||||
cursor: cursor ? { id: cursor } : undefined,
|
skip: limit * input.page,
|
||||||
|
where: {
|
||||||
|
name: {
|
||||||
|
contains: input.search,
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
let nextCursor: typeof cursor | undefined = undefined;
|
const countUsers = await ctx.prisma.user.count({
|
||||||
if (users.length > limit) {
|
where: {
|
||||||
const nextItem = users.pop();
|
name: {
|
||||||
nextCursor = nextItem!.id;
|
contains: input.search,
|
||||||
}
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
users: users.map((user) => ({
|
users: users.map((user) => ({
|
||||||
@@ -186,7 +196,7 @@ export const userRouter = createTRPCRouter({
|
|||||||
email: user.email,
|
email: user.email,
|
||||||
emailVerified: user.emailVerified,
|
emailVerified: user.emailVerified,
|
||||||
})),
|
})),
|
||||||
nextCursor,
|
countPages: Math.ceil(countUsers / limit),
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
createUser: publicProcedure.input(createNewUserSchema).mutation(async ({ ctx, input }) => {
|
createUser: publicProcedure.input(createNewUserSchema).mutation(async ({ ctx, input }) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user