From 282ec8deff775e8e084c46231f5eba2d167c4c37 Mon Sep 17 00:00:00 2001 From: aliang Date: Mon, 20 Jan 2025 15:10:19 +0800 Subject: [PATCH] chore(ui): add prevalidate to sso creation (#3726) * 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> --- .../sso/components/credential-list.tsx | 14 +--- .../sso/components/ldap-credential-form.tsx | 16 ++++- .../sso/components/oauth-credential-form.tsx | 62 +++++++++++++---- .../sso/components/sso-type-radio.tsx | 2 +- .../components/oauth-credential-detail.tsx | 5 +- .../components/ldap-credential-detail.tsx | 1 + .../sso/new/component/new-page.tsx | 68 ++++++++++++++----- ee/tabby-ui/lib/tabby/query.ts | 11 +++ 8 files changed, 132 insertions(+), 47 deletions(-) diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/components/credential-list.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/components/credential-list.tsx index 423d481139aa..c4236e63f9c9 100644 --- a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/components/credential-list.tsx +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/components/credential-list.tsx @@ -6,7 +6,6 @@ import { useRouter } from 'next/navigation' import { compact, find } from 'lodash-es' import { useQuery } from 'urql' -import { graphql } from '@/lib/gql/generates' import { AuthProviderKind, LdapCredentialQuery, @@ -14,7 +13,7 @@ import { 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 { @@ -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 diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/components/ldap-credential-form.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/components/ldap-credential-form.tsx index eca1bafb668d..d8579011404d 100644 --- a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/components/ldap-credential-form.tsx +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/components/ldap-credential-form.tsx @@ -90,13 +90,18 @@ interface LDAPFormProps extends React.HTMLAttributes { isNew?: boolean defaultValues?: Partial | 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() @@ -157,7 +162,7 @@ export function LDAPCredentialForm({ .then(res => !!res?.data?.ldapCredential) if (hasExistingProvider) { form.setError('root', { - message: 'Provider already exists.' + message: providerExistedError }) return } @@ -205,8 +210,13 @@ export function LDAPCredentialForm({ return (
+ {existed && ( +
+ {providerExistedError} +
+ )} @@ -381,7 +391,7 @@ export function LDAPCredentialForm({ name="skipTlsVerify" render={({ field }) => ( - Connection security + Connection security
+const updateFormSchema = defaultFormSchema.extend({ + clientSecret: z.string().optional() +}) + +const providerExistedError = + 'Provider already exists. Please choose another one' interface OAuthCredentialFormProps extends React.HTMLAttributes { isNew?: boolean - provider: OAuthProvider - defaultValues?: Partial | undefined - onSuccess?: (formValues: OAuthCredentialFormValues) => void + defaultProvider: OAuthProvider + defaultValues?: Partial> | undefined + onSuccess?: (formValues: z.infer) => 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() @@ -96,14 +106,26 @@ export default function OAuthCredentialForm({ 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({ + 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>({ resolver: zodResolver(formSchema), defaultValues: formatedDefaultValues }) @@ -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) => { 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 } diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/components/sso-type-radio.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/components/sso-type-radio.tsx index ec60c197d7a4..6a0f338abb51 100644 --- a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/components/sso-type-radio.tsx +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/components/sso-type-radio.tsx @@ -17,7 +17,7 @@ export function SSOTypeRadio({ readonly }: SSOTypeRadioProps) { return ( -
+
= ({ >
diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/detail/ldap/components/ldap-credential-detail.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/detail/ldap/components/ldap-credential-detail.tsx index 04a05718c610..731970c51a41 100644 --- a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/detail/ldap/components/ldap-credential-detail.tsx +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/detail/ldap/components/ldap-credential-detail.tsx @@ -44,6 +44,7 @@ export const LdapCredentialDetail: React.FC<
diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/new/component/new-page.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/new/component/new-page.tsx index a4bfd9d66b34..68a87c8401bf 100644 --- a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/new/component/new-page.tsx +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/new/component/new-page.tsx @@ -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' @@ -13,27 +18,58 @@ import { SSOTypeRadio } from '../../components/sso-type-radio' export function NewPage() { const [type, setType] = useState('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 ( -
- - {type === 'oauth' ? ( - - ) : ( - - )} -
+ }> +
+ + {type === 'oauth' ? ( + + ) : ( + + )} +
+
) } diff --git a/ee/tabby-ui/lib/tabby/query.ts b/ee/tabby-ui/lib/tabby/query.ts index ae448b950896..87bdfd9dc9ba 100644 --- a/ee/tabby-ui/lib/tabby/query.ts +++ b/ee/tabby-ui/lib/tabby/query.ts @@ -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 {