@skyhook-io/k8s-ui 1.17.0 → 1.17.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 (227) hide show
  1. package/README.md +41 -0
  2. package/package.json +3 -3
  3. package/src/components/charts/AreaChart.interaction.test.tsx +118 -0
  4. package/src/components/charts/AreaChart.test.tsx +25 -0
  5. package/src/components/charts/AreaChart.tsx +141 -12
  6. package/src/components/charts/index.ts +1 -0
  7. package/src/components/checks/ChecksView.tsx +18 -1
  8. package/src/components/cnpg/CNPGBackupSummary.tsx +57 -9
  9. package/src/components/cnpg/CNPGClusterHASection.test.tsx +90 -0
  10. package/src/components/cnpg/CNPGClusterHASection.tsx +331 -0
  11. package/src/components/cnpg/CNPGClusterSummary.test.tsx +275 -0
  12. package/src/components/cnpg/CNPGClusterSummary.tsx +310 -96
  13. package/src/components/cnpg/CNPGConnectSection.test.tsx +64 -0
  14. package/src/components/cnpg/CNPGConnectSection.tsx +178 -0
  15. package/src/components/cnpg/CNPGDeclarativeSummary.tsx +157 -26
  16. package/src/components/cnpg/CNPGImageCatalogSummary.tsx +7 -4
  17. package/src/components/cnpg/CNPGLogicalPath.test.tsx +22 -0
  18. package/src/components/cnpg/CNPGLogicalPath.tsx +150 -0
  19. package/src/components/cnpg/CNPGObjectStoreSummary.tsx +9 -6
  20. package/src/components/cnpg/CNPGObjectSummary.test.tsx +193 -2
  21. package/src/components/cnpg/CNPGPoolerScheduling.test.tsx +36 -0
  22. package/src/components/cnpg/CNPGPoolerSummary.tsx +241 -14
  23. package/src/components/cnpg/CNPGSharedSummary.tsx +15 -8
  24. package/src/components/cnpg/CNPGWALArchivingFact.test.tsx +39 -0
  25. package/src/components/cnpg/backupRuns.test.ts +17 -0
  26. package/src/components/cnpg/backupRuns.ts +19 -0
  27. package/src/components/cnpg/connect.test.ts +124 -0
  28. package/src/components/cnpg/connect.ts +155 -0
  29. package/src/components/cnpg/databaseRole.test.ts +38 -0
  30. package/src/components/cnpg/databaseRole.ts +80 -0
  31. package/src/components/cnpg/ha.test.ts +446 -0
  32. package/src/components/cnpg/ha.ts +621 -0
  33. package/src/components/cnpg/index.ts +18 -0
  34. package/src/components/cnpg/logicalReplication.test.ts +121 -0
  35. package/src/components/cnpg/logicalReplication.ts +252 -0
  36. package/src/components/cnpg/pooler.test.ts +84 -0
  37. package/src/components/cnpg/pooler.ts +225 -0
  38. package/src/components/cnpg/primitives.tsx +21 -117
  39. package/src/components/cnpg/problems.ts +12 -0
  40. package/src/components/cnpg/relations.test.ts +63 -13
  41. package/src/components/cnpg/relations.ts +61 -52
  42. package/src/components/cnpg/schedule.ts +35 -0
  43. package/src/components/cnpg/workspace-disk.test.ts +97 -0
  44. package/src/components/cnpg/workspace-fleet-metrics.test.ts +250 -0
  45. package/src/components/cnpg/workspace-problems.test.ts +175 -0
  46. package/src/components/cnpg/workspace.test.ts +380 -9
  47. package/src/components/cnpg/workspace.ts +1137 -136
  48. package/src/components/dock/DockContext.test.tsx +32 -0
  49. package/src/components/dock/DockContext.tsx +18 -4
  50. package/src/components/dock/LocalTerminalTab.test.tsx +40 -0
  51. package/src/components/dock/LocalTerminalTab.tsx +4 -3
  52. package/src/components/dock/TerminalTab.tsx +20 -8
  53. package/src/components/facts/certainty.tsx +36 -0
  54. package/src/components/facts/facts.test.tsx +24 -0
  55. package/src/components/facts/facts.tsx +62 -0
  56. package/src/components/facts/index.ts +6 -0
  57. package/src/components/facts/managed-by.test.tsx +17 -0
  58. package/src/components/facts/managed-by.tsx +36 -0
  59. package/src/components/facts/public-exports.test.tsx +35 -0
  60. package/src/components/gitops/GitOpsGraphFilterRail.tsx +6 -2
  61. package/src/components/gitops/GitOpsHealthSourceNotice.test.tsx +38 -0
  62. package/src/components/gitops/GitOpsHealthSourceNotice.tsx +13 -3
  63. package/src/components/gitops/insights/ArgoResourceDiff.test.tsx +115 -0
  64. package/src/components/gitops/insights/ArgoResourceDiff.tsx +22 -5
  65. package/src/components/gitops/insights/DriftPanel.tsx +62 -0
  66. package/src/components/gitops/insights/GitOpsInsightViews.tsx +24 -71
  67. package/src/components/gitops/tree/GitOpsTreeGraph.test.ts +26 -1
  68. package/src/components/gitops/tree/GitOpsTreeGraph.tsx +38 -55
  69. package/src/components/gitops/tree/layout.test.ts +111 -0
  70. package/src/components/gitops/tree/layout.ts +128 -0
  71. package/src/components/gitops/tree/tree-helpers.test.ts +17 -0
  72. package/src/components/gitops/tree/tree-helpers.ts +7 -1
  73. package/src/components/issues/ConditionalCleanup.test.tsx +82 -0
  74. package/src/components/issues/IssuesView.tsx +40 -5
  75. package/src/components/issues/ResourceIssuesSection.test.tsx +28 -0
  76. package/src/components/issues/ResourceIssuesSection.tsx +3 -0
  77. package/src/components/issues/index.ts +3 -1
  78. package/src/components/issues/issue-timing.ts +12 -1
  79. package/src/components/issues/issues.test.ts +49 -1
  80. package/src/components/issues/severity.ts +21 -0
  81. package/src/components/issues/types.ts +23 -0
  82. package/src/components/logs/LogCore.theme.test.tsx +81 -3
  83. package/src/components/logs/LogCore.tsx +25 -17
  84. package/src/components/logs/LogToolbarSelects.tsx +3 -1
  85. package/src/components/logs/LogsViewer.test.tsx +291 -0
  86. package/src/components/logs/LogsViewer.tsx +72 -11
  87. package/src/components/logs/StructuredLogLine.test.tsx +76 -0
  88. package/src/components/logs/StructuredLogLine.tsx +22 -30
  89. package/src/components/logs/WorkloadLogsViewer.test.tsx +65 -0
  90. package/src/components/logs/WorkloadLogsViewer.tsx +39 -10
  91. package/src/components/logs/useLogBuffer.test.ts +8 -0
  92. package/src/components/logs/useLogStream.ts +17 -1
  93. package/src/components/problems/index.ts +1 -0
  94. package/src/components/problems/problems.test.tsx +63 -0
  95. package/src/components/problems/problems.tsx +165 -0
  96. package/src/components/resources/ActiveFilterBar.test.tsx +78 -0
  97. package/src/components/resources/ActiveFilterBar.tsx +86 -0
  98. package/src/components/resources/ResourcesSidebar.keyboard.test.tsx +42 -0
  99. package/src/components/resources/ResourcesSidebar.test.tsx +36 -2
  100. package/src/components/resources/ResourcesSidebar.tsx +92 -41
  101. package/src/components/resources/ResourcesView.kind-filter-memory.test.ts +52 -0
  102. package/src/components/resources/ResourcesView.kind-view.test.tsx +149 -0
  103. package/src/components/resources/ResourcesView.per-container.test.ts +56 -1
  104. package/src/components/resources/ResourcesView.tsx +302 -109
  105. package/src/components/resources/curated-column-ownership.test.ts +3 -3
  106. package/src/components/resources/renderers/AutoscalingReferences.test.tsx +286 -0
  107. package/src/components/resources/renderers/CAPIClusterRenderer.tsx +3 -2
  108. package/src/components/resources/renderers/CAPIExactReferenceNavigation.test.tsx +27 -0
  109. package/src/components/resources/renderers/CNPGBackupRenderer.test.tsx +23 -0
  110. package/src/components/resources/renderers/CNPGBackupRenderer.tsx +8 -19
  111. package/src/components/resources/renderers/CNPGClusterRenderer.tsx +5 -7
  112. package/src/components/resources/renderers/CNPGDeclarativeRenderer.tsx +25 -0
  113. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.test.tsx +32 -0
  114. package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +7 -15
  115. package/src/components/resources/renderers/CompositeRenderer.navigation.test.tsx +35 -0
  116. package/src/components/resources/renderers/CompositeRenderer.tsx +3 -3
  117. package/src/components/resources/renderers/CompositionRenderer.tsx +3 -3
  118. package/src/components/resources/renderers/ContourExactReferenceNavigation.test.tsx +34 -0
  119. package/src/components/resources/renderers/ContourHTTPProxyRenderer.tsx +11 -10
  120. package/src/components/resources/renderers/EndpointSliceRenderer.test.tsx +39 -1
  121. package/src/components/resources/renderers/EndpointSliceRenderer.tsx +14 -13
  122. package/src/components/resources/renderers/HPARenderer.tsx +56 -16
  123. package/src/components/resources/renderers/IngressRenderer.defaultBackend.test.tsx +33 -0
  124. package/src/components/resources/renderers/IngressRenderer.tsx +30 -3
  125. package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +9 -9
  126. package/src/components/resources/renderers/IstioExactReferenceNavigation.test.tsx +34 -0
  127. package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +5 -8
  128. package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +13 -11
  129. package/src/components/resources/renderers/KnativeEventingRenderer.tsx +24 -31
  130. package/src/components/resources/renderers/KnativeExactReferenceNavigation.test.tsx +47 -0
  131. package/src/components/resources/renderers/KnativeFlowRenderer.tsx +6 -7
  132. package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +7 -9
  133. package/src/components/resources/renderers/KnativeReferenceLink.tsx +14 -0
  134. package/src/components/resources/renderers/KnativeReferenceLinks.test.tsx +60 -0
  135. package/src/components/resources/renderers/KnativeSourceRenderer.tsx +9 -11
  136. package/src/components/resources/renderers/ManagedResourceRenderer.tsx +2 -2
  137. package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +3 -2
  138. package/src/components/resources/renderers/PodRenderer.test.tsx +32 -0
  139. package/src/components/resources/renderers/PodRenderer.tsx +41 -29
  140. package/src/components/resources/renderers/SimpleRouteRenderer.tsx +1 -1
  141. package/src/components/resources/renderers/VPARenderer.tsx +17 -11
  142. package/src/components/resources/resource-utils-capi.ts +1 -1
  143. package/src/components/resources/resource-utils-cnpg.test.ts +35 -10
  144. package/src/components/resources/resource-utils-cnpg.ts +51 -20
  145. package/src/components/resources/resource-utils-contour.test.ts +44 -0
  146. package/src/components/resources/resource-utils-contour.ts +16 -0
  147. package/src/components/resources/resource-utils-istio.ts +12 -8
  148. package/src/components/resources/resource-utils-knative.ts +18 -0
  149. package/src/components/resources/resource-utils.ts +82 -6
  150. package/src/components/resources/rules-column-width.test.ts +1 -1
  151. package/src/components/resources/summarize-scheduler-message.test.ts +30 -0
  152. package/src/components/shared/ActionConfirmDialog.test.tsx +154 -0
  153. package/src/components/shared/ActionConfirmDialog.tsx +246 -0
  154. package/src/components/shared/CreateResourceDialog.test.tsx +330 -0
  155. package/src/components/shared/CreateResourceDialog.tsx +126 -80
  156. package/src/components/shared/DetailShell.test.tsx +80 -0
  157. package/src/components/shared/DetailShell.tsx +85 -10
  158. package/src/components/shared/EditableYamlView.test.tsx +7 -0
  159. package/src/components/shared/EditableYamlView.tsx +1 -1
  160. package/src/components/shared/ResourceActionsBar.tsx +11 -4
  161. package/src/components/shared/ResourceRendererDispatch.tsx +12 -9
  162. package/src/components/shared/index.ts +2 -0
  163. package/src/components/timeline/TimelineSwimlanes.tsx +1 -1
  164. package/src/components/timeline/timeline-filters.ts +1 -0
  165. package/src/components/topology/K8sResourceNode.test.ts +18 -1
  166. package/src/components/topology/K8sResourceNode.tsx +44 -2
  167. package/src/components/topology/TopologyFilterSidebar.tsx +1 -0
  168. package/src/components/topology/TopologyGraph.tsx +3 -71
  169. package/src/components/topology/TopologySearch.tsx +1 -0
  170. package/src/components/topology/internet-groups.test.ts +35 -0
  171. package/src/components/topology/internet-groups.ts +55 -0
  172. package/src/components/topology/layout.ts +1 -1
  173. package/src/components/ui/Badge.tsx +1 -0
  174. package/src/components/ui/FoldSection.tsx +68 -0
  175. package/src/components/ui/ForceDeleteConfirmDialog.test.tsx +62 -0
  176. package/src/components/ui/ForceDeleteConfirmDialog.tsx +24 -12
  177. package/src/components/ui/RefLink.tsx +20 -0
  178. package/src/components/ui/RowActionMenu.tsx +8 -3
  179. package/src/components/ui/crd-definition-navigation.test.tsx +160 -0
  180. package/src/components/ui/drawer-components.test.tsx +110 -1
  181. package/src/components/ui/drawer-components.tsx +62 -9
  182. package/src/components/ui/index.ts +3 -1
  183. package/src/components/ui/monacoEditorFeatures.ts +77 -0
  184. package/src/components/ui/monacoRuntime.ts +14 -19
  185. package/src/components/ui/status-tone.test.ts +10 -1
  186. package/src/components/ui/status-tone.tsx +29 -0
  187. package/src/components/workload/WorkloadView.deleted.test.tsx +43 -0
  188. package/src/components/workload/WorkloadView.relationships.test.tsx +37 -0
  189. package/src/components/workload/WorkloadView.spec.test.tsx +149 -0
  190. package/src/components/workload/WorkloadView.tsx +180 -64
  191. package/src/index.ts +14 -1
  192. package/src/monaco-deep.d.ts +19 -12
  193. package/src/theme/components.css +13 -0
  194. package/src/types/capacity.ts +11 -2
  195. package/src/types/core.ts +29 -2
  196. package/src/types/gitops-flux-status.test.ts +32 -1
  197. package/src/types/gitops-insights.ts +19 -2
  198. package/src/types/gitops-tree.ts +10 -0
  199. package/src/types/gitops.ts +26 -6
  200. package/src/utils/api-resources.ts +7 -0
  201. package/src/utils/application-history.ts +1 -0
  202. package/src/utils/cnpg-backup.test.ts +26 -0
  203. package/src/utils/cnpg-backup.ts +70 -0
  204. package/src/utils/format.test.ts +9 -0
  205. package/src/utils/format.ts +4 -0
  206. package/src/utils/gitops-owner.ts +3 -2
  207. package/src/utils/grant.test.ts +19 -0
  208. package/src/utils/grant.ts +35 -0
  209. package/src/utils/index.ts +4 -0
  210. package/src/utils/k8s-errors.ts +7 -7
  211. package/src/utils/log-format.test.ts +41 -0
  212. package/src/utils/log-format.ts +71 -15
  213. package/src/utils/log-level.ts +12 -8
  214. package/src/utils/navigation.test.ts +57 -0
  215. package/src/utils/navigation.ts +86 -8
  216. package/src/utils/object-reference-navigation.test.ts +43 -0
  217. package/src/utils/resource-hierarchy.test.ts +37 -0
  218. package/src/utils/resource-hierarchy.ts +15 -7
  219. package/src/utils/resource-icons.ts +2 -0
  220. package/src/utils/skeleton-yaml.test.ts +21 -0
  221. package/src/utils/skeleton-yaml.ts +16 -0
  222. package/src/utils/url-path.test.ts +35 -0
  223. package/src/utils/url-path.ts +21 -0
  224. package/src/utils/validators.test.ts +4 -13
  225. package/src/utils/validators.ts +5 -19
  226. package/src/utils/view-transition.test.ts +35 -1
  227. package/src/utils/view-transition.ts +16 -1
