diff --git a/messages/en-US.json b/messages/en-US.json index 0cfd8f6f..df0907a5 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -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." } diff --git a/src/components/ClientResourcesTable.tsx b/src/components/ClientResourcesTable.tsx index 023ef00c..5a7f031d 100644 --- a/src/components/ClientResourcesTable.tsx +++ b/src/components/ClientResourcesTable.tsx @@ -329,8 +329,11 @@ export default function ClientResourcesTable({ orgId={orgId} sites={sites} onSuccess={() => { - router.refresh(); - setEditingResource(null); + // 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={() => { - router.refresh(); + // Delay refresh to allow modal to close smoothly + setTimeout(() => { + router.refresh(); + }, 150); }} /> diff --git a/src/components/CreateInternalResourceDialog.tsx b/src/components/CreateInternalResourceDialog.tsx index 00e8ce96..afdaa77e 100644 --- a/src/components/CreateInternalResourceDialog.tsx +++ b/src/components/CreateInternalResourceDialog.tsx @@ -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 - .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.' - } - ); +// 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: 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 ( - + {t("createInternalResourceDialogCreateClientResource")} @@ -516,179 +511,180 @@ export default function CreateInternalResourceDialog({ className="space-y-6" id="create-internal-resource-form" > - {/* Resource Properties Form */} -
-

- {t( - "createInternalResourceDialogResourceProperties" + {/* Name and Site - Side by Side */} +
+ ( + + + {t( + "createInternalResourceDialogName" + )} + + + + + + )} -

-
- ( - - - {t( - "createInternalResourceDialogName" - )} - - - - - - - )} - /> + /> - ( - - - {t( - "site" - )} - - - - - - - - - - - - - {t( - "noSitesFound" - )} - - - {availableSites.map( - ( - site - ) => ( - { - field.onChange( - site.siteId - ); - }} - > - - { - site.name - } - - ) - )} - - - - - - - - )} - /> - - ( - - - {t( - "createInternalResourceDialogMode" - )} - - - - - )} - /> - {/* - {mode === "port" && ( - <> -
+ /> + + + {t( + "noSitesFound" + )} + + + {availableSites.map( + (site) => ( + { + field.onChange( + site.siteId + ); + }} + > + + { + site.name + } + + ) + )} + + + + + + + + )} + /> +
+ + {/* Tabs for Network Settings and Access Control */} + + {/* Network Settings Tab */} +
+
+
+ +
+ {t( + "editInternalResourceDialogDestinationDescription" + )} +
+
+ +
+ {/* Mode - Smaller select */} +
( - {t("createInternalResourceDialogProtocol")} + {t( + "createInternalResourceDialogMode" + )} @@ -708,22 +708,29 @@ export default function CreateInternalResourceDialog({ )} /> +
+ {/* Destination - Larger input */} +
( - {t("createInternalResourceDialogSitePort")} + + {t( + "createInternalResourceDialogDestination" + )} + - field.onChange( - e.target.value === "" ? undefined : parseInt(e.target.value) - ) - } + {...field} /> @@ -731,418 +738,396 @@ export default function CreateInternalResourceDialog({ )} />
- - )} */} -
-
- {/* Target Configuration Form */} -
-

- {t( - "createInternalResourceDialogTargetConfiguration" - )} -

