@@ -218,6 +218,7 @@ const OntologyExplorer: React.FC<OntologyExplorerProps> = ({
218218 scope === 'term' ? contextData ?. data ?. glossary ?. id : undefined ;
219219
220220 const [ loading , setLoading ] = useState ( true ) ;
221+ const [ isLoadingMore , setIsLoadingMore ] = useState ( false ) ;
221222 const [ graphData , setGraphData ] = useState < OntologyGraphData | null > ( null ) ;
222223 const [ assetGraphData , setAssetGraphData ] =
223224 useState < OntologyGraphData | null > ( null ) ;
@@ -1259,8 +1260,64 @@ const OntologyExplorer: React.FC<OntologyExplorerProps> = ({
12591260 Object . entries ( counts ) . slice ( 0 , DATA_MODE_MAX_RENDER_COUNT )
12601261 ) ;
12611262
1263+ const baseGraph = buildGraphFromCounts ( termCounts ) ;
1264+
1265+ const savedGraph = savedModelGraphRef . current ;
1266+ if ( savedGraph && savedGraph . edges . length > 0 ) {
1267+ const fqnSet = new Set (
1268+ baseGraph . nodes
1269+ . map ( ( n ) => n . fullyQualifiedName )
1270+ . filter ( ( fqn ) : fqn is string => Boolean ( fqn ) )
1271+ ) ;
1272+ const uuidToFqn = new Map < string , string > ( ) ;
1273+ savedGraph . nodes . forEach ( ( n ) => {
1274+ if ( n . id && n . fullyQualifiedName ) {
1275+ uuidToFqn . set ( n . id , n . fullyQualifiedName ) ;
1276+ }
1277+ } ) ;
1278+
1279+ const existingEdgeKeys = new Set (
1280+ baseGraph . edges . map ( ( e ) => `${ e . from } -${ e . to } ` )
1281+ ) ;
1282+ const termTermEdges : OntologyEdge [ ] = [ ] ;
1283+
1284+ savedGraph . edges . forEach ( ( edge ) => {
1285+ if ( edge . relationType === 'parentOf' ) {
1286+ return ;
1287+ }
1288+ const fromFqn = uuidToFqn . get ( edge . from ) ;
1289+ const toFqn = uuidToFqn . get ( edge . to ) ;
1290+ if (
1291+ ! fromFqn ||
1292+ ! toFqn ||
1293+ ! fqnSet . has ( fromFqn ) ||
1294+ ! fqnSet . has ( toFqn )
1295+ ) {
1296+ return ;
1297+ }
1298+ const key = `${ fromFqn } -${ toFqn } ` ;
1299+ if ( ! existingEdgeKeys . has ( key ) ) {
1300+ existingEdgeKeys . add ( key ) ;
1301+ termTermEdges . push ( {
1302+ from : fromFqn ,
1303+ to : toFqn ,
1304+ label : edge . label ,
1305+ relationType : edge . relationType ,
1306+ } ) ;
1307+ }
1308+ } ) ;
1309+
1310+ return {
1311+ graphData : {
1312+ nodes : baseGraph . nodes ,
1313+ edges : [ ...baseGraph . edges , ...termTermEdges ] ,
1314+ } ,
1315+ termCounts,
1316+ } ;
1317+ }
1318+
12621319 return {
1263- graphData : buildGraphFromCounts ( termCounts ) ,
1320+ graphData : baseGraph ,
12641321 termCounts,
12651322 } ;
12661323 } ,
@@ -1421,6 +1478,7 @@ const OntologyExplorer: React.FC<OntologyExplorerProps> = ({
14211478 setAssetGraphData ( null ) ;
14221479 setTermAssetCounts ( { } ) ;
14231480 setGraphData ( mergedData ) ;
1481+ lastLoadCompletedRef . current = Date . now ( ) ;
14241482 } catch ( error ) {
14251483 showErrorToast (
14261484 isAxiosError ( error ) ? error : String ( error ) ,
@@ -1539,6 +1597,7 @@ const OntologyExplorer: React.FC<OntologyExplorerProps> = ({
15391597 filters . glossaryIds
15401598 ) ;
15411599 setLoading ( true ) ;
1600+ setGraphData ( null ) ;
15421601 setTermAssetCounts ( { } ) ;
15431602 loadDataModeTerms ( glossaryFilterIds )
15441603 . then (
@@ -1829,7 +1888,7 @@ const OntologyExplorer: React.FC<OntologyExplorerProps> = ({
18291888 }
18301889
18311890 isLoadingMoreRef . current = true ;
1832- setLoading ( true ) ;
1891+ setIsLoadingMore ( true ) ;
18331892 loadNextTermPage ( )
18341893 . then ( ( terms ) => {
18351894 const newPageData = buildGraphFromAllTerms ( terms , glossaries ) ;
@@ -1863,7 +1922,7 @@ const OntologyExplorer: React.FC<OntologyExplorerProps> = ({
18631922 . finally ( ( ) => {
18641923 lastLoadCompletedRef . current = Date . now ( ) ;
18651924 isLoadingMoreRef . current = false ;
1866- setLoading ( false ) ;
1925+ setIsLoadingMore ( false ) ;
18671926 } ) ;
18681927 } , [
18691928 explorationMode ,
@@ -2009,6 +2068,126 @@ const OntologyExplorer: React.FC<OntologyExplorerProps> = ({
20092068 return items ;
20102069 } , [ graphDataToShow , dataSource , explorationMode , t ] ) ;
20112070
2071+ const renderGraphContent = ( ) => {
2072+ if ( loading && ! graphDataToShow ) {
2073+ return (
2074+ < div
2075+ className = "tw:absolute tw:inset-0 tw:z-3 tw:flex tw:flex-col tw:items-center tw:justify-center"
2076+ data-testid = "ontology-graph-loading" >
2077+ < div
2078+ aria-label = { t ( 'label.loading' ) }
2079+ className = "tw:h-10 tw:w-10 tw:animate-spin tw:rounded-full tw:border-2 tw:border-border-secondary tw:border-t-(--color-bg-brand-solid)"
2080+ role = "status"
2081+ />
2082+ < Typography as = "p" className = "tw:mt-4 tw:text-tertiary" >
2083+ { t ( 'label.loading-graph' ) }
2084+ </ Typography >
2085+ </ div >
2086+ ) ;
2087+ }
2088+
2089+ if (
2090+ isHierarchyView &&
2091+ hierarchyGraphData !== null &&
2092+ hierarchyGraphData . edges . length === 0
2093+ ) {
2094+ return (
2095+ < div
2096+ className = "tw:absolute tw:inset-0 tw:z-3 tw:flex tw:flex-col tw:items-center tw:justify-center"
2097+ data-testid = "ontology-graph-hierarchy-empty" >
2098+ < Typography as = "p" className = "tw:text-center tw:text-tertiary" >
2099+ { t ( 'message.no-hierarchical-relations-found' ) }
2100+ </ Typography >
2101+ </ div >
2102+ ) ;
2103+ }
2104+
2105+ if ( ! graphDataToShow || graphDataToShow . nodes . length === 0 ) {
2106+ const hasActiveFilter =
2107+ withoutOntologyAutocompleteAll ( filters . glossaryIds ) . length > 0 ||
2108+ withoutOntologyAutocompleteAll ( filters . relationTypes ) . length > 0 ;
2109+
2110+ return (
2111+ < div
2112+ className = "tw:absolute tw:inset-0 tw:z-3 tw:flex tw:flex-col tw:items-center tw:justify-center"
2113+ data-testid = "ontology-graph-empty" >
2114+ < Typography as = "p" className = "tw:text-center tw:text-tertiary" >
2115+ { hasActiveFilter
2116+ ? t ( 'message.no-data-available-for-selected-filter' )
2117+ : t ( 'message.no-glossary-terms-found' ) }
2118+ </ Typography >
2119+ </ div >
2120+ ) ;
2121+ }
2122+
2123+ return (
2124+ < >
2125+ { filters . searchQuery . trim ( ) ? (
2126+ < div
2127+ aria-hidden
2128+ className = "tw:pointer-events-none tw:absolute tw:inset-0 tw:z-1 tw:bg-gray-950/6"
2129+ />
2130+ ) : null }
2131+ < div className = "tw:relative tw:z-1 tw:h-full tw:w-full tw:min-h-0" >
2132+ < OntologyGraph
2133+ edges = { graphDataToShow . edges }
2134+ expandedTermIds = {
2135+ explorationMode === 'data' ? expandedTermIds : undefined
2136+ }
2137+ explorationMode = { isHierarchyView ? 'hierarchy' : explorationMode }
2138+ focusNodeId = {
2139+ explorationMode === 'data'
2140+ ? selectedNode ?. id ?? entityId
2141+ : entityId
2142+ }
2143+ glossaryColorMap = { glossaryColorMap }
2144+ graphSearchHighlight = { graphSearchHighlight }
2145+ hierarchyCombos = {
2146+ isHierarchyView && hierarchyGraphData
2147+ ? hierarchyGraphData . combos . map ( ( c ) => ( {
2148+ id : c . id ,
2149+ label : c . label ,
2150+ glossaryId : c . glossaryId ,
2151+ } ) )
2152+ : undefined
2153+ }
2154+ nodePositions = { hierarchyBakedPositions }
2155+ nodes = { graphDataToShow . nodes }
2156+ ref = { graphRef }
2157+ selectedNodeId = {
2158+ explorationMode === 'data' && expandedTermIds . size > 1
2159+ ? null
2160+ : selectedNode ?. id
2161+ }
2162+ settings = { settings }
2163+ onNodeClick = { handleGraphNodeClick }
2164+ onNodeContextMenu = { handleGraphNodeContextMenu }
2165+ onNodeDoubleClick = { handleGraphNodeDoubleClick }
2166+ onPaneClick = { handleGraphPaneClick }
2167+ onScrollNearEdge = { handleScrollNearEdge }
2168+ />
2169+ { isLoadingMore && (
2170+ < >
2171+ < div className = "tw:absolute tw:inset-0 tw:z-1 tw:cursor-wait" />
2172+ < div className = "tw:pointer-events-none tw:absolute tw:bottom-20 tw:left-1/2 tw:z-2 tw:-translate-x-1/2" >
2173+ < div className = "tw:flex tw:items-center tw:gap-2 tw:rounded-full tw:border tw:border-utility-gray-blue-100 tw:bg-white tw:px-4 tw:py-2 tw:shadow-md" >
2174+ < div
2175+ aria-label = { t ( 'label.loading' ) }
2176+ className = "tw:h-4 tw:w-4 tw:animate-spin tw:rounded-full tw:border-2 tw:border-border-secondary tw:border-t-(--color-bg-brand-solid)"
2177+ role = "status"
2178+ />
2179+ < Typography size = "text-sm" weight = "medium" >
2180+ { t ( 'label.loading-more-terms' ) }
2181+ </ Typography >
2182+ </ div >
2183+ </ div >
2184+ </ >
2185+ ) }
2186+ </ div >
2187+ </ >
2188+ ) ;
2189+ } ;
2190+
20122191 return (
20132192 < div
20142193 className = { classNames (
@@ -2146,93 +2325,7 @@ const OntologyExplorer: React.FC<OntologyExplorerProps> = ({
21462325 ONTOLOGY_GRAPH_BACKDROP_CLASS ,
21472326 'tw:overflow-hidden'
21482327 ) } >
2149- { loading ? (
2150- < div
2151- className = "tw:absolute tw:inset-0 tw:z-3 tw:flex tw:flex-col tw:items-center tw:justify-center"
2152- data-testid = "ontology-graph-loading" >
2153- < div
2154- aria-label = { t ( 'label.loading' ) }
2155- className = "tw:h-10 tw:w-10 tw:animate-spin tw:rounded-full tw:border-2 tw:border-border-secondary tw:border-t-(--color-bg-brand-solid)"
2156- role = "status"
2157- />
2158- < Typography as = "p" className = "tw:mt-4 tw:text-tertiary" >
2159- { t ( 'label.loading-graph' ) }
2160- </ Typography >
2161- </ div >
2162- ) : isHierarchyView &&
2163- hierarchyGraphData !== null &&
2164- hierarchyGraphData . edges . length === 0 ? (
2165- < div
2166- className = "tw:absolute tw:inset-0 tw:z-3 tw:flex tw:flex-col tw:items-center tw:justify-center"
2167- data-testid = "ontology-graph-hierarchy-empty" >
2168- < Typography as = "p" className = "tw:text-center tw:text-tertiary" >
2169- { t ( 'message.no-hierarchical-relations-found' ) }
2170- </ Typography >
2171- </ div >
2172- ) : ! graphDataToShow || graphDataToShow . nodes . length === 0 ? (
2173- < div
2174- className = "tw:absolute tw:inset-0 tw:z-3 tw:flex tw:flex-col tw:items-center tw:justify-center"
2175- data-testid = "ontology-graph-empty" >
2176- < Typography as = "p" className = "tw:text-center tw:text-tertiary" >
2177- { withoutOntologyAutocompleteAll ( filters . glossaryIds ) . length >
2178- 0 ||
2179- withoutOntologyAutocompleteAll ( filters . relationTypes ) . length >
2180- 0
2181- ? t ( 'message.no-data-available-for-selected-filter' )
2182- : t ( 'message.no-glossary-terms-found' ) }
2183- </ Typography >
2184- </ div >
2185- ) : (
2186- < >
2187- { filters . searchQuery . trim ( ) ? (
2188- < div
2189- aria-hidden
2190- className = "tw:pointer-events-none tw:absolute tw:inset-0 tw:z-1 tw:bg-gray-950/6"
2191- />
2192- ) : null }
2193- < div className = "tw:relative tw:z-1 tw:h-full tw:w-full tw:min-h-0" >
2194- < OntologyGraph
2195- edges = { graphDataToShow . edges }
2196- expandedTermIds = {
2197- explorationMode === 'data' ? expandedTermIds : undefined
2198- }
2199- explorationMode = {
2200- isHierarchyView ? 'hierarchy' : explorationMode
2201- }
2202- focusNodeId = {
2203- explorationMode === 'data'
2204- ? selectedNode ?. id ?? entityId
2205- : entityId
2206- }
2207- glossaryColorMap = { glossaryColorMap }
2208- graphSearchHighlight = { graphSearchHighlight }
2209- hierarchyCombos = {
2210- isHierarchyView && hierarchyGraphData
2211- ? hierarchyGraphData . combos . map ( ( c ) => ( {
2212- id : c . id ,
2213- label : c . label ,
2214- glossaryId : c . glossaryId ,
2215- } ) )
2216- : undefined
2217- }
2218- nodePositions = { hierarchyBakedPositions }
2219- nodes = { graphDataToShow . nodes }
2220- ref = { graphRef }
2221- selectedNodeId = {
2222- explorationMode === 'data' && expandedTermIds . size > 1
2223- ? null
2224- : selectedNode ?. id
2225- }
2226- settings = { settings }
2227- onNodeClick = { handleGraphNodeClick }
2228- onNodeContextMenu = { handleGraphNodeContextMenu }
2229- onNodeDoubleClick = { handleGraphNodeDoubleClick }
2230- onPaneClick = { handleGraphPaneClick }
2231- onScrollNearEdge = { handleScrollNearEdge }
2232- />
2233- </ div >
2234- </ >
2235- ) }
2328+ { renderGraphContent ( ) }
22362329 </ div >
22372330
22382331 { selectedNode && (
0 commit comments