Merge pull request #2670 from Fredkiss3/feat/selector-filtering

Feat: selector filtering
This commit is contained in:
Milo Schwartz
2026-03-29 12:26:51 -07:00
committed by GitHub
14 changed files with 597 additions and 297 deletions

View File

@@ -1,15 +1,10 @@
"use client";
import { HorizontalTabs } from "@app/components/HorizontalTabs";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import { StrategySelect } from "@app/components/StrategySelect";
import { Tag, TagInput } from "@app/components/tags/tag-input";
import { Button } from "@app/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList
} from "@app/components/ui/command";
import {
Form,
FormControl,
@@ -32,24 +27,24 @@ import {
SelectValue
} from "@app/components/ui/select";
import { Switch } from "@app/components/ui/switch";
import { getUserDisplayName } from "@app/lib/getUserDisplayName";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { cn } from "@app/lib/cn";
import { getUserDisplayName } from "@app/lib/getUserDisplayName";
import { orgQueries, resourceQueries } from "@app/lib/queries";
import { useQueries, useQuery } from "@tanstack/react-query";
import { zodResolver } from "@hookform/resolvers/zod";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { ListSitesResponse } from "@server/routers/site";
import { UserType } from "@server/types/UserTypes";
import { Check, ChevronsUpDown, ExternalLink } from "lucide-react";
import { useQuery } from "@tanstack/react-query";
import { ChevronsUpDown, ExternalLink } from "lucide-react";
import { useTranslations } from "next-intl";
import { useEffect, useRef, useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { HorizontalTabs } from "@app/components/HorizontalTabs";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import { StrategySelect } from "@app/components/StrategySelect";
import { SitesSelector, type Selectedsite } from "./site-selector";
import { CaretSortIcon } from "@radix-ui/react-icons";
import { MachinesSelector } from "./machines-selector";
// --- Helpers (shared) ---
@@ -258,7 +253,14 @@ export function InternalResourceForm({
authDaemonPort: z.number().int().positive().optional().nullable(),
roles: z.array(tagSchema).optional(),
users: z.array(tagSchema).optional(),
clients: z.array(tagSchema).optional()
clients: z
.array(
z.object({
clientId: z.number(),
name: z.string()
})
)
.optional()
});
type FormData = z.infer<typeof formSchema>;
@@ -267,7 +269,7 @@ export function InternalResourceForm({
const rolesQuery = useQuery(orgQueries.roles({ orgId }));
const usersQuery = useQuery(orgQueries.users({ orgId }));
const clientsQuery = useQuery(orgQueries.clients({ orgId }));
const clientsQuery = useQuery(orgQueries.machineClients({ orgId }));
const resourceRolesQuery = useQuery({
...resourceQueries.siteResourceRoles({
siteResourceId: siteResourceId ?? 0
@@ -325,12 +327,9 @@ export function InternalResourceForm({
}));
}
if (clientsData) {
existingClients = (
clientsData as { clientId: number; name: string }[]
).map((c) => ({
id: c.clientId.toString(),
text: c.name
}));
existingClients = [
...(clientsData as { clientId: number; name: string }[])
];
}
}
@@ -416,6 +415,10 @@ export function InternalResourceForm({
clients: []
};
const [selectedSite, setSelectedSite] = useState<Selectedsite>(
availableSites[0]
);
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues
@@ -537,9 +540,15 @@ export function InternalResourceForm({
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit((values) =>
onSubmit(values as InternalResourceFormValues)
)}
onSubmit={form.handleSubmit((values) => {
onSubmit({
...values,
clients: (values.clients ?? []).map((c) => ({
id: c.clientId.toString(),
text: c.name
}))
});
})}
className="space-y-6"
id={formId}
>
@@ -600,46 +609,14 @@ export function InternalResourceForm({
</FormControl>
</PopoverTrigger>
<PopoverContent className="w-full p-0">
<Command>
<CommandInput
placeholder={t("searchSites")}
/>
<CommandList>
<CommandEmpty>
{t("noSitesFound")}
</CommandEmpty>
<CommandGroup>
{availableSites.map(
(site) => (
<CommandItem
key={
site.siteId
}
value={
site.name
}
onSelect={() =>
field.onChange(
site.siteId
)
}
>
<Check
className={cn(
"mr-2 h-4 w-4",
field.value ===
site.siteId
? "opacity-100"
: "opacity-0"
)}
/>
{site.name}
</CommandItem>
)
)}
</CommandGroup>
</CommandList>
</Command>
<SitesSelector
orgId={orgId}
selectedSite={selectedSite}
onSelectSite={(site) => {
setSelectedSite(site);
field.onChange(site.siteId);
}}
/>
</PopoverContent>
</Popover>
<FormMessage />
@@ -649,8 +626,7 @@ export function InternalResourceForm({
</div>
<HorizontalTabs
clientSide={true}
defaultTab={0}
clientSide
items={[
{
title: t(
@@ -667,7 +643,7 @@ export function InternalResourceForm({
: [{ title: t("sshAccess"), href: "#" }])
]}
>
<div className="space-y-4 mt-4">
<div className="space-y-4 mt-4 p-1">
<div>
<div className="mb-8">
<label className="font-medium block">
@@ -1038,7 +1014,7 @@ export function InternalResourceForm({
</div>
</div>
<div className="space-y-4 mt-4">
<div className="space-y-4 mt-4 p-1">
<div className="mb-8">
<label className="font-medium block">
{t("editInternalResourceDialogAccessControl")}
@@ -1158,48 +1134,73 @@ export function InternalResourceForm({
<FormLabel>
{t("machineClients")}
</FormLabel>
<FormControl>
<TagInput
{...field}
activeTagIndex={
activeClientsTagIndex
}
setActiveTagIndex={
setActiveClientsTagIndex
}
placeholder={
t(
"accessClientSelect"
) ||
"Select machine clients"
}
size="sm"
tags={
form.getValues()
.clients ?? []
}
setTags={(newClients) =>
form.setValue(
"clients",
newClients as [
Tag,
...Tag[]
]
)
}
enableAutocomplete={
true
}
autocompleteOptions={
allClients
}
allowDuplicates={false}
restrictTagsToAutocompleteOptions={
true
}
sortTags={true}
/>
</FormControl>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
role="combobox"
className={cn(
"justify-between w-full",
"text-muted-foreground pl-1.5"
)}
>
<span
className={cn(
"inline-flex items-center gap-1",
"overflow-x-auto"
)}
>
{(
field.value ??
[]
).map(
(
client
) => (
<span
key={
client.clientId
}
className={cn(
"bg-muted-foreground/20 font-normal text-foreground rounded-sm",
"py-1 px-1.5 text-xs"
)}
>
{
client.name
}
</span>
)
)}
<span className="pl-1">
{t(
"accessClientSelect"
)}
</span>
</span>
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent className="p-0">
<MachinesSelector
selectedMachines={
field.value ??
[]
}
orgId={orgId}
onSelectMachines={(
machines
) => {
form.setValue(
"clients",
machines
);
}}
/>
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
)}
@@ -1211,7 +1212,7 @@ export function InternalResourceForm({
{/* SSH Access tab */}
{!disableEnterpriseFeatures && mode !== "cidr" && (
<div className="space-y-4 mt-4">
<div className="space-y-4 mt-4 p-1">
<PaidFeaturesAlert tiers={tierMatrix.sshPam} />
<div className="mb-8">
<label className="font-medium block">