@skyhook-io/k8s-ui 1.7.11 → 1.7.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/package.json +1 -1
  2. package/src/components/applications/AppChips.tsx +109 -0
  3. package/src/components/applications/AppTooltips.tsx +199 -0
  4. package/src/components/applications/ApplicationDetail.tsx +671 -0
  5. package/src/components/applications/ApplicationsList.tsx +569 -0
  6. package/src/components/applications/ReadyBar.tsx +22 -0
  7. package/src/components/applications/index.ts +8 -0
  8. package/src/components/audit/AuditFindingsTable.tsx +3 -25
  9. package/src/components/dock/TerminalTab.tsx +4 -2
  10. package/src/components/gitops/insights/GitOpsInsightViews.tsx +1 -0
  11. package/src/components/issues/IssuesView.tsx +64 -17
  12. package/src/components/issues/index.ts +1 -1
  13. package/src/components/issues/issues.test.ts +4 -4
  14. package/src/components/issues/severity.ts +5 -0
  15. package/src/components/issues/types.ts +43 -0
  16. package/src/components/logs/LogCore.tsx +13 -2
  17. package/src/components/logs/LogToolbarSelects.tsx +6 -2
  18. package/src/components/logs/LogsViewer.tsx +66 -10
  19. package/src/components/logs/WorkloadLogsViewer.tsx +68 -13
  20. package/src/components/logs/useLogStream.ts +41 -3
  21. package/src/components/resources/ResourcesView.tsx +550 -52
  22. package/src/components/resources/column-filter-serialization.test.ts +26 -0
  23. package/src/components/resources/get-default-container-name.test.ts +31 -0
  24. package/src/components/resources/renderers/DeviceClassRenderer.tsx +49 -0
  25. package/src/components/resources/renderers/NodeRenderer.tsx +7 -0
  26. package/src/components/resources/renderers/NvidiaClusterPolicyRenderer.tsx +57 -0
  27. package/src/components/resources/renderers/NvidiaDriverRenderer.tsx +47 -0
  28. package/src/components/resources/renderers/PodRenderer.tsx +2 -2
  29. package/src/components/resources/renderers/ResourceClaimRenderer.tsx +118 -0
  30. package/src/components/resources/renderers/ResourceClaimTemplateRenderer.tsx +39 -0
  31. package/src/components/resources/renderers/ResourceSliceRenderer.tsx +72 -0
  32. package/src/components/resources/renderers/WorkloadRenderer.tsx +5 -4
  33. package/src/components/resources/renderers/dra-cells.tsx +80 -0
  34. package/src/components/resources/renderers/index.ts +8 -0
  35. package/src/components/resources/renderers/nvidia-cells.tsx +43 -0
  36. package/src/components/resources/resource-utils-dra.ts +90 -0
  37. package/src/components/resources/resource-utils-nvidia.ts +63 -0
  38. package/src/components/resources/resource-utils.ts +62 -4
  39. package/src/components/shared/DetailShell.tsx +14 -7
  40. package/src/components/shared/EditableYamlView.tsx +37 -17
  41. package/src/components/shared/ResourceActionsBar.tsx +5 -4
  42. package/src/components/shared/ResourceRendererDispatch.test.tsx +103 -0
  43. package/src/components/shared/ResourceRendererDispatch.tsx +27 -2
  44. package/src/components/timeline/TimelineList.tsx +3 -32
  45. package/src/components/timeline/TimelineSwimlanes.tsx +3 -31
  46. package/src/components/topology/K8sResourceNode.tsx +26 -5
  47. package/src/components/topology/TopologyGraph.tsx +102 -3
  48. package/src/components/topology/layout.ts +36 -11
  49. package/src/components/ui/CenteredEmpty.tsx +27 -0
  50. package/src/components/ui/ConfirmDialog.tsx +1 -1
  51. package/src/components/ui/SearchBox.tsx +85 -0
  52. package/src/components/ui/drawer-components.tsx +23 -1
  53. package/src/components/ui/index.ts +1 -0
  54. package/src/components/workload/WorkloadView.tsx +167 -33
  55. package/src/components/workload/index.ts +1 -1
  56. package/src/hooks/useKeyboardShortcuts.tsx +3 -1
  57. package/src/index.ts +4 -0
  58. package/src/types/core.ts +1 -0
  59. package/src/utils/api-resources.ts +21 -0
  60. package/src/utils/applications.test.ts +207 -0
  61. package/src/utils/applications.ts +674 -0
  62. package/src/utils/custom-columns.test.ts +111 -0
  63. package/src/utils/custom-columns.ts +49 -0
  64. package/src/utils/extended-resources.test.ts +152 -0
  65. package/src/utils/extended-resources.ts +121 -0
  66. package/src/utils/format.ts +11 -0
  67. package/src/utils/index.ts +3 -0
  68. package/src/utils/topology-neighborhood.test.ts +185 -0
  69. package/src/utils/topology-neighborhood.ts +262 -0
  70. package/src/utils/workload-colors.ts +36 -0
