Fix docker button and positioning

This commit is contained in:
Owen
2025-10-15 20:21:15 -07:00
parent 08eeb12519
commit ee3df081ef
2 changed files with 254 additions and 181 deletions

View File

@@ -1007,14 +1007,9 @@ export default function ReverseProxyTargets(props: {
) => { ) => {
updateTarget(row.original.targetId, { updateTarget(row.original.targetId, {
...row.original, ...row.original,
ip: hostname ip: hostname,
...(port && { port: port })
}); });
if (port) {
updateTarget(row.original.targetId, {
...row.original,
port: port
});
}
}; };
return ( return (

View File

@@ -58,7 +58,16 @@ 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, CircleCheck, CircleX, Info, MoveRight, Plus, Settings, SquareArrowOutUpRight } from "lucide-react"; import {
ArrowRight,
CircleCheck,
CircleX,
Info,
MoveRight,
Plus,
Settings,
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";
@@ -89,16 +98,25 @@ import { isTargetValid } from "@server/lib/validators";
import { ListTargetsResponse } from "@server/routers/target"; import { ListTargetsResponse } from "@server/routers/target";
import { DockerManager, DockerState } from "@app/lib/docker"; import { DockerManager, DockerState } from "@app/lib/docker";
import { parseHostTarget } from "@app/lib/parseHostTarget"; import { parseHostTarget } from "@app/lib/parseHostTarget";
import { toASCII, toUnicode } from 'punycode'; import { toASCII, toUnicode } from "punycode";
import { DomainRow } from "../../../../../components/DomainsTable"; import { DomainRow } from "../../../../../components/DomainsTable";
import { finalizeSubdomainSanitize } from "@app/lib/subdomain-utils"; import { finalizeSubdomainSanitize } from "@app/lib/subdomain-utils";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@app/components/ui/tooltip"; import {
import { PathMatchDisplay, PathMatchModal, PathRewriteDisplay, PathRewriteModal } from "@app/components/PathMatchRenameModal"; Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger
} from "@app/components/ui/tooltip";
import {
PathMatchDisplay,
PathMatchModal,
PathRewriteDisplay,
PathRewriteModal
} from "@app/components/PathMatchRenameModal";
import { Badge } from "@app/components/ui/badge"; import { Badge } from "@app/components/ui/badge";
import HealthCheckDialog from "@app/components/HealthCheckDialog"; import HealthCheckDialog from "@app/components/HealthCheckDialog";
import { SwitchInput } from "@app/components/SwitchInput"; import { SwitchInput } from "@app/components/SwitchInput";
const baseResourceFormSchema = z.object({ const baseResourceFormSchema = z.object({
name: z.string().min(1).max(255), name: z.string().min(1).max(255),
http: z.boolean() http: z.boolean()
@@ -119,50 +137,57 @@ const targetsSettingsSchema = z.object({
stickySession: z.boolean() stickySession: z.boolean()
}); });
const addTargetSchema = z
const addTargetSchema = z.object({ .object({
ip: z.string().refine(isTargetValid), ip: z.string().refine(isTargetValid),
method: z.string().nullable(), method: z.string().nullable(),
port: z.coerce.number().int().positive(), port: z.coerce.number().int().positive(),
siteId: z.number().int().positive(), siteId: z.number().int().positive(),
path: z.string().optional().nullable(), path: z.string().optional().nullable(),
pathMatchType: z.enum(["exact", "prefix", "regex"]).optional().nullable(), pathMatchType: z
rewritePath: z.string().optional().nullable(), .enum(["exact", "prefix", "regex"])
rewritePathType: z.enum(["exact", "prefix", "regex", "stripPrefix"]).optional().nullable(), .optional()
priority: z.number().int().min(1).max(1000) .nullable(),
}).refine( rewritePath: z.string().optional().nullable(),
(data) => { rewritePathType: z
// If path is provided, pathMatchType must be provided .enum(["exact", "prefix", "regex", "stripPrefix"])
if (data.path && !data.pathMatchType) { .optional()
return false; .nullable(),
} priority: z.number().int().min(1).max(1000)
// If pathMatchType is provided, path must be provided })
if (data.pathMatchType && !data.path) { .refine(
return false; (data) => {
} // If path is provided, pathMatchType must be provided
// Validate path based on pathMatchType if (data.path && !data.pathMatchType) {
if (data.path && data.pathMatchType) { return false;
switch (data.pathMatchType) {
case "exact":
case "prefix":
// Path should start with /
return data.path.startsWith("/");
case "regex":
// Validate regex
try {
new RegExp(data.path);
return true;
} catch {
return false;
}
} }
// If pathMatchType is provided, path must be provided
if (data.pathMatchType && !data.path) {
return false;
}
// Validate path based on pathMatchType
if (data.path && data.pathMatchType) {
switch (data.pathMatchType) {
case "exact":
case "prefix":
// Path should start with /
return data.path.startsWith("/");
case "regex":
// Validate regex
try {
new RegExp(data.path);
return true;
} catch {
return false;
}
}
}
return true;
},
{
message: "Invalid path configuration"
} }
return true; )
},
{
message: "Invalid path configuration"
}
)
.refine( .refine(
(data) => { (data) => {
// If rewritePath is provided, rewritePathType must be provided // If rewritePath is provided, rewritePathType must be provided
@@ -221,7 +246,9 @@ export default function Page() {
// Target management state // Target management state
const [targets, setTargets] = useState<LocalTarget[]>([]); const [targets, setTargets] = useState<LocalTarget[]>([]);
const [targetsToRemove, setTargetsToRemove] = useState<number[]>([]); const [targetsToRemove, setTargetsToRemove] = useState<number[]>([]);
const [dockerStates, setDockerStates] = useState<Map<number, DockerState>>(new Map()); const [dockerStates, setDockerStates] = useState<Map<number, DockerState>>(
new Map()
);
const [selectedTargetForHealthCheck, setSelectedTargetForHealthCheck] = const [selectedTargetForHealthCheck, setSelectedTargetForHealthCheck] =
useState<LocalTarget | null>(null); useState<LocalTarget | null>(null);
@@ -290,12 +317,12 @@ export default function Page() {
...(!env.flags.allowRawResources ...(!env.flags.allowRawResources
? [] ? []
: [ : [
{ {
id: "raw" as ResourceType, id: "raw" as ResourceType,
title: t("resourceRaw"), title: t("resourceRaw"),
description: t("resourceRawDescription") description: t("resourceRawDescription")
} }
]) ])
]; ];
const baseForm = useForm({ const baseForm = useForm({
@@ -330,7 +357,7 @@ export default function Page() {
pathMatchType: null, pathMatchType: null,
rewritePath: null, rewritePath: null,
rewritePathType: null, rewritePathType: null,
priority: 100, priority: 100
} as z.infer<typeof addTargetSchema> } as z.infer<typeof addTargetSchema>
}); });
@@ -360,14 +387,14 @@ export default function Page() {
const dockerManager = new DockerManager(api, siteId); const dockerManager = new DockerManager(api, siteId);
const dockerState = await dockerManager.initializeDocker(); const dockerState = await dockerManager.initializeDocker();
setDockerStates(prev => new Map(prev.set(siteId, dockerState))); setDockerStates((prev) => new Map(prev.set(siteId, dockerState)));
}; };
const refreshContainersForSite = async (siteId: number) => { const refreshContainersForSite = async (siteId: number) => {
const dockerManager = new DockerManager(api, siteId); const dockerManager = new DockerManager(api, siteId);
const containers = await dockerManager.fetchContainers(); const containers = await dockerManager.fetchContainers();
setDockerStates(prev => { setDockerStates((prev) => {
const newMap = new Map(prev); const newMap = new Map(prev);
const existingState = newMap.get(siteId); const existingState = newMap.get(siteId);
if (existingState) { if (existingState) {
@@ -378,11 +405,13 @@ export default function Page() {
}; };
const getDockerStateForSite = (siteId: number): DockerState => { const getDockerStateForSite = (siteId: number): DockerState => {
return dockerStates.get(siteId) || { return (
isEnabled: false, dockerStates.get(siteId) || {
isAvailable: false, isEnabled: false,
containers: [] isAvailable: false,
}; containers: []
}
);
}; };
async function addTarget(data: z.infer<typeof addTargetSchema>) { async function addTarget(data: z.infer<typeof addTargetSchema>) {
@@ -443,7 +472,7 @@ export default function Page() {
pathMatchType: null, pathMatchType: null,
rewritePath: null, rewritePath: null,
rewritePathType: null, rewritePathType: null,
priority: 100, priority: 100
}); });
} }
@@ -463,11 +492,11 @@ export default function Page() {
targets.map((target) => targets.map((target) =>
target.targetId === targetId target.targetId === targetId
? { ? {
...target, ...target,
...data, ...data,
updated: true, updated: true
// siteType: site?.type || null // siteType: site?.type || null
} }
: target : target
) )
); );
@@ -497,7 +526,9 @@ export default function Page() {
: undefined; : undefined;
Object.assign(payload, { Object.assign(payload, {
subdomain: sanitizedSubdomain ? toASCII(sanitizedSubdomain) : undefined, subdomain: sanitizedSubdomain
? toASCII(sanitizedSubdomain)
: undefined,
domainId: httpData.domainId, domainId: httpData.domainId,
protocol: "tcp" protocol: "tcp"
}); });
@@ -660,7 +691,7 @@ export default function Page() {
const rawDomains = res.data.data.domains as DomainRow[]; const rawDomains = res.data.data.domains as DomainRow[];
const domains = rawDomains.map((domain) => ({ const domains = rawDomains.map((domain) => ({
...domain, ...domain,
baseDomain: toUnicode(domain.baseDomain), baseDomain: toUnicode(domain.baseDomain)
})); }));
setBaseDomains(domains); setBaseDomains(domains);
// if (domains.length) { // if (domains.length) {
@@ -683,10 +714,10 @@ export default function Page() {
targets.map((target) => targets.map((target) =>
target.targetId === targetId target.targetId === targetId
? { ? {
...target, ...target,
...config, ...config,
updated: true updated: true
} }
: target : target
) )
); );
@@ -712,9 +743,7 @@ export default function Page() {
<Info className="h-4 w-4 text-muted-foreground" /> <Info className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger> </TooltipTrigger>
<TooltipContent className="max-w-xs"> <TooltipContent className="max-w-xs">
<p> <p>{t("priorityDescription")}</p>
{t("priorityDescription")}
</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
@@ -895,19 +924,39 @@ export default function Page() {
) => { ) => {
updateTarget(row.original.targetId, { updateTarget(row.original.targetId, {
...row.original, ...row.original,
ip: hostname ip: hostname,
...(port && { port: port })
}); });
if (port) {
updateTarget(row.original.targetId, {
...row.original,
port: port
});
}
}; };
return ( return (
<div className="flex items-center w-full"> <div className="flex items-center w-full">
<div className="flex items-center w-full justify-start py-0 space-x-2 px-0 cursor-default border border-input shadow-2xs rounded-md"> <div className="flex items-center w-full justify-start py-0 space-x-2 px-0 cursor-default border border-input shadow-2xs 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
)
}
/>
);
})()}
<Popover> <Popover>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button
@@ -916,7 +965,7 @@ export default function Page() {
className={cn( className={cn(
"w-[180px] justify-between text-sm font-medium border-r pr-4 rounded-none h-8 hover:bg-transparent", "w-[180px] justify-between text-sm font-medium border-r pr-4 rounded-none h-8 hover:bg-transparent",
!row.original.siteId && !row.original.siteId &&
"text-muted-foreground" "text-muted-foreground"
)} )}
> >
<span className="truncate max-w-[90px]"> <span className="truncate max-w-[90px]">
@@ -969,30 +1018,6 @@ export default function Page() {
</Command> </Command>
</PopoverContent> </PopoverContent>
</Popover> </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 <Select
defaultValue={row.original.method ?? "http"} defaultValue={row.original.method ?? "http"}
@@ -1464,10 +1489,10 @@ export default function Page() {
.target .target
.value .value
? parseInt( ? parseInt(
e e
.target .target
.value .value
) )
: undefined : undefined
) )
} }
@@ -1546,60 +1571,87 @@ export default function Page() {
<TableHeader> <TableHeader>
{table {table
.getHeaderGroups() .getHeaderGroups()
.map((headerGroup) => ( .map(
<TableRow key={headerGroup.id}> (
{headerGroup.headers.map( headerGroup
(header) => ( ) => (
<TableHead <TableRow
key={header.id} key={
> headerGroup.id
{header.isPlaceholder }
? null >
: flexRender( {headerGroup.headers.map(
header (
.column header
.columnDef ) => (
.header, <TableHead
header.getContext()
)}
</TableHead>
)
)}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table
.getRowModel()
.rows.map((row) => (
<TableRow key={row.id}>
{row
.getVisibleCells()
.map((cell) => (
<TableCell
key={ key={
cell.id header.id
} }
> >
{flexRender( {header.isPlaceholder
cell ? null
.column : flexRender(
.columnDef header
.cell, .column
cell.getContext() .columnDef
)} .header,
</TableCell> header.getContext()
))} )}
</TableHead>
)
)}
</TableRow> </TableRow>
)) )
)}
</TableHeader>
<TableBody>
{table.getRowModel()
.rows?.length ? (
table
.getRowModel()
.rows.map(
(row) => (
<TableRow
key={
row.id
}
>
{row
.getVisibleCells()
.map(
(
cell
) => (
<TableCell
key={
cell.id
}
>
{flexRender(
cell
.column
.columnDef
.cell,
cell.getContext()
)}
</TableCell>
)
)}
</TableRow>
)
)
) : ( ) : (
<TableRow> <TableRow>
<TableCell <TableCell
colSpan={columns.length} colSpan={
columns.length
}
className="h-24 text-center" className="h-24 text-center"
> >
{t("targetNoOne")} {t(
"targetNoOne"
)}
</TableCell> </TableCell>
</TableRow> </TableRow>
)} )}
@@ -1621,8 +1673,12 @@ export default function Page() {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Switch <Switch
id="advanced-mode-toggle" id="advanced-mode-toggle"
checked={isAdvancedMode} checked={
onCheckedChange={setIsAdvancedMode} isAdvancedMode
}
onCheckedChange={
setIsAdvancedMode
}
/> />
<label <label
htmlFor="advanced-mode-toggle" htmlFor="advanced-mode-toggle"
@@ -1639,7 +1695,10 @@ export default function Page() {
<p className="text-muted-foreground mb-4"> <p className="text-muted-foreground mb-4">
{t("targetNoOne")} {t("targetNoOne")}
</p> </p>
<Button onClick={addNewTarget} variant="outline"> <Button
onClick={addNewTarget}
variant="outline"
>
<Plus className="h-4 w-4 mr-2" /> <Plus className="h-4 w-4 mr-2" />
{t("addTarget")} {t("addTarget")}
</Button> </Button>
@@ -1685,24 +1744,36 @@ export default function Page() {
<HealthCheckDialog <HealthCheckDialog
open={healthCheckDialogOpen} open={healthCheckDialogOpen}
setOpen={setHealthCheckDialogOpen} setOpen={setHealthCheckDialogOpen}
targetId={selectedTargetForHealthCheck.targetId} targetId={
selectedTargetForHealthCheck.targetId
}
targetAddress={`${selectedTargetForHealthCheck.ip}:${selectedTargetForHealthCheck.port}`} targetAddress={`${selectedTargetForHealthCheck.ip}:${selectedTargetForHealthCheck.port}`}
targetMethod={ targetMethod={
selectedTargetForHealthCheck.method || undefined selectedTargetForHealthCheck.method ||
undefined
} }
initialConfig={{ initialConfig={{
hcEnabled: hcEnabled:
selectedTargetForHealthCheck.hcEnabled || false, selectedTargetForHealthCheck.hcEnabled ||
hcPath: selectedTargetForHealthCheck.hcPath || "/", false,
hcPath:
selectedTargetForHealthCheck.hcPath ||
"/",
hcMethod: hcMethod:
selectedTargetForHealthCheck.hcMethod || "GET", selectedTargetForHealthCheck.hcMethod ||
"GET",
hcInterval: hcInterval:
selectedTargetForHealthCheck.hcInterval || 5, selectedTargetForHealthCheck.hcInterval ||
hcTimeout: selectedTargetForHealthCheck.hcTimeout || 5, 5,
hcTimeout:
selectedTargetForHealthCheck.hcTimeout ||
5,
hcHeaders: hcHeaders:
selectedTargetForHealthCheck.hcHeaders || undefined, selectedTargetForHealthCheck.hcHeaders ||
undefined,
hcScheme: hcScheme:
selectedTargetForHealthCheck.hcScheme || undefined, selectedTargetForHealthCheck.hcScheme ||
undefined,
hcHostname: hcHostname:
selectedTargetForHealthCheck.hcHostname || selectedTargetForHealthCheck.hcHostname ||
selectedTargetForHealthCheck.ip, selectedTargetForHealthCheck.ip,
@@ -1713,8 +1784,11 @@ export default function Page() {
selectedTargetForHealthCheck.hcFollowRedirects || selectedTargetForHealthCheck.hcFollowRedirects ||
true, true,
hcStatus: hcStatus:
selectedTargetForHealthCheck.hcStatus || undefined, selectedTargetForHealthCheck.hcStatus ||
hcMode: selectedTargetForHealthCheck.hcMode || "http", undefined,
hcMode:
selectedTargetForHealthCheck.hcMode ||
"http",
hcUnhealthyInterval: hcUnhealthyInterval:
selectedTargetForHealthCheck.hcUnhealthyInterval || selectedTargetForHealthCheck.hcUnhealthyInterval ||
30 30
@@ -1749,7 +1823,9 @@ export default function Page() {
{t("resourceAddEntrypoints")} {t("resourceAddEntrypoints")}
</h3> </h3>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
{t("resourceAddEntrypointsEditFile")} {t(
"resourceAddEntrypointsEditFile"
)}
</p> </p>
<CopyTextBox <CopyTextBox
text={`entryPoints: text={`entryPoints:
@@ -1764,7 +1840,9 @@ export default function Page() {
{t("resourceExposePorts")} {t("resourceExposePorts")}
</h3> </h3>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
{t("resourceExposePortsEditFile")} {t(
"resourceExposePortsEditFile"
)}
</p> </p>
<CopyTextBox <CopyTextBox
text={`ports: text={`ports: