mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-10 04: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",
|
"resourceLoginPageDescription": "Customize the login page for individual resources",
|
||||||
"enterConfirmation": "Enter confirmation",
|
"enterConfirmation": "Enter confirmation",
|
||||||
"blueprintViewDetails": "Details",
|
"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}
|
orgId={orgId}
|
||||||
sites={sites}
|
sites={sites}
|
||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
|
// Delay refresh to allow modal to close smoothly
|
||||||
|
setTimeout(() => {
|
||||||
router.refresh();
|
router.refresh();
|
||||||
setEditingResource(null);
|
setEditingResource(null);
|
||||||
|
}, 150);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -341,7 +344,10 @@ export default function ClientResourcesTable({
|
|||||||
orgId={orgId}
|
orgId={orgId}
|
||||||
sites={sites}
|
sites={sites}
|
||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
|
// Delay refresh to allow modal to close smoothly
|
||||||
|
setTimeout(() => {
|
||||||
router.refresh();
|
router.refresh();
|
||||||
|
}, 150);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ import { useTranslations } from "next-intl";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { HorizontalTabs, TabItem } from "@app/components/HorizontalTabs";
|
||||||
// import { InfoPopup } from "@app/components/ui/info-popup";
|
// import { InfoPopup } from "@app/components/ui/info-popup";
|
||||||
|
|
||||||
// Helper to validate port range string format
|
// 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
|
// 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()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
.nullable()
|
.nullable()
|
||||||
.refine(
|
.refine((val) => isValidPortRangeString(val), {
|
||||||
(val) => isValidPortRangeString(val),
|
message: getPortRangeValidationMessage(t)
|
||||||
{
|
});
|
||||||
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.'
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Helper to determine the port mode from a port range string
|
// Helper to determine the port mode from a port range string
|
||||||
type PortMode = "all" | "blocked" | "custom";
|
type PortMode = "all" | "blocked" | "custom";
|
||||||
@@ -161,25 +163,18 @@ export default function CreateInternalResourceDialog({
|
|||||||
.string()
|
.string()
|
||||||
.min(1, t("createInternalResourceDialogNameRequired"))
|
.min(1, t("createInternalResourceDialogNameRequired"))
|
||||||
.max(255, t("createInternalResourceDialogNameMaxLength")),
|
.max(255, t("createInternalResourceDialogNameMaxLength")),
|
||||||
// mode: z.enum(["host", "cidr", "port"]),
|
|
||||||
mode: z.enum(["host", "cidr"]),
|
|
||||||
destination: z.string().min(1),
|
|
||||||
siteId: z
|
siteId: z
|
||||||
.int()
|
.int()
|
||||||
.positive(t("createInternalResourceDialogPleaseSelectSite")),
|
.positive(t("createInternalResourceDialogPleaseSelectSite")),
|
||||||
// protocol: z.enum(["tcp", "udp"]),
|
// mode: z.enum(["host", "cidr", "port"]),
|
||||||
// proxyPort: z.int()
|
mode: z.enum(["host", "cidr"]),
|
||||||
// .positive()
|
// protocol: z.enum(["tcp", "udp"]).nullish(),
|
||||||
// .min(1, t("createInternalResourceDialogProxyPortMin"))
|
// proxyPort: z.int().positive().min(1, t("createInternalResourceDialogProxyPortMin")).max(65535, t("createInternalResourceDialogProxyPortMax")).nullish(),
|
||||||
// .max(65535, t("createInternalResourceDialogProxyPortMax")),
|
destination: z.string().min(1),
|
||||||
// destinationPort: z.int()
|
// destinationPort: z.int().positive().min(1, t("createInternalResourceDialogDestinationPortMin")).max(65535, t("createInternalResourceDialogDestinationPortMax")).nullish(),
|
||||||
// .positive()
|
|
||||||
// .min(1, t("createInternalResourceDialogDestinationPortMin"))
|
|
||||||
// .max(65535, t("createInternalResourceDialogDestinationPortMax"))
|
|
||||||
// .nullish(),
|
|
||||||
alias: z.string().nullish(),
|
alias: z.string().nullish(),
|
||||||
tcpPortRangeString: portRangeStringSchema,
|
tcpPortRangeString: createPortRangeStringSchema(t),
|
||||||
udpPortRangeString: portRangeStringSchema,
|
udpPortRangeString: createPortRangeStringSchema(t),
|
||||||
disableIcmp: z.boolean().optional(),
|
disableIcmp: z.boolean().optional(),
|
||||||
roles: z
|
roles: z
|
||||||
.array(
|
.array(
|
||||||
@@ -453,8 +448,8 @@ export default function CreateInternalResourceDialog({
|
|||||||
variant: "default"
|
variant: "default"
|
||||||
});
|
});
|
||||||
|
|
||||||
onSuccess?.();
|
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
|
onSuccess?.();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating internal resource:", error);
|
console.error("Error creating internal resource:", error);
|
||||||
toast({
|
toast({
|
||||||
@@ -498,7 +493,7 @@ export default function CreateInternalResourceDialog({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Credenza open={open} onOpenChange={setOpen}>
|
<Credenza open={open} onOpenChange={setOpen}>
|
||||||
<CredenzaContent className="max-w-2xl">
|
<CredenzaContent className="max-w-3xl">
|
||||||
<CredenzaHeader>
|
<CredenzaHeader>
|
||||||
<CredenzaTitle>
|
<CredenzaTitle>
|
||||||
{t("createInternalResourceDialogCreateClientResource")}
|
{t("createInternalResourceDialogCreateClientResource")}
|
||||||
@@ -516,14 +511,8 @@ export default function CreateInternalResourceDialog({
|
|||||||
className="space-y-6"
|
className="space-y-6"
|
||||||
id="create-internal-resource-form"
|
id="create-internal-resource-form"
|
||||||
>
|
>
|
||||||
{/* Resource Properties Form */}
|
{/* Name and Site - Side by Side */}
|
||||||
<div>
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<h3 className="text-lg font-semibold mb-4">
|
|
||||||
{t(
|
|
||||||
"createInternalResourceDialogResourceProperties"
|
|
||||||
)}
|
|
||||||
</h3>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="name"
|
name="name"
|
||||||
@@ -547,11 +536,7 @@ export default function CreateInternalResourceDialog({
|
|||||||
name="siteId"
|
name="siteId"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex flex-col">
|
<FormItem className="flex flex-col">
|
||||||
<FormLabel>
|
<FormLabel>{t("site")}</FormLabel>
|
||||||
{t(
|
|
||||||
"site"
|
|
||||||
)}
|
|
||||||
</FormLabel>
|
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -566,9 +551,7 @@ export default function CreateInternalResourceDialog({
|
|||||||
>
|
>
|
||||||
{field.value
|
{field.value
|
||||||
? availableSites.find(
|
? availableSites.find(
|
||||||
(
|
(site) =>
|
||||||
site
|
|
||||||
) =>
|
|
||||||
site.siteId ===
|
site.siteId ===
|
||||||
field.value
|
field.value
|
||||||
)?.name
|
)?.name
|
||||||
@@ -594,9 +577,7 @@ export default function CreateInternalResourceDialog({
|
|||||||
</CommandEmpty>
|
</CommandEmpty>
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
{availableSites.map(
|
{availableSites.map(
|
||||||
(
|
(site) => (
|
||||||
site
|
|
||||||
) => (
|
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={
|
key={
|
||||||
site.siteId
|
site.siteId
|
||||||
@@ -634,7 +615,59 @@ export default function CreateInternalResourceDialog({
|
|||||||
</FormItem>
|
</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
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="mode"
|
name="mode"
|
||||||
@@ -649,7 +682,9 @@ export default function CreateInternalResourceDialog({
|
|||||||
onValueChange={
|
onValueChange={
|
||||||
field.onChange
|
field.onChange
|
||||||
}
|
}
|
||||||
value={field.value}
|
value={
|
||||||
|
field.value
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
@@ -657,7 +692,6 @@ export default function CreateInternalResourceDialog({
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{/* <SelectItem value="port">{t("createInternalResourceDialogModePort")}</SelectItem> */}
|
|
||||||
<SelectItem value="host">
|
<SelectItem value="host">
|
||||||
{t(
|
{t(
|
||||||
"createInternalResourceDialogModeHost"
|
"createInternalResourceDialogModeHost"
|
||||||
@@ -674,76 +708,16 @@ export default function CreateInternalResourceDialog({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{/*
|
</div>
|
||||||
{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>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
{/* Destination - Larger input */}
|
||||||
control={form.control}
|
<div
|
||||||
name="proxyPort"
|
className={
|
||||||
render={({ field }) => (
|
mode === "cidr"
|
||||||
<FormItem>
|
? "col-span-3"
|
||||||
<FormLabel>{t("createInternalResourceDialogSitePort")}</FormLabel>
|
: "col-span-5"
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
value={field.value || ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
field.onChange(
|
|
||||||
e.target.value === "" ? undefined : parseInt(e.target.value)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
/>
|
>
|
||||||
</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
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="destination"
|
name="destination"
|
||||||
@@ -754,59 +728,20 @@ export default function CreateInternalResourceDialog({
|
|||||||
"createInternalResourceDialogDestination"
|
"createInternalResourceDialogDestination"
|
||||||
)}
|
)}
|
||||||
</FormLabel>
|
</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>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
{...field}
|
||||||
value={field.value || ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
field.onChange(
|
|
||||||
e.target.value === "" ? undefined : parseInt(e.target.value)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
|
||||||
{t("createInternalResourceDialogDestinationPortDescription")}
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)} */}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Alias */}
|
{/* Alias - Equally sized input (if allowed) */}
|
||||||
{mode !== "cidr" && (
|
{mode !== "cidr" && (
|
||||||
<div>
|
<div className="col-span-4">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="alias"
|
name="alias"
|
||||||
@@ -821,82 +756,137 @@ export default function CreateInternalResourceDialog({
|
|||||||
<Input
|
<Input
|
||||||
{...field}
|
{...field}
|
||||||
value={
|
value={
|
||||||
field.value ?? ""
|
field.value ??
|
||||||
|
""
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
|
||||||
{t(
|
|
||||||
"createInternalResourceDialogAliasDescription"
|
|
||||||
)}
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Port Restrictions Section */}
|
{/* Ports and Restrictions */}
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-semibold mb-4">
|
|
||||||
{t("portRestrictions")}
|
|
||||||
</h3>
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* TCP Ports */}
|
{/* 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
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="tcpPortRangeString"
|
name="tcpPortRangeString"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<FormLabel className="min-w-10">
|
|
||||||
TCP
|
|
||||||
</FormLabel>
|
|
||||||
{/*<InfoPopup
|
{/*<InfoPopup
|
||||||
info={t("tcpPortsDescription")}
|
info={t("tcpPortsDescription")}
|
||||||
/>*/}
|
/>*/}
|
||||||
<Select
|
<Select
|
||||||
value={tcpPortMode}
|
value={
|
||||||
onValueChange={(value: PortMode) => {
|
tcpPortMode
|
||||||
setTcpPortMode(value);
|
}
|
||||||
|
onValueChange={(
|
||||||
|
value: PortMode
|
||||||
|
) => {
|
||||||
|
setTcpPortMode(
|
||||||
|
value
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<FormControl>
|
||||||
<SelectTrigger className="w-[110px]">
|
<SelectTrigger className="w-[110px]">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="all">
|
<SelectItem value="all">
|
||||||
{t("allPorts")}
|
{t(
|
||||||
|
"allPorts"
|
||||||
|
)}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="blocked">
|
<SelectItem value="blocked">
|
||||||
{t("blocked")}
|
{t(
|
||||||
|
"blocked"
|
||||||
|
)}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="custom">
|
<SelectItem value="custom">
|
||||||
{t("custom")}
|
{t(
|
||||||
|
"custom"
|
||||||
|
)}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
{tcpPortMode === "custom" ? (
|
{tcpPortMode ===
|
||||||
|
"custom" ? (
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="80,443,8000-9000"
|
placeholder="80,443,8000-9000"
|
||||||
value={tcpCustomPorts}
|
value={
|
||||||
onChange={(e) =>
|
tcpCustomPorts
|
||||||
setTcpCustomPorts(e.target.value)
|
}
|
||||||
|
onChange={(
|
||||||
|
e
|
||||||
|
) =>
|
||||||
|
setTcpCustomPorts(
|
||||||
|
e
|
||||||
|
.target
|
||||||
|
.value
|
||||||
|
)
|
||||||
}
|
}
|
||||||
className="flex-1"
|
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
) : (
|
) : (
|
||||||
<Input
|
<Input
|
||||||
disabled
|
disabled
|
||||||
placeholder={
|
placeholder={
|
||||||
tcpPortMode === "all"
|
tcpPortMode ===
|
||||||
? t("allPortsAllowed")
|
"all"
|
||||||
: t("allPortsBlocked")
|
? t(
|
||||||
|
"allPortsAllowed"
|
||||||
|
)
|
||||||
|
: t(
|
||||||
|
"allPortsBlocked"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
className="flex-1"
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -904,61 +894,114 @@ export default function CreateInternalResourceDialog({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* UDP Ports */}
|
{/* 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
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="udpPortRangeString"
|
name="udpPortRangeString"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<FormLabel className="min-w-10">
|
|
||||||
UDP
|
|
||||||
</FormLabel>
|
|
||||||
{/*<InfoPopup
|
{/*<InfoPopup
|
||||||
info={t("udpPortsDescription")}
|
info={t("udpPortsDescription")}
|
||||||
/>*/}
|
/>*/}
|
||||||
<Select
|
<Select
|
||||||
value={udpPortMode}
|
value={
|
||||||
onValueChange={(value: PortMode) => {
|
udpPortMode
|
||||||
setUdpPortMode(value);
|
}
|
||||||
|
onValueChange={(
|
||||||
|
value: PortMode
|
||||||
|
) => {
|
||||||
|
setUdpPortMode(
|
||||||
|
value
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<FormControl>
|
||||||
<SelectTrigger className="w-[110px]">
|
<SelectTrigger className="w-[110px]">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="all">
|
<SelectItem value="all">
|
||||||
{t("allPorts")}
|
{t(
|
||||||
|
"allPorts"
|
||||||
|
)}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="blocked">
|
<SelectItem value="blocked">
|
||||||
{t("blocked")}
|
{t(
|
||||||
|
"blocked"
|
||||||
|
)}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="custom">
|
<SelectItem value="custom">
|
||||||
{t("custom")}
|
{t(
|
||||||
|
"custom"
|
||||||
|
)}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
{udpPortMode === "custom" ? (
|
{udpPortMode ===
|
||||||
|
"custom" ? (
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="53,123,500-600"
|
placeholder="53,123,500-600"
|
||||||
value={udpCustomPorts}
|
value={
|
||||||
onChange={(e) =>
|
udpCustomPorts
|
||||||
setUdpCustomPorts(e.target.value)
|
}
|
||||||
|
onChange={(
|
||||||
|
e
|
||||||
|
) =>
|
||||||
|
setUdpCustomPorts(
|
||||||
|
e
|
||||||
|
.target
|
||||||
|
.value
|
||||||
|
)
|
||||||
}
|
}
|
||||||
className="flex-1"
|
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
) : (
|
) : (
|
||||||
<Input
|
<Input
|
||||||
disabled
|
disabled
|
||||||
placeholder={
|
placeholder={
|
||||||
udpPortMode === "all"
|
udpPortMode ===
|
||||||
? t("allPortsAllowed")
|
"all"
|
||||||
: t("allPortsBlocked")
|
? t(
|
||||||
|
"allPortsAllowed"
|
||||||
|
)
|
||||||
|
: t(
|
||||||
|
"allPortsBlocked"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
className="flex-1"
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -966,25 +1009,66 @@ export default function CreateInternalResourceDialog({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* ICMP Toggle */}
|
{/* 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
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="disableIcmp"
|
name="disableIcmp"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<FormLabel className="min-w-10">
|
|
||||||
ICMP
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Switch
|
<Switch
|
||||||
checked={!field.value}
|
checked={
|
||||||
onCheckedChange={(checked) => field.onChange(!checked)}
|
!field.value
|
||||||
|
}
|
||||||
|
onCheckedChange={(
|
||||||
|
checked
|
||||||
|
) =>
|
||||||
|
field.onChange(
|
||||||
|
!checked
|
||||||
|
)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
{field.value ? t("blocked") : t("allowed")}
|
{field.value
|
||||||
|
? t(
|
||||||
|
"blocked"
|
||||||
|
)
|
||||||
|
: t(
|
||||||
|
"allowed"
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -993,13 +1077,25 @@ export default function CreateInternalResourceDialog({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Access Control Section */}
|
{/* Access Control Tab */}
|
||||||
<div>
|
<div className="space-y-4 mt-4">
|
||||||
<h3 className="text-lg font-semibold mb-4">
|
<div className="mb-8">
|
||||||
{t("resourceUsersRoles")}
|
<label className="font-medium block">
|
||||||
</h3>
|
{t(
|
||||||
|
"editInternalResourceDialogAccessControl"
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
{t(
|
||||||
|
"editInternalResourceDialogAccessControlDescription"
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
{/* Roles */}
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="roles"
|
name="roles"
|
||||||
@@ -1023,9 +1119,12 @@ export default function CreateInternalResourceDialog({
|
|||||||
size="sm"
|
size="sm"
|
||||||
tags={
|
tags={
|
||||||
form.getValues()
|
form.getValues()
|
||||||
.roles || []
|
.roles ||
|
||||||
|
[]
|
||||||
}
|
}
|
||||||
setTags={(newRoles) => {
|
setTags={(
|
||||||
|
newRoles
|
||||||
|
) => {
|
||||||
form.setValue(
|
form.setValue(
|
||||||
"roles",
|
"roles",
|
||||||
newRoles as [
|
newRoles as [
|
||||||
@@ -1040,7 +1139,9 @@ export default function CreateInternalResourceDialog({
|
|||||||
autocompleteOptions={
|
autocompleteOptions={
|
||||||
allRoles
|
allRoles
|
||||||
}
|
}
|
||||||
allowDuplicates={false}
|
allowDuplicates={
|
||||||
|
false
|
||||||
|
}
|
||||||
restrictTagsToAutocompleteOptions={
|
restrictTagsToAutocompleteOptions={
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -1056,6 +1157,8 @@ export default function CreateInternalResourceDialog({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Users */}
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="users"
|
name="users"
|
||||||
@@ -1078,10 +1181,13 @@ export default function CreateInternalResourceDialog({
|
|||||||
)}
|
)}
|
||||||
tags={
|
tags={
|
||||||
form.getValues()
|
form.getValues()
|
||||||
.users || []
|
.users ||
|
||||||
|
[]
|
||||||
}
|
}
|
||||||
size="sm"
|
size="sm"
|
||||||
setTags={(newUsers) => {
|
setTags={(
|
||||||
|
newUsers
|
||||||
|
) => {
|
||||||
form.setValue(
|
form.setValue(
|
||||||
"users",
|
"users",
|
||||||
newUsers as [
|
newUsers as [
|
||||||
@@ -1096,7 +1202,9 @@ export default function CreateInternalResourceDialog({
|
|||||||
autocompleteOptions={
|
autocompleteOptions={
|
||||||
allUsers
|
allUsers
|
||||||
}
|
}
|
||||||
allowDuplicates={false}
|
allowDuplicates={
|
||||||
|
false
|
||||||
|
}
|
||||||
restrictTagsToAutocompleteOptions={
|
restrictTagsToAutocompleteOptions={
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -1107,6 +1215,8 @@ export default function CreateInternalResourceDialog({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Clients (Machines) */}
|
||||||
{hasMachineClients && (
|
{hasMachineClients && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -1114,7 +1224,9 @@ export default function CreateInternalResourceDialog({
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex flex-col items-start">
|
<FormItem className="flex flex-col items-start">
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
{t("machineClients")}
|
{t(
|
||||||
|
"machineClients"
|
||||||
|
)}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<TagInput
|
<TagInput
|
||||||
@@ -1160,7 +1272,9 @@ export default function CreateInternalResourceDialog({
|
|||||||
restrictTagsToAutocompleteOptions={
|
restrictTagsToAutocompleteOptions={
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
sortTags={true}
|
sortTags={
|
||||||
|
true
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -1170,6 +1284,7 @@ export default function CreateInternalResourceDialog({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</HorizontalTabs>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</CredenzaBody>
|
</CredenzaBody>
|
||||||
|
|||||||
@@ -56,7 +56,14 @@ import {
|
|||||||
} from "@app/components/ui/popover";
|
} from "@app/components/ui/popover";
|
||||||
import { cn } from "@app/lib/cn";
|
import { cn } from "@app/lib/cn";
|
||||||
import { ListSitesResponse } from "@server/routers/site";
|
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";
|
// import { InfoPopup } from "@app/components/ui/info-popup";
|
||||||
|
|
||||||
// Helper to validate port range string format
|
// Helper to validate port range string format
|
||||||
@@ -85,7 +92,12 @@ const isValidPortRangeString = (val: string | undefined | null): boolean => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (startPort < 1 || startPort > 65535 || endPort < 1 || endPort > 65535) {
|
if (
|
||||||
|
startPort < 1 ||
|
||||||
|
startPort > 65535 ||
|
||||||
|
endPort < 1 ||
|
||||||
|
endPort > 65535
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,17 +119,18 @@ const isValidPortRangeString = (val: string | undefined | null): boolean => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Port range string schema for client-side validation
|
// 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()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
.nullable()
|
.nullable()
|
||||||
.refine(
|
.refine((val) => isValidPortRangeString(val), {
|
||||||
(val) => isValidPortRangeString(val),
|
message: getPortRangeValidationMessage(t)
|
||||||
{
|
});
|
||||||
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.'
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Helper to determine the port mode from a port range string
|
// Helper to determine the port mode from a port range string
|
||||||
type PortMode = "all" | "blocked" | "custom";
|
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
|
// 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 === "all") return "*";
|
||||||
if (mode === "blocked") return "";
|
if (mode === "blocked") return "";
|
||||||
return customValue;
|
return customValue;
|
||||||
@@ -188,8 +204,8 @@ export default function EditInternalResourceDialog({
|
|||||||
destination: z.string().min(1),
|
destination: z.string().min(1),
|
||||||
// destinationPort: z.int().positive().min(1, t("editInternalResourceDialogDestinationPortMin")).max(65535, t("editInternalResourceDialogDestinationPortMax")).nullish(),
|
// destinationPort: z.int().positive().min(1, t("editInternalResourceDialogDestinationPortMin")).max(65535, t("editInternalResourceDialogDestinationPortMax")).nullish(),
|
||||||
alias: z.string().nullish(),
|
alias: z.string().nullish(),
|
||||||
tcpPortRangeString: portRangeStringSchema,
|
tcpPortRangeString: createPortRangeStringSchema(t),
|
||||||
udpPortRangeString: portRangeStringSchema,
|
udpPortRangeString: createPortRangeStringSchema(t),
|
||||||
disableIcmp: z.boolean().optional(),
|
disableIcmp: z.boolean().optional(),
|
||||||
roles: z
|
roles: z
|
||||||
.array(
|
.array(
|
||||||
@@ -352,6 +368,9 @@ export default function EditInternalResourceDialog({
|
|||||||
number | null
|
number | null
|
||||||
>(null);
|
>(null);
|
||||||
|
|
||||||
|
// Collapsible state for ports and restrictions
|
||||||
|
const [isPortsExpanded, setIsPortsExpanded] = useState(false);
|
||||||
|
|
||||||
// Port restriction UI state
|
// Port restriction UI state
|
||||||
const [tcpPortMode, setTcpPortMode] = useState<PortMode>(
|
const [tcpPortMode, setTcpPortMode] = useState<PortMode>(
|
||||||
getPortModeFromString(resource.tcpPortRangeString)
|
getPortModeFromString(resource.tcpPortRangeString)
|
||||||
@@ -446,9 +465,7 @@ export default function EditInternalResourceDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update the site resource
|
// Update the site resource
|
||||||
await api.post(
|
await api.post(`/site-resource/${resource.id}`, {
|
||||||
`/site-resource/${resource.id}`,
|
|
||||||
{
|
|
||||||
name: data.name,
|
name: data.name,
|
||||||
siteId: data.siteId,
|
siteId: data.siteId,
|
||||||
mode: data.mode,
|
mode: data.mode,
|
||||||
@@ -468,8 +485,7 @@ export default function EditInternalResourceDialog({
|
|||||||
roleIds: (data.roles || []).map((r) => parseInt(r.id)),
|
roleIds: (data.roles || []).map((r) => parseInt(r.id)),
|
||||||
userIds: (data.users || []).map((u) => u.id),
|
userIds: (data.users || []).map((u) => u.id),
|
||||||
clientIds: (data.clients || []).map((c) => parseInt(c.id))
|
clientIds: (data.clients || []).map((c) => parseInt(c.id))
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
// Update roles, users, and clients
|
// Update roles, users, and clients
|
||||||
// await Promise.all([
|
// await Promise.all([
|
||||||
@@ -502,8 +518,8 @@ export default function EditInternalResourceDialog({
|
|||||||
variant: "default"
|
variant: "default"
|
||||||
});
|
});
|
||||||
|
|
||||||
onSuccess?.();
|
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
|
onSuccess?.();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating internal resource:", error);
|
console.error("Error updating internal resource:", error);
|
||||||
toast({
|
toast({
|
||||||
@@ -543,18 +559,26 @@ export default function EditInternalResourceDialog({
|
|||||||
clients: []
|
clients: []
|
||||||
});
|
});
|
||||||
// Reset port mode state
|
// Reset port mode state
|
||||||
setTcpPortMode(getPortModeFromString(resource.tcpPortRangeString));
|
setTcpPortMode(
|
||||||
setUdpPortMode(getPortModeFromString(resource.udpPortRangeString));
|
getPortModeFromString(resource.tcpPortRangeString)
|
||||||
|
);
|
||||||
|
setUdpPortMode(
|
||||||
|
getPortModeFromString(resource.udpPortRangeString)
|
||||||
|
);
|
||||||
setTcpCustomPorts(
|
setTcpCustomPorts(
|
||||||
resource.tcpPortRangeString && resource.tcpPortRangeString !== "*"
|
resource.tcpPortRangeString &&
|
||||||
|
resource.tcpPortRangeString !== "*"
|
||||||
? resource.tcpPortRangeString
|
? resource.tcpPortRangeString
|
||||||
: ""
|
: ""
|
||||||
);
|
);
|
||||||
setUdpCustomPorts(
|
setUdpCustomPorts(
|
||||||
resource.udpPortRangeString && resource.udpPortRangeString !== "*"
|
resource.udpPortRangeString &&
|
||||||
|
resource.udpPortRangeString !== "*"
|
||||||
? resource.udpPortRangeString
|
? resource.udpPortRangeString
|
||||||
: ""
|
: ""
|
||||||
);
|
);
|
||||||
|
// Reset visibility states
|
||||||
|
setIsPortsExpanded(false);
|
||||||
previousResourceId.current = resource.id;
|
previousResourceId.current = resource.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -602,25 +626,33 @@ export default function EditInternalResourceDialog({
|
|||||||
clients: []
|
clients: []
|
||||||
});
|
});
|
||||||
// Reset port mode state
|
// Reset port mode state
|
||||||
setTcpPortMode(getPortModeFromString(resource.tcpPortRangeString));
|
setTcpPortMode(
|
||||||
setUdpPortMode(getPortModeFromString(resource.udpPortRangeString));
|
getPortModeFromString(resource.tcpPortRangeString)
|
||||||
|
);
|
||||||
|
setUdpPortMode(
|
||||||
|
getPortModeFromString(resource.udpPortRangeString)
|
||||||
|
);
|
||||||
setTcpCustomPorts(
|
setTcpCustomPorts(
|
||||||
resource.tcpPortRangeString && resource.tcpPortRangeString !== "*"
|
resource.tcpPortRangeString &&
|
||||||
|
resource.tcpPortRangeString !== "*"
|
||||||
? resource.tcpPortRangeString
|
? resource.tcpPortRangeString
|
||||||
: ""
|
: ""
|
||||||
);
|
);
|
||||||
setUdpCustomPorts(
|
setUdpCustomPorts(
|
||||||
resource.udpPortRangeString && 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
|
// Reset previous resource ID to ensure clean state on next open
|
||||||
previousResourceId.current = null;
|
previousResourceId.current = null;
|
||||||
}
|
}
|
||||||
setOpen(open);
|
setOpen(open);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CredenzaContent className="max-w-2xl">
|
<CredenzaContent className="max-w-3xl">
|
||||||
<CredenzaHeader>
|
<CredenzaHeader>
|
||||||
<CredenzaTitle>
|
<CredenzaTitle>
|
||||||
{t("editInternalResourceDialogEditClientResource")}
|
{t("editInternalResourceDialogEditClientResource")}
|
||||||
@@ -639,14 +671,8 @@ export default function EditInternalResourceDialog({
|
|||||||
className="space-y-6"
|
className="space-y-6"
|
||||||
id="edit-internal-resource-form"
|
id="edit-internal-resource-form"
|
||||||
>
|
>
|
||||||
{/* Resource Properties Form */}
|
{/* Name and Site - Side by Side */}
|
||||||
<div>
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<h3 className="text-lg font-semibold mb-4">
|
|
||||||
{t(
|
|
||||||
"editInternalResourceDialogResourceProperties"
|
|
||||||
)}
|
|
||||||
</h3>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="name"
|
name="name"
|
||||||
@@ -670,11 +696,7 @@ export default function EditInternalResourceDialog({
|
|||||||
name="siteId"
|
name="siteId"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex flex-col">
|
<FormItem className="flex flex-col">
|
||||||
<FormLabel>
|
<FormLabel>{t("site")}</FormLabel>
|
||||||
{t(
|
|
||||||
"site"
|
|
||||||
)}
|
|
||||||
</FormLabel>
|
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -689,9 +711,7 @@ export default function EditInternalResourceDialog({
|
|||||||
>
|
>
|
||||||
{field.value
|
{field.value
|
||||||
? availableSites.find(
|
? availableSites.find(
|
||||||
(
|
(site) =>
|
||||||
site
|
|
||||||
) =>
|
|
||||||
site.siteId ===
|
site.siteId ===
|
||||||
field.value
|
field.value
|
||||||
)?.name
|
)?.name
|
||||||
@@ -717,9 +737,7 @@ export default function EditInternalResourceDialog({
|
|||||||
</CommandEmpty>
|
</CommandEmpty>
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
{availableSites.map(
|
{availableSites.map(
|
||||||
(
|
(site) => (
|
||||||
site
|
|
||||||
) => (
|
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={
|
key={
|
||||||
site.siteId
|
site.siteId
|
||||||
@@ -757,7 +775,59 @@ export default function EditInternalResourceDialog({
|
|||||||
</FormItem>
|
</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
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="mode"
|
name="mode"
|
||||||
@@ -772,7 +842,9 @@ export default function EditInternalResourceDialog({
|
|||||||
onValueChange={
|
onValueChange={
|
||||||
field.onChange
|
field.onChange
|
||||||
}
|
}
|
||||||
value={field.value}
|
value={
|
||||||
|
field.value
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
@@ -780,7 +852,6 @@ export default function EditInternalResourceDialog({
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{/* <SelectItem value="port">{t("editInternalResourceDialogModePort")}</SelectItem> */}
|
|
||||||
<SelectItem value="host">
|
<SelectItem value="host">
|
||||||
{t(
|
{t(
|
||||||
"editInternalResourceDialogModeHost"
|
"editInternalResourceDialogModeHost"
|
||||||
@@ -797,64 +868,16 @@ export default function EditInternalResourceDialog({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* {mode === "port" && (
|
{/* Destination - Larger input */}
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div
|
||||||
<FormField
|
className={
|
||||||
control={form.control}
|
mode === "cidr"
|
||||||
name="protocol"
|
? "col-span-3"
|
||||||
render={({ field }) => (
|
: "col-span-5"
|
||||||
<FormItem>
|
}
|
||||||
<FormLabel>{t("editInternalResourceDialogProtocol")}</FormLabel>
|
|
||||||
<Select
|
|
||||||
onValueChange={field.onChange}
|
|
||||||
value={field.value ?? undefined}
|
|
||||||
>
|
>
|
||||||
<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
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="destination"
|
name="destination"
|
||||||
@@ -865,50 +888,20 @@ export default function EditInternalResourceDialog({
|
|||||||
"editInternalResourceDialogDestination"
|
"editInternalResourceDialogDestination"
|
||||||
)}
|
)}
|
||||||
</FormLabel>
|
</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>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
{...field}
|
||||||
value={field.value || ""}
|
|
||||||
onChange={(e) => field.onChange(e.target.value === "" ? undefined : parseInt(e.target.value) || 0)}
|
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)} */}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Alias */}
|
{/* Alias - Equally sized input (if allowed) */}
|
||||||
{mode !== "cidr" && (
|
{mode !== "cidr" && (
|
||||||
<div>
|
<div className="col-span-4">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="alias"
|
name="alias"
|
||||||
@@ -923,82 +916,137 @@ export default function EditInternalResourceDialog({
|
|||||||
<Input
|
<Input
|
||||||
{...field}
|
{...field}
|
||||||
value={
|
value={
|
||||||
field.value ?? ""
|
field.value ??
|
||||||
|
""
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
|
||||||
{t(
|
|
||||||
"editInternalResourceDialogAliasDescription"
|
|
||||||
)}
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Port Restrictions Section */}
|
{/* Ports and Restrictions */}
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-semibold mb-4">
|
|
||||||
{t("portRestrictions")}
|
|
||||||
</h3>
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* TCP Ports */}
|
{/* 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
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="tcpPortRangeString"
|
name="tcpPortRangeString"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<FormLabel className="min-w-10">
|
|
||||||
TCP
|
|
||||||
</FormLabel>
|
|
||||||
{/*<InfoPopup
|
{/*<InfoPopup
|
||||||
info={t("tcpPortsDescription")}
|
info={t("tcpPortsDescription")}
|
||||||
/>*/}
|
/>*/}
|
||||||
<Select
|
<Select
|
||||||
value={tcpPortMode}
|
value={
|
||||||
onValueChange={(value: PortMode) => {
|
tcpPortMode
|
||||||
setTcpPortMode(value);
|
}
|
||||||
|
onValueChange={(
|
||||||
|
value: PortMode
|
||||||
|
) => {
|
||||||
|
setTcpPortMode(
|
||||||
|
value
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<FormControl>
|
||||||
<SelectTrigger className="w-[110px]">
|
<SelectTrigger className="w-[110px]">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="all">
|
<SelectItem value="all">
|
||||||
{t("allPorts")}
|
{t(
|
||||||
|
"allPorts"
|
||||||
|
)}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="blocked">
|
<SelectItem value="blocked">
|
||||||
{t("blocked")}
|
{t(
|
||||||
|
"blocked"
|
||||||
|
)}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="custom">
|
<SelectItem value="custom">
|
||||||
{t("custom")}
|
{t(
|
||||||
|
"custom"
|
||||||
|
)}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
{tcpPortMode === "custom" ? (
|
{tcpPortMode ===
|
||||||
|
"custom" ? (
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="80,443,8000-9000"
|
placeholder="80,443,8000-9000"
|
||||||
value={tcpCustomPorts}
|
value={
|
||||||
onChange={(e) =>
|
tcpCustomPorts
|
||||||
setTcpCustomPorts(e.target.value)
|
}
|
||||||
|
onChange={(
|
||||||
|
e
|
||||||
|
) =>
|
||||||
|
setTcpCustomPorts(
|
||||||
|
e
|
||||||
|
.target
|
||||||
|
.value
|
||||||
|
)
|
||||||
}
|
}
|
||||||
className="flex-1"
|
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
) : (
|
) : (
|
||||||
<Input
|
<Input
|
||||||
disabled
|
disabled
|
||||||
placeholder={
|
placeholder={
|
||||||
tcpPortMode === "all"
|
tcpPortMode ===
|
||||||
? t("allPortsAllowed")
|
"all"
|
||||||
: t("allPortsBlocked")
|
? t(
|
||||||
|
"allPortsAllowed"
|
||||||
|
)
|
||||||
|
: t(
|
||||||
|
"allPortsBlocked"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
className="flex-1"
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -1006,61 +1054,114 @@ export default function EditInternalResourceDialog({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* UDP Ports */}
|
{/* 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
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="udpPortRangeString"
|
name="udpPortRangeString"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<FormLabel className="min-w-10">
|
|
||||||
UDP
|
|
||||||
</FormLabel>
|
|
||||||
{/*<InfoPopup
|
{/*<InfoPopup
|
||||||
info={t("udpPortsDescription")}
|
info={t("udpPortsDescription")}
|
||||||
/>*/}
|
/>*/}
|
||||||
<Select
|
<Select
|
||||||
value={udpPortMode}
|
value={
|
||||||
onValueChange={(value: PortMode) => {
|
udpPortMode
|
||||||
setUdpPortMode(value);
|
}
|
||||||
|
onValueChange={(
|
||||||
|
value: PortMode
|
||||||
|
) => {
|
||||||
|
setUdpPortMode(
|
||||||
|
value
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<FormControl>
|
||||||
<SelectTrigger className="w-[110px]">
|
<SelectTrigger className="w-[110px]">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="all">
|
<SelectItem value="all">
|
||||||
{t("allPorts")}
|
{t(
|
||||||
|
"allPorts"
|
||||||
|
)}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="blocked">
|
<SelectItem value="blocked">
|
||||||
{t("blocked")}
|
{t(
|
||||||
|
"blocked"
|
||||||
|
)}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="custom">
|
<SelectItem value="custom">
|
||||||
{t("custom")}
|
{t(
|
||||||
|
"custom"
|
||||||
|
)}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
{udpPortMode === "custom" ? (
|
{udpPortMode ===
|
||||||
|
"custom" ? (
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="53,123,500-600"
|
placeholder="53,123,500-600"
|
||||||
value={udpCustomPorts}
|
value={
|
||||||
onChange={(e) =>
|
udpCustomPorts
|
||||||
setUdpCustomPorts(e.target.value)
|
}
|
||||||
|
onChange={(
|
||||||
|
e
|
||||||
|
) =>
|
||||||
|
setUdpCustomPorts(
|
||||||
|
e
|
||||||
|
.target
|
||||||
|
.value
|
||||||
|
)
|
||||||
}
|
}
|
||||||
className="flex-1"
|
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
) : (
|
) : (
|
||||||
<Input
|
<Input
|
||||||
disabled
|
disabled
|
||||||
placeholder={
|
placeholder={
|
||||||
udpPortMode === "all"
|
udpPortMode ===
|
||||||
? t("allPortsAllowed")
|
"all"
|
||||||
: t("allPortsBlocked")
|
? t(
|
||||||
|
"allPortsAllowed"
|
||||||
|
)
|
||||||
|
: t(
|
||||||
|
"allPortsBlocked"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
className="flex-1"
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -1068,25 +1169,66 @@ export default function EditInternalResourceDialog({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* ICMP Toggle */}
|
{/* 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
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="disableIcmp"
|
name="disableIcmp"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<FormLabel className="min-w-10">
|
|
||||||
ICMP
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Switch
|
<Switch
|
||||||
checked={!field.value}
|
checked={
|
||||||
onCheckedChange={(checked) => field.onChange(!checked)}
|
!field.value
|
||||||
|
}
|
||||||
|
onCheckedChange={(
|
||||||
|
checked
|
||||||
|
) =>
|
||||||
|
field.onChange(
|
||||||
|
!checked
|
||||||
|
)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
{field.value ? t("blocked") : t("allowed")}
|
{field.value
|
||||||
|
? t(
|
||||||
|
"blocked"
|
||||||
|
)
|
||||||
|
: t(
|
||||||
|
"allowed"
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -1095,18 +1237,30 @@ export default function EditInternalResourceDialog({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Access Control Section */}
|
{/* Access Control Tab */}
|
||||||
<div>
|
<div className="space-y-4 mt-4">
|
||||||
<h3 className="text-lg font-semibold mb-4">
|
<div className="mb-8">
|
||||||
{t("resourceUsersRoles")}
|
<label className="font-medium block">
|
||||||
</h3>
|
{t(
|
||||||
|
"editInternalResourceDialogAccessControl"
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
{t(
|
||||||
|
"editInternalResourceDialogAccessControlDescription"
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{loadingRolesUsers ? (
|
{loadingRolesUsers ? (
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
{t("loading")}
|
{t("loading")}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
{/* Roles */}
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="roles"
|
name="roles"
|
||||||
@@ -1130,7 +1284,8 @@ export default function EditInternalResourceDialog({
|
|||||||
size="sm"
|
size="sm"
|
||||||
tags={
|
tags={
|
||||||
form.getValues()
|
form.getValues()
|
||||||
.roles || []
|
.roles ||
|
||||||
|
[]
|
||||||
}
|
}
|
||||||
setTags={(
|
setTags={(
|
||||||
newRoles
|
newRoles
|
||||||
@@ -1159,14 +1314,11 @@ export default function EditInternalResourceDialog({
|
|||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
<FormDescription>
|
|
||||||
{t(
|
|
||||||
"resourceRoleDescription"
|
|
||||||
)}
|
|
||||||
</FormDescription>
|
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Users */}
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="users"
|
name="users"
|
||||||
@@ -1189,7 +1341,8 @@ export default function EditInternalResourceDialog({
|
|||||||
)}
|
)}
|
||||||
tags={
|
tags={
|
||||||
form.getValues()
|
form.getValues()
|
||||||
.users || []
|
.users ||
|
||||||
|
[]
|
||||||
}
|
}
|
||||||
size="sm"
|
size="sm"
|
||||||
setTags={(
|
setTags={(
|
||||||
@@ -1222,6 +1375,8 @@ export default function EditInternalResourceDialog({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Clients (Machines) */}
|
||||||
{hasMachineClients && (
|
{hasMachineClients && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -1277,7 +1432,9 @@ export default function EditInternalResourceDialog({
|
|||||||
restrictTagsToAutocompleteOptions={
|
restrictTagsToAutocompleteOptions={
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
sortTags={true}
|
sortTags={
|
||||||
|
true
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -1288,6 +1445,7 @@ export default function EditInternalResourceDialog({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</HorizontalTabs>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</CredenzaBody>
|
</CredenzaBody>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useParams, usePathname } from "next/navigation";
|
import { useParams, usePathname } from "next/navigation";
|
||||||
import { cn } from "@app/lib/cn";
|
import { cn } from "@app/lib/cn";
|
||||||
@@ -20,17 +20,22 @@ interface HorizontalTabsProps {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
items: TabItem[];
|
items: TabItem[];
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
clientSide?: boolean;
|
||||||
|
defaultTab?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HorizontalTabs({
|
export function HorizontalTabs({
|
||||||
children,
|
children,
|
||||||
items,
|
items,
|
||||||
disabled = false
|
disabled = false,
|
||||||
|
clientSide = false,
|
||||||
|
defaultTab = 0
|
||||||
}: HorizontalTabsProps) {
|
}: HorizontalTabsProps) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const { licenseStatus, isUnlocked } = useLicenseStatusContext();
|
const { licenseStatus, isUnlocked } = useLicenseStatusContext();
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
const [activeClientTab, setActiveClientTab] = useState(defaultTab);
|
||||||
|
|
||||||
function hydrateHref(href: string) {
|
function hydrateHref(href: string) {
|
||||||
return href
|
return href
|
||||||
@@ -43,6 +48,73 @@ export function HorizontalTabs({
|
|||||||
.replace("{remoteExitNodeId}", params.remoteExitNodeId as string);
|
.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 (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const buttonVariants = cva(
|
|||||||
destructive:
|
destructive:
|
||||||
"bg-destructive text-white dark:text-destructive-foreground hover:bg-destructive/90 ",
|
"bg-destructive text-white dark:text-destructive-foreground hover:bg-destructive/90 ",
|
||||||
outline:
|
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:
|
outlinePrimary:
|
||||||
"border border-primary bg-card hover:bg-primary/10 text-primary ",
|
"border border-primary bg-card hover:bg-primary/10 text-primary ",
|
||||||
secondary:
|
secondary:
|
||||||
|
|||||||
@@ -228,7 +228,7 @@ export const resourceQueries = {
|
|||||||
queryFn: async ({ signal, meta }) => {
|
queryFn: async ({ signal, meta }) => {
|
||||||
const res = await meta!.api.get<
|
const res = await meta!.api.get<
|
||||||
AxiosResponse<ListSiteResourceUsersResponse>
|
AxiosResponse<ListSiteResourceUsersResponse>
|
||||||
>(`/resource/${resourceId}/users`, { signal });
|
>(`/site-resource/${resourceId}/users`, { signal });
|
||||||
return res.data.data.users;
|
return res.data.data.users;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -238,7 +238,7 @@ export const resourceQueries = {
|
|||||||
queryFn: async ({ signal, meta }) => {
|
queryFn: async ({ signal, meta }) => {
|
||||||
const res = await meta!.api.get<
|
const res = await meta!.api.get<
|
||||||
AxiosResponse<ListSiteResourceRolesResponse>
|
AxiosResponse<ListSiteResourceRolesResponse>
|
||||||
>(`/resource/${resourceId}/roles`, { signal });
|
>(`/site-resource/${resourceId}/roles`, { signal });
|
||||||
|
|
||||||
return res.data.data.roles;
|
return res.data.data.roles;
|
||||||
}
|
}
|
||||||
@@ -249,7 +249,7 @@ export const resourceQueries = {
|
|||||||
queryFn: async ({ signal, meta }) => {
|
queryFn: async ({ signal, meta }) => {
|
||||||
const res = await meta!.api.get<
|
const res = await meta!.api.get<
|
||||||
AxiosResponse<ListSiteResourceClientsResponse>
|
AxiosResponse<ListSiteResourceClientsResponse>
|
||||||
>(`/resource/${resourceId}/clients`, { signal });
|
>(`/site-resource/${resourceId}/clients`, { signal });
|
||||||
|
|
||||||
return res.data.data.clients;
|
return res.data.data.clients;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user