Add Smart Host Parsing

This commit is contained in:
Pallavi
2025-08-22 13:07:03 +05:30
parent 60d8831399
commit 9557f755a5
3 changed files with 207 additions and 152 deletions

View File

@@ -94,6 +94,7 @@ import {
CommandList CommandList
} from "@app/components/ui/command"; } from "@app/components/ui/command";
import { Badge } from "@app/components/ui/badge"; import { Badge } from "@app/components/ui/badge";
import { parseHostTarget } from "@app/lib/parseHostTarget";
const addTargetSchema = z.object({ const addTargetSchema = z.object({
ip: z.string().refine(isTargetValid), ip: z.string().refine(isTargetValid),
@@ -417,11 +418,11 @@ export default function ReverseProxyTargets(props: {
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
) )
); );
@@ -545,7 +546,7 @@ export default function ReverseProxyTargets(props: {
className={cn( className={cn(
"justify-between flex-1", "justify-between flex-1",
!row.original.siteId && !row.original.siteId &&
"text-muted-foreground" "text-muted-foreground"
)} )}
> >
{row.original.siteId {row.original.siteId
@@ -614,31 +615,31 @@ export default function ReverseProxyTargets(props: {
}, },
...(resource.http ...(resource.http
? [ ? [
{ {
accessorKey: "method", accessorKey: "method",
header: t("method"), header: t("method"),
cell: ({ row }: { row: Row<LocalTarget> }) => ( cell: ({ row }: { row: Row<LocalTarget> }) => (
<Select <Select
defaultValue={row.original.method ?? ""} defaultValue={row.original.method ?? ""}
onValueChange={(value) => onValueChange={(value) =>
updateTarget(row.original.targetId, { updateTarget(row.original.targetId, {
...row.original, ...row.original,
method: value method: value
}) })
} }
> >
<SelectTrigger> <SelectTrigger>
{row.original.method} {row.original.method}
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="http">http</SelectItem> <SelectItem value="http">http</SelectItem>
<SelectItem value="https">https</SelectItem> <SelectItem value="https">https</SelectItem>
<SelectItem value="h2c">h2c</SelectItem> <SelectItem value="h2c">h2c</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
) )
} }
] ]
: []), : []),
{ {
accessorKey: "ip", accessorKey: "ip",
@@ -647,13 +648,25 @@ export default function ReverseProxyTargets(props: {
<Input <Input
defaultValue={row.original.ip} defaultValue={row.original.ip}
className="min-w-[150px]" className="min-w-[150px]"
onBlur={(e) => onBlur={(e) => {
updateTarget(row.original.targetId, { const parsed = parseHostTarget(e.target.value);
...row.original, if (parsed) {
ip: e.target.value updateTarget(row.original.targetId, {
}) ...row.original,
} method: parsed.protocol,
ip: parsed.host,
port: parsed.port
});
} else {
updateTarget(row.original.targetId, {
...row.original,
ip: e.target.value
});
}
}}
/> />
) )
}, },
{ {
@@ -785,21 +798,21 @@ export default function ReverseProxyTargets(props: {
className={cn( className={cn(
"justify-between flex-1", "justify-between flex-1",
!field.value && !field.value &&
"text-muted-foreground" "text-muted-foreground"
)} )}
> >
{field.value {field.value
? sites.find( ? sites.find(
( (
site site
) => ) =>
site.siteId === site.siteId ===
field.value field.value
) )
?.name ?.name
: t( : t(
"siteSelect" "siteSelect"
)} )}
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" /> <CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button> </Button>
</FormControl> </FormControl>
@@ -865,18 +878,18 @@ export default function ReverseProxyTargets(props: {
); );
return selectedSite && return selectedSite &&
selectedSite.type === selectedSite.type ===
"newt" ? (() => { "newt" ? (() => {
const dockerState = getDockerStateForSite(selectedSite.siteId); const dockerState = getDockerStateForSite(selectedSite.siteId);
return ( return (
<ContainersSelector <ContainersSelector
site={selectedSite} site={selectedSite}
containers={dockerState.containers} containers={dockerState.containers}
isAvailable={dockerState.isAvailable} isAvailable={dockerState.isAvailable}
onContainerSelect={handleContainerSelect} onContainerSelect={handleContainerSelect}
onRefresh={() => refreshContainersForSite(selectedSite.siteId)} onRefresh={() => refreshContainersForSite(selectedSite.siteId)}
/> />
); );
})() : null; })() : null;
})()} })()}
</div> </div>
<FormMessage /> <FormMessage />
@@ -942,11 +955,22 @@ export default function ReverseProxyTargets(props: {
name="ip" name="ip"
render={({ field }) => ( render={({ field }) => (
<FormItem className="relative"> <FormItem className="relative">
<FormLabel> <FormLabel>{t("targetAddr")}</FormLabel>
{t("targetAddr")}
</FormLabel>
<FormControl> <FormControl>
<Input id="ip" {...field} /> <Input
id="ip"
{...field}
onBlur={(e) => {
const parsed = parseHostTarget(e.target.value);
if (parsed) {
addTargetForm.setValue("method", parsed.protocol);
addTargetForm.setValue("ip", parsed.host);
addTargetForm.setValue("port", parsed.port);
} else {
field.onBlur();
}
}}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -1048,12 +1072,12 @@ export default function ReverseProxyTargets(props: {
{header.isPlaceholder {header.isPlaceholder
? null ? null
: flexRender( : flexRender(
header header
.column .column
.columnDef .columnDef
.header, .header,
header.getContext() header.getContext()
)} )}
</TableHead> </TableHead>
) )
)} )}

View File

@@ -88,6 +88,7 @@ import { ArrayElement } from "@server/types/ArrayElement";
import { isTargetValid } from "@server/lib/validators"; 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";
const baseResourceFormSchema = z.object({ const baseResourceFormSchema = z.object({
name: z.string().min(1).max(255), name: z.string().min(1).max(255),
@@ -164,12 +165,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<BaseResourceFormValues>({ const baseForm = useForm<BaseResourceFormValues>({
@@ -301,11 +302,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
) )
); );
@@ -520,7 +521,7 @@ export default function Page() {
className={cn( className={cn(
"justify-between flex-1", "justify-between flex-1",
!row.original.siteId && !row.original.siteId &&
"text-muted-foreground" "text-muted-foreground"
)} )}
> >
{row.original.siteId {row.original.siteId
@@ -589,31 +590,31 @@ export default function Page() {
}, },
...(baseForm.watch("http") ...(baseForm.watch("http")
? [ ? [
{ {
accessorKey: "method", accessorKey: "method",
header: t("method"), header: t("method"),
cell: ({ row }: { row: Row<LocalTarget> }) => ( cell: ({ row }: { row: Row<LocalTarget> }) => (
<Select <Select
defaultValue={row.original.method ?? ""} defaultValue={row.original.method ?? ""}
onValueChange={(value) => onValueChange={(value) =>
updateTarget(row.original.targetId, { updateTarget(row.original.targetId, {
...row.original, ...row.original,
method: value method: value
}) })
} }
> >
<SelectTrigger> <SelectTrigger>
{row.original.method} {row.original.method}
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="http">http</SelectItem> <SelectItem value="http">http</SelectItem>
<SelectItem value="https">https</SelectItem> <SelectItem value="https">https</SelectItem>
<SelectItem value="h2c">h2c</SelectItem> <SelectItem value="h2c">h2c</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
) )
} }
] ]
: []), : []),
{ {
accessorKey: "ip", accessorKey: "ip",
@@ -622,12 +623,23 @@ export default function Page() {
<Input <Input
defaultValue={row.original.ip} defaultValue={row.original.ip}
className="min-w-[150px]" className="min-w-[150px]"
onBlur={(e) => onBlur={(e) => {
updateTarget(row.original.targetId, { const parsed = parseHostTarget(e.target.value);
...row.original,
ip: e.target.value if (parsed) {
}) updateTarget(row.original.targetId, {
} ...row.original,
method: parsed.protocol,
ip: parsed.host,
port: parsed.port ? Number(parsed.port) : undefined,
});
} else {
updateTarget(row.original.targetId, {
...row.original,
ip: e.target.value,
});
}
}}
/> />
) )
}, },
@@ -909,10 +921,10 @@ export default function Page() {
.target .target
.value .value
? parseInt( ? parseInt(
e e
.target .target
.value .value
) )
: undefined : undefined
) )
} }
@@ -1015,21 +1027,21 @@ export default function Page() {
className={cn( className={cn(
"justify-between flex-1", "justify-between flex-1",
!field.value && !field.value &&
"text-muted-foreground" "text-muted-foreground"
)} )}
> >
{field.value {field.value
? sites.find( ? sites.find(
( (
site site
) => ) =>
site.siteId === site.siteId ===
field.value field.value
) )
?.name ?.name
: t( : t(
"siteSelect" "siteSelect"
)} )}
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" /> <CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button> </Button>
</FormControl> </FormControl>
@@ -1097,18 +1109,18 @@ export default function Page() {
); );
return selectedSite && return selectedSite &&
selectedSite.type === selectedSite.type ===
"newt" ? (() => { "newt" ? (() => {
const dockerState = getDockerStateForSite(selectedSite.siteId); const dockerState = getDockerStateForSite(selectedSite.siteId);
return ( return (
<ContainersSelector <ContainersSelector
site={selectedSite} site={selectedSite}
containers={dockerState.containers} containers={dockerState.containers}
isAvailable={dockerState.isAvailable} isAvailable={dockerState.isAvailable}
onContainerSelect={handleContainerSelect} onContainerSelect={handleContainerSelect}
onRefresh={() => refreshContainersForSite(selectedSite.siteId)} onRefresh={() => refreshContainersForSite(selectedSite.siteId)}
/> />
); );
})() : null; })() : null;
})()} })()}
</div> </div>
<FormMessage /> <FormMessage />
@@ -1176,21 +1188,25 @@ export default function Page() {
)} )}
<FormField <FormField
control={ control={addTargetForm.control}
addTargetForm.control
}
name="ip" name="ip"
render={({ field }) => ( render={({ field }) => (
<FormItem className="relative"> <FormItem className="relative">
<FormLabel> <FormLabel>{t("targetAddr")}</FormLabel>
{t(
"targetAddr"
)}
</FormLabel>
<FormControl> <FormControl>
<Input <Input
id="ip" id="ip"
{...field} {...field}
onBlur={(e) => {
const parsed = parseHostTarget(e.target.value);
if (parsed) {
addTargetForm.setValue("method", parsed.protocol);
addTargetForm.setValue("ip", parsed.host);
addTargetForm.setValue("port", parsed.port);
} else {
field.onBlur();
}
}}
/> />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
@@ -1270,12 +1286,12 @@ export default function Page() {
{header.isPlaceholder {header.isPlaceholder
? null ? null
: flexRender( : flexRender(
header header
.column .column
.columnDef .columnDef
.header, .header,
header.getContext() header.getContext()
)} )}
</TableHead> </TableHead>
) )
)} )}

View File

@@ -0,0 +1,15 @@
export function parseHostTarget(input: string) {
try {
const normalized = input.match(/^https?:\/\//) ? input : `http://${input}`;
const url = new URL(normalized);
const protocol = url.protocol.replace(":", ""); // http | https
const host = url.hostname;
const port = url.port ? parseInt(url.port, 10) : protocol === "https" ? 443 : 80;
return { protocol, host, port };
} catch {
return null;
}
}