mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-04 17:56:38 +00:00
change target config ui for create resource
This commit is contained in:
@@ -969,7 +969,7 @@ export default function ReverseProxyTargets(props: {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
<div className="flex items-center justify-center bg-gray-400 text-black px-1 h-9">
|
<div className="flex items-center justify-center bg-gray-200 text-black px-2 h-9">
|
||||||
{"://"}
|
{"://"}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1009,11 +1009,10 @@ export default function ReverseProxyTargets(props: {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center justify-center bg-gray-400 text-black px-1 h-9">
|
<div className="flex items-center justify-center bg-gray-200 text-black px-2 h-9">
|
||||||
{":"}
|
{":"}
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
|
||||||
placeholder="Port"
|
placeholder="Port"
|
||||||
defaultValue={row.original.port}
|
defaultValue={row.original.port}
|
||||||
className="min-w-[60px] pl-0 border-none placeholder-gray-400"
|
className="min-w-[60px] pl-0 border-none placeholder-gray-400"
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ import {
|
|||||||
} from "@app/components/ui/popover";
|
} from "@app/components/ui/popover";
|
||||||
import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons";
|
import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons";
|
||||||
import { cn } from "@app/lib/cn";
|
import { cn } from "@app/lib/cn";
|
||||||
import { ArrowRight, Info, MoveRight, Plus, SquareArrowOutUpRight } from "lucide-react";
|
import { ArrowRight, CircleCheck, CircleX, Info, MoveRight, Plus, SquareArrowOutUpRight } from "lucide-react";
|
||||||
import CopyTextBox from "@app/components/CopyTextBox";
|
import CopyTextBox from "@app/components/CopyTextBox";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
@@ -96,6 +96,7 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@app/c
|
|||||||
import { PathMatchDisplay, PathMatchModal, PathRewriteDisplay, PathRewriteModal } from "@app/components/PathMatchRenameModal";
|
import { PathMatchDisplay, PathMatchModal, PathRewriteDisplay, PathRewriteModal } from "@app/components/PathMatchRenameModal";
|
||||||
import { TargetModal } from "@app/components/TargetModal";
|
import { TargetModal } from "@app/components/TargetModal";
|
||||||
import { TargetDisplay } from "@app/components/TargetDisplay";
|
import { TargetDisplay } from "@app/components/TargetDisplay";
|
||||||
|
import { Badge } from "@app/components/ui/badge";
|
||||||
|
|
||||||
|
|
||||||
const baseResourceFormSchema = z.object({
|
const baseResourceFormSchema = z.object({
|
||||||
@@ -606,6 +607,21 @@ export default function Page() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const columns: ColumnDef<LocalTarget>[] = [
|
const columns: ColumnDef<LocalTarget>[] = [
|
||||||
|
{
|
||||||
|
accessorKey: "enabled",
|
||||||
|
header: t("enabled"),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Switch
|
||||||
|
defaultChecked={row.original.enabled}
|
||||||
|
onCheckedChange={(val) =>
|
||||||
|
updateTarget(row.original.targetId, {
|
||||||
|
...row.original,
|
||||||
|
enabled: val
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "priority",
|
id: "priority",
|
||||||
header: () => (
|
header: () => (
|
||||||
@@ -674,55 +690,37 @@ export default function Page() {
|
|||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Button
|
<MoveRight className="ml-1 h-4 w-4" />
|
||||||
variant="text"
|
|
||||||
size="sm"
|
|
||||||
className="px-1"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
updateTarget(row.original.targetId, {
|
|
||||||
...row.original,
|
|
||||||
path: null,
|
|
||||||
pathMatchType: null,
|
|
||||||
rewritePath: null,
|
|
||||||
rewritePathType: null
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{/* <MoveRight className="ml-1 h-4 w-4" /> */}
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<PathMatchModal
|
<div className="flex items-center gap-1">
|
||||||
value={{
|
<PathMatchModal
|
||||||
path: row.original.path,
|
value={{
|
||||||
|
path: row.original.path,
|
||||||
pathMatchType: row.original.pathMatchType,
|
pathMatchType: row.original.pathMatchType,
|
||||||
}}
|
}}
|
||||||
onChange={(config) => updateTarget(row.original.targetId, config)}
|
onChange={(config) => updateTarget(row.original.targetId, config)}
|
||||||
trigger={
|
trigger={
|
||||||
<Button variant="outline">
|
<Button variant="outline">
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
{t("matchPath")}
|
{t("matchPath")}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<MoveRight className="ml-1 h-4 w-4" />
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "siteId",
|
accessorKey: "address",
|
||||||
header: t("site"),
|
header: t("address"),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const selectedSite = sites.find(
|
const selectedSite = sites.find(
|
||||||
(site) => site.siteId === row.original.siteId
|
(site) => site.siteId === row.original.siteId
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleContainerSelectForTarget = (
|
const handleContainerSelectForTarget = (hostname: string, port?: number) => {
|
||||||
hostname: string,
|
|
||||||
port?: number
|
|
||||||
) => {
|
|
||||||
updateTarget(row.original.targetId, {
|
updateTarget(row.original.targetId, {
|
||||||
...row.original,
|
...row.original,
|
||||||
ip: hostname
|
ip: hostname
|
||||||
@@ -736,158 +734,151 @@ export default function Page() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-2 items-center">
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
role="combobox"
|
|
||||||
className={cn(
|
|
||||||
"justify-between flex-1",
|
|
||||||
!row.original.siteId &&
|
|
||||||
"text-muted-foreground"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{row.original.siteId
|
|
||||||
? selectedSite?.name
|
|
||||||
: t("siteSelect")}
|
|
||||||
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="p-0">
|
|
||||||
<Command>
|
|
||||||
<CommandInput
|
|
||||||
placeholder={t("siteSearch")}
|
|
||||||
/>
|
|
||||||
<CommandList>
|
|
||||||
<CommandEmpty>
|
|
||||||
{t("siteNotFound")}
|
|
||||||
</CommandEmpty>
|
|
||||||
<CommandGroup>
|
|
||||||
{sites.map((site) => (
|
|
||||||
<CommandItem
|
|
||||||
value={`${site.siteId}:${site.name}:${site.niceId}`}
|
|
||||||
key={site.siteId}
|
|
||||||
onSelect={() => {
|
|
||||||
updateTarget(
|
|
||||||
row.original
|
|
||||||
.targetId,
|
|
||||||
{
|
|
||||||
siteId: site.siteId
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CheckIcon
|
|
||||||
className={cn(
|
|
||||||
"mr-2 h-4 w-4",
|
|
||||||
site.siteId ===
|
|
||||||
row.original
|
|
||||||
.siteId
|
|
||||||
? "opacity-100"
|
|
||||||
: "opacity-0"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{site.name}
|
|
||||||
</CommandItem>
|
|
||||||
))}
|
|
||||||
</CommandGroup>
|
|
||||||
</CommandList>
|
|
||||||
</Command>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
{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)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "target",
|
|
||||||
header: t("target"),
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const hasTarget = !!(row.original.ip || row.original.port || row.original.method);
|
|
||||||
|
|
||||||
return hasTarget ? (
|
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<TargetModal
|
<Button variant={"outline"} className="w-full justify-start py-0 space-x-2 px-0 hover:bg-card cursor-default">
|
||||||
value={{
|
<Popover>
|
||||||
method: row.original.method,
|
<PopoverTrigger asChild>
|
||||||
ip: row.original.ip,
|
<Button
|
||||||
port: row.original.port
|
variant="ghost"
|
||||||
}}
|
role="combobox"
|
||||||
onChange={(config) =>
|
className={cn(
|
||||||
updateTarget(row.original.targetId, {
|
"min-w-[90px] justify-between text-sm font-medium border-r pr-4 rounded-none h-8 hover:bg-transparent",
|
||||||
...row.original,
|
!row.original.siteId && "text-muted-foreground"
|
||||||
...config
|
)}
|
||||||
})
|
>
|
||||||
}
|
{row.original.siteId ? selectedSite?.name : t("siteSelect")}
|
||||||
showMethod={baseForm.watch("http")}
|
<CaretSortIcon className="ml-2h-4 w-4 shrink-0 opacity-50" />
|
||||||
trigger={
|
</Button>
|
||||||
<Button
|
</PopoverTrigger>
|
||||||
variant="outline"
|
<PopoverContent className="p-0 w-[180px]">
|
||||||
className="flex items-center gap-2 max-w-md text-left cursor-pointer"
|
<Command>
|
||||||
>
|
<CommandInput placeholder={t("siteSearch")} />
|
||||||
<TargetDisplay
|
<CommandList>
|
||||||
value={{
|
<CommandEmpty>{t("siteNotFound")}</CommandEmpty>
|
||||||
method: row.original.method,
|
<CommandGroup>
|
||||||
ip: row.original.ip,
|
{sites.map((site) => (
|
||||||
port: row.original.port
|
<CommandItem
|
||||||
}}
|
key={site.siteId}
|
||||||
showMethod={baseForm.watch("http")}
|
value={`${site.siteId}:${site.name}`}
|
||||||
/>
|
onSelect={() =>
|
||||||
</Button>
|
updateTarget(row.original.targetId, { siteId: site.siteId })
|
||||||
}
|
}
|
||||||
/>
|
>
|
||||||
<Button
|
<CheckIcon
|
||||||
variant="ghost"
|
className={cn(
|
||||||
size="sm"
|
"mr-2 h-4 w-4",
|
||||||
className="h-8 w-8 p-0"
|
site.siteId === row.original.siteId
|
||||||
onClick={(e) => {
|
? "opacity-100"
|
||||||
e.stopPropagation();
|
: "opacity-0"
|
||||||
updateTarget(row.original.targetId, {
|
)}
|
||||||
...row.original,
|
/>
|
||||||
method: null,
|
{site.name}
|
||||||
ip: "",
|
</CommandItem>
|
||||||
port: undefined
|
))}
|
||||||
});
|
</CommandGroup>
|
||||||
}}
|
</CommandList>
|
||||||
>
|
</Command>
|
||||||
×
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
{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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
|
||||||
|
<Select
|
||||||
|
defaultValue={row.original.method ?? "http"}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
updateTarget(row.original.targetId, {
|
||||||
|
...row.original,
|
||||||
|
method: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-8 px-2 w-[70px] text-sm font-normal border-none bg-transparent shadow-none focus:ring-0 focus:outline-none focus-visible:ring-0 data-[state=open]:bg-transparent">
|
||||||
|
{row.original.method || "http"}
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="http">http</SelectItem>
|
||||||
|
<SelectItem value="https">https</SelectItem>
|
||||||
|
<SelectItem value="h2c">h2c</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-center bg-gray-200 text-black px-2 h-9">
|
||||||
|
{"://"}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
defaultValue={row.original.ip}
|
||||||
|
placeholder="IP / Hostname"
|
||||||
|
className="min-w-[130px] border-none placeholder-gray-400"
|
||||||
|
onBlur={(e) => {
|
||||||
|
const input = e.target.value.trim();
|
||||||
|
const hasProtocol = /^(https?|h2c):\/\//.test(input);
|
||||||
|
const hasPort = /:\d+(?:\/|$)/.test(input);
|
||||||
|
|
||||||
|
if (hasProtocol || hasPort) {
|
||||||
|
const parsed = parseHostTarget(input);
|
||||||
|
if (parsed) {
|
||||||
|
updateTarget(row.original.targetId, {
|
||||||
|
...row.original,
|
||||||
|
method: hasProtocol
|
||||||
|
? parsed.protocol
|
||||||
|
: row.original.method,
|
||||||
|
ip: parsed.host,
|
||||||
|
port: hasPort
|
||||||
|
? parsed.port
|
||||||
|
: row.original.port
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
updateTarget(row.original.targetId, {
|
||||||
|
...row.original,
|
||||||
|
ip: input
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
updateTarget(row.original.targetId, {
|
||||||
|
...row.original,
|
||||||
|
ip: input
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center justify-center bg-gray-200 text-black px-2 h-9">
|
||||||
|
{":"}
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
placeholder="Port"
|
||||||
|
defaultValue={row.original.port}
|
||||||
|
className="min-w-[60px] pl-0 border-none placeholder-gray-400"
|
||||||
|
onBlur={(e) =>
|
||||||
|
updateTarget(row.original.targetId, {
|
||||||
|
...row.original,
|
||||||
|
port: parseInt(e.target.value, 10)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
<MoveRight className="mr-2 h-4 w-4" />
|
<MoveRight className="ml-1 h-4 w-4" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<TargetModal
|
|
||||||
value={{
|
|
||||||
method: row.original.method,
|
|
||||||
ip: row.original.ip,
|
|
||||||
port: row.original.port
|
|
||||||
}}
|
|
||||||
onChange={(config) =>
|
|
||||||
updateTarget(row.original.targetId, {
|
|
||||||
...row.original,
|
|
||||||
...config
|
|
||||||
})
|
|
||||||
}
|
|
||||||
showMethod={baseForm.watch("http")}
|
|
||||||
trigger={
|
|
||||||
<Button variant="outline">
|
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
|
||||||
{t("configureTarget")}
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -895,18 +886,22 @@ export default function Page() {
|
|||||||
accessorKey: "rewritePath",
|
accessorKey: "rewritePath",
|
||||||
header: t("rewritePath"),
|
header: t("rewritePath"),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const hasRewritePath = !!(row.original.rewritePath || row.original.rewritePathType);
|
const hasRewritePath = !!(
|
||||||
const noPathMatch = !row.original.path && !row.original.pathMatchType;
|
row.original.rewritePath || row.original.rewritePathType
|
||||||
|
);
|
||||||
|
const noPathMatch =
|
||||||
|
!row.original.path && !row.original.pathMatchType;
|
||||||
|
|
||||||
return hasRewritePath && !noPathMatch ? (
|
return hasRewritePath && !noPathMatch ? (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
{/* <MoveRight className="mr-2 h-4 w-4" /> */}
|
|
||||||
<PathRewriteModal
|
<PathRewriteModal
|
||||||
value={{
|
value={{
|
||||||
rewritePath: row.original.rewritePath,
|
rewritePath: row.original.rewritePath,
|
||||||
rewritePathType: row.original.rewritePathType,
|
rewritePathType: row.original.rewritePathType
|
||||||
}}
|
}}
|
||||||
onChange={(config) => updateTarget(row.original.targetId, config)}
|
onChange={(config) =>
|
||||||
|
updateTarget(row.original.targetId, config)
|
||||||
|
}
|
||||||
trigger={
|
trigger={
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -915,36 +910,25 @@ export default function Page() {
|
|||||||
>
|
>
|
||||||
<PathRewriteDisplay
|
<PathRewriteDisplay
|
||||||
value={{
|
value={{
|
||||||
rewritePath: row.original.rewritePath,
|
rewritePath:
|
||||||
rewritePathType: row.original.rewritePathType,
|
row.original.rewritePath,
|
||||||
|
rewritePathType:
|
||||||
|
row.original.rewritePathType
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="text"
|
|
||||||
className="px-1"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
updateTarget(row.original.targetId, {
|
|
||||||
...row.original,
|
|
||||||
rewritePath: null,
|
|
||||||
rewritePathType: null,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<PathRewriteModal
|
<PathRewriteModal
|
||||||
value={{
|
value={{
|
||||||
rewritePath: row.original.rewritePath,
|
rewritePath: row.original.rewritePath,
|
||||||
rewritePathType: row.original.rewritePathType,
|
rewritePathType: row.original.rewritePathType
|
||||||
}}
|
}}
|
||||||
onChange={(config) => updateTarget(row.original.targetId, config)}
|
onChange={(config) =>
|
||||||
|
updateTarget(row.original.targetId, config)
|
||||||
|
}
|
||||||
trigger={
|
trigger={
|
||||||
<Button variant="outline" disabled={noPathMatch}>
|
<Button variant="outline" disabled={noPathMatch}>
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
@@ -954,22 +938,7 @@ export default function Page() {
|
|||||||
disabled={noPathMatch}
|
disabled={noPathMatch}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "enabled",
|
|
||||||
header: t("enabled"),
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<Switch
|
|
||||||
defaultChecked={row.original.enabled}
|
|
||||||
onCheckedChange={(val) =>
|
|
||||||
updateTarget(row.original.targetId, {
|
|
||||||
...row.original,
|
|
||||||
enabled: val
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "actions",
|
id: "actions",
|
||||||
|
|||||||
Reference in New Issue
Block a user