-
- ( - - - {t( - "createInternalResourceDialogDestination" - )} - - - - - - {mode === "host" && - t( - "createInternalResourceDialogDestinationHostDescription" + {/* Alias - Equally sized input (if allowed) */} + {mode !== "cidr" && ( +
+ ( + + + {t( + "createInternalResourceDialogAlias" + )} + + + + + + )} - {mode === "cidr" && - t( - "createInternalResourceDialogDestinationCidrDescription" - )} - {/* {mode === "port" && t("createInternalResourceDialogDestinationIPDescription")} */} - - - - )} - /> - - {/* {mode === "port" && ( - ( - - - {t("targetPort")} - - - - field.onChange( - e.target.value === "" ? undefined : parseInt(e.target.value) - ) - } - /> - - - {t("createInternalResourceDialogDestinationPortDescription")} - - - + /> +
)} - /> - )} */} -
-
+
+
- {/* Alias */} - {mode !== "cidr" && ( -
- ( - - + {/* Ports and Restrictions */} +
+ {/* TCP Ports */} +
+ +
+ {t( + "editInternalResourceDialogPortRestrictionsDescription" + )} +
+
+
+
+ {t( - "createInternalResourceDialogAlias" + "editInternalResourceDialogTcp" )} - - - - +
+
+ ( + +
+ {/**/} + + {tcpPortMode === + "custom" ? ( + + + setTcpCustomPorts( + e + .target + .value + ) + } + /> + + ) : ( + + )} +
+ +
+ )} + /> +
+
+ + {/* UDP Ports */} +
+
+ {t( - "createInternalResourceDialogAliasDescription" + "editInternalResourceDialogUdp" )} - - - - )} - /> -
- )} - - {/* Port Restrictions Section */} -
-

- {t("portRestrictions")} -

-
- {/* TCP Ports */} - ( - -
- - TCP - - {/**/} - - {tcpPortMode === "custom" ? ( - - - setTcpCustomPorts(e.target.value) - } - className="flex-1" - /> - - ) : ( - - )} -
- -
- )} - /> - - {/* UDP Ports */} - ( - -
- - UDP - - {/**/} - - {udpPortMode === "custom" ? ( - - - setUdpCustomPorts(e.target.value) - } - className="flex-1" - /> - - ) : ( - - )} -
- -
- )} - /> - - {/* ICMP Toggle */} - ( - -
- - ICMP - - - field.onChange(!checked)} - /> - - - {field.value ? t("blocked") : t("allowed")} - -
- -
- )} - /> -
-
- - {/* Access Control Section */} -
-

- {t("resourceUsersRoles")} -

-
- ( - - - {t("roles")} - - { - form.setValue( - "roles", - newRoles as [ - Tag, - ...Tag[] - ] - ); - }} - enableAutocomplete={ - true - } - autocompleteOptions={ - allRoles - } - allowDuplicates={false} - restrictTagsToAutocompleteOptions={ - true - } - sortTags={true} - /> - - - +
+
+ ( + +
+ {/**/} + + {udpPortMode === + "custom" ? ( + + + setUdpCustomPorts( + e + .target + .value + ) + } + /> + + ) : ( + + )} +
+ +
+ )} + /> +
+
+ + {/* ICMP Toggle */} +
+
+ {t( - "resourceRoleDescription" + "editInternalResourceDialogIcmp" )} - - - )} - /> - ( - - - {t("users")} - - { - form.setValue( - "users", - newUsers as [ - Tag, - ...Tag[] - ] - ); - }} - enableAutocomplete={ - true - } - autocompleteOptions={ - allUsers - } - allowDuplicates={false} - restrictTagsToAutocompleteOptions={ - true - } - sortTags={true} - /> - - - - )} - /> - {hasMachineClients && ( +
+
+ ( + +
+ + + field.onChange( + !checked + ) + } + /> + + + {field.value + ? t( + "blocked" + ) + : t( + "allowed" + )} + +
+ +
+ )} + /> +
+
+
+
+ + {/* Access Control Tab */} +
+
+ +
+ {t( + "editInternalResourceDialogAccessControlDescription" + )} +
+
+
+ {/* Roles */} ( - {t("machineClients")} + {t("roles")} { form.setValue( - "clients", - newClients as [ + "roles", + newRoles as [ Tag, ...Tag[] ] @@ -1152,7 +1137,70 @@ export default function CreateInternalResourceDialog({ true } autocompleteOptions={ - allClients + allRoles + } + allowDuplicates={ + false + } + restrictTagsToAutocompleteOptions={ + true + } + sortTags={true} + /> + + + + {t( + "resourceRoleDescription" + )} + + + )} + /> + + {/* Users */} + ( + + + {t("users")} + + + { + form.setValue( + "users", + newUsers as [ + Tag, + ...Tag[] + ] + ); + }} + enableAutocomplete={ + true + } + autocompleteOptions={ + allUsers } allowDuplicates={ false @@ -1167,9 +1215,76 @@ export default function CreateInternalResourceDialog({ )} /> - )} + + {/* Clients (Machines) */} + {hasMachineClients && ( + ( + + + {t( + "machineClients" + )} + + + { + form.setValue( + "clients", + newClients as [ + Tag, + ...Tag[] + ] + ); + }} + enableAutocomplete={ + true + } + autocompleteOptions={ + allClients + } + allowDuplicates={ + false + } + restrictTagsToAutocompleteOptions={ + true + } + sortTags={ + true + } + /> + + + + )} + /> + )} +
-
+ diff --git a/src/components/EditInternalResourceDialog.tsx b/src/components/EditInternalResourceDialog.tsx index 5d5745c7..88d98aa5 100644 --- a/src/components/EditInternalResourceDialog.tsx +++ b/src/components/EditInternalResourceDialog.tsx @@ -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 - .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.' - } - ); +// 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: 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( getPortModeFromString(resource.tcpPortRangeString) @@ -446,30 +465,27 @@ export default function EditInternalResourceDialog({ } // Update the site resource - await api.post( - `/site-resource/${resource.id}`, - { - name: data.name, - siteId: data.siteId, - mode: data.mode, - // protocol: data.mode === "port" ? data.protocol : null, - // proxyPort: data.mode === "port" ? data.proxyPort : null, - // destinationPort: data.mode === "port" ? data.destinationPort : null, - destination: data.destination, - alias: - data.alias && - typeof data.alias === "string" && - data.alias.trim() - ? data.alias - : null, - tcpPortRangeString: data.tcpPortRangeString, - udpPortRangeString: data.udpPortRangeString, - disableIcmp: data.disableIcmp ?? false, - roleIds: (data.roles || []).map((r) => parseInt(r.id)), - userIds: (data.users || []).map((u) => u.id), - clientIds: (data.clients || []).map((c) => parseInt(c.id)) - } - ); + await api.post(`/site-resource/${resource.id}`, { + name: data.name, + siteId: data.siteId, + mode: data.mode, + // protocol: data.mode === "port" ? data.protocol : null, + // proxyPort: data.mode === "port" ? data.proxyPort : null, + // destinationPort: data.mode === "port" ? data.destinationPort : null, + destination: data.destination, + alias: + data.alias && + typeof data.alias === "string" && + data.alias.trim() + ? data.alias + : null, + tcpPortRangeString: data.tcpPortRangeString, + udpPortRangeString: data.udpPortRangeString, + disableIcmp: data.disableIcmp ?? false, + 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); }} > - + {t("editInternalResourceDialogEditClientResource")} @@ -639,627 +671,628 @@ export default function EditInternalResourceDialog({ className="space-y-6" id="edit-internal-resource-form" > - {/* Resource Properties Form */} -
-

- {t( - "editInternalResourceDialogResourceProperties" + {/* Name and Site - Side by Side */} +
+ ( + + + {t( + "editInternalResourceDialogName" + )} + + + + + + )} -

-
- ( - - - {t( - "editInternalResourceDialogName" - )} - - - - - - - )} - /> + /> - ( - - - {t( - "site" - )} - - - - - - - - - - - - - {t( - "noSitesFound" - )} - - - {availableSites.map( - ( - site - ) => ( - { - field.onChange( - site.siteId - ); - }} - > - - { - site.name - } - - ) - )} - - - - - - - - )} - /> - - ( - - - {t( - "editInternalResourceDialogMode" - )} - - - - - )} - /> - - {/* {mode === "port" && ( -
- ( - - {t("editInternalResourceDialogProtocol")} - - - - )} - /> - - ( - - {t("editInternalResourceDialogSitePort")} - - field.onChange(e.target.value === "" ? undefined : parseInt(e.target.value) || 0)} - /> - - - - )} - /> -
- )} */} -
-
- - {/* Target Configuration Form */} -
-

- {t( - "editInternalResourceDialogTargetConfiguration" + {field.value + ? availableSites.find( + (site) => + site.siteId === + field.value + )?.name + : t( + "selectSite" + )} + + + + + + + + + + {t( + "noSitesFound" + )} + + + {availableSites.map( + (site) => ( + { + field.onChange( + site.siteId + ); + }} + > + + { + site.name + } + + ) + )} + + + + + + + )} -

-
- ( - - - {t( - "editInternalResourceDialogDestination" - )} - - - - - - {mode === "host" && - t( - "editInternalResourceDialogDestinationHostDescription" - )} - {mode === "cidr" && - t( - "editInternalResourceDialogDestinationCidrDescription" - )} - {/* {mode === "port" && t("editInternalResourceDialogDestinationIPDescription")} */} - - - - )} - /> - - {/* {mode === "port" && ( - ( - - {t("targetPort")} - - field.onChange(e.target.value === "" ? undefined : parseInt(e.target.value) || 0)} - /> - - - - )} - /> - )} */} -
+ />
- {/* Alias */} - {mode !== "cidr" && ( -
- ( - - - {t( - "editInternalResourceDialogAlias" + {/* Tabs for Network Settings and Access Control */} + + {/* Network Settings Tab */} +
+
+
+ +
+ {t( + "editInternalResourceDialogDestinationDescription" + )} +
+
+ +
+ {/* Mode - Smaller select */} +
+ ( + + + {t( + "editInternalResourceDialogMode" + )} + + + + )} - - - +
+ + {/* Destination - Larger input */} +
+ ( + + + {t( + "editInternalResourceDialogDestination" + )} + + + + + + + )} + /> +
+ + {/* Alias - Equally sized input (if allowed) */} + {mode !== "cidr" && ( +
+ ( + + + {t( + "editInternalResourceDialogAlias" + )} + + + + + + + )} /> - - - {t( - "editInternalResourceDialogAliasDescription" - )} - - - - )} - /> -
- )} - - {/* Port Restrictions Section */} -
-

- {t("portRestrictions")} -

-
- {/* TCP Ports */} - ( - -
- - TCP - - {/**/} - - {tcpPortMode === "custom" ? ( - - - setTcpCustomPorts(e.target.value) - } - className="flex-1" - /> - - ) : ( - - )}
- -
- )} - /> - - {/* UDP Ports */} - ( - -
- - UDP - - {/**/} - - {udpPortMode === "custom" ? ( - - - setUdpCustomPorts(e.target.value) - } - className="flex-1" - /> - - ) : ( - - )} -
- -
- )} - /> - - {/* ICMP Toggle */} - ( - -
- - ICMP - - - field.onChange(!checked)} - /> - - - {field.value ? t("blocked") : t("allowed")} - -
- -
- )} - /> -
-
- - {/* Access Control Section */} -
-

- {t("resourceUsersRoles")} -

