@skyhook-io/k8s-ui 1.17.1 → 1.18.1

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 (107) hide show
  1. package/README.md +13 -0
  2. package/package.json +4 -4
  3. package/src/components/compare/CompareResourcePicker.tsx +18 -4
  4. package/src/components/gitops/GitOpsDetailLayout.test.tsx +64 -0
  5. package/src/components/gitops/GitOpsDetailLayout.tsx +22 -15
  6. package/src/components/gitops/GitOpsGraphFilterRail.tsx +2 -2
  7. package/src/components/gitops/GitOpsTableView.coverage.test.tsx +27 -0
  8. package/src/components/gitops/GitOpsTableView.tsx +33 -4
  9. package/src/components/gitops/detail-helpers.test.ts +58 -13
  10. package/src/components/gitops/detail-helpers.ts +37 -6
  11. package/src/components/gitops/index.ts +1 -0
  12. package/src/components/gitops/insights/ArgoResourceDiff.test.tsx +27 -0
  13. package/src/components/gitops/insights/GitOpsInsightViews.tsx +16 -10
  14. package/src/components/gitops/insights/insights-helpers.test.ts +18 -0
  15. package/src/components/gitops/insights/insights-helpers.ts +7 -0
  16. package/src/components/gitops/tree/GitOpsTreeGraph.test.ts +73 -9
  17. package/src/components/gitops/tree/GitOpsTreeGraph.tsx +45 -28
  18. package/src/components/gitops/tree/tree-helpers.test.ts +6 -0
  19. package/src/components/gitops/tree/tree-helpers.ts +5 -1
  20. package/src/components/issues/IssuesView.tsx +18 -10
  21. package/src/components/issues/ResourceIssuesSection.tsx +5 -2
  22. package/src/components/issues/diagnostic.ts +2 -0
  23. package/src/components/issues/index.ts +1 -1
  24. package/src/components/issues/issues.test.ts +26 -0
  25. package/src/components/issues/severity.ts +9 -2
  26. package/src/components/issues/types.ts +32 -4
  27. package/src/components/logs/LogCore.theme.test.tsx +126 -1
  28. package/src/components/logs/LogCore.tsx +159 -40
  29. package/src/components/logs/LogsViewer.test.tsx +183 -60
  30. package/src/components/logs/LogsViewer.tsx +49 -12
  31. package/src/components/logs/StructuredLogLine.tsx +1 -1
  32. package/src/components/logs/WorkloadLogsViewer.tsx +7 -1
  33. package/src/components/logs/useLogBuffer.reading.test.tsx +149 -0
  34. package/src/components/logs/useLogBuffer.ts +48 -18
  35. package/src/components/logs/useLogSearch.ts +3 -1
  36. package/src/components/namespace-switcher/NamespacePicker.test.tsx +236 -2
  37. package/src/components/namespace-switcher/NamespacePicker.tsx +42 -8
  38. package/src/components/resources/ColumnPickerList.test.tsx +46 -0
  39. package/src/components/resources/ColumnPickerList.tsx +174 -0
  40. package/src/components/resources/ResourcesSidebar.test.tsx +49 -0
  41. package/src/components/resources/ResourcesSidebar.tsx +36 -21
  42. package/src/components/resources/ResourcesView.kind-view.test.tsx +149 -2
  43. package/src/components/resources/ResourcesView.selected-kind.test.ts +14 -0
  44. package/src/components/resources/ResourcesView.trivy-search.test.tsx +59 -0
  45. package/src/components/resources/ResourcesView.tsx +198 -93
  46. package/src/components/resources/column-order.test.ts +104 -0
  47. package/src/components/resources/column-order.ts +64 -0
  48. package/src/components/resources/renderers/ArgoApplicationRenderer.test.tsx +15 -0
  49. package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +4 -1
  50. package/src/components/resources/renderers/CronJobRenderer.test.tsx +31 -0
  51. package/src/components/resources/renderers/CronJobRenderer.tsx +28 -20
  52. package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +22 -9
  53. package/src/components/resources/renderers/PodRenderer.sidecar-terminal.test.tsx +200 -0
  54. package/src/components/resources/renderers/PodRenderer.test.tsx +115 -0
  55. package/src/components/resources/renderers/PodRenderer.tsx +72 -20
  56. package/src/components/resources/resource-utils-trivy.test.ts +87 -0
  57. package/src/components/resources/resource-utils-trivy.ts +122 -0
  58. package/src/components/resources/resource-utils.ts +15 -2
  59. package/src/components/resources/summarize-scheduler-message.test.ts +1 -0
  60. package/src/components/shared/DetailShell.tsx +9 -7
  61. package/src/components/shared/DrainPlanDialog.test.tsx +7 -0
  62. package/src/components/shared/DrainPlanDialog.tsx +6 -2
  63. package/src/components/shared/ManagedByChip.test.tsx +22 -0
  64. package/src/components/shared/ManagedByChip.tsx +24 -4
  65. package/src/components/shared/ResourceActionsBar.identity.test.tsx +64 -0
  66. package/src/components/shared/ResourceActionsBar.tsx +12 -6
  67. package/src/components/shared/ResourceRendererDispatch.test.tsx +49 -0
  68. package/src/components/shared/ResourceRendererDispatch.tsx +48 -31
  69. package/src/components/timeline/DiffViewer.test.tsx +69 -0
  70. package/src/components/timeline/DiffViewer.tsx +113 -4
  71. package/src/components/timeline/TimelineList.tsx +5 -1
  72. package/src/components/timeline/shared.test.tsx +48 -0
  73. package/src/components/timeline/shared.tsx +14 -19
  74. package/src/components/ui/FetchResult.test.tsx +19 -2
  75. package/src/components/ui/FetchResult.tsx +13 -1
  76. package/src/components/ui/MultiSelectPicker.test.tsx +20 -0
  77. package/src/components/ui/MultiSelectPicker.tsx +20 -2
  78. package/src/components/ui/PageHeader.tsx +6 -4
  79. package/src/components/ui/Toast.tsx +48 -30
  80. package/src/components/ui/drawer-components.test.tsx +253 -0
  81. package/src/components/ui/drawer-components.tsx +315 -11
  82. package/src/components/ui/drawer-older-replicasets.test.tsx +45 -0
  83. package/src/components/ui/index.ts +1 -1
  84. package/src/components/workload/WorkloadView.relationships.test.tsx +33 -0
  85. package/src/components/workload/WorkloadView.timelineFocus.test.tsx +96 -0
  86. package/src/components/workload/WorkloadView.tsx +68 -14
  87. package/src/hooks/index.ts +2 -0
  88. package/src/hooks/useMediaQuery.ts +21 -0
  89. package/src/hooks/usePhoneLayout.ts +12 -0
  90. package/src/theme/tailwind-theme.css +5 -0
  91. package/src/types/core.ts +91 -3
  92. package/src/types/gitops-flux-inventory.test.ts +24 -0
  93. package/src/types/gitops-insights.ts +1 -0
  94. package/src/types/gitops-tree.ts +2 -0
  95. package/src/types/gitops.ts +5 -5
  96. package/src/types/index.ts +3 -0
  97. package/src/types/read-coverage.ts +65 -0
  98. package/src/types/resource-coverage.ts +40 -0
  99. package/src/utils/format.test.ts +7 -0
  100. package/src/utils/format.ts +6 -1
  101. package/src/utils/gitops-owner.test.ts +42 -1
  102. package/src/utils/gitops-owner.ts +22 -0
  103. package/src/utils/index.ts +2 -0
  104. package/src/utils/read-actions.test.ts +22 -0
  105. package/src/utils/read-actions.ts +56 -0
  106. package/src/utils/resource-coverage.test.ts +121 -0
  107. package/src/utils/resource-coverage.ts +117 -0
