From 17bfec6d1d74f1c8c366dcc8f3ef70cae56504ab Mon Sep 17 00:00:00 2001 From: rcourtman Date: Fri, 27 Mar 2026 14:15:22 +0000 Subject: [PATCH] Gate discovery tab on AI discovery setting --- .../Discovery/DiscoveryTab.test.tsx | 66 +++++++++++++++++ .../src/components/Discovery/DiscoveryTab.tsx | 72 +++++++++++++++---- 2 files changed, 125 insertions(+), 13 deletions(-) create mode 100644 frontend-modern/src/components/Discovery/DiscoveryTab.test.tsx diff --git a/frontend-modern/src/components/Discovery/DiscoveryTab.test.tsx b/frontend-modern/src/components/Discovery/DiscoveryTab.test.tsx new file mode 100644 index 000000000..90d419b96 --- /dev/null +++ b/frontend-modern/src/components/Discovery/DiscoveryTab.test.tsx @@ -0,0 +1,66 @@ +import { cleanup, render, screen, waitFor } from '@solidjs/testing-library'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { DiscoveryTab } from './DiscoveryTab'; + +const getDiscoveryMock = vi.fn(); +const getDiscoveryInfoMock = vi.fn(); +const getConnectedAgentsMock = vi.fn(); +const getSettingsMock = vi.fn(); + +vi.mock('../../api/discovery', () => ({ + getDiscovery: (...args: unknown[]) => getDiscoveryMock(...args), + getDiscoveryInfo: (...args: unknown[]) => getDiscoveryInfoMock(...args), + triggerDiscovery: vi.fn(), + updateDiscoveryNotes: vi.fn(), + formatDiscoveryAge: vi.fn(() => 'just now'), + getCategoryDisplayName: vi.fn((value: string) => value), + getConfidenceLevel: vi.fn(() => ({ label: 'High confidence', color: 'text-green-600' })), + getConnectedAgents: (...args: unknown[]) => getConnectedAgentsMock(...args), +})); + +vi.mock('@/api/ai', () => ({ + AIAPI: { + getSettings: (...args: unknown[]) => getSettingsMock(...args), + }, +})); + +vi.mock('../../api/guestMetadata', () => ({ + GuestMetadataAPI: { + updateMetadata: vi.fn(), + }, +})); + +describe('DiscoveryTab', () => { + beforeEach(() => { + getDiscoveryMock.mockReset(); + getDiscoveryInfoMock.mockReset(); + getConnectedAgentsMock.mockReset(); + getSettingsMock.mockReset(); + }); + + afterEach(() => { + cleanup(); + }); + + it('does not fetch discovery data when AI discovery is disabled', async () => { + getSettingsMock.mockResolvedValue({ discovery_enabled: false }); + + render(() => ( + + )); + + await waitFor(() => { + expect(screen.getByText('AI Discovery Disabled')).toBeInTheDocument(); + }); + + expect(getDiscoveryMock).not.toHaveBeenCalled(); + expect(getDiscoveryInfoMock).not.toHaveBeenCalled(); + expect(getConnectedAgentsMock).not.toHaveBeenCalled(); + }); +}); diff --git a/frontend-modern/src/components/Discovery/DiscoveryTab.tsx b/frontend-modern/src/components/Discovery/DiscoveryTab.tsx index 08e7bc710..f6364fbd8 100644 --- a/frontend-modern/src/components/Discovery/DiscoveryTab.tsx +++ b/frontend-modern/src/components/Discovery/DiscoveryTab.tsx @@ -10,6 +10,7 @@ import { getConfidenceLevel, getConnectedAgents, } from '../../api/discovery'; +import { AIAPI } from '@/api/ai'; import { GuestMetadataAPI } from '../../api/guestMetadata'; import { eventBus } from '../../stores/events'; @@ -98,10 +99,25 @@ export const DiscoveryTab: Component = (props) => { // Track if we initiated scan via HTTP to prevent WebSocket race conditions const [httpScanInProgress, setHttpScanInProgress] = createSignal(false); + const [aiSettings] = createResource(async () => { + try { + return await AIAPI.getSettings(); + } catch { + return null; + } + }); + + const discoveryFeatureResolved = () => !aiSettings.loading; + const discoveryFeatureEnabled = () => + discoveryFeatureResolved() && aiSettings()?.discovery_enabled !== false; + const discoveryFeatureKnownDisabled = () => + discoveryFeatureResolved() && aiSettings()?.discovery_enabled === false; + // Fetch discovery info (AI provider, commands) - used for pre-scan transparency const [discoveryInfo] = createResource( - () => props.resourceType, + () => discoveryFeatureEnabled() ? props.resourceType : null, async (type) => { + if (!type) return null; try { return await getDiscoveryInfo(type); } catch { @@ -112,7 +128,11 @@ export const DiscoveryTab: Component = (props) => { // Fetch connected agents (for WebSocket command execution) const [connectedAgents] = createResource( - async () => { + () => discoveryFeatureEnabled() && props.resourceType === 'host', + async (enabled) => { + if (!enabled) { + return { count: 0, agents: [] }; + } try { return await getConnectedAgents(); } catch { @@ -149,8 +169,11 @@ export const DiscoveryTab: Component = (props) => { // Fetch discovery data const [discovery, { refetch, mutate }] = createResource( - () => ({ type: props.resourceType, host: props.hostId, id: props.resourceId }), + () => discoveryFeatureEnabled() ? ({ type: props.resourceType, host: props.hostId, id: props.resourceId }) : null, async (params) => { + if (!params) { + return null; + } try { const result = await getDiscovery(params.type, params.host, params.id); setHasFetched(true); @@ -189,6 +212,10 @@ export const DiscoveryTab: Component = (props) => { // Handle triggering a new discovery const handleTriggerDiscovery = async (force = false) => { + if (!discoveryFeatureEnabled()) { + setScanError('AI discovery is disabled in Settings → AI.'); + return; + } setIsScanning(true); setHttpScanInProgress(true); // Prevent WebSocket from resetting state setScanProgress(null); @@ -265,6 +292,9 @@ export const DiscoveryTab: Component = (props) => { const resourceId = () => makeResourceId(props.resourceType, props.hostId, props.resourceId); createEffect(() => { + if (!discoveryFeatureEnabled()) { + return; + } const unsubscribe = eventBus.on('ai_discovery_progress', (progress) => { // Only update if this progress is for our resource if (progress && progress.resource_id === resourceId()) { @@ -329,8 +359,24 @@ export const DiscoveryTab: Component = (props) => { return (
+ +
+
+ + + +
+

AI Discovery Disabled

+

+ Enable infrastructure discovery in Settings → AI before using this tab. +

+
+
+
+
+ {/* AI Provider Badge - Always visible when AI is configured */} - +
= (props) => { {/* "What Discovery Does" Explanation - Shown when no discovery yet */} - +
@@ -385,7 +431,7 @@ export const DiscoveryTab: Component = (props) => { {/* Commands Preview - Expandable before first scan */} - 0}> + 0}>
= (props) => { {/* Loading state - delayed to prevent flash for fast loads */} - +
Loading discovery... @@ -424,7 +470,7 @@ export const DiscoveryTab: Component = (props) => { {/* Scan Progress Bar */} - +
@@ -457,7 +503,7 @@ export const DiscoveryTab: Component = (props) => { {/* Scanning state without WebSocket progress - show live timer */} - +
@@ -473,7 +519,7 @@ export const DiscoveryTab: Component = (props) => { {/* Scan Success */} - +
@@ -485,7 +531,7 @@ export const DiscoveryTab: Component = (props) => { {/* Scan Error */} - +
@@ -508,7 +554,7 @@ export const DiscoveryTab: Component = (props) => { {/* No discovery yet - only show after initial fetch completes to prevent flash */} - +
@@ -587,7 +633,7 @@ export const DiscoveryTab: Component = (props) => { {/* Discovery exists but has no meaningful data - show re-scan option */} - +