diff --git a/backend/src/server/services/definitions/example.rs b/backend/src/server/services/definitions/example.rs deleted file mode 100644 index 830be5c3..00000000 --- a/backend/src/server/services/definitions/example.rs +++ /dev/null @@ -1,26 +0,0 @@ -use crate::server::services::definitions::{create_service, ServiceDefinitionFactory}; -use crate::server::services::types::patterns::Pattern; -use crate::server::hosts::types::ports::PortBase; -use crate::server::services::types::types::ServiceDefinition; -use crate::server::services::types::categories::ServiceCategory; - -#[derive(Default, Clone, Eq, PartialEq, Hash)] -pub struct YourService; - -impl ServiceDefinition for YourService { - fn name(&self) -> &'static str { "Your Service Name" } // < 15 chars - fn description(&self) -> &'static str { "Brief description" } // < 60 chars - fn category(&self) -> ServiceCategory { ServiceCategory::Web } - - fn discovery_pattern(&self) -> Pattern { - // Choose appropriate pattern - Pattern::Port(PortBase::new_tcp(8080)) - } - - // Optional overrides: - // fn is_generic(&self) -> bool { false } - // fn is_gateway(&self) -> bool { false } - // fn icon(&self) -> &'static str { "icon-name" } -} - -inventory::submit!(ServiceDefinitionFactory::new(create_service::)); \ No newline at end of file diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index 83ed5255..a4574605 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -608,8 +608,7 @@ Discovery is the process of scanning your network to find hosts and services. **Scheduled (Automatic)** - Runs on a cron schedule -- Default: Hourly (`0 0 */1 * * *`) -- Can be customized to any cron expression +- Default: Every 24 hours (`0 0 */1 * * *`) - Enable/disable without deleting ### Creating a Discovery @@ -685,6 +684,42 @@ The topology view generates an interactive diagram of your network structure. Discovery Sessions

+### Topology State + +Topology will display different states depending on if the underlying data has changed since it was last built. Pressing Rebuild will have different effects depending on topology state. + +Rebuilding the graph will reset some user-provided changes, such as node positions and subnet sizes. Edge handles will be preserved when possible, but may also be overwritten as the layout of the graph may need to change depending on entities being added or removed. + +**Up to date** +

+ Discovery Sessions +

+- All data is up to date +- There is usually no reason to rebuild the graph in this state + +**Stale** +

+ Discovery Sessions +

+- One or more entities have been added or updated since the visualization was last rebuilt +- Rebuilding the graph will add those entities + +**Conflict** +

+ Discovery Sessions +

+- One or more entities on the graph have been deleted since it was last rebuilt +- Rebuilding the graph will remove those entities +- The specific entities that will be removed will be displayed in a modal so you can confirm before they are removed. + +**Locked** +

+ Discovery Sessions +

+- Other states will not be displayed +- Topology cannot be rebuilt +- Press Lock to enter the locked state + ### Visual Elements **Subnet Containers** @@ -692,15 +727,14 @@ The topology view generates an interactive diagram of your network structure. - Shows subnet name and CIDR - Can be resized manually -**Host Nodes** +**Interface Nodes** - Represent network interfaces - Show services bound to that interface - Display IP addresses and hostnames -**Service Nodes** +**Services** - Icons representing detected services - Show service name and ports -- Color-coded by category **Edges** - Lines connecting related nodes @@ -708,7 +742,6 @@ The topology view generates an interactive diagram of your network structure. - Host interfaces - Group relationships - Docker container links - - Gateway connections **Left Zone** - Optional section within each subnet @@ -723,24 +756,12 @@ Access the options panel via the button on the right side of the topology view: Topology Options

-**General Options** - -*Network Selection* -- Choose which networks to include in the diagram -- Multi-select to overlay multiple networks -- Useful for comparing environments - -*Service Category Filters* -- Hide specific categories (Media, Development, etc.) -- Reduces clutter for large networks -- Categories remain in data, just hidden from view - **Visual Options** *Don't Fade Edges* - Show all edges at full opacity - Default behavior fades unselected edges -- Enable for clearer edge visibility +- Enable for clearer edge visibility and screenshot appearance *Hide Resize Handles* - Remove subnet resize handles from corners @@ -801,7 +822,10 @@ Access the options panel via the button on the right side of the topology view: **Node Positioning** - Click and drag any node to reposition -- Reset by refreshing the topology + +**Edge Handle** +- Select an edge to show handles (same-colored circle at edge ends) +- Move handles to other locations on node **Subnet Sizing** - Drag subnet corners to resize diff --git a/media/topology_conflict.png b/media/topology_conflict.png new file mode 100644 index 00000000..aa599023 Binary files /dev/null and b/media/topology_conflict.png differ diff --git a/media/topology_fresh.png b/media/topology_fresh.png new file mode 100644 index 00000000..c044bc12 Binary files /dev/null and b/media/topology_fresh.png differ diff --git a/media/topology_full.png b/media/topology_full.png index 98ca82af..6399d440 100644 Binary files a/media/topology_full.png and b/media/topology_full.png differ diff --git a/media/topology_locked.png b/media/topology_locked.png new file mode 100644 index 00000000..f3a85f07 Binary files /dev/null and b/media/topology_locked.png differ diff --git a/media/topology_stale.png b/media/topology_stale.png new file mode 100644 index 00000000..de28e0c1 Binary files /dev/null and b/media/topology_stale.png differ diff --git a/ui/src/lib/features/topology/components/TopologyModal.svelte b/ui/src/lib/features/topology/components/TopologyModal.svelte index 3456e873..ac221e72 100644 --- a/ui/src/lib/features/topology/components/TopologyModal.svelte +++ b/ui/src/lib/features/topology/components/TopologyModal.svelte @@ -4,6 +4,8 @@ import { createEmptyTopologyFormData, createTopology, + topologies, + topology, topologyOptions, updateTopology } from '../store'; @@ -11,21 +13,33 @@ import ModalHeaderIcon from '$lib/shared/components/layout/ModalHeaderIcon.svelte'; import TopologyDetailsForm from './TopologyDetailsForm.svelte'; - export let isOpen = false; - export let onSubmit: () => Promise | void; - export let onClose: () => void; - export let topo: Topology | null = null; + let { + isOpen = $bindable(false), + onSubmit, + onClose, + topo = null + }: { + isOpen: boolean; + onSubmit: () => Promise | void; + onClose: () => void; + topo: Topology | null; + } = $props(); - $: isEditing = topo != null; - $: title = isEditing ? `Edit ${topo?.name}` : 'Create Topology'; + let isEditing = $derived(topo != null); + let title = $derived(isEditing ? `Edit ${topo?.name}` : 'Create Topology'); - let loading = false; - let formData: Topology = createEmptyTopologyFormData(); + let loading = $state(false); + let formData: Topology = $derived(topo ? { ...topo } : createEmptyTopologyFormData()); + + $effect(() => { + void $topology; + void $topologies; + }); // Reset form when modal opens - $: if (isOpen) { - resetForm(); - } + $effect(() => { + if (isOpen) resetForm(); + }); function resetForm() { formData = topo ? { ...topo } : createEmptyTopologyFormData(); @@ -52,7 +66,8 @@ } } - let colorHelper = entities.getColorHelper('Subnet'); + let colorHelper = $state(entities.getColorHelper('Topology')); + let Icon = $state(entities.getIconComponent('Topology')); - +
diff --git a/ui/src/lib/features/topology/components/TopologyTab.svelte b/ui/src/lib/features/topology/components/TopologyTab.svelte index 1f28e562..35341f30 100644 --- a/ui/src/lib/features/topology/components/TopologyTab.svelte +++ b/ui/src/lib/features/topology/components/TopologyTab.svelte @@ -33,11 +33,18 @@ import InlineWarning from '$lib/shared/components/feedback/InlineWarning.svelte'; import { formatTimestamp } from '$lib/shared/utils/formatting'; + const loading = loadData([getHosts, getServices, getSubnets, getGroups, getTopologies]); + let isCreateEditOpen = $state(false); let editingTopology: Topology | null = $state(null); let isRefreshConflictsOpen = $state(false); + $effect(() => { + void $topology; + void $topologies; + }); + function handleCreateTopology() { isCreateEditOpen = true; editingTopology = null; @@ -134,8 +141,6 @@ let lockedByUser = $derived( $topology?.locked_by ? $users.find((u) => u.id === $topology.locked_by) : null ); - - const loading = loadData([getHosts, getServices, getSubnets, getGroups, getTopologies]); diff --git a/ui/src/lib/features/topology/components/visualization/CustomEdge.svelte b/ui/src/lib/features/topology/components/visualization/CustomEdge.svelte index 62c4afca..75a8177e 100644 --- a/ui/src/lib/features/topology/components/visualization/CustomEdge.svelte +++ b/ui/src/lib/features/topology/components/visualization/CustomEdge.svelte @@ -35,6 +35,10 @@ const edgeData = data as TopologyEdge; const edgeTypeMetadata = edgeTypes.getMetadata(edgeData.edge_type); + $effect(() => { + void $topology; + }); + // Get group reactively - updates when groups store changes let group = $derived.by(() => { if (!$topology?.groups) return null; diff --git a/ui/src/lib/features/topology/components/visualization/InterfaceNode.svelte b/ui/src/lib/features/topology/components/visualization/InterfaceNode.svelte index 56d867ec..f42fd934 100644 --- a/ui/src/lib/features/topology/components/visualization/InterfaceNode.svelte +++ b/ui/src/lib/features/topology/components/visualization/InterfaceNode.svelte @@ -16,6 +16,10 @@ height = height ? height : 0; width = width ? width : 0; + $effect(() => { + void $topology; + }); + let host = $derived( $topology ? $topology.hosts.find((h) => h.id == nodeData.host_id) : undefined ); diff --git a/ui/src/lib/features/topology/components/visualization/SubnetNode.svelte b/ui/src/lib/features/topology/components/visualization/SubnetNode.svelte index 0630017c..86846e3e 100644 --- a/ui/src/lib/features/topology/components/visualization/SubnetNode.svelte +++ b/ui/src/lib/features/topology/components/visualization/SubnetNode.svelte @@ -22,6 +22,10 @@ let nodeStyle = $derived(`width: ${width}px; height: ${height}px;`); let hasInfra = $derived(infra_width > 0); + $effect(() => { + void $topology; + }); + let subnet = $derived($topology ? $topology.subnets.find((s) => s.id == id) : undefined); const viewport = useViewport(); diff --git a/ui/src/lib/features/topology/components/visualization/TopologyViewer.svelte b/ui/src/lib/features/topology/components/visualization/TopologyViewer.svelte index 1fdf11eb..4dee012e 100644 --- a/ui/src/lib/features/topology/components/visualization/TopologyViewer.svelte +++ b/ui/src/lib/features/topology/components/visualization/TopologyViewer.svelte @@ -15,6 +15,7 @@ optionsPanelExpanded, selectedEdge, selectedNode, + topologies, topology, updateTopology } from '../../store'; @@ -48,6 +49,11 @@ // Store pending edges until nodes are ready let pendingEdges: Edge[] = []; + $effect(() => { + void $topology; + void $topologies; + }); + $effect(() => { if ($topology && ($topology.edges || $topology.nodes)) { void loadTopologyData(); diff --git a/ui/src/lib/shared/components/forms/selection/display/TopologyDisplay.svelte b/ui/src/lib/shared/components/forms/selection/display/TopologyDisplay.svelte index 9fe8e5da..9b0610fc 100644 --- a/ui/src/lib/shared/components/forms/selection/display/TopologyDisplay.svelte +++ b/ui/src/lib/shared/components/forms/selection/display/TopologyDisplay.svelte @@ -1,4 +1,4 @@ -