Skip to content

Commit

Permalink
feat(ui, webserver): allow deactivating a user (TabbyML#1319)
Browse files Browse the repository at this point in the history
* feat(ui): users pagination

* feat(ui): simple pagination

* feat(ui): pagination

* feat(ui): updateUserActive

* [autofix.ci] apply automated fixes

* fix(ui): display status badge

* fix(ui): add toast

* fix(ui): add error toast

* fix(ui): format

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
  • Loading branch information
liangfung and autofix-ci[bot] authored Jan 29, 2024
1 parent cb50aea commit 6399606
Show file tree
Hide file tree
Showing 9 changed files with 299 additions and 67 deletions.
94 changes: 60 additions & 34 deletions ee/tabby-ui/app/(dashboard)/team/components/invitation-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import moment from 'moment'
import { useQuery } from 'urql'

import { graphql } from '@/lib/gql/generates'
import { useMutation } from '@/lib/tabby/gql'
import { QueryVariables, useMutation } from '@/lib/tabby/gql'
import { Button } from '@/components/ui/button'
import { IconTrash } from '@/components/ui/icons'
import {
Expand All @@ -21,25 +21,51 @@ import { CopyButton } from '@/components/copy-button'
import CreateInvitationForm from './create-invitation-form'

const listInvitations = graphql(/* GraphQL */ `
query ListInvitations {
invitations {
id
email
code
createdAt
query ListInvitations(
$after: String
$before: String
$first: Int
$last: Int
) {
invitationsNext(
after: $after
before: $before
first: $first
last: $last
) {
edges {
node {
id
email
code
createdAt
}
cursor
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
`)

const deleteInvitationMutation = graphql(/* GraphQL */ `
mutation DeleteInvitation($id: Int!) {
deleteInvitation(id: $id)
mutation DeleteInvitation($id: ID!) {
deleteInvitationNext(id: $id)
}
`)

export default function InvitationTable() {
const [{ data }, reexecuteQuery] = useQuery({ query: listInvitations })
const invitations = data?.invitations
const [queryVariables, setQueryVariables] =
React.useState<QueryVariables<typeof listInvitations>>()
const [{ data }, reexecuteQuery] = useQuery({
query: listInvitations,
variables: queryVariables
})
const invitations = data?.invitationsNext?.edges
const [origin, setOrigin] = useState('')
useEffect(() => {
setOrigin(new URL(window.location.href).origin)
Expand All @@ -52,9 +78,9 @@ export default function InvitationTable() {
})

return (
invitations && (
<Table>
{invitations.length > 0 && (
<div>
<Table className="border-b">
{!!invitations?.length && (
<TableHeader>
<TableRow>
<TableHead className="w-[25%]">Invitee</TableHead>
Expand All @@ -64,32 +90,32 @@ export default function InvitationTable() {
</TableHeader>
)}
<TableBody>
{invitations.map((x, i) => {
const link = `${origin}/auth/signup?invitationCode=${x.code}`
{invitations?.map(x => {
const link = `${origin}/auth/signup?invitationCode=${x.node.code}`
return (
<TableRow key={i}>
<TableCell>{x.email}</TableCell>
<TableCell>{moment.utc(x.createdAt).fromNow()}</TableCell>
<TableCell className="text-center">
<CopyButton value={link} />
<Button
size="icon"
variant="hover-destructive"
onClick={() => deleteInvitation(x)}
>
<IconTrash />
</Button>
<TableRow key={x.node.id}>
<TableCell>{x.node.email}</TableCell>
<TableCell>{moment.utc(x.node.createdAt).fromNow()}</TableCell>
<TableCell className="flex justify-end">
<div className="flex gap-1">
<CopyButton value={link} />
<Button
size="icon"
variant="hover-destructive"
onClick={() => deleteInvitation(x.node)}
>
<IconTrash />
</Button>
</div>
</TableCell>
</TableRow>
)
})}
<TableRow>
<TableCell className="p-2">
<CreateInvitationForm onCreated={() => reexecuteQuery()} />
</TableCell>
</TableRow>
</TableBody>
</Table>
)
<div className="mt-4 flex items-start justify-between">
<CreateInvitationForm onCreated={() => reexecuteQuery()} />
</div>
</div>
)
}
163 changes: 132 additions & 31 deletions ee/tabby-ui/app/(dashboard)/team/components/user-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,20 @@

import React from 'react'
import moment from 'moment'
import { toast } from 'sonner'
import { useQuery } from 'urql'

import { graphql } from '@/lib/gql/generates'
import { QueryVariables, useMutation } from '@/lib/tabby/gql'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { IconMore } from '@/components/ui/icons'
import {
Table,
TableBody,
Expand All @@ -16,45 +26,136 @@ import {
} from '@/components/ui/table'

const listUsers = graphql(/* GraphQL */ `
query ListUsers {
users {
email
isAdmin
createdAt
query ListUsersNext(
$after: String
$before: String
$first: Int
$last: Int
) {
usersNext(after: $after, before: $before, first: $first, last: $last) {
edges {
node {
id
email
isAdmin
createdAt
active
}
cursor
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
`)

const updateUserActiveMutation = graphql(/* GraphQL */ `
mutation UpdateUserActive($id: ID!, $active: Boolean!) {
updateUserActive(id: $id, active: $active)
}
`)

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

const updateUserActive = useMutation(updateUserActiveMutation, {
onCompleted(values) {
if (values?.updateUserActive) {
toast.success('success')
reexecuteQuery()
}
},
onError: e => {
toast.error(e.message || 'update failed')
}
})

return (
users && (
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[25%]">Email</TableHead>
<TableHead className="w-[45%]">Joined</TableHead>
<TableHead className="text-center">Level</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{users.map((x, i) => (
<TableRow key={i}>
<TableCell>{x.email}</TableCell>
<TableCell>{moment.utc(x.createdAt).fromNow()}</TableCell>
<TableCell className="text-center">
{x.isAdmin ? (
<Badge>ADMIN</Badge>
) : (
<Badge variant="secondary">MEMBER</Badge>
)}
</TableCell>
!!users?.length && (
<>
<Table className="border-b">
<TableHeader>
<TableRow>
<TableHead className="w-[25%]">Email</TableHead>
<TableHead className="w-[35%]">Joined</TableHead>
<TableHead className="w-[15%] text-center">Status</TableHead>
<TableHead className="w-[15%] text-center">Level</TableHead>
<TableHead className="w-[100px]"></TableHead>
</TableRow>
))}
</TableBody>
</Table>
</TableHeader>
<TableBody>
{users.map(x => (
<TableRow key={x.node.id}>
<TableCell>{x.node.email}</TableCell>
<TableCell>{moment.utc(x.node.createdAt).fromNow()}</TableCell>
<TableCell className="text-center">
{x.node.active ? (
<Badge variant="successful">Active</Badge>
) : (
<Badge variant="secondary">Inactive</Badge>
)}
</TableCell>
<TableCell className="text-center">
{x.node.isAdmin ? (
<Badge>ADMIN</Badge>
) : (
<Badge variant="secondary">MEMBER</Badge>
)}
</TableCell>
<TableCell className="flex justify-end">
<DropdownMenu>
<DropdownMenuTrigger>
{x.node.isAdmin ? null : (
<Button size="icon" variant="ghost">
<IconMore />
</Button>
)}
</DropdownMenuTrigger>
<DropdownMenuContent collisionPadding={{ right: 16 }}>
{x.node.active && (
<DropdownMenuItem
onSelect={() =>
updateUserActive({
id: x.node.id,
active: false
})
}
className="cursor-pointer"
>
<span className="ml-2">Deactivate</span>
</DropdownMenuItem>
)}
{!x.node.active && (
<DropdownMenuItem
onSelect={() =>
updateUserActive({
id: x.node.id,
active: true
})
}
className="cursor-pointer"
>
<span className="ml-2">Activate</span>
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</>
)
)
}
6 changes: 6 additions & 0 deletions ee/tabby-ui/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;

--successful: 140.62 84.21% 92.55%;
--successful-foreground: 142.78 64.23% 24.12%;

--ring: 38 31% 25%;

--radius: 0.5rem;
Expand Down Expand Up @@ -66,6 +69,9 @@
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 39 3.2% 99.35%;

--successful: 143.81 61.17% 20.2%;
--successful-foreground: 141.71 76.64% 73.14%;

--ring: 39 32% 87%;

--selection: 221, 13%, 28%
Expand Down
33 changes: 33 additions & 0 deletions ee/tabby-ui/components/simple-pagination.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React from 'react'

import { cn } from '@/lib/utils'

import { Button } from './ui/button'
import { IconChevronRight } from './ui/icons'

interface SimplePagination extends React.HTMLAttributes<HTMLDivElement> {
hasPreviousPage: boolean | undefined
hasNextPage: boolean | undefined
onNext: () => void
onPrev: () => void
}
const SimplePagination: React.FC<SimplePagination> = ({
className,
hasPreviousPage,
hasNextPage,
onNext,
onPrev
}) => {
return (
<div className={cn('flex items-center gap-2', className)}>
<Button disabled={!hasPreviousPage} onClick={onPrev}>
<IconChevronRight className="rotate-180" />{' '}
</Button>
<Button disabled={!hasNextPage} onClick={onNext}>
<IconChevronRight />{' '}
</Button>
</div>
)
}

export { SimplePagination }
2 changes: 2 additions & 0 deletions ee/tabby-ui/components/ui/badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ const badgeVariants = cva(
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
destructive:
'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
successful:
'border-transparent bg-successful text-successful-foreground hover:bg-successful/80',
outline: 'text-foreground'
}
},
Expand Down
Loading

0 comments on commit 6399606

Please sign in to comment.