From 82310df07a99fec2ff38f9b83ceca1c8afdedd19 Mon Sep 17 00:00:00 2001 From: Kuba <78603704+jakub-tldr@users.noreply.github.com> Date: Mon, 16 Mar 2026 12:39:43 +0100 Subject: [PATCH 1/6] show modal on no available locations --- web/messages/en/modal.json | 7 +- .../NetworkDevicesPage/NetworkDevicesPage.tsx | 2 + .../NetworkDevicesTable.tsx | 5 +- .../NoAvailableLocationsModal.tsx | 70 +++++++++++++++++++ .../shared/hooks/modalControls/modalTypes.ts | 2 + 5 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 web/src/pages/NetworkDevicesPage/modals/NoAvailableLocationsModal/NoAvailableLocationsModal.tsx diff --git a/web/messages/en/modal.json b/web/messages/en/modal.json index b2592755da..98f8c63123 100644 --- a/web/messages/en/modal.json +++ b/web/messages/en/modal.json @@ -159,5 +159,10 @@ "modal_assign_user_device_ip_card_title": "{deviceName} IP settings", "modal_assign_user_device_ip_assignment_description": "You can change the IP address for this device separately in each location/network one-by-one.", "modal_assign_user_device_ip_success": "{deviceName}'s IP addresses were successfully updated.", - "modal_assign_user_device_ip_error": "Failed to update IP addresses" + "modal_assign_user_device_ip_error": "Failed to update IP addresses", + "modal_no_available_locations_title": "Cannot add network device", + "modal_no_available_locations_subtitle": "No available locations", + "modal_no_available_locations_body": "Network devices can only be added to locations that have MFA disabled. You don't have any locations that meet this requirement.", + "modal_no_available_locations_hint": "Use the button below to go to the Locations page and create a location with MFA disabled.", + "modal_no_available_locations_go_to_locations": "Go to locations" } diff --git a/web/src/pages/NetworkDevicesPage/NetworkDevicesPage.tsx b/web/src/pages/NetworkDevicesPage/NetworkDevicesPage.tsx index a5dae90760..7f9407f6f1 100644 --- a/web/src/pages/NetworkDevicesPage/NetworkDevicesPage.tsx +++ b/web/src/pages/NetworkDevicesPage/NetworkDevicesPage.tsx @@ -7,6 +7,7 @@ import { DeleteNetworkDeviceModal } from './modals/DeleteNetworkDeviceModal/Dele import { EditNetworkDeviceModal } from './modals/EditNetworkDeviceModal/EditNetworkDeviceModal'; import { NetworkDeviceConfigModal } from './modals/NetworkDeviceConfigModal/NetworkDeviceConfigModal'; import { NetworkDeviceTokenModal } from './modals/NetworkDeviceTokenModal/NetworkDeviceTokenModal'; +import { NoAvailableLocationsModal } from './modals/NoAvailableLocationsModal/NoAvailableLocationsModal'; import { NetworkDevicesTable } from './NetworkDevicesTable'; export const NetworkDevicesPage = () => { @@ -24,6 +25,7 @@ export const NetworkDevicesPage = () => { + > ); }; diff --git a/web/src/pages/NetworkDevicesPage/NetworkDevicesTable.tsx b/web/src/pages/NetworkDevicesPage/NetworkDevicesTable.tsx index 29df046ae1..743e164ac4 100644 --- a/web/src/pages/NetworkDevicesPage/NetworkDevicesTable.tsx +++ b/web/src/pages/NetworkDevicesPage/NetworkDevicesTable.tsx @@ -60,7 +60,10 @@ export const NetworkDevicesTable = ({ networkDevices }: Props) => { ['name'], ['asc'], ); - if (!availableLocations.length) return; + if (!availableLocations.length) { + openModal(ModalName.NoAvailableLocations); + return; + } const { data: availableIps } = await api.network_device.getAvailableIp( availableLocations[0].id, ); diff --git a/web/src/pages/NetworkDevicesPage/modals/NoAvailableLocationsModal/NoAvailableLocationsModal.tsx b/web/src/pages/NetworkDevicesPage/modals/NoAvailableLocationsModal/NoAvailableLocationsModal.tsx new file mode 100644 index 0000000000..5f89f0bb42 --- /dev/null +++ b/web/src/pages/NetworkDevicesPage/modals/NoAvailableLocationsModal/NoAvailableLocationsModal.tsx @@ -0,0 +1,70 @@ +import { useEffect, useState } from 'react'; +import { useNavigate } from '@tanstack/react-router'; +import { m } from '../../../../paraglide/messages'; +import { AppText } from '../../../../shared/defguard-ui/components/AppText/AppText'; +import { Divider } from '../../../../shared/defguard-ui/components/Divider/Divider'; +import { SizedBox } from '../../../../shared/defguard-ui/components/SizedBox/SizedBox'; +import { TextStyle, ThemeSpacing, ThemeVariable } from '../../../../shared/defguard-ui/types'; +import { + subscribeCloseModal, + subscribeOpenModal, +} from '../../../../shared/hooks/modalControls/modalsSubjects'; +import { ModalName } from '../../../../shared/hooks/modalControls/modalTypes'; +import { LicenseModal } from '../../../../shared/components/modals/LicenseModal/LicenseModal'; +import { Controls } from '../../../../shared/components/Controls/Controls'; +import { Button } from '../../../../shared/defguard-ui/components/Button/Button'; + +const modalNameValue = ModalName.NoAvailableLocations; + +export const NoAvailableLocationsModal = () => { + const [isOpen, setOpen] = useState(false); + const navigate = useNavigate(); + + useEffect(() => { + const openSub = subscribeOpenModal(modalNameValue, () => { + setOpen(true); + }); + const closeSub = subscribeCloseModal(modalNameValue, () => setOpen(false)); + return () => { + openSub.unsubscribe(); + closeSub.unsubscribe(); + }; + }, []); + + return ( + setOpen(false)} + afterClose={() => {}} + > + + {m.modal_no_available_locations_title()} + + + + {m.modal_no_available_locations_body()} + + + + {m.modal_no_available_locations_hint()} + + + + setOpen(false)} + /> + { + setOpen(false); + navigate({ to: '/locations' }); + }} + /> + + + + ); +}; diff --git a/web/src/shared/hooks/modalControls/modalTypes.ts b/web/src/shared/hooks/modalControls/modalTypes.ts index b52673c342..e6e88451b7 100644 --- a/web/src/shared/hooks/modalControls/modalTypes.ts +++ b/web/src/shared/hooks/modalControls/modalTypes.ts @@ -76,6 +76,7 @@ export const ModalName = { AssignUserIP: 'assignUserIP', AssignUserDeviceIP: 'assignUserDeviceIP', ConfirmAction: 'confirmAction', + NoAvailableLocations: 'noAvailableLocations', } as const; export type ModalNameValue = (typeof ModalName)[keyof typeof ModalName]; @@ -245,6 +246,7 @@ const modalOpenArgsSchema = z.discriminatedUnion('name', [ name: z.literal(ModalName.ConfirmAction), data: z.custom(), }), + z.object({ name: z.literal(ModalName.NoAvailableLocations) }), ]); export type ModalOpenEvent = z.infer; From 34ef7493e6a0b9bcea2f7e6adb33cfde6ed13f4b Mon Sep 17 00:00:00 2001 From: Kuba <78603704+jakub-tldr@users.noreply.github.com> Date: Mon, 16 Mar 2026 16:45:01 +0100 Subject: [PATCH 2/6] make modal smaller --- web/messages/en/modal.json | 2 +- .../NoAvailableLocationsModal.tsx | 30 +++++++------------ 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/web/messages/en/modal.json b/web/messages/en/modal.json index 98f8c63123..e5c47e8a14 100644 --- a/web/messages/en/modal.json +++ b/web/messages/en/modal.json @@ -162,7 +162,7 @@ "modal_assign_user_device_ip_error": "Failed to update IP addresses", "modal_no_available_locations_title": "Cannot add network device", "modal_no_available_locations_subtitle": "No available locations", - "modal_no_available_locations_body": "Network devices can only be added to locations that have MFA disabled. You don't have any locations that meet this requirement.", + "modal_no_available_locations_body": "Network devices can only be added to locations that have MFA disabled. You don't have any locations that meet this requirement. You can create a new location in the Locations section.", "modal_no_available_locations_hint": "Use the button below to go to the Locations page and create a location with MFA disabled.", "modal_no_available_locations_go_to_locations": "Go to locations" } diff --git a/web/src/pages/NetworkDevicesPage/modals/NoAvailableLocationsModal/NoAvailableLocationsModal.tsx b/web/src/pages/NetworkDevicesPage/modals/NoAvailableLocationsModal/NoAvailableLocationsModal.tsx index 5f89f0bb42..3c577c4383 100644 --- a/web/src/pages/NetworkDevicesPage/modals/NoAvailableLocationsModal/NoAvailableLocationsModal.tsx +++ b/web/src/pages/NetworkDevicesPage/modals/NoAvailableLocationsModal/NoAvailableLocationsModal.tsx @@ -1,18 +1,16 @@ -import { useEffect, useState } from 'react'; import { useNavigate } from '@tanstack/react-router'; +import { useEffect, useState } from 'react'; import { m } from '../../../../paraglide/messages'; +import { Controls } from '../../../../shared/components/Controls/Controls'; import { AppText } from '../../../../shared/defguard-ui/components/AppText/AppText'; -import { Divider } from '../../../../shared/defguard-ui/components/Divider/Divider'; -import { SizedBox } from '../../../../shared/defguard-ui/components/SizedBox/SizedBox'; -import { TextStyle, ThemeSpacing, ThemeVariable } from '../../../../shared/defguard-ui/types'; +import { Button } from '../../../../shared/defguard-ui/components/Button/Button'; +import { Modal } from '../../../../shared/defguard-ui/components/Modal/Modal'; +import { TextStyle } from '../../../../shared/defguard-ui/types'; import { subscribeCloseModal, subscribeOpenModal, } from '../../../../shared/hooks/modalControls/modalsSubjects'; import { ModalName } from '../../../../shared/hooks/modalControls/modalTypes'; -import { LicenseModal } from '../../../../shared/components/modals/LicenseModal/LicenseModal'; -import { Controls } from '../../../../shared/components/Controls/Controls'; -import { Button } from '../../../../shared/defguard-ui/components/Button/Button'; const modalNameValue = ModalName.NoAvailableLocations; @@ -32,27 +30,21 @@ export const NoAvailableLocationsModal = () => { }, []); return ( - setOpen(false)} afterClose={() => {}} > - - {m.modal_no_available_locations_title()} - - - + {m.modal_no_available_locations_body()} - - - {m.modal_no_available_locations_hint()} - setOpen(false)} /> @@ -65,6 +57,6 @@ export const NoAvailableLocationsModal = () => { /> - + ); }; From c2f052cb837a70d62f572ebacaba271eb7ebf50e Mon Sep 17 00:00:00 2001 From: Kuba <78603704+jakub-tldr@users.noreply.github.com> Date: Mon, 16 Mar 2026 16:53:47 +0100 Subject: [PATCH 3/6] use generic modal --- .../pages/NetworkDevicesPage/NetworkDevicesPage.tsx | 2 -- .../pages/NetworkDevicesPage/NetworkDevicesTable.tsx | 10 +++++++++- web/src/shared/hooks/modalControls/modalTypes.ts | 2 -- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/web/src/pages/NetworkDevicesPage/NetworkDevicesPage.tsx b/web/src/pages/NetworkDevicesPage/NetworkDevicesPage.tsx index 7f9407f6f1..a5dae90760 100644 --- a/web/src/pages/NetworkDevicesPage/NetworkDevicesPage.tsx +++ b/web/src/pages/NetworkDevicesPage/NetworkDevicesPage.tsx @@ -7,7 +7,6 @@ import { DeleteNetworkDeviceModal } from './modals/DeleteNetworkDeviceModal/Dele import { EditNetworkDeviceModal } from './modals/EditNetworkDeviceModal/EditNetworkDeviceModal'; import { NetworkDeviceConfigModal } from './modals/NetworkDeviceConfigModal/NetworkDeviceConfigModal'; import { NetworkDeviceTokenModal } from './modals/NetworkDeviceTokenModal/NetworkDeviceTokenModal'; -import { NoAvailableLocationsModal } from './modals/NoAvailableLocationsModal/NoAvailableLocationsModal'; import { NetworkDevicesTable } from './NetworkDevicesTable'; export const NetworkDevicesPage = () => { @@ -25,7 +24,6 @@ export const NetworkDevicesPage = () => { - > ); }; diff --git a/web/src/pages/NetworkDevicesPage/NetworkDevicesTable.tsx b/web/src/pages/NetworkDevicesPage/NetworkDevicesTable.tsx index 743e164ac4..eb4a3ed2a9 100644 --- a/web/src/pages/NetworkDevicesPage/NetworkDevicesTable.tsx +++ b/web/src/pages/NetworkDevicesPage/NetworkDevicesTable.tsx @@ -1,4 +1,5 @@ import { useMutation } from '@tanstack/react-query'; +import { useNavigate } from '@tanstack/react-router'; import { createColumnHelper, getCoreRowModel, @@ -45,6 +46,7 @@ type RowData = NetworkDevice; const columnHelper = createColumnHelper(); export const NetworkDevicesTable = ({ networkDevices }: Props) => { + const navigate = useNavigate(); const reservedNames = useMemo( () => networkDevices.map((n) => n.name), [networkDevices], @@ -61,7 +63,13 @@ export const NetworkDevicesTable = ({ networkDevices }: Props) => { ['asc'], ); if (!availableLocations.length) { - openModal(ModalName.NoAvailableLocations); + openModal(ModalName.ConfirmAction, { + title: m.modal_no_available_locations_title(), + contentMd: m.modal_no_available_locations_body(), + actionPromise: async () => {}, + onSuccess: () => navigate({ to: '/locations' }), + submitProps: { text: m.modal_no_available_locations_go_to_locations() }, + }); return; } const { data: availableIps } = await api.network_device.getAvailableIp( diff --git a/web/src/shared/hooks/modalControls/modalTypes.ts b/web/src/shared/hooks/modalControls/modalTypes.ts index e6e88451b7..b52673c342 100644 --- a/web/src/shared/hooks/modalControls/modalTypes.ts +++ b/web/src/shared/hooks/modalControls/modalTypes.ts @@ -76,7 +76,6 @@ export const ModalName = { AssignUserIP: 'assignUserIP', AssignUserDeviceIP: 'assignUserDeviceIP', ConfirmAction: 'confirmAction', - NoAvailableLocations: 'noAvailableLocations', } as const; export type ModalNameValue = (typeof ModalName)[keyof typeof ModalName]; @@ -246,7 +245,6 @@ const modalOpenArgsSchema = z.discriminatedUnion('name', [ name: z.literal(ModalName.ConfirmAction), data: z.custom(), }), - z.object({ name: z.literal(ModalName.NoAvailableLocations) }), ]); export type ModalOpenEvent = z.infer; From 80f91d622cfe35ae60a3bf3159978b4aac28b868 Mon Sep 17 00:00:00 2001 From: Kuba <78603704+jakub-tldr@users.noreply.github.com> Date: Mon, 16 Mar 2026 16:54:27 +0100 Subject: [PATCH 4/6] delete file --- .../NoAvailableLocationsModal.tsx | 62 ------------------- 1 file changed, 62 deletions(-) delete mode 100644 web/src/pages/NetworkDevicesPage/modals/NoAvailableLocationsModal/NoAvailableLocationsModal.tsx diff --git a/web/src/pages/NetworkDevicesPage/modals/NoAvailableLocationsModal/NoAvailableLocationsModal.tsx b/web/src/pages/NetworkDevicesPage/modals/NoAvailableLocationsModal/NoAvailableLocationsModal.tsx deleted file mode 100644 index 3c577c4383..0000000000 --- a/web/src/pages/NetworkDevicesPage/modals/NoAvailableLocationsModal/NoAvailableLocationsModal.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { useNavigate } from '@tanstack/react-router'; -import { useEffect, useState } from 'react'; -import { m } from '../../../../paraglide/messages'; -import { Controls } from '../../../../shared/components/Controls/Controls'; -import { AppText } from '../../../../shared/defguard-ui/components/AppText/AppText'; -import { Button } from '../../../../shared/defguard-ui/components/Button/Button'; -import { Modal } from '../../../../shared/defguard-ui/components/Modal/Modal'; -import { TextStyle } from '../../../../shared/defguard-ui/types'; -import { - subscribeCloseModal, - subscribeOpenModal, -} from '../../../../shared/hooks/modalControls/modalsSubjects'; -import { ModalName } from '../../../../shared/hooks/modalControls/modalTypes'; - -const modalNameValue = ModalName.NoAvailableLocations; - -export const NoAvailableLocationsModal = () => { - const [isOpen, setOpen] = useState(false); - const navigate = useNavigate(); - - useEffect(() => { - const openSub = subscribeOpenModal(modalNameValue, () => { - setOpen(true); - }); - const closeSub = subscribeCloseModal(modalNameValue, () => setOpen(false)); - return () => { - openSub.unsubscribe(); - closeSub.unsubscribe(); - }; - }, []); - - return ( - setOpen(false)} - afterClose={() => {}} - > - - {m.modal_no_available_locations_body()} - - - - setOpen(false)} - /> - { - setOpen(false); - navigate({ to: '/locations' }); - }} - /> - - - - ); -}; From 9afd3863206ce5e94685e750a28f74d5152bee12 Mon Sep 17 00:00:00 2001 From: Kuba <78603704+jakub-tldr@users.noreply.github.com> Date: Mon, 16 Mar 2026 16:56:35 +0100 Subject: [PATCH 5/6] remove unused translations --- web/messages/en/modal.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/web/messages/en/modal.json b/web/messages/en/modal.json index e5c47e8a14..0e0ed3b7af 100644 --- a/web/messages/en/modal.json +++ b/web/messages/en/modal.json @@ -161,8 +161,6 @@ "modal_assign_user_device_ip_success": "{deviceName}'s IP addresses were successfully updated.", "modal_assign_user_device_ip_error": "Failed to update IP addresses", "modal_no_available_locations_title": "Cannot add network device", - "modal_no_available_locations_subtitle": "No available locations", "modal_no_available_locations_body": "Network devices can only be added to locations that have MFA disabled. You don't have any locations that meet this requirement. You can create a new location in the Locations section.", - "modal_no_available_locations_hint": "Use the button below to go to the Locations page and create a location with MFA disabled.", "modal_no_available_locations_go_to_locations": "Go to locations" } From ec157f3dce97bea7e630cbc35f5994cf08dfe10e Mon Sep 17 00:00:00 2001 From: Kuba <78603704+jakub-tldr@users.noreply.github.com> Date: Mon, 16 Mar 2026 16:58:05 +0100 Subject: [PATCH 6/6] fix action --- web/src/pages/NetworkDevicesPage/NetworkDevicesTable.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/web/src/pages/NetworkDevicesPage/NetworkDevicesTable.tsx b/web/src/pages/NetworkDevicesPage/NetworkDevicesTable.tsx index eb4a3ed2a9..1b6064a4e7 100644 --- a/web/src/pages/NetworkDevicesPage/NetworkDevicesTable.tsx +++ b/web/src/pages/NetworkDevicesPage/NetworkDevicesTable.tsx @@ -66,8 +66,7 @@ export const NetworkDevicesTable = ({ networkDevices }: Props) => { openModal(ModalName.ConfirmAction, { title: m.modal_no_available_locations_title(), contentMd: m.modal_no_available_locations_body(), - actionPromise: async () => {}, - onSuccess: () => navigate({ to: '/locations' }), + actionPromise: async () => navigate({ to: '/locations' }), submitProps: { text: m.modal_no_available_locations_go_to_locations() }, }); return;