mirror of
https://github.com/fosrl/pangolin.git
synced 2026-04-01 07:26:38 +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 { db, resources } from "@server/db";
|
||||||
import { z } from "zod";
|
|
||||||
import { db } from "@server/db";
|
|
||||||
import { Resource, resources, sites } from "@server/db";
|
|
||||||
import { eq, and } from "drizzle-orm";
|
|
||||||
import response from "@server/lib/response";
|
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 stoi from "@server/lib/stoi";
|
||||||
|
import logger from "@server/logger";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
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({
|
const getResourceSchema = z.strictObject({
|
||||||
resourceId: z
|
resourceId: z
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ function queryTargets(resourceId: number) {
|
|||||||
resourceId: targets.resourceId,
|
resourceId: targets.resourceId,
|
||||||
siteId: targets.siteId,
|
siteId: targets.siteId,
|
||||||
siteType: sites.type,
|
siteType: sites.type,
|
||||||
|
siteName: sites.name,
|
||||||
hcEnabled: targetHealthCheck.hcEnabled,
|
hcEnabled: targetHealthCheck.hcEnabled,
|
||||||
hcPath: targetHealthCheck.hcPath,
|
hcPath: targetHealthCheck.hcPath,
|
||||||
hcScheme: targetHealthCheck.hcScheme,
|
hcScheme: targetHealthCheck.hcScheme,
|
||||||
|
|||||||
@@ -124,20 +124,15 @@ export default function ReverseProxyTargetsPage(props: {
|
|||||||
resourceId: resource.resourceId
|
resourceId: resource.resourceId
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
const { data: sites = [], isLoading: isLoadingSites } = useQuery(
|
|
||||||
orgQueries.sites({
|
|
||||||
orgId: params.orgId
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isLoadingSites || isLoadingTargets) {
|
if (isLoadingTargets) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsContainer>
|
<SettingsContainer>
|
||||||
<ProxyResourceTargetsForm
|
<ProxyResourceTargetsForm
|
||||||
sites={sites}
|
orgId={params.orgId}
|
||||||
initialTargets={remoteTargets}
|
initialTargets={remoteTargets}
|
||||||
resource={resource}
|
resource={resource}
|
||||||
/>
|
/>
|
||||||
@@ -160,12 +155,12 @@ export default function ReverseProxyTargetsPage(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ProxyResourceTargetsForm({
|
function ProxyResourceTargetsForm({
|
||||||
sites,
|
orgId,
|
||||||
initialTargets,
|
initialTargets,
|
||||||
resource
|
resource
|
||||||
}: {
|
}: {
|
||||||
initialTargets: LocalTarget[];
|
initialTargets: LocalTarget[];
|
||||||
sites: ListSitesResponse["sites"];
|
orgId: string;
|
||||||
resource: GetResourceResponse;
|
resource: GetResourceResponse;
|
||||||
}) {
|
}) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
@@ -243,17 +238,21 @@ function ProxyResourceTargetsForm({
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const { data: sites = [] } = useQuery(
|
||||||
|
orgQueries.sites({
|
||||||
|
orgId
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const updateTarget = useCallback(
|
const updateTarget = useCallback(
|
||||||
(targetId: number, data: Partial<LocalTarget>) => {
|
(targetId: number, data: Partial<LocalTarget>) => {
|
||||||
setTargets((prevTargets) => {
|
setTargets((prevTargets) => {
|
||||||
const site = sites.find((site) => site.siteId === data.siteId);
|
|
||||||
return prevTargets.map((target) =>
|
return prevTargets.map((target) =>
|
||||||
target.targetId === targetId
|
target.targetId === targetId
|
||||||
? {
|
? {
|
||||||
...target,
|
...target,
|
||||||
...data,
|
...data,
|
||||||
updated: true,
|
updated: true
|
||||||
siteType: site ? site.type : target.siteType
|
|
||||||
}
|
}
|
||||||
: target
|
: target
|
||||||
);
|
);
|
||||||
@@ -453,7 +452,7 @@ function ProxyResourceTargetsForm({
|
|||||||
return (
|
return (
|
||||||
<ResourceTargetAddressItem
|
<ResourceTargetAddressItem
|
||||||
isHttp={isHttp}
|
isHttp={isHttp}
|
||||||
sites={sites}
|
orgId={orgId}
|
||||||
getDockerStateForSite={getDockerStateForSite}
|
getDockerStateForSite={getDockerStateForSite}
|
||||||
proxyTarget={row.original}
|
proxyTarget={row.original}
|
||||||
refreshContainersForSite={refreshContainersForSite}
|
refreshContainersForSite={refreshContainersForSite}
|
||||||
@@ -619,6 +618,7 @@ function ProxyResourceTargetsForm({
|
|||||||
method: isHttp ? "http" : null,
|
method: isHttp ? "http" : null,
|
||||||
port: 0,
|
port: 0,
|
||||||
siteId: sites.length > 0 ? sites[0].siteId : 0,
|
siteId: sites.length > 0 ? sites[0].siteId : 0,
|
||||||
|
siteName: sites.length > 0 ? sites[0].name : "",
|
||||||
path: isHttp ? null : null,
|
path: isHttp ? null : null,
|
||||||
pathMatchType: isHttp ? null : null,
|
pathMatchType: isHttp ? null : null,
|
||||||
rewritePath: isHttp ? null : null,
|
rewritePath: isHttp ? null : null,
|
||||||
|
|||||||
@@ -216,9 +216,7 @@ export default function Page() {
|
|||||||
const [remoteExitNodes, setRemoteExitNodes] = useState<
|
const [remoteExitNodes, setRemoteExitNodes] = useState<
|
||||||
ListRemoteExitNodesResponse["remoteExitNodes"]
|
ListRemoteExitNodesResponse["remoteExitNodes"]
|
||||||
>([]);
|
>([]);
|
||||||
const [loadingExitNodes, setLoadingExitNodes] = useState(
|
const [loadingExitNodes, setLoadingExitNodes] = useState(build === "saas");
|
||||||
build === "saas"
|
|
||||||
);
|
|
||||||
|
|
||||||
const [createLoading, setCreateLoading] = useState(false);
|
const [createLoading, setCreateLoading] = useState(false);
|
||||||
const [showSnippets, setShowSnippets] = useState(false);
|
const [showSnippets, setShowSnippets] = useState(false);
|
||||||
@@ -282,6 +280,7 @@ export default function Page() {
|
|||||||
method: isHttp ? "http" : null,
|
method: isHttp ? "http" : null,
|
||||||
port: 0,
|
port: 0,
|
||||||
siteId: sites.length > 0 ? sites[0].siteId : 0,
|
siteId: sites.length > 0 ? sites[0].siteId : 0,
|
||||||
|
siteName: sites.length > 0 ? sites[0].name : "",
|
||||||
path: isHttp ? null : null,
|
path: isHttp ? null : null,
|
||||||
pathMatchType: isHttp ? null : null,
|
pathMatchType: isHttp ? null : null,
|
||||||
rewritePath: isHttp ? null : null,
|
rewritePath: isHttp ? null : null,
|
||||||
@@ -336,8 +335,7 @@ export default function Page() {
|
|||||||
|
|
||||||
// In saas mode with no exit nodes, force HTTP
|
// In saas mode with no exit nodes, force HTTP
|
||||||
const showTypeSelector =
|
const showTypeSelector =
|
||||||
build !== "saas" ||
|
build !== "saas" || (!loadingExitNodes && remoteExitNodes.length > 0);
|
||||||
(!loadingExitNodes && remoteExitNodes.length > 0);
|
|
||||||
|
|
||||||
const baseForm = useForm({
|
const baseForm = useForm({
|
||||||
resolver: zodResolver(baseResourceFormSchema),
|
resolver: zodResolver(baseResourceFormSchema),
|
||||||
@@ -600,7 +598,10 @@ export default function Page() {
|
|||||||
toast({
|
toast({
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
title: t("resourceErrorCreate"),
|
title: t("resourceErrorCreate"),
|
||||||
description: formatAxiosError(e, t("resourceErrorCreateMessageDescription"))
|
description: formatAxiosError(
|
||||||
|
e,
|
||||||
|
t("resourceErrorCreateMessageDescription")
|
||||||
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -826,7 +827,8 @@ export default function Page() {
|
|||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<ResourceTargetAddressItem
|
<ResourceTargetAddressItem
|
||||||
isHttp={isHttp}
|
isHttp={isHttp}
|
||||||
sites={sites}
|
orgId={orgId!.toString()}
|
||||||
|
// sites={sites}
|
||||||
getDockerStateForSite={getDockerStateForSite}
|
getDockerStateForSite={getDockerStateForSite}
|
||||||
proxyTarget={row.original}
|
proxyTarget={row.original}
|
||||||
refreshContainersForSite={refreshContainersForSite}
|
refreshContainersForSite={refreshContainersForSite}
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import { cn } from "@app/lib/cn";
|
import { cn } from "@app/lib/cn";
|
||||||
import type { DockerState } from "@app/lib/docker";
|
import type { DockerState } from "@app/lib/docker";
|
||||||
import { parseHostTarget } from "@app/lib/parseHostTarget";
|
import { parseHostTarget } from "@app/lib/parseHostTarget";
|
||||||
|
import { orgQueries } from "@app/lib/queries";
|
||||||
import { CaretSortIcon } from "@radix-ui/react-icons";
|
import { CaretSortIcon } from "@radix-ui/react-icons";
|
||||||
import type { ListSitesResponse } from "@server/routers/site";
|
import type { ListSitesResponse } from "@server/routers/site";
|
||||||
import { type ListTargetsResponse } from "@server/routers/target";
|
import { type ListTargetsResponse } from "@server/routers/target";
|
||||||
import type { ArrayElement } from "@server/types/ArrayElement";
|
import type { ArrayElement } from "@server/types/ArrayElement";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { CheckIcon } from "lucide-react";
|
import { CheckIcon } from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useState } from "react";
|
||||||
import { ContainersSelector } from "./ContainersSelector";
|
import { ContainersSelector } from "./ContainersSelector";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -20,7 +23,6 @@ import {
|
|||||||
import { Input } from "./ui/input";
|
import { Input } from "./ui/input";
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
|
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger } from "./ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger } from "./ui/select";
|
||||||
import { useEffect } from "react";
|
|
||||||
|
|
||||||
type SiteWithUpdateAvailable = ListSitesResponse["sites"][number];
|
type SiteWithUpdateAvailable = ListSitesResponse["sites"][number];
|
||||||
|
|
||||||
@@ -36,14 +38,14 @@ export type LocalTarget = Omit<
|
|||||||
export type ResourceTargetAddressItemProps = {
|
export type ResourceTargetAddressItemProps = {
|
||||||
getDockerStateForSite: (siteId: number) => DockerState;
|
getDockerStateForSite: (siteId: number) => DockerState;
|
||||||
updateTarget: (targetId: number, data: Partial<LocalTarget>) => void;
|
updateTarget: (targetId: number, data: Partial<LocalTarget>) => void;
|
||||||
sites: SiteWithUpdateAvailable[];
|
orgId: string;
|
||||||
proxyTarget: LocalTarget;
|
proxyTarget: LocalTarget;
|
||||||
isHttp: boolean;
|
isHttp: boolean;
|
||||||
refreshContainersForSite: (siteId: number) => void;
|
refreshContainersForSite: (siteId: number) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ResourceTargetAddressItem({
|
export function ResourceTargetAddressItem({
|
||||||
sites,
|
orgId,
|
||||||
getDockerStateForSite,
|
getDockerStateForSite,
|
||||||
updateTarget,
|
updateTarget,
|
||||||
proxyTarget,
|
proxyTarget,
|
||||||
@@ -52,10 +54,34 @@ export function ResourceTargetAddressItem({
|
|||||||
}: ResourceTargetAddressItemProps) {
|
}: ResourceTargetAddressItemProps) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
|
||||||
const selectedSite = sites.find(
|
const [siteSearchQuery, setSiteSearchQuery] = useState("");
|
||||||
(site) => site.siteId === proxyTarget.siteId
|
|
||||||
|
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 = (
|
const handleContainerSelectForTarget = (
|
||||||
hostname: string,
|
hostname: string,
|
||||||
port?: number
|
port?: number
|
||||||
@@ -70,28 +96,23 @@ export function ResourceTargetAddressItem({
|
|||||||
return (
|
return (
|
||||||
<div className="flex items-center w-full" key={proxyTarget.targetId}>
|
<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">
|
<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 && selectedSite.type === "newt" && (
|
||||||
selectedSite.type === "newt" &&
|
<ContainersSelector
|
||||||
(() => {
|
site={selectedSite}
|
||||||
const dockerState = getDockerStateForSite(
|
containers={
|
||||||
selectedSite.siteId
|
getDockerStateForSite(selectedSite.siteId)
|
||||||
);
|
.containers
|
||||||
return (
|
}
|
||||||
<ContainersSelector
|
isAvailable={
|
||||||
site={selectedSite}
|
getDockerStateForSite(selectedSite.siteId)
|
||||||
containers={dockerState.containers}
|
.isAvailable
|
||||||
isAvailable={dockerState.isAvailable}
|
}
|
||||||
onContainerSelect={
|
onContainerSelect={handleContainerSelectForTarget}
|
||||||
handleContainerSelectForTarget
|
onRefresh={() =>
|
||||||
}
|
refreshContainersForSite(selectedSite.siteId)
|
||||||
onRefresh={() =>
|
}
|
||||||
refreshContainersForSite(
|
/>
|
||||||
selectedSite.siteId
|
)}
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
|
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
@@ -113,8 +134,11 @@ export function ResourceTargetAddressItem({
|
|||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="p-0 w-45">
|
<PopoverContent className="p-0 w-45">
|
||||||
<Command>
|
<Command shouldFilter={false}>
|
||||||
<CommandInput placeholder={t("siteSearch")} />
|
<CommandInput
|
||||||
|
placeholder={t("siteSearch")}
|
||||||
|
onValueChange={(v) => setSiteSearchQuery(v)}
|
||||||
|
/>
|
||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandEmpty>{t("siteNotFound")}</CommandEmpty>
|
<CommandEmpty>{t("siteNotFound")}</CommandEmpty>
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
@@ -122,14 +146,18 @@ export function ResourceTargetAddressItem({
|
|||||||
<CommandItem
|
<CommandItem
|
||||||
key={site.siteId}
|
key={site.siteId}
|
||||||
value={`${site.siteId}:${site.name}`}
|
value={`${site.siteId}:${site.name}`}
|
||||||
onSelect={() =>
|
onSelect={() => {
|
||||||
updateTarget(
|
updateTarget(
|
||||||
proxyTarget.targetId,
|
proxyTarget.targetId,
|
||||||
{
|
{
|
||||||
siteId: site.siteId
|
siteId: site.siteId,
|
||||||
|
siteType: site.type,
|
||||||
|
siteName: site.name
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
}
|
|
||||||
|
setSelectedSite(site);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<CheckIcon
|
<CheckIcon
|
||||||
className={cn(
|
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({
|
queryOptions({
|
||||||
queryKey: ["ORG", orgId, "SITES"] as const,
|
queryKey: ["ORG", orgId, "SITES", { query, perPage }] as const,
|
||||||
queryFn: async ({ signal, meta }) => {
|
queryFn: async ({ signal, meta }) => {
|
||||||
const sp = new URLSearchParams({
|
const sp = new URLSearchParams({
|
||||||
pageSize: "10000"
|
pageSize: perPage.toString()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (query?.trim()) {
|
||||||
|
sp.set("query", query);
|
||||||
|
}
|
||||||
|
|
||||||
const res = await meta!.api.get<
|
const res = await meta!.api.get<
|
||||||
AxiosResponse<ListSitesResponse>
|
AxiosResponse<ListSitesResponse>
|
||||||
>(`/org/${orgId}/sites?${sp.toString()}`, { signal });
|
>(`/org/${orgId}/sites?${sp.toString()}`, { signal });
|
||||||
|
|||||||
Reference in New Issue
Block a user