@@ -219,8 +219,6 @@ export function ApplicationDetail({ app, onBack, renderWorkload, topology, topol
219219 [ app . workloads ] ,
220220 )
221221 const overall = worstHealth ( [ app . health , ...workloads . map ( ( w ) => w . health ) ] )
222- const verdictTone = HEALTH_META [ overall ] . pill
223- const verdictLabel = HEALTH_META [ overall ] . label
224222 const workloadClass = workloadClassOf ( app . workload_class )
225223 const versions = useMemo ( ( ) => Array . from ( new Set ( ( app . versions || [ ] ) . filter ( Boolean ) ) ) , [ app . versions ] )
226224 const ready = workloads . reduce ( ( n , w ) => n + ( w . ready ?? 0 ) , 0 )
@@ -256,6 +254,14 @@ export function ApplicationDetail({ app, onBack, renderWorkload, topology, topol
256254 // the app graph rather than silently rendering a different workload under a
257255 // URL that names the missing one.
258256 const selected = rawSelected !== null && ! selectedWorkload ? null : rawSelected
257+ const focusedWorkload = appGraphAvailable && selectedWorkload ? selectedWorkload : null
258+ const focusedWorkloadClass = focusedWorkload ? workloadClassOf ( focusedWorkload . workload_class ) : null
259+ const headerHealth = focusedWorkload ? healthOf ( focusedWorkload . health ) : overall
260+ const verdictTone = HEALTH_META [ headerHealth ] . pill
261+ const verdictLabel = HEALTH_META [ headerHealth ] . label
262+ const headerReady = focusedWorkload ? focusedWorkload . ready : ready
263+ const headerDesired = focusedWorkload ? focusedWorkload . desired : desired
264+ const focusedVersion = focusedWorkload ?. appVersion || focusedWorkload ?. version
259265 const showBatchActivityPanel = batchActivity . length > 0 && ( workloads . length === 1 || ( appGraphAvailable && selected === null ) )
260266 useEffect ( ( ) => {
261267 if ( selectedWorkloadKey !== undefined && selectedWorkloadKey !== null && ! selectedWorkload ) {
@@ -330,14 +336,33 @@ export function ApplicationDetail({ app, onBack, renderWorkload, topology, topol
330336 < span className = { `flex h-8 w-8 items-center justify-center rounded-md ring-1 ring-inset ${ verdictTone } ` } >
331337 < Boxes className = "h-4 w-4" aria-hidden />
332338 </ span >
333- < h1 className = "text-2xl font-semibold text-theme-text-primary" > { app . name } </ h1 >
334- < ProvenanceBadge tier = { app . tier } appKey = { app . key } confidence = { app . confidence } />
335- < CategoryChip category = { app . category } addonReason = { app . addonReason } />
336- < ClassBadge workloadClass = { workloadClass } composition = { classCompositionOf ( app ) } />
337- < BatchSignalChip signal = { batchSignal } />
339+ { focusedWorkload ? (
340+ < >
341+ < div className = "min-w-0" >
342+ < div className = "mb-0.5 flex min-w-0 items-center gap-1.5 text-xs text-theme-text-tertiary" >
343+ < button type = "button" onClick = { ( ) => setSelected ( null ) } className = "truncate hover:text-theme-text-primary" title = "Back to application overview" >
344+ { app . name }
345+ </ button >
346+ < span aria-hidden > /</ span >
347+ < span className = "truncate" > { focusedWorkload . kind } </ span >
348+ </ div >
349+ < h1 className = "truncate text-2xl font-semibold text-theme-text-primary" > { focusedWorkload . name } </ h1 >
350+ </ div >
351+ < span className = { `${ CHIP } ${ CHIP_TONE . neutral } ` } > { focusedWorkload . kind } </ span >
352+ { focusedWorkloadClass && < ClassBadge workloadClass = { focusedWorkloadClass } /> }
353+ </ >
354+ ) : (
355+ < >
356+ < h1 className = "text-2xl font-semibold text-theme-text-primary" > { app . name } </ h1 >
357+ < ProvenanceBadge tier = { app . tier } appKey = { app . key } confidence = { app . confidence } />
358+ < CategoryChip category = { app . category } addonReason = { app . addonReason } />
359+ < ClassBadge workloadClass = { workloadClass } composition = { classCompositionOf ( app ) } />
360+ < BatchSignalChip signal = { batchSignal } />
361+ </ >
362+ ) }
338363 < div className = "ml-auto flex flex-wrap items-center justify-end gap-2" >
339364 < span className = { `inline-flex items-center gap-2 rounded-md px-2.5 py-1 ring-1 ring-inset ${ verdictTone } ` } >
340- < StatusDot tone = { mapHealthToTone ( overall ) } />
365+ < StatusDot tone = { mapHealthToTone ( headerHealth ) } />
341366 < span className = "text-sm font-semibold" > { verdictLabel } </ span >
342367 </ span >
343368 { restartSignal && (
@@ -359,43 +384,68 @@ export function ApplicationDetail({ app, onBack, renderWorkload, topology, topol
359384
360385 { /* Context strip */ }
361386 < div className = "flex flex-wrap items-center gap-x-5 gap-y-2 border-b border-theme-border px-4 py-2 sm:px-6" >
362- { identityInstances && identityInstances . length > 1 ? (
363- // The Environment fact IS the switcher when this app runs in several
364- // envs — prominent, in existing header space, no extra row. Inline
365- // pills for a handful; a picker beyond that (scales to ~any count).
366- < div className = "flex min-w-0 items-center gap-1.5" >
367- < span className = "text-[10px] uppercase tracking-wide text-theme-text-tertiary" > Environment</ span >
368- < EnvSwitcher identityKey = { app . identity ?. key ?? '' } instances = { identityInstances } activeKey = { activeInstanceKey ?? app . key } onSwitch = { onSwitchInstance } />
369- </ div >
370- ) : env ? (
371- < ContextFact label = "Environment" >
372- { inferred ? (
373- < Tooltip content = { `Inferred from namespace "${ namespace || env } " — confirm with an environment label.` } delay = { 150 } >
374- < span className = "italic" > ~{ env } </ span >
375- </ Tooltip >
376- ) : (
377- env
387+ { focusedWorkload ? (
388+ < >
389+ < ContextFact label = "Application" >
390+ < button type = "button" onClick = { ( ) => setSelected ( null ) } className = "hover:text-theme-text-primary" >
391+ { app . name }
392+ </ button >
393+ </ ContextFact >
394+ { focusedWorkload . namespace && (
395+ < ContextFact label = "Namespace" >
396+ < span className = "font-mono" > { focusedWorkload . namespace } </ span >
397+ </ ContextFact >
378398 ) }
379- </ ContextFact >
380- ) : null }
381- { namespace ? (
382- < ContextFact label = "Namespace" >
383- < span className = "font-mono" > { namespace } </ span >
384- </ ContextFact >
385- ) : namespaces . length > 1 ? (
386- < ContextFact label = "Namespaces" >
387- < Tooltip content = { namespaces . join ( ', ' ) } delay = { 150 } >
388- < span > { namespaces . length } namespaces</ span >
389- </ Tooltip >
390- </ ContextFact >
391- ) : null }
392- < ContextFact label = "Ready" >
393- < ReadyBar ready = { ready } desired = { desired } width = "w-16" />
394- </ ContextFact >
395- { ( app . appVersion || versions . length > 0 ) && (
396- < ContextFact label = "Version" >
397- < VersionInfo app = { app } variant = "fact" />
398- </ ContextFact >
399+ < ContextFact label = "Ready" >
400+ < ReadyBar ready = { headerReady } desired = { headerDesired } width = "w-16" />
401+ </ ContextFact >
402+ { focusedVersion && (
403+ < ContextFact label = "Version" >
404+ < span className = "font-mono" > { midTruncate ( focusedVersion , 32 ) } </ span >
405+ </ ContextFact >
406+ ) }
407+ </ >
408+ ) : (
409+ < >
410+ { identityInstances && identityInstances . length > 1 ? (
411+ // The Environment fact IS the switcher when this app runs in several
412+ // envs — prominent, in existing header space, no extra row. Inline
413+ // pills for a handful; a picker beyond that (scales to ~any count).
414+ < div className = "flex min-w-0 items-center gap-1.5" >
415+ < span className = "text-[10px] uppercase tracking-wide text-theme-text-tertiary" > Environment</ span >
416+ < EnvSwitcher identityKey = { app . identity ?. key ?? '' } instances = { identityInstances } activeKey = { activeInstanceKey ?? app . key } onSwitch = { onSwitchInstance } />
417+ </ div >
418+ ) : env ? (
419+ < ContextFact label = "Environment" >
420+ { inferred ? (
421+ < Tooltip content = { `Inferred from namespace "${ namespace || env } " — confirm with an environment label.` } delay = { 150 } >
422+ < span className = "italic" > ~{ env } </ span >
423+ </ Tooltip >
424+ ) : (
425+ env
426+ ) }
427+ </ ContextFact >
428+ ) : null }
429+ { namespace ? (
430+ < ContextFact label = "Namespace" >
431+ < span className = "font-mono" > { namespace } </ span >
432+ </ ContextFact >
433+ ) : namespaces . length > 1 ? (
434+ < ContextFact label = "Namespaces" >
435+ < Tooltip content = { namespaces . join ( ', ' ) } delay = { 150 } >
436+ < span > { namespaces . length } namespaces</ span >
437+ </ Tooltip >
438+ </ ContextFact >
439+ ) : null }
440+ < ContextFact label = "Ready" >
441+ < ReadyBar ready = { headerReady } desired = { headerDesired } width = "w-16" />
442+ </ ContextFact >
443+ { ( app . appVersion || versions . length > 0 ) && (
444+ < ContextFact label = "Version" >
445+ < VersionInfo app = { app } variant = "fact" />
446+ </ ContextFact >
447+ ) }
448+ </ >
399449 ) }
400450 </ div >
401451
@@ -412,41 +462,49 @@ export function ApplicationDetail({ app, onBack, renderWorkload, topology, topol
412462 runtime. A single-workload app skips straight to its runtime. */ }
413463 { workloads . length > 1 ? (
414464 < div className = "flex h-[calc(100vh-13rem)] min-h-[480px]" >
415- < WorkloadRail
416- workloads = { workloads }
417- colorByWorkload = { ownership ?. colorByWorkload ?? null }
418- showAllEntry = { appGraphAvailable }
419- // Without an app graph the body always renders a workload (selected ??
420- // workloads[0]); mirror that so the rail marks it active.
421- selectedKey = { appGraphAvailable ? selected : selected ?? ( workloads [ 0 ] ? workloadKey ( workloads [ 0 ] ) : null ) }
422- focusedOwnerId = { focusedOwnerId }
423- onSelect = { setSelected }
424- onFocus = { setFocusedOwnerId }
425- />
426- < div className = "relative min-h-0 flex-1 overflow-hidden bg-theme-surface" >
427- { appGraphAvailable && selected === null ? (
428- < TopologyGraph
429- topology = { appGraph }
430- viewMode = "resources"
431- groupingMode = "namespace"
432- hideGroupHeader
433- onNodeClick = { handleAppNodeClick }
434- showExportButton = { false }
435- focusNodeId = { appGraphFocusId }
465+ { focusedWorkload ? (
466+ < div key = { workloadKey ( focusedWorkload ) } className = "min-h-0 flex-1 overflow-hidden bg-theme-surface" >
467+ { renderWorkload ( focusedWorkload ) }
468+ </ div >
469+ ) : (
470+ < >
471+ < WorkloadRail
472+ workloads = { workloads }
473+ colorByWorkload = { ownership ?. colorByWorkload ?? null }
474+ showAllEntry = { appGraphAvailable }
475+ // Without an app graph the body always renders a workload (selected ??
476+ // workloads[0]); mirror that so the rail marks it active.
477+ selectedKey = { appGraphAvailable ? selected : selected ?? ( workloads [ 0 ] ? workloadKey ( workloads [ 0 ] ) : null ) }
436478 focusedOwnerId = { focusedOwnerId }
437- onNodeHover = { handleNodeHover }
479+ onSelect = { setSelected }
480+ onFocus = { setFocusedOwnerId }
438481 />
439- ) : selected === null && topologyLoading ? (
440- // The graph is this view's landing — hold a loader while the
441- // topology fetch is in flight instead of flashing the first
442- // workload's runtime (which would fire its data fetches too).
443- < PaneLoader label = "Loading topology…" className = "absolute inset-0" />
444- ) : (
445- < div key = { workloadKey ( selectedWorkload ?? workloads [ 0 ] ) } className = "h-full min-h-0" >
446- { renderWorkload ( selectedWorkload ?? workloads [ 0 ] ) }
482+ < div className = "relative min-h-0 flex-1 overflow-hidden bg-theme-surface" >
483+ { appGraphAvailable && selected === null ? (
484+ < TopologyGraph
485+ topology = { appGraph }
486+ viewMode = "resources"
487+ groupingMode = "namespace"
488+ hideGroupHeader
489+ onNodeClick = { handleAppNodeClick }
490+ showExportButton = { false }
491+ focusNodeId = { appGraphFocusId }
492+ focusedOwnerId = { focusedOwnerId }
493+ onNodeHover = { handleNodeHover }
494+ />
495+ ) : selected === null && topologyLoading ? (
496+ // The graph is this view's landing — hold a loader while the
497+ // topology fetch is in flight instead of flashing the first
498+ // workload's runtime (which would fire its data fetches too).
499+ < PaneLoader label = "Loading topology…" className = "absolute inset-0" />
500+ ) : (
501+ < div key = { workloadKey ( selectedWorkload ?? workloads [ 0 ] ) } className = "h-full min-h-0" >
502+ { renderWorkload ( selectedWorkload ?? workloads [ 0 ] ) }
503+ </ div >
504+ ) }
447505 </ div >
448- ) }
449- </ div >
506+ </ >
507+ ) }
450508 </ div >
451509 ) : (
452510 renderRuntime ( workloads [ 0 ] , renderWorkload )
0 commit comments