@@ -0,0 +1,32 @@
1
+ // @vitest-environment jsdom
2
+ import { act } from 'react'
3
+ import { createRoot } from 'react-dom/client'
4
+ import { expect, it } from 'vitest'
5
+ import { DockProvider, useDock, useOpenTerminal, type DockContextValue } from './DockContext'
6
+ Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })
7
+
8
+ it('reuses normal terminals and separates explicit executables and cluster identities', async () => {
9
+ let dock!: DockContextValue
10
+ let open!: ReturnType<typeof useOpenTerminal>
11
+ function Host() { dock = useDock(); open = useOpenTerminal(); return null }
12
+ const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host)
13
+ await act(async () => root.render(<DockProvider><Host /></DockProvider>))
14
+ const base = { namespace: 'app', podName: 'worker', containerName: 'app', containers: ['app'], orgId: 'org', clusterId: 'first' }
15
+ try {
16
+ await act(async () => open(base)); await act(async () => open(base))
17
+ expect(dock.tabs).toHaveLength(1); expect(dock.tabs[0].title).toBe('worker/app')
18
+ await act(async () => open({ ...base, shell: 'psql', title: 'Database', sessionNote: 'Primary' }))
19
+ expect(dock.tabs).toHaveLength(2)
20
+ expect(dock.tabs[1]).toMatchObject({ shell: 'psql', title: 'Database', sessionNote: 'Primary' })
21
+ const commandId = dock.tabs[1].id
22
+ await act(async () => open({ ...base, shell: 'psql', title: 'Database standby', sessionNote: 'Standby' }))
23
+ expect(dock.tabs).toHaveLength(2)
24
+ expect(dock.tabs[1]).toMatchObject({ id: commandId, title: 'Database standby', sessionNote: 'Standby' })
25
+ const defaultId = dock.tabs[0].id
26
+ await act(async () => dock.setTabTitle(defaultId, 'My shell'))
27
+ await act(async () => open(base))
28
+ expect(dock.tabs[0].title).toBe('My shell')
29
+ await act(async () => open({ ...base, clusterId: 'second', shell: 'psql' })); expect(dock.tabs).toHaveLength(3)
30
+ await act(async () => open({ ...base, orgId: 'other' })); expect(dock.tabs).toHaveLength(4)
31
+ } finally { await act(async () => root.unmount()); host.remove() }
32
+ })
@@ -16,6 +16,10 @@ export interface DockTab {
16
16
  podName?: string
17
17
  containerName?: string
18
18
  containers?: string[]
19
+ /** Terminal: run this command instead of a shell (one argv element, e.g. "psql"). */
20
+ shell?: string
21
+ /** Terminal: a short line shown in the toolbar, e.g. what the session is connected to. */
22
+ sessionNote?: string
19
23
  // Workload logs props
20
24
  workloadKind?: string
21
25
  workloadName?: string
@@ -87,10 +91,14 @@ export function DockProvider({ children }: { children: ReactNode }) {
87
91
  }
88
92
  return t.namespace === tabData.namespace &&
89
93
  t.podName === tabData.podName &&
90
- t.containerName === tabData.containerName
94
+ t.containerName === tabData.containerName &&
95
+ t.shell === tabData.shell
91
96
  })
