Skip to content

Commit

Permalink
feat(ui): implement pagination of members table (#1326)
Browse files Browse the repository at this point in the history
* feat(ui): provider pagination of members table

* fix(ui): default pagesize

* [autofix.ci] apply automated fixes

* fix(ui): use shadcn pagination

* [autofix.ci] apply automated fixes

* fix(ui): handle the data being returned

* fix(ui): refactoring operation colum style

* fix(ui): invitation table pagination

* [autofix.ci] apply automated fixes

* feat(ui): integrate @urql/exchange-graphcache

* [autofix.ci] apply automated fixes

* fix(ui): format

* Apply suggestions from code review

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Meng Zhang <[email protected]>
  • Loading branch information
3 people authored Feb 2, 2024
1 parent 2f14062 commit f0c67f9
Show file tree
Hide file tree
Showing 8 changed files with 411 additions and 73 deletions.
158 changes: 139 additions & 19 deletions ee/tabby-ui/app/(dashboard)/team/components/invitation-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,24 @@

import React, { useEffect, useState } from 'react'
import moment from 'moment'
import { useQuery } from 'urql'
import { toast } from 'sonner'
import { useClient, useQuery } from 'urql'

import { graphql } from '@/lib/gql/generates'
import { QueryVariables, useMutation } from '@/lib/tabby/gql'
import {
InvitationEdge,
ListInvitationsQueryVariables
} from '@/lib/gql/generates/graphql'
import { useMutation } from '@/lib/tabby/gql'
import { Button } from '@/components/ui/button'
import { IconTrash } from '@/components/ui/icons'
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationNext,
PaginationPrevious
} from '@/components/ui/pagination'
import {
Table,
TableBody,
Expand All @@ -20,7 +32,7 @@ import { CopyButton } from '@/components/copy-button'

import CreateInvitationForm from './create-invitation-form'

const listInvitations = graphql(/* GraphQL */ `
export const listInvitations = graphql(/* GraphQL */ `
query ListInvitations(
$after: String
$before: String
Expand Down Expand Up @@ -58,29 +70,118 @@ const deleteInvitationMutation = graphql(/* GraphQL */ `
}
`)

const PAGE_SIZE = 20
export default function InvitationTable() {
const [queryVariables, setQueryVariables] =
React.useState<QueryVariables<typeof listInvitations>>()
const [{ data }, reexecuteQuery] = useQuery({
const client = useClient()
const [{ data, fetching }] = useQuery({
query: listInvitations,
variables: queryVariables
variables: { first: PAGE_SIZE }
})
const invitations = data?.invitationsNext?.edges
// if a new invitation was created, fetching all records and navigating to the last page
const [fetchingLastPage, setFetchingLastPage] = React.useState(false)

const [currentPage, setCurrentPage] = React.useState(1)
const edges = data?.invitationsNext?.edges
const pageInfo = data?.invitationsNext?.pageInfo
const pageNum = Math.ceil((edges?.length || 0) / PAGE_SIZE)

const currentPageInvits = React.useMemo(() => {
return edges?.slice?.(
(currentPage - 1) * PAGE_SIZE,
currentPage * PAGE_SIZE
)
}, [currentPage, edges])

const hasNextPage = pageInfo?.hasNextPage || currentPage < pageNum
const hasPrevPage = currentPage > 1

const fetchInvitations = (variables: ListInvitationsQueryVariables) => {
return client.query(listInvitations, variables).toPromise()
}

const fetchInvitationsSequentially = async (
cursor?: string
): Promise<number> => {
const res = await fetchInvitations({ first: PAGE_SIZE, after: cursor })
let count = res?.data?.invitationsNext?.edges?.length || 0
const _pageInfo = res?.data?.invitationsNext?.pageInfo
if (_pageInfo?.hasNextPage && _pageInfo?.endCursor) {
// cacheExchange will merge the edges
count = await fetchInvitationsSequentially(_pageInfo.endCursor)
}
return count
}

const fetchAllRecords = async () => {
try {
setFetchingLastPage(true)
const count = fetchInvitationsSequentially(
pageInfo?.endCursor ?? undefined
)
return count
} catch (e) {
return 0
} finally {
setFetchingLastPage(false)
}
}

const [origin, setOrigin] = useState('')
useEffect(() => {
setOrigin(new URL(window.location.href).origin)
}, [])

const deleteInvitation = useMutation(deleteInvitationMutation, {
onCompleted() {
reexecuteQuery()
const deleteInvitation = useMutation(deleteInvitationMutation)

const handleInvitationCreated = async () => {
toast.success('Invitation created')
fetchAllRecords().then(count => {
setCurrentPage(getPageNumber(count))
})
}

const handleNavToPrevPage = () => {
if (currentPage <= 1) return
if (fetchingLastPage || fetching) return
setCurrentPage(p => p - 1)
}

const handleFetchNextPage = () => {
if (!hasNextPage) return
if (fetchingLastPage || fetching) return

fetchInvitations({ first: PAGE_SIZE, after: pageInfo?.endCursor }).then(
data => {
if (data?.data?.invitationsNext?.edges?.length) {
setCurrentPage(p => p + 1)
}
}
)
}

const handleDeleteInvatation = (node: InvitationEdge['node']) => {
deleteInvitation({ id: node.id }).then(res => {
if (res?.error) {
toast.error(res.error.message)
return
}
if (res?.data?.deleteInvitationNext) {
toast.success(`${node.email} deleted`)
}
})
}

React.useEffect(() => {
if (pageNum < currentPage && currentPage > 1) {
setCurrentPage(pageNum)
}
})
}, [pageNum, currentPage])

return (
<div>
<Table className="border-b">
{!!invitations?.length && (
<CreateInvitationForm onCreated={handleInvitationCreated} />
<Table className="mt-4 border-b">
{!!currentPageInvits?.length && (
<TableHeader>
<TableRow>
<TableHead className="w-[25%]">Invitee</TableHead>
Expand All @@ -90,7 +191,7 @@ export default function InvitationTable() {
</TableHeader>
)}
<TableBody>
{invitations?.map(x => {
{currentPageInvits?.map(x => {
const link = `${origin}/auth/signup?invitationCode=${x.node.code}`
return (
<TableRow key={x.node.id}>
Expand All @@ -102,7 +203,7 @@ export default function InvitationTable() {
<Button
size="icon"
variant="hover-destructive"
onClick={() => deleteInvitation(x.node)}
onClick={() => handleDeleteInvatation(x.node)}
>
<IconTrash />
</Button>
Expand All @@ -113,9 +214,28 @@ export default function InvitationTable() {
})}
</TableBody>
</Table>
<div className="mt-4 flex items-start justify-between">
<CreateInvitationForm onCreated={() => reexecuteQuery()} />
</div>
{(hasNextPage || hasPrevPage) && (
<Pagination className="my-4">
<PaginationContent>
<PaginationItem>
<PaginationPrevious
disabled={!hasPrevPage}
onClick={handleNavToPrevPage}
/>
</PaginationItem>
<PaginationItem>
<PaginationNext
disabled={!hasNextPage}
onClick={handleFetchNextPage}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
)}
</div>
)
}

function getPageNumber(count?: number) {
return Math.ceil((count || 0) / PAGE_SIZE)
}
83 changes: 69 additions & 14 deletions ee/tabby-ui/app/(dashboard)/team/components/user-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { toast } from 'sonner'
import { useQuery } from 'urql'

import { graphql } from '@/lib/gql/generates'
import type { ListUsersNextQuery } from '@/lib/gql/generates/graphql'
import { QueryVariables, useMutation } from '@/lib/tabby/gql'
import type { ArrayElementType } from '@/lib/types'
import { Badge } from '@/components/ui/badge'
Expand All @@ -17,6 +18,13 @@ import {
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { IconMore } from '@/components/ui/icons'
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationNext,
PaginationPrevious
} from '@/components/ui/pagination'
import {
Table,
TableBody,
Expand Down Expand Up @@ -60,19 +68,34 @@ const updateUserActiveMutation = graphql(/* GraphQL */ `
}
`)

const PAGE_SIZE = 20
export default function UsersTable() {
const [queryVariables, setQueryVariables] =
React.useState<QueryVariables<typeof listUsers>>()
const [{ data }, reexecuteQuery] = useQuery({
const [queryVariables, setQueryVariables] = React.useState<
QueryVariables<typeof listUsers>
>({ first: PAGE_SIZE })
const [{ data, error }, reexecuteQuery] = useQuery({
query: listUsers,
variables: queryVariables
})
const users = data?.usersNext?.edges
const [users, setUsers] = React.useState<ListUsersNextQuery['usersNext']>()

React.useEffect(() => {
const _users = data?.usersNext
if (_users?.edges?.length) {
setUsers(_users)
}
}, [data])

React.useEffect(() => {
if (error?.message) {
toast.error(error.message)
}
}, [error])

const updateUserActive = useMutation(updateUserActiveMutation)

const onUpdateUserActive = (
node: ArrayElementType<typeof users>['node'],
node: ArrayElementType<ListUsersNextQuery['usersNext']['edges']>['node'],
active: boolean
) => {
updateUserActive({ id: node.id, active }).then(response => {
Expand All @@ -89,8 +112,10 @@ export default function UsersTable() {
})
}

const pageInfo = users?.pageInfo

return (
!!users?.length && (
!!users?.edges?.length && (
<>
<Table className="border-b">
<TableHeader>
Expand All @@ -103,7 +128,7 @@ export default function UsersTable() {
</TableRow>
</TableHeader>
<TableBody>
{users.map(x => (
{users.edges.map(x => (
<TableRow key={x.node.id}>
<TableCell>{x.node.email}</TableCell>
<TableCell>{moment.utc(x.node.createdAt).fromNow()}</TableCell>
Expand All @@ -121,14 +146,16 @@ export default function UsersTable() {
<Badge variant="secondary">MEMBER</Badge>
)}
</TableCell>
<TableCell className="flex justify-end">
<TableCell className="text-end">
<DropdownMenu>
<DropdownMenuTrigger>
{x.node.isAdmin ? null : (
<Button size="icon" variant="ghost">
<IconMore />
</Button>
)}
<DropdownMenuTrigger asChild>
<div className="h-8">
{x.node.isAdmin ? null : (
<Button size="icon" variant="ghost">
<IconMore />
</Button>
)}
</div>
</DropdownMenuTrigger>
<DropdownMenuContent collisionPadding={{ right: 16 }}>
{x.node.active && (
Expand All @@ -154,6 +181,34 @@ export default function UsersTable() {
))}
</TableBody>
</Table>
{(pageInfo?.hasNextPage || pageInfo?.hasPreviousPage) && (
<Pagination className="my-4">
<PaginationContent>
<PaginationItem>
<PaginationPrevious
disabled={!pageInfo?.hasPreviousPage}
onClick={e =>
setQueryVariables({
last: PAGE_SIZE,
before: pageInfo?.startCursor
})
}
/>
</PaginationItem>
<PaginationItem>
<PaginationNext
disabled={!pageInfo?.hasNextPage}
onClick={e =>
setQueryVariables({
first: PAGE_SIZE,
after: pageInfo?.endCursor
})
}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
)}
</>
)
)
Expand Down
33 changes: 0 additions & 33 deletions ee/tabby-ui/components/simple-pagination.tsx

This file was deleted.

Loading

0 comments on commit f0c67f9

Please sign in to comment.