@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,62 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act } from 'react'
|
|
3
|
+
import { createRoot, type Root } from 'react-dom/client'
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
5
|
+
import { ForceDeleteConfirmDialog } from './ForceDeleteConfirmDialog'
|
|
6
|
+
|
|
7
|
+
let root: Root
|
|
8
|
+
const base = { open: true, onClose: vi.fn(), onConfirm: vi.fn(), resourceName: 'web', resourceKind: 'Deployment', namespaceName: 'demo', isLoading: false }
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })
|
|
11
|
+
const host = document.createElement('div')
|
|
12
|
+
document.body.appendChild(host)
|
|
13
|
+
root = createRoot(host)
|
|
14
|
+
})
|
|
15
|
+
afterEach(async () => {
|
|
16
|
+
await act(async () => root.unmount())
|
|
17
|
+
document.body.replaceChildren()
|
|
18
|
+
vi.clearAllMocks()
|
|
19
|
+
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: false })
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
describe('Deletion preview states', () => {
|
|
23
|
+
it('states the namespace-wide deletion consequence without relying on topology', async () => {
|
|
24
|
+
await act(async () => root.render(<ForceDeleteConfirmDialog {...base} resourceKind="Namespace" resourceName="demo" namespaceName="" cascadeRootResolved={false} />))
|
|
25
|
+
expect(document.body.textContent).toContain('delete the namespace "demo" and the resources in it')
|
|
26
|
+
expect(document.body.textContent).not.toContain("not in Radar's topology")
|
|
27
|
+
})
|
|
28
|
+
it('shows one advisory statement for a resolved preview', async () => {
|
|
29
|
+
await act(async () => root.render(<ForceDeleteConfirmDialog {...base} cascadeRootResolved cascadeDependents={[{ kind: 'Pod', namespace: 'demo', name: 'web-pod' }]} />))
|
|
30
|
+
const toggle = [...document.querySelectorAll('button')].find(b => b.textContent?.includes('May affect'))!
|
|
31
|
+
await act(async () => toggle.click())
|
|
32
|
+
expect(document.body.textContent).toContain('web-pod')
|
|
33
|
+
expect(document.body.textContent?.match(/does not predict what will be deleted/g)).toHaveLength(1)
|
|
34
|
+
expect(document.body.textContent).not.toContain('Other resources may be affected')
|
|
35
|
+
})
|
|
36
|
+
it.each([[], undefined])('omits the advisory statement for an empty resolved preview (%j)', async (dependents) => {
|
|
37
|
+
await act(async () => root.render(<ForceDeleteConfirmDialog {...base} cascadeRootResolved cascadeDependents={dependents} />))
|
|
38
|
+
expect(document.body.textContent).not.toContain('May affect')
|
|
39
|
+
expect(document.body.textContent).not.toContain('does not predict')
|
|
40
|
+
expect(document.body.textContent).not.toContain('unavailable')
|
|
41
|
+
expect(document.body.textContent).not.toContain('Other resources may be affected')
|
|
42
|
+
})
|
|
43
|
+
it('distinguishes an observed unresolved root from a fetch failure', async () => {
|
|
44
|
+
await act(async () => root.render(<ForceDeleteConfirmDialog {...base} cascadeRootResolved={false} />))
|
|
45
|
+
expect(document.body.textContent).toContain("This resource is not in Radar's topology")
|
|
46
|
+
await act(async () => root.render(<ForceDeleteConfirmDialog {...base} cascadeRootResolved={false} cascadePreviewFailed cascadeDependents={[{ kind: 'Pod', namespace: 'demo', name: 'stale' }]} />))
|
|
47
|
+
expect(document.body.textContent).toContain('Related-resource preview unavailable.')
|
|
48
|
+
expect(document.body.textContent).not.toContain('This resource is not in')
|
|
49
|
+
expect(document.body.textContent).not.toContain('Other resources may be affected')
|
|
50
|
+
expect(document.body.textContent).not.toContain('May affect')
|
|
51
|
+
const confirm = [...document.querySelectorAll('button')].find(b => b.textContent === 'Delete')!
|
|
52
|
+
expect(confirm.disabled).toBe(false)
|
|
53
|
+
await act(async () => confirm.click())
|
|
54
|
+
expect(base.onConfirm).toHaveBeenCalledWith(false)
|
|
55
|
+
})
|
|
56
|
+
it('shows loading without presenting stale failure or success as a result', async () => {
|
|
57
|
+
await act(async () => root.render(<ForceDeleteConfirmDialog {...base} cascadeLoading cascadePreviewFailed cascadeRootResolved />))
|
|
58
|
+
expect(document.body.textContent).toContain('Checking related resources')
|
|
59
|
+
expect(document.body.textContent).not.toContain('unavailable')
|
|
60
|
+
expect(document.body.textContent).not.toContain('does not predict')
|
|
61
|
+
})
|
|
62
|
+
})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState, useMemo } from 'react'
|
|
2
|
-
import {
|
|
2
|
+
import { Loader2 } from 'lucide-react'
|
|
3
3
|
import { Collapse, CollapseChevron, useDisclosure } from './Collapse'
|
|
4
4
|
import { ConfirmDialog } from './ConfirmDialog'
|
|
5
5
|
import { pluralize } from '../../utils/pluralize'
|
|
@@ -22,6 +22,7 @@ interface ForceDeleteConfirmDialogProps {
|
|
|
22
22
|
cascadeDependents?: CascadeDependent[]
|
|
23
23
|
cascadeLoading?: boolean
|
|
24
24
|
cascadeRootResolved?: boolean
|
|
25
|
+
cascadePreviewFailed?: boolean
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
export function ForceDeleteConfirmDialog({
|
|
@@ -35,8 +36,10 @@ export function ForceDeleteConfirmDialog({
|
|
|
35
36
|
cascadeDependents,
|
|
36
37
|
cascadeLoading,
|
|
37
38
|
cascadeRootResolved,
|
|
39
|
+
cascadePreviewFailed,
|
|
38
40
|
}: ForceDeleteConfirmDialogProps) {
|
|
39
41
|
const [forceDelete, setForceDelete] = useState(false)
|
|
42
|
+
const isNamespace = resourceKind.toLowerCase() === 'namespace'
|
|
40
43
|
|
|
41
44
|
function handleClose() {
|
|
42
45
|
onClose()
|
|
@@ -54,7 +57,7 @@ export function ForceDeleteConfirmDialog({
|
|
|
54
57
|
onConfirm={handleConfirm}
|
|
55
58
|
title="Delete Resource"
|
|
56
59
|
message={`Are you sure you want to delete "${resourceName}"?`}
|
|
57
|
-
details={`This will permanently delete the ${resourceKind} "${resourceName}" from the "${namespaceName}" namespace.`}
|
|
60
|
+
details={isNamespace ? `This will permanently delete the namespace "${resourceName}" and the resources in it.` : `This will permanently delete the ${resourceKind} "${resourceName}" from the "${namespaceName}" namespace.`}
|
|
58
61
|
confirmLabel={forceDelete ? 'Force Delete' : 'Delete'}
|
|
59
62
|
variant="danger"
|
|
60
63
|
isLoading={isLoading}
|
|
@@ -63,21 +66,30 @@ export function ForceDeleteConfirmDialog({
|
|
|
63
66
|
{cascadeLoading && (
|
|
64
67
|
<div className="flex items-center gap-2 text-xs text-theme-text-tertiary">
|
|
65
68
|
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
|
66
|
-
Checking
|
|
69
|
+
Checking related resources...
|
|
67
70
|
</div>
|
|
68
71
|
)}
|
|
69
72
|
|
|
70
|
-
{!cascadeLoading && cascadeDependents && cascadeDependents.length > 0 && (
|
|
73
|
+
{!cascadeLoading && !cascadePreviewFailed && cascadeDependents && cascadeDependents.length > 0 && (
|
|
71
74
|
<CascadeDependentsList dependents={cascadeDependents} />
|
|
72
75
|
)}
|
|
73
76
|
|
|
74
|
-
{!cascadeLoading &&
|
|
75
|
-
<
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
{!cascadeLoading && cascadePreviewFailed && (
|
|
78
|
+
<p className="text-xs text-theme-text-tertiary">
|
|
79
|
+
Related-resource preview unavailable.
|
|
80
|
+
</p>
|
|
81
|
+
)}
|
|
82
|
+
|
|
83
|
+
{!isNamespace && !cascadeLoading && !cascadePreviewFailed && cascadeRootResolved === false && (
|
|
84
|
+
<p className="text-xs text-theme-text-tertiary">
|
|
85
|
+
This resource is not in Radar's topology, so its related resources cannot be previewed.
|
|
86
|
+
</p>
|
|
87
|
+
)}
|
|
88
|
+
|
|
89
|
+
{!cascadeLoading && !cascadePreviewFailed && cascadeRootResolved === true && cascadeDependents && cascadeDependents.length > 0 && (
|
|
90
|
+
<p className="text-xs text-theme-text-secondary">
|
|
91
|
+
This list shows visible related resources; it does not predict what will be deleted.
|
|
92
|
+
</p>
|
|
81
93
|
)}
|
|
82
94
|
|
|
83
95
|
<label className="flex items-center gap-2 text-sm text-theme-text-secondary">
|
|
@@ -120,7 +132,7 @@ function CascadeDependentsList({ dependents }: { dependents: CascadeDependent[]
|
|
|
120
132
|
>
|
|
121
133
|
<CollapseChevron open={expanded} inheritColor className="w-3.5 h-3.5" />
|
|
122
134
|
<span>
|
|
123
|
-
|
|
135
|
+
May affect {pluralize(dependents.length, 'related resource')}
|
|
124
136
|
</span>
|
|
125
137
|
</button>
|
|
126
138
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import { clsx } from 'clsx'
|
|
3
|
+
import type { ResourceRef } from '../../types/core'
|
|
4
|
+
|
|
5
|
+
export type NavigateToRef = (ref: ResourceRef) => void
|
|
6
|
+
|
|
7
|
+
/** A reference to another object: a link when the host can navigate, plain text otherwise. */
|
|
8
|
+
export function RefLink({ refTo, onNavigate, children, mono }: { refTo: ResourceRef; onNavigate?: NavigateToRef; children?: ReactNode; mono?: boolean }) {
|
|
9
|
+
const label = children ?? refTo.name
|
|
10
|
+
if (!onNavigate) return <span className={clsx(mono && 'font-mono')}>{label}</span>
|
|
11
|
+
return (
|
|
12
|
+
<button
|
|
13
|
+
type="button"
|
|
14
|
+
onClick={() => onNavigate(refTo)}
|
|
15
|
+
className={clsx('text-accent-text hover:underline text-left break-all', mono && 'font-mono')}
|
|
16
|
+
>
|
|
17
|
+
{label}
|
|
18
|
+
</button>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
@@ -56,14 +56,19 @@ export function RowActionMenu({ items, ariaLabel = 'Row actions', compact = true
|
|
|
56
56
|
const onDown = (e: MouseEvent) => {
|
|
57
57
|
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false)
|
|
58
58
|
}
|
|
59
|
+
// Capture phase, consumed: Escape that closes the menu must not also run
|
|
60
|
+
// the page's own Escape (close the drawer, leave the full view).
|
|
59
61
|
const onKey = (e: KeyboardEvent) => {
|
|
60
|
-
if (e.key
|
|
62
|
+
if (e.key !== 'Escape') return
|
|
63
|
+
e.preventDefault()
|
|
64
|
+
e.stopPropagation()
|
|
65
|
+
setOpen(false)
|
|
61
66
|
}
|
|
62
67
|
document.addEventListener('mousedown', onDown)
|
|
63
|
-
document.addEventListener('keydown', onKey)
|
|
68
|
+
document.addEventListener('keydown', onKey, true)
|
|
64
69
|
return () => {
|
|
65
70
|
document.removeEventListener('mousedown', onDown)
|
|
66
|
-
document.removeEventListener('keydown', onKey)
|
|
71
|
+
document.removeEventListener('keydown', onKey, true)
|
|
67
72
|
}
|
|
68
73
|
}, [open])
|
|
69
74
|
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act } from "react";
|
|
3
|
+
import { createRoot } from "react-dom/client";
|
|
4
|
+
import { renderToStaticMarkup } from "react-dom/server";
|
|
5
|
+
import { afterEach, expect, it, vi } from "vitest";
|
|
6
|
+
import { MetadataSection } from "./drawer-components";
|
|
7
|
+
import {
|
|
8
|
+
customResourceDefinitionRef,
|
|
9
|
+
initNavigationMap,
|
|
10
|
+
resetNavigationMap,
|
|
11
|
+
} from "../../utils/navigation";
|
|
12
|
+
|
|
13
|
+
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
|
|
14
|
+
afterEach(resetNavigationMap);
|
|
15
|
+
const resources = [
|
|
16
|
+
{
|
|
17
|
+
group: "plants.example.io",
|
|
18
|
+
version: "v1",
|
|
19
|
+
kind: "Cactus",
|
|
20
|
+
name: "cacti",
|
|
21
|
+
definitionName: "cacti.plants.example.io",
|
|
22
|
+
namespaced: true,
|
|
23
|
+
isCrd: true,
|
|
24
|
+
verbs: ["get"],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
group: "other.example.io",
|
|
28
|
+
version: "v1",
|
|
29
|
+
kind: "Cactus",
|
|
30
|
+
name: "cactuses",
|
|
31
|
+
definitionName: "cactuses.other.example.io",
|
|
32
|
+
namespaced: false,
|
|
33
|
+
isCrd: true,
|
|
34
|
+
verbs: ["get"],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
group: "apps",
|
|
38
|
+
version: "v1",
|
|
39
|
+
kind: "Deployment",
|
|
40
|
+
name: "deployments",
|
|
41
|
+
namespaced: true,
|
|
42
|
+
isCrd: false,
|
|
43
|
+
verbs: ["get"],
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
it("uses exact discovered CRD group and plural instead of guessing from the Kind", () => {
|
|
48
|
+
expect(
|
|
49
|
+
customResourceDefinitionRef("plants.example.io/v1", "Cactus"),
|
|
50
|
+
).toBeNull();
|
|
51
|
+
initNavigationMap(resources);
|
|
52
|
+
expect(customResourceDefinitionRef("plants.example.io/v1", "Cactus")).toEqual(
|
|
53
|
+
{
|
|
54
|
+
kind: "CustomResourceDefinition",
|
|
55
|
+
group: "apiextensions.k8s.io",
|
|
56
|
+
namespace: "",
|
|
57
|
+
name: "cacti.plants.example.io",
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
expect(
|
|
61
|
+
customResourceDefinitionRef("other.example.io/v1", "Cactus")?.name,
|
|
62
|
+
).toBe("cactuses.other.example.io");
|
|
63
|
+
expect(customResourceDefinitionRef("apps/v1", "Deployment")).toBeNull();
|
|
64
|
+
initNavigationMap([
|
|
65
|
+
...resources,
|
|
66
|
+
{
|
|
67
|
+
group: "metrics.k8s.io",
|
|
68
|
+
version: "v1beta1",
|
|
69
|
+
kind: "PodMetrics",
|
|
70
|
+
name: "pods",
|
|
71
|
+
namespaced: true,
|
|
72
|
+
isCrd: true,
|
|
73
|
+
verbs: ["get"],
|
|
74
|
+
},
|
|
75
|
+
]);
|
|
76
|
+
expect(
|
|
77
|
+
customResourceDefinitionRef("metrics.k8s.io/v1beta1", "PodMetrics"),
|
|
78
|
+
).toBeNull();
|
|
79
|
+
resetNavigationMap();
|
|
80
|
+
expect(
|
|
81
|
+
customResourceDefinitionRef("plants.example.io/v1", "Cactus"),
|
|
82
|
+
).toBeNull();
|
|
83
|
+
initNavigationMap([
|
|
84
|
+
{
|
|
85
|
+
...resources[0],
|
|
86
|
+
name: "cactuses",
|
|
87
|
+
definitionName: "cactuses.plants.example.io",
|
|
88
|
+
},
|
|
89
|
+
]);
|
|
90
|
+
expect(
|
|
91
|
+
customResourceDefinitionRef("plants.example.io/v1", "Cactus")?.name,
|
|
92
|
+
).toBe("cactuses.plants.example.io");
|
|
93
|
+
expect(
|
|
94
|
+
customResourceDefinitionRef("unknown.example.io/v1", "Cactus"),
|
|
95
|
+
).toBeNull();
|
|
96
|
+
expect(customResourceDefinitionRef(undefined, "Cactus")).toBeNull();
|
|
97
|
+
initNavigationMap([]);
|
|
98
|
+
expect(
|
|
99
|
+
customResourceDefinitionRef("plants.example.io/v1", "Cactus"),
|
|
100
|
+
).toBeNull();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("navigates from instance metadata to its cluster-scoped definition", async () => {
|
|
104
|
+
initNavigationMap(resources);
|
|
105
|
+
const element = document.createElement("div");
|
|
106
|
+
const root = createRoot(element);
|
|
107
|
+
const onNavigate = vi.fn();
|
|
108
|
+
try {
|
|
109
|
+
await act(async () =>
|
|
110
|
+
root.render(
|
|
111
|
+
<MetadataSection
|
|
112
|
+
data={{
|
|
113
|
+
apiVersion: "plants.example.io/v1",
|
|
114
|
+
kind: "Cactus",
|
|
115
|
+
metadata: { name: "live", namespace: "team" },
|
|
116
|
+
}}
|
|
117
|
+
onNavigate={onNavigate}
|
|
118
|
+
/>,
|
|
119
|
+
),
|
|
120
|
+
);
|
|
121
|
+
const button = [...element.querySelectorAll("button")].find(
|
|
122
|
+
(b) => b.textContent === "cacti.plants.example.io",
|
|
123
|
+
);
|
|
124
|
+
expect(button).toBeDefined();
|
|
125
|
+
await act(async () => button!.click());
|
|
126
|
+
expect(onNavigate).toHaveBeenCalledWith({
|
|
127
|
+
kind: "CustomResourceDefinition",
|
|
128
|
+
group: "apiextensions.k8s.io",
|
|
129
|
+
namespace: "",
|
|
130
|
+
name: "cacti.plants.example.io",
|
|
131
|
+
});
|
|
132
|
+
} finally {
|
|
133
|
+
await act(async () => root.unmount());
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("keeps exact Kind casing within one API group", () => {
|
|
138
|
+
initNavigationMap([
|
|
139
|
+
...resources,
|
|
140
|
+
{ ...resources[0], kind: "CACTUS", name: "uppercacti", definitionName: "uppercacti.plants.example.io" },
|
|
141
|
+
]);
|
|
142
|
+
expect(customResourceDefinitionRef("plants.example.io/v1", "Cactus")?.name).toBe("cacti.plants.example.io");
|
|
143
|
+
expect(customResourceDefinitionRef("plants.example.io/v1", "CACTUS")?.name).toBe("uppercacti.plants.example.io");
|
|
144
|
+
expect(customResourceDefinitionRef("plants.example.io/v1", "cactus")).toBeNull();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("does not add a guessed definition to built-in or undiscovered metadata", () => {
|
|
148
|
+
initNavigationMap(resources);
|
|
149
|
+
const html = renderToStaticMarkup(
|
|
150
|
+
<MetadataSection
|
|
151
|
+
data={{
|
|
152
|
+
apiVersion: "apps/v1",
|
|
153
|
+
kind: "Deployment",
|
|
154
|
+
metadata: { name: "live", uid: "native" },
|
|
155
|
+
}}
|
|
156
|
+
/>,
|
|
157
|
+
);
|
|
158
|
+
expect(html).toContain("native");
|
|
159
|
+
expect(html).not.toContain("Definition");
|
|
160
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
import { renderToString } from 'react-dom/server'
|
|
3
|
-
import { EventsSection, ProblemAlerts, OperationalIssuesShownContext, RelatedResourcesSection, defaultConditionTone } from './drawer-components'
|
|
3
|
+
import { AlertBanner, EventsSection, ProblemAlerts, OperationalIssuesShownContext, RelatedResourcesSection, defaultConditionTone } from './drawer-components'
|
|
4
4
|
|
|
5
5
|
const problems = [
|
|
6
6
|
{ color: 'red' as const, message: 'Application is Degraded' },
|
|
@@ -127,3 +127,112 @@ describe('namespace deletion condition polarity', () => {
|
|
|
127
127
|
expect(defaultConditionTone({ type, status: 'Unknown' })).toBe('unknown')
|
|
128
128
|
})
|
|
129
129
|
})
|
|
130
|
+
|
|
131
|
+
describe('monitors projection', () => {
|
|
132
|
+
it('renders Monitored By alone and deduplicates by API group', () => {
|
|
133
|
+
const ref = { kind: 'Widget', group: 'one.example', namespace: 'demo', name: 'shared' }
|
|
134
|
+
const html = renderToString(<RelatedResourcesSection relationships={{ monitors: [ref, ref, { ...ref, group: 'two.example' }] }} onNavigate={() => {}} />)
|
|
135
|
+
expect(html).toContain('Related Resources')
|
|
136
|
+
expect(html).toContain('Monitored By')
|
|
137
|
+
expect(html.match(/<button/g)).toHaveLength(3)
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
describe('monitorTargets projection', () => {
|
|
143
|
+
it('renders Monitors alone and deduplicates by API group', () => {
|
|
144
|
+
const ref = { kind: 'Widget', group: 'one.example', namespace: 'demo', name: 'shared' }
|
|
145
|
+
const html = renderToString(<RelatedResourcesSection relationships={{ monitorTargets: [ref, ref, { ...ref, group: 'two.example' }] }} onNavigate={() => {}} />)
|
|
146
|
+
expect(html).toContain('Related Resources')
|
|
147
|
+
expect(html).toContain('Monitors')
|
|
148
|
+
expect(html.match(/<button/g)).toHaveLength(3)
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
describe('backends projection', () => {
|
|
154
|
+
it('renders Routes To alone and deduplicates by API group', () => {
|
|
155
|
+
const ref = { kind: 'Widget', group: 'one.example', namespace: 'demo', name: 'shared' }
|
|
156
|
+
const html = renderToString(<RelatedResourcesSection relationships={{ backends: [ref, ref, { ...ref, group: 'two.example' }] }} onNavigate={() => {}} />)
|
|
157
|
+
expect(html).toContain('Related Resources')
|
|
158
|
+
expect(html).toContain('Routes To')
|
|
159
|
+
expect(html.match(/<button/g)).toHaveLength(3)
|
|
160
|
+
})
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
describe('routedFrom projection', () => {
|
|
165
|
+
it('renders Routed From alone and deduplicates by API group', () => {
|
|
166
|
+
const ref = { kind: 'Widget', group: 'one.example', namespace: 'demo', name: 'shared' }
|
|
167
|
+
const html = renderToString(<RelatedResourcesSection relationships={{ routedFrom: [ref, ref, { ...ref, group: 'two.example' }] }} onNavigate={() => {}} />)
|
|
168
|
+
expect(html).toContain('Related Resources')
|
|
169
|
+
expect(html).toContain('Routed From')
|
|
170
|
+
expect(html.match(/<button/g)).toHaveLength(3)
|
|
171
|
+
})
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
describe('protects projection', () => {
|
|
176
|
+
it('renders Selects alone and deduplicates by API group', () => {
|
|
177
|
+
const ref = { kind: 'Widget', group: 'one.example', namespace: 'demo', name: 'shared' }
|
|
178
|
+
const html = renderToString(<RelatedResourcesSection relationships={{ protects: [ref, ref, { ...ref, group: 'two.example' }] }} onNavigate={() => {}} />)
|
|
179
|
+
expect(html).toContain('Related Resources')
|
|
180
|
+
expect(html).toContain('Selects')
|
|
181
|
+
expect(html.match(/<button/g)).toHaveLength(3)
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
describe('healthChecks projection', () => {
|
|
187
|
+
it('renders Health Checks alone and deduplicates by API group', () => {
|
|
188
|
+
const ref = { kind: 'Widget', group: 'one.example', namespace: 'demo', name: 'shared' }
|
|
189
|
+
const html = renderToString(<RelatedResourcesSection relationships={{ healthChecks: [ref, ref, { ...ref, group: 'two.example' }] }} onNavigate={() => {}} />)
|
|
190
|
+
expect(html).toContain('Related Resources')
|
|
191
|
+
expect(html).toContain('Health Checks')
|
|
192
|
+
expect(html.match(/<button/g)).toHaveLength(3)
|
|
193
|
+
})
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
describe('stagedPolicies projection', () => {
|
|
198
|
+
it('renders Staged Policies alone and deduplicates by API group', () => {
|
|
199
|
+
const ref = { kind: 'Widget', group: 'one.example', namespace: 'demo', name: 'shared' }
|
|
200
|
+
const html = renderToString(<RelatedResourcesSection relationships={{ stagedPolicies: [ref, ref, { ...ref, group: 'two.example' }] }} onNavigate={() => {}} />)
|
|
201
|
+
expect(html).toContain('Related Resources')
|
|
202
|
+
expect(html).toContain('Staged Policies')
|
|
203
|
+
expect(html.match(/<button/g)).toHaveLength(3)
|
|
204
|
+
})
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
describe('routing version skew', () => {
|
|
209
|
+
it('keeps older Services, Gateways, Routes and Pods fields visible', () => {
|
|
210
|
+
const html = renderToString(<RelatedResourcesSection relationships={{ services: [{kind: 'Service', namespace: 'demo', name: 'web'}], gateways: [{kind: 'HTTPRoute', group: 'gateway.networking.k8s.io', namespace: 'demo', name: 'old-route'}], routes: [{kind: 'IngressRoute', group: 'traefik.io', namespace: 'demo', name: 'route'}], pods: [{kind: 'Deployment', group: 'apps', namespace: 'demo', name: 'old-target'}] }} />)
|
|
211
|
+
for (const label of ['Services', 'Gateways', 'Routes', 'Pods', 'old-route', 'old-target']) expect(html).toContain(label)
|
|
212
|
+
})
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
describe('related resource chips', () => {
|
|
216
|
+
it('tells a Knative Service apart from a core Service', () => {
|
|
217
|
+
const html = renderToString(<RelatedResourcesSection relationships={{
|
|
218
|
+
services: [{ kind: 'Service', namespace: 'team', name: 'web-private' }],
|
|
219
|
+
routedFrom: [{ kind: 'Service', group: 'serving.knative.dev', namespace: 'team', name: 'web' }],
|
|
220
|
+
}} onNavigate={() => {}} />)
|
|
221
|
+
expect(html).toContain('>svc<!-- -->/</span>web-private<')
|
|
222
|
+
expect(html).toContain('>ksvc<!-- -->/</span>web<')
|
|
223
|
+
})
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
describe('AlertBanner optional actions', () => {
|
|
228
|
+
it('retains the title-only default without introducing a layout wrapper', () => {
|
|
229
|
+
const html = renderToString(<AlertBanner variant="info" title="Paused" />)
|
|
230
|
+
expect(html).toMatch(/<div class="text-sm font-medium [^"]*">Paused<\/div>/)
|
|
231
|
+
expect(html).not.toContain('justify-between'); expect(html).not.toContain('flex-1')
|
|
232
|
+
})
|
|
233
|
+
it.each([undefined, 'Check connection'])('places the optional action next to the title, retaining the body (%s)', message => {
|
|
234
|
+
const html = renderToString(<AlertBanner variant="warning" title="Metrics unavailable" message={message} action={<button>Open settings</button>} />)
|
|
235
|
+
expect(html).toContain('Metrics unavailable'); expect(html).toContain('<button>Open settings</button>')
|
|
236
|
+
expect(html).toContain('flex-wrap'); if (message) expect(html).toContain(message)
|
|
237
|
+
})
|
|
238
|
+
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { dedupeResourceRefs, omitResourceRefs } from '../../utils/resource-refs'
|
|
2
|
+
import { customResourceDefinitionRef, getCRDDefinitionsSnapshot, subscribeCRDDefinitions } from '../../utils/navigation'
|
|
2
3
|
import { Disclosure } from './Disclosure'
|
|
3
|
-
import { createContext, useContext, useState } from 'react'
|
|
4
|
+
import { createContext, useContext, useState, useSyncExternalStore } from 'react'
|
|
4
5
|
import { Copy, Check, Tag, AlertTriangle, CheckCircle, ExternalLink, Layers, X, Minus } from 'lucide-react'
|
|
5
6
|
import { clsx } from 'clsx'
|
|
6
7
|
import { formatDuration, formatResources } from '../resources/resource-utils'
|
|
@@ -392,6 +393,8 @@ export function ConditionsSection({
|
|
|
392
393
|
// ============================================================================
|
|
393
394
|
|
|
394
395
|
const ALERT_COLORS = {
|
|
396
|
+
alert: { bg: 'status-alert', border: '', title: '', message: '', list: '', bullet: '' },
|
|
397
|
+
unknown: { bg: 'status-unknown', border: '', title: '', message: '', list: '', bullet: '' },
|
|
395
398
|
error: { bg: 'bg-red-50 dark:bg-red-950/30', border: 'border-red-200 dark:border-red-800/30', title: 'text-red-700 dark:text-red-300', message: 'text-red-600/80 dark:text-red-300/70', list: 'text-red-600 dark:text-red-400', bullet: 'text-red-400/60 dark:text-red-400/50' },
|
|
396
399
|
warning: { bg: 'bg-amber-50 dark:bg-amber-950/30', border: 'border-amber-200 dark:border-amber-800/30', title: 'text-amber-700 dark:text-amber-300', message: 'text-amber-600/80 dark:text-amber-300/70', list: 'text-amber-600 dark:text-amber-400', bullet: 'text-amber-400/60 dark:text-amber-400/50' },
|
|
397
400
|
info: { bg: 'bg-sky-50 dark:bg-sky-950/30', border: 'border-sky-200 dark:border-sky-800/30', title: 'text-sky-700 dark:text-sky-300', message: 'text-sky-600/80 dark:text-sky-300/70', list: 'text-sky-600 dark:text-sky-400', bullet: 'text-sky-400/60 dark:text-sky-400/50' },
|
|
@@ -399,6 +402,8 @@ const ALERT_COLORS = {
|
|
|
399
402
|
} as const
|
|
400
403
|
|
|
401
404
|
const DEFAULT_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {
|
|
405
|
+
alert: AlertTriangle,
|
|
406
|
+
unknown: AlertTriangle,
|
|
402
407
|
error: AlertTriangle,
|
|
403
408
|
warning: AlertTriangle,
|
|
404
409
|
info: AlertTriangle,
|
|
@@ -406,17 +411,19 @@ const DEFAULT_ICONS: Record<string, React.ComponentType<{ className?: string }>>
|
|
|
406
411
|
}
|
|
407
412
|
|
|
408
413
|
interface AlertBannerProps {
|
|
409
|
-
variant:
|
|
414
|
+
variant: keyof typeof ALERT_COLORS
|
|
410
415
|
icon?: React.ComponentType<{ className?: string }>
|
|
411
416
|
title: string
|
|
412
417
|
message?: React.ReactNode
|
|
413
418
|
items?: string[]
|
|
414
419
|
children?: React.ReactNode
|
|
415
|
-
/**
|
|
420
|
+
/** A control at the end of the title line, e.g. a link to where the problem is handled. */
|
|
421
|
+
action?: React.ReactNode
|
|
422
|
+
/** Replaces the default bottom margin (`mb-4`). */
|
|
416
423
|
className?: string
|
|
417
424
|
}
|
|
418
425
|
|
|
419
|
-
export function AlertBanner({ variant, icon, title, message, items, children, className }: AlertBannerProps) {
|
|
426
|
+
export function AlertBanner({ variant, icon, title, message, items, children, action, className }: AlertBannerProps) {
|
|
420
427
|
const colors = ALERT_COLORS[variant]
|
|
421
428
|
const Icon = icon || DEFAULT_ICONS[variant]
|
|
422
429
|
const hasBody = message || items || children
|
|
@@ -427,7 +434,14 @@ export function AlertBanner({ variant, icon, title, message, items, children, cl
|
|
|
427
434
|
<Icon className={clsx('w-4 h-4 shrink-0', colors.title, hasBody && 'mt-0.5')} />
|
|
428
435
|
{hasBody ? (
|
|
429
436
|
<div className="flex-1 min-w-0">
|
|
430
|
-
|
|
437
|
+
{action ? (
|
|
438
|
+
<div className={clsx('flex flex-wrap items-baseline justify-between gap-x-3 gap-y-1', items && 'mb-1')}>
|
|
439
|
+
<div className={clsx('min-w-0 text-sm font-medium', colors.title)}>{title}</div>
|
|
440
|
+
<div className="shrink-0">{action}</div>
|
|
441
|
+
</div>
|
|
442
|
+
) : (
|
|
443
|
+
<div className={clsx('text-sm font-medium', colors.title, items && 'mb-1')}>{title}</div>
|
|
444
|
+
)}
|
|
431
445
|
{message && <div className={clsx('text-xs mt-1 break-words', colors.message)}>{message}</div>}
|
|
432
446
|
{items && items.length > 0 && (
|
|
433
447
|
<ul className={clsx('text-xs space-y-1', colors.list)}>
|
|
@@ -441,6 +455,11 @@ export function AlertBanner({ variant, icon, title, message, items, children, cl
|
|
|
441
455
|
)}
|
|
442
456
|
{children}
|
|
443
457
|
</div>
|
|
458
|
+
) : action ? (
|
|
459
|
+
<div className="flex min-w-0 flex-1 flex-wrap items-center justify-between gap-x-3 gap-y-1">
|
|
460
|
+
<div className={clsx('min-w-0 text-sm font-medium', colors.title)}>{title}</div>
|
|
461
|
+
<div className="shrink-0">{action}</div>
|
|
462
|
+
</div>
|
|
444
463
|
) : (
|
|
445
464
|
<div className={clsx('text-sm font-medium', colors.title)}>{title}</div>
|
|
446
465
|
)}
|
|
@@ -527,13 +546,16 @@ export function AnnotationsSection({ data }: { data: any }) {
|
|
|
527
546
|
)
|
|
528
547
|
}
|
|
529
548
|
|
|
530
|
-
export function MetadataSection({ data }: { data: any }) {
|
|
549
|
+
export function MetadataSection({ data, onNavigate }: { data: any; onNavigate?: (ref: ResourceRef) => void }) {
|
|
550
|
+
useSyncExternalStore(subscribeCRDDefinitions, getCRDDefinitionsSnapshot, getCRDDefinitionsSnapshot)
|
|
531
551
|
const meta = data.metadata
|
|
532
552
|
if (!meta) return null
|
|
553
|
+
const definition = customResourceDefinitionRef(data.apiVersion, data.kind)
|
|
533
554
|
|
|
534
555
|
return (
|
|
535
556
|
<Section title="Metadata" defaultExpanded>
|
|
536
557
|
<PropertyList>
|
|
558
|
+
{definition && <Property label="Definition" value={<ResourceLink {...definition} onNavigate={onNavigate} />} />}
|
|
537
559
|
<Property label="UID" value={meta.uid} />
|
|
538
560
|
<Property label="Resource Version" value={meta.resourceVersion} />
|
|
539
561
|
<Property label="Generation" value={meta.generation} />
|
|
@@ -739,7 +761,7 @@ export function formatKindName(kind: string): string {
|
|
|
739
761
|
pods: 'Pod', deployments: 'Deployment', daemonsets: 'DaemonSet', statefulsets: 'StatefulSet',
|
|
740
762
|
replicasets: 'ReplicaSet', services: 'Service', endpointslices: 'EndpointSlice', ingresses: 'Ingress',
|
|
741
763
|
gateways: 'Gateway', httproutes: 'HTTPRoute', grpcroutes: 'GRPCRoute',
|
|
742
|
-
tcproutes: 'TCPRoute', tlsroutes: 'TLSRoute', configmaps: 'ConfigMap',
|
|
764
|
+
tcproutes: 'TCPRoute', tlsroutes: 'TLSRoute', udproutes: 'UDPRoute', configmaps: 'ConfigMap',
|
|
743
765
|
secrets: 'Secret', jobs: 'Job', cronjobs: 'CronJob', hpas: 'HPA',
|
|
744
766
|
horizontalpodautoscalers: 'HPA', nodes: 'Node', namespaces: 'Namespace',
|
|
745
767
|
persistentvolumeclaims: 'PVC', persistentvolumes: 'PV',
|
|
@@ -828,6 +850,13 @@ export function RelatedResourcesSection({ relationships, onNavigate }: RelatedRe
|
|
|
828
850
|
(relationships.resourceClaims && relationships.resourceClaims.length > 0) ||
|
|
829
851
|
(relationships.dependencies && relationships.dependencies.length > 0) ||
|
|
830
852
|
(relationships.dependents && relationships.dependents.length > 0) ||
|
|
853
|
+
(relationships.monitors && relationships.monitors.length > 0) ||
|
|
854
|
+
(relationships.monitorTargets && relationships.monitorTargets.length > 0) ||
|
|
855
|
+
(relationships.backends && relationships.backends.length > 0) ||
|
|
856
|
+
(relationships.routedFrom && relationships.routedFrom.length > 0) ||
|
|
857
|
+
(relationships.protects && relationships.protects.length > 0) ||
|
|
858
|
+
(relationships.healthChecks && relationships.healthChecks.length > 0) ||
|
|
859
|
+
(relationships.stagedPolicies && relationships.stagedPolicies.length > 0) ||
|
|
831
860
|
relationships.scaleTarget
|
|
832
861
|
|
|
833
862
|
if (!hasRelationships) return null
|
|
@@ -889,6 +918,27 @@ export function RelatedResourcesSection({ relationships, onNavigate }: RelatedRe
|
|
|
889
918
|
{relationships.dependents && relationships.dependents.length > 0 && (
|
|
890
919
|
<RelationshipGroup label="Required By" refs={dedupeResourceRefs(relationships.dependents)} onNavigate={onNavigate} />
|
|
891
920
|
)}
|
|
921
|
+
{relationships.monitors && relationships.monitors.length > 0 && (
|
|
922
|
+
<RelationshipGroup label="Monitored By" refs={dedupeResourceRefs(relationships.monitors)} onNavigate={onNavigate} />
|
|
923
|
+
)}
|
|
924
|
+
{relationships.monitorTargets && relationships.monitorTargets.length > 0 && (
|
|
925
|
+
<RelationshipGroup label="Monitors" refs={dedupeResourceRefs(relationships.monitorTargets)} onNavigate={onNavigate} />
|
|
926
|
+
)}
|
|
927
|
+
{relationships.backends && relationships.backends.length > 0 && (
|
|
928
|
+
<RelationshipGroup label="Routes To" refs={dedupeResourceRefs(relationships.backends)} onNavigate={onNavigate} />
|
|
929
|
+
)}
|
|
930
|
+
{relationships.routedFrom && relationships.routedFrom.length > 0 && (
|
|
931
|
+
<RelationshipGroup label="Routed From" refs={dedupeResourceRefs(relationships.routedFrom)} onNavigate={onNavigate} />
|
|
932
|
+
)}
|
|
933
|
+
{relationships.protects && relationships.protects.length > 0 && (
|
|
934
|
+
<RelationshipGroup label="Selects" refs={dedupeResourceRefs(relationships.protects)} onNavigate={onNavigate} />
|
|
935
|
+
)}
|
|
936
|
+
{relationships.healthChecks && relationships.healthChecks.length > 0 && (
|
|
937
|
+
<RelationshipGroup label="Health Checks" refs={dedupeResourceRefs(relationships.healthChecks)} onNavigate={onNavigate} />
|
|
938
|
+
)}
|
|
939
|
+
{relationships.stagedPolicies && relationships.stagedPolicies.length > 0 && (
|
|
940
|
+
<RelationshipGroup label="Staged Policies" refs={dedupeResourceRefs(relationships.stagedPolicies)} onNavigate={onNavigate} />
|
|
941
|
+
)}
|
|
892
942
|
{relationships.scaleTarget && (
|
|
893
943
|
<RelationshipGroup label="Scale Target" refs={[relationships.scaleTarget]} onNavigate={onNavigate} />
|
|
894
944
|
)}
|
|
@@ -940,7 +990,7 @@ export interface ResourceRefBadgeProps {
|
|
|
940
990
|
/** Reusable chip/badge for showing a related resource with click-to-navigate */
|
|
941
991
|
export function ResourceRefBadge({ resourceRef, onClick, wrapAtSeparator }: ResourceRefBadgeProps) {
|
|
942
992
|
const kindClass = getKindColor(resourceRef.kind)
|
|
943
|
-
const kindName = formatKindForRef(resourceRef.kind)
|
|
993
|
+
const kindName = formatKindForRef(resourceRef.kind, resourceRef.group)
|
|
944
994
|
const content = wrapAtSeparator ? (
|
|
945
995
|
<>
|
|
946
996
|
<span className="shrink-0 opacity-60">{kindName}/</span>
|
|
@@ -1003,8 +1053,10 @@ export function ResourceLink({ name, kind, namespace = '', group, label, onNavig
|
|
|
1003
1053
|
)
|
|
1004
1054
|
}
|
|
1005
1055
|
|
|
1006
|
-
function formatKindForRef(kind: string): string {
|
|
1056
|
+
function formatKindForRef(kind: string, group?: string): string {
|
|
1007
1057
|
const k = kind.toLowerCase()
|
|
1058
|
+
// A Knative Service shares its kind with core Services; use Knative's own short name.
|
|
1059
|
+
if (k === 'service' && group === 'serving.knative.dev') return 'ksvc'
|
|
1008
1060
|
const shortNames: Record<string, string> = {
|
|
1009
1061
|
deployment: 'deploy',
|
|
1010
1062
|
daemonset: 'ds',
|
|
@@ -1017,6 +1069,7 @@ function formatKindForRef(kind: string): string {
|
|
|
1017
1069
|
httproute: 'hr',
|
|
1018
1070
|
grpcroute: 'grpc',
|
|
1019
1071
|
tcproute: 'tcp',
|
|
1072
|
+
udproute: 'udp',
|
|
1020
1073
|
tlsroute: 'tls',
|
|
1021
1074
|
secret: 'secret',
|
|
1022
1075
|
pod: 'pod',
|
|
@@ -19,7 +19,9 @@ export type { SelectMenuOption } from './SelectMenu'
|
|
|
19
19
|
export { Input } from './Input'
|
|
20
20
|
export { FilterPill } from './FilterPill'
|
|
21
21
|
export type { FilterPillTone } from './FilterPill'
|
|
22
|
-
export { StatusDot, mapHealthToTone } from './status-tone'
|
|
22
|
+
export { StatusDot, mapHealthToTone, toneFillClass, toneTextClass, worseTone } from './status-tone'
|
|
23
|
+
export { RefLink, type NavigateToRef } from './RefLink'
|
|
24
|
+
export { FoldSection, SectionHeading, type FoldSummary } from './FoldSection'
|
|
23
25
|
export type { StatusTone, StatusDotProps } from './status-tone'
|
|
24
26
|
export { DialogPortal } from './DialogPortal'
|
|
25
27
|
export { ConfirmDialog } from './ConfirmDialog'
|