92
97
 
93
98
  if (existingTab) {
99
+ if (tabData.type === 'terminal' && tabData.sessionNote !== undefined) {
100
+ setTabs(prev => prev.map(tab => tab.id === existingTab.id ? { ...tab, sessionNote: tabData.sessionNote } : tab))
101
+ }
94
102
  setActiveTabId(existingTab.id)
95
103
  setIsExpanded(true)
96
104
  return existingTab.id
@@ -197,7 +205,7 @@ export function useDockReservedHeight(): number {
197
205
  // Convenience hooks for adding specific tab types
198
206
 
199
207
  export function useOpenTerminal() {
200
- const { addTab } = useDock()
208
+ const { addTab, setTabTitle } = useDock()
201
209
 
202
210
  return (opts: {
203
211
  namespace: string
@@ -207,18 +215,24 @@ export function useOpenTerminal() {
207
215
  orgId?: string
208
216
  clusterId?: string
209
217
  clusterName?: string
218
+ shell?: string
219
+ sessionNote?: string
220
+ title?: string
210
221
  }) => {
211
- addTab({
222
+ const id = addTab({
212
223
  type: 'terminal',
213
- title: `${opts.podName}/${opts.containerName}`,
224
+ title: opts.title ?? `${opts.podName}/${opts.containerName}`,
214
225
  namespace: opts.namespace,
215
226
  podName: opts.podName,
216
227
  containerName: opts.containerName,
217
228
  containers: opts.containers,
229
+ shell: opts.shell,
230
+ sessionNote: opts.sessionNote,
218
231
  orgId: opts.orgId,
219
232
  clusterId: opts.clusterId,
220
233
  clusterName: opts.clusterName,
221
234
  })
235
+ if (opts.title !== undefined) setTabTitle(id, opts.title)
222
236
  }
223
237
  }
224
238
 
@@ -4,6 +4,7 @@ import { createRoot, type Root } from 'react-dom/client'
4
4
  import { afterEach, beforeEach, expect, it, vi } from 'vitest'
5
5
  import { LocalTerminalTab, type LocalTerminalTabProps } from './LocalTerminalTab'
6
6
  import { TerminalTab } from './TerminalTab'
7
+ import { DockProvider, useDock, useOpenTerminal } from './DockContext'
7
8
 
8
9
  vi.mock('@xterm/xterm', () => ({
9
10
  Terminal: class {
@@ -187,3 +188,42 @@ it.each(['local', 'pod'])('allows Retry after a %s terminal creation error', asy
187
188
  await act(async () => Socket.instances[0].onopen?.())
188
189
  expect(element.textContent).not.toContain('creation failed')
189
190
  })
191
+
192
+
193
+ it.each([undefined, 'psql'])('keeps shell defaults and command-specific unavailable guidance (%s)', async command => {
194
+ const createSession = vi.fn(async () => ({ wsUrl: 'ws://localhost' }))
195
+ const createDebugContainer = vi.fn()
196
+ await act(async () => root.render(<TerminalTab namespace="app" podName="worker" containerName="app" containers={['app', 'sidecar']} createSession={createSession} createDebugContainer={createDebugContainer} command={command} note={command ? 'Database session' : undefined} />))
197
+ expect(createSession).toHaveBeenCalledWith('app')
198
+ expect(element.querySelector('select') !== null).toBe(!command)
199
+ if (command) expect(element.textContent).toContain('Database session')
200
+ await act(async () => Socket.instances[0].onmessage?.({ data: JSON.stringify({ type: 'error', errorType: 'shell_not_found', data: 'executable file not found' }) }))
201
+ if (command) {
202
+ expect(element.textContent).toContain('Command not available')
203
+ expect(element.textContent).toContain('psql')
204
+ expect(element.textContent).not.toContain('debug container')
205
+ expect(element.textContent).not.toContain('/bin/sh')
206
+ } else {
207
+ expect(element.textContent).toContain('Shell not available')
208
+ expect(element.textContent).toContain('/bin/sh')
209
+ expect(element.textContent).toContain('Start debug container')
210
+ }
211
+ })
212
+
213
+
214
+ it('refreshes command session context when reusing its tab without reconnecting', async () => {
215
+ let open!: ReturnType<typeof useOpenTerminal>
216
+ const createSession = vi.fn(async () => ({ wsUrl: 'ws://localhost' }))
217
+ function Host() {
218
+ open = useOpenTerminal()
219
+ const { tabs } = useDock()
220
+ return <>{tabs.map(tab => <TerminalTab key={tab.id} namespace={tab.namespace!} podName={tab.podName!} containerName={tab.containerName!} containers={tab.containers!} command={tab.shell} note={tab.sessionNote} createSession={createSession} />)}</>
221
+ }
222
+ await act(async () => root.render(<DockProvider><Host /></DockProvider>))
223
+ const base = { namespace: 'app', podName: 'worker', containerName: 'app', containers: ['app'], shell: 'psql', title: 'Primary', sessionNote: 'Read-write' }
224
+ await act(async () => open(base))
225
+ await act(async () => Socket.instances[0].onopen?.())
226
+ await act(async () => open({ ...base, title: 'Standby', sessionNote: 'Read-only' }))
227
+ expect(element.textContent).toContain('Read-only'); expect(element.textContent).not.toContain('Read-write')
228
+ expect(Socket.instances).toHaveLength(1); expect(createSession).toHaveBeenCalledOnce()
229
+ })
@@ -8,6 +8,7 @@ import { clsx } from 'clsx'
8
8
  import { setupTerminalClipboard, copyTerminalSelection } from './terminalClipboard'
9
9
  import { TerminalClipboardToolbar } from './TerminalClipboardToolbar'
10
10
  import { useMultilinePasteConfirm } from './useMultilinePasteConfirm'
11
+ import { toneTextClass } from '../ui/status-tone'
11
12
 
12
13
  export interface LocalTerminalSessionInfo {
13
14
  context: string
@@ -292,9 +293,9 @@ export function LocalTerminalTab({
292
293
  {/* Terminal or error */}
293
294
  <div ref={terminalRef} className="absolute top-8 left-0 right-0 bottom-0 bg-[#0f172a] [&_.xterm-viewport]:!bg-[#0f172a]" />
294
295
  {error && (
295
- <div key="error" className="absolute top-8 left-0 right-0 bottom-0 flex flex-col items-center justify-center p-4 text-center bg-slate-900">
296
- <div className="text-red-400 mb-2 text-sm">Failed to connect</div>
297
- <div className="text-xs text-theme-text-disabled mb-3">{error}</div>
296
+ <div key="error" className="absolute top-8 left-0 right-0 bottom-0 flex flex-col items-center justify-center p-4 text-center bg-theme-base">
297
+ <div className={clsx(toneTextClass('unhealthy'), 'mb-2 text-sm')}>Failed to connect</div>
298
+ <div className="text-xs text-theme-text-secondary mb-3">{error}</div>
298
299
  <button
299
300
  onClick={connect}
300
301
  disabled={!connectAllowed}
@@ -8,6 +8,7 @@ import { clsx } from 'clsx'
8
8
  import { setupTerminalClipboard, copyTerminalSelection } from './terminalClipboard'
9
9
  import { TerminalClipboardToolbar } from './TerminalClipboardToolbar'
10
10
  import { useMultilinePasteConfirm } from './useMultilinePasteConfirm'
11
+ import { toneTextClass } from '../ui/status-tone'
11
12
 
12
13
  export interface TerminalTabProps {
13
14
  namespace: string
@@ -19,6 +20,10 @@ export interface TerminalTabProps {
19
20
  createSession: (containerName: string) => Promise<{ wsUrl: string }>
20
21
  /** Optional: creates a debug (ephemeral) container. If omitted, the debug button is hidden. */
21
22
  createDebugContainer?: (targetContainer: string) => Promise<{ containerName: string }>
23
+ /** Optional: a short line in the toolbar describing the session. */
24
+ note?: string
25
+ /** Executable supplied by the host session callback; pins its target container. */
26
+ command?: string
22
27
  }
23
28
 
24
29
  export function TerminalTab({
@@ -29,6 +34,8 @@ export function TerminalTab({
29
34
  isActive = true,
30
35
  createSession,
31
36
  createDebugContainer,
37
+ note,
38
+ command,
32
39
  }: TerminalTabProps) {
33
40
  const terminalRef = useRef<HTMLDivElement>(null)
34
41
  const xtermRef = useRef<XTerm | null>(null)
@@ -269,8 +276,9 @@ export function TerminalTab({
269
276
  )}
270
277
  />
271
278
  <span className="text-xs text-theme-text-tertiary">{podName}</span>
279
+ {note && <span className="truncate text-xs text-theme-text-secondary">{note}</span>}
272
280
 
273
- {containers.length > 1 && (
281
+ {!command && containers.length > 1 && (
274
282
  <div className="relative">
275
283
  <select
276
284
  value={selectedContainer}
@@ -300,18 +308,22 @@ export function TerminalTab({
300
308
 
301
309
  <div ref={terminalRef} className="absolute top-8 left-0 right-0 bottom-0 bg-[#0f172a] [&_.xterm-viewport]:!bg-[#0f172a]" />
302
310
  {error && (
303
- <div key="error" className="absolute top-8 left-0 right-0 bottom-0 flex flex-col items-center justify-center p-4 text-center bg-slate-900">
311
+ <div key="error" className="absolute top-8 left-0 right-0 bottom-0 flex flex-col items-center justify-center p-4 text-center bg-theme-base">
304
312
  {errorType === 'shell_not_found' ? (
305
313
  <>
306
- <div className="text-amber-400 mb-2 text-sm">Shell not available</div>
307
- <div className="text-xs text-theme-text-tertiary mb-4 max-w-md">
314
+ <div className="text-warning-text mb-2 text-sm">{command ? 'Command not available' : 'Shell not available'}</div>
315
+ {command ? (
316
+ <div className="text-xs text-theme-text-tertiary mb-4 max-w-md">
317
+ Container <span className="font-mono text-theme-text-secondary">{selectedContainer}</span> could not start <span className="font-mono text-theme-text-secondary">{command}</span>. Check that the executable is installed in this container.
318
+ </div>
319
+ ) : <div className="text-xs text-theme-text-tertiary mb-4 max-w-md">
308
320
  Container <span className="font-mono text-theme-text-secondary">{selectedContainer}</span> doesn&apos;t
309
321
  have a shell (/bin/sh). This is common with distroless or minimal container images
310
322
  {containers.length > 1 ? ' — try another container above, or' : '. You can'} create a
311
323
  debug container to troubleshoot.
312
- </div>
324
+ </div>}
313
325
  <div className="flex gap-2">
314
- {createDebugContainerRef.current && (
326
+ {!command && createDebugContainerRef.current && (
315
327
  <button
316
328
  onClick={handleCreateDebugContainer}
317
329
  disabled={isCreatingDebug}
@@ -335,8 +347,8 @@ export function TerminalTab({
335
347
  </>
336
348
  ) : (
337
349
  <>
338
- <div className="text-red-400 mb-2 text-sm">Failed to connect</div>
339
- <div className="text-xs text-theme-text-disabled mb-3">{error}</div>
350
+ <div className={clsx(toneTextClass('unhealthy'), 'mb-2 text-sm')}>Failed to connect</div>
351
+ <div className="text-xs text-theme-text-secondary mb-3">{error}</div>
340
352
  <button
341
353
  onClick={connect}
342
354
  className="flex items-center gap-2 px-3 py-1.5 btn-brand text-xs rounded"
@@ -0,0 +1,36 @@
1
+ import { WithTooltip } from '../ui/Tooltip'
2
+
3
+ /**
4
+ * How exactly a value is known. A value read only in part is a lower bound,
5
+ * never shown as if exact.
6
+ */
7
+ export type Certainty = 'exact' | 'lower_bound' | 'upper_bound' | 'unknown'
8
+
9
+ export function certaintyGlyph(certainty: Certainty): string {
10
+ if (certainty === 'exact') return '='
11
+ if (certainty === 'lower_bound') return '≥'
12
+ if (certainty === 'upper_bound') return '≤'
13
+ return '?'
14
+ }
15
+
16
+ export function certaintyValueLabel(certainty: Certainty): string {
17
+ if (certainty === 'exact') return 'Exact'
18
+ if (certainty === 'lower_bound') return 'Lower bound'
19
+ if (certainty === 'upper_bound') return 'Upper bound'
20
+ return 'Unknown certainty'
21
+ }
22
+
23
+ export function CertaintyGlyph({ certainty, title }: { certainty: Certainty; title?: string }) {
24
+ return (
25
+ <WithTooltip tip={title ?? certaintyValueLabel(certainty)}>
26
+ <span
27
+ tabIndex={0}
28
+ role="note"
29
+ aria-label={title ?? certaintyValueLabel(certainty)}
30
+ className="cursor-help rounded border border-theme-border-light px-1 font-mono text-[10px] leading-tight text-theme-text-tertiary focus-visible:outline focus-visible:outline-2 focus-visible:outline-skyhook-500"
31
+ >
32
+ {certaintyGlyph(certainty)}
33
+ </span>
34
+ </WithTooltip>
35
+ )
36
+ }
@@ -0,0 +1,24 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { renderToStaticMarkup } from 'react-dom/server'
3
+ import { FactValue } from './facts'
4
+ import { worseTone } from '../ui/status-tone'
5
+
6
+ describe('FactValue', () => {
7
+ const twoDaysAgo = new Date(Date.now() - 2 * 24 * 3600 * 1000 - 60_000).toISOString()
8
+ it('reads a still-current state as lasting since its timestamp', () => {
9
+ const html = renderToStaticMarkup(<FactValue fact={{ text: 'Failing', tone: 'unhealthy', at: twoDaysAgo, atMeaning: 'since' }} />)
10
+ expect(html).toContain('Failing<span class="text-theme-text-secondary"> for 2d</span>')
11
+ expect(html).not.toContain('ago')
12
+ })
13
+ it('reads a past event as an age', () => {
14
+ const html = renderToStaticMarkup(<FactValue fact={{ text: 'Completed', tone: 'healthy', at: twoDaysAgo }} />)
15
+ expect(html).toContain('2d ago')
16
+ })
17
+ })
18
+
19
+ it('combines informational and unread readings independently of argument order', () => {
20
+ expect(worseTone('healthy', 'neutral')).toBe('neutral')
21
+ expect(worseTone('neutral', 'healthy')).toBe('neutral')
22
+ expect(worseTone('neutral', 'unknown')).toBe('unknown')
23
+ expect(worseTone('unknown', 'neutral')).toBe('unknown')
24
+ })
@@ -0,0 +1,62 @@
1
+ import type { ReactNode } from 'react'
2
+ import { clsx } from 'clsx'
3
+ import type { HealthLevel } from '../resources/resource-utils'
4
+ import { formatAge } from '../resources/resource-utils'
5
+ import { formatRelativeAgeTime } from '../../utils/format'
6
+ import { toneTextClass } from '../ui/status-tone'
7
+ import { Tooltip } from '../ui/Tooltip'
8
+
9
+ /**
10
+ * One observed value and where it came from. A value the cluster does not
11
+ * report is a fact too: its text says so and its tone is `unknown`, never a
12
+ * zero or a calm default.
13
+ */
14
+ export interface Fact {
15
+ text: string
16
+ tone: HealthLevel
17
+ /** Where the value comes from, shown next to it so claims carry their source. */
18
+ source?: string
19
+ /** A timestamp the text refers to; the UI renders it as an age. */
20
+ at?: string
21
+ /** `since`: `at` is when a still-current state began, rendered "Failing for 2d" rather than "· 2d ago". */
22
+ atMeaning?: 'since'
23
+ /** The full explanation behind a short `source`, shown on hover only. */
24
+ detail?: string
25
+ }
26
+
27
+ export function FactValue({ fact, className }: { fact: Fact; className?: string }) {
28
+ const age = fact.at && fact.atMeaning === 'since' ? formatAge(fact.at) : null
29
+ const when = fact.at && fact.atMeaning !== 'since' ? formatRelativeAgeTime(fact.at) : null
30
+ const body = (
31
+ <span className={clsx(toneTextClass(fact.tone), className)}>
32
+ {fact.text}
33
+ {age && <span className="text-theme-text-secondary"> for {age}</span>}
34
+ {when && <span className="text-theme-text-secondary">{fact.text ? ' · ' : ''}{when}</span>}
35
+ </span>
36
+ )
37
+ if (!fact.source && !fact.at && !fact.detail) return body
38
+ return (
39
+ <Tooltip content={[fact.at ? `${fact.atMeaning === 'since' ? 'since ' : ''}${new Date(fact.at).toUTCString()}` : null, fact.source, fact.detail].filter(Boolean).join(' · ')} position="top">
40
+ {body}
41
+ </Tooltip>
42
+ )
43
+ }
44
+
45
+ export function FactSource({ fact }: { fact: Fact }) {
46
+ if (!fact.source) return null
47
+ return <div className="mt-0.5 text-[11.5px] text-theme-text-tertiary">{fact.source}</div>
48
+ }
49
+
50
+ /** Label/value rows. Empty values stay visible: an unread value is shown as unread, not hidden. */
51
+ export function FactGrid({ children }: { children: ReactNode }) {
52
+ return <dl className="grid grid-cols-[9.5rem_minmax(0,1fr)] items-baseline gap-x-4 gap-y-2 text-sm">{children}</dl>
53
+ }
54
+
55
+ export function FactRow({ label, children }: { label: ReactNode; children: ReactNode }) {
56
+ return (
57
+ <>
58
+ <dt className="text-theme-text-tertiary">{label}</dt>
59
+ <dd className="min-w-0 break-words text-theme-text-primary">{children}</dd>
60
+ </>
61
+ )
62
+ }
@@ -0,0 +1,6 @@
1
+ // How a surface shows what the cluster reported: each value with its source,
2
+ // partial and unread values marked as such (see DESIGN.md, "Unknown, partial
3
+ // and denied values"). For any renderer, summary or workspace screen.
4
+ export * from './facts'
5
+ export * from './certainty'
6
+ export * from './managed-by'
@@ -0,0 +1,17 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { renderToStaticMarkup } from 'react-dom/server'
3
+ import { ManagedByText, managedByLabel } from './managed-by'
4
+
5
+ describe('ManagedByText', () => {
6
+ it('names the GitOps manager and links it when its namespace is recorded', () => {
7
+ const app = { kind: 'Application', group: 'argoproj.io', namespace: 'argocd', name: 'payments' }
8
+ const html = renderToStaticMarkup(<ManagedByText refTo={app} onNavigate={() => {}} />)
9
+ expect(html).toContain('Argo CD application')
10
+ expect(html).toMatch(/<button[^>]*>argocd\/payments<\/button>/)
11
+ expect(renderToStaticMarkup(<ManagedByText refTo={{ ...app, namespace: '' }} onNavigate={() => {}} />)).not.toContain('<button')
12
+ })
13
+ it('says nothing for a manager that is not a GitOps controller', () => {
14
+ expect(renderToStaticMarkup(<ManagedByText refTo={{ kind: 'HelmRelease', namespace: 'db', name: 'pg' }} />)).toBe('')
15
+ expect(managedByLabel({ kind: 'Kustomization', group: 'kustomize.toolkit.fluxcd.io', namespace: 'flux-system', name: 'apps' })).toBe('Flux Kustomization flux-system/apps')
16
+ })
17
+ })
@@ -0,0 +1,36 @@
1
+ import type { ResourceRef } from '../../types/core'
2
+ import { gitOpsOwnerFromRef, type GitOpsOwnerRef } from '../../utils/gitops-owner'
3
+ import { RefLink, type NavigateToRef } from '../ui/RefLink'
4
+
5
+ function managerLabel(owner: GitOpsOwnerRef): string {
6
+ if (owner.tool === 'argocd') return 'Argo CD application'
7
+ return owner.kind === 'helmreleases' ? 'Flux HelmRelease' : 'Flux Kustomization'
8
+ }
9
+
10
+ /**
11
+ * The GitOps object that manages a resource, from the server's manager
12
+ * detection. Renders nothing for a manager that is not a GitOps controller,
13
+ * and plain text when the manager's namespace is not recorded.
14
+ */
15
+ export function ManagedByText({ refTo, onNavigate }: { refTo: ResourceRef; onNavigate?: NavigateToRef }) {
16
+ const owner = gitOpsOwnerFromRef(refTo)
17
+ if (!owner) return null
18
+ return (
19
+ <span>
20
+ {managerLabel(owner)}{' '}
21
+ {refTo.namespace ? (
22
+ <RefLink refTo={refTo} onNavigate={onNavigate} mono>
23
+ {`${refTo.namespace}/${refTo.name}`}
24
+ </RefLink>
25
+ ) : (
26
+ <span className="font-mono">{refTo.name}</span>
27
+ )}
28
+ </span>
29
+ )
30
+ }
31
+
32
+ /** The manager's label and name as text, for a table cell. */
33
+ export function managedByLabel(refTo: ResourceRef | undefined): string | undefined {
34
+ const owner = refTo && gitOpsOwnerFromRef(refTo)
35
+ return owner ? `${managerLabel(owner)} ${refTo!.namespace ? `${refTo!.namespace}/` : ''}${refTo!.name}` : undefined
36
+ }
@@ -0,0 +1,35 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { renderToStaticMarkup } from 'react-dom/server'
3
+ import {
4
+ FactValue, FactGrid, FactRow, FactSource, RefLink, SummaryHeading,
5
+ WorkspaceProblemCallout, ProblemCallout, ToneDot, toneTextClass,
6
+ type CNPGFact, type CNPGNavigate, type CNPGRef,
7
+ } from '../../index'
8
+ import { FactValue as SharedFactValue } from './facts'
9
+ import { FactValue as CNPGFactValue } from '../cnpg/primitives'
10
+
11
+ describe('public presentation compatibility', () => {
12
+ it('routes existing root and CNPG fact exports to one implementation', () => {
13
+ expect(FactValue).toBe(SharedFactValue)
14
+ expect(CNPGFactValue).toBe(SharedFactValue)
15
+ const fact: CNPGFact = { text: 'Not read — access denied', tone: 'unknown', source: 'Kubernetes API' }
16
+ const html = renderToStaticMarkup(<FactGrid><FactRow label="Instances"><FactValue fact={fact} /><FactSource fact={fact} /></FactRow></FactGrid>)
17
+ expect(html).toContain('Not read — access denied')
18
+ expect(html).toContain('Kubernetes API')
19
+ expect(html).not.toContain('healthy')
20
+ })
21
+ it('keeps the existing optional-group reference and navigation signatures', () => {
22
+ const ref: CNPGRef = { kind: 'Pod', namespace: 'jobs', name: 'worker' }
23
+ const navigate: CNPGNavigate = () => {}
24
+ expect(renderToStaticMarkup(<RefLink refTo={ref} onNavigate={navigate} />)).toContain('<button')
25
+ expect(renderToStaticMarkup(<RefLink refTo={ref} />)).not.toContain('<button')
26
+ })
27
+ it('preserves the existing heading, tone and CNPG problem callout interfaces', () => {
28
+ expect(renderToStaticMarkup(<SummaryHeading>State</SummaryHeading>)).toContain('State')
29
+ expect(renderToStaticMarkup(<ToneDot tone="unhealthy" />)).toContain('bg-rose-500')
30
+ expect(toneTextClass('unknown')).toBe('text-theme-text-tertiary')
31
+ const problem = { id: 'p', category: 'availability' as const, severity: 'warning' as const, title: 'Needs attention', source: 'issue' as const, subject: { kind: 'Pod', group: '', namespace: 'jobs', name: 'worker' } }
32
+ expect(renderToStaticMarkup(<ProblemCallout problem={problem} />)).toContain('worker')
33
+ expect(renderToStaticMarkup(<WorkspaceProblemCallout problem={{ ...problem, tone: 'degraded' }} rootKind="Deployment" />)).toContain('worker')
34
+ })
35
+ })
@@ -4,6 +4,7 @@ import { GitOpsFacetButton, GitOpsFilterSection } from './GitOpsTableView'
4
4
  import { Input } from '../ui/Input'
5
5
  import type { GitOpsResourceTree } from '../../types/gitops-tree'
6
6
  import type { GitOpsTreePreset } from './tree'
7
+ import { treeNodeFilterRole } from './tree/tree-helpers'
7
8
 
8
9
  // =============================================================================
9
10
  // GitOpsGraphFilterRail — the left-side filter sidebar that pairs with
@@ -33,10 +34,12 @@ export function buildTreeFacets(tree: GitOpsResourceTree | null | undefined): Gi
33
34
  kinds: countValues(
34
35
  nodes.filter((n) => n.role !== 'group').map((n) => n.ref.kind).filter(Boolean),
35
36
  ),
36
- sync: countValues(nodes.map((n) => n.sync || 'Unknown')),
37
+ // Dependencies have no sync state of their own; counting them as Unknown
38
+ // would read as a problem.
39
+ sync: countValues(nodes.filter((n) => !n.dependency || n.sync).map((n) => n.sync || 'Unknown')),
37
40
  health: countValues(nodes.map((n) => n.health || 'Unknown')),
38
41
  namespaces: countValues(nodes.map((n) => n.ref.namespace || '(cluster)')),
39
- roles: countValues(nodes.map((n) => n.role)),
42
+ roles: countValues(nodes.map(treeNodeFilterRole)),
40
43
  }
41
44
  }
42
45
 
@@ -211,6 +214,7 @@ function roleLabel(value: string): string {
211
214
  root: 'Root',
212
215
  declared: 'Declared',
213
216
  generated: 'Generated',
217
+ dependency: 'Dependency',
214
218
  group: 'Groups',
215
219
  } as Record<string, string>
216
220
  )[value] ?? value
@@ -0,0 +1,38 @@
1
+ import { renderToStaticMarkup } from 'react-dom/server'
2
+ import { describe, expect, test } from 'vitest'
3
+
4
+ import { APP_TREE_API_ERROR_NOTICE, APP_TREE_API_PENDING_NOTICE, GitOpsHealthSourceNotice, hasHealthSettingsHint, type HealthSourceNoticeSummary } from './GitOpsHealthSourceNotice'
5
+
6
+ const summary = (extra: Partial<HealthSourceNoticeSummary>): HealthSourceNoticeSummary => ({
7
+ tool: 'argocd',
8
+ health: 'Degraded',
9
+ resourceHealthMode: 'appTree',
10
+ resourceHealthApiError: 'the connection is still being set up',
11
+ ...extra,
12
+ })
13
+
14
+ describe('GitOpsHealthSourceNotice while Radar connects to Argo CD', () => {
15
+ test('says it is connecting, without sending the user to Settings', () => {
16
+ const s = summary({ resourceHealthApiPending: true })
17
+ const html = renderToStaticMarkup(<GitOpsHealthSourceNotice summary={s} onOpenSettings={() => {}} />)
18
+ expect(html).toContain(APP_TREE_API_PENDING_NOTICE.replaceAll("'", '&#x27;'))
19
+ expect(html).not.toContain(APP_TREE_API_ERROR_NOTICE.replaceAll("'", '&#x27;'))
20
+ expect(html).not.toContain('Settings')
21
+ expect(hasHealthSettingsHint(s, [])).toBe(false)
22
+ })
23
+
24
+ test('with nothing configured, discovery running keeps the connect link', () => {
25
+ const s = summary({ resourceHealthApiError: undefined, resourceHealthApiPending: true })
26
+ expect(hasHealthSettingsHint(s, [])).toBe(true)
27
+ const html = renderToStaticMarkup(<GitOpsHealthSourceNotice summary={s} onOpenSettings={() => {}} />)
28
+ expect(html).toContain('connect Radar to your Argo CD server')
29
+ })
30
+
31
+ test('a real failure still reads as one and offers Settings', () => {
32
+ const s = summary({ resourceHealthApiError: 'the request timed out' })
33
+ const html = renderToStaticMarkup(<GitOpsHealthSourceNotice summary={s} onOpenSettings={() => {}} />)
34
+ expect(html).toContain('the request timed out')
35
+ expect(html).toContain('Check Argo CD in Settings')
36
+ expect(hasHealthSettingsHint(s, [])).toBe(true)
37
+ })
38
+ })
@@ -31,6 +31,9 @@ export const APP_TREE_PERSIST_REMEDY =
31
31
  // why — the user who configured it doesn't need the Argo CD 3 history
32
32
  // first — then say what the rows below are meanwhile.
33
33
  export const APP_TREE_API_ERROR_NOTICE = "Radar couldn't read Argo's per-resource health from your Argo CD server"
34
+ // The host is still connecting to argocd-server and will ask again; there
35
+ // is nothing to fix, so no Settings link.
36
+ export const APP_TREE_API_PENDING_NOTICE = "Radar is still connecting to your Argo CD server to read Argo's per-resource health."
34
37
  export const APP_TREE_API_ERROR_FINDINGS = 'Meanwhile, rows marked "Found by Radar" are problems Radar detected by looking at the resources itself.'
35
38
  export const APP_TREE_API_ERROR_NO_FINDINGS = "Radar didn't find a problem on its own."
36
39
 
@@ -39,7 +42,7 @@ export const REMOTE_DESTINATION_NOTICE =
39
42
  export const REMOTE_FLUX_TARGET_NOTICE =
40
43
  "This applies its resources to a different cluster, so they aren't visible from here."
41
44
 
42
- export type HealthSourceNoticeSummary = Pick<GitOpsInsightSummary, 'tool' | 'health' | 'resourceHealthMode' | 'remoteDestination' | 'resourceHealthFromApi' | 'resourceHealthApiError'>
45
+ export type HealthSourceNoticeSummary = Pick<GitOpsInsightSummary, 'tool' | 'health' | 'resourceHealthMode' | 'remoteDestination' | 'resourceHealthFromApi' | 'resourceHealthApiError' | 'resourceHealthApiPending'>
43
46
 
44
47
  // The appTree notice explains problems Radar found on its own; on an app
45
48
  // Argo calls Healthy nothing is derived (the host doesn't run the overlay
@@ -63,6 +66,7 @@ export function hasRadarFinding(changes: GitOpsChange[] | undefined): boolean {
63
66
 
64
67
  export function hasHealthSettingsHint(summary: HealthSourceNoticeSummary | undefined, changes: GitOpsChange[] | undefined): boolean {
65
68
  return healthSourceNoticeKind(summary) === 'appTree'
69
+ && !(summary?.resourceHealthApiPending && summary.resourceHealthApiError)
66
70
  && (!!summary?.resourceHealthApiError || (!hasRadarFinding(changes) && summary?.health === 'Degraded'))
67
71
  }
68
72
 
@@ -101,7 +105,8 @@ export function GitOpsHealthSourceNotice({
101
105
  // app with no Radar finding has nothing here to attribute.
102
106
  const radarFinding = hasRadarFinding(changes)
103
107
  const apiError = summary?.resourceHealthApiError
104
- if (kind === 'appTree' && !radarFinding && !hasHealthSettingsHint(summary, changes)) return null
108
+ const pending = !!summary?.resourceHealthApiPending && !!apiError
109
+ if (kind === 'appTree' && !radarFinding && !pending && !hasHealthSettingsHint(summary, changes)) return null
105
110
  const settingsLink = (label: string) =>
106
111
  onOpenSettings ? (
107
112
  <button type="button" onClick={onOpenSettings} className="underline decoration-theme-border underline-offset-2 hover:text-theme-text-primary">
@@ -123,7 +128,12 @@ export function GitOpsHealthSourceNotice({
123
128
  )
124
129
  ) : (
125
130
  <>
126
- {apiError ? (
131
+ {pending ? (
132
+ <>
133
+ {APP_TREE_API_PENDING_NOTICE}{' '}
134
+ {radarFinding ? APP_TREE_API_ERROR_FINDINGS : APP_TREE_API_ERROR_NO_FINDINGS}
135
+ </>
136
+ ) : apiError ? (
127
137
  <>
128
138
  {APP_TREE_API_ERROR_NOTICE}: {apiError}.
129
139
  {onOpenSettings && <> {settingsNote} {settingsLink('Check Argo CD in Settings')}.</>}