@@ -43,7 +43,7 @@ import type { NavigateToResource } from '../../utils/navigation'
43
43
  import { refToSelectedResource, pluralToKind, knownKindForPluralWithGroup, kindToPlural, kindToPluralWithGroup, apiVersionToGroup } from '../../utils/navigation'
44
44
  import { neighborhoodFor, seedNodeIds, topologyNodeResourceKind } from '../../utils/topology-neighborhood'
45
45
  import { TopologyGraph } from '../topology/TopologyGraph'
46
- import { gitOpsOwnerFromRelationships, type GitOpsOwnerRef } from '../../utils/gitops-owner'
46
+ import { gitOpsOwnerFromRelationships, managerListing, type GitOpsOwnerRef } from '../../utils/gitops-owner'
47
47
  import { gitOpsRouteForResource } from '../../utils/gitops-route'
48
48
  import { buildResourceHierarchy, getAllEventsFromHierarchy, isProblematicEvent, type ResourceLane } from '../../utils/resource-hierarchy'
49
49
  import { TimelineSwimlanes, type TimeWindow } from '../timeline/TimelineSwimlanes'
@@ -57,7 +57,7 @@ import {
57
57
  import { ResourceRendererDispatch, getResourceStatus, diagnoseHealthHint, type DiagnoseHealthHint, type RendererOverrides } from '../shared/ResourceRendererDispatch'
58
58
  import type { ScalerDiagnosis } from '../resources/renderers/WorkloadRenderer'
59
59
  import { DetailShell, type DetailShellTab } from '../shared/DetailShell'
60
- import { HelmManagedByChip, ManagedByChip, type HelmOwnerRef } from '../shared/ManagedByChip'
60
+ import { gitOpsOwnerKindLabel, HelmManagedByChip, ManagedByChip, type HelmOwnerRef } from '../shared/ManagedByChip'
61
61
  import { AlertBanner, getKindColorOutline, displayKindName, OperationalIssuesShownContext, ResourceRefBadge } from '../ui/drawer-components'
62
62
  import { Badge, type BadgeSeverity } from '../ui/Badge'
63
63
  import { Tooltip } from '../ui/Tooltip'
@@ -216,6 +216,8 @@ interface WorkloadViewProps {
216
216
  /** Timeline events the history lanes are built from: this workload's own
217
217
  * history, or events for its namespace from a host that has no scoped query. */
218
218
  allEvents?: TimelineEvent[]
219
+ /** Reveals one event, loading older pages if needed. Use a new object for each navigation request. */
220
+ timelineFocusEvent?: { id: string }
219
221
  /** Older history exists beyond allEvents. */
220
222
  historyTruncated?: boolean
221
223
  /** Loads the next older page of history; omit when the host can't page. */
@@ -353,6 +355,8 @@ interface WorkloadViewProps {
353
355
  namespace: string
354
356
  name: string
355
357
  resource: any
358
+ /** Place resource notices within the replacement layout's content area. */
359
+ leadContent?: ReactNode
356
360
  }) => ReactNode
357
361
  /** Render the metrics tab content */
358
362
  renderMetricsTab?: (props: { kind: string; namespace: string; name: string }) => ReactNode
@@ -398,6 +402,8 @@ interface WorkloadViewProps {
398
402
  * live issues for this resource). Avoids showing the same failure twice.
399
403
  * Also gates the `renderOverviewLead` wrapper (see above). */
400
404
  hasOperationalIssues?: boolean
405
+ /** Also show the lead for neutral context without suppressing renderer problems. */
406
+ hasOverviewLead?: boolean
401
407
  /** True while the live-issues fetch is still pending. Renderers suppress their
402
408
  * banners during this window too — a banner whose failure the pipeline covers
403
409
  * would otherwise flash on first paint before the issues arrive and hide it. */
@@ -465,6 +471,7 @@ export function WorkloadView({
465
471
  refetch: refetchProp,
466
472
  // Timeline
467
473
  allEvents,
474
+ timelineFocusEvent,
468
475
  historyTruncated = false,
469
476
  onLoadOlderHistory,
470
477
  loadingOlderHistory = false,
@@ -518,6 +525,7 @@ export function WorkloadView({
518
525
  renderOverviewIntro,
519
526
  renderOverviewLead,
520
527
  hasOperationalIssues,
528
+ hasOverviewLead,
521
529
  operationalIssuesPending,
522
530
  // Actions bar
523
531
  actionsBarProps,
@@ -718,6 +726,18 @@ export function WorkloadView({
718
726
  const metadata = useMemo(() => extractMetadata(kind, resource), [kind, resource])
719
727
  const relationshipGitOpsOwner = useMemo(() => gitOpsOwnerFromRelationships(relationships), [relationships])
720
728
  const gitopsOwner = resolvedGitOpsOwner ?? relationshipGitOpsOwner
729
+ const listing = useMemo(
730
+ () => managerListing(gitopsOwner, relationships?.managers, gitOpsOwnerVerified, !relationships?.ownership?.managers),
731
+ [gitopsOwner, relationships?.managers, relationships?.ownership?.managers, gitOpsOwnerVerified],
732
+ )
733
+ // When the chip's manager is the only one whose status lists the resource,
734
+ // the chip says it all: Related Resources would repeat it.
735
+ const rendererRelationships = useMemo(
736
+ () => (relationships && listing?.state === 'confirmed' ? { ...relationships, managers: undefined } : relationships),
737
+ [relationships, listing],
738
+ )
739
+ // When they disagree, Related Resources says which manager the label names.
740
+ const trackedBy = listing?.state === 'conflict' && gitopsOwner ? `${gitOpsOwnerKindLabel(gitopsOwner)} ${gitopsOwner.namespace ? `${gitopsOwner.namespace}/` : ''}${gitopsOwner.name}` : undefined
721
741
  // When the resource itself is a portal GitOps CR (Application, Kustomization,
722
742
  // HelmRelease, etc.), surface a link to its dedicated GitOps detail page —
723
743
  // the drawer's renderer is thorough but the tab has the tree + insights +
@@ -930,7 +950,12 @@ export function WorkloadView({
930
950
  useEffect(() => {
931
951
  if (shouldCommitFallback && activeTab !== 'overview') onTabChange?.('overview', { replace: true })
932
952
  }, [activeTab, onTabChange, shouldCommitFallback])
933
- const expandedOverview = expanded ? renderExpandedOverview?.({ kind, apiKind, namespace, name, resource }) : null
953
+ const expandedOverview = expanded ? renderExpandedOverview?.({
954
+ kind, apiKind, namespace, name, resource,
955
+ leadContent: (hasOverviewLead ?? hasOperationalIssues) && renderOverviewLeadForState
956
+ ? renderOverviewLeadForState({ kind, namespace, name })
957
+ : undefined,
958
+ }) : null
934
959
  const overviewIntro = renderOverviewIntro?.({ kind, namespace, name })
935
960
 
936
961
  // ── Collapsed (drawer) mode ──────────────────────────────────────────────
@@ -1012,7 +1037,7 @@ export function WorkloadView({
1012
1037
  <p className="text-sm text-theme-text-tertiary">{namespace}</p>
1013
1038
  {(gitopsOwner || helmOwner || (gitOpsResourcePath && onNavigateGitOpsPath) || onOpenLiveTraffic) && (
1014
1039
  <div className="mt-1 flex flex-wrap items-center gap-1.5">
1015
- {gitopsOwner && <ManagedByChip owner={gitopsOwner} status={gitOpsOwnerStatus} verified={gitOpsOwnerVerified} pending={gitOpsOwnerPending} source={gitOpsOwnerSource} onOpen={onOpenGitOpsResource} />}
1040
+ {gitopsOwner && <ManagedByChip owner={gitopsOwner} status={gitOpsOwnerStatus} verified={gitOpsOwnerVerified} pending={gitOpsOwnerPending} source={gitOpsOwnerSource} listing={listing} onOpen={onOpenGitOpsResource} />}
1016
1041
  {helmOwner && <HelmManagedByChip owner={helmOwner} source={helmOwnerSource} onOpen={onOpenHelmRelease} />}
1017
1042
  {gitOpsResourcePath && onNavigateGitOpsPath && (
1018
1043
  <OpenInGitOpsChip onClick={() => onNavigateGitOpsPath(gitOpsResourcePath)} />
@@ -1090,15 +1115,17 @@ export function WorkloadView({
1090
1115
  drawerSummary
1091
1116
  ) : (
1092
1117
  <>
1093
- {renderOverviewLeadForState && hasOperationalIssues && (
1118
+ {renderOverviewLeadForState && (hasOverviewLead ?? hasOperationalIssues) && (
1094
1119
  <div className="px-4 pt-4">
1095
1120
  {renderOverviewLeadForState({ kind, namespace, name })}
1096
1121
  </div>
1097
1122
  )}
1123
+ {overviewIntro && <div className="px-4 pt-4">{overviewIntro}</div>}
1098
1124
  <ResourceRendererDispatch
1099
1125
  resource={selectedResource}
1100
1126
  data={resource}
1101
- relationships={relationships}
1127
+ relationships={rendererRelationships}
1128
+ trackedBy={trackedBy}
1102
1129
  certificateInfo={certificateInfo}
1103
1130
  hpaDiagnosis={hpaDiagnosis}
1104
1131
  scalerDiagnostics={scalerDiagnostics}
@@ -1196,7 +1223,7 @@ export function WorkloadView({
1196
1223
  </Tooltip>
1197
1224
  ),
1198
1225
  gitopsOwner && (
1199
- <ManagedByChip key="gitops" owner={gitopsOwner} status={gitOpsOwnerStatus} verified={gitOpsOwnerVerified} pending={gitOpsOwnerPending} source={gitOpsOwnerSource} onOpen={onOpenGitOpsResource} variant="block" />
1226
+ <ManagedByChip key="gitops" owner={gitopsOwner} status={gitOpsOwnerStatus} verified={gitOpsOwnerVerified} pending={gitOpsOwnerPending} source={gitOpsOwnerSource} listing={listing} onOpen={onOpenGitOpsResource} variant="block" />
1200
1227
  ),
1201
1228
  helmOwner && (
1202
1229
  <HelmManagedByChip key="helm" owner={helmOwner} source={helmOwnerSource} onOpen={onOpenHelmRelease} variant="block" />
@@ -1303,11 +1330,6 @@ export function WorkloadView({
1303
1330
  <div className="h-full min-h-0 overflow-y-auto">{expandedSummary}</div>
1304
1331
  ) : effectiveTab === 'overview' && expandedOverview ? (
1305
1332
  <div className="h-full min-h-0">
1306
- {hasOperationalIssues && renderOverviewLeadForState && (
1307
- <div className="px-4 pt-4">
1308
- {renderOverviewLeadForState({ kind, namespace, name })}
1309
- </div>
1310
- )}
1311
1333
  {expandedOverview}
1312
1334
  </div>
1313
1335
  ) : effectiveTab === 'spec' && specTab?.render && !resourceGone ? (
@@ -1316,7 +1338,8 @@ export function WorkloadView({
1316
1338
  <InfoTab
1317
1339
  resource={resource}
1318
1340
  selectedResource={selectedResource}
1319
- relationships={relationships}
1341
+ relationships={rendererRelationships}
1342
+ trackedBy={trackedBy}
1320
1343
  certificateInfo={certificateInfo}
1321
1344
  hpaDiagnosis={hpaDiagnosis}
1322
1345
  scalerDiagnostics={scalerDiagnostics}
@@ -1350,7 +1373,7 @@ export function WorkloadView({
1350
1373
  // A host's spec lead replaces the operational issues there: its Overview already leads with them.
1351
1374
  effectiveTab === 'spec' && specTab?.lead
1352
1375
  ? specTab.lead
1353
- : hasOperationalIssues && renderOverviewLeadForState
1376
+ : (hasOverviewLead ?? hasOperationalIssues) && renderOverviewLeadForState
1354
1377
  ? renderOverviewLeadForState({ kind, namespace, name })
1355
1378
  : undefined
1356
1379
  }
@@ -1377,6 +1400,7 @@ export function WorkloadView({
1377
1400
  )}
1378
1401
  {effectiveTab === 'timeline' && (
1379
1402
  <EventsTab
1403
+ focusEvent={timelineFocusEvent}
1380
1404
  events={resourceEvents}
1381
1405
  isLoading={eventsLoading}
1382
1406
  truncated={historyTruncated}
@@ -1682,6 +1706,7 @@ function OwnershipHeading({
1682
1706
  // ============================================================================
1683
1707
 
1684
1708
  function EventsTab({
1709
+ focusEvent,
1685
1710
  events,
1686
1711
  isLoading,
1687
1712
  truncated,
@@ -1697,6 +1722,7 @@ function EventsTab({
1697
1722
  topology,
1698
1723
  onResourceClick,
1699
1724
  }: {
1725
+ focusEvent?: { id: string }
1700
1726
  events: TimelineEvent[]
1701
1727
  isLoading: boolean
1702
1728
  truncated?: boolean
@@ -1792,6 +1818,22 @@ function EventsTab({
1792
1818
  onSelectEvent(id)
1793
1819
  }, [onSelectEvent])
1794
1820
 
1821
+ const focusedRequest = useRef<typeof focusEvent>(undefined)
1822
+ const requestedPage = useRef<{ focus: typeof focusEvent; events: TimelineEvent[] } | undefined>(undefined)
1823
+ const focusFound = Boolean(focusEvent && cleanEvents.some((event) => event.id === focusEvent.id))
1824
+ useEffect(() => {
1825
+ if (!focusEvent || focusedRequest.current === focusEvent || isLoading) return
1826
+ if (focusFound) {
1827
+ handleSelect(focusEvent.id)
1828
+ focusedRequest.current = focusEvent
1829
+ } else if (truncated && onLoadOlder && !loadingOlder && !olderError && !error) {
1830
+ // Strict Mode can replay this effect before the loading prop updates.
1831
+ if (requestedPage.current?.focus === focusEvent && requestedPage.current.events === events) return
1832
+ requestedPage.current = { focus: focusEvent, events }
1833
+ onLoadOlder()
1834
+ }
1835
+ }, [focusEvent, focusFound, events, isLoading, truncated, onLoadOlder, loadingOlder, olderError, error, handleSelect])
1836
+
1795
1837
  if (isLoading) {
1796
1838
  return (
1797
1839
  <div className="flex items-center justify-center h-full text-theme-text-tertiary">
@@ -1821,6 +1863,15 @@ function EventsTab({
1821
1863
 
1822
1864
  return (
1823
1865
  <div className="h-full flex flex-col overflow-hidden">
1866
+ {focusEvent && !focusFound && (
1867
+ <div className="shrink-0 border-b border-theme-border px-4 py-2 text-xs text-theme-text-secondary" role="status">
1868
+ {olderError || error
1869
+ ? "Couldn't find the reported error because some history couldn't be loaded."
1870
+ : truncated && onLoadOlder
1871
+ ? 'Finding the reported error in older history…'
1872
+ : 'The reported error is not available in the retained timeline history.'}
1873
+ </div>
1874
+ )}
1824
1875
  {error && (
1825
1876
  <div className="shrink-0 flex flex-wrap items-center gap-x-2 border-b border-theme-border bg-theme-base px-4 py-1.5 text-xs" role="alert">
1826
1877
  <span className="text-[var(--color-error)]" title={error.message}>
@@ -1926,6 +1977,7 @@ function InfoTab({
1926
1977
  resource,
1927
1978
  selectedResource,
1928
1979
  relationships,
1980
+ trackedBy,
1929
1981
  certificateInfo,
1930
1982
  hpaDiagnosis,
1931
1983
  scalerDiagnostics,
@@ -1962,6 +2014,7 @@ function InfoTab({
1962
2014
  resource: any
1963
2015
  selectedResource: SelectedResource
1964
2016
  relationships?: Relationships
2017
+ trackedBy?: string
1965
2018
  certificateInfo?: any
1966
2019
  hpaDiagnosis?: HPADiagnosis
1967
2020
  scalerDiagnostics?: ScalerDiagnosis[]
@@ -2016,6 +2069,7 @@ function InfoTab({
2016
2069
  resource={selectedResource}
2017
2070
  data={resource}
2018
2071
  relationships={relationships}
2072
+ trackedBy={trackedBy}
2019
2073
  certificateInfo={certificateInfo}
2020
2074
  hpaDiagnosis={hpaDiagnosis}
2021
2075
  scalerDiagnostics={scalerDiagnostics}
@@ -1,6 +1,8 @@
1
1
  export { useAnimatedUnmount } from './useAnimatedUnmount'
2
2
  export { useDebouncedValue } from './useDebouncedValue'
3
3
  export { useRefreshAnimation } from './useRefreshAnimation'
4
+ export { useMediaQuery } from './useMediaQuery'
5
+ export { usePhoneLayout, PHONE_MEDIA_QUERY } from './usePhoneLayout'
4
6
  export {
5
7
  KeyboardShortcutProvider,
6
8
  useRegisterShortcut,
@@ -0,0 +1,21 @@
1
+ import { useEffect, useState } from 'react'
2
+
3
+ // Subscribe to a CSS media query. SSR-safe (returns false before mount) and
4
+ // updates on viewport changes.
5
+ export function useMediaQuery(query: string): boolean {
6
+ const [matches, setMatches] = useState(() => {
7
+ if (typeof window === 'undefined' || !window.matchMedia) return false
8
+ return window.matchMedia(query).matches
9
+ })
10
+
11
+ useEffect(() => {
12
+ if (typeof window === 'undefined' || !window.matchMedia) return
13
+ const mql = window.matchMedia(query)
14
+ const onChange = () => setMatches(mql.matches)
15
+ onChange()
16
+ mql.addEventListener('change', onChange)
17
+ return () => mql.removeEventListener('change', onChange)
18
+ }, [query])
19
+
20
+ return matches
21
+ }
@@ -0,0 +1,12 @@
1
+ import { useMediaQuery } from './useMediaQuery'
2
+
3
+ // A phone: a narrow screen whose primary pointer is touch. A desktop browser
4
+ // narrowed below this width keeps the desktop layout (and its content floor),
5
+ // since a wider window can show the full UI; browser dev-tools device mode
6
+ // emulates touch and gets the phone layout. Keep in sync with the `phone:`
7
+ // variant in theme/tailwind-theme.css.
8
+ export const PHONE_MEDIA_QUERY = '(max-width: 639px) and (pointer: coarse)'
9
+
10
+ export function usePhoneLayout(): boolean {
11
+ return useMediaQuery(PHONE_MEDIA_QUERY)
12
+ }
@@ -6,6 +6,11 @@
6
6
  * Import after variables.css.
7
7
  */
8
8
 
9
+ /* `phone:` styles apply on a narrow touch-first screen only (see
10
+ * PHONE_MEDIA_QUERY in hooks/usePhoneLayout.ts). A narrowed desktop window
11
+ * keeps the desktop layout. */
12
+ @custom-variant phone (@media (max-width: 639px) and (pointer: coarse));
13
+
9
14
  @theme inline {
10
15
  /* Backgrounds */
11
16
  --color-theme-sidebar: var(--bg-sidebar);
package/src/types/core.ts CHANGED
@@ -126,6 +126,7 @@ export interface Capabilities {
126
126
  helmWrite: boolean // Helm write operations (install, upgrade, rollback, uninstall, apply values)
127
127
  nodeWrite: boolean // Node write operations (cordon, uncordon, drain)
128
128
  workloadWrites?: WorkloadWritePermissions // Workload patch permissions (restart/scale controls)
129
+ mcpReadOnly?: boolean // Full MCP endpoint is disabled; read-only endpoints remain available
129
130
  mcpEnabled: boolean // MCP server is running
130
131
  // Karpenter discovery and NodePool read state. Optional on the wire for the
131
132
  // same newer-frontend/older-backend reason as `deployment` below — consumers
@@ -160,6 +161,9 @@ export interface CloudConnectCapability {
160
161
  }
161
162
 
162
163
  export interface FeatureCapabilities {
164
+ helmSourceIdentity?: boolean
165
+ resourceReadCoverage?: boolean
166
+ searchCoverage?: boolean
163
167
  yamlReview?: boolean
164
168
  yamlSchemas?: boolean
165
169
  workloadImages?: boolean
@@ -650,10 +654,23 @@ export interface Relationships {
650
654
  sourceResourceVersion?: string
651
655
  mirrors?: ResourceRef[]
652
656
  }
653
- owner?: ResourceRef
657
+ owner?: ResourceRef // The owner Kubernetes acts through: the controller reference, else the only reference
658
+ owners?: ResourceRef[] // Every owner reference that resolves to an existing owner
654
659
  deployment?: ResourceRef // Grandparent Deployment (for Pods owned by ReplicaSets)
655
660
  managedBy?: ResourceRef[] // Topmost meaningful manager(s): GitOps controller (ArgoCD Application / Flux Kustomization / Flux HelmRelease), Helm release, or the topmost K8s owner. Synthesized server-side; replaces client-side detectGitOpsOwner.
656
- children?: ResourceRef[]
661
+ children?: ResourceRef[] // Objects whose owner references name this one, less olderRevisions
662
+ // Children kept apart because they only keep history: ReplicaSets that run
663
+ // no Pods and aren't their owner's current revision. ownership.children
664
+ // covers both lists together.
665
+ olderRevisions?: ResourceRef[]
666
+ // Management without an owner reference: GitOps member lists, class
667
+ // bindings, spec and status references. Never ownership or garbage collection.
668
+ managers?: ResourceRef[]
669
+ manages?: ResourceRef[]
670
+ /** How complete the owner-reference and recorded-membership answers are.
671
+ * Absent: no completeness is claimed, whether because the server answers
672
+ * Owner and Children from graph edges or because the resource is an Event. */
673
+ ownership?: OwnershipCoverage
657
674
  services?: ResourceRef[]
658
675
  ingresses?: ResourceRef[]
659
676
  gateways?: ResourceRef[]
@@ -680,6 +697,51 @@ export interface Relationships {
680
697
  resourceClaims?: ResourceRef[] // For Pods: DRA ResourceClaims (direct + template-generated)
681
698
  }
682
699
 
700
+ /** A kind Radar couldn't read, in namespace or (without one) in several or all of them, or the cluster for a cluster-scoped kind; reason is a readreason code. No kind stands for kinds discovery didn't list; no group is the core group. */
701
+ export interface OwnershipGap {
702
+ group?: string
703
+ kind?: string
704
+ namespace?: string
705
+ reason: string
706
+ }
707
+
708
+ export interface OwnershipCoverage {
709
+ /** The owner reference's state when no owner is named: gone, replaced, withheld, unknown, unresolvable, excluded (it names an Event) or several. */
710
+ primaryOwnerState?: string
711
+ /** The owner the reference names when it is gone, replaced or unknown. */
712
+ ownerRef?: ResourceRef
713
+ /** A readreason code saying why an unknown owner couldn't be checked. */
714
+ ownerReason?: string
715
+ children: {
716
+ /** Children are every child the viewer may read. */
717
+ complete: boolean
718
+ notChecked?: OwnershipGap[]
719
+ truncated?: boolean
720
+ /** The viewer may read only some of the places children could be. */
721
+ narrowed?: boolean
722
+ }
723
+ /** For a GitOps manager whose status lists its members. */
724
+ manages?: {
725
+ membership: 'recorded' | 'remote' | 'unreadable'
726
+ /** A readreason code when the member list couldn't be read, as opposed to the status recording none. */
727
+ unavailableReason?: string
728
+ missing?: number
729
+ withheld?: number
730
+ unknown?: number
731
+ truncated?: boolean
732
+ }
733
+ /** Set when the search for the GitOps managers whose status lists this resource, for `managers`, couldn't settle every one the viewer may read. */
734
+ managers?: {
735
+ /** Managers with a member list Radar can't read, any of which may list this resource. */
736
+ unreadable?: number
737
+ /** Managers listing this resource's coordinates for a cluster Radar can't confirm is this one. */
738
+ remote?: number
739
+ notChecked?: OwnershipGap[]
740
+ }
741
+ /** A readreason code when Radar couldn't answer at all. */
742
+ unavailableReason?: string
743
+ }
744
+
683
745
  // Parsed X.509 certificate metadata (from backend cert parsing)
684
746
  export interface CertificateInfo {
685
747
  subject: string
@@ -1089,7 +1151,7 @@ export interface UpgradeInfo {
1089
1151
  error?: string
1090
1152
  // Machine-readable source-resolution failure, used to make the Helm drawer
1091
1153
  // explain whether tracking an OCI source can help.
1092
- sourceIssue?: 'untracked' | 'repo_index_error' | 'ambiguous_repository' | 'ambiguous_source' | 'source_unavailable'
1154
+ sourceIssue?: 'source_confirmation_required' | 'source_change_required' | 'release_changing' | 'untracked' | 'repo_index_error' | 'ambiguous_repository' | 'ambiguous_source' | 'source_unavailable'
1093
1155
  // True only when the error is a genuinely untracked source (registering a
1094
1156
  // chart source could fix it). Kept for compatibility; prefer sourceIssue.
1095
1157
  untracked?: boolean
@@ -1099,7 +1161,26 @@ export type ChartSourceCandidate =
1099
1161
  | { type: 'repository'; reference: string; url: string }
1100
1162
  | { type: 'oci'; reference: string; url?: never }
1101
1163
 
1164
+ export interface SourceUpgradeSelection {
1165
+ source: ChartSourceCandidate
1166
+ expectedRevision: number
1167
+ expectedSource: ChartSourceCandidate | null
1168
+ previewDigest?: string
1169
+ }
1170
+
1171
+ export interface ChartSourceSuggestion {
1172
+ source: ChartSourceCandidate
1173
+ current: boolean
1174
+ hasInstalledVersion: boolean
1175
+ state: 'available' | 'index_unavailable' | 'unchecked'
1176
+ }
1177
+
1102
1178
  export interface ChartSourceStatus {
1179
+ changing?: boolean
1180
+ revision?: number
1181
+ confirmed?: boolean
1182
+ sourceChangeRequired?: boolean
1183
+ suggestions?: ChartSourceSuggestion[]
1103
1184
  recorded?: ChartSourceCandidate
1104
1185
  configured: boolean
1105
1186
  available: boolean
@@ -1120,6 +1201,9 @@ export interface ApplyValuesRequest {
1120
1201
 
1121
1202
  // Response for previewing values changes
1122
1203
  export interface ValuesPreviewResponse {
1204
+ baselineSource?: ChartSourceCandidate
1205
+ previewDigest?: string
1206
+ baselineRevision?: number
1123
1207
  currentValues: Record<string, unknown>
1124
1208
  newValues: Record<string, unknown>
1125
1209
  manifestDiff: string
@@ -1188,6 +1272,7 @@ export interface InstallChartRequest {
1188
1272
 
1189
1273
  // ArtifactHub chart with rich metadata
1190
1274
  export interface ArtifactHubChart {
1275
+ official?: boolean
1191
1276
  packageId: string
1192
1277
  name: string
1193
1278
  version: string
@@ -1210,6 +1295,9 @@ export interface ArtifactHubChart {
1210
1295
 
1211
1296
  // ArtifactHub repository info
1212
1297
  export interface ArtifactHubRepository {
1298
+ organizationDisplayName?: string
1299
+ userAlias?: string
1300
+ displayName?: string
1213
1301
  name: string
1214
1302
  url: string
1215
1303
  official?: boolean
@@ -0,0 +1,24 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { parseFluxInventory } from './gitops'
3
+
4
+ // Mirrors TestParseFluxInventoryID in pkg/gitops/helpers_test.go: the
5
+ // Kustomization renderer and the Go tree read the same inventory IDs.
6
+ describe('parseFluxInventory', () => {
7
+ it('reads a colon in an RBAC name, written as a double underscore', () => {
8
+ expect(
9
+ parseFluxInventory([
10
+ { id: '_system__metrics-server_rbac.authorization.k8s.io_ClusterRole', v: 'v1' },
11
+ { id: 'kube-system_cert-manager__leaderelection_rbac.authorization.k8s.io_Role', v: 'v1' },
12
+ ]),
13
+ ).toEqual([
14
+ { group: 'rbac.authorization.k8s.io', kind: 'ClusterRole', namespace: '', name: 'system:metrics-server' },
15
+ { group: 'rbac.authorization.k8s.io', kind: 'Role', namespace: 'kube-system', name: 'cert-manager:leaderelection' },
16
+ ])
17
+ })
18
+
19
+ it('keeps single underscores in a name and the empty core group', () => {
20
+ expect(parseFluxInventory([{ id: 'ns_my_weird_name__ConfigMap' }])).toEqual([
21
+ { group: '', kind: 'ConfigMap', namespace: 'ns', name: 'my_weird_name' },
22
+ ])
23
+ })
24
+ })
@@ -170,6 +170,7 @@ export interface GitOpsChange {
170
170
  hookPhase?: string
171
171
  hasDesired: boolean
172
172
  hasLive: boolean
173
+ missing?: boolean
173
174
  // Structured per-field diff between the desired state (parsed from
174
175
  // kubectl.kubernetes.io/last-applied-configuration) and the live spec.
175
176
  // Undefined when the diff couldn't be computed (no annotation, SSA-applied
@@ -24,6 +24,8 @@ export interface GitOpsTreeNode {
24
24
  tool: GitOpsTreeTool
25
25
  sync?: SyncStatus
26
26
  health?: GitOpsHealthStatus
27
+ // A local lookup confirmed NotFound; independent of controller health.
28
+ missing?: boolean
27
29
  // Who assessed `health`. 'controller' is the GitOps controller's own
28
30
  // verdict; 'radar' is Radar's read of the live object (topology status, or
29
31
  // the issues engine's finding when the controller's verdict isn't
@@ -446,9 +446,7 @@ export function argoApplicationSetConditionsToGitOpsStatus(
446
446
  export function parseFluxInventory(entries: Array<{ id: string; v?: string }>): ManagedResource[] {
447
447
  return entries
448
448
  .map(entry => {
449
- // Format: namespace_name_group_kind
450
- // The challenge is that namespace, name, and group can all contain underscores
451
- // But the format is always: ns_name_group_kind where kind is the last part
449
+ // Format: namespace_name_group_kind; only the name can hold an underscore.
452
450
  const id = entry.id || ''
453
451
  const parts = id.split('_')
454
452
 
@@ -460,9 +458,11 @@ export function parseFluxInventory(entries: Array<{ id: string; v?: string }>):
460
458
  // Kind is always last, group is second-to-last
461
459
  const kind = parts[parts.length - 1]
462
460
  const group = parts[parts.length - 2]
463
- // Namespace is first, name is everything in between
461
+ // Namespace is first, name is everything in between. Flux writes a colon
462
+ // in an RBAC object's name as a double underscore (system:metrics-server
463
+ // is system__metrics-server) and reads every "__" back as a colon.
464
464
  const namespace = parts[0]
465
- const name = parts.slice(1, -2).join('_')
465
+ const name = parts.slice(1, -2).join('_').replaceAll('__', ':')
466
466
 
467
467
  // Validate required fields
468
468
  if (!kind || !name) {
@@ -1,4 +1,5 @@
1
1
  export * from './core'
2
+ export * from './read-coverage'
2
3
  export * from './fetch-error'
3
4
  export * from './gitops'
4
5
  export * from './gitops-tree'
@@ -6,3 +7,5 @@ export * from './gitops-insights'
6
7
  export * from './rbac'
7
8
  export * from './capacity'
8
9
  export * from './policy'
10
+ export * from './read-coverage'
11
+ export * from './resource-coverage'
@@ -0,0 +1,65 @@
1
+ // Coverage of a Radar read, as Radar's pkg/resourceapi serializes it. Codes
2
+ // come from Radar's pkg/readreason.
3
+
4
+ /**
5
+ * A known, finite set of `count` members. `names` lists all of them, sorted,
6
+ * when there are few enough for the response; otherwise it is absent and which
7
+ * members are in the set is unknown.
8
+ */
9
+ export interface ReadNameSet {
10
+ count: number
11
+ names?: string[]
12
+ }
13
+
14
+ /**
15
+ * How far a gap reaches along one axis when it can't list it: `all` of the
16
+ * answer's scope, or an `unknown` part of it. A value a client doesn't know
17
+ * reads as unknown.
18
+ */
19
+ export type ReadExtent = 'all' | 'unknown' | (string & {})
20
+
21
+ /** A gap's set along one axis: listed like a ReadNameSet, or an extent. */
22
+ export type ReadGapSet<T> = { count: number; names?: T[]; extent?: undefined } | { extent: ReadExtent; count?: undefined; names?: undefined }
23
+
24
+ /** A kind; an omitted group is the core group. */
25
+ export interface ReadKindRef {
26
+ kind: string
27
+ group?: string
28
+ }
29
+
30
+ export interface ReadObjectRef {
31
+ kind: string
32
+ group?: string
33
+ namespace?: string
34
+ name: string
35
+ }
36
+
37
+ /**
38
+ * Part of an answer Radar did not read, and why. A gap covers every
39
+ * combination of its axes. An axis that applies is always present, listed or
40
+ * as an extent; an absent axis doesn't apply to the gap (namespaces on a
41
+ * cluster-scoped kind). Within one answer a namespace is in at most one gap.
42
+ * `action` is what the code calls for, so a code the client doesn't know still
43
+ * says what resolves it.
44
+ */
45
+ export interface ReadGap {
46
+ code: string
47
+ action?: string
48
+ kinds?: ReadGapSet<ReadKindRef>
49
+ namespaces?: ReadGapSet<string>
50
+ objects?: { count: number; names?: ReadObjectRef[] }
51
+ }
52
+
53
+ /** Why an implicitly broad read covers less than everything. */
54
+ export type ReadNarrowing = 'user_access' | 'namespace_pin' | 'namespace_collection' | 'kind_collection' | (string & {})
55
+
56
+ /**
57
+ * A search's coverage: `partial` when something the query named couldn't be
58
+ * searched or a read failed; `narrowedBy` when a broad query searched only
59
+ * what the caller's access and Radar's collection allow.
60
+ */
61
+ export interface SearchCoverage {
62
+ status: 'complete' | 'partial'
63
+ narrowedBy?: ReadNarrowing[]
64
+ gaps: ReadGap[]
65
+ }
@@ -0,0 +1,40 @@
1
+ import type { ReadGap, ReadNameSet, ReadNarrowing } from './read-coverage'
2
+
3
+ export interface ResourceReadScope {
4
+ type: 'cluster' | 'all_namespaces' | 'namespaces'
5
+ namespaces?: ReadNameSet
6
+ }
7
+
8
+ /**
9
+ * A single-kind read's coverage. `status` is relative to `scope`: the
10
+ * namespaces the request named, or what remained of every namespace after
11
+ * `narrowedBy` (the caller's access, Radar's pin, what Radar collects) narrowed it.
12
+ */
13
+ export interface ResourceReadCoverage {
14
+ status: 'complete' | 'partial'
15
+ scope: ResourceReadScope
16
+ narrowedBy?: ReadNarrowing[]
17
+ /** What Radar read; on every successful answer, never on a failed read's coverage. */
18
+ read?: ResourceReadScope
19
+ gaps: ReadGap[]
20
+ }
21
+
22
+ export type ResourceCoverage = ResourceReadCoverage | { status: 'unknown' }
23
+
24
+ export interface ResourceListResponse<T> {
25
+ items: T[]
26
+ coverage: ResourceReadCoverage
27
+ /** The lowercased plural from the request path, not a Kubernetes Kind. */
28
+ kind?: string
29
+ /** Omitted for the core group. */
30
+ group?: string
31
+ columns?: import('../components/resources/printer-columns').PrinterColumnDef[] | null
32
+ cells?: Record<string, unknown[]> | null
33
+ }
34
+
35
+ /** A failed resource read's body, on REST and in MCP results alike. */
36
+ export interface ResourceReadErrorBody {
37
+ error: string
38
+ error_code: string
39
+ coverage?: ResourceReadCoverage
40
+ }
@@ -50,6 +50,13 @@ describe('parseMemoryToBytes', () => {
50
50
  expect(parseMemoryToBytes('1E3')).toBe(1000)
51
51
  expect(formatMemoryString('1e9')).toBe('954 MiB')
52
52
  })
53
+
54
+ it('reads sub-unit suffixes as fractions of a byte', () => {
55
+ expect(parseMemoryToBytes('1181116006400m')).toBeCloseTo(1181116006.4)
56
+ expect(parseMemoryToBytes('1000000u')).toBeCloseTo(1)
57
+ expect(parseMemoryToBytes('1000000000n')).toBeCloseTo(1)
58
+ expect(formatMemoryString('18360985190400m')).toBe('17.1 GiB')
59
+ })
53
60
  })
54
61
 
55
62
  describe('parseQuantityToNumber', () => {