mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-12 05:36:38 +00:00
improved private resource modal
This commit is contained in:
@@ -2318,5 +2318,19 @@
|
||||
"resourceLoginPageDescription": "Customize the login page for individual resources",
|
||||
"enterConfirmation": "Enter confirmation",
|
||||
"blueprintViewDetails": "Details",
|
||||
"defaultIdentityProvider": "Default Identity Provider"
|
||||
"defaultIdentityProvider": "Default Identity Provider",
|
||||
"editInternalResourceDialogNetworkSettings": "Network Settings",
|
||||
"editInternalResourceDialogAccessPolicy": "Access Policy",
|
||||
"editInternalResourceDialogAddRoles": "Add Roles",
|
||||
"editInternalResourceDialogAddUsers": "Add Users",
|
||||
"editInternalResourceDialogAddClients": "Add Clients",
|
||||
"editInternalResourceDialogDestinationLabel": "Destination",
|
||||
"editInternalResourceDialogDestinationDescription": "Specify the destination address for the internal resource. This can be a hostname, IP address, or CIDR range depending on the selected mode. Optionally set an internal DNS alias for easier identification.",
|
||||
"editInternalResourceDialogPortRestrictionsDescription": "Restrict access to specific TCP/UDP ports or allow/block all ports.",
|
||||
"editInternalResourceDialogTcp": "TCP",
|
||||
"editInternalResourceDialogUdp": "UDP",
|
||||
"editInternalResourceDialogIcmp": "ICMP",
|
||||
"editInternalResourceDialogAccessControl": "Access Control",
|
||||
"editInternalResourceDialogAccessControlDescription": "Control which roles, users, and machine clients have access to this resource when connected. Admins always have access.",
|
||||
"editInternalResourceDialogPortRangeValidationError": "Port range must be \"*\" for all ports, or a comma-separated list of ports and ranges (e.g., \"80,443,8000-9000\"). Ports must be between 1 and 65535."
|
||||
}
|
||||
|
||||
@@ -329,8 +329,11 @@ export default function ClientResourcesTable({
|
||||
orgId={orgId}
|
||||
sites={sites}
|
||||
onSuccess={() => {
|
||||
// Delay refresh to allow modal to close smoothly
|
||||
setTimeout(() => {
|
||||
router.refresh();
|
||||
setEditingResource(null);
|
||||
}, 150);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -341,7 +344,10 @@ export default function ClientResourcesTable({
|
||||
orgId={orgId}
|
||||
sites={sites}
|
||||
onSuccess={() => {
|
||||
// Delay refresh to allow modal to close smoothly
|
||||
setTimeout(() => {
|
||||
router.refresh();
|
||||
}, 150);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -58,6 +58,7 @@ import { useTranslations } from "next-intl";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { HorizontalTabs, TabItem } from "@app/components/HorizontalTabs";
|
||||
// import { InfoPopup } from "@app/components/ui/info-popup";
|
||||
|
||||
// Helper to validate port range string format
|
||||
@@ -108,17 +109,18 @@ const isValidPortRangeString = (val: string | undefined | null): boolean => {
|
||||
};
|
||||
|
||||
// Port range string schema for client-side validation
|
||||
const portRangeStringSchema = z
|
||||
// Note: This schema is defined outside the component, so we'll use a function to get the message
|
||||
const getPortRangeValidationMessage = (t: (key: string) => string) =>
|
||||
t("editInternalResourceDialogPortRangeValidationError");
|
||||
|
||||
const createPortRangeStringSchema = (t: (key: string) => string) =>
|
||||
z
|
||||
.string()
|
||||
.optional()
|
||||
.nullable()
|
||||
.refine(
|
||||
(val) => isValidPortRangeString(val),
|
||||
{
|
||||
message:
|
||||
'Port range must be "*" for all ports, or a comma-separated list of ports and ranges (e.g., "80,443,8000-9000"). Ports must be between 1 and 65535.'
|
||||
}
|
||||
);
|
||||
.refine((val) => isValidPortRangeString(val), {
|
||||
message: getPortRangeValidationMessage(t)
|
||||
});
|
||||
|
||||
// Helper to determine the port mode from a port range string
|
||||
type PortMode = "all" | "blocked" | "custom";
|
||||
@@ -161,25 +163,18 @@ export default function CreateInternalResourceDialog({
|
||||
.string()
|
||||
.min(1, t("createInternalResourceDialogNameRequired"))
|
||||
.max(255, t("createInternalResourceDialogNameMaxLength")),
|
||||
// mode: z.enum(["host", "cidr", "port"]),
|
||||
mode: z.enum(["host", "cidr"]),
|
||||
destination: z.string().min(1),
|
||||
siteId: z
|
||||
.int()
|
||||
.positive(t("createInternalResourceDialogPleaseSelectSite")),
|
||||
// protocol: z.enum(["tcp", "udp"]),
|
||||
// proxyPort: z.int()
|
||||
// .positive()
|
||||
// .min(1, t("createInternalResourceDialogProxyPortMin"))
|
||||
// .max(65535, t("createInternalResourceDialogProxyPortMax")),
|
||||
// destinationPort: z.int()
|
||||
// .positive()
|
||||
// .min(1, t("createInternalResourceDialogDestinationPortMin"))
|
||||
// .max(65535, t("createInternalResourceDialogDestinationPortMax"))
|
||||
// .nullish(),
|
||||
// mode: z.enum(["host", "cidr", "port"]),
|
||||
mode: z.enum(["host", "cidr"]),
|
||||
// protocol: z.enum(["tcp", "udp"]).nullish(),
|
||||
// proxyPort: z.int().positive().min(1, t("createInternalResourceDialogProxyPortMin")).max(65535, t("createInternalResourceDialogProxyPortMax")).nullish(),
|
||||
destination: z.string().min(1),
|
||||
// destinationPort: z.int().positive().min(1, t("createInternalResourceDialogDestinationPortMin")).max(65535, t("createInternalResourceDialogDestinationPortMax")).nullish(),
|
||||
alias: z.string().nullish(),
|
||||
tcpPortRangeString: portRangeStringSchema,
|
||||
udpPortRangeString: portRangeStringSchema,
|
||||
tcpPortRangeString: createPortRangeStringSchema(t),
|
||||
udpPortRangeString: createPortRangeStringSchema(t),
|
||||
disableIcmp: z.boolean().optional(),
|
||||
roles: z
|
||||
.array(
|
||||
@@ -453,8 +448,8 @@ export default function CreateInternalResourceDialog({
|
||||
variant: "default"
|
||||
});
|
||||
|
||||
onSuccess?.();
|
||||
setOpen(false);
|
||||
onSuccess?.();
|
||||
} catch (error) {
|
||||
console.error("Error creating internal resource:", error);
|
||||
toast({
|
||||
@@ -498,7 +493,7 @@ export default function CreateInternalResourceDialog({
|
||||
|
||||
return (
|
||||
<Credenza open={open} onOpenChange={setOpen}>
|
||||
<CredenzaContent className="max-w-2xl">
|
||||
<CredenzaContent className="max-w-3xl">
|
||||
<CredenzaHeader>
|
||||
<CredenzaTitle>
|
||||
{t("createInternalResourceDialogCreateClientResource")}
|
||||
@@ -516,14 +511,8 @@ export default function CreateInternalResourceDialog({
|
||||
className="space-y-6"
|
||||
id="create-internal-resource-form"
|
||||
>
|
||||
{/* Resource Properties Form */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
{t(
|
||||
"createInternalResourceDialogResourceProperties"
|
||||
)}
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
{/* Name and Site - Side by Side */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
@@ -547,11 +536,7 @@ export default function CreateInternalResourceDialog({
|
||||
name="siteId"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel>
|
||||
{t(
|
||||
"site"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormLabel>{t("site")}</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
@@ -566,9 +551,7 @@ export default function CreateInternalResourceDialog({
|
||||
>
|
||||
{field.value
|
||||
? availableSites.find(
|
||||
(
|
||||
site
|
||||
) =>
|
||||
(site) =>
|
||||
site.siteId ===
|
||||
field.value
|
||||
)?.name
|
||||
@@ -594,9 +577,7 @@ export default function CreateInternalResourceDialog({
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{availableSites.map(
|
||||
(
|
||||
site
|
||||
) => (
|
||||
(site) => (
|
||||
<CommandItem
|
||||
key={
|
||||
site.siteId
|
||||
@@ -634,7 +615,59 @@ export default function CreateInternalResourceDialog({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Tabs for Network Settings and Access Control */}
|
||||
<HorizontalTabs
|
||||
clientSide={true}
|
||||
defaultTab={0}
|
||||
items={[
|
||||
{
|
||||
title: t(
|
||||
"editInternalResourceDialogNetworkSettings"
|
||||
),
|
||||
href: "#"
|
||||
},
|
||||
{
|
||||
title: t(
|
||||
"editInternalResourceDialogAccessPolicy"
|
||||
),
|
||||
href: "#"
|
||||
}
|
||||
]}
|
||||
>
|
||||
{/* Network Settings Tab */}
|
||||
<div className="space-y-4 mt-4">
|
||||
<div>
|
||||
<div className="mb-8">
|
||||
<label className="font-medium block">
|
||||
{t(
|
||||
"editInternalResourceDialogDestinationLabel"
|
||||
)}
|
||||
</label>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t(
|
||||
"editInternalResourceDialogDestinationDescription"
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"grid gap-4 items-start",
|
||||
mode === "cidr"
|
||||
? "grid-cols-4"
|
||||
: "grid-cols-12"
|
||||
)}
|
||||
>
|
||||
{/* Mode - Smaller select */}
|
||||
<div
|
||||
className={
|
||||
mode === "cidr"
|
||||
? "col-span-1"
|
||||
: "col-span-3"
|
||||
}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="mode"
|
||||
@@ -649,7 +682,9 @@ export default function CreateInternalResourceDialog({
|
||||
onValueChange={
|
||||
field.onChange
|
||||
}
|
||||
value={field.value}
|
||||
value={
|
||||
field.value
|
||||
}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
@@ -657,7 +692,6 @@ export default function CreateInternalResourceDialog({
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{/* <SelectItem value="port">{t("createInternalResourceDialogModePort")}</SelectItem> */}
|
||||
<SelectItem value="host">
|
||||
{t(
|
||||
"createInternalResourceDialogModeHost"
|
||||
@@ -674,76 +708,16 @@ export default function CreateInternalResourceDialog({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{/*
|
||||
{mode === "port" && (
|
||||
<>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="protocol"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("createInternalResourceDialogProtocol")}
|
||||
</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
value={field.value ?? undefined}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="tcp">
|
||||
{t("createInternalResourceDialogTcp")}
|
||||
</SelectItem>
|
||||
<SelectItem value="udp">
|
||||
{t("createInternalResourceDialogUdp")}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="proxyPort"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("createInternalResourceDialogSitePort")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
value={field.value || ""}
|
||||
onChange={(e) =>
|
||||
field.onChange(
|
||||
e.target.value === "" ? undefined : parseInt(e.target.value)
|
||||
)
|
||||
{/* Destination - Larger input */}
|
||||
<div
|
||||
className={
|
||||
mode === "cidr"
|
||||
? "col-span-3"
|
||||
: "col-span-5"
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)} */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Target Configuration Form */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
{t(
|
||||
"createInternalResourceDialogTargetConfiguration"
|
||||
)}
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="destination"
|
||||
@@ -754,59 +728,20 @@ export default function CreateInternalResourceDialog({
|
||||
"createInternalResourceDialogDestination"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{mode === "host" &&
|
||||
t(
|
||||
"createInternalResourceDialogDestinationHostDescription"
|
||||
)}
|
||||
{mode === "cidr" &&
|
||||
t(
|
||||
"createInternalResourceDialogDestinationCidrDescription"
|
||||
)}
|
||||
{/* {mode === "port" && t("createInternalResourceDialogDestinationIPDescription")} */}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* {mode === "port" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="destinationPort"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("targetPort")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
value={field.value || ""}
|
||||
onChange={(e) =>
|
||||
field.onChange(
|
||||
e.target.value === "" ? undefined : parseInt(e.target.value)
|
||||
)
|
||||
}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t("createInternalResourceDialogDestinationPortDescription")}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)} */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Alias */}
|
||||
{/* Alias - Equally sized input (if allowed) */}
|
||||
{mode !== "cidr" && (
|
||||
<div>
|
||||
<div className="col-span-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="alias"
|
||||
@@ -821,82 +756,137 @@ export default function CreateInternalResourceDialog({
|
||||
<Input
|
||||
{...field}
|
||||
value={
|
||||
field.value ?? ""
|
||||
field.value ??
|
||||
""
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"createInternalResourceDialogAliasDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Port Restrictions Section */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
{t("portRestrictions")}
|
||||
</h3>
|
||||
{/* Ports and Restrictions */}
|
||||
<div className="space-y-4">
|
||||
{/* TCP Ports */}
|
||||
<div className="my-8">
|
||||
<label className="font-medium block">
|
||||
{t("portRestrictions")}
|
||||
</label>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t(
|
||||
"editInternalResourceDialogPortRestrictionsDescription"
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"grid gap-4 items-start",
|
||||
mode === "cidr"
|
||||
? "grid-cols-4"
|
||||
: "grid-cols-12"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
mode === "cidr"
|
||||
? "col-span-1"
|
||||
: "col-span-3"
|
||||
}
|
||||
>
|
||||
<FormLabel className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
||||
{t(
|
||||
"editInternalResourceDialogTcp"
|
||||
)}
|
||||
</FormLabel>
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
mode === "cidr"
|
||||
? "col-span-3"
|
||||
: "col-span-9"
|
||||
}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="tcpPortRangeString"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="flex items-center gap-2">
|
||||
<FormLabel className="min-w-10">
|
||||
TCP
|
||||
</FormLabel>
|
||||
{/*<InfoPopup
|
||||
info={t("tcpPortsDescription")}
|
||||
/>*/}
|
||||
<Select
|
||||
value={tcpPortMode}
|
||||
onValueChange={(value: PortMode) => {
|
||||
setTcpPortMode(value);
|
||||
value={
|
||||
tcpPortMode
|
||||
}
|
||||
onValueChange={(
|
||||
value: PortMode
|
||||
) => {
|
||||
setTcpPortMode(
|
||||
value
|
||||
);
|
||||
}}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger className="w-[110px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">
|
||||
{t("allPorts")}
|
||||
{t(
|
||||
"allPorts"
|
||||
)}
|
||||
</SelectItem>
|
||||
<SelectItem value="blocked">
|
||||
{t("blocked")}
|
||||
{t(
|
||||
"blocked"
|
||||
)}
|
||||
</SelectItem>
|
||||
<SelectItem value="custom">
|
||||
{t("custom")}
|
||||
{t(
|
||||
"custom"
|
||||
)}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{tcpPortMode === "custom" ? (
|
||||
{tcpPortMode ===
|
||||
"custom" ? (
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="80,443,8000-9000"
|
||||
value={tcpCustomPorts}
|
||||
onChange={(e) =>
|
||||
setTcpCustomPorts(e.target.value)
|
||||
value={
|
||||
tcpCustomPorts
|
||||
}
|
||||
onChange={(
|
||||
e
|
||||
) =>
|
||||
setTcpCustomPorts(
|
||||
e
|
||||
.target
|
||||
.value
|
||||
)
|
||||
}
|
||||
className="flex-1"
|
||||
/>
|
||||
</FormControl>
|
||||
) : (
|
||||
<Input
|
||||
disabled
|
||||
placeholder={
|
||||
tcpPortMode === "all"
|
||||
? t("allPortsAllowed")
|
||||
: t("allPortsBlocked")
|
||||
tcpPortMode ===
|
||||
"all"
|
||||
? t(
|
||||
"allPortsAllowed"
|
||||
)
|
||||
: t(
|
||||
"allPortsBlocked"
|
||||
)
|
||||
}
|
||||
className="flex-1"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -904,61 +894,114 @@ export default function CreateInternalResourceDialog({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* UDP Ports */}
|
||||
<div
|
||||
className={cn(
|
||||
"grid gap-4 items-start",
|
||||
mode === "cidr"
|
||||
? "grid-cols-4"
|
||||
: "grid-cols-12"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
mode === "cidr"
|
||||
? "col-span-1"
|
||||
: "col-span-3"
|
||||
}
|
||||
>
|
||||
<FormLabel className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
||||
{t(
|
||||
"editInternalResourceDialogUdp"
|
||||
)}
|
||||
</FormLabel>
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
mode === "cidr"
|
||||
? "col-span-3"
|
||||
: "col-span-9"
|
||||
}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="udpPortRangeString"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="flex items-center gap-2">
|
||||
<FormLabel className="min-w-10">
|
||||
UDP
|
||||
</FormLabel>
|
||||
{/*<InfoPopup
|
||||
info={t("udpPortsDescription")}
|
||||
/>*/}
|
||||
<Select
|
||||
value={udpPortMode}
|
||||
onValueChange={(value: PortMode) => {
|
||||
setUdpPortMode(value);
|
||||
value={
|
||||
udpPortMode
|
||||
}
|
||||
onValueChange={(
|
||||
value: PortMode
|
||||
) => {
|
||||
setUdpPortMode(
|
||||
value
|
||||
);
|
||||
}}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger className="w-[110px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">
|
||||
{t("allPorts")}
|
||||
{t(
|
||||
"allPorts"
|
||||
)}
|
||||
</SelectItem>
|
||||
<SelectItem value="blocked">
|
||||
{t("blocked")}
|
||||
{t(
|
||||
"blocked"
|
||||
)}
|
||||
</SelectItem>
|
||||
<SelectItem value="custom">
|
||||
{t("custom")}
|
||||
{t(
|
||||
"custom"
|
||||
)}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{udpPortMode === "custom" ? (
|
||||
{udpPortMode ===
|
||||
"custom" ? (
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="53,123,500-600"
|
||||
value={udpCustomPorts}
|
||||
onChange={(e) =>
|
||||
setUdpCustomPorts(e.target.value)
|
||||
value={
|
||||
udpCustomPorts
|
||||
}
|
||||
onChange={(
|
||||
e
|
||||
) =>
|
||||
setUdpCustomPorts(
|
||||
e
|
||||
.target
|
||||
.value
|
||||
)
|
||||
}
|
||||
className="flex-1"
|
||||
/>
|
||||
</FormControl>
|
||||
) : (
|
||||
<Input
|
||||
disabled
|
||||
placeholder={
|
||||
udpPortMode === "all"
|
||||
? t("allPortsAllowed")
|
||||
: t("allPortsBlocked")
|
||||
udpPortMode ===
|
||||
"all"
|
||||
? t(
|
||||
"allPortsAllowed"
|
||||
)
|
||||
: t(
|
||||
"allPortsBlocked"
|
||||
)
|
||||
}
|
||||
className="flex-1"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -966,25 +1009,66 @@ export default function CreateInternalResourceDialog({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ICMP Toggle */}
|
||||
<div
|
||||
className={cn(
|
||||
"grid gap-4 items-start",
|
||||
mode === "cidr"
|
||||
? "grid-cols-4"
|
||||
: "grid-cols-12"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
mode === "cidr"
|
||||
? "col-span-1"
|
||||
: "col-span-3"
|
||||
}
|
||||
>
|
||||
<FormLabel className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
||||
{t(
|
||||
"editInternalResourceDialogIcmp"
|
||||
)}
|
||||
</FormLabel>
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
mode === "cidr"
|
||||
? "col-span-3"
|
||||
: "col-span-9"
|
||||
}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="disableIcmp"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="flex items-center gap-2">
|
||||
<FormLabel className="min-w-10">
|
||||
ICMP
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={!field.value}
|
||||
onCheckedChange={(checked) => field.onChange(!checked)}
|
||||
checked={
|
||||
!field.value
|
||||
}
|
||||
onCheckedChange={(
|
||||
checked
|
||||
) =>
|
||||
field.onChange(
|
||||
!checked
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{field.value ? t("blocked") : t("allowed")}
|
||||
{field.value
|
||||
? t(
|
||||
"blocked"
|
||||
)
|
||||
: t(
|
||||
"allowed"
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<FormMessage />
|
||||
@@ -993,13 +1077,25 @@ export default function CreateInternalResourceDialog({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Access Control Section */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
{t("resourceUsersRoles")}
|
||||
</h3>
|
||||
{/* Access Control Tab */}
|
||||
<div className="space-y-4 mt-4">
|
||||
<div className="mb-8">
|
||||
<label className="font-medium block">
|
||||
{t(
|
||||
"editInternalResourceDialogAccessControl"
|
||||
)}
|
||||
</label>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t(
|
||||
"editInternalResourceDialogAccessControlDescription"
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{/* Roles */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="roles"
|
||||
@@ -1023,9 +1119,12 @@ export default function CreateInternalResourceDialog({
|
||||
size="sm"
|
||||
tags={
|
||||
form.getValues()
|
||||
.roles || []
|
||||
.roles ||
|
||||
[]
|
||||
}
|
||||
setTags={(newRoles) => {
|
||||
setTags={(
|
||||
newRoles
|
||||
) => {
|
||||
form.setValue(
|
||||
"roles",
|
||||
newRoles as [
|
||||
@@ -1040,7 +1139,9 @@ export default function CreateInternalResourceDialog({
|
||||
autocompleteOptions={
|
||||
allRoles
|
||||
}
|
||||
allowDuplicates={false}
|
||||
allowDuplicates={
|
||||
false
|
||||
}
|
||||
restrictTagsToAutocompleteOptions={
|
||||
true
|
||||
}
|
||||
@@ -1056,6 +1157,8 @@ export default function CreateInternalResourceDialog({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Users */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="users"
|
||||
@@ -1078,10 +1181,13 @@ export default function CreateInternalResourceDialog({
|
||||
)}
|
||||
tags={
|
||||
form.getValues()
|
||||
.users || []
|
||||
.users ||
|
||||
[]
|
||||
}
|
||||
size="sm"
|
||||
setTags={(newUsers) => {
|
||||
setTags={(
|
||||
newUsers
|
||||
) => {
|
||||
form.setValue(
|
||||
"users",
|
||||
newUsers as [
|
||||
@@ -1096,7 +1202,9 @@ export default function CreateInternalResourceDialog({
|
||||
autocompleteOptions={
|
||||
allUsers
|
||||
}
|
||||
allowDuplicates={false}
|
||||
allowDuplicates={
|
||||
false
|
||||
}
|
||||
restrictTagsToAutocompleteOptions={
|
||||
true
|
||||
}
|
||||
@@ -1107,6 +1215,8 @@ export default function CreateInternalResourceDialog({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Clients (Machines) */}
|
||||
{hasMachineClients && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
@@ -1114,7 +1224,9 @@ export default function CreateInternalResourceDialog({
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col items-start">
|
||||
<FormLabel>
|
||||
{t("machineClients")}
|
||||
{t(
|
||||
"machineClients"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<TagInput
|
||||
@@ -1160,7 +1272,9 @@ export default function CreateInternalResourceDialog({
|
||||
restrictTagsToAutocompleteOptions={
|
||||
true
|
||||
}
|
||||
sortTags={true}
|
||||
sortTags={
|
||||
true
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
@@ -1170,6 +1284,7 @@ export default function CreateInternalResourceDialog({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</HorizontalTabs>
|
||||
</form>
|
||||
</Form>
|
||||
</CredenzaBody>
|
||||
|
||||
@@ -56,7 +56,14 @@ import {
|
||||
} from "@app/components/ui/popover";
|
||||
import { cn } from "@app/lib/cn";
|
||||
import { ListSitesResponse } from "@server/routers/site";
|
||||
import { Check, ChevronsUpDown } from "lucide-react";
|
||||
import { Check, ChevronsUpDown, ChevronDown } from "lucide-react";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger
|
||||
} from "@app/components/ui/collapsible";
|
||||
import { HorizontalTabs, TabItem } from "@app/components/HorizontalTabs";
|
||||
import { Separator } from "@app/components/ui/separator";
|
||||
// import { InfoPopup } from "@app/components/ui/info-popup";
|
||||
|
||||
// Helper to validate port range string format
|
||||
@@ -85,7 +92,12 @@ const isValidPortRangeString = (val: string | undefined | null): boolean => {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (startPort < 1 || startPort > 65535 || endPort < 1 || endPort > 65535) {
|
||||
if (
|
||||
startPort < 1 ||
|
||||
startPort > 65535 ||
|
||||
endPort < 1 ||
|
||||
endPort > 65535
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -107,17 +119,18 @@ const isValidPortRangeString = (val: string | undefined | null): boolean => {
|
||||
};
|
||||
|
||||
// Port range string schema for client-side validation
|
||||
const portRangeStringSchema = z
|
||||
// Note: This schema is defined outside the component, so we'll use a function to get the message
|
||||
const getPortRangeValidationMessage = (t: (key: string) => string) =>
|
||||
t("editInternalResourceDialogPortRangeValidationError");
|
||||
|
||||
const createPortRangeStringSchema = (t: (key: string) => string) =>
|
||||
z
|
||||
.string()
|
||||
.optional()
|
||||
.nullable()
|
||||
.refine(
|
||||
(val) => isValidPortRangeString(val),
|
||||
{
|
||||
message:
|
||||
'Port range must be "*" for all ports, or a comma-separated list of ports and ranges (e.g., "80,443,8000-9000"). Ports must be between 1 and 65535.'
|
||||
}
|
||||
);
|
||||
.refine((val) => isValidPortRangeString(val), {
|
||||
message: getPortRangeValidationMessage(t)
|
||||
});
|
||||
|
||||
// Helper to determine the port mode from a port range string
|
||||
type PortMode = "all" | "blocked" | "custom";
|
||||
@@ -128,7 +141,10 @@ const getPortModeFromString = (val: string | undefined | null): PortMode => {
|
||||
};
|
||||
|
||||
// Helper to get the port string for API from mode and custom value
|
||||
const getPortStringFromMode = (mode: PortMode, customValue: string): string | undefined => {
|
||||
const getPortStringFromMode = (
|
||||
mode: PortMode,
|
||||
customValue: string
|
||||
): string | undefined => {
|
||||
if (mode === "all") return "*";
|
||||
if (mode === "blocked") return "";
|
||||
return customValue;
|
||||
@@ -188,8 +204,8 @@ export default function EditInternalResourceDialog({
|
||||
destination: z.string().min(1),
|
||||
// destinationPort: z.int().positive().min(1, t("editInternalResourceDialogDestinationPortMin")).max(65535, t("editInternalResourceDialogDestinationPortMax")).nullish(),
|
||||
alias: z.string().nullish(),
|
||||
tcpPortRangeString: portRangeStringSchema,
|
||||
udpPortRangeString: portRangeStringSchema,
|
||||
tcpPortRangeString: createPortRangeStringSchema(t),
|
||||
udpPortRangeString: createPortRangeStringSchema(t),
|
||||
disableIcmp: z.boolean().optional(),
|
||||
roles: z
|
||||
.array(
|
||||
@@ -352,6 +368,9 @@ export default function EditInternalResourceDialog({
|
||||
number | null
|
||||
>(null);
|
||||
|
||||
// Collapsible state for ports and restrictions
|
||||
const [isPortsExpanded, setIsPortsExpanded] = useState(false);
|
||||
|
||||
// Port restriction UI state
|
||||
const [tcpPortMode, setTcpPortMode] = useState<PortMode>(
|
||||
getPortModeFromString(resource.tcpPortRangeString)
|
||||
@@ -446,9 +465,7 @@ export default function EditInternalResourceDialog({
|
||||
}
|
||||
|
||||
// Update the site resource
|
||||
await api.post(
|
||||
`/site-resource/${resource.id}`,
|
||||
{
|
||||
await api.post(`/site-resource/${resource.id}`, {
|
||||
name: data.name,
|
||||
siteId: data.siteId,
|
||||
mode: data.mode,
|
||||
@@ -468,8 +485,7 @@ export default function EditInternalResourceDialog({
|
||||
roleIds: (data.roles || []).map((r) => parseInt(r.id)),
|
||||
userIds: (data.users || []).map((u) => u.id),
|
||||
clientIds: (data.clients || []).map((c) => parseInt(c.id))
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// Update roles, users, and clients
|
||||
// await Promise.all([
|
||||
@@ -502,8 +518,8 @@ export default function EditInternalResourceDialog({
|
||||
variant: "default"
|
||||
});
|
||||
|
||||
onSuccess?.();
|
||||
setOpen(false);
|
||||
onSuccess?.();
|
||||
} catch (error) {
|
||||
console.error("Error updating internal resource:", error);
|
||||
toast({
|
||||
@@ -543,18 +559,26 @@ export default function EditInternalResourceDialog({
|
||||
clients: []
|
||||
});
|
||||
// Reset port mode state
|
||||
setTcpPortMode(getPortModeFromString(resource.tcpPortRangeString));
|
||||
setUdpPortMode(getPortModeFromString(resource.udpPortRangeString));
|
||||
setTcpPortMode(
|
||||
getPortModeFromString(resource.tcpPortRangeString)
|
||||
);
|
||||
setUdpPortMode(
|
||||
getPortModeFromString(resource.udpPortRangeString)
|
||||
);
|
||||
setTcpCustomPorts(
|
||||
resource.tcpPortRangeString && resource.tcpPortRangeString !== "*"
|
||||
resource.tcpPortRangeString &&
|
||||
resource.tcpPortRangeString !== "*"
|
||||
? resource.tcpPortRangeString
|
||||
: ""
|
||||
);
|
||||
setUdpCustomPorts(
|
||||
resource.udpPortRangeString && resource.udpPortRangeString !== "*"
|
||||
resource.udpPortRangeString &&
|
||||
resource.udpPortRangeString !== "*"
|
||||
? resource.udpPortRangeString
|
||||
: ""
|
||||
);
|
||||
// Reset visibility states
|
||||
setIsPortsExpanded(false);
|
||||
previousResourceId.current = resource.id;
|
||||
}
|
||||
|
||||
@@ -602,25 +626,33 @@ export default function EditInternalResourceDialog({
|
||||
clients: []
|
||||
});
|
||||
// Reset port mode state
|
||||
setTcpPortMode(getPortModeFromString(resource.tcpPortRangeString));
|
||||
setUdpPortMode(getPortModeFromString(resource.udpPortRangeString));
|
||||
setTcpPortMode(
|
||||
getPortModeFromString(resource.tcpPortRangeString)
|
||||
);
|
||||
setUdpPortMode(
|
||||
getPortModeFromString(resource.udpPortRangeString)
|
||||
);
|
||||
setTcpCustomPorts(
|
||||
resource.tcpPortRangeString && resource.tcpPortRangeString !== "*"
|
||||
resource.tcpPortRangeString &&
|
||||
resource.tcpPortRangeString !== "*"
|
||||
? resource.tcpPortRangeString
|
||||
: ""
|
||||
);
|
||||
setUdpCustomPorts(
|
||||
resource.udpPortRangeString && resource.udpPortRangeString !== "*"
|
||||
resource.udpPortRangeString &&
|
||||
resource.udpPortRangeString !== "*"
|
||||
? resource.udpPortRangeString
|
||||
: ""
|
||||
);
|
||||
// Reset visibility states
|
||||
setIsPortsExpanded(false);
|
||||
// Reset previous resource ID to ensure clean state on next open
|
||||
previousResourceId.current = null;
|
||||
}
|
||||
setOpen(open);
|
||||
}}
|
||||
>
|
||||
<CredenzaContent className="max-w-2xl">
|
||||
<CredenzaContent className="max-w-3xl">
|
||||
<CredenzaHeader>
|
||||
<CredenzaTitle>
|
||||
{t("editInternalResourceDialogEditClientResource")}
|
||||
@@ -639,14 +671,8 @@ export default function EditInternalResourceDialog({
|
||||
className="space-y-6"
|
||||
id="edit-internal-resource-form"
|
||||
>
|
||||
{/* Resource Properties Form */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
{t(
|
||||
"editInternalResourceDialogResourceProperties"
|
||||
)}
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
{/* Name and Site - Side by Side */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
@@ -670,11 +696,7 @@ export default function EditInternalResourceDialog({
|
||||
name="siteId"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel>
|
||||
{t(
|
||||
"site"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormLabel>{t("site")}</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
@@ -689,9 +711,7 @@ export default function EditInternalResourceDialog({
|
||||
>
|
||||
{field.value
|
||||
? availableSites.find(
|
||||
(
|
||||
site
|
||||
) =>
|
||||
(site) =>
|
||||
site.siteId ===
|
||||
field.value
|
||||
)?.name
|
||||
@@ -717,9 +737,7 @@ export default function EditInternalResourceDialog({
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{availableSites.map(
|
||||
(
|
||||
site
|
||||
) => (
|
||||
(site) => (
|
||||
<CommandItem
|
||||
key={
|
||||
site.siteId
|
||||
@@ -757,7 +775,59 @@ export default function EditInternalResourceDialog({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Tabs for Network Settings and Access Control */}
|
||||
<HorizontalTabs
|
||||
clientSide={true}
|
||||
defaultTab={0}
|
||||
items={[
|
||||
{
|
||||
title: t(
|
||||
"editInternalResourceDialogNetworkSettings"
|
||||
),
|
||||
href: "#"
|
||||
},
|
||||
{
|
||||
title: t(
|
||||
"editInternalResourceDialogAccessPolicy"
|
||||
),
|
||||
href: "#"
|
||||
}
|
||||
]}
|
||||
>
|
||||
{/* Network Settings Tab */}
|
||||
<div className="space-y-4 mt-4">
|
||||
<div>
|
||||
<div className="mb-8">
|
||||
<label className="font-medium block">
|
||||
{t(
|
||||
"editInternalResourceDialogDestinationLabel"
|
||||
)}
|
||||
</label>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t(
|
||||
"editInternalResourceDialogDestinationDescription"
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"grid gap-4 items-start",
|
||||
mode === "cidr"
|
||||
? "grid-cols-4"
|
||||
: "grid-cols-12"
|
||||
)}
|
||||
>
|
||||
{/* Mode - Smaller select */}
|
||||
<div
|
||||
className={
|
||||
mode === "cidr"
|
||||
? "col-span-1"
|
||||
: "col-span-3"
|
||||
}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="mode"
|
||||
@@ -772,7 +842,9 @@ export default function EditInternalResourceDialog({
|
||||
onValueChange={
|
||||
field.onChange
|
||||
}
|
||||
value={field.value}
|
||||
value={
|
||||
field.value
|
||||
}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
@@ -780,7 +852,6 @@ export default function EditInternalResourceDialog({
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{/* <SelectItem value="port">{t("editInternalResourceDialogModePort")}</SelectItem> */}
|
||||
<SelectItem value="host">
|
||||
{t(
|
||||
"editInternalResourceDialogModeHost"
|
||||
@@ -797,64 +868,16 @@ export default function EditInternalResourceDialog({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* {mode === "port" && (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="protocol"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("editInternalResourceDialogProtocol")}</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
value={field.value ?? undefined}
|
||||
{/* Destination - Larger input */}
|
||||
<div
|
||||
className={
|
||||
mode === "cidr"
|
||||
? "col-span-3"
|
||||
: "col-span-5"
|
||||
}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="tcp">TCP</SelectItem>
|
||||
<SelectItem value="udp">UDP</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="proxyPort"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("editInternalResourceDialogSitePort")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
value={field.value || ""}
|
||||
onChange={(e) => field.onChange(e.target.value === "" ? undefined : parseInt(e.target.value) || 0)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)} */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Target Configuration Form */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
{t(
|
||||
"editInternalResourceDialogTargetConfiguration"
|
||||
)}
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="destination"
|
||||
@@ -865,50 +888,20 @@ export default function EditInternalResourceDialog({
|
||||
"editInternalResourceDialogDestination"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{mode === "host" &&
|
||||
t(
|
||||
"editInternalResourceDialogDestinationHostDescription"
|
||||
)}
|
||||
{mode === "cidr" &&
|
||||
t(
|
||||
"editInternalResourceDialogDestinationCidrDescription"
|
||||
)}
|
||||
{/* {mode === "port" && t("editInternalResourceDialogDestinationIPDescription")} */}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* {mode === "port" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="destinationPort"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("targetPort")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
value={field.value || ""}
|
||||
onChange={(e) => field.onChange(e.target.value === "" ? undefined : parseInt(e.target.value) || 0)}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)} */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Alias */}
|
||||
{/* Alias - Equally sized input (if allowed) */}
|
||||
{mode !== "cidr" && (
|
||||
<div>
|
||||
<div className="col-span-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="alias"
|
||||
@@ -923,82 +916,137 @@ export default function EditInternalResourceDialog({
|
||||
<Input
|
||||
{...field}
|
||||
value={
|
||||
field.value ?? ""
|
||||
field.value ??
|
||||
""
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"editInternalResourceDialogAliasDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Port Restrictions Section */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
{t("portRestrictions")}
|
||||
</h3>
|
||||
{/* Ports and Restrictions */}
|
||||
<div className="space-y-4">
|
||||
{/* TCP Ports */}
|
||||
<div className="my-8">
|
||||
<label className="font-medium block">
|
||||
{t("portRestrictions")}
|
||||
</label>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t(
|
||||
"editInternalResourceDialogPortRestrictionsDescription"
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"grid gap-4 items-start",
|
||||
mode === "cidr"
|
||||
? "grid-cols-4"
|
||||
: "grid-cols-12"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
mode === "cidr"
|
||||
? "col-span-1"
|
||||
: "col-span-3"
|
||||
}
|
||||
>
|
||||
<FormLabel className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
||||
{t(
|
||||
"editInternalResourceDialogTcp"
|
||||
)}
|
||||
</FormLabel>
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
mode === "cidr"
|
||||
? "col-span-3"
|
||||
: "col-span-9"
|
||||
}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="tcpPortRangeString"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="flex items-center gap-2">
|
||||
<FormLabel className="min-w-10">
|
||||
TCP
|
||||
</FormLabel>
|
||||
{/*<InfoPopup
|
||||
info={t("tcpPortsDescription")}
|
||||
/>*/}
|
||||
<Select
|
||||
value={tcpPortMode}
|
||||
onValueChange={(value: PortMode) => {
|
||||
setTcpPortMode(value);
|
||||
value={
|
||||
tcpPortMode
|
||||
}
|
||||
onValueChange={(
|
||||
value: PortMode
|
||||
) => {
|
||||
setTcpPortMode(
|
||||
value
|
||||
);
|
||||
}}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger className="w-[110px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">
|
||||
{t("allPorts")}
|
||||
{t(
|
||||
"allPorts"
|
||||
)}
|
||||
</SelectItem>
|
||||
<SelectItem value="blocked">
|
||||
{t("blocked")}
|
||||
{t(
|
||||
"blocked"
|
||||
)}
|
||||
</SelectItem>
|
||||
<SelectItem value="custom">
|
||||
{t("custom")}
|
||||
{t(
|
||||
"custom"
|
||||
)}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{tcpPortMode === "custom" ? (
|
||||
{tcpPortMode ===
|
||||
"custom" ? (
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="80,443,8000-9000"
|
||||
value={tcpCustomPorts}
|
||||
onChange={(e) =>
|
||||
setTcpCustomPorts(e.target.value)
|
||||
value={
|
||||
tcpCustomPorts
|
||||
}
|
||||
onChange={(
|
||||
e
|
||||
) =>
|
||||
setTcpCustomPorts(
|
||||
e
|
||||
.target
|
||||
.value
|
||||
)
|
||||
}
|
||||
className="flex-1"
|
||||
/>
|
||||
</FormControl>
|
||||
) : (
|
||||
<Input
|
||||
disabled
|
||||
placeholder={
|
||||
tcpPortMode === "all"
|
||||
? t("allPortsAllowed")
|
||||
: t("allPortsBlocked")
|
||||
tcpPortMode ===
|
||||
"all"
|
||||
? t(
|
||||
"allPortsAllowed"
|
||||
)
|
||||
: t(
|
||||
"allPortsBlocked"
|
||||
)
|
||||
}
|
||||
className="flex-1"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -1006,61 +1054,114 @@ export default function EditInternalResourceDialog({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* UDP Ports */}
|
||||
<div
|
||||
className={cn(
|
||||
"grid gap-4 items-start",
|
||||
mode === "cidr"
|
||||
? "grid-cols-4"
|
||||
: "grid-cols-12"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
mode === "cidr"
|
||||
? "col-span-1"
|
||||
: "col-span-3"
|
||||
}
|
||||
>
|
||||
<FormLabel className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
||||
{t(
|
||||
"editInternalResourceDialogUdp"
|
||||
)}
|
||||
</FormLabel>
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
mode === "cidr"
|
||||
? "col-span-3"
|
||||
: "col-span-9"
|
||||
}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="udpPortRangeString"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="flex items-center gap-2">
|
||||
<FormLabel className="min-w-10">
|
||||
UDP
|
||||
</FormLabel>
|
||||
{/*<InfoPopup
|
||||
info={t("udpPortsDescription")}
|
||||
/>*/}
|
||||
<Select
|
||||
value={udpPortMode}
|
||||
onValueChange={(value: PortMode) => {
|
||||
setUdpPortMode(value);
|
||||
value={
|
||||
udpPortMode
|
||||
}
|
||||
onValueChange={(
|
||||
value: PortMode
|
||||
) => {
|
||||
setUdpPortMode(
|
||||
value
|
||||
);
|
||||
}}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger className="w-[110px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">
|
||||
{t("allPorts")}
|
||||
{t(
|
||||
"allPorts"
|
||||
)}
|
||||
</SelectItem>
|
||||
<SelectItem value="blocked">
|
||||
{t("blocked")}
|
||||
{t(
|
||||
"blocked"
|
||||
)}
|
||||
</SelectItem>
|
||||
<SelectItem value="custom">
|
||||
{t("custom")}
|
||||
{t(
|
||||
"custom"
|
||||
)}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{udpPortMode === "custom" ? (
|
||||
{udpPortMode ===
|
||||
"custom" ? (
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="53,123,500-600"
|
||||
value={udpCustomPorts}
|
||||
onChange={(e) =>
|
||||
setUdpCustomPorts(e.target.value)
|
||||
value={
|
||||
udpCustomPorts
|
||||
}
|
||||
onChange={(
|
||||
e
|
||||
) =>
|
||||
setUdpCustomPorts(
|
||||
e
|
||||
.target
|
||||
.value
|
||||
)
|
||||
}
|
||||
className="flex-1"
|
||||
/>
|
||||
</FormControl>
|
||||
) : (
|
||||
<Input
|
||||
disabled
|
||||
placeholder={
|
||||
udpPortMode === "all"
|
||||
? t("allPortsAllowed")
|
||||
: t("allPortsBlocked")
|
||||
udpPortMode ===
|
||||
"all"
|
||||
? t(
|
||||
"allPortsAllowed"
|
||||
)
|
||||
: t(
|
||||
"allPortsBlocked"
|
||||
)
|
||||
}
|
||||
className="flex-1"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -1068,25 +1169,66 @@ export default function EditInternalResourceDialog({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ICMP Toggle */}
|
||||
<div
|
||||
className={cn(
|
||||
"grid gap-4 items-start",
|
||||
mode === "cidr"
|
||||
? "grid-cols-4"
|
||||
: "grid-cols-12"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
mode === "cidr"
|
||||
? "col-span-1"
|
||||
: "col-span-3"
|
||||
}
|
||||
>
|
||||
<FormLabel className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
||||
{t(
|
||||
"editInternalResourceDialogIcmp"
|
||||
)}
|
||||
</FormLabel>
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
mode === "cidr"
|
||||
? "col-span-3"
|
||||
: "col-span-9"
|
||||
}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="disableIcmp"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<div className="flex items-center gap-2">
|
||||
<FormLabel className="min-w-10">
|
||||
ICMP
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={!field.value}
|
||||
onCheckedChange={(checked) => field.onChange(!checked)}
|
||||
checked={
|
||||
!field.value
|
||||
}
|
||||
onCheckedChange={(
|
||||
checked
|
||||
) =>
|
||||
field.onChange(
|
||||
!checked
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{field.value ? t("blocked") : t("allowed")}
|
||||
{field.value
|
||||
? t(
|
||||
"blocked"
|
||||
)
|
||||
: t(
|
||||
"allowed"
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<FormMessage />
|
||||
@@ -1095,18 +1237,30 @@ export default function EditInternalResourceDialog({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Access Control Section */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
{t("resourceUsersRoles")}
|
||||
</h3>
|
||||
{/* Access Control Tab */}
|
||||
<div className="space-y-4 mt-4">
|
||||
<div className="mb-8">
|
||||
<label className="font-medium block">
|
||||
{t(
|
||||
"editInternalResourceDialogAccessControl"
|
||||
)}
|
||||
</label>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t(
|
||||
"editInternalResourceDialogAccessControlDescription"
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{loadingRolesUsers ? (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t("loading")}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{/* Roles */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="roles"
|
||||
@@ -1130,7 +1284,8 @@ export default function EditInternalResourceDialog({
|
||||
size="sm"
|
||||
tags={
|
||||
form.getValues()
|
||||
.roles || []
|
||||
.roles ||
|
||||
[]
|
||||
}
|
||||
setTags={(
|
||||
newRoles
|
||||
@@ -1159,14 +1314,11 @@ export default function EditInternalResourceDialog({
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
{t(
|
||||
"resourceRoleDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Users */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="users"
|
||||
@@ -1189,7 +1341,8 @@ export default function EditInternalResourceDialog({
|
||||
)}
|
||||
tags={
|
||||
form.getValues()
|
||||
.users || []
|
||||
.users ||
|
||||
[]
|
||||
}
|
||||
size="sm"
|
||||
setTags={(
|
||||
@@ -1222,6 +1375,8 @@ export default function EditInternalResourceDialog({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Clients (Machines) */}
|
||||
{hasMachineClients && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
@@ -1277,7 +1432,9 @@ export default function EditInternalResourceDialog({
|
||||
restrictTagsToAutocompleteOptions={
|
||||
true
|
||||
}
|
||||
sortTags={true}
|
||||
sortTags={
|
||||
true
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
@@ -1288,6 +1445,7 @@ export default function EditInternalResourceDialog({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</HorizontalTabs>
|
||||
</form>
|
||||
</Form>
|
||||
</CredenzaBody>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
import { cn } from "@app/lib/cn";
|
||||
@@ -20,17 +20,22 @@ interface HorizontalTabsProps {
|
||||
children: React.ReactNode;
|
||||
items: TabItem[];
|
||||
disabled?: boolean;
|
||||
clientSide?: boolean;
|
||||
defaultTab?: number;
|
||||
}
|
||||
|
||||
export function HorizontalTabs({
|
||||
children,
|
||||
items,
|
||||
disabled = false
|
||||
disabled = false,
|
||||
clientSide = false,
|
||||
defaultTab = 0
|
||||
}: HorizontalTabsProps) {
|
||||
const pathname = usePathname();
|
||||
const params = useParams();
|
||||
const { licenseStatus, isUnlocked } = useLicenseStatusContext();
|
||||
const t = useTranslations();
|
||||
const [activeClientTab, setActiveClientTab] = useState(defaultTab);
|
||||
|
||||
function hydrateHref(href: string) {
|
||||
return href
|
||||
@@ -43,6 +48,73 @@ export function HorizontalTabs({
|
||||
.replace("{remoteExitNodeId}", params.remoteExitNodeId as string);
|
||||
}
|
||||
|
||||
// Client-side mode: render tabs as buttons with state management
|
||||
if (clientSide) {
|
||||
const childrenArray = React.Children.toArray(children);
|
||||
const activeChild = childrenArray[activeClientTab] || null;
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="relative">
|
||||
<div className="overflow-x-auto scrollbar-hide">
|
||||
<div className="flex space-x-4 border-b min-w-max">
|
||||
{items.map((item, index) => {
|
||||
const isActive = activeClientTab === index;
|
||||
const isProfessional =
|
||||
item.showProfessional && !isUnlocked();
|
||||
const isDisabled =
|
||||
disabled ||
|
||||
(isProfessional && !isUnlocked());
|
||||
|
||||
return (
|
||||
<button
|
||||
key={index}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (!isDisabled) {
|
||||
setActiveClientTab(index);
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
"px-4 py-2 text-sm font-medium transition-colors whitespace-nowrap relative",
|
||||
isActive
|
||||
? "text-primary after:absolute after:bottom-0 after:left-0 after:right-0 after:h-0.75 after:bg-primary after:rounded-full"
|
||||
: "text-muted-foreground hover:text-foreground",
|
||||
isDisabled && "cursor-not-allowed"
|
||||
)}
|
||||
disabled={isDisabled}
|
||||
tabIndex={isDisabled ? -1 : undefined}
|
||||
aria-disabled={isDisabled}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center space-x-2",
|
||||
isDisabled && "opacity-60"
|
||||
)}
|
||||
>
|
||||
{item.icon && item.icon}
|
||||
<span>{item.title}</span>
|
||||
{isProfessional && (
|
||||
<Badge
|
||||
variant="outlinePrimary"
|
||||
className="ml-2"
|
||||
>
|
||||
{t("licenseBadge")}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-6">{activeChild}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Server-side mode: original behavior with routing
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="relative">
|
||||
|
||||
@@ -15,7 +15,7 @@ const buttonVariants = cva(
|
||||
destructive:
|
||||
"bg-destructive text-white dark:text-destructive-foreground hover:bg-destructive/90 ",
|
||||
outline:
|
||||
"border border-input bg-card hover:bg-accent hover:text-accent-foreground ",
|
||||
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground ",
|
||||
outlinePrimary:
|
||||
"border border-primary bg-card hover:bg-primary/10 text-primary ",
|
||||
secondary:
|
||||
|
||||
@@ -228,7 +228,7 @@ export const resourceQueries = {
|
||||
queryFn: async ({ signal, meta }) => {
|
||||
const res = await meta!.api.get<
|
||||
AxiosResponse<ListSiteResourceUsersResponse>
|
||||
>(`/resource/${resourceId}/users`, { signal });
|
||||
>(`/site-resource/${resourceId}/users`, { signal });
|
||||
return res.data.data.users;
|
||||
}
|
||||
}),
|
||||
@@ -238,7 +238,7 @@ export const resourceQueries = {
|
||||
queryFn: async ({ signal, meta }) => {
|
||||
const res = await meta!.api.get<
|
||||
AxiosResponse<ListSiteResourceRolesResponse>
|
||||
>(`/resource/${resourceId}/roles`, { signal });
|
||||
>(`/site-resource/${resourceId}/roles`, { signal });
|
||||
|
||||
return res.data.data.roles;
|
||||
}
|
||||
@@ -249,7 +249,7 @@ export const resourceQueries = {
|
||||
queryFn: async ({ signal, meta }) => {
|
||||
const res = await meta!.api.get<
|
||||
AxiosResponse<ListSiteResourceClientsResponse>
|
||||
>(`/resource/${resourceId}/clients`, { signal });
|
||||
>(`/site-resource/${resourceId}/clients`, { signal });
|
||||
|
||||
return res.data.data.clients;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user