Skip to content

Commit

Permalink
chore(ui): add prevalidate to sso creation (#3726)
Browse files Browse the repository at this point in the history
* chore(ui): prevalidate sso creation

* update

* [autofix.ci] apply automated fixes

* update

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
  • Loading branch information
liangfung and autofix-ci[bot] authored Jan 20, 2025
1 parent 9a3300a commit 72478e5
Show file tree
Hide file tree
Showing 8 changed files with 132 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@ import { useRouter } from 'next/navigation'
import { compact, find } from 'lodash-es'
import { useQuery } from 'urql'

import { graphql } from '@/lib/gql/generates'
import {
AuthProviderKind,
LdapCredentialQuery,
LicenseType,
OAuthCredentialQuery,
OAuthProvider
} from '@/lib/gql/generates/graphql'
import { ldapCredentialQuery } from '@/lib/tabby/query'
import { ldapCredentialQuery, oauthCredential } from '@/lib/tabby/query'
import { Button, buttonVariants } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import {
Expand All @@ -29,17 +28,6 @@ import LoadingWrapper from '@/components/loading-wrapper'

import { PROVIDER_METAS } from '../constant'

export const oauthCredential = graphql(/* GraphQL */ `
query OAuthCredential($provider: OAuthProvider!) {
oauthCredential(provider: $provider) {
provider
clientId
createdAt
updatedAt
}
}
`)

export const CredentialList = () => {
const authProviderKindCount = useMemo(() => {
return Object.keys(AuthProviderKind).length
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,18 @@ interface LDAPFormProps extends React.HTMLAttributes<HTMLDivElement> {
isNew?: boolean
defaultValues?: Partial<LDAPFormValues> | undefined
onSuccess?: (formValues: LDAPFormValues) => void
existed?: boolean
}

const providerExistedError =
'LDAP provider already exists and cannot be created again.'

export function LDAPCredentialForm({
className,
isNew,
defaultValues,
onSuccess,
existed,
...props
}: LDAPFormProps) {
const router = useRouter()
Expand Down Expand Up @@ -157,7 +162,7 @@ export function LDAPCredentialForm({
.then(res => !!res?.data?.ldapCredential)
if (hasExistingProvider) {
form.setError('root', {
message: 'Provider already exists.'
message: providerExistedError
})
return
}
Expand Down Expand Up @@ -205,8 +210,13 @@ export function LDAPCredentialForm({
return (
<Form {...form}>
<div className={cn('grid gap-2', className)} {...props}>
{existed && (
<div className="mt-2 text-sm font-medium text-destructive">
{providerExistedError}
</div>
)}
<form
className="grid gap-4"
className="mt-6 grid gap-4"
onSubmit={form.handleSubmit(onSubmit)}
ref={formRef}
>
Expand Down Expand Up @@ -381,7 +391,7 @@ export function LDAPCredentialForm({
name="skipTlsVerify"
render={({ field }) => (
<FormItem>
<FormLabel required>Connection security</FormLabel>
<FormLabel>Connection security</FormLabel>
<div className="flex items-center gap-1">
<FormControl>
<Checkbox
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import * as z from 'zod'
import { graphql } from '@/lib/gql/generates'
import { LicenseType, OAuthProvider } from '@/lib/gql/generates/graphql'
import { useMutation } from '@/lib/tabby/gql'
import { oauthCredential } from '@/lib/tabby/query'
import { cn } from '@/lib/utils'
import {
AlertDialog,
Expand Down Expand Up @@ -46,7 +47,6 @@ import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
import { CopyButton } from '@/components/copy-button'
import { LicenseGuard } from '@/components/license-guard'

import { oauthCredential } from './credential-list'
import { SubTitle } from './form-sub-title'

export const updateOauthCredentialMutation = graphql(/* GraphQL */ `
Expand All @@ -67,43 +67,65 @@ const oauthCallbackUrl = graphql(/* GraphQL */ `
}
`)

const formSchema = z.object({
const defaultFormSchema = z.object({
clientId: z.string(),
clientSecret: z.string().optional(),
clientSecret: z.string(),
provider: z.nativeEnum(OAuthProvider)
})

export type OAuthCredentialFormValues = z.infer<typeof formSchema>
const updateFormSchema = defaultFormSchema.extend({
clientSecret: z.string().optional()
})

const providerExistedError =
'Provider already exists. Please choose another one'

interface OAuthCredentialFormProps
extends React.HTMLAttributes<HTMLDivElement> {
isNew?: boolean
provider: OAuthProvider
defaultValues?: Partial<OAuthCredentialFormValues> | undefined
onSuccess?: (formValues: OAuthCredentialFormValues) => void
defaultProvider: OAuthProvider
defaultValues?: Partial<z.infer<typeof defaultFormSchema>> | undefined
onSuccess?: (formValues: z.infer<typeof defaultFormSchema>) => void
/**
* for creation, if there are existed providers, show a error message
*/
existedProviders?: OAuthProvider[]
}

export default function OAuthCredentialForm({
className,
isNew,
provider,
defaultProvider,
defaultValues,
onSuccess,
existedProviders,
...props
}: OAuthCredentialFormProps) {
const router = useRouter()
const client = useClient()
const formatedDefaultValues = React.useMemo(() => {
return {
...(defaultValues || {}),
provider
provider: defaultProvider || OAuthProvider.Github
}
}, [])

const [deleteAlertVisible, setDeleteAlertVisible] = React.useState(false)
const [isDeleting, setIsDeleting] = React.useState(false)

const form = useForm<OAuthCredentialFormValues>({
const formSchema = React.useMemo(() => {
if (!isNew) return updateFormSchema

return defaultFormSchema.extend({
provider: z
.nativeEnum(OAuthProvider)
.refine(v => !existedProviders?.includes(v), {
message: providerExistedError
})
})
}, [isNew, existedProviders])

const form = useForm<z.infer<typeof defaultFormSchema>>({
resolver: zodResolver(formSchema),
defaultValues: formatedDefaultValues
})
Expand All @@ -125,16 +147,32 @@ export default function OAuthCredentialForm({
form
})

const provider = form.watch('provider')

React.useEffect(() => {
if (!isNew) {
return
}

if (provider && existedProviders?.includes(provider)) {
form.setError('provider', {
message: providerExistedError
})
} else {
form.clearErrors('provider')
}
}, [provider, isNew, existedProviders])

const deleteOAuthCredential = useMutation(deleteOauthCredentialMutation)

const onSubmit = async (values: OAuthCredentialFormValues) => {
const onSubmit = async (values: z.infer<typeof defaultFormSchema>) => {
if (isNew) {
const hasExistingProvider = await client
.query(oauthCredential, { provider: values.provider })
.then(res => !!res?.data?.oauthCredential)
if (hasExistingProvider) {
form.setError('provider', {
message: 'Provider already exists. Please choose another one'
message: providerExistedError
})
return
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function SSOTypeRadio({
readonly
}: SSOTypeRadioProps) {
return (
<div className={cn('mb-6 space-y-2', className)}>
<div className={cn('space-y-2', className)}>
<Label>Type</Label>
<RadioGroup
value={value}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import { isNil, pickBy } from 'lodash-es'
import { useQuery } from 'urql'

import { OAuthProvider } from '@/lib/gql/generates/graphql'
import { oauthCredential } from '@/lib/tabby/query'
import LoadingWrapper from '@/components/loading-wrapper'
import { ListSkeleton } from '@/components/skeleton'

import { oauthCredential } from '../../../components/credential-list'
import OAuthCredentialForm from '../../../components/oauth-credential-form'
import { SSOTypeRadio } from '../../../components/sso-type-radio'

Expand Down Expand Up @@ -46,9 +46,10 @@ const OAuthCredentialDetail: React.FC<OAuthCredentialDetailProps> = ({
>
<SSOTypeRadio value="oauth" readonly />
<OAuthCredentialForm
provider={provider}
defaultValues={defaultValues}
onSuccess={onSubmitSuccess}
className="mt-6"
defaultProvider={provider}
/>
</LoadingWrapper>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export const LdapCredentialDetail: React.FC<
<LDAPCredentialForm
defaultValues={defaultValues}
onSuccess={onSubmitSuccess}
className="mt-6"
/>
</LoadingWrapper>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@

import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { compact } from 'lodash-es'
import { useQuery } from 'urql'

import { OAuthProvider } from '@/lib/gql/generates/graphql'
import { ldapCredentialQuery, oauthCredential } from '@/lib/tabby/query'
import { SSOType } from '@/lib/types'
import LoadingWrapper from '@/components/loading-wrapper'
import { ListSkeleton } from '@/components/skeleton'

import { LDAPCredentialForm } from '../../components/ldap-credential-form'
import OAuthCredentialForm from '../../components/oauth-credential-form'
Expand All @@ -13,27 +18,58 @@ import { SSOTypeRadio } from '../../components/sso-type-radio'
export function NewPage() {
const [type, setType] = useState<SSOType>('oauth')
const router = useRouter()
const [{ data: githubData, fetching: fetchingGithub }] = useQuery({
query: oauthCredential,
variables: { provider: OAuthProvider.Github }
})
const [{ data: googleData, fetching: fetchingGoogle }] = useQuery({
query: oauthCredential,
variables: { provider: OAuthProvider.Google }
})
const [{ data: gitlabData, fetching: fetchingGitlab }] = useQuery({
query: oauthCredential,
variables: { provider: OAuthProvider.Gitlab }
})

const [{ data: ldapData, fetching: fetchingLdap }] = useQuery({
query: ldapCredentialQuery
})

const fetchingProviders =
fetchingGithub || fetchingGoogle || fetchingGitlab || fetchingLdap

const isLdapExisted = !!ldapData?.ldapCredential
const existedProviders = compact([
githubData?.oauthCredential && OAuthProvider.Github,
googleData?.oauthCredential && OAuthProvider.Google,
gitlabData?.oauthCredential && OAuthProvider.Gitlab
])

const onCreateSuccess = () => {
router.replace('/settings/sso')
}

return (
<div>
<SSOTypeRadio value={type} onChange={setType} />
{type === 'oauth' ? (
<OAuthCredentialForm
provider={OAuthProvider.Github}
isNew
onSuccess={onCreateSuccess}
/>
) : (
<LDAPCredentialForm
isNew
defaultValues={{ skipTlsVerify: false }}
onSuccess={onCreateSuccess}
/>
)}
</div>
<LoadingWrapper loading={fetchingProviders} fallback={<ListSkeleton />}>
<div>
<SSOTypeRadio value={type} onChange={setType} />
{type === 'oauth' ? (
<OAuthCredentialForm
isNew
defaultProvider={OAuthProvider.Github}
existedProviders={existedProviders}
onSuccess={onCreateSuccess}
className="mt-6"
/>
) : (
<LDAPCredentialForm
isNew
defaultValues={{ skipTlsVerify: false }}
onSuccess={onCreateSuccess}
existed={isLdapExisted}
/>
)}
</div>
</LoadingWrapper>
)
}
11 changes: 11 additions & 0 deletions ee/tabby-ui/lib/tabby/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,17 @@ export const ldapCredentialQuery = graphql(/* GraphQL */ `
}
`)

export const oauthCredential = graphql(/* GraphQL */ `
query OAuthCredential($provider: OAuthProvider!) {
oauthCredential(provider: $provider) {
provider
clientId
createdAt
updatedAt
}
}
`)

export const repositorySourceListQuery = graphql(/* GraphQL */ `
query RepositorySourceList {
repositoryList {
Expand Down

0 comments on commit 72478e5

Please sign in to comment.