Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui): improve admin setup ux #1671

Merged
merged 11 commits into from
Mar 21, 2024
39 changes: 39 additions & 0 deletions ee/tabby-ui/app/auth/signup/components/admin-register.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
.admin-register-wrap {
transition: transform 0.35s ease-out;
padding-top: 20vh;
padding-bottom: 20vh;
}

.step-mask {
position: relative;
pointer-events: none;
user-select: none;
border-color: hsl(var(--muted-foreground) / 0.1);

}
.step-mask:before {
content: "";
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: linear-gradient(
0deg,
hsl(var(--background) / 1) 0%,
hsl(var(--background) / 1) 30%,
hsl(var(--background) / 0.5) 100%
);
pointer-events: none;
user-select: none;
z-index: 10;
width: 110%;
}
.step-mask.remote:before {
background: linear-gradient(
0deg,
hsl(var(--background) / 1) 0%,
hsl(var(--background) / 1) 30%,
hsl(var(--background) / 0.9) 100%
);
}
96 changes: 96 additions & 0 deletions ee/tabby-ui/app/auth/signup/components/admin-register.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
'use client'

import { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation'

import { cn } from '@/lib/utils'
import { Button } from '@/components/ui/button'

import { UserAuthForm } from './user-register-form'

import './admin-register.css'

function AdminRegisterStep({
step,
currentStep,
children
}: {
step: number
currentStep: number
children: React.ReactNode
}) {
return (
<div
id={`step-${step}`}
className={cn('border-l border-foreground py-8 pl-12 pr-2', {
'step-mask': step !== currentStep,
remote: Math.abs(currentStep - step) > 1
})}
>
{children}
</div>
)
}

export default function AdminRegister() {
const router = useRouter()
const [currentStep, setCurrentStep] = useState(1)

useEffect(() => {
if (currentStep === 1) return
document
.getElementById(`step-${currentStep}`)
?.scrollIntoView({ behavior: 'smooth' })
}, [currentStep])

return (
<div className="admin-register-wrap h-screen w-[600px] overflow-hidden">
<AdminRegisterStep step={1} currentStep={currentStep}>
<h2 className="text-3xl font-semibold tracking-tight first:mt-0">
Welcome!
</h2>
<p className="mt-2 leading-7 text-muted-foreground">
Your tabby server is live and ready to use. This step by step guide
will help you set up your admin account.
</p>
<p className="leading-7 text-muted-foreground">
Admin account is the highest level of access in your server. Once
created, you can invite other members to join your server.
</p>
<Button className="mt-5 w-48" onClick={() => setCurrentStep(2)}>
Start
</Button>
</AdminRegisterStep>

<AdminRegisterStep step={2} currentStep={currentStep}>
<h3 className="text-2xl font-semibold tracking-tight">
Create Admin Account
</h3>
<p className="mb-3 leading-7 text-muted-foreground">
Please store your password in a safe place. We do not store your
password and cannot recover it for you.
</p>
<UserAuthForm
onSuccess={() => setCurrentStep(3)}
buttonClass="self-start w-48"
/>
</AdminRegisterStep>

<AdminRegisterStep step={3} currentStep={currentStep}>
<h3 className="text-2xl font-semibold tracking-tight">
Congratulations!
</h3>
<p className="leading-7 text-muted-foreground">
You have successfully created an admin account.
</p>
<p className="mb-3 leading-7 text-muted-foreground">
To start, navigate to the dashboard and invite other members to join
your server.
</p>
<Button className="mt-5 w-48" onClick={() => router.replace('/')}>
Go to dashboard
</Button>
</AdminRegisterStep>
</div>
)
}
23 changes: 12 additions & 11 deletions ee/tabby-ui/app/auth/signup/components/signup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,30 @@

import { useSearchParams } from 'next/navigation'

import AdminRegister from './admin-register'
import { UserAuthForm } from './user-register-form'

export default function Signup() {
const searchParams = useSearchParams()
const invitationCode = searchParams.get('invitationCode') || undefined
const isAdmin = searchParams.get('isAdmin') || false

const title = isAdmin ? 'Create an admin account' : 'Create an account'

const description = isAdmin
? 'Your instance will be secured, only registered users can access it.'
: 'Fill form below to create your account'

if (isAdmin || invitationCode) {
return <Content title={title} description={description} show />
} else {
if (isAdmin) return <AdminRegister />
if (invitationCode) {
return (
<Content
title="No invitation code"
description="Please contact your Tabby admin for an invitation code to register"
title="Create an account"
description="Fill form below to create your account"
show
/>
)
}
return (
<Content
title="No invitation code"
description="Please contact your Tabby admin for an invitation code to register"
/>
)
}

function Content({
Expand Down
21 changes: 17 additions & 4 deletions ee/tabby-ui/app/auth/signup/components/user-register-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,15 @@ const formSchema = z.object({

interface UserAuthFormProps extends React.HTMLAttributes<HTMLDivElement> {
invitationCode?: string
onSuccess?: () => void
buttonClass?: string
}

export function UserAuthForm({
className,
invitationCode,
onSuccess,
buttonClass,
...props
}: UserAuthFormProps) {
const form = useForm<z.infer<typeof formSchema>>({
Expand All @@ -71,7 +75,11 @@ export function UserAuthForm({
const onSubmit = useMutation(registerUser, {
async onCompleted(values) {
if (await signIn(values?.register)) {
router.replace('/')
if (onSuccess) {
onSuccess()
} else {
router.replace('/')
}
}
},
form
Expand All @@ -95,6 +103,7 @@ export function UserAuthForm({
autoComplete="email"
autoCorrect="off"
{...field}
value={field.value ?? ''}
/>
</FormControl>
<FormMessage />
Expand All @@ -108,7 +117,7 @@ export function UserAuthForm({
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" {...field} />
<Input type="password" {...field} value={field.value ?? ''} />
</FormControl>
<FormMessage />
</FormItem>
Expand All @@ -121,7 +130,7 @@ export function UserAuthForm({
<FormItem>
<FormLabel>Confirm Password</FormLabel>
<FormControl>
<Input type="password" {...field} />
<Input type="password" {...field} value={field.value ?? ''} />
</FormControl>
<FormMessage />
</FormItem>
Expand All @@ -138,7 +147,11 @@ export function UserAuthForm({
</FormItem>
)}
/>
<Button type="submit" className="mt-2" disabled={isSubmitting}>
<Button
type="submit"
className={cn('mt-2', buttonClass)}
disabled={isSubmitting}
>
{isSubmitting && (
<IconSpinner className="mr-2 h-4 w-4 animate-spin" />
)}
Expand Down
11 changes: 8 additions & 3 deletions ee/tabby-ui/lib/tabby/auth.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from 'react'
import { usePathname, useRouter } from 'next/navigation'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import useLocalStorage from 'use-local-storage'

import { graphql } from '@/lib/gql/generates'
Expand Down Expand Up @@ -282,6 +282,7 @@ function useAuthenticatedSession() {
const isAdminInitialized = useIsAdminInitialized()
const router = useRouter()
const pathName = usePathname()
const searchParams = useSearchParams()
const { data: session, status } = useSession()

React.useEffect(() => {
Expand All @@ -290,8 +291,12 @@ function useAuthenticatedSession() {
if (isAdminInitialized === undefined) return

if (!isAdminInitialized) {
router.replace('/auth/signup?isAdmin=true')
} else if (!redirectWhitelist.includes(pathName)) {
return router.replace('/auth/signup?isAdmin=true')
}

const isAdminSignup =
pathName === '/auth/signup' && searchParams.get('isAdmin') === 'true'
if (!redirectWhitelist.includes(pathName) || isAdminSignup) {
router.replace('/auth/signin')
}
}, [isAdminInitialized, status])
Expand Down
Loading