diff --git a/messages/en-US.json b/messages/en-US.json index a10a45421..f5de37878 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1771,6 +1771,8 @@ "aiResourceProvidersUpdated": "Providers updated", "aiResourceProvidersErrorUpdate": "Failed to update providers", "aiResourceAliasRequired": "Alias is required for inference resources", + "aiResourceDomainConfiguration": "Domain configuration", + "aiResourceDomainConfigurationDescription": "Choose the domain clients will use to reach this inference resource.", "sidebarApiKeys": "API Keys", "sidebarProvisioning": "Provisioning", "sidebarSettings": "Settings", diff --git a/server/routers/siteResource/createSiteResource.ts b/server/routers/siteResource/createSiteResource.ts index feeca0938..815d7adb8 100644 --- a/server/routers/siteResource/createSiteResource.ts +++ b/server/routers/siteResource/createSiteResource.ts @@ -168,7 +168,7 @@ const createSiteResourceSchema = z ) .refine( (data) => { - // destination is only optional for ssh mode with native authDaemonMode + // destination is only optional for ssh mode with native authDaemonMode or inference if ( (data.mode === "ssh" && data.authDaemonMode === "native") || data.mode == "inference" @@ -182,7 +182,7 @@ const createSiteResourceSchema = z }, { message: - "Destination is required unless mode is ssh with authDaemonMode native" + "Destination is required unless mode is ssh with authDaemonMode native or inference" } ) .refine( @@ -509,7 +509,15 @@ export async function createSiteResource( const existingResource = await db .select() .from(siteResources) - .where(eq(siteResources.fullDomain, fullDomain)); + .where( + and( + eq(siteResources.fullDomain, fullDomain), + ne( + siteResources.requiresExitNodeConnection, + mode == "inference" + ) + ) + ); // exclude looking at the ones on exit nodes if this is an inference resource if (existingResource.length > 0) { return next( @@ -529,11 +537,7 @@ export async function createSiteResource( .where( and( eq(siteResources.orgId, orgId), - eq(siteResources.alias, alias.trim()), - ne( - siteResources.requiresExitNodeConnection, - mode == "inference" - ) // exclude looking at the ones on exit nodes if this is an inference resource + eq(siteResources.alias, alias.trim()) ) ) .limit(1); diff --git a/server/routers/siteResource/updateSiteResource.ts b/server/routers/siteResource/updateSiteResource.ts index 457c401d4..754c82a65 100644 --- a/server/routers/siteResource/updateSiteResource.ts +++ b/server/routers/siteResource/updateSiteResource.ts @@ -172,7 +172,7 @@ const updateSiteResourceSchema = z }, { message: - "Destination is required unless mode is ssh with authDaemonMode native" + "Destination is required unless mode is ssh with authDaemonMode native or inference" } ) .refine( @@ -481,7 +481,15 @@ export async function updateSiteResource( const [existingDomain] = await db .select() .from(siteResources) - .where(eq(siteResources.fullDomain, fullDomain)); + .where( + and( + eq(siteResources.fullDomain, fullDomain), + ne( + siteResources.requiresExitNodeConnection, + mode == "inference" + ) + ) + ); // exclude looking at the ones on exit nodes if this is an inference resource if ( existingDomain && @@ -511,11 +519,7 @@ export async function updateSiteResource( and( eq(siteResources.orgId, existingSiteResource.orgId), eq(siteResources.alias, alias.trim()), - ne(siteResources.siteResourceId, siteResourceId), // exclude self - ne( - siteResources.requiresExitNodeConnection, - mode == "inference" - ) // exclude looking at the ones on exit nodes if this is an inference resource + ne(siteResources.siteResourceId, siteResourceId) // exclude self ) ) .limit(1); diff --git a/src/app/[orgId]/settings/resources/private/[niceId]/general/page.tsx b/src/app/[orgId]/settings/resources/private/[niceId]/general/page.tsx index b0af75ec0..8dbdb3809 100644 --- a/src/app/[orgId]/settings/resources/private/[niceId]/general/page.tsx +++ b/src/app/[orgId]/settings/resources/private/[niceId]/general/page.tsx @@ -24,9 +24,7 @@ import { } from "@app/components/ui/form"; import { Input } from "@app/components/ui/input"; import { SwitchInput } from "@app/components/SwitchInput"; -import { PrivateResourceAliasField } from "@app/components/PrivateResourceDestinationFields"; import { createGeneralFormSchema } from "@app/lib/privateResourceForm"; -import { asAnyControl, asAnyWatch } from "@app/lib/formControlUtils"; import { zodResolver } from "@hookform/resolvers/zod"; import { useTranslations } from "next-intl"; import { useActionState, useMemo } from "react"; @@ -37,12 +35,8 @@ import { useSaveSiteResource } from "@app/hooks/useSaveSiteResource"; export default function PrivateResourceGeneralPage() { const t = useTranslations(); const { save, siteResource } = useSaveSiteResource(); - const isInference = siteResource.mode === "inference"; - const formSchema = useMemo( - () => createGeneralFormSchema(t, { requireAlias: isInference }), - [t, isInference] - ); + const formSchema = useMemo(() => createGeneralFormSchema(t), [t]); type FormValues = z.infer; const form = useForm({ @@ -50,8 +44,7 @@ export default function PrivateResourceGeneralPage() { defaultValues: { name: siteResource.name, niceId: siteResource.niceId, - enabled: siteResource.enabled, - alias: siteResource.alias ?? null + enabled: siteResource.enabled } }); @@ -63,13 +56,7 @@ export default function PrivateResourceGeneralPage() { await save({ name: data.name, niceId: data.niceId, - enabled: data.enabled, - ...(isInference - ? { - mode: "inference" as const, - alias: data.alias - } - : {}) + enabled: data.enabled }); }, null); @@ -165,17 +152,6 @@ export default function PrivateResourceGeneralPage() { )} /> - {isInference && ( - - - - )} diff --git a/src/app/[orgId]/settings/resources/private/[niceId]/inference/page.tsx b/src/app/[orgId]/settings/resources/private/[niceId]/inference/page.tsx index 34534fdd4..78f13370c 100644 --- a/src/app/[orgId]/settings/resources/private/[niceId]/inference/page.tsx +++ b/src/app/[orgId]/settings/resources/private/[niceId]/inference/page.tsx @@ -1,15 +1,318 @@ -import type { Metadata } from "next"; -import { redirect } from "next/navigation"; +"use client"; -export const metadata: Metadata = { - title: "Private Resource" -}; +import { + SettingsContainer, + SettingsFormCell, + SettingsFormGrid, + SettingsSection, + SettingsSectionBody, + SettingsSectionDescription, + SettingsSectionFooter, + SettingsSectionForm, + SettingsSectionHeader, + SettingsSectionTitle, + SettingsSubsectionDescription, + SettingsSubsectionHeader, + SettingsSubsectionTitle +} from "@app/components/Settings"; +import { + AiProvidersSelector, + type SelectedAiProvider +} from "@app/components/AiProvidersSelector"; +import DomainPicker from "@app/components/DomainPicker"; +import { Button } from "@app/components/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage +} from "@app/components/ui/form"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { useSaveSiteResource } from "@app/hooks/useSaveSiteResource"; +import { useSiteResourceContext } from "@app/hooks/useSiteResourceContext"; +import { toast } from "@app/hooks/useToast"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; +import { resourceQueries } from "@app/lib/queries"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { useTranslations } from "next-intl"; +import { useRouter } from "next/navigation"; +import { useActionState, useEffect, useMemo, useState } from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; -export default async function PrivateResourceInferencePage(props: { - params: Promise<{ niceId: string; orgId: string }>; -}) { - const params = await props.params; - redirect( - `/${params.orgId}/settings/resources/private/${params.niceId}/providers` +export default function PrivateResourceInferencePage() { + const t = useTranslations(); + const router = useRouter(); + const { env } = useEnvContext(); + const api = createApiClient({ env }); + const queryClient = useQueryClient(); + const { siteResource } = useSiteResourceContext(); + const { save } = useSaveSiteResource(); + + useEffect(() => { + if (siteResource.mode !== "inference") { + router.replace( + `/${siteResource.orgId}/settings/resources/private/${siteResource.niceId}/general` + ); + } + }, [router, siteResource.mode, siteResource.niceId, siteResource.orgId]); + + const formSchema = useMemo( + () => + z.object({ + providerIds: z.array(z.number().int().positive()), + httpConfigSubdomain: z.string().nullish(), + httpConfigDomainId: z.string().nullish(), + httpConfigFullDomain: z.string().nullish() + }), + [] + ); + type FormValues = z.infer; + + const [selectedProviders, setSelectedProviders] = useState< + SelectedAiProvider[] + >([]); + + const attachedQuery = useQuery({ + ...resourceQueries.siteResourceAiProviders({ + siteResourceId: siteResource.id + }), + enabled: siteResource.mode === "inference" + }); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + providerIds: [], + httpConfigSubdomain: siteResource.subdomain ?? null, + httpConfigDomainId: siteResource.domainId ?? null, + httpConfigFullDomain: siteResource.fullDomain ?? null + } + }); + + const httpConfigSubdomain = form.watch("httpConfigSubdomain"); + const httpConfigDomainId = form.watch("httpConfigDomainId"); + const httpConfigFullDomain = form.watch("httpConfigFullDomain"); + + useEffect(() => { + if (!attachedQuery.data) return; + const providers = attachedQuery.data.map((provider) => ({ + id: String(provider.providerId), + text: provider.name + })); + setSelectedProviders(providers); + form.setValue( + "providerIds", + attachedQuery.data.map((p) => p.providerId) + ); + }, [attachedQuery.data, form]); + + const [, formAction, saveLoading] = useActionState(async () => { + const isValid = await form.trigger(); + if (!isValid) return; + + const data = form.getValues(); + try { + await save({ + mode: "inference", + httpConfigSubdomain: data.httpConfigSubdomain, + httpConfigDomainId: data.httpConfigDomainId, + httpConfigFullDomain: data.httpConfigFullDomain + }); + + await api.post(`/site-resource/${siteResource.id}/ai-providers`, { + providers: data.providerIds.map((providerId) => ({ + providerId, + modelAccessMode: "catalog" + })) + }); + + await queryClient.invalidateQueries( + resourceQueries.siteResourceAiProviders({ + siteResourceId: siteResource.id + }) + ); + + toast({ + title: t("success"), + description: t("aiResourceProvidersUpdated") + }); + } catch (error) { + toast({ + variant: "destructive", + title: t("aiResourceProvidersErrorUpdate"), + description: formatAxiosError( + error, + t("aiResourceProvidersErrorUpdate") + ) + }); + } + }, null); + + if (siteResource.mode !== "inference") { + return null; + } + + return ( + + + + + {t("aiResourceProviders")} + + + {t("aiResourceProvidersDescription")} + + + + + +
+ + + + ( + + + {t( + "aiResourceProviders" + )} + + + { + setSelectedProviders( + providers + ); + form.setValue( + "providerIds", + providers.map( + (p) => + parseInt( + p.id, + 10 + ) + ), + { + shouldValidate: true + } + ); + }} + /> + + + {t( + "aiResourceProvidersHelp" + )} + + + + )} + /> + + + + + + {t( + "aiResourceDomainConfiguration" + )} + + + {t( + "aiResourceDomainConfigurationDescription" + )} + + + + + { + if (res === null) { + form.setValue( + "httpConfigSubdomain", + null + ); + form.setValue( + "httpConfigDomainId", + null + ); + form.setValue( + "httpConfigFullDomain", + null + ); + return; + } + form.setValue( + "httpConfigSubdomain", + res.subdomain ?? null + ); + form.setValue( + "httpConfigDomainId", + res.domainId + ); + form.setValue( + "httpConfigFullDomain", + res.fullDomain + ); + }} + /> + + +
+ +
+
+ + + + +
+
); } diff --git a/src/app/[orgId]/settings/resources/private/[niceId]/layout.tsx b/src/app/[orgId]/settings/resources/private/[niceId]/layout.tsx index 57064e545..f07b92673 100644 --- a/src/app/[orgId]/settings/resources/private/[niceId]/layout.tsx +++ b/src/app/[orgId]/settings/resources/private/[niceId]/layout.tsx @@ -55,37 +55,20 @@ export default async function PrivateResourceLayout( | "sshSettings" | "inferenceSettings"; - const isInference = siteResource.mode === "inference"; - - const navItems = isInference - ? [ - { - title: t("general"), - href: `/{orgId}/settings/resources/private/{niceId}/general` - }, - { - title: t("aiResourceProviders"), - href: `/{orgId}/settings/resources/private/{niceId}/providers` - }, - { - title: t("authentication"), - href: `/{orgId}/settings/resources/private/{niceId}/access` - } - ] - : [ - { - title: t("general"), - href: `/{orgId}/settings/resources/private/{niceId}/general` - }, - { - title: t(modeSettingsKey), - href: `/{orgId}/settings/resources/private/{niceId}/${siteResource.mode}` - }, - { - title: t("authentication"), - href: `/{orgId}/settings/resources/private/{niceId}/access` - } - ]; + const navItems = [ + { + title: t("general"), + href: `/{orgId}/settings/resources/private/{niceId}/general` + }, + { + title: t(modeSettingsKey), + href: `/{orgId}/settings/resources/private/{niceId}/${siteResource.mode}` + }, + { + title: t("authentication"), + href: `/{orgId}/settings/resources/private/{niceId}/access` + } + ]; return ( <> diff --git a/src/app/[orgId]/settings/resources/private/[niceId]/providers/page.tsx b/src/app/[orgId]/settings/resources/private/[niceId]/providers/page.tsx deleted file mode 100644 index dcbc4ea29..000000000 --- a/src/app/[orgId]/settings/resources/private/[niceId]/providers/page.tsx +++ /dev/null @@ -1,230 +0,0 @@ -"use client"; - -import { - SettingsContainer, - SettingsFormCell, - SettingsFormGrid, - SettingsSection, - SettingsSectionBody, - SettingsSectionDescription, - SettingsSectionFooter, - SettingsSectionForm, - SettingsSectionHeader, - SettingsSectionTitle -} from "@app/components/Settings"; -import { - AiProvidersSelector, - type SelectedAiProvider -} from "@app/components/AiProvidersSelector"; -import { Button } from "@app/components/ui/button"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage -} from "@app/components/ui/form"; -import { useEnvContext } from "@app/hooks/useEnvContext"; -import { useSiteResourceContext } from "@app/hooks/useSiteResourceContext"; -import { toast } from "@app/hooks/useToast"; -import { createApiClient, formatAxiosError } from "@app/lib/api"; -import { resourceQueries } from "@app/lib/queries"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { useTranslations } from "next-intl"; -import { useRouter } from "next/navigation"; -import { useActionState, useEffect, useMemo, useState } from "react"; -import { useForm } from "react-hook-form"; -import { z } from "zod"; - -export default function PrivateResourceProvidersPage() { - const t = useTranslations(); - const router = useRouter(); - const { env } = useEnvContext(); - const api = createApiClient({ env }); - const queryClient = useQueryClient(); - const { siteResource } = useSiteResourceContext(); - - useEffect(() => { - if (siteResource.mode !== "inference") { - router.replace( - `/${siteResource.orgId}/settings/resources/private/${siteResource.niceId}/general` - ); - } - }, [router, siteResource.mode, siteResource.niceId, siteResource.orgId]); - - const formSchema = useMemo( - () => - z.object({ - providerIds: z.array(z.number().int().positive()) - }), - [] - ); - type FormValues = z.infer; - - const [selectedProviders, setSelectedProviders] = useState< - SelectedAiProvider[] - >([]); - - const attachedQuery = useQuery({ - ...resourceQueries.siteResourceAiProviders({ - siteResourceId: siteResource.id - }), - enabled: siteResource.mode === "inference" - }); - - const form = useForm({ - resolver: zodResolver(formSchema), - defaultValues: { - providerIds: [] - } - }); - - useEffect(() => { - if (!attachedQuery.data) return; - const providers = attachedQuery.data.map((provider) => ({ - id: String(provider.providerId), - text: provider.name - })); - setSelectedProviders(providers); - form.reset({ - providerIds: attachedQuery.data.map((p) => p.providerId) - }); - }, [attachedQuery.data, form]); - - const [, formAction, saveLoading] = useActionState(async () => { - const isValid = await form.trigger(); - if (!isValid) return; - - const data = form.getValues(); - try { - await api.post(`/site-resource/${siteResource.id}/ai-providers`, { - providers: data.providerIds.map((providerId) => ({ - providerId, - modelAccessMode: "catalog" - })) - }); - - await queryClient.invalidateQueries( - resourceQueries.siteResourceAiProviders({ - siteResourceId: siteResource.id - }) - ); - - toast({ - title: t("success"), - description: t("aiResourceProvidersUpdated") - }); - } catch (error) { - toast({ - variant: "destructive", - title: t("aiResourceProvidersErrorUpdate"), - description: formatAxiosError( - error, - t("aiResourceProvidersErrorUpdate") - ) - }); - } - }, null); - - if (siteResource.mode !== "inference") { - return null; - } - - return ( - - - - - {t("aiResourceProviders")} - - - {t("aiResourceProvidersDescription")} - - - - - -
- - - - ( - - - {t( - "aiResourceProviders" - )} - - - { - setSelectedProviders( - providers - ); - form.setValue( - "providerIds", - providers.map( - (p) => - parseInt( - p.id, - 10 - ) - ), - { - shouldValidate: true - } - ); - }} - /> - - - {t( - "aiResourceProvidersHelp" - )} - - - - )} - /> - - -
- -
-
- - - - -
-
- ); -} diff --git a/src/app/[orgId]/settings/resources/public/[niceId]/inference/page.tsx b/src/app/[orgId]/settings/resources/public/[niceId]/inference/page.tsx index 8ea8d2dd5..8741159b4 100644 --- a/src/app/[orgId]/settings/resources/public/[niceId]/inference/page.tsx +++ b/src/app/[orgId]/settings/resources/public/[niceId]/inference/page.tsx @@ -1,15 +1,230 @@ -import type { Metadata } from "next"; -import { redirect } from "next/navigation"; +"use client"; -export const metadata: Metadata = { - title: "Public Resource" -}; +import { + SettingsContainer, + SettingsFormCell, + SettingsFormGrid, + SettingsSection, + SettingsSectionBody, + SettingsSectionDescription, + SettingsSectionFooter, + SettingsSectionForm, + SettingsSectionHeader, + SettingsSectionTitle +} from "@app/components/Settings"; +import { + AiProvidersSelector, + type SelectedAiProvider +} from "@app/components/AiProvidersSelector"; +import { Button } from "@app/components/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage +} from "@app/components/ui/form"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { useResourceContext } from "@app/hooks/useResourceContext"; +import { toast } from "@app/hooks/useToast"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; +import { resourceQueries } from "@app/lib/queries"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { useTranslations } from "next-intl"; +import { useRouter } from "next/navigation"; +import { useActionState, useEffect, useMemo, useState } from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; -export default async function PublicResourceInferencePage(props: { - params: Promise<{ niceId: string; orgId: string }>; -}) { - const params = await props.params; - redirect( - `/${params.orgId}/settings/resources/public/${params.niceId}/providers` +export default function PublicResourceInferencePage() { + const t = useTranslations(); + const router = useRouter(); + const { env } = useEnvContext(); + const api = createApiClient({ env }); + const queryClient = useQueryClient(); + const { resource } = useResourceContext(); + + useEffect(() => { + if (resource.mode !== "inference") { + router.replace( + `/${resource.orgId}/settings/resources/public/${resource.niceId}/general` + ); + } + }, [router, resource.mode, resource.niceId, resource.orgId]); + + const formSchema = useMemo( + () => + z.object({ + providerIds: z.array(z.number().int().positive()) + }), + [] + ); + type FormValues = z.infer; + + const [selectedProviders, setSelectedProviders] = useState< + SelectedAiProvider[] + >([]); + + const attachedQuery = useQuery({ + ...resourceQueries.resourceAiProviders({ + resourceId: resource.resourceId + }), + enabled: resource.mode === "inference" + }); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + providerIds: [] + } + }); + + useEffect(() => { + if (!attachedQuery.data) return; + const providers = attachedQuery.data.map((provider) => ({ + id: String(provider.providerId), + text: provider.name + })); + setSelectedProviders(providers); + form.reset({ + providerIds: attachedQuery.data.map((p) => p.providerId) + }); + }, [attachedQuery.data, form]); + + const [, formAction, saveLoading] = useActionState(async () => { + const isValid = await form.trigger(); + if (!isValid) return; + + const data = form.getValues(); + try { + await api.post(`/resource/${resource.resourceId}/ai-providers`, { + providers: data.providerIds.map((providerId) => ({ + providerId, + modelAccessMode: "catalog" + })) + }); + + await queryClient.invalidateQueries( + resourceQueries.resourceAiProviders({ + resourceId: resource.resourceId + }) + ); + + toast({ + title: t("success"), + description: t("aiResourceProvidersUpdated") + }); + } catch (error) { + toast({ + variant: "destructive", + title: t("aiResourceProvidersErrorUpdate"), + description: formatAxiosError( + error, + t("aiResourceProvidersErrorUpdate") + ) + }); + } + }, null); + + if (resource.mode !== "inference") { + return null; + } + + return ( + + + + + {t("aiResourceProviders")} + + + {t("aiResourceProvidersDescription")} + + + + + +
+ + + + ( + + + {t( + "aiResourceProviders" + )} + + + { + setSelectedProviders( + providers + ); + form.setValue( + "providerIds", + providers.map( + (p) => + parseInt( + p.id, + 10 + ) + ), + { + shouldValidate: true + } + ); + }} + /> + + + {t( + "aiResourceProvidersHelp" + )} + + + + )} + /> + + +
+ +
+
+ + + + +
+
); } diff --git a/src/app/[orgId]/settings/resources/public/[niceId]/layout.tsx b/src/app/[orgId]/settings/resources/public/[niceId]/layout.tsx index 302650bf2..7eb664b5a 100644 --- a/src/app/[orgId]/settings/resources/public/[niceId]/layout.tsx +++ b/src/app/[orgId]/settings/resources/public/[niceId]/layout.tsx @@ -82,20 +82,7 @@ export default async function ResourceLayout(props: ResourceLayoutProps) { redirect(`/${params.orgId}/settings/resources`); } - const isInference = resource.mode === "inference"; - - const navItems = isInference - ? [ - { - title: t("general"), - href: `/{orgId}/settings/resources/public/{niceId}/general` - }, - { - title: t("aiResourceProviders"), - href: `/{orgId}/settings/resources/public/{niceId}/providers` - } - ] - : [ + const navItems = [ { title: t("general"), href: `/{orgId}/settings/resources/public/{niceId}/general` diff --git a/src/app/[orgId]/settings/resources/public/[niceId]/providers/page.tsx b/src/app/[orgId]/settings/resources/public/[niceId]/providers/page.tsx deleted file mode 100644 index 51182c2a8..000000000 --- a/src/app/[orgId]/settings/resources/public/[niceId]/providers/page.tsx +++ /dev/null @@ -1,230 +0,0 @@ -"use client"; - -import { - SettingsContainer, - SettingsFormCell, - SettingsFormGrid, - SettingsSection, - SettingsSectionBody, - SettingsSectionDescription, - SettingsSectionFooter, - SettingsSectionForm, - SettingsSectionHeader, - SettingsSectionTitle -} from "@app/components/Settings"; -import { - AiProvidersSelector, - type SelectedAiProvider -} from "@app/components/AiProvidersSelector"; -import { Button } from "@app/components/ui/button"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage -} from "@app/components/ui/form"; -import { useEnvContext } from "@app/hooks/useEnvContext"; -import { useResourceContext } from "@app/hooks/useResourceContext"; -import { toast } from "@app/hooks/useToast"; -import { createApiClient, formatAxiosError } from "@app/lib/api"; -import { resourceQueries } from "@app/lib/queries"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { useTranslations } from "next-intl"; -import { useRouter } from "next/navigation"; -import { useActionState, useEffect, useMemo, useState } from "react"; -import { useForm } from "react-hook-form"; -import { z } from "zod"; - -export default function PublicResourceProvidersPage() { - const t = useTranslations(); - const router = useRouter(); - const { env } = useEnvContext(); - const api = createApiClient({ env }); - const queryClient = useQueryClient(); - const { resource } = useResourceContext(); - - useEffect(() => { - if (resource.mode !== "inference") { - router.replace( - `/${resource.orgId}/settings/resources/public/${resource.niceId}/general` - ); - } - }, [router, resource.mode, resource.niceId, resource.orgId]); - - const formSchema = useMemo( - () => - z.object({ - providerIds: z.array(z.number().int().positive()) - }), - [] - ); - type FormValues = z.infer; - - const [selectedProviders, setSelectedProviders] = useState< - SelectedAiProvider[] - >([]); - - const attachedQuery = useQuery({ - ...resourceQueries.resourceAiProviders({ - resourceId: resource.resourceId - }), - enabled: resource.mode === "inference" - }); - - const form = useForm({ - resolver: zodResolver(formSchema), - defaultValues: { - providerIds: [] - } - }); - - useEffect(() => { - if (!attachedQuery.data) return; - const providers = attachedQuery.data.map((provider) => ({ - id: String(provider.providerId), - text: provider.name - })); - setSelectedProviders(providers); - form.reset({ - providerIds: attachedQuery.data.map((p) => p.providerId) - }); - }, [attachedQuery.data, form]); - - const [, formAction, saveLoading] = useActionState(async () => { - const isValid = await form.trigger(); - if (!isValid) return; - - const data = form.getValues(); - try { - await api.post(`/resource/${resource.resourceId}/ai-providers`, { - providers: data.providerIds.map((providerId) => ({ - providerId, - modelAccessMode: "catalog" - })) - }); - - await queryClient.invalidateQueries( - resourceQueries.resourceAiProviders({ - resourceId: resource.resourceId - }) - ); - - toast({ - title: t("success"), - description: t("aiResourceProvidersUpdated") - }); - } catch (error) { - toast({ - variant: "destructive", - title: t("aiResourceProvidersErrorUpdate"), - description: formatAxiosError( - error, - t("aiResourceProvidersErrorUpdate") - ) - }); - } - }, null); - - if (resource.mode !== "inference") { - return null; - } - - return ( - - - - - {t("aiResourceProviders")} - - - {t("aiResourceProvidersDescription")} - - - - - -
- - - - ( - - - {t( - "aiResourceProviders" - )} - - - { - setSelectedProviders( - providers - ); - form.setValue( - "providerIds", - providers.map( - (p) => - parseInt( - p.id, - 10 - ) - ), - { - shouldValidate: true - } - ); - }} - /> - - - {t( - "aiResourceProvidersHelp" - )} - - - - )} - /> - - -
- -
-
- - - - -
-
- ); -} diff --git a/src/lib/privateResourceForm.ts b/src/lib/privateResourceForm.ts index e336a45a1..3eb83cce3 100644 --- a/src/lib/privateResourceForm.ts +++ b/src/lib/privateResourceForm.ts @@ -305,7 +305,13 @@ export function buildUpdateSiteResourcePayload( typeof data.alias === "string" && data.alias.trim() ? data.alias - : null + : null, + domainId: data.httpConfigDomainId + ? data.httpConfigDomainId + : undefined, + subdomain: data.httpConfigSubdomain + ? data.httpConfigSubdomain + : undefined }), ...((data.mode === "host" || data.mode === "cidr") && { tcpPortRangeString: data.tcpPortRangeString,