From 18ed38889fa19ad32009043b48652458a959e669 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Tue, 17 Mar 2026 04:07:02 +0100 Subject: [PATCH 01/10] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20filter=20sites=20ser?= =?UTF-8?q?ver=20side=20in=20resource=20target?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/routers/resource/getResource.ts | 17 ++-- server/routers/target/listTargets.ts | 1 + .../resources/proxy/[niceId]/proxy/page.tsx | 26 ++--- .../settings/resources/proxy/create/page.tsx | 16 ++-- .../resource-target-address-item.tsx | 94 ++++++++++++------- src/lib/queries.ts | 18 +++- 6 files changed, 107 insertions(+), 65 deletions(-) diff --git a/server/routers/resource/getResource.ts b/server/routers/resource/getResource.ts index cd870dcbf..7a52c0a85 100644 --- a/server/routers/resource/getResource.ts +++ b/server/routers/resource/getResource.ts @@ -1,15 +1,14 @@ -import { Request, Response, NextFunction } from "express"; -import { z } from "zod"; -import { db } from "@server/db"; -import { Resource, resources, sites } from "@server/db"; -import { eq, and } from "drizzle-orm"; +import { db, resources } from "@server/db"; import response from "@server/lib/response"; -import HttpCode from "@server/types/HttpCode"; -import createHttpError from "http-errors"; -import { fromError } from "zod-validation-error"; -import logger from "@server/logger"; import stoi from "@server/lib/stoi"; +import logger from "@server/logger"; import { OpenAPITags, registry } from "@server/openApi"; +import HttpCode from "@server/types/HttpCode"; +import { and, eq } from "drizzle-orm"; +import { NextFunction, Request, Response } from "express"; +import createHttpError from "http-errors"; +import { z } from "zod"; +import { fromError } from "zod-validation-error"; const getResourceSchema = z.strictObject({ resourceId: z diff --git a/server/routers/target/listTargets.ts b/server/routers/target/listTargets.ts index e4ef45f3b..18e932afa 100644 --- a/server/routers/target/listTargets.ts +++ b/server/routers/target/listTargets.ts @@ -40,6 +40,7 @@ function queryTargets(resourceId: number) { resourceId: targets.resourceId, siteId: targets.siteId, siteType: sites.type, + siteName: sites.name, hcEnabled: targetHealthCheck.hcEnabled, hcPath: targetHealthCheck.hcPath, hcScheme: targetHealthCheck.hcScheme, diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx index 51f11a2c3..aff10dc52 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx @@ -124,20 +124,15 @@ export default function ReverseProxyTargetsPage(props: { resourceId: resource.resourceId }) ); - const { data: sites = [], isLoading: isLoadingSites } = useQuery( - orgQueries.sites({ - orgId: params.orgId - }) - ); - if (isLoadingSites || isLoadingTargets) { + if (isLoadingTargets) { return null; } return ( @@ -160,12 +155,12 @@ export default function ReverseProxyTargetsPage(props: { } function ProxyResourceTargetsForm({ - sites, + orgId, initialTargets, resource }: { initialTargets: LocalTarget[]; - sites: ListSitesResponse["sites"]; + orgId: string; resource: GetResourceResponse; }) { const t = useTranslations(); @@ -243,17 +238,21 @@ function ProxyResourceTargetsForm({ }); }, []); + const { data: sites = [] } = useQuery( + orgQueries.sites({ + orgId + }) + ); + const updateTarget = useCallback( (targetId: number, data: Partial) => { setTargets((prevTargets) => { - const site = sites.find((site) => site.siteId === data.siteId); return prevTargets.map((target) => target.targetId === targetId ? { ...target, ...data, - updated: true, - siteType: site ? site.type : target.siteType + updated: true } : target ); @@ -453,7 +452,7 @@ function ProxyResourceTargetsForm({ return ( 0 ? sites[0].siteId : 0, + siteName: sites.length > 0 ? sites[0].name : "", path: isHttp ? null : null, pathMatchType: isHttp ? null : null, rewritePath: isHttp ? null : null, diff --git a/src/app/[orgId]/settings/resources/proxy/create/page.tsx b/src/app/[orgId]/settings/resources/proxy/create/page.tsx index 4c8cb8443..9a9eb3ba2 100644 --- a/src/app/[orgId]/settings/resources/proxy/create/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/create/page.tsx @@ -216,9 +216,7 @@ export default function Page() { const [remoteExitNodes, setRemoteExitNodes] = useState< ListRemoteExitNodesResponse["remoteExitNodes"] >([]); - const [loadingExitNodes, setLoadingExitNodes] = useState( - build === "saas" - ); + const [loadingExitNodes, setLoadingExitNodes] = useState(build === "saas"); const [createLoading, setCreateLoading] = useState(false); const [showSnippets, setShowSnippets] = useState(false); @@ -282,6 +280,7 @@ export default function Page() { method: isHttp ? "http" : null, port: 0, siteId: sites.length > 0 ? sites[0].siteId : 0, + siteName: sites.length > 0 ? sites[0].name : "", path: isHttp ? null : null, pathMatchType: isHttp ? null : null, rewritePath: isHttp ? null : null, @@ -336,8 +335,7 @@ export default function Page() { // In saas mode with no exit nodes, force HTTP const showTypeSelector = - build !== "saas" || - (!loadingExitNodes && remoteExitNodes.length > 0); + build !== "saas" || (!loadingExitNodes && remoteExitNodes.length > 0); const baseForm = useForm({ resolver: zodResolver(baseResourceFormSchema), @@ -600,7 +598,10 @@ export default function Page() { toast({ variant: "destructive", title: t("resourceErrorCreate"), - description: formatAxiosError(e, t("resourceErrorCreateMessageDescription")) + description: formatAxiosError( + e, + t("resourceErrorCreateMessageDescription") + ) }); } @@ -826,7 +827,8 @@ export default function Page() { cell: ({ row }) => ( DockerState; updateTarget: (targetId: number, data: Partial) => void; - sites: SiteWithUpdateAvailable[]; + orgId: string; proxyTarget: LocalTarget; isHttp: boolean; refreshContainersForSite: (siteId: number) => void; }; export function ResourceTargetAddressItem({ - sites, + orgId, getDockerStateForSite, updateTarget, proxyTarget, @@ -52,10 +54,34 @@ export function ResourceTargetAddressItem({ }: ResourceTargetAddressItemProps) { const t = useTranslations(); - const selectedSite = sites.find( - (site) => site.siteId === proxyTarget.siteId + const [siteSearchQuery, setSiteSearchQuery] = useState(""); + + const { data: sites = [] } = useQuery( + orgQueries.sites({ + orgId, + query: siteSearchQuery, + perPage: 10 + }) ); + const [selectedSite, setSelectedSite] = useState | null>(() => { + if ( + proxyTarget.siteName && + proxyTarget.siteType && + proxyTarget.siteId + ) { + return { + name: proxyTarget.siteName, + siteId: proxyTarget.siteId, + type: proxyTarget.siteType + }; + } + return null; + }); + const handleContainerSelectForTarget = ( hostname: string, port?: number @@ -70,28 +96,23 @@ export function ResourceTargetAddressItem({ return (
- {selectedSite && - selectedSite.type === "newt" && - (() => { - const dockerState = getDockerStateForSite( - selectedSite.siteId - ); - return ( - - refreshContainersForSite( - selectedSite.siteId - ) - } - /> - ); - })()} + {selectedSite && selectedSite.type === "newt" && ( + + refreshContainersForSite(selectedSite.siteId) + } + /> + )} @@ -113,8 +134,11 @@ export function ResourceTargetAddressItem({ - - + + setSiteSearchQuery(v)} + /> {t("siteNotFound")} @@ -122,14 +146,18 @@ export function ResourceTargetAddressItem({ + onSelect={() => { updateTarget( proxyTarget.targetId, { - siteId: site.siteId + siteId: site.siteId, + siteType: site.type, + siteName: site.name } - ) - } + ); + + setSelectedSite(site); + }} > + sites: ({ + orgId, + query, + perPage = 10_000 + }: { + orgId: string; + query?: string; + perPage?: number; + }) => queryOptions({ - queryKey: ["ORG", orgId, "SITES"] as const, + queryKey: ["ORG", orgId, "SITES", { query, perPage }] as const, queryFn: async ({ signal, meta }) => { const sp = new URLSearchParams({ - pageSize: "10000" + pageSize: perPage.toString() }); + if (query?.trim()) { + sp.set("query", query); + } + const res = await meta!.api.get< AxiosResponse >(`/org/${orgId}/sites?${sp.toString()}`, { signal }); From 435cae06a2f5472b228496b2cf37c5f9aa2d9579 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Tue, 17 Mar 2026 04:16:24 +0100 Subject: [PATCH 02/10] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resource-target-address-item.tsx | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/components/resource-target-address-item.tsx b/src/components/resource-target-address-item.tsx index 2e06c99a9..dfefc3bf2 100644 --- a/src/components/resource-target-address-item.tsx +++ b/src/components/resource-target-address-item.tsx @@ -9,7 +9,7 @@ import type { ArrayElement } from "@server/types/ArrayElement"; import { useQuery } from "@tanstack/react-query"; import { CheckIcon } from "lucide-react"; import { useTranslations } from "next-intl"; -import { useState } from "react"; +import { useMemo, useState } from "react"; import { ContainersSelector } from "./ContainersSelector"; import { Button } from "./ui/button"; import { @@ -82,6 +82,22 @@ export function ResourceTargetAddressItem({ return null; }); + const sitesShown = useMemo(() => { + const allSites: Array< + Pick + > = [...sites]; + if ( + selectedSite !== null && + !( + allSites.find((site) => site.siteId)?.siteId === + selectedSite?.siteId + ) + ) { + allSites.unshift(selectedSite); + } + return allSites; + }, [sites, selectedSite]); + const handleContainerSelectForTarget = ( hostname: string, port?: number @@ -137,12 +153,13 @@ export function ResourceTargetAddressItem({ setSiteSearchQuery(v)} /> {t("siteNotFound")} - {sites.map((site) => ( + {sitesShown.map((site) => ( Date: Thu, 19 Mar 2026 00:35:26 +0100 Subject: [PATCH 03/10] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=20make=20site=20sel?= =?UTF-8?q?ector=20popover=20its=20own=20component?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resource-target-address-item.tsx | 80 +++------------- src/components/site-selector.tsx | 91 +++++++++++++++++++ 2 files changed, 104 insertions(+), 67 deletions(-) create mode 100644 src/components/site-selector.tsx diff --git a/src/components/resource-target-address-item.tsx b/src/components/resource-target-address-item.tsx index dfefc3bf2..851b64b54 100644 --- a/src/components/resource-target-address-item.tsx +++ b/src/components/resource-target-address-item.tsx @@ -23,6 +23,7 @@ import { import { Input } from "./ui/input"; import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; import { Select, SelectContent, SelectItem, SelectTrigger } from "./ui/select"; +import { SitesSelector } from "./site-selector"; type SiteWithUpdateAvailable = ListSitesResponse["sites"][number]; @@ -54,16 +55,6 @@ export function ResourceTargetAddressItem({ }: ResourceTargetAddressItemProps) { const t = useTranslations(); - const [siteSearchQuery, setSiteSearchQuery] = useState(""); - - const { data: sites = [] } = useQuery( - orgQueries.sites({ - orgId, - query: siteSearchQuery, - perPage: 10 - }) - ); - const [selectedSite, setSelectedSite] = useState { - const allSites: Array< - Pick - > = [...sites]; - if ( - selectedSite !== null && - !( - allSites.find((site) => site.siteId)?.siteId === - selectedSite?.siteId - ) - ) { - allSites.unshift(selectedSite); - } - return allSites; - }, [sites, selectedSite]); - const handleContainerSelectForTarget = ( hostname: string, port?: number @@ -150,47 +125,18 @@ export function ResourceTargetAddressItem({ - - setSiteSearchQuery(v)} - /> - - {t("siteNotFound")} - - {sitesShown.map((site) => ( - { - updateTarget( - proxyTarget.targetId, - { - siteId: site.siteId, - siteType: site.type, - siteName: site.name - } - ); - - setSelectedSite(site); - }} - > - - {site.name} - - ))} - - - + { + updateTarget(proxyTarget.targetId, { + siteId: site.siteId, + siteType: site.type, + siteName: site.name + }); + setSelectedSite(site); + }} + /> diff --git a/src/components/site-selector.tsx b/src/components/site-selector.tsx new file mode 100644 index 000000000..9b7d44034 --- /dev/null +++ b/src/components/site-selector.tsx @@ -0,0 +1,91 @@ +import { orgQueries } from "@app/lib/queries"; +import type { ListSitesResponse } from "@server/routers/site"; +import { useQuery } from "@tanstack/react-query"; +import { useMemo, useState } from "react"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList +} from "./ui/command"; +import { cn } from "@app/lib/cn"; +import { CheckIcon } from "lucide-react"; +import { useTranslations } from "next-intl"; +import { useDebounce } from "use-debounce"; + +type Selectedsite = Pick< + ListSitesResponse["sites"][number], + "name" | "siteId" | "type" +>; + +export type SitesSelectorProps = { + orgId: string; + selectedSite?: Selectedsite | null; + onSelectSite: (selected: Selectedsite) => void; +}; + +export function SitesSelector({ + orgId, + selectedSite, + onSelectSite +}: SitesSelectorProps) { + const t = useTranslations(); + const [siteSearchQuery, setSiteSearchQuery] = useState(""); + const [debouncedQuery] = useDebounce(siteSearchQuery, 150); + + const { data: sites = [] } = useQuery( + orgQueries.sites({ + orgId, + query: debouncedQuery, + perPage: 10 + }) + ); + + // always include the selected site in the list of sites shown + const sitesShown = useMemo(() => { + const allSites: Array = [...sites]; + if ( + selectedSite && + !allSites.find((site) => site.siteId === selectedSite?.siteId) + ) { + allSites.unshift(selectedSite); + } + return allSites; + }, [sites, selectedSite]); + + return ( + + setSiteSearchQuery(v)} + /> + + {t("siteNotFound")} + + {sitesShown.map((site) => ( + { + onSelectSite(site); + }} + > + + {site.name} + + ))} + + + + ); +} From 8f33e25782b1dccb80997014d321bc2258b2a3c9 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Thu, 19 Mar 2026 01:18:27 +0100 Subject: [PATCH 04/10] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20use=20site=20selecto?= =?UTF-8?q?r=20on=20private=20resources?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/InternalResourceForm.tsx | 81 +++++++------------------ src/components/site-selector.tsx | 2 +- 2 files changed, 24 insertions(+), 59 deletions(-) diff --git a/src/components/InternalResourceForm.tsx b/src/components/InternalResourceForm.tsx index 6df1aceb7..fa1ee22d3 100644 --- a/src/components/InternalResourceForm.tsx +++ b/src/components/InternalResourceForm.tsx @@ -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,22 @@ 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"; // --- Helpers (shared) --- @@ -407,6 +400,10 @@ export function InternalResourceForm({ clients: [] }; + const [selectedSite, setSelectedSite] = useState( + availableSites[0] + ); + const form = useForm({ resolver: zodResolver(formSchema), defaultValues @@ -578,46 +575,14 @@ export function InternalResourceForm({ - - - - - {t("noSitesFound")} - - - {availableSites.map( - (site) => ( - - field.onChange( - site.siteId - ) - } - > - - {site.name} - - ) - )} - - - + { + setSelectedSite(site); + field.onChange(site.siteId); + }} + /> diff --git a/src/components/site-selector.tsx b/src/components/site-selector.tsx index 9b7d44034..0afd94c9a 100644 --- a/src/components/site-selector.tsx +++ b/src/components/site-selector.tsx @@ -15,7 +15,7 @@ import { CheckIcon } from "lucide-react"; import { useTranslations } from "next-intl"; import { useDebounce } from "use-debounce"; -type Selectedsite = Pick< +export type Selectedsite = Pick< ListSitesResponse["sites"][number], "name" | "siteId" | "type" >; From e15703164d231c284090ee04e86b48a7e357cfe5 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Thu, 19 Mar 2026 04:44:24 +0100 Subject: [PATCH 05/10] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20resource=20selector?= =?UTF-8?q?=20in=20create=20share=20link=20form?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/CreateShareLinkForm.tsx | 83 +++++++++++++++++--------- src/components/resource-selector.tsx | 81 +++++++++++++++++++++++++ src/lib/queries.ts | 18 +++++- 3 files changed, 151 insertions(+), 31 deletions(-) create mode 100644 src/components/resource-selector.tsx diff --git a/src/components/CreateShareLinkForm.tsx b/src/components/CreateShareLinkForm.tsx index 2f6f9aff2..fef1fb0e1 100644 --- a/src/components/CreateShareLinkForm.tsx +++ b/src/components/CreateShareLinkForm.tsx @@ -69,6 +69,7 @@ import { import AccessTokenSection from "@app/components/AccessTokenUsage"; import { useTranslations } from "next-intl"; import { toUnicode } from "punycode"; +import { ResourceSelector, type SelectedResource } from "./resource-selector"; type FormProps = { open: boolean; @@ -99,18 +100,21 @@ export default function CreateShareLinkForm({ orgQueries.resources({ orgId: org?.org.orgId ?? "" }) ); - const resources = useMemo( - () => - allResources - .filter((r) => r.http) - .map((r) => ({ - resourceId: r.resourceId, - name: r.name, - niceId: r.niceId, - resourceUrl: `${r.ssl ? "https://" : "http://"}${toUnicode(r.fullDomain || "")}/` - })), - [allResources] - ); + const [selectedResource, setSelectedResource] = + useState(null); + + // const resources = useMemo( + // () => + // allResources + // .filter((r) => r.http) + // .map((r) => ({ + // resourceId: r.resourceId, + // name: r.name, + // niceId: r.niceId, + // resourceUrl: `${r.ssl ? "https://" : "http://"}${toUnicode(r.fullDomain || "")}/` + // })), + // [allResources] + // ); const formSchema = z.object({ resourceId: z.number({ message: t("shareErrorSelectResource") }), @@ -199,15 +203,11 @@ export default function CreateShareLinkForm({ setAccessToken(token.accessToken); setAccessTokenId(token.accessTokenId); - const resource = resources.find( - (r) => r.resourceId === values.resourceId - ); - onCreated?.({ accessTokenId: token.accessTokenId, resourceId: token.resourceId, resourceName: values.resourceName, - resourceNiceId: resource ? resource.niceId : "", + resourceNiceId: selectedResource ? selectedResource.niceId : "", title: token.title, createdAt: token.createdAt, expiresAt: token.expiresAt @@ -217,10 +217,10 @@ export default function CreateShareLinkForm({ setLoading(false); } - function getSelectedResourceName(id: number) { - const resource = resources.find((r) => r.resourceId === id); - return `${resource?.name}`; - } + // function getSelectedResourceName(id: number) { + // const resource = resources.find((r) => r.resourceId === id); + // return `${resource?.name}`; + // } return ( <> @@ -241,7 +241,7 @@ export default function CreateShareLinkForm({ -
+
{!link && (
- {field.value - ? getSelectedResourceName( - field.value - ) + {selectedResource?.name + ? selectedResource.name : t( "resourceSelect" )} @@ -281,7 +279,7 @@ export default function CreateShareLinkForm({ - + {/* - + */} + + { + form.setValue( + "resourceId", + r.resourceId + ); + form.setValue( + "resourceName", + r.name + ); + form.setValue( + "resourceUrl", + `${r.ssl ? "https://" : "http://"}${toUnicode(r.fullDomain || "")}/` + ); + setSelectedResource( + r + ); + }} + /> diff --git a/src/components/resource-selector.tsx b/src/components/resource-selector.tsx new file mode 100644 index 000000000..a940894b0 --- /dev/null +++ b/src/components/resource-selector.tsx @@ -0,0 +1,81 @@ +import { orgQueries } from "@app/lib/queries"; +import { useQuery } from "@tanstack/react-query"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList +} from "./ui/command"; +import { useState } from "react"; +import { useTranslations } from "next-intl"; +import { CheckIcon } from "lucide-react"; +import { cn } from "@app/lib/cn"; +import type { ListResourcesResponse } from "@server/routers/resource"; +import { useDebounce } from "use-debounce"; + +export type SelectedResource = Pick< + ListResourcesResponse["resources"][number], + "name" | "resourceId" | "fullDomain" | "niceId" | "ssl" +>; + +export type ResourceSelectorProps = { + orgId: string; + selectedResource?: SelectedResource | null; + onSelectResource: (resource: SelectedResource) => void; +}; + +export function ResourceSelector({ + orgId, + selectedResource, + onSelectResource +}: ResourceSelectorProps) { + const t = useTranslations(); + const [resourceSearchQuery, setResourceSearchQuery] = useState(""); + + const [debouncedSearchQuery] = useDebounce(resourceSearchQuery, 150); + + const { data: resources = [] } = useQuery( + orgQueries.resources({ + orgId: orgId, + query: debouncedSearchQuery, + perPage: 10 + }) + ); + + return ( + + + + {t("resourcesNotFound")} + + {resources.map((r) => ( + { + onSelectResource(r); + }} + > + + {`${r.name}`} + + ))} + + + + ); +} diff --git a/src/lib/queries.ts b/src/lib/queries.ts index f6de28fc0..dfa706a88 100644 --- a/src/lib/queries.ts +++ b/src/lib/queries.ts @@ -191,14 +191,26 @@ export const orgQueries = { } }), - resources: ({ orgId }: { orgId: string }) => + resources: ({ + orgId, + query, + perPage = 10_000 + }: { + orgId: string; + query?: string; + perPage?: number; + }) => queryOptions({ - queryKey: ["ORG", orgId, "RESOURCES"] as const, + queryKey: ["ORG", orgId, "RESOURCES", { query, perPage }] as const, queryFn: async ({ signal, meta }) => { const sp = new URLSearchParams({ - pageSize: "10000" + pageSize: perPage.toString() }); + if (query?.trim()) { + sp.set("query", query); + } + const res = await meta!.api.get< AxiosResponse >(`/org/${orgId}/resources?${sp.toString()}`, { signal }); From ce58e71c440f40a8ea26d6d4d7546c19f16b658a Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Fri, 20 Mar 2026 03:59:10 +0100 Subject: [PATCH 06/10] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20make=20machine=20sel?= =?UTF-8?q?ector=20a=20multi-combobox?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- messages/en-US.json | 5 + src/components/CreateShareLinkForm.tsx | 59 ---------- src/components/InternalResourceForm.tsx | 140 ++++++++++++++---------- src/components/MachineClientsTable.tsx | 2 +- src/components/UserDevicesTable.tsx | 2 +- src/components/machine-selector.tsx | 108 ++++++++++++++++++ src/components/resource-selector.tsx | 19 +++- src/lib/queries.ts | 16 ++- 8 files changed, 231 insertions(+), 120 deletions(-) create mode 100644 src/components/machine-selector.tsx diff --git a/messages/en-US.json b/messages/en-US.json index 3d5352293..06be10e65 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -148,6 +148,11 @@ "createLink": "Create Link", "resourcesNotFound": "No resources found", "resourceSearch": "Search resources", + "machineSearch": "Search machines", + "machinesSearch": "Search machine clients...", + "machineNotFound": "No machines found", + "userDeviceSearch": "Search user devices", + "userDevicesSearch": "Search user devices...", "openMenu": "Open menu", "resource": "Resource", "title": "Title", diff --git a/src/components/CreateShareLinkForm.tsx b/src/components/CreateShareLinkForm.tsx index fef1fb0e1..d0e26a1c2 100644 --- a/src/components/CreateShareLinkForm.tsx +++ b/src/components/CreateShareLinkForm.tsx @@ -217,11 +217,6 @@ export default function CreateShareLinkForm({ setLoading(false); } - // function getSelectedResourceName(id: number) { - // const resource = resources.find((r) => r.resourceId === id); - // return `${resource?.name}`; - // } - return ( <> - {/* - - - - {t( - "resourcesNotFound" - )} - - - {resources.map( - ( - r - ) => ( - { - form.setValue( - "resourceId", - r.resourceId - ); - form.setValue( - "resourceName", - r.name - ); - form.setValue( - "resourceUrl", - r.resourceUrl - ); - }} - > - - {`${r.name}`} - - ) - )} - - - */} - ; @@ -252,7 +261,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 @@ -310,12 +319,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 }[]) + ]; } } @@ -592,8 +598,7 @@ export function InternalResourceForm({
-
+
-
+