@@ -115,47 +53,24 @@ function MainPanel() {
-
-
-
Workers
-
- {!!registrationTokenRes?.registrationToken && (
-
- Registeration token:{' '}
-
- {registrationTokenRes.registrationToken}
-
-
-
- )}
-
-
- {!!workers?.[WorkerKind.Completion] && (
- <>
- {workers[WorkerKind.Completion].map((worker, i) => {
- return
- })}
- >
- )}
- {!!workers?.[WorkerKind.Chat] && (
- <>
- {workers[WorkerKind.Chat].map((worker, i) => {
- return
- })}
- >
- )}
-
-
+
+ Token:
+
+ {data.me.authToken}
+
+
+
+ Use credentials above for IDE extensions / plugins authentication, see{' '}
+
+ configurations
+ {' '}
+ for details
+
)
}
diff --git a/ee/tabby-ui/app/(dashboard)/team/components/create-invitation-form.tsx b/ee/tabby-ui/app/(dashboard)/team/components/create-invitation-form.tsx
new file mode 100644
index 0000000..d91607b
--- /dev/null
+++ b/ee/tabby-ui/app/(dashboard)/team/components/create-invitation-form.tsx
@@ -0,0 +1,81 @@
+'use client'
+
+import * as React from 'react'
+import { zodResolver } from '@hookform/resolvers/zod'
+import { useForm } from 'react-hook-form'
+import * as z from 'zod'
+
+import { graphql } from '@/lib/gql/generates'
+import { useGraphQLForm } from '@/lib/tabby/gql'
+import { Button } from '@/components/ui/button'
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormMessage
+} from '@/components/ui/form'
+import { Input } from '@/components/ui/input'
+
+const createInvitation = graphql(/* GraphQL */ `
+ mutation CreateInvitation($email: String!) {
+ createInvitation(email: $email)
+ }
+`)
+
+const formSchema = z.object({
+ email: z.string().email('Invalid email address')
+})
+
+export default function CreateInvitationForm({
+ onCreated
+}: {
+ onCreated: () => void
+}) {
+ const form = useForm
>({
+ resolver: zodResolver(formSchema)
+ })
+
+ const { isSubmitting } = form.formState
+ const { onSubmit } = useGraphQLForm(createInvitation, {
+ onSuccess: () => {
+ form.reset({ email: '' })
+ onCreated()
+ },
+ onError: (path, message) => form.setError(path as any, { message })
+ })
+
+ return (
+
+ )
+}
diff --git a/ee/tabby-ui/app/(dashboard)/team/components/invitation-table.tsx b/ee/tabby-ui/app/(dashboard)/team/components/invitation-table.tsx
new file mode 100644
index 0000000..037a60d
--- /dev/null
+++ b/ee/tabby-ui/app/(dashboard)/team/components/invitation-table.tsx
@@ -0,0 +1,95 @@
+'use client'
+
+import React, { useEffect, useState } from 'react'
+import moment from 'moment'
+
+import { graphql } from '@/lib/gql/generates'
+import { useAuthenticatedGraphQLQuery, useGraphQLForm } from '@/lib/tabby/gql'
+import { Button } from '@/components/ui/button'
+import { IconTrash } from '@/components/ui/icons'
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow
+} from '@/components/ui/table'
+import { CopyButton } from '@/components/copy-button'
+
+import CreateInvitationForm from './create-invitation-form'
+
+const listInvitations = graphql(/* GraphQL */ `
+ query ListInvitations {
+ invitations {
+ id
+ email
+ code
+ createdAt
+ }
+ }
+`)
+
+const deleteInvitationMutation = graphql(/* GraphQL */ `
+ mutation DeleteInvitation($id: Int!) {
+ deleteInvitation(id: $id)
+ }
+`)
+
+export default function InvitationTable() {
+ const { data, mutate } = useAuthenticatedGraphQLQuery(listInvitations)
+ const invitations = data?.invitations
+ const [origin, setOrigin] = useState('')
+ useEffect(() => {
+ setOrigin(new URL(window.location.href).origin)
+ }, [])
+
+ const { onSubmit: deleteInvitation } = useGraphQLForm(
+ deleteInvitationMutation,
+ {
+ onSuccess: () => mutate()
+ }
+ )
+
+ return (
+ invitations && (
+
+
+
+ Invitee
+ Created
+
+
+
+
+ {invitations.map((x, i) => {
+ const link = `${origin}/auth/signup?invitationCode=${x.code}`
+ return (
+
+
+ {x.email}
+
+ {moment.utc(x.createdAt).fromNow()}
+
+
+
+
+
+ )
+ })}
+
+
+ mutate()} />
+
+
+
+
+ )
+ )
+}
diff --git a/ee/tabby-ui/app/(dashboard)/team/components/team.tsx b/ee/tabby-ui/app/(dashboard)/team/components/team.tsx
new file mode 100644
index 0000000..7639703
--- /dev/null
+++ b/ee/tabby-ui/app/(dashboard)/team/components/team.tsx
@@ -0,0 +1,26 @@
+'use client'
+
+import { CardContent, CardHeader, CardTitle } from '@/components/ui/card'
+
+import InvitationTable from './invitation-table'
+
+export default function Team() {
+ return (
+
+
+
+ Invites
+
+
+
+
+
+
+
+ Users
+
+
+
+
+ )
+}
diff --git a/ee/tabby-ui/app/(dashboard)/team/page.tsx b/ee/tabby-ui/app/(dashboard)/team/page.tsx
new file mode 100644
index 0000000..88c8a17
--- /dev/null
+++ b/ee/tabby-ui/app/(dashboard)/team/page.tsx
@@ -0,0 +1,15 @@
+import { Metadata } from 'next'
+
+import Team from './components/team'
+
+export const metadata: Metadata = {
+ title: 'Team Management'
+}
+
+export default function IndexPage() {
+ return (
+
+
+
+ )
+}
diff --git a/ee/tabby-ui/app/(dashboard)/swagger/page.tsx b/ee/tabby-ui/app/api/page.tsx
similarity index 54%
rename from ee/tabby-ui/app/(dashboard)/swagger/page.tsx
rename to ee/tabby-ui/app/api/page.tsx
index f0e713f..531c36c 100644
--- a/ee/tabby-ui/app/(dashboard)/swagger/page.tsx
+++ b/ee/tabby-ui/app/api/page.tsx
@@ -1,5 +1,7 @@
import { Metadata } from 'next'
+import { Header } from '@/components/header'
+
export const metadata: Metadata = {
title: 'API'
}
@@ -7,5 +9,10 @@ export const metadata: Metadata = {
const serverUrl = process.env.NEXT_PUBLIC_TABBY_SERVER_URL || ''
export default function IndexPage() {
- return
+ return (
+ <>
+
+
+ >
+ )
}
diff --git a/ee/tabby-ui/components/header.tsx b/ee/tabby-ui/components/header.tsx
index a9c9974..901f241 100644
--- a/ee/tabby-ui/components/header.tsx
+++ b/ee/tabby-ui/components/header.tsx
@@ -21,7 +21,7 @@ export function Header() {
useAuthenticatedSession()
const { data } = useHealth()
- const workers = useWorkers(data)
+ const workers = useWorkers()
const isChatEnabled = has(workers, WorkerKind.Chat)
const version = data?.version?.git_describe
const { data: latestRelease } = useLatestRelease()
@@ -34,6 +34,9 @@ export function Header() {
Dashboard
+
+ API
+
{isChatEnabled && (
{
+ if (status !== 'authenticated') return
+
+ if (!localStorage.getItem(COMMUNITY_DIALOG_SHOWN_KEY)) {
+ setOpen(true)
+ localStorage.setItem(COMMUNITY_DIALOG_SHOWN_KEY, 'true')
+ }
+ }, [status])
+
+ return (
+
+ )
+}
diff --git a/ee/tabby-ui/components/ui/button.tsx b/ee/tabby-ui/components/ui/button.tsx
index 4dd6924..5c19ac6 100644
--- a/ee/tabby-ui/components/ui/button.tsx
+++ b/ee/tabby-ui/components/ui/button.tsx
@@ -13,6 +13,8 @@ const buttonVariants = cva(
'bg-primary text-primary-foreground shadow-md hover:bg-primary/90',
destructive:
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
+ 'hover-destructive':
+ 'shadow-none hover:bg-destructive/90 hover:text-destructive-foreground',
outline:
'border border-input hover:bg-accent hover:text-accent-foreground',
secondary:
diff --git a/ee/tabby-ui/components/ui/icons.tsx b/ee/tabby-ui/components/ui/icons.tsx
index eaad117..ea15ee7 100644
--- a/ee/tabby-ui/components/ui/icons.tsx
+++ b/ee/tabby-ui/components/ui/icons.tsx
@@ -615,6 +615,51 @@ function IconUnlock({ className, ...props }: React.ComponentProps<'svg'>) {
)
}
+function IconHome({ className, ...props }: React.ComponentProps<'svg'>) {
+ return (
+
+ )
+}
+
+function IconNetwork({ className, ...props }: React.ComponentProps<'svg'>) {
+ return (
+
+ )
+}
+
export {
IconEdit,
IconNextChat,
@@ -647,5 +692,7 @@ export {
IconNotice,
IconSymbolFunction,
IconLogout,
- IconUnlock
+ IconUnlock,
+ IconHome,
+ IconNetwork
}
diff --git a/ee/tabby-ui/components/ui/table.tsx b/ee/tabby-ui/components/ui/table.tsx
new file mode 100644
index 0000000..893974f
--- /dev/null
+++ b/ee/tabby-ui/components/ui/table.tsx
@@ -0,0 +1,117 @@
+import * as React from 'react'
+
+import { cn } from '@/lib/utils'
+
+const Table = React.forwardRef<
+ HTMLTableElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+Table.displayName = 'Table'
+
+const TableHeader = React.forwardRef<
+ HTMLTableSectionElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+TableHeader.displayName = 'TableHeader'
+
+const TableBody = React.forwardRef<
+ HTMLTableSectionElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+TableBody.displayName = 'TableBody'
+
+const TableFooter = React.forwardRef<
+ HTMLTableSectionElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+ tr]:last:border-b-0',
+ className
+ )}
+ {...props}
+ />
+))
+TableFooter.displayName = 'TableFooter'
+
+const TableRow = React.forwardRef<
+ HTMLTableRowElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+TableRow.displayName = 'TableRow'
+
+const TableHead = React.forwardRef<
+ HTMLTableCellElement,
+ React.ThHTMLAttributes
+>(({ className, ...props }, ref) => (
+ |
+))
+TableHead.displayName = 'TableHead'
+
+const TableCell = React.forwardRef<
+ HTMLTableCellElement,
+ React.TdHTMLAttributes
+>(({ className, ...props }, ref) => (
+ |
+))
+TableCell.displayName = 'TableCell'
+
+const TableCaption = React.forwardRef<
+ HTMLTableCaptionElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+TableCaption.displayName = 'TableCaption'
+
+export {
+ Table,
+ TableHeader,
+ TableBody,
+ TableFooter,
+ TableHead,
+ TableRow,
+ TableCell,
+ TableCaption
+}
diff --git a/ee/tabby-ui/lib/hooks/use-workers.ts b/ee/tabby-ui/lib/hooks/use-workers.ts
index a5d145a..982b16b 100644
--- a/ee/tabby-ui/lib/hooks/use-workers.ts
+++ b/ee/tabby-ui/lib/hooks/use-workers.ts
@@ -5,7 +5,7 @@ import { graphql } from '@/lib/gql/generates'
import { Worker, WorkerKind } from '@/lib/gql/generates/graphql'
import { useAuthenticatedGraphQLQuery } from '@/lib/tabby/gql'
-import type { HealthInfo } from './use-health'
+import { useHealth, type HealthInfo } from './use-health'
const modelNameMap: Record = {
[WorkerKind.Chat]: 'chat_model',
@@ -43,7 +43,8 @@ export const getAllWorkersDocument = graphql(/* GraphQL */ `
}
`)
-function useWorkers(healthInfo?: HealthInfo) {
+function useWorkers() {
+ const { data: healthInfo } = useHealth()
const { data } = useAuthenticatedGraphQLQuery(getAllWorkersDocument)
let workers = data?.workers
diff --git a/ee/tabby-ui/lib/tabby/gql.ts b/ee/tabby-ui/lib/tabby/gql.ts
index af02901..ae981ab 100644
--- a/ee/tabby-ui/lib/tabby/gql.ts
+++ b/ee/tabby-ui/lib/tabby/gql.ts
@@ -30,7 +30,7 @@ export function useGraphQLForm<
) {
const { data } = useSession()
const accessToken = data?.accessToken
- const onSubmit = async (variables: TVariables) => {
+ const onSubmit = async (variables?: TVariables) => {
let res
try {
res = await gqlClient.request({
@@ -43,6 +43,7 @@ export function useGraphQLForm<
: undefined
})
} catch (err) {
+ console.error('err', err)
const { errors = [] } = (err as any).response as GraphQLResponse
for (const error of errors) {
if (error.extensions && error.extensions['validation-errors']) {
diff --git a/ee/tabby-ui/package.json b/ee/tabby-ui/package.json
index 34513d6..5b768a6 100644
--- a/ee/tabby-ui/package.json
+++ b/ee/tabby-ui/package.json
@@ -43,6 +43,7 @@
"graphql-request": "^6.1.0",
"jwt-decode": "^4.0.0",
"lodash-es": "^4.17.21",
+ "moment": "^2.29.4",
"nanoid": "^4.0.2",
"next": "^13.4.7",
"next-themes": "^0.2.1",
diff --git a/ee/tabby-ui/yarn.lock b/ee/tabby-ui/yarn.lock
index 44aeaa7..e35b15c 100644
--- a/ee/tabby-ui/yarn.lock
+++ b/ee/tabby-ui/yarn.lock
@@ -5250,6 +5250,11 @@ minimist@^1.2.0, minimist@^1.2.6:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
+moment@^2.29.4:
+ version "2.29.4"
+ resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108"
+ integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==
+
mri@^1.1.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b"
diff --git a/ee/tabby-webserver/src/service/db/invitations.rs b/ee/tabby-webserver/src/service/db/invitations.rs
index c3b7a5f..454e6df 100644
--- a/ee/tabby-webserver/src/service/db/invitations.rs
+++ b/ee/tabby-webserver/src/service/db/invitations.rs
@@ -50,6 +50,10 @@ impl DbConn {
}
pub async fn create_invitation(&self, email: String) -> Result {
+ if self.get_user_by_email(&email).await?.is_some() {
+ return Err(anyhow!("User already registered"));
+ }
+
let code = Uuid::new_v4().to_string();
let res = self
.conn
@@ -60,9 +64,6 @@ impl DbConn {
Ok(rowid)
})
.await?;
- if res != 1 {
- return Err(anyhow!("failed to create invitation"));
- }
Ok(res as i32)
}