mirror of
https://github.com/fosrl/pangolin.git
synced 2026-08-07 04:58:44 +02:00
Add domain to the resource selection
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<typeof formSchema>;
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
@@ -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() {
|
||||
)}
|
||||
/>
|
||||
</SettingsFormCell>
|
||||
{isInference && (
|
||||
<SettingsFormCell span="half">
|
||||
<PrivateResourceAliasField
|
||||
control={asAnyControl(
|
||||
form.control
|
||||
)}
|
||||
watch={asAnyWatch(form.watch)}
|
||||
labelPrefix="edit"
|
||||
/>
|
||||
</SettingsFormCell>
|
||||
)}
|
||||
</SettingsFormGrid>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
@@ -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<typeof formSchema>;
|
||||
|
||||
const [selectedProviders, setSelectedProviders] = useState<
|
||||
SelectedAiProvider[]
|
||||
>([]);
|
||||
|
||||
const attachedQuery = useQuery({
|
||||
...resourceQueries.siteResourceAiProviders({
|
||||
siteResourceId: siteResource.id
|
||||
}),
|
||||
enabled: siteResource.mode === "inference"
|
||||
});
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
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 (
|
||||
<SettingsContainer>
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("aiResourceProviders")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("aiResourceProvidersDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
|
||||
<SettingsSectionBody>
|
||||
<SettingsSectionForm variant="half">
|
||||
<Form {...form}>
|
||||
<form
|
||||
action={formAction}
|
||||
id="private-resource-providers-form"
|
||||
>
|
||||
<SettingsFormGrid>
|
||||
<SettingsFormCell span="full">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="providerIds"
|
||||
render={() => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"aiResourceProviders"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<AiProvidersSelector
|
||||
orgId={
|
||||
siteResource.orgId
|
||||
}
|
||||
selectedProviders={
|
||||
selectedProviders
|
||||
}
|
||||
disabled={
|
||||
attachedQuery.isLoading ||
|
||||
saveLoading
|
||||
}
|
||||
onSelectProviders={(
|
||||
providers
|
||||
) => {
|
||||
setSelectedProviders(
|
||||
providers
|
||||
);
|
||||
form.setValue(
|
||||
"providerIds",
|
||||
providers.map(
|
||||
(p) =>
|
||||
parseInt(
|
||||
p.id,
|
||||
10
|
||||
)
|
||||
),
|
||||
{
|
||||
shouldValidate: true
|
||||
}
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"aiResourceProvidersHelp"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</SettingsFormCell>
|
||||
|
||||
<SettingsFormCell span="full">
|
||||
<SettingsSubsectionHeader>
|
||||
<SettingsSubsectionTitle>
|
||||
{t(
|
||||
"aiResourceDomainConfiguration"
|
||||
)}
|
||||
</SettingsSubsectionTitle>
|
||||
<SettingsSubsectionDescription>
|
||||
{t(
|
||||
"aiResourceDomainConfigurationDescription"
|
||||
)}
|
||||
</SettingsSubsectionDescription>
|
||||
</SettingsSubsectionHeader>
|
||||
</SettingsFormCell>
|
||||
<SettingsFormCell span="full">
|
||||
<DomainPicker
|
||||
key={`inference-domain-${siteResource.id}`}
|
||||
orgId={siteResource.orgId}
|
||||
cols={2}
|
||||
hideFreeDomain
|
||||
defaultSubdomain={
|
||||
httpConfigSubdomain ??
|
||||
undefined
|
||||
}
|
||||
defaultDomainId={
|
||||
httpConfigDomainId ??
|
||||
undefined
|
||||
}
|
||||
defaultFullDomain={
|
||||
httpConfigFullDomain ??
|
||||
undefined
|
||||
}
|
||||
onDomainChange={(res) => {
|
||||
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
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</SettingsFormCell>
|
||||
</SettingsFormGrid>
|
||||
</form>
|
||||
</Form>
|
||||
</SettingsSectionForm>
|
||||
</SettingsSectionBody>
|
||||
|
||||
<SettingsSectionFooter>
|
||||
<Button
|
||||
type="submit"
|
||||
form="private-resource-providers-form"
|
||||
loading={saveLoading}
|
||||
disabled={attachedQuery.isLoading}
|
||||
>
|
||||
{t("saveSettings")}
|
||||
</Button>
|
||||
</SettingsSectionFooter>
|
||||
</SettingsSection>
|
||||
</SettingsContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
@@ -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<typeof formSchema>;
|
||||
|
||||
const [selectedProviders, setSelectedProviders] = useState<
|
||||
SelectedAiProvider[]
|
||||
>([]);
|
||||
|
||||
const attachedQuery = useQuery({
|
||||
...resourceQueries.siteResourceAiProviders({
|
||||
siteResourceId: siteResource.id
|
||||
}),
|
||||
enabled: siteResource.mode === "inference"
|
||||
});
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
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 (
|
||||
<SettingsContainer>
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("aiResourceProviders")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("aiResourceProvidersDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
|
||||
<SettingsSectionBody>
|
||||
<SettingsSectionForm variant="half">
|
||||
<Form {...form}>
|
||||
<form
|
||||
action={formAction}
|
||||
id="private-resource-providers-form"
|
||||
>
|
||||
<SettingsFormGrid>
|
||||
<SettingsFormCell span="full">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="providerIds"
|
||||
render={() => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"aiResourceProviders"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<AiProvidersSelector
|
||||
orgId={
|
||||
siteResource.orgId
|
||||
}
|
||||
selectedProviders={
|
||||
selectedProviders
|
||||
}
|
||||
disabled={
|
||||
attachedQuery.isLoading ||
|
||||
saveLoading
|
||||
}
|
||||
onSelectProviders={(
|
||||
providers
|
||||
) => {
|
||||
setSelectedProviders(
|
||||
providers
|
||||
);
|
||||
form.setValue(
|
||||
"providerIds",
|
||||
providers.map(
|
||||
(p) =>
|
||||
parseInt(
|
||||
p.id,
|
||||
10
|
||||
)
|
||||
),
|
||||
{
|
||||
shouldValidate: true
|
||||
}
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"aiResourceProvidersHelp"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</SettingsFormCell>
|
||||
</SettingsFormGrid>
|
||||
</form>
|
||||
</Form>
|
||||
</SettingsSectionForm>
|
||||
</SettingsSectionBody>
|
||||
|
||||
<SettingsSectionFooter>
|
||||
<Button
|
||||
type="submit"
|
||||
form="private-resource-providers-form"
|
||||
loading={saveLoading}
|
||||
disabled={attachedQuery.isLoading}
|
||||
>
|
||||
{t("saveSettings")}
|
||||
</Button>
|
||||
</SettingsSectionFooter>
|
||||
</SettingsSection>
|
||||
</SettingsContainer>
|
||||
);
|
||||
}
|
||||
@@ -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<typeof formSchema>;
|
||||
|
||||
const [selectedProviders, setSelectedProviders] = useState<
|
||||
SelectedAiProvider[]
|
||||
>([]);
|
||||
|
||||
const attachedQuery = useQuery({
|
||||
...resourceQueries.resourceAiProviders({
|
||||
resourceId: resource.resourceId
|
||||
}),
|
||||
enabled: resource.mode === "inference"
|
||||
});
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
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 (
|
||||
<SettingsContainer>
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("aiResourceProviders")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("aiResourceProvidersDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
|
||||
<SettingsSectionBody>
|
||||
<SettingsSectionForm variant="half">
|
||||
<Form {...form}>
|
||||
<form
|
||||
action={formAction}
|
||||
id="public-resource-providers-form"
|
||||
>
|
||||
<SettingsFormGrid>
|
||||
<SettingsFormCell span="full">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="providerIds"
|
||||
render={() => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"aiResourceProviders"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<AiProvidersSelector
|
||||
orgId={
|
||||
resource.orgId
|
||||
}
|
||||
selectedProviders={
|
||||
selectedProviders
|
||||
}
|
||||
disabled={
|
||||
attachedQuery.isLoading ||
|
||||
saveLoading
|
||||
}
|
||||
onSelectProviders={(
|
||||
providers
|
||||
) => {
|
||||
setSelectedProviders(
|
||||
providers
|
||||
);
|
||||
form.setValue(
|
||||
"providerIds",
|
||||
providers.map(
|
||||
(p) =>
|
||||
parseInt(
|
||||
p.id,
|
||||
10
|
||||
)
|
||||
),
|
||||
{
|
||||
shouldValidate: true
|
||||
}
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"aiResourceProvidersHelp"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</SettingsFormCell>
|
||||
</SettingsFormGrid>
|
||||
</form>
|
||||
</Form>
|
||||
</SettingsSectionForm>
|
||||
</SettingsSectionBody>
|
||||
|
||||
<SettingsSectionFooter>
|
||||
<Button
|
||||
type="submit"
|
||||
form="public-resource-providers-form"
|
||||
loading={saveLoading}
|
||||
disabled={attachedQuery.isLoading}
|
||||
>
|
||||
{t("saveSettings")}
|
||||
</Button>
|
||||
</SettingsSectionFooter>
|
||||
</SettingsSection>
|
||||
</SettingsContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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<typeof formSchema>;
|
||||
|
||||
const [selectedProviders, setSelectedProviders] = useState<
|
||||
SelectedAiProvider[]
|
||||
>([]);
|
||||
|
||||
const attachedQuery = useQuery({
|
||||
...resourceQueries.resourceAiProviders({
|
||||
resourceId: resource.resourceId
|
||||
}),
|
||||
enabled: resource.mode === "inference"
|
||||
});
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
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 (
|
||||
<SettingsContainer>
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("aiResourceProviders")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("aiResourceProvidersDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
|
||||
<SettingsSectionBody>
|
||||
<SettingsSectionForm variant="half">
|
||||
<Form {...form}>
|
||||
<form
|
||||
action={formAction}
|
||||
id="public-resource-providers-form"
|
||||
>
|
||||
<SettingsFormGrid>
|
||||
<SettingsFormCell span="full">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="providerIds"
|
||||
render={() => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"aiResourceProviders"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<AiProvidersSelector
|
||||
orgId={
|
||||
resource.orgId
|
||||
}
|
||||
selectedProviders={
|
||||
selectedProviders
|
||||
}
|
||||
disabled={
|
||||
attachedQuery.isLoading ||
|
||||
saveLoading
|
||||
}
|
||||
onSelectProviders={(
|
||||
providers
|
||||
) => {
|
||||
setSelectedProviders(
|
||||
providers
|
||||
);
|
||||
form.setValue(
|
||||
"providerIds",
|
||||
providers.map(
|
||||
(p) =>
|
||||
parseInt(
|
||||
p.id,
|
||||
10
|
||||
)
|
||||
),
|
||||
{
|
||||
shouldValidate: true
|
||||
}
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"aiResourceProvidersHelp"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</SettingsFormCell>
|
||||
</SettingsFormGrid>
|
||||
</form>
|
||||
</Form>
|
||||
</SettingsSectionForm>
|
||||
</SettingsSectionBody>
|
||||
|
||||
<SettingsSectionFooter>
|
||||
<Button
|
||||
type="submit"
|
||||
form="public-resource-providers-form"
|
||||
loading={saveLoading}
|
||||
disabled={attachedQuery.isLoading}
|
||||
>
|
||||
{t("saveSettings")}
|
||||
</Button>
|
||||
</SettingsSectionFooter>
|
||||
</SettingsSection>
|
||||
</SettingsContainer>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user