mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-09 04:06:36 +00:00
adjust target config column
This commit is contained in:
@@ -469,6 +469,8 @@
|
|||||||
"proxyErrorInvalidHeader": "Invalid custom Host Header value. Use domain name format, or save empty to unset custom Host Header.",
|
"proxyErrorInvalidHeader": "Invalid custom Host Header value. Use domain name format, or save empty to unset custom Host Header.",
|
||||||
"proxyErrorTls": "Invalid TLS Server Name. Use domain name format, or save empty to remove the TLS Server Name.",
|
"proxyErrorTls": "Invalid TLS Server Name. Use domain name format, or save empty to remove the TLS Server Name.",
|
||||||
"proxyEnableSSL": "Enable SSL (https)",
|
"proxyEnableSSL": "Enable SSL (https)",
|
||||||
|
"target": "Target",
|
||||||
|
"configureTargets": "Configure Targets",
|
||||||
"targetErrorFetch": "Failed to fetch targets",
|
"targetErrorFetch": "Failed to fetch targets",
|
||||||
"targetErrorFetchDescription": "An error occurred while fetching targets",
|
"targetErrorFetchDescription": "An error occurred while fetching targets",
|
||||||
"siteErrorFetch": "Failed to fetch resource",
|
"siteErrorFetch": "Failed to fetch resource",
|
||||||
|
|||||||
@@ -110,6 +110,8 @@ import {
|
|||||||
} from "@app/components/PathMatchRenameModal";
|
} from "@app/components/PathMatchRenameModal";
|
||||||
import { Badge } from "@app/components/ui/badge";
|
import { Badge } from "@app/components/ui/badge";
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@app/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@app/components/ui/tooltip";
|
||||||
|
import { TargetModal } from "@app/components/TargetModal";
|
||||||
|
import { TargetDisplay } from "@app/components/TargetDisplay";
|
||||||
|
|
||||||
const addTargetSchema = z
|
const addTargetSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -537,11 +539,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
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -552,10 +554,10 @@ export default function ReverseProxyTargets(props: {
|
|||||||
targets.map((target) =>
|
targets.map((target) =>
|
||||||
target.targetId === targetId
|
target.targetId === targetId
|
||||||
? {
|
? {
|
||||||
...target,
|
...target,
|
||||||
...config,
|
...config,
|
||||||
updated: true
|
updated: true
|
||||||
}
|
}
|
||||||
: target
|
: target
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -743,9 +745,9 @@ export default function ReverseProxyTargets(props: {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="text"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="px-1"
|
className="h-8 w-8 p-0"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
updateTarget(row.original.targetId, {
|
updateTarget(row.original.targetId, {
|
||||||
@@ -760,7 +762,7 @@ export default function ReverseProxyTargets(props: {
|
|||||||
×
|
×
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* <MoveRight className="ml-1 h-4 w-4" /> */}
|
<MoveRight className="ml-1 h-4 w-4" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<PathMatchModal
|
<PathMatchModal
|
||||||
@@ -806,7 +808,7 @@ export default function ReverseProxyTargets(props: {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-1 items-center">
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
@@ -815,7 +817,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
|
||||||
@@ -892,91 +894,84 @@ export default function ReverseProxyTargets(props: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
...(resource.http
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
accessorKey: "method",
|
|
||||||
header: t("method"),
|
|
||||||
cell: ({ row }: { row: Row<LocalTarget> }) => (
|
|
||||||
<Select
|
|
||||||
defaultValue={row.original.method ?? ""}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
updateTarget(row.original.targetId, {
|
|
||||||
...row.original,
|
|
||||||
method: value
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
{row.original.method}
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="http">http</SelectItem>
|
|
||||||
<SelectItem value="https">https</SelectItem>
|
|
||||||
<SelectItem value="h2c">h2c</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
{
|
{
|
||||||
accessorKey: "ip",
|
accessorKey: "target",
|
||||||
header: t("targetAddr"),
|
header: t("target"),
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => {
|
||||||
<Input
|
const hasTarget = !!(row.original.ip || row.original.port || row.original.method);
|
||||||
defaultValue={row.original.ip}
|
|
||||||
className="min-w-[150px]"
|
|
||||||
onBlur={(e) => {
|
|
||||||
const input = e.target.value.trim();
|
|
||||||
const hasProtocol = /^(https?|h2c):\/\//.test(input);
|
|
||||||
const hasPort = /:\d+(?:\/|$)/.test(input);
|
|
||||||
|
|
||||||
if (hasProtocol || hasPort) {
|
return hasTarget ? (
|
||||||
const parsed = parseHostTarget(input);
|
<div className="flex items-center gap-1">
|
||||||
if (parsed) {
|
<TargetModal
|
||||||
|
value={{
|
||||||
|
method: row.original.method,
|
||||||
|
ip: row.original.ip,
|
||||||
|
port: row.original.port
|
||||||
|
}}
|
||||||
|
onChange={(config) =>
|
||||||
updateTarget(row.original.targetId, {
|
updateTarget(row.original.targetId, {
|
||||||
...row.original,
|
...row.original,
|
||||||
method: hasProtocol
|
...config
|
||||||
? 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 {
|
showMethod={resource.http}
|
||||||
|
trigger={
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="flex items-center gap-2 p-2 max-w-md w-full text-left cursor-pointer"
|
||||||
|
>
|
||||||
|
<TargetDisplay
|
||||||
|
value={{
|
||||||
|
method: row.original.method,
|
||||||
|
ip: row.original.ip,
|
||||||
|
port: row.original.port
|
||||||
|
}}
|
||||||
|
showMethod={resource.http}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
updateTarget(row.original.targetId, {
|
||||||
|
...row.original,
|
||||||
|
method: null,
|
||||||
|
ip: "",
|
||||||
|
port: undefined
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</Button>
|
||||||
|
<MoveRight className="mr-2 h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<TargetModal
|
||||||
|
value={{
|
||||||
|
method: row.original.method,
|
||||||
|
ip: row.original.ip,
|
||||||
|
port: row.original.port
|
||||||
|
}}
|
||||||
|
onChange={(config) =>
|
||||||
updateTarget(row.original.targetId, {
|
updateTarget(row.original.targetId, {
|
||||||
...row.original,
|
...row.original,
|
||||||
ip: input
|
...config
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
}}
|
showMethod={resource.http}
|
||||||
/>
|
trigger={
|
||||||
)
|
<Button variant="outline">
|
||||||
},
|
<Plus className="h-4 w-4 mr-2" />
|
||||||
{
|
{t("configureTarget")}
|
||||||
accessorKey: "port",
|
</Button>
|
||||||
header: t("targetPort"),
|
}
|
||||||
cell: ({ row }) => (
|
/>
|
||||||
<Input
|
);
|
||||||
type="number"
|
}
|
||||||
defaultValue={row.original.port}
|
|
||||||
className="min-w-[100px]"
|
|
||||||
onBlur={(e) =>
|
|
||||||
updateTarget(row.original.targetId, {
|
|
||||||
...row.original,
|
|
||||||
port: parseInt(e.target.value, 10)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "rewritePath",
|
accessorKey: "rewritePath",
|
||||||
@@ -990,7 +985,6 @@ export default function ReverseProxyTargets(props: {
|
|||||||
|
|
||||||
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,
|
||||||
@@ -1017,9 +1011,9 @@ export default function ReverseProxyTargets(props: {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="text"
|
className="h-8 w-8 p-0"
|
||||||
className="px-1"
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
updateTarget(row.original.targetId, {
|
updateTarget(row.original.targetId, {
|
||||||
@@ -1238,21 +1232,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>
|
||||||
@@ -1318,34 +1312,34 @@ export default function ReverseProxyTargets(props: {
|
|||||||
);
|
);
|
||||||
return selectedSite &&
|
return selectedSite &&
|
||||||
selectedSite.type ===
|
selectedSite.type ===
|
||||||
"newt"
|
"newt"
|
||||||
? (() => {
|
? (() => {
|
||||||
const dockerState =
|
const dockerState =
|
||||||
getDockerStateForSite(
|
getDockerStateForSite(
|
||||||
selectedSite.siteId
|
selectedSite.siteId
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<ContainersSelector
|
<ContainersSelector
|
||||||
site={
|
site={
|
||||||
selectedSite
|
selectedSite
|
||||||
}
|
}
|
||||||
containers={
|
containers={
|
||||||
dockerState.containers
|
dockerState.containers
|
||||||
}
|
}
|
||||||
isAvailable={
|
isAvailable={
|
||||||
dockerState.isAvailable
|
dockerState.isAvailable
|
||||||
}
|
}
|
||||||
onContainerSelect={
|
onContainerSelect={
|
||||||
handleContainerSelect
|
handleContainerSelect
|
||||||
}
|
}
|
||||||
onRefresh={() =>
|
onRefresh={() =>
|
||||||
refreshContainersForSite(
|
refreshContainersForSite(
|
||||||
selectedSite.siteId
|
selectedSite.siteId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})()
|
})()
|
||||||
: null;
|
: null;
|
||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
@@ -1558,7 +1552,7 @@ export default function ReverseProxyTargets(props: {
|
|||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</SettingsSectionForm>
|
</SettingsSectionForm>
|
||||||
<div className="">
|
<div className="overflow-x-auto">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
{table
|
{table
|
||||||
@@ -1573,12 +1567,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>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|||||||
44
src/components/TargetDisplay.tsx
Normal file
44
src/components/TargetDisplay.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { Globe, Hash, Shield } from "lucide-react";
|
||||||
|
|
||||||
|
interface TargetDisplayProps {
|
||||||
|
value: {
|
||||||
|
method?: string | null;
|
||||||
|
ip?: string;
|
||||||
|
port?: number;
|
||||||
|
};
|
||||||
|
showMethod?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TargetDisplay({ value, showMethod = true }: TargetDisplayProps) {
|
||||||
|
const { method, ip, port } = value;
|
||||||
|
|
||||||
|
if (!ip && !port && !method) {
|
||||||
|
return <span className="text-muted-foreground text-sm">Not configured</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-0 text-sm font-mono">
|
||||||
|
{showMethod && method && (
|
||||||
|
<span className="inline-flex items-center gap-1 font-medium">
|
||||||
|
{method === "https" && <Shield className="h-3 w-3 text-green-600 dark:text-green-400" />}
|
||||||
|
<span className={method === "https" ? "text-green-600 dark:text-green-400" : ""}>
|
||||||
|
{method}<span className="text-muted-foreground">://</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{ip && (
|
||||||
|
<span className="inline-flex items-center font-medium">
|
||||||
|
{ip}
|
||||||
|
{port && <span className="text-muted-foreground">:</span>}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{port && (
|
||||||
|
<span className="inline-flex items-center text-muted-foreground font-medium">
|
||||||
|
{port}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
144
src/components/TargetModal.tsx
Normal file
144
src/components/TargetModal.tsx
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
interface TargetConfig {
|
||||||
|
method?: string | null;
|
||||||
|
ip?: string;
|
||||||
|
port?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TargetModalProps {
|
||||||
|
value: TargetConfig;
|
||||||
|
onChange: (config: TargetConfig) => void;
|
||||||
|
trigger: React.ReactNode;
|
||||||
|
showMethod?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TargetModal({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
trigger,
|
||||||
|
showMethod = true
|
||||||
|
}: TargetModalProps) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [config, setConfig] = useState<TargetConfig>(value);
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
onChange(config);
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseHostTarget = (input: string) => {
|
||||||
|
const protocolMatch = input.match(/^(https?|h2c):\/\//);
|
||||||
|
const protocol = protocolMatch ? protocolMatch[1] : null;
|
||||||
|
const withoutProtocol = input.replace(/^(https?|h2c):\/\//, '');
|
||||||
|
|
||||||
|
const portMatch = withoutProtocol.match(/:(\d+)(?:\/|$)/);
|
||||||
|
const port = portMatch ? parseInt(portMatch[1], 10) : null;
|
||||||
|
const host = withoutProtocol.replace(/:\d+(?:\/|$)/, '').replace(/\/$/, '');
|
||||||
|
|
||||||
|
return { protocol, host, port };
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleHostChange = (input: string) => {
|
||||||
|
const trimmed = input.trim();
|
||||||
|
const hasProtocol = /^(https?|h2c):\/\//.test(trimmed);
|
||||||
|
const hasPort = /:\d+(?:\/|$)/.test(trimmed);
|
||||||
|
|
||||||
|
if (hasProtocol || hasPort) {
|
||||||
|
const parsed = parseHostTarget(trimmed);
|
||||||
|
setConfig({
|
||||||
|
...config,
|
||||||
|
...(hasProtocol && parsed.protocol ? { method: parsed.protocol } : {}),
|
||||||
|
ip: parsed.host,
|
||||||
|
...(hasPort && parsed.port ? { port: parsed.port } : {})
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setConfig({ ...config, ip: trimmed });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogTrigger asChild>{trigger}</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-[500px]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Configure Target</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="grid gap-4 py-4">
|
||||||
|
{showMethod && (
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="method">Method</Label>
|
||||||
|
<Select
|
||||||
|
value={config.method || "http"}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setConfig({ ...config, method: value })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="method">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="http">http</SelectItem>
|
||||||
|
<SelectItem value="https">https</SelectItem>
|
||||||
|
<SelectItem value="h2c">h2c</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="host">IP Address / Hostname</Label>
|
||||||
|
<Input
|
||||||
|
id="host"
|
||||||
|
placeholder="e.g., 192.168.1.1 or example.com"
|
||||||
|
value={config.ip || ""}
|
||||||
|
onChange={(e) => setConfig({ ...config, ip: e.target.value })}
|
||||||
|
onBlur={(e) => handleHostChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
You can also paste: http://example.com:8080
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="port">Port</Label>
|
||||||
|
<Input
|
||||||
|
id="port"
|
||||||
|
type="number"
|
||||||
|
placeholder="e.g., 8080"
|
||||||
|
value={config.port || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setConfig({
|
||||||
|
...config,
|
||||||
|
port: parseInt(e.target.value, 10) || undefined
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button variant="outline" onClick={() => setOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSave}>Save</Button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user