- {loadingRolesUsers ? ( -
- {t("loading")} + )} +
- ) : ( + + {/* Ports and Restrictions */}
- ( - - - {t("roles")} - - - { - form.setValue( - "roles", - newRoles as [ - Tag, - ...Tag[] - ] - ); - }} - enableAutocomplete={ - true - } - autocompleteOptions={ - allRoles - } - allowDuplicates={ - false - } - restrictTagsToAutocompleteOptions={ - true - } - sortTags={true} - /> - - - - {t( - "resourceRoleDescription" - )} - - + {/* TCP Ports */} +
+ +
+ {t( + "editInternalResourceDialogPortRestrictionsDescription" + )} +
+
+
- ( - - - {t("users")} - - - { - form.setValue( - "users", - newUsers as [ - Tag, - ...Tag[] - ] - ); - }} - enableAutocomplete={ - true - } - autocompleteOptions={ - allUsers - } - allowDuplicates={ - false - } - restrictTagsToAutocompleteOptions={ - true - } - sortTags={true} - /> - - - + > +
+ + {t( + "editInternalResourceDialogTcp" + )} + +
+
+ ( + +
+ {/**/} + + {tcpPortMode === + "custom" ? ( + + + setTcpCustomPorts( + e + .target + .value + ) + } + /> + + ) : ( + + )} +
+ +
+ )} + /> +
+
+ + {/* UDP Ports */} +
- {hasMachineClients && ( + > +
+ + {t( + "editInternalResourceDialogUdp" + )} + +
+
+ ( + +
+ {/**/} + + {udpPortMode === + "custom" ? ( + + + setUdpCustomPorts( + e + .target + .value + ) + } + /> + + ) : ( + + )} +
+ +
+ )} + /> +
+
+ + {/* ICMP Toggle */} +
+
+ + {t( + "editInternalResourceDialogIcmp" + )} + +
+
+ ( + +
+ + + field.onChange( + !checked + ) + } + /> + + + {field.value + ? t( + "blocked" + ) + : t( + "allowed" + )} + +
+ +
+ )} + /> +
+
+
+
+ + {/* Access Control Tab */} +
+
+ +
+ {t( + "editInternalResourceDialogAccessControlDescription" + )} +
+
+ {loadingRolesUsers ? ( +
+ {t("loading")} +
+ ) : ( +
+ {/* Roles */} ( - {t( - "machineClients" - )} + {t("roles")} { form.setValue( - "clients", - newClients as [ + "roles", + newRoles as [ Tag, ...Tag[] ] @@ -1269,7 +1302,7 @@ export default function EditInternalResourceDialog({ true } autocompleteOptions={ - machineClients + allRoles } allowDuplicates={ false @@ -1284,10 +1317,135 @@ export default function EditInternalResourceDialog({ )} /> - )} -
- )} -
+ + {/* Users */} + ( + + + {t("users")} + + + { + form.setValue( + "users", + newUsers as [ + Tag, + ...Tag[] + ] + ); + }} + enableAutocomplete={ + true + } + autocompleteOptions={ + allUsers + } + allowDuplicates={ + false + } + restrictTagsToAutocompleteOptions={ + true + } + sortTags={true} + /> + + + + )} + /> + + {/* Clients (Machines) */} + {hasMachineClients && ( + ( + + + {t( + "machineClients" + )} + + + { + form.setValue( + "clients", + newClients as [ + Tag, + ...Tag[] + ] + ); + }} + enableAutocomplete={ + true + } + autocompleteOptions={ + machineClients + } + allowDuplicates={ + false + } + restrictTagsToAutocompleteOptions={ + true + } + sortTags={ + true + } + /> + + + + )} + /> + )} +
+ )} +
+
diff --git a/src/components/HorizontalTabs.tsx b/src/components/HorizontalTabs.tsx index 72093e0d..717a3c12 100644 --- a/src/components/HorizontalTabs.tsx +++ b/src/components/HorizontalTabs.tsx @@ -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 ( +
+
+
+
+ {items.map((item, index) => { + const isActive = activeClientTab === index; + const isProfessional = + item.showProfessional && !isUnlocked(); + const isDisabled = + disabled || + (isProfessional && !isUnlocked()); + + return ( + + ); + })} +
+
+
+
{activeChild}
+
+ ); + } + + // Server-side mode: original behavior with routing return (
diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index c3037250..f530ace1 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -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: diff --git a/src/lib/queries.ts b/src/lib/queries.ts index 0dc44147..e90f6eea 100644 --- a/src/lib/queries.ts +++ b/src/lib/queries.ts @@ -228,7 +228,7 @@ export const resourceQueries = { queryFn: async ({ signal, meta }) => { const res = await meta!.api.get< AxiosResponse - >(`/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 - >(`/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 - >(`/resource/${resourceId}/clients`, { signal }); + >(`/site-resource/${resourceId}/clients`, { signal }); return res.data.data.clients; }