From 95bd2b7940f0c5b2368b6d50aed2f9d294e534d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20=C5=9Al=C4=99zak?= <102536422+filipslezaklab@users.noreply.github.com> Date: Fri, 3 Jul 2026 14:02:15 +0200 Subject: [PATCH 1/6] ui fixes batch 3 (#1007) --- .../AddPage/components/AddCard/style.scss | 10 +++--- .../ConnectModalMfaSettings.tsx | 2 +- .../components/Icon/icons/IconReport.tsx | 12 +++---- .../LocationCardMfaEmailView.tsx | 32 ++++++++++-------- .../LocationCardMfaTotpView.tsx | 32 ++++++++++-------- .../OverviewLocationCard/style.scss | 19 +++++++++-- .../components/ConnectionWatcher/style.scss | 11 +++++- .../shared/components/WindowHeader/style.scss | 6 +++- src-tauri/icons/128x128.png | Bin 16016 -> 0 bytes src-tauri/icons/128x128@2x.png | Bin 52424 -> 0 bytes src-tauri/icons/32x32.png | Bin 2080 -> 0 bytes src-tauri/icons/64x64.png | Bin 5600 -> 0 bytes src-tauri/icons/Square107x107Logo.png | Bin 12143 -> 0 bytes src-tauri/icons/Square142x142Logo.png | Bin 18909 -> 0 bytes src-tauri/icons/Square150x150Logo.png | Bin 20609 -> 0 bytes src-tauri/icons/Square284x284Logo.png | Bin 63733 -> 0 bytes src-tauri/icons/Square30x30Logo.png | Bin 1912 -> 0 bytes src-tauri/icons/Square310x310Logo.png | Bin 75652 -> 0 bytes src-tauri/icons/Square44x44Logo.png | Bin 3253 -> 0 bytes src-tauri/icons/Square71x71Logo.png | Bin 6650 -> 0 bytes src-tauri/icons/Square89x89Logo.png | Bin 9258 -> 0 bytes src-tauri/icons/StoreLogo.png | Bin 3861 -> 0 bytes src-tauri/icons/icon.png | Bin 182335 -> 0 bytes src-tauri/icons/macos/128x128.png | Bin 0 -> 13319 bytes src-tauri/icons/macos/128x128@2x.png | Bin 0 -> 41131 bytes src-tauri/icons/macos/32x32.png | Bin 0 -> 1884 bytes src-tauri/icons/macos/64x64.png | Bin 0 -> 4833 bytes src-tauri/icons/macos/Square107x107Logo.png | Bin 0 -> 10262 bytes src-tauri/icons/macos/Square142x142Logo.png | Bin 0 -> 15736 bytes src-tauri/icons/macos/Square150x150Logo.png | Bin 0 -> 16962 bytes src-tauri/icons/macos/Square284x284Logo.png | Bin 0 -> 50052 bytes src-tauri/icons/macos/Square30x30Logo.png | Bin 0 -> 1758 bytes src-tauri/icons/macos/Square310x310Logo.png | Bin 0 -> 58582 bytes src-tauri/icons/macos/Square44x44Logo.png | Bin 0 -> 2898 bytes src-tauri/icons/macos/Square71x71Logo.png | Bin 0 -> 5760 bytes src-tauri/icons/macos/Square89x89Logo.png | Bin 0 -> 7885 bytes src-tauri/icons/macos/StoreLogo.png | Bin 0 -> 3536 bytes src-tauri/icons/macos/icon.icns | Bin 0 -> 786625 bytes src-tauri/icons/macos/icon.ico | Bin 0 -> 54590 bytes src-tauri/icons/macos/icon.png | Bin 0 -> 150280 bytes src-tauri/icons/windows/128x128.png | Bin 0 -> 17233 bytes src-tauri/icons/windows/128x128@2x.png | Bin 0 -> 56151 bytes src-tauri/icons/windows/32x32.png | Bin 0 -> 2213 bytes src-tauri/icons/windows/64x64.png | Bin 0 -> 6088 bytes src-tauri/icons/windows/Square107x107Logo.png | Bin 0 -> 13127 bytes src-tauri/icons/windows/Square142x142Logo.png | Bin 0 -> 20302 bytes src-tauri/icons/windows/Square150x150Logo.png | Bin 0 -> 22014 bytes src-tauri/icons/windows/Square284x284Logo.png | Bin 0 -> 68129 bytes src-tauri/icons/windows/Square30x30Logo.png | Bin 0 -> 2035 bytes src-tauri/icons/windows/Square310x310Logo.png | Bin 0 -> 81002 bytes src-tauri/icons/windows/Square44x44Logo.png | Bin 0 -> 3483 bytes src-tauri/icons/windows/Square71x71Logo.png | Bin 0 -> 7204 bytes src-tauri/icons/windows/Square89x89Logo.png | Bin 0 -> 10019 bytes src-tauri/icons/windows/StoreLogo.png | Bin 0 -> 4206 bytes src-tauri/icons/{ => windows}/icon.icns | Bin 1117556 -> 1117556 bytes src-tauri/icons/{ => windows}/icon.ico | Bin src-tauri/icons/windows/icon.png | Bin 0 -> 210774 bytes src-tauri/src/window_manager/mod.rs | 2 +- src-tauri/tauri.conf.json | 30 ++++++---------- src-tauri/tauri.dmg.conf.json | 15 ++++---- src-tauri/tauri.macos.conf.json | 7 ++++ 61 files changed, 106 insertions(+), 72 deletions(-) delete mode 100644 src-tauri/icons/128x128.png delete mode 100644 src-tauri/icons/128x128@2x.png delete mode 100644 src-tauri/icons/32x32.png delete mode 100644 src-tauri/icons/64x64.png delete mode 100644 src-tauri/icons/Square107x107Logo.png delete mode 100644 src-tauri/icons/Square142x142Logo.png delete mode 100644 src-tauri/icons/Square150x150Logo.png delete mode 100644 src-tauri/icons/Square284x284Logo.png delete mode 100644 src-tauri/icons/Square30x30Logo.png delete mode 100644 src-tauri/icons/Square310x310Logo.png delete mode 100644 src-tauri/icons/Square44x44Logo.png delete mode 100644 src-tauri/icons/Square71x71Logo.png delete mode 100644 src-tauri/icons/Square89x89Logo.png delete mode 100644 src-tauri/icons/StoreLogo.png delete mode 100644 src-tauri/icons/icon.png create mode 100644 src-tauri/icons/macos/128x128.png create mode 100644 src-tauri/icons/macos/128x128@2x.png create mode 100644 src-tauri/icons/macos/32x32.png create mode 100644 src-tauri/icons/macos/64x64.png create mode 100644 src-tauri/icons/macos/Square107x107Logo.png create mode 100644 src-tauri/icons/macos/Square142x142Logo.png create mode 100644 src-tauri/icons/macos/Square150x150Logo.png create mode 100644 src-tauri/icons/macos/Square284x284Logo.png create mode 100644 src-tauri/icons/macos/Square30x30Logo.png create mode 100644 src-tauri/icons/macos/Square310x310Logo.png create mode 100644 src-tauri/icons/macos/Square44x44Logo.png create mode 100644 src-tauri/icons/macos/Square71x71Logo.png create mode 100644 src-tauri/icons/macos/Square89x89Logo.png create mode 100644 src-tauri/icons/macos/StoreLogo.png create mode 100644 src-tauri/icons/macos/icon.icns create mode 100644 src-tauri/icons/macos/icon.ico create mode 100644 src-tauri/icons/macos/icon.png create mode 100644 src-tauri/icons/windows/128x128.png create mode 100644 src-tauri/icons/windows/128x128@2x.png create mode 100644 src-tauri/icons/windows/32x32.png create mode 100644 src-tauri/icons/windows/64x64.png create mode 100644 src-tauri/icons/windows/Square107x107Logo.png create mode 100644 src-tauri/icons/windows/Square142x142Logo.png create mode 100644 src-tauri/icons/windows/Square150x150Logo.png create mode 100644 src-tauri/icons/windows/Square284x284Logo.png create mode 100644 src-tauri/icons/windows/Square30x30Logo.png create mode 100644 src-tauri/icons/windows/Square310x310Logo.png create mode 100644 src-tauri/icons/windows/Square44x44Logo.png create mode 100644 src-tauri/icons/windows/Square71x71Logo.png create mode 100644 src-tauri/icons/windows/Square89x89Logo.png create mode 100644 src-tauri/icons/windows/StoreLogo.png rename src-tauri/icons/{ => windows}/icon.icns (99%) rename src-tauri/icons/{ => windows}/icon.ico (100%) create mode 100644 src-tauri/icons/windows/icon.png diff --git a/new-ui/src/pages/full/AddPage/components/AddCard/style.scss b/new-ui/src/pages/full/AddPage/components/AddCard/style.scss index d41fc982..e8d89154 100644 --- a/new-ui/src/pages/full/AddPage/components/AddCard/style.scss +++ b/new-ui/src/pages/full/AddPage/components/AddCard/style.scss @@ -1,17 +1,16 @@ .add-card { --border: var(--border-action-disabled); --shadow: box-shadow: 0 4px 12px 0 rgb(13 47 157 / 0%); - --color: var(--fg-white-80); + --color: var(--fg-white-60); box-sizing: border-box; border-radius: 12px; border: 1px solid var(--border); box-shadow: var(--shadow); - color: var(--color); overflow: hidden; transition-duration: 160ms; transition-timing-function: ease-out; - transition-property: border-color, box-shadow, color; + transition-property: border-color, box-shadow; max-width: 342px; min-height: 172px; @@ -48,9 +47,12 @@ } .description { - color: inherit; + color: var(--color); font: var(--t-body-xs-400); padding-bottom: var(--spacing-xl); + transition-duration: 160ms; + transition-timing-function: ease-out; + transition-property: color; } > .track { diff --git a/new-ui/src/pages/full/OverviewPage/components/ConnectModal/views/ConnectModalMfaSettings/ConnectModalMfaSettings.tsx b/new-ui/src/pages/full/OverviewPage/components/ConnectModal/views/ConnectModalMfaSettings/ConnectModalMfaSettings.tsx index 6506d08f..35e111b5 100644 --- a/new-ui/src/pages/full/OverviewPage/components/ConnectModal/views/ConnectModalMfaSettings/ConnectModalMfaSettings.tsx +++ b/new-ui/src/pages/full/OverviewPage/components/ConnectModal/views/ConnectModalMfaSettings/ConnectModalMfaSettings.tsx @@ -41,7 +41,7 @@ export const ConnectModalMfaSettings = () => { const [selectedMethod, setSelectedMethod] = useState(currentMethod); - const [setAsDefault, setSetAsDefault] = useState(true); + const [setAsDefault, setSetAsDefault] = useState(perviousView === null); const MfaFactorsList = useMemo((): MfaMethodValue[] => { if (location?.location_mfa_mode === LocationMfaMode.Internal) { diff --git a/new-ui/src/shared/components/Icon/icons/IconReport.tsx b/new-ui/src/shared/components/Icon/icons/IconReport.tsx index 14d31604..8bbb5bb8 100644 --- a/new-ui/src/shared/components/Icon/icons/IconReport.tsx +++ b/new-ui/src/shared/components/Icon/icons/IconReport.tsx @@ -4,17 +4,15 @@ export const IconReport = (props: SVGProps) => { return ( ); diff --git a/new-ui/src/shared/components/LocationCard/views/LocationCardMfaEmailView/LocationCardMfaEmailView.tsx b/new-ui/src/shared/components/LocationCard/views/LocationCardMfaEmailView/LocationCardMfaEmailView.tsx index 5066c7c5..e03ecb8c 100644 --- a/new-ui/src/shared/components/LocationCard/views/LocationCardMfaEmailView/LocationCardMfaEmailView.tsx +++ b/new-ui/src/shared/components/LocationCard/views/LocationCardMfaEmailView/LocationCardMfaEmailView.tsx @@ -38,17 +38,22 @@ export const LocationCardMfaEmailView = () => { const [emailCode, setEmailCode] = useState(null); const [error, setError] = useState(null); - const handleVerify = useCallback(() => { - if (!isPresent(emailCode)) { - setError('Enter code'); - return; - } - if (emailCode.length !== 6) { - setError('6 digits are required'); - return; - } - verifyCode(emailCode); - }, [emailCode, verifyCode]); + const handleVerify = useCallback( + (argCode?: string | null) => { + const toCheck = argCode ?? emailCode; + + if (!isPresent(toCheck)) { + setError('Enter code'); + return; + } + if (toCheck.length !== 6) { + setError('6 digits are required'); + return; + } + verifyCode(toCheck); + }, + [emailCode, verifyCode], + ); // biome-ignore lint/correctness/useExhaustiveDependencies: side effect of code input useEffect(() => { @@ -83,8 +88,7 @@ export const LocationCardMfaEmailView = () => { onChange={setEmailCode} error={startError ?? error} onSuccessPaste={(value) => { - setEmailCode(value); - handleVerify(); + handleVerify(value); }} /> @@ -107,7 +111,7 @@ export const LocationCardMfaEmailView = () => { + {isOpen && ( + + { + setOpen(false); + }} + {...getFloatingProps()} + /> + + )} + + ); +}; diff --git a/new-ui/src/pages/full/OverviewPage/components/OverviewActionsButton/style.scss b/new-ui/src/pages/full/OverviewPage/components/OverviewActionsButton/style.scss new file mode 100644 index 00000000..ef83a801 --- /dev/null +++ b/new-ui/src/pages/full/OverviewPage/components/OverviewActionsButton/style.scss @@ -0,0 +1,15 @@ +.overview-header-actions { + border: 0; + background: transparent; + display: flex; + flex-flow: row nowrap; + align-items: center; + column-gap: var(--spacing-xs); + cursor: pointer; + user-select: none; + + p { + font: var(--t-body-xs-500); + color: var(--fg-white-80); + } +} diff --git a/new-ui/src/pages/full/OverviewPage/components/UpdateInstanceModal/UpdateInstanceModal.tsx b/new-ui/src/pages/full/OverviewPage/components/UpdateInstanceModal/UpdateInstanceModal.tsx new file mode 100644 index 00000000..5808b46e --- /dev/null +++ b/new-ui/src/pages/full/OverviewPage/components/UpdateInstanceModal/UpdateInstanceModal.tsx @@ -0,0 +1,145 @@ +import { useEffect, useMemo, useState } from 'react'; +import z from 'zod'; +import { Button } from '../../../../../shared/components/Button/Button'; +import { ButtonVariant } from '../../../../../shared/components/Button/types'; +import { Controls } from '../../../../../shared/components/Controls/Controls'; +import { Modal } from '../../../../../shared/components/Modal/Modal'; +import { SizedBox } from '../../../../../shared/components/SizedBox/SizedBox'; +import { edgeApi } from '../../../../../shared/edge-api/api'; +import { useAppForm } from '../../../../../shared/form'; +import { formChangeLogic } from '../../../../../shared/formLogic'; +import { + closeModal, + subscribeCloseModal, + subscribeOpenModal, +} from '../../../../../shared/hooks/modalControls/modalsSubjects'; +import { ModalName } from '../../../../../shared/hooks/modalControls/modalTypes'; +import type { OpenUpdateInstanceModalData } from '../../../../../shared/hooks/modalControls/types'; +import { Snackbar } from '../../../../../shared/providers/snackbar/snackbar'; +import { ThemeSpacing } from '../../../../../shared/types'; +import { isPresent } from '../../../../../shared/utils/isPresent'; + +const modalNameKey = ModalName.UpdateInstance; + +export const UpdateInstanceModal = () => { + const [isOpen, setOpen] = useState(false); + const [modalData, setModalData] = useState(null); + + useEffect(() => { + const openSub = subscribeOpenModal(modalNameKey, (data) => { + setModalData(data); + setOpen(true); + }); + const closeSub = subscribeCloseModal(modalNameKey, () => setOpen(false)); + return () => { + openSub.unsubscribe(); + closeSub.unsubscribe(); + }; + }, []); + return ( + { + setOpen(false); + }} + > + {isOpen && isPresent(modalData) && } + + ); +}; + +const formSchema = z.object({ + url: z.string().min(1, 'Field is required'), + token: z.string('Field is required').min(1, 'Field is required.'), +}); + +type FormFields = z.infer; + +const ModalContent = ({ data }: { data: OpenUpdateInstanceModalData }) => { + const defaultValues = useMemo( + (): FormFields => ({ + url: data.url, + token: '', + }), + [data.url], + ); + + const form = useAppForm({ + defaultValues, + validationLogic: formChangeLogic, + validators: { + onSubmit: formSchema, + onChange: formSchema, + }, + onSubmit: async ({ value, formApi }) => { + const result = await edgeApi.updateInstance({ + instanceId: data.instanceId, + url: value.url, + token: value.token, + }); + if (result.error) { + if (result.isCredentialsError) { + formApi.setErrorMap({ + onSubmit: { + fields: { + token: 'Invalid Token or URL', + url: 'Invalid Token or URL', + }, + }, + }); + } else { + Snackbar.error(result.error); + } + return; + } + Snackbar.default('Instance updated.'); + closeModal(modalNameKey); + }, + }); + + return ( +
{ + e.stopPropagation(); + e.preventDefault(); + form.handleSubmit(); + }} + > + + + {(field) => } + + + + {(field) => } + + +
+
+
+
+
+ ); +}; diff --git a/new-ui/src/pages/full/OverviewPage/components/UpdateTunnelModal/UpdateTunnelModal.tsx b/new-ui/src/pages/full/OverviewPage/components/UpdateTunnelModal/UpdateTunnelModal.tsx new file mode 100644 index 00000000..91d89873 --- /dev/null +++ b/new-ui/src/pages/full/OverviewPage/components/UpdateTunnelModal/UpdateTunnelModal.tsx @@ -0,0 +1,253 @@ +import { useMutation } from '@tanstack/react-query'; +import { useEffect, useMemo, useState } from 'react'; +import z from 'zod'; +import { Button } from '../../../../../shared/components/Button/Button'; +import { ButtonVariant } from '../../../../../shared/components/Button/types'; +import { Controls } from '../../../../../shared/components/Controls/Controls'; +import { Modal } from '../../../../../shared/components/Modal/Modal'; +import { SizedBox } from '../../../../../shared/components/SizedBox/SizedBox'; +import { Split } from '../../../../../shared/components/Split/Split'; +import { useAppForm } from '../../../../../shared/form'; +import { formChangeLogic } from '../../../../../shared/formLogic'; +import { + closeModal, + subscribeCloseModal, + subscribeOpenModal, +} from '../../../../../shared/hooks/modalControls/modalsSubjects'; +import { ModalName } from '../../../../../shared/hooks/modalControls/modalTypes'; +import type { OpenUpdateTunnelModalData } from '../../../../../shared/hooks/modalControls/types'; +import { Snackbar } from '../../../../../shared/providers/snackbar/snackbar'; +import { api } from '../../../../../shared/rust-api/api'; +import { ThemeSpacing } from '../../../../../shared/types'; +import { isPresent } from '../../../../../shared/utils/isPresent'; +import { + cidrRegex, + patternValidEndpoint, + patternValidIpV6WithMask, + patternValidIpWithMask, + patternValidWireguardKey, +} from '../../../../../shared/utils/patterns'; + +const modalNameKey = ModalName.UpdateTunnel; + +export const UpdateTunnelModal = () => { + const [isOpen, setOpen] = useState(false); + const [modalData, setModalData] = useState(null); + + useEffect(() => { + const openSub = subscribeOpenModal(modalNameKey, (data) => { + setModalData(data); + setOpen(true); + }); + const closeSub = subscribeCloseModal(modalNameKey, () => setOpen(false)); + return () => { + openSub.unsubscribe(); + closeSub.unsubscribe(); + }; + }, []); + + return ( + { + setOpen(false); + }} + > + {isOpen && isPresent(modalData) && } + + ); +}; + +const formSchema = z.object({ + name: z.string().trim().min(1, 'Field is required'), + address: z.string().refine((value) => { + if (!value) return false; + return value + .split(',') + .map((ip) => ip.trim()) + .every( + (ip) => patternValidIpWithMask.test(ip) || patternValidIpV6WithMask.test(ip), + ); + }, 'Field is invalid'), + prvkey: z + .string() + .refine((v) => patternValidWireguardKey.test(v), 'Invalid WireGuard key'), + pubkey: z + .string() + .refine((v) => patternValidWireguardKey.test(v), 'Invalid WireGuard key'), + server_pubkey: z + .string() + .refine((v) => patternValidWireguardKey.test(v), 'Invalid WireGuard key'), + preshared_key: z + .string() + .refine((v) => !v || patternValidWireguardKey.test(v), 'Invalid WireGuard key'), + endpoint: z.string().refine((v) => patternValidEndpoint.test(v), 'Invalid address'), + dns: z.string(), + allowed_ips: z.string().refine((v) => { + if (!v) return true; + return v + .split(',') + .map((s) => s.trim()) + .every((cidr) => cidrRegex.test(cidr)); + }, 'Invalid CIDR notation'), + persistent_keep_alive: z.number().int().min(0), + pre_up: z.string(), + post_up: z.string(), + pre_down: z.string(), + post_down: z.string(), +}); + +type FormFields = z.infer; + +const ModalContent = ({ data }: { data: OpenUpdateTunnelModalData }) => { + const { mutateAsync: updateTunnel, isPending } = useMutation({ + mutationFn: api.updateTunnel, + }); + + const defaultValues = useMemo( + (): FormFields => ({ + name: data.name, + address: data.address, + prvkey: data.prvkey, + pubkey: data.pubkey, + server_pubkey: data.server_pubkey, + preshared_key: data.preshared_key ?? '', + endpoint: data.endpoint, + dns: data.dns ?? '', + allowed_ips: data.allowed_ips ?? '', + persistent_keep_alive: data.persistent_keep_alive, + pre_up: data.pre_up ?? '', + post_up: data.post_up ?? '', + pre_down: data.pre_down ?? '', + post_down: data.post_down ?? '', + }), + [data], + ); + + const form = useAppForm({ + defaultValues, + validationLogic: formChangeLogic, + validators: { + onSubmit: formSchema, + onChange: formSchema, + }, + onSubmit: async ({ value }) => { + try { + await updateTunnel({ + id: data.id as number, + route_all_traffic: data.route_all_traffic, + ...value, + }); + Snackbar.default('Tunnel updated.'); + closeModal(modalNameKey); + } catch (e) { + Snackbar.error(String(e)); + } + }, + }); + + return ( +
+
{ + e.stopPropagation(); + e.preventDefault(); + form.handleSubmit(); + }} + > + + + {(field) => } + + + + {(field) => } + + + + + {(field) => } + + + {(field) => ( + + )} + + + + + + {(field) => } + + + {(field) => } + + + + + + {(field) => } + + + {(field) => } + + + + + {(field) => ( + + )} + + + + {(field) => } + + + + + {(field) => } + + + {(field) => } + + + + + + {(field) => } + + + {(field) => } + + + +
+
+
+
+
+
+ ); +}; diff --git a/new-ui/src/pages/full/OverviewPage/style.scss b/new-ui/src/pages/full/OverviewPage/style.scss index 44389549..32d8c8a0 100644 --- a/new-ui/src/pages/full/OverviewPage/style.scss +++ b/new-ui/src/pages/full/OverviewPage/style.scss @@ -22,39 +22,25 @@ padding: var(--spacing-md) calc(var(--spacing-lg) - 6px) 0 var(--spacing-lg); } - .scroll-container { - > .header { - display: flex; - flex-flow: row nowrap; - align-items: center; - justify-content: flex-start; - user-select: none; - - > p { - font: var(--t-h5); - color: var(--fg-white-100); - } - - > button { - display: flex; - flex-flow: row nowrap; - align-items: center; - justify-content: center; - margin-left: auto; - padding: 0; - height: 28px; - border: 0; - background: transparent; - column-gap: var(--spacing-xs); - user-select: none; + > .header { + display: flex; + flex-flow: row nowrap; + align-items: center; + justify-content: flex-start; + user-select: none; + min-height: 28px; + + > p { + font: var(--t-h5); + color: var(--fg-white-100); + } - span { - font: var(--t-body-xs-500); - color: var(--fg-white-80); - } - } + > .right { + margin-left: auto; } + } + .scroll-container { > .locations { display: flex; flex-flow: column; diff --git a/new-ui/src/shared/components/Modal/style.scss b/new-ui/src/shared/components/Modal/style.scss index 49262f6b..c697ab88 100644 --- a/new-ui/src/shared/components/Modal/style.scss +++ b/new-ui/src/shared/components/Modal/style.scss @@ -4,6 +4,7 @@ border-radius: var(--radius-lg); width: 100%; max-width: var(--max-width); + flex-shrink: 0; position: relative; overflow: hidden; diff --git a/new-ui/src/shared/edge-api/api.ts b/new-ui/src/shared/edge-api/api.ts index 16a65dc8..757013ec 100644 --- a/new-ui/src/shared/edge-api/api.ts +++ b/new-ui/src/shared/edge-api/api.ts @@ -20,13 +20,15 @@ import type { MfaSetupFinishRequest, MfaSetupFinishResponse, MfaSetupStartResponse, + UpdateInstanceRequest, + UpdateInstanceResult, } from './types'; const getPlatformHeader = (): Promise => invoke(TauriCommand.GetPlatformHeader); const getInstances = (): Promise => invoke(TauriCommand.AllInstances); const deleteInstance = (instanceId: number): Promise => invoke(TauriCommand.DeleteInstance, { instanceId }); -const updateInstance = (args: { +const updateInstanceRecord = (args: { instanceId: number; response: CreateDeviceResponse; }): Promise => invoke(TauriCommand.UpdateInstance, args); @@ -117,7 +119,10 @@ const addInstance = async (values: AddInstanceRequest): Promise => { + try { + const proxyUrl = buildProxyUrl(values.url); + const edgeHeaders = await getEdgeRequestHeaders(); + + const instances = await getInstances(); + const existing = instances.find((i) => i.id === values.instanceId); + if (!existing) return { error: 'Instance no longer exists.' }; + + const startRes = await fetch(`${proxyUrl}/enrollment/start`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...edgeHeaders }, + body: JSON.stringify({ token: values.token }), + }); + + if (!startRes.ok) { + const body = (await startRes.json()) as { error?: string }; + return { + error: body.error ?? `Enrollment start failed (${startRes.status})`, + isCredentialsError: true, + }; + } + + const cookie = startRes.headers + .getSetCookie() + .find((c) => c.startsWith('defguard_proxy=')); + if (!cookie) return { error: 'Auth cookie missing from enrollment response' }; + + const resp = (await startRes.json()) as EnrollmentStartResponse; + + if (resp.instance.id !== existing.uuid) { + return { + error: 'Provided token belongs to a different instance.', + isCredentialsError: true, + }; + } + + const netRes = await fetch(`${proxyUrl}/enrollment/network_info`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Cookie: cookie, + ...edgeHeaders, + }, + body: JSON.stringify({ pubkey: existing.pubkey }), + }); + + if (!netRes.ok) { + const body = (await netRes.json()) as { error?: string }; + return { error: body.error ?? `network_info failed (${netRes.status})` }; + } + + await updateInstanceRecord({ + instanceId: existing.id, + response: await netRes.json(), + }); + return {}; + } catch (e) { + return { error: e instanceof Error ? e.message : String(e) }; + } +}; + const startMfaSetup = async ( proxyUrl: string, cookie: string, @@ -209,6 +278,7 @@ export const edgeApi = { getEdgeRequestHeaders, createDevice, addInstance, + updateInstance: updateExistingInstance, startMfaSetup, activateUser, finishMfaSetup, diff --git a/new-ui/src/shared/edge-api/types.ts b/new-ui/src/shared/edge-api/types.ts index 85a5e3d2..ce7611bc 100644 --- a/new-ui/src/shared/edge-api/types.ts +++ b/new-ui/src/shared/edge-api/types.ts @@ -57,6 +57,13 @@ export type AddInstanceResult = { error?: string; }; +export type UpdateInstanceRequest = { instanceId: number; url: string; token: string }; +export type UpdateInstanceResult = { + error?: string; + /** Set when the error is about the provided url/token and should be shown on the form fields. */ + isCredentialsError?: boolean; +}; + export type MfaSetupStartRequest = { method: MfaMethodValue }; export type MfaSetupStartResponse = { totp_secret?: string }; diff --git a/new-ui/src/shared/hooks/modalControls/modalTypes.ts b/new-ui/src/shared/hooks/modalControls/modalTypes.ts index 085db115..0d4793f2 100644 --- a/new-ui/src/shared/hooks/modalControls/modalTypes.ts +++ b/new-ui/src/shared/hooks/modalControls/modalTypes.ts @@ -1,17 +1,21 @@ import z from 'zod'; +import type { OpenUpdateInstanceModalData, OpenUpdateTunnelModalData } from './types'; export const ModalName = { - Test: 'test', + UpdateInstance: 'update-instance', + UpdateTunnel: 'update-tunnel', } as const; export type ModalNameValue = (typeof ModalName)[keyof typeof ModalName]; const modalOpenArgsSchema = z.discriminatedUnion('name', [ z.object({ - name: z.literal(ModalName.Test), - data: z.object({ - test: z.string(), - }), + name: z.literal(ModalName.UpdateInstance), + data: z.custom(), + }), + z.object({ + name: z.literal(ModalName.UpdateTunnel), + data: z.custom(), }), ]); diff --git a/new-ui/src/shared/hooks/modalControls/types.ts b/new-ui/src/shared/hooks/modalControls/types.ts new file mode 100644 index 00000000..d8d0dc8e --- /dev/null +++ b/new-ui/src/shared/hooks/modalControls/types.ts @@ -0,0 +1,8 @@ +import type { TunnelInfo } from '../../rust-api/types'; + +export type OpenUpdateInstanceModalData = { + instanceId: number; + url: string; +}; + +export type OpenUpdateTunnelModalData = TunnelInfo; diff --git a/new-ui/src/shared/providers/TauriEventProvider.tsx b/new-ui/src/shared/providers/TauriEventProvider.tsx index 62e801ef..23c0e9b1 100644 --- a/new-ui/src/shared/providers/TauriEventProvider.tsx +++ b/new-ui/src/shared/providers/TauriEventProvider.tsx @@ -56,6 +56,8 @@ export const TauriEventProvider = ({ children }: PropsWithChildren) => { void queryClient.invalidateQueries({ queryKey: ['locations'] }); void queryClient.invalidateQueries({ queryKey: ['location-details'] }); void queryClient.invalidateQueries({ queryKey: ['has-any-visible-locations'] }); + void queryClient.invalidateQueries({ queryKey: ['tunnels'] }); + void queryClient.invalidateQueries({ queryKey: ['tunnel-details'] }); }), listen(TauriEvent.AppVersionFetch, (event) => { diff --git a/new-ui/src/shared/rust-api/api.ts b/new-ui/src/shared/rust-api/api.ts index 2ccb9e06..9d2ca937 100644 --- a/new-ui/src/shared/rust-api/api.ts +++ b/new-ui/src/shared/rust-api/api.ts @@ -22,6 +22,7 @@ import type { TunnelInfo, TunnelRequest, UpdateInstanceArgs, + UpdateTunnelRequest, } from './types'; import { TauriCommand } from './types'; @@ -82,7 +83,7 @@ const parseTunnelConfig = (data: { const saveTunnel = (tunnel: TunnelRequest): Promise => invoke(TauriCommand.SaveTunnel, { tunnel }); -const updateTunnel = (tunnel: TunnelRequest): Promise => +const updateTunnel = (tunnel: UpdateTunnelRequest): Promise => invoke(TauriCommand.UpdateTunnel, { tunnel }); const deleteTunnel = (tunnelId: number): Promise => diff --git a/new-ui/src/shared/rust-api/types.ts b/new-ui/src/shared/rust-api/types.ts index 6f5cfbe1..c84aabf1 100644 --- a/new-ui/src/shared/rust-api/types.ts +++ b/new-ui/src/shared/rust-api/types.ts @@ -355,6 +355,12 @@ export type TunnelRequest = { post_down?: string; }; +export type UpdateTunnelRequest = TunnelRequest & { + id: number; + preshared_key?: string; + route_all_traffic: boolean; +}; + export type SaveConfigArgs = { privateKey: string; response: CreateDeviceResponse; From a3a8052725ff3b60723fe09915a0f0d03127cbd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20=C5=9Al=C4=99zak?= Date: Mon, 6 Jul 2026 10:41:27 +0200 Subject: [PATCH 3/6] auto update selection --- .../src/shared/providers/AppDataContext.tsx | 47 +++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/new-ui/src/shared/providers/AppDataContext.tsx b/new-ui/src/shared/providers/AppDataContext.tsx index 1fd3df49..e6b60c2c 100644 --- a/new-ui/src/shared/providers/AppDataContext.tsx +++ b/new-ui/src/shared/providers/AppDataContext.tsx @@ -1,6 +1,12 @@ import { useQuery } from '@tanstack/react-query'; import { clone } from 'radashi'; -import { createContext, type PropsWithChildren, useCallback, useContext } from 'react'; +import { + createContext, + type PropsWithChildren, + useCallback, + useContext, + useEffect, +} from 'react'; import { api } from '../rust-api/api'; import { getInstancesQueryOptions, @@ -37,8 +43,11 @@ export const useAppData = (): AppDataContextValue => { }; export const AppDataProvider = ({ children }: PropsWithChildren) => { - const { data: instances = [] } = useQuery(getInstancesQueryOptions); - const { data: tunnels = [] } = useQuery(getTunnelsQueryOptions); + const { data: instances = [], isPending: instancesPending } = useQuery( + getInstancesQueryOptions, + ); + const { data: tunnels = [], isPending: tunnelsPending } = + useQuery(getTunnelsQueryOptions); const { data: sessionState } = useQuery(getSessionStateQueryOptions); const isEmpty = instances.length === 0 && tunnels.length === 0; @@ -46,6 +55,38 @@ export const AppDataProvider = ({ children }: PropsWithChildren) => { api.patchSessionState({ view_selection: selection }); }, []); + // keep the selection valid when its instance/tunnel gets removed + useEffect(() => { + if (instancesPending || tunnelsPending || sessionState === undefined) return; + + const selection = sessionState.view_selection ?? null; + const isValid = + selection !== null && + (selection.kind === 'instance' + ? instances.some((instance) => instance.id === selection.id) + : tunnels.some((tunnel) => tunnel.id === selection.id)); + + if (isValid) return; + + const fallback: OverviewViewSelection | null = + instances.length > 0 + ? { kind: 'instance', id: instances[0].id } + : tunnels.length > 0 + ? { kind: 'tunnel', id: tunnels[0].id } + : null; + + if (selection === null && fallback === null) return; + + setViewSelection(fallback); + }, [ + instances, + tunnels, + instancesPending, + tunnelsPending, + sessionState, + setViewSelection, + ]); + const setConnectionMethod = useCallback( (id: number, conType: ConnectionType, method: MfaMethodValue) => { const cloned = clone(sessionState?.connection_mfa_method ?? {}); From fa47f054b4a2f6a667f948e1ff7c0e02bf736557 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20=C5=9Al=C4=99zak?= Date: Mon, 6 Jul 2026 11:08:15 +0200 Subject: [PATCH 4/6] fix delete tunnel lacking event emmit --- src-tauri/src/commands.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 197e37fb..36847b72 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -1155,6 +1155,10 @@ pub async fn delete_tunnel(tunnel_id: Id, handle: AppHandle) -> Result<(), Error transaction.commit().await?; + handle + .emit(EventKey::LocationUpdate.into(), ()) + .map_err(tauri_err_to_app_err)?; + info!("Successfully deleted tunnel {tunnel}"); Ok(()) } From 0a141afa9c484d68a523de4150dba21c2cf6bf43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20=C5=9Al=C4=99zak?= Date: Mon, 6 Jul 2026 12:09:52 +0200 Subject: [PATCH 5/6] add confirm modals --- new-ui/package.json | 2 + new-ui/pnpm-lock.yaml | 127 ++++++++++++++++++ .../OverviewActionsButton.tsx | 34 ++++- new-ui/src/routes/__root.tsx | 2 + .../components/ConfirmModal/ConfirmModal.tsx | 68 ++++++++++ .../shared/components/ConfirmModal/style.scss | 6 + .../RenderMarkdown/RenderMarkdown.tsx | 37 +++++ .../components/RenderMarkdown/style.scss | 6 + .../hooks/confirmModal/useConfirmModal.tsx | 37 +++++ 9 files changed, 317 insertions(+), 2 deletions(-) create mode 100644 new-ui/src/shared/components/ConfirmModal/ConfirmModal.tsx create mode 100644 new-ui/src/shared/components/ConfirmModal/style.scss create mode 100644 new-ui/src/shared/components/RenderMarkdown/RenderMarkdown.tsx create mode 100644 new-ui/src/shared/components/RenderMarkdown/style.scss create mode 100644 new-ui/src/shared/hooks/confirmModal/useConfirmModal.tsx diff --git a/new-ui/package.json b/new-ui/package.json index 80765b5a..4ee6e500 100644 --- a/new-ui/package.json +++ b/new-ui/package.json @@ -44,6 +44,8 @@ "react-dom": "^19.2.7", "react-loading-skeleton": "^3.5.0", "react-markdown": "^10.1.0", + "rehype-raw": "^7.0.0", + "rehype-sanitize": "^6.0.0", "rxjs": "^7.8.2", "sass": "^1.101.0", "zod": "^4.4.3", diff --git a/new-ui/pnpm-lock.yaml b/new-ui/pnpm-lock.yaml index 4763eaba..dd88174f 100644 --- a/new-ui/pnpm-lock.yaml +++ b/new-ui/pnpm-lock.yaml @@ -101,6 +101,12 @@ importers: react-markdown: specifier: ^10.1.0 version: 10.1.0(@types/react@19.2.17)(react@19.2.7) + rehype-raw: + specifier: ^7.0.0 + version: 7.0.0 + rehype-sanitize: + specifier: ^6.0.0 + version: 6.0.0 rxjs: specifier: ^7.8.2 version: 7.8.2 @@ -1139,6 +1145,10 @@ packages: emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -1256,12 +1266,30 @@ packages: resolution: {integrity: sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==} engines: {node: '>=20'} + hast-util-from-parse5@8.0.3: + resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==} + + hast-util-parse-selector@4.0.0: + resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} + + hast-util-raw@9.1.0: + resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==} + + hast-util-sanitize@5.0.2: + resolution: {integrity: sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg==} + hast-util-to-jsx-runtime@2.3.6: resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + hast-util-to-parse5@8.0.1: + resolution: {integrity: sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==} + hast-util-whitespace@3.0.0: resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + hastscript@9.0.1: + resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} + hookified@1.15.1: resolution: {integrity: sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==} @@ -1275,6 +1303,9 @@ packages: html-url-attributes@3.0.1: resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + ignore@7.0.5: resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} @@ -1637,6 +1668,9 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -1734,6 +1768,12 @@ packages: resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} engines: {node: '>= 20.19.0'} + rehype-raw@7.0.0: + resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} + + rehype-sanitize@6.0.0: + resolution: {integrity: sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==} + remark-parse@11.0.0: resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} @@ -1988,6 +2028,9 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + vfile-location@5.0.3: + resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} + vfile-message@4.0.3: resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} @@ -2037,6 +2080,9 @@ packages: yaml: optional: true + web-namespaces@2.0.1: + resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} @@ -2939,6 +2985,8 @@ snapshots: emoji-regex@8.0.0: {} + entities@6.0.1: {} + env-paths@2.2.1: {} error-ex@1.3.4: @@ -3040,6 +3088,43 @@ snapshots: dependencies: hookified: 1.15.1 + hast-util-from-parse5@8.0.3: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + devlop: 1.1.0 + hastscript: 9.0.1 + property-information: 7.2.0 + vfile: 6.0.3 + vfile-location: 5.0.3 + web-namespaces: 2.0.1 + + hast-util-parse-selector@4.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-raw@9.1.0: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + '@ungap/structured-clone': 1.3.2 + hast-util-from-parse5: 8.0.3 + hast-util-to-parse5: 8.0.1 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + parse5: 7.3.0 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-sanitize@5.0.2: + dependencies: + '@types/hast': 3.0.4 + '@ungap/structured-clone': 1.3.2 + unist-util-position: 5.0.0 + hast-util-to-jsx-runtime@2.3.6: dependencies: '@types/estree': 1.0.9 @@ -3060,10 +3145,28 @@ snapshots: transitivePeerDependencies: - supports-color + hast-util-to-parse5@8.0.1: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + hast-util-whitespace@3.0.0: dependencies: '@types/hast': 3.0.4 + hastscript@9.0.1: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + hast-util-parse-selector: 4.0.0 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + hookified@1.15.1: {} hookified@2.2.0: {} @@ -3072,6 +3175,8 @@ snapshots: html-url-attributes@3.0.1: {} + html-void-elements@3.0.0: {} + ignore@7.0.5: {} immutable@5.1.9: {} @@ -3523,6 +3628,10 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + parse5@7.3.0: + dependencies: + entities: 6.0.1 + pathe@2.0.3: {} picocolors@1.1.1: {} @@ -3608,6 +3717,17 @@ snapshots: readdirp@5.0.0: {} + rehype-raw@7.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-raw: 9.1.0 + vfile: 6.0.3 + + rehype-sanitize@6.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-sanitize: 5.0.2 + remark-parse@11.0.0: dependencies: '@types/mdast': 4.0.4 @@ -3902,6 +4022,11 @@ snapshots: util-deprecate@1.0.2: {} + vfile-location@5.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile: 6.0.3 + vfile-message@4.0.3: dependencies: '@types/unist': 3.0.3 @@ -3925,6 +4050,8 @@ snapshots: jiti: 2.7.0 sass: 1.101.0 + web-namespaces@2.0.1: {} + webpack-virtual-modules@0.6.2: {} which@1.3.1: diff --git a/new-ui/src/pages/full/OverviewPage/components/OverviewActionsButton/OverviewActionsButton.tsx b/new-ui/src/pages/full/OverviewPage/components/OverviewActionsButton/OverviewActionsButton.tsx index 8e72a2a0..86af5b43 100644 --- a/new-ui/src/pages/full/OverviewPage/components/OverviewActionsButton/OverviewActionsButton.tsx +++ b/new-ui/src/pages/full/OverviewPage/components/OverviewActionsButton/OverviewActionsButton.tsx @@ -14,8 +14,10 @@ import { ThemeVariable } from '../../../../../shared/types'; import './style.scss'; import { error } from '@tauri-apps/plugin-log'; import { useCallback, useMemo, useState } from 'react'; +import { ButtonVariant } from '../../../../../shared/components/Button/types'; import { Menu } from '../../../../../shared/components/Menu/Menu'; import type { MenuItemsGroup } from '../../../../../shared/components/Menu/types'; +import { useConfirmModal } from '../../../../../shared/hooks/confirmModal/useConfirmModal'; import { openModal } from '../../../../../shared/hooks/modalControls/modalsSubjects'; import { ModalName } from '../../../../../shared/hooks/modalControls/modalTypes'; import { api } from '../../../../../shared/rust-api/api'; @@ -100,6 +102,34 @@ export const OverviewActionsButton = ({ selection, instance }: Props) => { } }, [selection]); + const handleDeleteIntent = useCallback(() => { + if (!selection) return; + switch (selection.kind) { + case 'instance': + useConfirmModal.getState().open({ + title: 'Delete instance', + content: `Are you sure you want to delete this instance ? You will lose access to all locations associated with this instance. This action cannot be undone.`, + submitProps: { + variant: ButtonVariant.Critical, + text: `Delete instance`, + }, + onSubmit: handleDelete, + }); + break; + case 'tunnel': + useConfirmModal.getState().open({ + title: 'Delete tunnel', + content: `Are you sure you want to delete this tunnel ?. This action cannot be undone.`, + submitProps: { + variant: ButtonVariant.Critical, + text: `Delete tunnel`, + }, + onSubmit: handleDelete, + }); + break; + } + }, [selection, handleDelete]); + const menuConfig = useMemo(() => { const config: MenuItemsGroup[] = [ { @@ -112,13 +142,13 @@ export const OverviewActionsButton = ({ selection, instance }: Props) => { { text: 'Delete', icon: 'delete', - onClick: handleDelete, + onClick: handleDeleteIntent, }, ], }, ]; return config; - }, [handleUpdate, handleDelete, selection?.kind]); + }, [handleUpdate, selection?.kind, handleDeleteIntent]); const text = () => { switch (selection?.kind) { diff --git a/new-ui/src/routes/__root.tsx b/new-ui/src/routes/__root.tsx index 305dc691..5148267b 100644 --- a/new-ui/src/routes/__root.tsx +++ b/new-ui/src/routes/__root.tsx @@ -1,5 +1,6 @@ import type { QueryClient } from '@tanstack/react-query'; import { createRootRouteWithContext, Outlet } from '@tanstack/react-router'; +import { ConfirmModal } from '../shared/components/ConfirmModal/ConfirmModal'; import { AppDataProvider } from '../shared/providers/AppDataContext'; import { SnackbarManager } from '../shared/providers/snackbar/SnackbarManager'; import { TauriEventProvider } from '../shared/providers/TauriEventProvider'; @@ -20,6 +21,7 @@ function RootComponent() { + diff --git a/new-ui/src/shared/components/ConfirmModal/ConfirmModal.tsx b/new-ui/src/shared/components/ConfirmModal/ConfirmModal.tsx new file mode 100644 index 00000000..cdda977c --- /dev/null +++ b/new-ui/src/shared/components/ConfirmModal/ConfirmModal.tsx @@ -0,0 +1,68 @@ +import './style.scss'; +import { useMutation } from '@tanstack/react-query'; +import { useShallow } from 'zustand/shallow'; +import { useConfirmModal } from '../../hooks/confirmModal/useConfirmModal'; +import { isPresent } from '../../utils/isPresent'; +import { Button } from '../Button/Button'; +import { ButtonVariant } from '../Button/types'; +import { Controls } from '../Controls/Controls'; +import { Modal } from '../Modal/Modal'; +import { RenderMarkdown } from '../RenderMarkdown/RenderMarkdown'; + +export const ConfirmModal = () => { + const [isOpen, title] = useConfirmModal(useShallow((s) => [s.visible, s.title])); + + return ( + { + useConfirmModal.setState({ visible: false }); + }} + afterClose={() => { + useConfirmModal.getState().reset(); + }} + > + + + ); +}; + +const ModalContent = () => { + const content = useConfirmModal((s) => s.content); + + const [cancelProps, submitProps, onSubmit] = useConfirmModal( + useShallow((s) => [s.cancelProps, s.submitProps, s.onSubmit]), + ); + + const { mutate, isPending } = useMutation({ mutationFn: onSubmit }); + + return ( + <> + + +
+
+
+ + ); +}; diff --git a/new-ui/src/shared/components/ConfirmModal/style.scss b/new-ui/src/shared/components/ConfirmModal/style.scss new file mode 100644 index 00000000..b2f56efa --- /dev/null +++ b/new-ui/src/shared/components/ConfirmModal/style.scss @@ -0,0 +1,6 @@ +#confirm-modal .markdown-render { + p { + font: var(--t-body-sm-400); + color: var(--fg-white-100); + } +} diff --git a/new-ui/src/shared/components/RenderMarkdown/RenderMarkdown.tsx b/new-ui/src/shared/components/RenderMarkdown/RenderMarkdown.tsx new file mode 100644 index 00000000..272ee503 --- /dev/null +++ b/new-ui/src/shared/components/RenderMarkdown/RenderMarkdown.tsx @@ -0,0 +1,37 @@ +import type { HTMLProps } from 'react'; +import './style.scss'; +import clsx from 'clsx'; +import ReactMarkdown from 'react-markdown'; +import rehypeRaw from 'rehype-raw'; +import rehypeSanitize, { defaultSchema } from 'rehype-sanitize'; + +const sanitizeSchema = { + ...defaultSchema, + tagNames: (defaultSchema.tagNames ?? []).filter((tag: string) => tag !== 'iframe'), +}; + +export const RenderMarkdown = ({ + content, + containerProps, +}: { + content?: string | null | undefined; + containerProps?: HTMLProps; +}) => { + const containerCustomClassName = containerProps?.className; + + return ( +
+ ); +}; diff --git a/new-ui/src/shared/components/RenderMarkdown/style.scss b/new-ui/src/shared/components/RenderMarkdown/style.scss new file mode 100644 index 00000000..c6d5a8fc --- /dev/null +++ b/new-ui/src/shared/components/RenderMarkdown/style.scss @@ -0,0 +1,6 @@ +.markdown-render { + a { + text-decoration: none; + color: var(--fg-action); + } +} diff --git a/new-ui/src/shared/hooks/confirmModal/useConfirmModal.tsx b/new-ui/src/shared/hooks/confirmModal/useConfirmModal.tsx new file mode 100644 index 00000000..7d394267 --- /dev/null +++ b/new-ui/src/shared/hooks/confirmModal/useConfirmModal.tsx @@ -0,0 +1,37 @@ +import { create } from 'zustand'; +import type { ButtonProps } from '../../components/Button/types'; + +type StoreValues = { + visible: boolean; + title: string; + content?: string | null; + cancelProps: ButtonProps | null; + submitProps: ButtonProps | null; + onSubmit: () => Promise; +}; + +const emptyPromise = async () => {}; + +const defaults: StoreValues = { + visible: false, + title: 'Confirm action', + content: null, + submitProps: null, + cancelProps: null, + onSubmit: emptyPromise, +}; + +interface Store extends StoreValues { + open: (values: Partial) => void; + reset: () => void; +} + +export const useConfirmModal = create()((set) => ({ + ...defaults, + open: (values) => { + set({ ...defaults, ...values, visible: true }); + }, + reset: () => { + set(defaults); + }, +})); From e1326407bf19dff660639244993180a0b20d2d9b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 10:11:35 +0000 Subject: [PATCH 6/6] chore(nix): update new-ui pnpm deps hash --- nix/package.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/package.nix b/nix/package.nix index 18cb095d..67916379 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -99,7 +99,7 @@ inherit version pnpm; src = ../new-ui; fetcherVersion = 3; - hash = "sha256-XtCu/xAvppQeKDMVJrr/AX10sEwNPvaIB4Hx7ReSTvw="; + hash = "sha256-9xqPuHyPc3VdM+Hv6/k42c4XIR1wBkOQZ2khXx8PmqM="; }; # Pre-build the new UI frontend so Tauri can serve it as WebviewUrl::App("compact/") and "full/".