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,18 +137,25 @@ 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
.enum(["exact", "prefix", "regex"])
.optional()
.nullable(),
rewritePath: z.string().optional().nullable(), rewritePath: z.string().optional().nullable(),
rewritePathType: z.enum(["exact", "prefix", "regex", "stripPrefix"]).optional().nullable(), rewritePathType: z
.enum(["exact", "prefix", "regex", "stripPrefix"])
.optional()
.nullable(),
priority: z.number().int().min(1).max(1000) priority: z.number().int().min(1).max(1000)
}).refine( })
.refine(
(data) => { (data) => {
// If path is provided, pathMatchType must be provided // If path is provided, pathMatchType must be provided
if (data.path && !data.pathMatchType) { if (data.path && !data.pathMatchType) {
@@ -162,7 +187,7 @@ const addTargetSchema = z.object({
{ {
message: "Invalid path configuration" 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);
@@ -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 (
dockerStates.get(siteId) || {
isEnabled: false, isEnabled: false,
isAvailable: false, isAvailable: false,
containers: [] 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
}); });
} }
@@ -465,7 +494,7 @@ export default function Page() {
? { ? {
...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) {
@@ -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
@@ -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"}
@@ -1546,12 +1571,23 @@ export default function Page() {
<TableHeader> <TableHeader>
{table {table
.getHeaderGroups() .getHeaderGroups()
.map((headerGroup) => ( .map(
<TableRow key={headerGroup.id}> (
headerGroup
) => (
<TableRow
key={
headerGroup.id
}
>
{headerGroup.headers.map( {headerGroup.headers.map(
(header) => ( (
header
) => (
<TableHead <TableHead
key={header.id} key={
header.id
}
> >
{header.isPlaceholder {header.isPlaceholder
? null ? null
@@ -1566,17 +1602,27 @@ export default function Page() {
) )
)} )}
</TableRow> </TableRow>
))} )
)}
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{table.getRowModel().rows?.length ? ( {table.getRowModel()
.rows?.length ? (
table table
.getRowModel() .getRowModel()
.rows.map((row) => ( .rows.map(
<TableRow key={row.id}> (row) => (
<TableRow
key={
row.id
}
>
{row {row
.getVisibleCells() .getVisibleCells()
.map((cell) => ( .map(
(
cell
) => (
<TableCell <TableCell
key={ key={
cell.id cell.id
@@ -1590,16 +1636,22 @@ export default function Page() {
cell.getContext() cell.getContext()
)} )}
</TableCell> </TableCell>
))} )
)}
</TableRow> </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: