mirror of
https://github.com/fosrl/pangolin.git
synced 2026-04-05 17:36:36 +00:00
Merge pull request #2670 from Fredkiss3/feat/selector-filtering
Feat: selector filtering
This commit is contained in:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user