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

chore(ui): add prevalidate to sso creation #3726

Merged
merged 4 commits into from
Jan 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading