@@ -22,23 +22,28 @@ import type { SovdResourceEntityType } from './types';
2222import {
2323 transformFaultsResponse ,
2424 transformOperationsResponse ,
25+ transformDataResponse ,
2526 transformConfigurationsResponse ,
2627 transformFault ,
2728 unwrapItems ,
2829} from './transforms' ;
2930import {
31+ getEntityDetail ,
3032 getEntityConfigurations ,
3133 getEntityOperations ,
3234 getEntityData ,
35+ getEntityDataItem ,
3336 getEntityFaults ,
3437 getEntityFaultDetail ,
3538 getEntityExecution ,
3639 postEntityExecution ,
3740 deleteEntityExecution ,
3841 deleteEntityFault ,
3942 putEntityConfiguration ,
43+ putEntityDataItem ,
4044 deleteEntityConfiguration ,
4145 deleteEntityConfigurations ,
46+ getEntityBulkData ,
4247} from './api-dispatch' ;
4348
4449const STORAGE_KEY = 'ros2_medkit_web_ui_server_url' ;
@@ -303,26 +308,53 @@ async function handleTopicSelection(ctx: SelectionContext, client: MedkitClient)
303308 const isTopicNodeData = 'isPublisher' in data && 'isSubscriber' in data && ! ( 'type' in data ) ;
304309
305310 if ( isTopicNodeData ) {
306- // TopicNodeData - need to fetch full details
311+ // TopicNodeData - need to fetch full topic details from the parent entity
307312 const { isPublisher, isSubscriber } = data as TopicNodeData ;
308- const apiPath = path . replace ( / ^ \/ s e r v e r / , '' ) ;
309- const pathSegments = apiPath . split ( '/' ) . filter ( Boolean ) ;
310- const entityType = ( pathSegments [ 0 ] || 'apps' ) as SovdResourceEntityType ;
311- const entityId = pathSegments [ 1 ] || '' ;
312- const { getEntityDetail } = await import ( './api-dispatch' ) ;
313- const { data : detailData } = await getEntityDetail ( client , entityType , entityId ) ;
314- const details = ( detailData || { id : entityId , name : entityId , type : entityType , href : apiPath } ) as SovdEntityDetails ;
315-
316- // Update tree with full data merged with direction info
317- const updatedTree = updateNodeInTree ( rootEntities , path , ( n ) => ( {
318- ...n ,
319- data : { ...( ( details as unknown as Record < string , unknown > ) ?. topicData as Record < string , unknown > ) , isPublisher, isSubscriber } ,
320- } ) ) ;
313+ const topicName = node . id ;
314+
315+ // Find parent entity by walking up the tree path
316+ const parentPath = path . split ( '/' ) . slice ( 0 , - 1 ) . join ( '/' ) ;
317+ const parentNode = findNode ( rootEntities , parentPath ) ;
318+ const parentType = parentNode ?. type || 'component' ;
319+ const entityType = `${ parentType } s` as SovdResourceEntityType ;
320+ const entityId = parentNode ?. id || '' ;
321+
322+ // Fetch the specific data item for this topic
323+ const { data : topicDetail } = await getEntityDataItem ( client , entityType , entityId , topicName ) ;
324+ const topicData = topicDetail as unknown as ComponentTopic | null ;
325+
326+ if ( topicData ) {
327+ // Update tree with full data merged with direction info
328+ const updatedTree = updateNodeInTree ( rootEntities , path , ( n ) => ( {
329+ ...n ,
330+ data : { ...topicData , isPublisher, isSubscriber } ,
331+ } ) ) ;
332+
333+ return {
334+ selectedPath : path ,
335+ selectedEntity : {
336+ id : node . id ,
337+ name : node . name ,
338+ href : node . href ,
339+ topicData : { ...topicData , isPublisher, isSubscriber } ,
340+ rosType : topicData . type ,
341+ type : 'topic' ,
342+ } ,
343+ rootEntities : updatedTree ,
344+ isLoadingDetails : false ,
345+ } ;
346+ }
321347
348+ // Fallback if topic fetch fails
322349 return {
323350 selectedPath : path ,
324- selectedEntity : details ,
325- rootEntities : updatedTree ,
351+ selectedEntity : {
352+ id : node . id ,
353+ name : node . name ,
354+ type : 'topic' ,
355+ href : node . href ,
356+ error : 'Failed to load topic details' ,
357+ } ,
326358 isLoadingDetails : false ,
327359 } ;
328360 }
@@ -520,6 +552,17 @@ function handleOperationSelection(ctx: SelectionContext): SelectionResult | null
520552 } ;
521553}
522554
555+ /**
556+ * Infer entity type from tree path depth.
557+ * Tree paths: /server/<areaId> (depth 1), /server/<areaId>/<componentId> (depth 2),
558+ * /server/<areaId>/<componentId>/<appId> (depth 3)
559+ */
560+ function inferEntityTypeFromDepth ( depth : number ) : SovdResourceEntityType {
561+ if ( depth <= 1 ) return 'areas' ;
562+ if ( depth === 2 ) return 'components' ;
563+ return 'apps' ;
564+ }
565+
523566/** Fallback: fetch entity details from API when not in tree */
524567async function fetchEntityFromApi (
525568 path : string ,
@@ -530,24 +573,23 @@ async function fetchEntityFromApi(
530573
531574 try {
532575 const apiPath = path . replace ( / ^ \/ s e r v e r / , '' ) ;
533- // Parse entity type and id from path: /areas/foo -> areas, foo
534576 const pathSegments = apiPath . split ( '/' ) . filter ( Boolean ) ;
535- const entityType = ( pathSegments [ 0 ] || 'areas' ) as SovdResourceEntityType ;
536- const entityId = pathSegments [ 1 ] || '' ;
537- const { data } = await ( await import ( './api-dispatch' ) ) . getEntityDetail ( client , entityType , entityId ) ;
538- const details = ( data || { id : entityId , name : entityId , type : entityType , href : apiPath } ) as SovdEntityDetails ;
577+ const entityId = pathSegments [ pathSegments . length - 1 ] || '' ;
578+ const entityType = inferEntityTypeFromDepth ( pathSegments . length ) ;
579+
580+ const { data } = await getEntityDetail ( client , entityType , entityId ) ;
581+ const details = ( data || { id : entityId , name : entityId , type : entityType . slice ( 0 , - 1 ) , href : apiPath } ) as SovdEntityDetails ;
539582 set ( { selectedEntity : details , isLoadingDetails : false } ) ;
540583 } catch ( error ) {
541584 const message = error instanceof Error ? error . message : 'Unknown error' ;
542585 toast . error ( `Failed to load entity details for ${ path } : ${ message } ` ) ;
543586
544- // Infer entity type from path structure
545587 const segments = path . split ( '/' ) . filter ( Boolean ) ;
546588 const id = segments [ segments . length - 1 ] || path ;
547- const inferredType = segments . length === 1 ? 'area' : segments . length === 2 ? 'component' : 'unknown' ;
589+ const inferredType = inferEntityTypeFromDepth ( segments . length ) ;
548590
549591 set ( {
550- selectedEntity : { id, name : id , type : inferredType , href : path , error : 'Failed to load details' } ,
592+ selectedEntity : { id, name : id , type : inferredType . slice ( 0 , - 1 ) , href : path , error : 'Failed to load details' } ,
551593 isLoadingDetails : false ,
552594 } ) ;
553595 }
@@ -1037,20 +1079,26 @@ export const useAppStore = create<AppState>()(
10371079
10381080 // Refresh the currently selected entity (re-fetch from server)
10391081 refreshSelectedEntity : async ( ) => {
1040- const { selectedPath, client } = get ( ) ;
1041- if ( ! selectedPath || ! client ) {
1082+ const { selectedPath, selectedEntity , client } = get ( ) ;
1083+ if ( ! selectedPath || ! client || ! selectedEntity ) {
10421084 return ;
10431085 }
10441086
10451087 set ( { isRefreshing : true } ) ;
10461088
10471089 try {
1048- // Convert tree path to API path (remove /server prefix)
1049- const apiPath = selectedPath . replace ( / ^ \/ s e r v e r / , '' ) ;
1050- const pathSegments = apiPath . split ( '/' ) . filter ( Boolean ) ;
1051- const entityType = ( pathSegments [ 0 ] || 'areas' ) as SovdResourceEntityType ;
1052- const entityId = pathSegments [ 1 ] || '' ;
1053- const { data } = await ( await import ( './api-dispatch' ) ) . getEntityDetail ( client , entityType , entityId ) ;
1090+ const entityType = `${ selectedEntity . type } s` as SovdResourceEntityType ;
1091+ const entityId = selectedEntity . id ;
1092+
1093+ // Only refresh actual entities (area, component, app, function)
1094+ const validTypes : SovdResourceEntityType [ ] = [ 'areas' , 'components' , 'apps' , 'functions' ] ;
1095+ if ( ! validTypes . includes ( entityType ) ) {
1096+ // For non-entity nodes (topic, fault, parameter), just clear refreshing
1097+ set ( { isRefreshing : false } ) ;
1098+ return ;
1099+ }
1100+
1101+ const { data } = await getEntityDetail ( client , entityType , entityId ) ;
10541102 if ( data ) {
10551103 set ( { selectedEntity : data as unknown as SovdEntityDetails , isRefreshing : false } ) ;
10561104 } else {
@@ -1589,7 +1637,7 @@ export const useAppStore = create<AppState>()(
15891637 if ( ! client ) return [ ] ;
15901638 const { data, error : fetchError } = await getEntityData ( client , entityType , entityId ) ;
15911639 if ( fetchError ) return [ ] ;
1592- return ( await import ( './transforms' ) ) . transformDataResponse ( data ) ;
1640+ return transformDataResponse ( data ) ;
15931641 } ,
15941642
15951643 fetchEntityOperations : async ( entityType : SovdResourceEntityType , entityId : string ) => {
@@ -1628,7 +1676,6 @@ export const useAppStore = create<AppState>()(
16281676 ) => {
16291677 const { client } = get ( ) ;
16301678 if ( ! client ) return ;
1631- const { putEntityDataItem } = await import ( './api-dispatch' ) ;
16321679 await putEntityDataItem ( client , entityType , entityId , dataId , request ) ;
16331680 } ,
16341681
@@ -1652,21 +1699,19 @@ export const useAppStore = create<AppState>()(
16521699 category : string ,
16531700 fileId : string
16541701 ) => {
1655- const { client } = get ( ) ;
1656- if ( ! client ) return null ;
1657- const { getEntityBulkData } = await import ( './api-dispatch' ) ;
1658- // For binary download, we need the URL to fetch directly with progress
1659- // Build URL from client base and use fetch directly
1702+ const { client, serverUrl } = get ( ) ;
1703+ if ( ! client || ! serverUrl ) return null ;
1704+
1705+ // Fetch file list to get filename
16601706 const { data } = await getEntityBulkData ( client , entityType , entityId , category ) ;
16611707 if ( ! data ) return null ;
1662- // Find the file descriptor to get download info
16631708 const items = ( data as unknown as { items ?: Array < { id : string ; name ?: string } > } ) ?. items || [ ] ;
16641709 const fileDesc = items . find ( ( item ) => item . id === fileId ) ;
16651710 const filename = fileDesc ?. name || fileId ;
1666- // Construct the download URL manually since openapi-fetch doesn't support blob responses well
1667- const { serverUrl : storedUrl } = get ( ) ;
1668- const baseUrl = storedUrl ? storedUrl . replace ( / \/ $ / , '' ) : '' ;
1669- const downloadUrl = `${ baseUrl } /${ entityType } /${ entityId } /bulk-data/${ category } /${ fileId } ` ;
1711+
1712+ // Download binary via fetch (openapi-fetch doesn't support blob responses)
1713+ const baseUrl = serverUrl . replace ( / \/ + $ / , '' ) ;
1714+ const downloadUrl = `${ baseUrl } /${ entityType } /${ encodeURIComponent ( entityId ) } /bulk-data/${ encodeURIComponent ( category ) } /${ encodeURIComponent ( fileId ) } ` ;
16701715 const response = await fetch ( downloadUrl ) ;
16711716 if ( ! response . ok ) return null ;
16721717 const blob = await response . blob ( ) ;
0 commit comments