From 2e4c2adc1a23447e542a854f9f30afdf5f9beb47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Thu, 16 Apr 2026 16:19:21 +0200 Subject: [PATCH 1/3] check if a gateway with a given name exists already during setup --- web/messages/en/gateway_wizard.json | 1 + .../steps/SetupGatewayComponentStep.tsx | 13 ++++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/web/messages/en/gateway_wizard.json b/web/messages/en/gateway_wizard.json index 338c3ea15..10d032f99 100644 --- a/web/messages/en/gateway_wizard.json +++ b/web/messages/en/gateway_wizard.json @@ -28,6 +28,7 @@ "gateway_setup_component_label_grpc_port": "gRPC Port", "gateway_setup_component_label_grpc_port_help": "If you have changed the default Gateway gRPC port, please change it here.", "gateway_setup_component_error_common_name_required": "Gateway Name is required", + "gateway_setup_component_error_common_name_duplicate": "A gateway with this name already exists", "gateway_setup_component_error_ip_or_domain_required": "IP or Domain is required", "gateway_setup_component_error_grpc_port_required": "gRPC Port is required", "gateway_setup_component_error_grpc_port_max": "gRPC Port must be less than 65536", diff --git a/web/src/pages/GatewaySetupPage/steps/SetupGatewayComponentStep.tsx b/web/src/pages/GatewaySetupPage/steps/SetupGatewayComponentStep.tsx index f42c208b3..77751286d 100644 --- a/web/src/pages/GatewaySetupPage/steps/SetupGatewayComponentStep.tsx +++ b/web/src/pages/GatewaySetupPage/steps/SetupGatewayComponentStep.tsx @@ -1,3 +1,4 @@ +import { useQuery } from '@tanstack/react-query'; import { useMemo } from 'react'; import z from 'zod'; import { useShallow } from 'zustand/react/shallow'; @@ -9,10 +10,11 @@ import { SizedBox } from '../../../shared/defguard-ui/components/SizedBox/SizedB import { ThemeSpacing } from '../../../shared/defguard-ui/types'; import { useAppForm } from '../../../shared/form'; import { formChangeLogic } from '../../../shared/formLogic'; +import { getGatewaysQueryOptions } from '../../../shared/query'; +import { Validate } from '../../../shared/validate'; import { GatewaySetupStep } from '../types'; import { useGatewayWizardStore } from '../useGatewayWizardStore'; import './style.scss'; -import { Validate } from '../../../shared/validate'; type FormFields = StoreValues; @@ -24,6 +26,7 @@ type StoreValues = { export const SetupGatewayComponentStep = () => { const setActiveStep = useGatewayWizardStore((s) => s.setActiveStep); + const { data: gateways } = useQuery(getGatewaysQueryOptions); const defaultValues = useGatewayWizardStore( useShallow( @@ -50,7 +53,11 @@ export const SetupGatewayComponentStep = () => { z.object({ common_name: z .string() - .min(1, m.edge_setup_component_error_common_name_required()), + .min(1, m.gateway_setup_component_error_common_name_required()) + .refine( + (val) => !gateways?.some((g) => g.name === val), + m.gateway_setup_component_error_common_name_duplicate(), + ), ip_or_domain: z .string() .min(1, m.edge_setup_component_error_ip_or_domain_required()) @@ -66,7 +73,7 @@ export const SetupGatewayComponentStep = () => { .min(1, m.edge_setup_component_error_grpc_port_required()) .max(65535, m.edge_setup_component_error_grpc_port_max()), }), - [], + [gateways], ); const form = useAppForm({ From 794721dd12ea06698479424ea58d5f9ea5c4e4a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Fri, 17 Apr 2026 09:19:35 +0200 Subject: [PATCH 2/3] also handle name validation in gateway edit form --- web/messages/en/gateway.json | 1 + .../pages/EditGatewayPage/EditGatewayPage.tsx | 47 ++++++++++++------- 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/web/messages/en/gateway.json b/web/messages/en/gateway.json index 3d8107ea1..199b6bd06 100644 --- a/web/messages/en/gateway.json +++ b/web/messages/en/gateway.json @@ -11,6 +11,7 @@ "gateway_helper_port": "", "gateway_edit_delete": "Delete", "gateway_edit_success": "Gateway updated", + "gateway_edit_error_name_duplicate": "A gateway with this name already exists", "gateway_edit_failed": "Failed to update gateway", "gateway_status_all_connected": "All gateways connected", "gateway_status_connected_count": "{count} gateways connected", diff --git a/web/src/pages/EditGatewayPage/EditGatewayPage.tsx b/web/src/pages/EditGatewayPage/EditGatewayPage.tsx index 0427a89b4..73f69686c 100644 --- a/web/src/pages/EditGatewayPage/EditGatewayPage.tsx +++ b/web/src/pages/EditGatewayPage/EditGatewayPage.tsx @@ -15,7 +15,11 @@ import { useAppForm } from '../../shared/form'; import { formChangeLogic } from '../../shared/formLogic'; import { openModal } from '../../shared/hooks/modalControls/modalsSubjects'; import { ModalName } from '../../shared/hooks/modalControls/modalTypes'; -import { getGatewayQueryOptions, getLocationQueryOptions } from '../../shared/query'; +import { + getGatewayQueryOptions, + getGatewaysQueryOptions, + getLocationQueryOptions, +} from '../../shared/query'; export const EditGatewayPage = () => { const { gatewayId } = useParams({ @@ -41,23 +45,9 @@ export const EditGatewayPage = () => { ); }; -const formSchema = z.object({ - name: z.string(m.form_error_required()).min(1, m.form_error_required()), - address: z.string().nullable(), - port: z.number().nullable(), - connected_at: z.string().nullable(), - disconnected_at: z.string().nullable(), - enabled: z.boolean(), - modified_at: z.string(), - modified_by: z.string(), - version: z.string().nullable(), - location_id: z.number(), -}); - -type FormFields = z.infer; - const EditGatewayForm = ({ gateway }: { gateway: Gateway }) => { const navigate = useNavigate(); + const { data: gateways } = useSuspenseQuery(getGatewaysQueryOptions); const { data: location } = useSuspenseQuery( getLocationQueryOptions(gateway.location_id), ); @@ -75,6 +65,31 @@ const EditGatewayForm = ({ gateway }: { gateway: Gateway }) => { }, }); + const formSchema = useMemo( + () => + z.object({ + name: z + .string(m.form_error_required()) + .min(1, m.form_error_required()) + .refine( + (val) => val === gateway.name || !gateways?.some((g) => g.name === val), + m.gateway_edit_error_name_duplicate(), + ), + address: z.string().nullable(), + port: z.number().nullable(), + connected_at: z.string().nullable(), + disconnected_at: z.string().nullable(), + enabled: z.boolean(), + modified_at: z.string(), + modified_by: z.string(), + version: z.string().nullable(), + location_id: z.number(), + }), + [gateways, gateway.name], + ); + + type FormFields = z.infer; + const defaultValues = useMemo((): FormFields => ({ ...gateway }), [gateway]); const form = useAppForm({ From 48eecd409f314db2c8c8073483b043e2388df380 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20W=C3=B3jcik?= Date: Fri, 17 Apr 2026 09:21:54 +0200 Subject: [PATCH 3/3] add validation for edge component names --- web/messages/en/edge.json | 1 + web/messages/en/edge_wizard.json | 1 + .../steps/SetupEdgeComponentStep.tsx | 11 ++++- web/src/pages/EditEdgePage/EditEdgePage.tsx | 41 ++++++++++++------- .../initial/steps/SetupEdgeComponentStep.tsx | 11 ++++- 5 files changed, 46 insertions(+), 19 deletions(-) diff --git a/web/messages/en/edge.json b/web/messages/en/edge.json index f6689c132..97375142d 100644 --- a/web/messages/en/edge.json +++ b/web/messages/en/edge.json @@ -12,6 +12,7 @@ "edge_edit_public_address": "Public domain", "edge_edit_delete": "Delete", "edge_edit_success": "Edge Component updated", + "edge_edit_error_name_duplicate": "An Edge Component with this name already exists", "edge_edit_failed": "Failed to update Edge Component", "edges_header_title": "All components", "edges_col_name": "Name", diff --git a/web/messages/en/edge_wizard.json b/web/messages/en/edge_wizard.json index 67c5b8637..2e146c20b 100644 --- a/web/messages/en/edge_wizard.json +++ b/web/messages/en/edge_wizard.json @@ -28,6 +28,7 @@ "edge_setup_component_label_grpc_port": "gRPC Port", "edge_setup_component_label_grpc_port_help": "If you have changed the default Edge gRPC port, please change it here.", "edge_setup_component_error_common_name_required": "Edge Name is required", + "edge_setup_component_error_common_name_duplicate": "An Edge Component with this name already exists", "edge_setup_component_error_ip_or_domain_required": "IP or Domain is required", "edge_setup_component_error_grpc_port_required": "gRPC Port is required", "edge_setup_component_error_grpc_port_max": "gRPC Port must be less than 65536", diff --git a/web/src/pages/EdgeSetupPage/steps/SetupEdgeComponentStep.tsx b/web/src/pages/EdgeSetupPage/steps/SetupEdgeComponentStep.tsx index 5fbc541ef..7f888e5f8 100644 --- a/web/src/pages/EdgeSetupPage/steps/SetupEdgeComponentStep.tsx +++ b/web/src/pages/EdgeSetupPage/steps/SetupEdgeComponentStep.tsx @@ -1,3 +1,4 @@ +import { useQuery } from '@tanstack/react-query'; import { useMemo } from 'react'; import z from 'zod'; import { useShallow } from 'zustand/react/shallow'; @@ -9,6 +10,7 @@ import { SizedBox } from '../../../shared/defguard-ui/components/SizedBox/SizedB import { ThemeSpacing } from '../../../shared/defguard-ui/types'; import { useAppForm } from '../../../shared/form'; import { formChangeLogic } from '../../../shared/formLogic'; +import { getEdgesQueryOptions } from '../../../shared/query'; import { Validate } from '../../../shared/validate'; import { EdgeSetupStep } from '../types'; import { useEdgeWizardStore } from '../useEdgeWizardStore'; @@ -23,6 +25,7 @@ type StoreValues = { export const SetupEdgeComponentStep = () => { const setActiveStep = useEdgeWizardStore((s) => s.setActiveStep); + const { data: edges } = useQuery(getEdgesQueryOptions); const defaultValues = useEdgeWizardStore( useShallow( @@ -49,7 +52,11 @@ export const SetupEdgeComponentStep = () => { z.object({ common_name: z .string() - .min(1, m.edge_setup_component_error_common_name_required()), + .min(1, m.edge_setup_component_error_common_name_required()) + .refine( + (val) => !edges?.some((e) => e.name === val), + m.edge_setup_component_error_common_name_duplicate(), + ), ip_or_domain: z .string() .min(1, m.edge_setup_component_error_ip_or_domain_required()) @@ -65,7 +72,7 @@ export const SetupEdgeComponentStep = () => { .min(1, m.edge_setup_component_error_grpc_port_required()) .max(65535, m.edge_setup_component_error_grpc_port_max()), }), - [], + [edges], ); const form = useAppForm({ diff --git a/web/src/pages/EditEdgePage/EditEdgePage.tsx b/web/src/pages/EditEdgePage/EditEdgePage.tsx index 6f3fb5a6e..798a2d32b 100644 --- a/web/src/pages/EditEdgePage/EditEdgePage.tsx +++ b/web/src/pages/EditEdgePage/EditEdgePage.tsx @@ -15,7 +15,7 @@ import { useAppForm } from '../../shared/form'; import { formChangeLogic } from '../../shared/formLogic'; import { openModal } from '../../shared/hooks/modalControls/modalsSubjects'; import { ModalName } from '../../shared/hooks/modalControls/modalTypes'; -import { getEdgeQueryOptions } from '../../shared/query'; +import { getEdgeQueryOptions, getEdgesQueryOptions } from '../../shared/query'; export const EditEdgePage = () => { const { edgeId } = useParams({ @@ -41,22 +41,33 @@ export const EditEdgePage = () => { ); }; -const formSchema = z.object({ - name: z.string(m.form_error_required()).min(1, m.form_error_required()), - address: z.string().nullable(), - port: z.number().nullable(), - connected_at: z.string().nullable(), - disconnected_at: z.string().nullable(), - modified_at: z.string(), - modified_by: z.string(), - version: z.string().nullable(), - enabled: z.boolean(), -}); - -type FormFields = z.infer; - const EditEdgeForm = ({ edge }: { edge: Edge }) => { const navigate = useNavigate(); + const { data: edges } = useSuspenseQuery(getEdgesQueryOptions); + + const formSchema = useMemo( + () => + z.object({ + name: z + .string(m.form_error_required()) + .min(1, m.form_error_required()) + .refine( + (val) => val === edge.name || !edges?.some((e) => e.name === val), + m.edge_edit_error_name_duplicate(), + ), + address: z.string().nullable(), + port: z.number().nullable(), + connected_at: z.string().nullable(), + disconnected_at: z.string().nullable(), + modified_at: z.string(), + modified_by: z.string(), + version: z.string().nullable(), + enabled: z.boolean(), + }), + [edges, edge.name], + ); + + type FormFields = z.infer; const { mutateAsync: editEdge } = useMutation({ mutationFn: api.edge.editEdge, diff --git a/web/src/pages/SetupPage/initial/steps/SetupEdgeComponentStep.tsx b/web/src/pages/SetupPage/initial/steps/SetupEdgeComponentStep.tsx index fed43b5b8..59e6bd29b 100644 --- a/web/src/pages/SetupPage/initial/steps/SetupEdgeComponentStep.tsx +++ b/web/src/pages/SetupPage/initial/steps/SetupEdgeComponentStep.tsx @@ -1,3 +1,4 @@ +import { useQuery } from '@tanstack/react-query'; import { useMemo } from 'react'; import z from 'zod'; import { useShallow } from 'zustand/react/shallow'; @@ -9,6 +10,7 @@ import { SizedBox } from '../../../../shared/defguard-ui/components/SizedBox/Siz import { ThemeSpacing } from '../../../../shared/defguard-ui/types'; import { useAppForm } from '../../../../shared/form'; import { formChangeLogic } from '../../../../shared/formLogic'; +import { getEdgesQueryOptions } from '../../../../shared/query'; import { Validate } from '../../../../shared/validate'; import { SetupPageStep } from '../types'; import { useSetupWizardStore } from '../useSetupWizardStore'; @@ -23,6 +25,7 @@ type StoreValues = { export const SetupEdgeComponentStep = () => { const setActiveStep = useSetupWizardStore((s) => s.setActiveStep); + const { data: edges } = useQuery(getEdgesQueryOptions); const defaultValues = useSetupWizardStore( useShallow( @@ -43,7 +46,11 @@ export const SetupEdgeComponentStep = () => { z.object({ common_name: z .string() - .min(1, m.edge_setup_component_error_common_name_required()), + .min(1, m.edge_setup_component_error_common_name_required()) + .refine( + (val) => !edges?.some((e) => e.name === val), + m.edge_setup_component_error_common_name_duplicate(), + ), ip_or_domain: z .string() .min(1, m.edge_setup_component_error_ip_or_domain_required()) @@ -59,7 +66,7 @@ export const SetupEdgeComponentStep = () => { .min(1, m.edge_setup_component_error_grpc_port_required()) .max(65535, m.edge_setup_component_error_grpc_port_max()), }), - [], + [edges], ); const form = useAppForm({