diff --git a/ee/tabby-ui/app/(dashboard)/cluster/components/cluster.tsx b/ee/tabby-ui/app/(dashboard)/cluster/components/cluster.tsx new file mode 100644 index 0000000..c1a37a1 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/cluster/components/cluster.tsx @@ -0,0 +1,90 @@ +'use client' + +import { graphql } from '@/lib/gql/generates' +import { WorkerKind } from '@/lib/gql/generates/graphql' +import { useHealth } from '@/lib/hooks/use-health' +import { useWorkers } from '@/lib/hooks/use-workers' +import { useAuthenticatedGraphQLQuery, useGraphQLForm } from '@/lib/tabby/gql' +import { Button } from '@/components/ui/button' +import { IconRefresh } from '@/components/ui/icons' +import { CopyButton } from '@/components/copy-button' + +import WorkerCard from './worker-card' + +const getRegistrationTokenDocument = graphql(/* GraphQL */ ` + query GetRegistrationToken { + registrationToken + } +`) + +const resetRegistrationTokenDocument = graphql(/* GraphQL */ ` + mutation ResetRegistrationToken { + resetRegistrationToken + } +`) + +export default function Workers() { + const { data: healthInfo } = useHealth() + const workers = useWorkers() + const { data: registrationTokenRes, mutate } = useAuthenticatedGraphQLQuery( + getRegistrationTokenDocument + ) + + const { onSubmit: resetRegistrationToken } = useGraphQLForm( + resetRegistrationTokenDocument, + { + onSuccess: () => mutate() + } + ) + + if (!healthInfo) return + + return ( +
+ {!!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 + })} + + )} + +
+
+ ) +} diff --git a/ee/tabby-ui/app/(dashboard)/components/worker-card.tsx b/ee/tabby-ui/app/(dashboard)/cluster/components/worker-card.tsx similarity index 100% rename from ee/tabby-ui/app/(dashboard)/components/worker-card.tsx rename to ee/tabby-ui/app/(dashboard)/cluster/components/worker-card.tsx diff --git a/ee/tabby-ui/app/(dashboard)/cluster/page.tsx b/ee/tabby-ui/app/(dashboard)/cluster/page.tsx new file mode 100644 index 0000000..1c040e5 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/cluster/page.tsx @@ -0,0 +1,11 @@ +import { Metadata } from 'next' + +import ClusterInfo from './components/cluster' + +export const metadata: Metadata = { + title: 'Cluster Information' +} + +export default function IndexPage() { + return +} diff --git a/ee/tabby-ui/app/(dashboard)/components/sidebar.tsx b/ee/tabby-ui/app/(dashboard)/components/sidebar.tsx index 9d02dfe..307dbf2 100644 --- a/ee/tabby-ui/app/(dashboard)/components/sidebar.tsx +++ b/ee/tabby-ui/app/(dashboard)/components/sidebar.tsx @@ -4,7 +4,9 @@ import Link from 'next/link' import { usePathname } from 'next/navigation' import { cva } from 'class-variance-authority' +import { useSession } from '@/lib/tabby/auth' import { cn } from '@/lib/utils' +import { IconHome, IconNetwork, IconUsers } from '@/components/ui/icons' import UserPanel from '@/components/user-panel' export interface SidebarProps { @@ -13,6 +15,8 @@ export interface SidebarProps { } export default function Sidebar({ children, className }: SidebarProps) { + const { data: session } = useSession() + const isAdmin = session?.isAdmin || false return (
diff --git a/ee/tabby-ui/app/(dashboard)/layout.tsx b/ee/tabby-ui/app/(dashboard)/layout.tsx index 74669b2..9036d30 100644 --- a/ee/tabby-ui/app/(dashboard)/layout.tsx +++ b/ee/tabby-ui/app/(dashboard)/layout.tsx @@ -5,7 +5,10 @@ import { Header } from '@/components/header' import Sidebar from './components/sidebar' export const metadata: Metadata = { - title: 'Dashboard' + title: { + default: 'Home', + template: `Tabby - %s` + } } interface DashboardLayoutProps { diff --git a/ee/tabby-ui/app/(dashboard)/page.tsx b/ee/tabby-ui/app/(dashboard)/page.tsx index 1c44d1c..b54e1a6 100644 --- a/ee/tabby-ui/app/(dashboard)/page.tsx +++ b/ee/tabby-ui/app/(dashboard)/page.tsx @@ -1,100 +1,38 @@ 'use client' -import { PropsWithChildren, useEffect, useState } from 'react' - import { graphql } from '@/lib/gql/generates' -import { WorkerKind } from '@/lib/gql/generates/graphql' import { useHealth } from '@/lib/hooks/use-health' -import { useWorkers } from '@/lib/hooks/use-workers' -import { useSession } from '@/lib/tabby/auth' import { useAuthenticatedGraphQLQuery } from '@/lib/tabby/gql' -import { buttonVariants } from '@/components/ui/button' -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle -} from '@/components/ui/dialog' -import { IconSlack } from '@/components/ui/icons' import { Separator } from '@/components/ui/separator' import { CopyButton } from '@/components/copy-button' - -import WorkerCard from './components/worker-card' - -const COMMUNITY_DIALOG_SHOWN_KEY = 'community-dialog-shown' +import SlackDialog from '@/components/slack-dialog' export default function Home() { - const { status } = useSession() - const [open, setOpen] = useState(false) - useEffect(() => { - if (status !== 'authenticated') return - - if (!localStorage.getItem(COMMUNITY_DIALOG_SHOWN_KEY)) { - setOpen(true) - localStorage.setItem(COMMUNITY_DIALOG_SHOWN_KEY, 'true') - } - }, [status]) - return (
- - - - Join the Tabby community - - Connect with other contributors building Tabby. Share knowledge, - get help, and contribute to the open-source project. - - - - - - Join us on Slack - - - - +
) } -interface LinkProps { - href: string -} - -function Link({ href, children }: PropsWithChildren) { - return ( - - {children} - - ) -} - function toBadgeString(str: string) { return encodeURIComponent(str.replaceAll('-', '--')) } -const getRegistrationTokenDocument = graphql(/* GraphQL */ ` - query GetRegistrationToken { - registrationToken +const meQuery = graphql(/* GraphQL */ ` + query MeQuery { + me { + authToken + } } `) function MainPanel() { const { data: healthInfo } = useHealth() - const workers = useWorkers(healthInfo) - const { data: registrationTokenRes } = useAuthenticatedGraphQLQuery( - getRegistrationTokenDocument - ) + const { data } = useAuthenticatedGraphQLQuery(meQuery) - if (!healthInfo) return + if (!healthInfo || !data) return return (
@@ -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