diff --git a/new-ui/package.json b/new-ui/package.json
index f37ffeec..5e3c0516 100644
--- a/new-ui/package.json
+++ b/new-ui/package.json
@@ -23,6 +23,7 @@
"@tauri-apps/api": "^2.11.0",
"@tauri-apps/plugin-http": "^2.5.9",
"@tauri-apps/plugin-log": "^2.8.0",
+ "@tauri-apps/plugin-os": "^2.3.2",
"@uidotdev/usehooks": "^2.4.1",
"byte-size": "^9.0.1",
"chart.js": "^4.5.1",
diff --git a/new-ui/pnpm-lock.yaml b/new-ui/pnpm-lock.yaml
index 09514ba4..98c6a6b7 100644
--- a/new-ui/pnpm-lock.yaml
+++ b/new-ui/pnpm-lock.yaml
@@ -38,6 +38,9 @@ importers:
'@tauri-apps/plugin-log':
specifier: ^2.8.0
version: 2.8.0
+ '@tauri-apps/plugin-os':
+ specifier: ^2.3.2
+ version: 2.3.2
'@uidotdev/usehooks':
specifier: ^2.4.1
version: 2.4.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
@@ -831,6 +834,9 @@ packages:
'@tauri-apps/plugin-log@2.8.0':
resolution: {integrity: sha512-a+7rOq3MJwpTOLLKbL8d0qGZ85hgHw5pNOWusA9o3cf7cEgtYHiGY/+O8fj8MvywQIGqFv0da2bYQDlrqLE7rw==}
+ '@tauri-apps/plugin-os@2.3.2':
+ resolution: {integrity: sha512-n+nXWeuSeF9wcEsSPmRnBEGrRgOy6jjkSU+UVCOV8YUGKb2erhDOxis7IqRXiRVHhY8XMKks00BJ0OAdkpf6+A==}
+
'@tybys/wasm-util@0.10.2':
resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==}
@@ -2368,6 +2374,10 @@ snapshots:
dependencies:
'@tauri-apps/api': 2.11.0
+ '@tauri-apps/plugin-os@2.3.2':
+ dependencies:
+ '@tauri-apps/api': 2.11.0
+
'@tybys/wasm-util@0.10.2':
dependencies:
tslib: 2.8.1
diff --git a/new-ui/src/app/App.tsx b/new-ui/src/app/App.tsx
index b1ffd0fc..f73839e6 100644
--- a/new-ui/src/app/App.tsx
+++ b/new-ui/src/app/App.tsx
@@ -1,7 +1,6 @@
import { QueryClientProvider } from '@tanstack/react-query';
import { RouterProvider } from '@tanstack/react-router';
import { MainBackground } from '../shared/components/MainBackground/MainBackground';
-import { TauriEventProvider } from '../shared/providers/TauriEventProvider';
import { queryClient } from './query';
import { router } from './router';
@@ -11,9 +10,7 @@ function App() {
-
-
-
+
diff --git a/new-ui/src/pages/compact/CompactLocationsPage/CompactLocationsPage.tsx b/new-ui/src/pages/compact/CompactLocationsPage/CompactLocationsPage.tsx
index 8549451b..e900e6bb 100644
--- a/new-ui/src/pages/compact/CompactLocationsPage/CompactLocationsPage.tsx
+++ b/new-ui/src/pages/compact/CompactLocationsPage/CompactLocationsPage.tsx
@@ -1,6 +1,8 @@
import './style.scss';
import { useQuery } from '@tanstack/react-query';
import { useLoaderData } from '@tanstack/react-router';
+import { platform } from '@tauri-apps/plugin-os';
+import clsx from 'clsx';
import { useEffect, useMemo } from 'react';
import { Button } from '../../../shared/components/Button/Button';
import { ButtonVariant } from '../../../shared/components/Button/types';
@@ -19,6 +21,8 @@ import { CompactPage } from '../CompactPage/CompactPage';
import { InstanceSwitcher } from './components/InstanceSwitcher';
import { useCompactLocationStore } from './hooks/useCompactLocationsStore';
+const isWindows = platform() === 'windows';
+
export const CompactLocationsPage = () => {
const selection = useCompactLocationStore((s) => s.compactViewSelection);
const openLocation = useCompactLocationStore((s) => s.expandedLocation);
@@ -65,7 +69,11 @@ export const CompactLocationsPage = () => {
}}
>
-
+
{isPresent(instanceInfo) &&
diff --git a/new-ui/src/pages/compact/CompactLocationsPage/style.scss b/new-ui/src/pages/compact/CompactLocationsPage/style.scss
index 65b91847..e5107c8f 100644
--- a/new-ui/src/pages/compact/CompactLocationsPage/style.scss
+++ b/new-ui/src/pages/compact/CompactLocationsPage/style.scss
@@ -1,7 +1,7 @@
#compact-locations-page {
display: flex;
flex-flow: column;
- min-height: 100dvh;
+ height: 100dvh;
> .compact-footer {
display: flex;
@@ -21,11 +21,22 @@
min-height: 0;
row-gap: var(--spacing-sm);
+ &.windows {
+ scrollbar-gutter: stable;
+ overflow-y: scroll;
+
+ > .locations {
+ padding-right: 6px;
+ }
+ }
+
> .locations {
display: flex;
flex-flow: column;
row-gap: var(--spacing-sm);
width: 100%;
+ box-sizing: border-box;
+ padding-right: 6px;
}
}
}
diff --git a/new-ui/src/routes/__root.tsx b/new-ui/src/routes/__root.tsx
index 0bf72615..0ba07739 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 { TauriEventProvider } from '../shared/providers/TauriEventProvider';
interface RouterContext {
queryClient: QueryClient;
@@ -12,5 +13,9 @@ export const Route = createRootRouteWithContext
()({
});
function RootComponent() {
- return ;
+ return (
+
+
+
+ );
}
diff --git a/new-ui/src/routes/empty.tsx b/new-ui/src/routes/empty.tsx
index d0197839..7753c91e 100644
--- a/new-ui/src/routes/empty.tsx
+++ b/new-ui/src/routes/empty.tsx
@@ -1,9 +1,22 @@
-import { createFileRoute } from '@tanstack/react-router';
+import { useQuery } from '@tanstack/react-query';
+import { createFileRoute, useNavigate } from '@tanstack/react-router';
+import { useEffect } from 'react';
+
+import { hasAnyVisibleLocationsQueryOptions } from '../shared/rust-api/query';
export const Route = createFileRoute('/empty')({
component: RouteComponent,
});
function RouteComponent() {
- return Hello "/empty"!
;
+ const navigate = useNavigate();
+ const { data: hasLocations } = useQuery(hasAnyVisibleLocationsQueryOptions);
+
+ useEffect(() => {
+ if (hasLocations === true) {
+ void navigate({ to: '/' });
+ }
+ }, [hasLocations, navigate]);
+
+ return ;
}
diff --git a/new-ui/src/shared/providers/TauriEventProvider.tsx b/new-ui/src/shared/providers/TauriEventProvider.tsx
index 94aabbc2..fee0bfa1 100644
--- a/new-ui/src/shared/providers/TauriEventProvider.tsx
+++ b/new-ui/src/shared/providers/TauriEventProvider.tsx
@@ -26,11 +26,13 @@ export const TauriEventProvider = ({ children }: PropsWithChildren) => {
listen(TauriEvent.InstanceUpdate, () => {
void queryClient.invalidateQueries({ queryKey: ['instances'] });
void queryClient.invalidateQueries({ queryKey: ['locations'] });
+ void queryClient.invalidateQueries({ queryKey: ['has-any-visible-locations'] });
}),
listen(TauriEvent.LocationUpdate, () => {
void queryClient.invalidateQueries({ queryKey: ['locations'] });
void queryClient.invalidateQueries({ queryKey: ['location-details'] });
+ void queryClient.invalidateQueries({ queryKey: ['has-any-visible-locations'] });
}),
listen(TauriEvent.AppVersionFetch, () => {
@@ -41,6 +43,7 @@ export const TauriEventProvider = ({ children }: PropsWithChildren) => {
void queryClient.invalidateQueries({ queryKey: ['settings'] });
void queryClient.invalidateQueries({ queryKey: ['provisioning-config'] });
void queryClient.invalidateQueries({ queryKey: ['instances'] });
+ void queryClient.invalidateQueries({ queryKey: ['has-any-visible-locations'] });
}),
listen(TauriEvent.DeadConnectionDropped, () => {
diff --git a/new-ui/src/shared/rust-api/api.ts b/new-ui/src/shared/rust-api/api.ts
index 435966e9..282afd73 100644
--- a/new-ui/src/shared/rust-api/api.ts
+++ b/new-ui/src/shared/rust-api/api.ts
@@ -41,6 +41,9 @@ const saveDeviceConfig = (args: SaveConfigArgs): Promise =>
invoke(TauriCommand.AllLocations, { instanceId });
+const hasAnyVisibleLocations = (): Promise =>
+ invoke(TauriCommand.HasAnyVisibleLocations);
+
const getLocationDetails = (args: LocationDetailsArgs): Promise =>
invoke(TauriCommand.LocationInterfaceDetails, args);
@@ -139,6 +142,7 @@ export const api = {
saveDeviceConfig,
// Locations
getLocations,
+ hasAnyVisibleLocations,
getLocationDetails,
updateLocationRouting,
setLocationMfaMethod,
diff --git a/new-ui/src/shared/rust-api/query.ts b/new-ui/src/shared/rust-api/query.ts
index d42ef1a6..0d44f3ce 100644
--- a/new-ui/src/shared/rust-api/query.ts
+++ b/new-ui/src/shared/rust-api/query.ts
@@ -21,6 +21,11 @@ export const getLocationsQueryOptions = (instanceId: number) =>
queryFn: () => api.getLocations(instanceId),
});
+export const hasAnyVisibleLocationsQueryOptions = queryOptions({
+ queryKey: ['has-any-visible-locations'] as const,
+ queryFn: () => api.hasAnyVisibleLocations(),
+});
+
export const getLocationDetailsQueryOptions = (args: LocationDetailsArgs) =>
queryOptions({
queryKey: ['location-details', args.locationId, args.connectionType] as const,
diff --git a/new-ui/src/shared/rust-api/types.ts b/new-ui/src/shared/rust-api/types.ts
index 708d1b51..4f161038 100644
--- a/new-ui/src/shared/rust-api/types.ts
+++ b/new-ui/src/shared/rust-api/types.ts
@@ -73,6 +73,7 @@ export const TauriCommand = {
SaveDeviceConfig: 'save_device_config',
// Locations
AllLocations: 'all_locations',
+ HasAnyVisibleLocations: 'has_any_visible_locations',
LocationInterfaceDetails: 'location_interface_details',
UpdateLocationRouting: 'update_location_routing',
SetLocationMfaMethod: 'set_location_mfa_method',
diff --git a/new-ui/src/shared/scss/_base.scss b/new-ui/src/shared/scss/_base.scss
index 1efbcd5b..fc20585f 100644
--- a/new-ui/src/shared/scss/_base.scss
+++ b/new-ui/src/shared/scss/_base.scss
@@ -71,3 +71,21 @@ ol {
margin: 0;
padding: 0;
}
+
+::-webkit-scrollbar {
+ width: 4px;
+}
+
+::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+::-webkit-scrollbar-thumb {
+ background: transparent;
+ border-radius: 100px;
+}
+
+* {
+ scrollbar-width: thin;
+ scrollbar-color: var(--fg-white-50) transparent;
+}
diff --git a/src-tauri/permissions/default.toml b/src-tauri/permissions/default.toml
index 610c903d..11d31ed3 100644
--- a/src-tauri/permissions/default.toml
+++ b/src-tauri/permissions/default.toml
@@ -3,6 +3,7 @@ identifier = "allow-app-commands"
description = "Allow all application commands for both UI windows (old-ui and new-ui)."
commands.allow = [
"all_locations",
+ "has_any_visible_locations",
"save_device_config",
"all_instances",
"connect",
diff --git a/src-tauri/src/bin/defguard-client.rs b/src-tauri/src/bin/defguard-client.rs
index 34a85400..15c10029 100644
--- a/src-tauri/src/bin/defguard-client.rs
+++ b/src-tauri/src/bin/defguard-client.rs
@@ -156,6 +156,7 @@ fn main() {
let app = Builder::default()
.invoke_handler(tauri::generate_handler![
all_locations,
+ has_any_visible_locations,
save_device_config,
all_instances,
connect,
@@ -194,8 +195,8 @@ fn main() {
])
.on_window_event(|window, event| {
if let WindowEvent::CloseRequested { api, .. } = event {
- // Only prevent close on the tray (new-ui) window; let other windows close normally.
- if window.label() == NEW_UI_WINDOW_ID {
+ let label = window.label();
+ if label == NEW_UI_WINDOW_ID || label == OLD_UI_WINDOW_ID {
#[cfg(not(target_os = "macos"))]
let _ = window.hide();
@@ -351,6 +352,15 @@ fn main() {
let state = AppState::new(config, provisioning_config);
app.manage(state);
+ // Pre-build both windows hidden so they can be shown/hidden without recreation.
+ if let Err(e) = WindowManager::build_tray_window(app_handle) {
+ warn!("Failed to pre-build tray window: {e}");
+ }
+ if let Err(e) = WindowManager::build_full_window(app_handle) {
+ warn!("Failed to pre-build full window: {e}");
+ }
+
+ // Decide which window to show based on platform and available locations.
#[cfg(target_os = "linux")]
{
let _ = WindowManager::open_full_view(app_handle);
@@ -363,7 +373,7 @@ fn main() {
if has_locations {
WindowManager::open_tray(app_handle)?;
} else {
- info!("No locations found, spawning full view on startup.");
+ info!("No locations found, showing full view on startup.");
let _ = WindowManager::open_full_view(app_handle);
}
}
diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs
index af70e7ba..4b989311 100644
--- a/src-tauri/src/commands.rs
+++ b/src-tauri/src/commands.rs
@@ -567,6 +567,26 @@ pub async fn all_locations(instance_id: Id) -> Result, Error>
Ok(location_info)
}
+/// Returns `true` if there is at least one visible (non-service) location across all instances.
+/// Shares the same visibility filter as [`all_locations`] (`include_service_locations = false`).
+#[tauri::command(async)]
+pub async fn has_any_visible_locations() -> Result {
+ trace!("Checking whether any visible locations exist.");
+ let instances = Instance::all(&*DB_POOL).await?;
+ for instance in &instances {
+ let locations = Location::find_by_instance_id(&*DB_POOL, instance.id, false).await?;
+ if !locations.is_empty() {
+ trace!(
+ "Found at least one visible location in instance {}.",
+ instance.name
+ );
+ return Ok(true);
+ }
+ }
+ trace!("No visible locations found.");
+ Ok(false)
+}
+
#[derive(Serialize, Debug)]
pub struct LocationInterfaceDetails {
pub location_id: Id,
diff --git a/src-tauri/src/tray.rs b/src-tauri/src/tray.rs
index 4339c6bd..b4efd353 100644
--- a/src-tauri/src/tray.rs
+++ b/src-tauri/src/tray.rs
@@ -8,6 +8,8 @@ use tauri::{
use tauri::tray::{MouseButton, MouseButtonState, TrayIconEvent};
+#[cfg(not(target_os = "linux"))]
+use crate::window_manager::WindowManager;
use crate::{
active_connections::{get_connection_id_by_type, ACTIVE_CONNECTIONS},
appstate::AppState,
@@ -38,8 +40,7 @@ fn store_tray_click_position(app: &AppHandle, event: &TrayIconEvent) {
button_state: MouseButtonState::Down,
rect,
..
- }
- | TrayIconEvent::DoubleClick { rect, .. } => Some(rect.position.to_physical(1.0)),
+ } => Some(rect.position.to_physical(1.0)),
_ => None,
};
@@ -141,30 +142,9 @@ async fn generate_tray_menu(app: &AppHandle) -> Result