mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-04 09:46:40 +00:00
Add Smart Host Parsing
This commit is contained in:
@@ -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>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|||||||
15
src/lib/parseHostTarget.ts
Normal file
15
src/lib/parseHostTarget.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user