@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.
- package/README.md +41 -0
- package/package.json +3 -3
- package/src/components/charts/AreaChart.interaction.test.tsx +118 -0
- package/src/components/charts/AreaChart.test.tsx +25 -0
- package/src/components/charts/AreaChart.tsx +141 -12
- package/src/components/charts/index.ts +1 -0
- package/src/components/checks/ChecksView.tsx +18 -1
- package/src/components/cnpg/CNPGBackupSummary.tsx +57 -9
- package/src/components/cnpg/CNPGClusterHASection.test.tsx +90 -0
- package/src/components/cnpg/CNPGClusterHASection.tsx +331 -0
- package/src/components/cnpg/CNPGClusterSummary.test.tsx +275 -0
- package/src/components/cnpg/CNPGClusterSummary.tsx +310 -96
- package/src/components/cnpg/CNPGConnectSection.test.tsx +64 -0
- package/src/components/cnpg/CNPGConnectSection.tsx +178 -0
- package/src/components/cnpg/CNPGDeclarativeSummary.tsx +157 -26
- package/src/components/cnpg/CNPGImageCatalogSummary.tsx +7 -4
- package/src/components/cnpg/CNPGLogicalPath.test.tsx +22 -0
- package/src/components/cnpg/CNPGLogicalPath.tsx +150 -0
- package/src/components/cnpg/CNPGObjectStoreSummary.tsx +9 -6
- package/src/components/cnpg/CNPGObjectSummary.test.tsx +193 -2
- package/src/components/cnpg/CNPGPoolerScheduling.test.tsx +36 -0
- package/src/components/cnpg/CNPGPoolerSummary.tsx +241 -14
- package/src/components/cnpg/CNPGSharedSummary.tsx +15 -8
- package/src/components/cnpg/CNPGWALArchivingFact.test.tsx +39 -0
- package/src/components/cnpg/backupRuns.test.ts +17 -0
- package/src/components/cnpg/backupRuns.ts +19 -0
- package/src/components/cnpg/connect.test.ts +124 -0
- package/src/components/cnpg/connect.ts +155 -0
- package/src/components/cnpg/databaseRole.test.ts +38 -0
- package/src/components/cnpg/databaseRole.ts +80 -0
- package/src/components/cnpg/ha.test.ts +446 -0
- package/src/components/cnpg/ha.ts +621 -0
- package/src/components/cnpg/index.ts +18 -0
- package/src/components/cnpg/logicalReplication.test.ts +121 -0
- package/src/components/cnpg/logicalReplication.ts +252 -0
- package/src/components/cnpg/pooler.test.ts +84 -0
- package/src/components/cnpg/pooler.ts +225 -0
- package/src/components/cnpg/primitives.tsx +21 -117
- package/src/components/cnpg/problems.ts +12 -0
- package/src/components/cnpg/relations.test.ts +63 -13
- package/src/components/cnpg/relations.ts +61 -52
- package/src/components/cnpg/schedule.ts +35 -0
- package/src/components/cnpg/workspace-disk.test.ts +97 -0
- package/src/components/cnpg/workspace-fleet-metrics.test.ts +250 -0
- package/src/components/cnpg/workspace-problems.test.ts +175 -0
- package/src/components/cnpg/workspace.test.ts +380 -9
- package/src/components/cnpg/workspace.ts +1137 -136
- package/src/components/dock/DockContext.test.tsx +32 -0
- package/src/components/dock/DockContext.tsx +18 -4
- package/src/components/dock/LocalTerminalTab.test.tsx +40 -0
- package/src/components/dock/LocalTerminalTab.tsx +4 -3
- package/src/components/dock/TerminalTab.tsx +20 -8
- package/src/components/facts/certainty.tsx +36 -0
- package/src/components/facts/facts.test.tsx +24 -0
- package/src/components/facts/facts.tsx +62 -0
- package/src/components/facts/index.ts +6 -0
- package/src/components/facts/managed-by.test.tsx +17 -0
- package/src/components/facts/managed-by.tsx +36 -0
- package/src/components/facts/public-exports.test.tsx +35 -0
- package/src/components/gitops/GitOpsGraphFilterRail.tsx +6 -2
- package/src/components/gitops/GitOpsHealthSourceNotice.test.tsx +38 -0
- package/src/components/gitops/GitOpsHealthSourceNotice.tsx +13 -3
- package/src/components/gitops/insights/ArgoResourceDiff.test.tsx +115 -0
- package/src/components/gitops/insights/ArgoResourceDiff.tsx +22 -5
- package/src/components/gitops/insights/DriftPanel.tsx +62 -0
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +24 -71
- package/src/components/gitops/tree/GitOpsTreeGraph.test.ts +26 -1
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +38 -55
- package/src/components/gitops/tree/layout.test.ts +111 -0
- package/src/components/gitops/tree/layout.ts +128 -0
- package/src/components/gitops/tree/tree-helpers.test.ts +17 -0
- package/src/components/gitops/tree/tree-helpers.ts +7 -1
- package/src/components/issues/ConditionalCleanup.test.tsx +82 -0
- package/src/components/issues/IssuesView.tsx +40 -5
- package/src/components/issues/ResourceIssuesSection.test.tsx +28 -0
- package/src/components/issues/ResourceIssuesSection.tsx +3 -0
- package/src/components/issues/index.ts +3 -1
- package/src/components/issues/issue-timing.ts +12 -1
- package/src/components/issues/issues.test.ts +49 -1
- package/src/components/issues/severity.ts +21 -0
- package/src/components/issues/types.ts +23 -0
- package/src/components/logs/LogCore.theme.test.tsx +81 -3
- package/src/components/logs/LogCore.tsx +25 -17
- package/src/components/logs/LogToolbarSelects.tsx +3 -1
- package/src/components/logs/LogsViewer.test.tsx +291 -0
- package/src/components/logs/LogsViewer.tsx +72 -11
- package/src/components/logs/StructuredLogLine.test.tsx +76 -0
- package/src/components/logs/StructuredLogLine.tsx +22 -30
- package/src/components/logs/WorkloadLogsViewer.test.tsx +65 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +39 -10
- package/src/components/logs/useLogBuffer.test.ts +8 -0
- package/src/components/logs/useLogStream.ts +17 -1
- package/src/components/problems/index.ts +1 -0
- package/src/components/problems/problems.test.tsx +63 -0
- package/src/components/problems/problems.tsx +165 -0
- package/src/components/resources/ActiveFilterBar.test.tsx +78 -0
- package/src/components/resources/ActiveFilterBar.tsx +86 -0
- package/src/components/resources/ResourcesSidebar.keyboard.test.tsx +42 -0
- package/src/components/resources/ResourcesSidebar.test.tsx +36 -2
- package/src/components/resources/ResourcesSidebar.tsx +92 -41
- package/src/components/resources/ResourcesView.kind-filter-memory.test.ts +52 -0
- package/src/components/resources/ResourcesView.kind-view.test.tsx +149 -0
- package/src/components/resources/ResourcesView.per-container.test.ts +56 -1
- package/src/components/resources/ResourcesView.tsx +302 -109
- package/src/components/resources/curated-column-ownership.test.ts +3 -3
- package/src/components/resources/renderers/AutoscalingReferences.test.tsx +286 -0
- package/src/components/resources/renderers/CAPIClusterRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIExactReferenceNavigation.test.tsx +27 -0
- package/src/components/resources/renderers/CNPGBackupRenderer.test.tsx +23 -0
- package/src/components/resources/renderers/CNPGBackupRenderer.tsx +8 -19
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +5 -7
- package/src/components/resources/renderers/CNPGDeclarativeRenderer.tsx +25 -0
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.test.tsx +32 -0
- package/src/components/resources/renderers/CNPGScheduledBackupRenderer.tsx +7 -15
- package/src/components/resources/renderers/CompositeRenderer.navigation.test.tsx +35 -0
- package/src/components/resources/renderers/CompositeRenderer.tsx +3 -3
- package/src/components/resources/renderers/CompositionRenderer.tsx +3 -3
- package/src/components/resources/renderers/ContourExactReferenceNavigation.test.tsx +34 -0
- package/src/components/resources/renderers/ContourHTTPProxyRenderer.tsx +11 -10
- package/src/components/resources/renderers/EndpointSliceRenderer.test.tsx +39 -1
- package/src/components/resources/renderers/EndpointSliceRenderer.tsx +14 -13
- package/src/components/resources/renderers/HPARenderer.tsx +56 -16
- package/src/components/resources/renderers/IngressRenderer.defaultBackend.test.tsx +33 -0
- package/src/components/resources/renderers/IngressRenderer.tsx +30 -3
- package/src/components/resources/renderers/IstioDestinationRuleRenderer.tsx +9 -9
- package/src/components/resources/renderers/IstioExactReferenceNavigation.test.tsx +34 -0
- package/src/components/resources/renderers/IstioVirtualServiceRenderer.tsx +5 -8
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +13 -11
- package/src/components/resources/renderers/KnativeEventingRenderer.tsx +24 -31
- package/src/components/resources/renderers/KnativeExactReferenceNavigation.test.tsx +47 -0
- package/src/components/resources/renderers/KnativeFlowRenderer.tsx +6 -7
- package/src/components/resources/renderers/KnativeNetworkingRenderer.tsx +7 -9
- package/src/components/resources/renderers/KnativeReferenceLink.tsx +14 -0
- package/src/components/resources/renderers/KnativeReferenceLinks.test.tsx +60 -0
- package/src/components/resources/renderers/KnativeSourceRenderer.tsx +9 -11
- package/src/components/resources/renderers/ManagedResourceRenderer.tsx +2 -2
- package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +3 -2
- package/src/components/resources/renderers/PodRenderer.test.tsx +32 -0
- package/src/components/resources/renderers/PodRenderer.tsx +41 -29
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +1 -1
- package/src/components/resources/renderers/VPARenderer.tsx +17 -11
- package/src/components/resources/resource-utils-capi.ts +1 -1
- package/src/components/resources/resource-utils-cnpg.test.ts +35 -10
- package/src/components/resources/resource-utils-cnpg.ts +51 -20
- package/src/components/resources/resource-utils-contour.test.ts +44 -0
- package/src/components/resources/resource-utils-contour.ts +16 -0
- package/src/components/resources/resource-utils-istio.ts +12 -8
- package/src/components/resources/resource-utils-knative.ts +18 -0
- package/src/components/resources/resource-utils.ts +82 -6
- package/src/components/resources/rules-column-width.test.ts +1 -1
- package/src/components/resources/summarize-scheduler-message.test.ts +30 -0
- package/src/components/shared/ActionConfirmDialog.test.tsx +154 -0
- package/src/components/shared/ActionConfirmDialog.tsx +246 -0
- package/src/components/shared/CreateResourceDialog.test.tsx +330 -0
- package/src/components/shared/CreateResourceDialog.tsx +126 -80
- package/src/components/shared/DetailShell.test.tsx +80 -0
- package/src/components/shared/DetailShell.tsx +85 -10
- package/src/components/shared/EditableYamlView.test.tsx +7 -0
- package/src/components/shared/EditableYamlView.tsx +1 -1
- package/src/components/shared/ResourceActionsBar.tsx +11 -4
- package/src/components/shared/ResourceRendererDispatch.tsx +12 -9
- package/src/components/shared/index.ts +2 -0
- package/src/components/timeline/TimelineSwimlanes.tsx +1 -1
- package/src/components/timeline/timeline-filters.ts +1 -0
- package/src/components/topology/K8sResourceNode.test.ts +18 -1
- package/src/components/topology/K8sResourceNode.tsx +44 -2
- package/src/components/topology/TopologyFilterSidebar.tsx +1 -0
- package/src/components/topology/TopologyGraph.tsx +3 -71
- package/src/components/topology/TopologySearch.tsx +1 -0
- package/src/components/topology/internet-groups.test.ts +35 -0
- package/src/components/topology/internet-groups.ts +55 -0
- package/src/components/topology/layout.ts +1 -1
- package/src/components/ui/Badge.tsx +1 -0
- package/src/components/ui/FoldSection.tsx +68 -0
- package/src/components/ui/ForceDeleteConfirmDialog.test.tsx +62 -0
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +24 -12
- package/src/components/ui/RefLink.tsx +20 -0
- package/src/components/ui/RowActionMenu.tsx +8 -3
- package/src/components/ui/crd-definition-navigation.test.tsx +160 -0
- package/src/components/ui/drawer-components.test.tsx +110 -1
- package/src/components/ui/drawer-components.tsx +62 -9
- package/src/components/ui/index.ts +3 -1
- package/src/components/ui/monacoEditorFeatures.ts +77 -0
- package/src/components/ui/monacoRuntime.ts +14 -19
- package/src/components/ui/status-tone.test.ts +10 -1
- package/src/components/ui/status-tone.tsx +29 -0
- package/src/components/workload/WorkloadView.deleted.test.tsx +43 -0
- package/src/components/workload/WorkloadView.relationships.test.tsx +37 -0
- package/src/components/workload/WorkloadView.spec.test.tsx +149 -0
- package/src/components/workload/WorkloadView.tsx +180 -64
- package/src/index.ts +14 -1
- package/src/monaco-deep.d.ts +19 -12
- package/src/theme/components.css +13 -0
- package/src/types/capacity.ts +11 -2
- package/src/types/core.ts +29 -2
- package/src/types/gitops-flux-status.test.ts +32 -1
- package/src/types/gitops-insights.ts +19 -2
- package/src/types/gitops-tree.ts +10 -0
- package/src/types/gitops.ts +26 -6
- package/src/utils/api-resources.ts +7 -0
- package/src/utils/application-history.ts +1 -0
- package/src/utils/cnpg-backup.test.ts +26 -0
- package/src/utils/cnpg-backup.ts +70 -0
- package/src/utils/format.test.ts +9 -0
- package/src/utils/format.ts +4 -0
- package/src/utils/gitops-owner.ts +3 -2
- package/src/utils/grant.test.ts +19 -0
- package/src/utils/grant.ts +35 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/k8s-errors.ts +7 -7
- package/src/utils/log-format.test.ts +41 -0
- package/src/utils/log-format.ts +71 -15
- package/src/utils/log-level.ts +12 -8
- package/src/utils/navigation.test.ts +57 -0
- package/src/utils/navigation.ts +86 -8
- package/src/utils/object-reference-navigation.test.ts +43 -0
- package/src/utils/resource-hierarchy.test.ts +37 -0
- package/src/utils/resource-hierarchy.ts +15 -7
- package/src/utils/resource-icons.ts +2 -0
- package/src/utils/skeleton-yaml.test.ts +21 -0
- package/src/utils/skeleton-yaml.ts +16 -0
- package/src/utils/url-path.test.ts +35 -0
- package/src/utils/url-path.ts +21 -0
- package/src/utils/validators.test.ts +4 -13
- package/src/utils/validators.ts +5 -19
- package/src/utils/view-transition.test.ts +35 -1
- 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-
|
|
296
|
-
<div className=
|
|
297
|
-
<div className="text-xs text-theme-text-
|
|
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-
|
|
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-
|
|
307
|
-
|
|
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'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=
|
|
339
|
-
<div className="text-xs text-theme-text-
|
|
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
|
-
|
|
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(
|
|
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("'", '''))
|
|
19
|
+
expect(html).not.toContain(APP_TREE_API_ERROR_NOTICE.replaceAll("'", '''))
|
|
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
|
-
|
|
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
|
-
{
|
|
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')}.</>}
|