@@ -1,7 +1,8 @@
1
1
  import { useState } from 'react'
2
2
  import { ChevronRight, Copy, Check, Tag, AlertTriangle, CheckCircle, ExternalLink, Layers, X, Minus } from 'lucide-react'
3
3
  import { clsx } from 'clsx'
4
- import { formatAge, formatDuration } from '../resources/resource-utils'
4
+ import { formatAge, formatDuration, formatResources } from '../resources/resource-utils'
5
+ import { getEffectiveResources } from '../../utils/extended-resources'
5
6
  import { Tooltip } from './Tooltip'
6
7
  import { getKindColorClass } from '../ui/Badge'
7
8
 
@@ -489,6 +490,18 @@ export function MetadataSection({ data }: { data: any }) {
489
490
  )
490
491
  }
491
492
 
493
+ // Templates never get apiserver request-defaulting, so render the effective
494
+ // view (requests, falling back to limits) — limits-only GPU specs included.
495
+ function ContainerResourcesLine({ resources }: { resources: any }) {
496
+ const effective = getEffectiveResources(resources)
497
+ if (Object.keys(effective).length === 0) return null
498
+ return (
499
+ <div className="text-xs text-theme-text-tertiary mt-1" title="Effective requests (requests, falling back to limits)">
500
+ Resources: {formatResources(effective)}
501
+ </div>
502
+ )
503
+ }
504
+
492
505
  export function PodTemplateSection({ template }: { template: any }) {
493
506
  if (!template) return null
494
507
  const initContainers = template.spec?.initContainers || []
@@ -508,6 +521,7 @@ export function PodTemplateSection({ template }: { template: any }) {
508
521
  $ {[...(c.command || []), ...(c.args || [])].join(' ')}
509
522
  </div>
510
523
  )}
524
+ <ContainerResourcesLine resources={c.resources} />
511
525
  </div>
512
526
  ))}
513
527
  <div className="text-xs text-theme-text-tertiary font-medium uppercase tracking-wide mt-3">Containers</div>
@@ -522,6 +536,7 @@ export function PodTemplateSection({ template }: { template: any }) {
522
536
  Ports: {c.ports.map((p: any) => `${p.name ? `${p.name}: ` : ''}${p.containerPort}/${p.protocol || 'TCP'}`).join(', ')}
523
537
  </div>
524
538
  )}
539
+ <ContainerResourcesLine resources={c.resources} />
525
540
  </div>
526
541
  ))}
527
542
  </div>
@@ -664,6 +679,9 @@ export function formatKindName(kind: string): string {
664
679
  horizontalpodautoscalers: 'HPA', nodes: 'Node', namespaces: 'Namespace',
665
680
  persistentvolumeclaims: 'PVC', persistentvolumes: 'PV',
666
681
  httpproxies: 'HTTPProxy',
682
+ resourceclaims: 'ResourceClaim', resourceclaimtemplates: 'ResourceClaimTemplate',
683
+ deviceclasses: 'DeviceClass', resourceslices: 'ResourceSlice',
684
+ clusterpolicies: 'ClusterPolicy', nvidiadrivers: 'NVIDIADriver',
667
685
  }
668
686
  if (names[k]) return names[k]
669
687
 
