add multi site selector for ha on private resources

This commit is contained in:
miloschwartz
2026-04-12 12:17:45 -07:00
parent 83ecf53776
commit 1564c4bee7
12 changed files with 356 additions and 79 deletions

View File

@@ -46,7 +46,11 @@ import { useTranslations } from "next-intl";
import { useEffect, useRef, useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { SitesSelector, type Selectedsite } from "./site-selector";
import {
MultiSitesSelector,
formatMultiSitesSelectorLabel
} from "./multi-site-selector";
import type { Selectedsite } from "./site-selector";
import { CaretSortIcon } from "@radix-ui/react-icons";
import { MachinesSelector } from "./machines-selector";
import DomainPicker from "@app/components/DomainPicker";
@@ -153,9 +157,32 @@ export type InternalResourceData = {
const tagSchema = z.object({ id: z.string(), text: z.string() });
function buildSelectedSitesForResource(
resource: InternalResourceData,
catalog: Site[]
): Selectedsite[] {
const fromCatalog = catalog.find((s) => s.siteId === resource.siteId);
if (fromCatalog) {
return [
{
name: fromCatalog.name,
siteId: fromCatalog.siteId,
type: fromCatalog.type
}
];
}
return [
{
name: resource.siteName,
siteId: resource.siteId,
type: "newt"
}
];
}
export type InternalResourceFormValues = {
name: string;
siteId: number;
siteIds: number[];
mode: InternalResourceMode;
destination: string;
alias?: string | null;
@@ -272,13 +299,14 @@ export function InternalResourceForm({
? "createInternalResourceDialogHttpConfigurationDescription"
: "editInternalResourceDialogHttpConfigurationDescription";
const siteIdsSchema = siteRequiredKey
? z.array(z.number().int().positive()).min(1, t(siteRequiredKey))
: z.array(z.number().int().positive()).min(1);
const formSchema = z
.object({
name: z.string().min(1, t(nameRequiredKey)).max(255, t(nameMaxKey)),
siteId: z
.number()
.int()
.positive(siteRequiredKey ? t(siteRequiredKey) : undefined),
siteIds: siteIdsSchema,
mode: z.enum(["host", "cidr", "http"]),
destination: z
.string()
@@ -467,7 +495,7 @@ export function InternalResourceForm({
variant === "edit" && resource
? {
name: resource.name,
siteId: resource.siteId,
siteIds: [resource.siteId],
mode: resource.mode ?? "host",
destination: resource.destination ?? "",
alias: resource.alias ?? null,
@@ -489,7 +517,7 @@ export function InternalResourceForm({
}
: {
name: "",
siteId: availableSites[0]?.siteId ?? 0,
siteIds: availableSites[0] ? [availableSites[0].siteId] : [],
mode: "host",
destination: "",
alias: null,
@@ -509,8 +537,18 @@ export function InternalResourceForm({
clients: []
};
const [selectedSite, setSelectedSite] = useState<Selectedsite>(
availableSites[0]
const [selectedSites, setSelectedSites] = useState<Selectedsite[]>(() =>
variant === "edit" && resource
? buildSelectedSitesForResource(resource, sites)
: availableSites[0]
? [
{
name: availableSites[0].name,
siteId: availableSites[0].siteId,
type: availableSites[0].type
}
]
: []
);
const form = useForm<FormData>({
@@ -542,7 +580,7 @@ export function InternalResourceForm({
if (variant === "create" && open) {
form.reset({
name: "",
siteId: availableSites[0]?.siteId ?? 0,
siteIds: availableSites[0] ? [availableSites[0].siteId] : [],
mode: "host",
destination: "",
alias: null,
@@ -561,12 +599,23 @@ export function InternalResourceForm({
users: [],
clients: []
});
setSelectedSites(
availableSites[0]
? [
{
name: availableSites[0].name,
siteId: availableSites[0].siteId,
type: availableSites[0].type
}
]
: []
);
setTcpPortMode("all");
setUdpPortMode("all");
setTcpCustomPorts("");
setUdpCustomPorts("");
}
}, [variant, open]);
}, [variant, open, form, sites]);
// Reset when edit dialog opens / resource changes
useEffect(() => {
@@ -575,7 +624,7 @@ export function InternalResourceForm({
if (resourceChanged) {
form.reset({
name: resource.name,
siteId: resource.siteId,
siteIds: [resource.siteId],
mode: resource.mode ?? "host",
destination: resource.destination ?? "",
alias: resource.alias ?? null,
@@ -594,6 +643,9 @@ export function InternalResourceForm({
users: [],
clients: []
});
setSelectedSites(
buildSelectedSitesForResource(resource, sites)
);
setTcpPortMode(
getPortModeFromString(resource.tcpPortRangeString)
);
@@ -615,7 +667,7 @@ export function InternalResourceForm({
previousResourceId.current = resource.id;
}
}
}, [variant, resource, form]);
}, [variant, resource, form, sites]);
// When edit dialog closes, clear previousResourceId so next open (for any resource) resets from fresh data
useEffect(() => {
@@ -651,8 +703,10 @@ export function InternalResourceForm({
<Form {...form}>
<form
onSubmit={form.handleSubmit((values) => {
const siteIds = values.siteIds;
onSubmit({
...values,
siteIds,
clients: (values.clients ?? []).map((c) => ({
id: c.clientId.toString(),
text: c.name
@@ -729,11 +783,11 @@ export function InternalResourceForm({
<div className="min-w-0 col-span-1">
<FormField
control={form.control}
name="siteId"
name="siteIds"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>
{t("site")}
{t("sites")}
</FormLabel>
<Popover>
<PopoverTrigger asChild>
@@ -743,40 +797,41 @@ export function InternalResourceForm({
role="combobox"
className={cn(
"w-full justify-between",
!field.value &&
selectedSites.length ===
0 &&
"text-muted-foreground"
)}
>
{field.value
? availableSites.find(
(s) =>
s.siteId ===
field.value
)?.name
: t(
"selectSite"
)}
<span className="truncate text-left">
{formatMultiSitesSelectorLabel(
selectedSites,
t
)}
</span>
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-full p-0">
<SitesSelector
<MultiSitesSelector
orgId={orgId}
selectedSite={
selectedSite
selectedSites={
selectedSites
}
filterTypes={[
"newt"
]}
onSelectSite={(
site
onSelectionChange={(
sites
) => {
setSelectedSite(
site
setSelectedSites(
sites
);
field.onChange(
site.siteId
sites.map(
(s) =>
s.siteId
)
);
}}
/>