mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-30 06:26:39 +00:00
♻️ filter sites server side in resource target
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user