@@ -726,6 +744,7 @@ export function RelatedResourcesSection({ relationships, onNavigate }: RelatedRe
726
744
  (relationships.scalers && relationships.scalers.length > 0) ||
727
745
  (relationships.pdbs && relationships.pdbs.length > 0) ||
728
746
  (relationships.networkPolicies && relationships.networkPolicies.length > 0) ||
747
+ (relationships.resourceClaims && relationships.resourceClaims.length > 0) ||
729
748
  relationships.scaleTarget
730
749
 
731
750
  if (!hasRelationships) return null
@@ -772,6 +791,9 @@ export function RelatedResourcesSection({ relationships, onNavigate }: RelatedRe
772
791
  {relationships.networkPolicies && relationships.networkPolicies.length > 0 && (
773
792
  <RelationshipGroup label="Network Policies" refs={dedupeRefs(relationships.networkPolicies)} onNavigate={onNavigate} />
774
793
  )}
794
+ {relationships.resourceClaims && relationships.resourceClaims.length > 0 && (
795
+ <RelationshipGroup label="Resource Claims" refs={dedupeRefs(relationships.resourceClaims)} onNavigate={onNavigate} />
796
+ )}
775
797
  {relationships.scaleTarget && (
776
798
  <RelationshipGroup label="Scale Target" refs={[relationships.scaleTarget]} onNavigate={onNavigate} />
777
799
  )}
@@ -6,6 +6,7 @@ export type { MiddleEllipsisProps } from './MiddleEllipsis'
6
6
  export { EmptyState } from './EmptyState'
7
7
  export type { EmptyStateTone, EmptyStateVariant } from './EmptyState'
8
8
  export { FetchResult } from './FetchResult'
9
+ export { SearchBox } from './SearchBox'
9
10
  export { FilterPill } from './FilterPill'
10
11
  export type { FilterPillTone } from './FilterPill'
11
12
  export { StatusDot, mapHealthToTone } from './status-tone'
@@ -3,6 +3,7 @@ import { flushSync } from 'react-dom'
3
3
  import { useRefreshAnimation } from '../../hooks/useRefreshAnimation'
4
4
  import { startViewTransitionSafe } from '../../utils/view-transition'
5
5
  import { FetchResult } from '../ui/FetchResult'
6
+ import { PaneLoader } from '../ui/PaneLoader'
6
7
  import { useRegisterShortcuts } from '../../hooks/useKeyboardShortcuts'
7
8
  import { clsx } from 'clsx'
8
9
  import {
@@ -19,11 +20,14 @@ import {
19
20
  Maximize2,
20
21
  X,
21
22
  BarChart3,
23
+ Network,
22
24
  } from 'lucide-react'
23
- import type { TimelineEvent, ResourceRef, Relationships, SelectedResource, ResolvedEnvFrom } from '../../types'
25
+ import type { TimelineEvent, ResourceRef, Relationships, SelectedResource, ResolvedEnvFrom, Topology, TopologyNode } from '../../types'
24
26
  import type { GitOpsStatus } from '../../types/gitops'
25
27
  import type { NavigateToResource } from '../../utils/navigation'
26
- import { refToSelectedResource, pluralToKind } from '../../utils/navigation'
28
+ import { refToSelectedResource, pluralToKind, kindToPlural, apiVersionToGroup } from '../../utils/navigation'
29
+ import { neighborhoodFor, seedNodeIds } from '../../utils/topology-neighborhood'
30
+ import { TopologyGraph } from '../topology/TopologyGraph'
27
31
  import { gitOpsOwnerFromRelationships, type GitOpsOwnerRef } from '../../utils/gitops-owner'
28
32
  import { gitOpsRouteForResource } from '../../utils/gitops-route'
29
33
  import { isChangeEvent, isHistoricalEvent } from '../../types'
@@ -48,8 +52,10 @@ import { ResourceRendererDispatch, getResourceStatus, type RendererOverrides } f
48
52
  import { DetailShell, type DetailShellTab } from '../shared/DetailShell'
49
53
  import { HelmManagedByChip, ManagedByChip, type HelmOwnerRef } from '../shared/ManagedByChip'
50
54
  import { getKindColorOutline, formatKindName } from '../ui/drawer-components'
55
+ import { midTruncate } from '../../utils/format'
51
56
 
52
- type TabType = 'overview' | 'timeline' | 'logs' | 'metrics' | 'yaml'
57
+ export type WorkloadTabType = 'overview' | 'topology' | 'timeline' | 'logs' | 'metrics' | 'yaml'
58
+ type TabType = WorkloadTabType
53
59
 
54
60
  // ============================================================================
55
61
  // MAIN WORKLOAD VIEW — presentation only, data injected via props
@@ -81,11 +87,16 @@ interface WorkloadViewProps {
81
87
  * the Escape shortcut.
82
88
  */
83
89
  breadcrumb?: ReactNode
90
+ /** Suppress the standalone back arrow — for embeddings where "back" has no
91
+ * meaningful target (a single-workload app has no app graph to return to). */
92
+ hideBackButton?: boolean
84
93
  /**
85
94
  * Controls injected into the shell's tab-row scope slot — e.g. a cluster /
86
95
  * workload picker in Radar Cloud. Absent in standalone Radar.
87
96
  */
88
97
  scopeControls?: ReactNode
98
+ /** Hide WorkloadView's own breadcrumb/identity header when a host page owns that chrome. */
99
+ compactHeader?: boolean
89
100
 
90
101
  // ── Data (injected by wrapper) ──────────────────────────────────────────
91
102
  /** The resource data object */
@@ -107,8 +118,8 @@ interface WorkloadViewProps {
107
118
  allEvents?: TimelineEvent[]
108
119
  /** Whether timeline events are loading */
109
120
  eventsLoading?: boolean
110
- /** Topology data for hierarchy building */
111
- topology?: any
121
+ /** Topology data for hierarchy building + the Topology tab's neighborhood. */
122
+ topology?: Topology
112
123
  resourceFocusedK8sEvents?: TimelineEvent[]
113
124
  resourceFocusedUpdates?: TimelineEvent[]
114
125
  resourceFocusedEventsLoading?: boolean
@@ -118,6 +129,8 @@ interface WorkloadViewProps {
118
129
  // ── Capabilities ─────────────────────────────────────────────────────────
119
130
  /** Whether secrets can be updated */
120
131
  canUpdateSecrets?: boolean
132
+ /** Whether YAML editing should be disabled for read-only host surfaces. */
133
+ readOnlyYaml?: boolean
121
134
 
122
135
  // ── Mutations ────────────────────────────────────────────────────────────
123
136
  /** Update a resource from YAML */
@@ -183,6 +196,12 @@ interface WorkloadViewProps {
183
196
  }) => ReactNode
184
197
  /** Render the metrics tab content */
185
198
  renderMetricsTab?: (props: { kind: string; namespace: string; name: string }) => ReactNode
199
+ /** Render a read-only YAML view for a related object from the workload's
200
+ * neighborhood. Providing this turns the YAML tab into an object explorer
201
+ * (rail of the workload + its Services/config/policies/pods); omitting it
202
+ * keeps the single-manifest YAML tab. Injected because resource fetching
203
+ * lives host-side. */
204
+ renderRelatedYaml?: (ref: { kind: string; namespace: string; name: string; group?: string }) => ReactNode
186
205
  /** Whether metrics are available for this resource kind */
187
206
  isMetricsAvailable?: (kind: string, resource: any) => boolean
188
207
  /** Render extra content at the bottom of the overview tab (e.g. audit findings) */
@@ -218,7 +237,9 @@ export function WorkloadView({
218
237
  initialTab,
219
238
  group,
220
239
  breadcrumb,
240
+ hideBackButton,
221
241
  scopeControls,
242
+ compactHeader,
222
243
  // Data
223
244
  resource,
224
245
  relationships,
@@ -237,6 +258,7 @@ export function WorkloadView({
237
258
  resourceFocusedUpdatesError = null,
238
259
  // Capabilities
239
260
  canUpdateSecrets,
261
+ readOnlyYaml,
240
262
  // Mutations
241
263
  onUpdateResource,
242
264
  isUpdatingResource,
@@ -246,6 +268,7 @@ export function WorkloadView({
246
268
  onTabChange,
247
269
  // Render props
248
270
  renderLogsTab,
271
+ renderRelatedYaml,
249
272
  renderMetricsTab,
250
273
  isMetricsAvailable,
251
274
  // Duplicate
@@ -292,7 +315,6 @@ export function WorkloadView({
292
315
  // startViewTransitionSafe handles the API-missing fallback AND
293
316
  // swallows the InvalidStateError that the API rejects with when
294
317
  // a new transition supersedes an in-flight one (rapid clicks).
295
- // (SKY-833 bug 49)
296
318
  startViewTransitionSafe(() => flushSync(() => setShowYaml(yaml)))
297
319
  }, [])
298
320
 
@@ -317,10 +339,75 @@ export function WorkloadView({
317
339
  })
318
340
  }, [allEvents, topology, kind, namespace, name])
319
341
 
342
+ // Topology tab — the seeded neighborhood around this one workload (its
343
+ // ownership core + attached Services/config/policies), not the whole namespace.
344
+ const neighborhoodSeed = useMemo(() => [{ kind, namespace, name }], [kind, namespace, name])
345
+ const neighborhood = useMemo(
346
+ () => (topology ? neighborhoodFor(topology, neighborhoodSeed) : null),
347
+ [topology, neighborhoodSeed],
348
+ )
349
+ const neighborhoodFocusId = useMemo(
350
+ () => (topology ? seedNodeIds(topology, neighborhoodSeed)[0] : undefined),
351
+ [topology, neighborhoodSeed],
352
+ )
353
+
354
+ // The Topology tab stays visible while topology is loading (the pane shows a
355
+ // loader) and hides only when topology arrived and nothing matched the seed.
356
+ // A deep-linked ?tab=topology that turns out unavailable falls back to
357
+ // overview instead of rendering an empty body under a hidden tab.
358
+ const topologyTabHidden = !!topology && (!neighborhood || neighborhood.nodes.length === 0)
359
+ const effectiveTab: TabType = activeTab === 'topology' && topologyTabHidden ? 'overview' : activeTab
360
+
361
+ // YAML tab object rail — the same neighborhood, as a manifest list: the
362
+ // workload first, then routing → config → policy/scaling → ownership.
363
+ const yamlObjects = useMemo(() => {
364
+ if (!neighborhood) return []
365
+ const order: Record<string, number> = {
366
+ Service: 1, Ingress: 1, HTTPRoute: 1,
367
+ ConfigMap: 2, Secret: 2,
368
+ HorizontalPodAutoscaler: 3, PodDisruptionBudget: 3, NetworkPolicy: 3,
369
+ ReplicaSet: 4, Pod: 5,
370
+ }
371
+ return neighborhood.nodes
372
+ .filter((n) => n.kind !== 'Internet' && n.kind !== 'PodGroup')
373
+ .map((n) => ({
374
+ id: n.id,
375
+ kind: n.kind as string,
376
+ namespace: (n.data?.namespace as string) || namespace,
377
+ name: n.name,
378
+ group: apiVersionToGroup(n.data?.apiVersion as string | undefined),
379
+ primary: n.id === neighborhoodFocusId,
380
+ }))
381
+ .sort((a, b) =>
382
+ a.primary !== b.primary
383
+ ? (a.primary ? -1 : 1)
384
+ : (order[a.kind] ?? 9) - (order[b.kind] ?? 9) || a.kind.localeCompare(b.kind) || a.name.localeCompare(b.name),
385
+ )
386
+ }, [neighborhood, neighborhoodFocusId, namespace])
387
+ // null = the workload's own manifest (the editable one).
388
+ const [yamlObjectId, setYamlObjectId] = useState<string | null>(null)
389
+ useEffect(() => setYamlObjectId(null), [kind, namespace, name])
390
+ const yamlObject = yamlObjectId ? yamlObjects.find((o) => o.id === yamlObjectId) : undefined
391
+ const handleTopologyNodeClick = useCallback(
392
+ (node: TopologyNode) => {
393
+ if (!onNavigateToResource || !node.kind || !node.name) return
394
+ onNavigateToResource({
395
+ kind: kindToPlural(node.kind),
396
+ namespace: (node.data?.namespace as string) || '',
397
+ name: node.name,
398
+ group: apiVersionToGroup(node.data?.apiVersion as string | undefined),
399
+ })
400
+ },
401
+ [onNavigateToResource],
402
+ )
403
+
320
404
  // Flatten events from hierarchy
321
405
  const resourceEvents = useMemo(() => {
322
406
  return getAllEventsFromHierarchy(resourceLanes)
323
407
  }, [resourceLanes])
408
+ const overviewEvents = resourceEvents.length > 0 ? resourceEvents : (resourceFocusedK8sEvents ?? [])
409
+ const overviewEventsLoading = resourceEvents.length > 0 ? eventsLoading : resourceFocusedEventsLoading
410
+ const overviewEventsError = resourceEvents.length > 0 ? undefined : resourceFocusedK8sError
324
411
 
325
412
  // Get pods from relationships and hierarchy
326
413
  const childPods = useMemo(() => {
@@ -446,6 +533,7 @@ export function WorkloadView({
446
533
  const showMetricsTab = isMetricsAvailable ? isMetricsAvailable(kind, resource) : false
447
534
  const tabs: DetailShellTab<TabType>[] = [
448
535
  { id: 'overview', label: 'Overview', icon: <Layers className="w-4 h-4" /> },
536
+ { id: 'topology', label: 'Topology', icon: <Network className="w-4 h-4" />, hidden: topologyTabHidden },
449
537
  {
450
538
  id: 'timeline',
451
539
  label: 'Timeline',
@@ -548,6 +636,7 @@ export function WorkloadView({
548
636
  data={resource}
549
637
  onCopy={(text) => copyToClipboard(text, 'yaml')}
550
638
  copied={copied === 'yaml'}
639
+ readOnly={readOnlyYaml}
551
640
  onSaved={handleSaved}
552
641
  onSave={onUpdateResource}
553
642
  isSaving={isUpdatingResource}
@@ -593,7 +682,7 @@ export function WorkloadView({
593
682
  <DetailShell
594
683
  breadcrumb={breadcrumb}
595
684
  nav={
596
- breadcrumb ? undefined : (
685
+ breadcrumb || hideBackButton ? undefined : (
597
686
  <button
598
687
  onClick={onBack}
599
688
  className="p-1.5 mt-0.5 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded-lg transition-colors"
@@ -673,13 +762,14 @@ export function WorkloadView({
673
762
  </>
674
763
  }
675
764
  tabs={tabs}
676
- activeTab={activeTab}
765
+ activeTab={effectiveTab}
677
766
  onTabChange={handleSetTab}
678
767
  scopeControls={scopeControls}
679
768
  tabStripEnd={<ResourceActionsBar resource={selectedResource} data={resource} hideLogs {...actionsBarProps} />}
680
769
  overlay={saveSuccess ? <SaveSuccessAnimation /> : null}
770
+ compactHeader={compactHeader}
681
771
  >
682
- {activeTab === 'overview' && (
772
+ {effectiveTab === 'overview' && (
683
773
  <InfoTab
684
774
  resource={resource}
685
775
  selectedResource={selectedResource}
@@ -695,15 +785,32 @@ export function WorkloadView({
695
785
  onSwitchToTimeline={() => handleSetTab('timeline')}
696
786
  rendererOverrides={rendererOverrides}
697
787
  resolvedEnvFrom={resolvedEnvFrom}
698
- events={resourceFocusedK8sEvents}
699
- eventsLoading={resourceFocusedEventsLoading}
788
+ events={overviewEvents}
789
+ eventsLoading={overviewEventsLoading}
700
790
  updates={resourceFocusedUpdates}
701
- eventsError={resourceFocusedK8sError}
791
+ eventsError={overviewEventsError}
702
792
  updatesError={resourceFocusedUpdatesError}
703
793
  extraContent={renderOverviewExtra && renderOverviewExtra({ kind, namespace, name })}
704
794
  />
705
795
  )}
706
- {activeTab === 'timeline' && (
796
+ {effectiveTab === 'topology' && (
797
+ <div className="relative h-full min-h-0 w-full">
798
+ {topology ? (
799
+ <TopologyGraph
800
+ topology={neighborhood}
801
+ viewMode="resources"
802
+ groupingMode="namespace"
803
+ hideGroupHeader
804
+ onNodeClick={handleTopologyNodeClick}
805
+ showExportButton={false}
806
+ focusNodeId={neighborhoodFocusId}
807
+ />
808
+ ) : (
809
+ <PaneLoader label="Loading topology…" className="absolute inset-0" />
810
+ )}
811
+ </div>
812
+ )}
813
+ {effectiveTab === 'timeline' && (
707
814
  <EventsTab
708
815
  events={resourceEvents}
709
816
  resourceLanes={resourceLanes}
@@ -716,7 +823,7 @@ export function WorkloadView({
716
823
  onSelectEvent={setSelectedEventId}
717
824
  />
718
825
  )}
719
- {activeTab === 'logs' && renderLogsTab && (
826
+ {effectiveTab === 'logs' && renderLogsTab && (
720
827
  renderLogsTab({
721
828
  kind,
722
829
  apiKind,
@@ -730,29 +837,56 @@ export function WorkloadView({
730
837
  onConsumeInitialContainer: () => setInitialContainer(null),
731
838
  })
732
839
  )}
733
- {activeTab === 'metrics' && renderMetricsTab && (
840
+ {effectiveTab === 'metrics' && renderMetricsTab && (
734
841
  <div className="h-full overflow-auto p-4">
735
842
  {renderMetricsTab({ kind: resource?.kind || kind, namespace, name })}
736
843
  </div>
737
844
  )}
738
- {activeTab === 'yaml' && (
739
- <div className="h-full overflow-auto">
740
- {!resource ? (
741
- <FetchResult loading={resourceLoading} error={resourceError} className="h-32" />
742
- ) : (
743
- <EditableYamlView
744
- resource={selectedResource}
745
- data={resource}
746
- onCopy={(text) => copyToClipboard(text, 'yaml')}
747
- copied={copied === 'yaml'}
748
- onSaved={handleSaved}
749
- onSave={onUpdateResource}
750
- isSaving={isUpdatingResource}
751
- saveError={updateResourceError}
752
- onDuplicate={onDuplicate}
753
- onDownload={onDownload}
754
- />
845
+ {effectiveTab === 'yaml' && (
846
+ <div className="flex h-full min-h-0">
847
+ {renderRelatedYaml && yamlObjects.length > 1 && (
848
+ <div className="flex w-56 shrink-0 flex-col gap-0.5 overflow-y-auto border-r border-theme-border bg-theme-base px-2 py-2">
849
+ <div className="px-1.5 pb-1 pt-0.5 text-[10px] font-medium uppercase tracking-wide text-theme-text-tertiary">Objects</div>
850
+ {yamlObjects.map((o) => {
851
+ const active = o.primary ? yamlObjectId === null : yamlObjectId === o.id
852
+ return (
853
+ <button
854
+ key={o.id}
855
+ type="button"
856
+ onClick={() => setYamlObjectId(o.primary ? null : o.id)}
857
+ className={clsx(
858
+ 'flex w-full flex-col rounded-md px-1.5 py-1.5 text-left transition-colors',
859
+ active ? 'selection selection-ring' : 'hover:bg-theme-hover',
860
+ )}
861
+ >
862
+ <span className="truncate text-xs font-medium text-theme-text-primary">{midTruncate(o.name, 26)}</span>
863
+ <span className="text-[10px] uppercase tracking-wide text-theme-text-tertiary">{formatKindName(o.kind)}</span>
864
+ </button>
865
+ )
866
+ })}
867
+ </div>
755
868
  )}
869
+ <div className="h-full min-w-0 flex-1 overflow-auto">
870
+ {yamlObject && !yamlObject.primary && renderRelatedYaml ? (
871
+ renderRelatedYaml(yamlObject)
872
+ ) : !resource ? (
873
+ <FetchResult loading={resourceLoading} error={resourceError} className="h-32" />
874
+ ) : (
875
+ <EditableYamlView
876
+ resource={selectedResource}
877
+ data={resource}
878
+ onCopy={(text) => copyToClipboard(text, 'yaml')}
879
+ copied={copied === 'yaml'}
880
+ readOnly={readOnlyYaml}
881
+ onSaved={handleSaved}
882
+ onSave={onUpdateResource}
883
+ isSaving={isUpdatingResource}
884
+ saveError={updateResourceError}
885
+ onDuplicate={onDuplicate}
886
+ onDownload={onDownload}
887
+ />
888
+ )}
889
+ </div>
756
890
  </div>
757
891
  )}
758
892
  </DetailShell>
@@ -1240,7 +1374,7 @@ function InfoTab({
1240
1374
  onClick={onSwitchToTimeline}
1241
1375
  className="text-xs text-theme-text-tertiary hover:text-theme-text-secondary transition-colors"
1242
1376
  >
1243
- These are events for this resource only. Switch to the <span className="underline">Timeline</span> tab to see events across all related resources.
1377
+ Showing recent events across this workload. Switch to the <span className="underline">Timeline</span> tab for full history and resource relationships.
1244
1378
  </button>
1245
1379
  )}
1246
1380
  renderSidebar={(sidebarSections) => (
@@ -1,2 +1,2 @@
1
- export { WorkloadView } from './WorkloadView'
1
+ export { WorkloadView, type WorkloadTabType } from './WorkloadView'
2
2
  export { ResourceDetailDrawer } from './ResourceDetailDrawer'
@@ -1,6 +1,6 @@
1
1
  import { createContext, useContext, useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react'
2
2
 
3
- export type ShortcutScope = 'global' | 'topology' | 'resources' | 'timeline' | 'helm' | 'gitops' | 'traffic' | 'drawer'
3
+ export type ShortcutScope = 'global' | 'topology' | 'resources' | 'timeline' | 'helm' | 'gitops' | 'traffic' | 'applications' | 'audit' | 'drawer'
4
4
 
5
5
  // Scope priority: higher number = higher priority (wins when multiple scopes active)
6
6
  const SCOPE_PRIORITY: Record<ShortcutScope, number> = {
@@ -11,6 +11,8 @@ const SCOPE_PRIORITY: Record<ShortcutScope, number> = {
11
11
  helm: 1,
12
12
  gitops: 1,
13
13
  traffic: 1,
14
+ applications: 1,
15
+ audit: 1,
14
16
  drawer: 2,
15
17
  }
16
18
 
package/src/index.ts CHANGED
@@ -53,6 +53,10 @@ export * from './components/issues'
53
53
  // Cluster switcher (shared trigger+dropdown for OSS Radar and Radar Hub)
54
54
  export * from './components/cluster-switcher'
55
55
 
56
+ // Applications (shared host-agnostic list + detail shell for the deployable-
57
+ // software surface; OSS renders single-cluster, Cloud adds the fleet layer)
58
+ export * from './components/applications'
59
+
56
60
  // Compare (ResourceCompareView, CompareResourcePicker, normalize utilities)
57
61
  export * from './components/compare'
58
62
 
package/src/types/core.ts CHANGED
@@ -441,6 +441,7 @@ export interface Relationships {
441
441
  pods?: ResourceRef[]
442
442
  serviceAccount?: ResourceRef // For Pods: derived from pod.spec.serviceAccountName
443
443
  node?: ResourceRef // For scheduled Pods: derived from pod.spec.nodeName
444
+ resourceClaims?: ResourceRef[] // For Pods: DRA ResourceClaims (direct + template-generated)
444
445
  }
445
446
 
446
447
  // Parsed X.509 certificate metadata (from backend cert parsing)
@@ -170,6 +170,27 @@ export function formatGroupName(group: string): string {
170
170
  'sparkoperator.k8s.io': 'Spark',
171
171
  'kubeflow.org': 'Kubeflow',
172
172
  'snapshot.storage.k8s.io': 'Snapshots',
173
+ 'karpenter.sh': 'Karpenter',
174
+ 'karpenter.k8s.aws': 'Karpenter',
175
+ 'karpenter.azure.com': 'Karpenter',
176
+ 'karpenter.k8s.gcp': 'Karpenter',
177
+ 'resource.k8s.io': 'Dynamic Resource Allocation',
178
+ 'kueue.x-k8s.io': 'Kueue',
179
+ 'autoscaling.x-k8s.io': 'Cluster Autoscaler',
180
+ 'serving.kserve.io': 'KServe',
181
+ 'ray.io': 'KubeRay',
182
+ 'leaderworkerset.x-k8s.io': 'LeaderWorkerSet',
183
+ 'jobset.x-k8s.io': 'JobSet',
184
+ 'inference.networking.k8s.io': 'Inference Gateway',
185
+ 'inference.networking.x-k8s.io': 'Inference Gateway',
186
+ 'nvidia.com': 'NVIDIA GPU Operator',
187
+ 'scheduling.run.ai': 'KAI Scheduler',
188
+ 'kai.scheduler': 'KAI Scheduler',
189
+ 'kaito.sh': 'KAITO',
190
+ 'batch.volcano.sh': 'Volcano',
191
+ 'scheduling.volcano.sh': 'Volcano',
192
+ 'flow.volcano.sh': 'Volcano',
193
+ 'bus.volcano.sh': 'Volcano',
173
194
  }
174
195
  if (knownGroups[group]) return knownGroups[group]
175
196
  // Suffix rules — for unbounded provider group sets (Crossplane providers ship