♻️ filter sites server side in resource target

This commit is contained in:
Fred KISSIE
2026-03-17 04:07:02 +01:00
parent bab09dff95
commit 18ed38889f
6 changed files with 107 additions and 65 deletions

View File

@@ -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

View File

@@ -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,

View File

@@ -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 (
<SettingsContainer>
<ProxyResourceTargetsForm
sites={sites}
orgId={params.orgId}
initialTargets={remoteTargets}
resource={resource}
/>
@@ -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<LocalTarget>) => {
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 (
<ResourceTargetAddressItem
isHttp={isHttp}
sites={sites}
orgId={orgId}
getDockerStateForSite={getDockerStateForSite}
proxyTarget={row.original}
refreshContainersForSite={refreshContainersForSite}
@@ -619,6 +618,7 @@ function ProxyResourceTargetsForm({
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,

View File

@@ -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 }) => (
<ResourceTargetAddressItem
isHttp={isHttp}
sites={sites}
orgId={orgId!.toString()}
// sites={sites}
getDockerStateForSite={getDockerStateForSite}
proxyTarget={row.original}
refreshContainersForSite={refreshContainersForSite}

View File

@@ -1,12 +1,15 @@
import { cn } from "@app/lib/cn";
import type { DockerState } from "@app/lib/docker";
import { parseHostTarget } from "@app/lib/parseHostTarget";
import { orgQueries } from "@app/lib/queries";
import { CaretSortIcon } from "@radix-ui/react-icons";
import type { ListSitesResponse } from "@server/routers/site";
import { type ListTargetsResponse } from "@server/routers/target";
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 { ContainersSelector } from "./ContainersSelector";
import { Button } from "./ui/button";
import {
@@ -20,7 +23,6 @@ import {
import { Input } from "./ui/input";
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
import { Select, SelectContent, SelectItem, SelectTrigger } from "./ui/select";
import { useEffect } from "react";
type SiteWithUpdateAvailable = ListSitesResponse["sites"][number];
@@ -36,14 +38,14 @@ export type LocalTarget = Omit<
export type ResourceTargetAddressItemProps = {
getDockerStateForSite: (siteId: number) => DockerState;
updateTarget: (targetId: number, data: Partial<LocalTarget>) => 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<Pick<
SiteWithUpdateAvailable,
"name" | "siteId" | "type"
> | 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 (
<div className="flex items-center w-full" key={proxyTarget.targetId}>
<div className="flex items-center w-full justify-start py-0 space-x-2 px-0 cursor-default border border-input rounded-md">
{selectedSite &&
selectedSite.type === "newt" &&
(() => {
const dockerState = getDockerStateForSite(
selectedSite.siteId
);
return (
<ContainersSelector
site={selectedSite}
containers={dockerState.containers}
isAvailable={dockerState.isAvailable}
onContainerSelect={
handleContainerSelectForTarget
}
onRefresh={() =>
refreshContainersForSite(
selectedSite.siteId
)
}
/>
);
})()}
{selectedSite && selectedSite.type === "newt" && (
<ContainersSelector
site={selectedSite}
containers={
getDockerStateForSite(selectedSite.siteId)
.containers
}
isAvailable={
getDockerStateForSite(selectedSite.siteId)
.isAvailable
}
onContainerSelect={handleContainerSelectForTarget}
onRefresh={() =>
refreshContainersForSite(selectedSite.siteId)
}
/>
)}
<Popover>
<PopoverTrigger asChild>
@@ -113,8 +134,11 @@ export function ResourceTargetAddressItem({
</Button>
</PopoverTrigger>
<PopoverContent className="p-0 w-45">
<Command>
<CommandInput placeholder={t("siteSearch")} />
<Command shouldFilter={false}>
<CommandInput
placeholder={t("siteSearch")}
onValueChange={(v) => setSiteSearchQuery(v)}
/>
<CommandList>
<CommandEmpty>{t("siteNotFound")}</CommandEmpty>
<CommandGroup>
@@ -122,14 +146,18 @@ export function ResourceTargetAddressItem({
<CommandItem
key={site.siteId}
value={`${site.siteId}:${site.name}`}
onSelect={() =>
onSelect={() => {
updateTarget(
proxyTarget.targetId,
{
siteId: site.siteId
siteId: site.siteId,
siteType: site.type,
siteName: site.name
}
)
}
);
setSelectedSite(site);
}}
>
<CheckIcon
className={cn(

View File

@@ -130,14 +130,26 @@ export const orgQueries = {
}
}),
sites: ({ orgId }: { orgId: string }) =>
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<ListSitesResponse>
>(`/org/${orgId}/sites?${sp.toString()}`, { signal });