@skyhook-io/k8s-ui 1.17.0 → 1.18.0
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 +6 -6
- 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 +7 -3
- 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 +142 -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 +40 -81
- package/src/components/gitops/insights/insights-helpers.test.ts +18 -0
- package/src/components/gitops/insights/insights-helpers.ts +7 -0
- package/src/components/gitops/tree/GitOpsTreeGraph.test.ts +91 -2
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +76 -76
- 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 +23 -0
- package/src/components/gitops/tree/tree-helpers.ts +12 -2
- package/src/components/issues/ConditionalCleanup.test.tsx +82 -0
- package/src/components/issues/IssuesView.tsx +44 -6
- package/src/components/issues/ResourceIssuesSection.test.tsx +28 -0
- package/src/components/issues/ResourceIssuesSection.tsx +8 -2
- package/src/components/issues/diagnostic.ts +2 -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 +75 -1
- package/src/components/issues/severity.ts +30 -2
- package/src/components/issues/types.ts +55 -4
- package/src/components/logs/LogCore.theme.test.tsx +206 -3
- package/src/components/logs/LogCore.tsx +180 -53
- package/src/components/logs/LogToolbarSelects.tsx +3 -1
- package/src/components/logs/LogsViewer.test.tsx +369 -0
- package/src/components/logs/LogsViewer.tsx +91 -13
- package/src/components/logs/StructuredLogLine.test.tsx +76 -0
- package/src/components/logs/StructuredLogLine.tsx +23 -31
- package/src/components/logs/WorkloadLogsViewer.test.tsx +65 -0
- package/src/components/logs/WorkloadLogsViewer.tsx +46 -11
- package/src/components/logs/useLogBuffer.reading.test.tsx +149 -0
- package/src/components/logs/useLogBuffer.test.ts +8 -0
- package/src/components/logs/useLogBuffer.ts +48 -18
- package/src/components/logs/useLogSearch.ts +3 -1
- 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/ColumnPickerList.test.tsx +46 -0
- package/src/components/resources/ColumnPickerList.tsx +174 -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 +296 -0
- package/src/components/resources/ResourcesView.per-container.test.ts +56 -1
- package/src/components/resources/ResourcesView.tsx +392 -181
- package/src/components/resources/column-order.test.ts +104 -0
- package/src/components/resources/column-order.ts +64 -0
- package/src/components/resources/curated-column-ownership.test.ts +3 -3
- package/src/components/resources/renderers/ArgoApplicationRenderer.test.tsx +15 -0
- package/src/components/resources/renderers/ArgoApplicationRenderer.tsx +4 -1
- 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/CronJobRenderer.test.tsx +31 -0
- package/src/components/resources/renderers/CronJobRenderer.tsx +28 -20
- package/src/components/resources/renderers/EndpointSliceRenderer.test.tsx +39 -1
- package/src/components/resources/renderers/EndpointSliceRenderer.tsx +14 -13
- package/src/components/resources/renderers/FluxHelmReleaseRenderer.tsx +22 -9
- 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.sidecar-terminal.test.tsx +200 -0
- package/src/components/resources/renderers/PodRenderer.test.tsx +32 -0
- package/src/components/resources/renderers/PodRenderer.tsx +81 -40
- 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 +96 -7
- package/src/components/resources/rules-column-width.test.ts +1 -1
- package/src/components/resources/summarize-scheduler-message.test.ts +31 -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/DrainPlanDialog.test.tsx +7 -0
- package/src/components/shared/DrainPlanDialog.tsx +6 -2
- package/src/components/shared/EditableYamlView.test.tsx +7 -0
- package/src/components/shared/EditableYamlView.tsx +1 -1
- package/src/components/shared/ResourceActionsBar.tsx +14 -7
- package/src/components/shared/ResourceRendererDispatch.tsx +15 -10
- package/src/components/shared/index.ts +2 -0
- package/src/components/timeline/DiffViewer.test.tsx +69 -0
- package/src/components/timeline/DiffViewer.tsx +113 -4
- package/src/components/timeline/TimelineSwimlanes.tsx +1 -1
- package/src/components/timeline/shared.test.tsx +48 -0
- package/src/components/timeline/shared.tsx +14 -19
- 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/Toast.tsx +15 -4
- 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 +4 -2
- 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 +193 -71
- 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 +53 -2
- package/src/types/gitops-flux-status.test.ts +32 -1
- package/src/types/gitops-insights.ts +20 -2
- package/src/types/gitops-tree.ts +12 -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
|
@@ -19,6 +19,7 @@ export interface DrainPlanPod {
|
|
|
19
19
|
pdb?: string
|
|
20
20
|
pdbChecked: boolean
|
|
21
21
|
terminating?: boolean
|
|
22
|
+
expectedSkip?: boolean
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
export interface DrainPlan {
|
|
@@ -95,7 +96,8 @@ const OUTCOME_LABEL: Record<DrainOutcome, string> = {
|
|
|
95
96
|
|
|
96
97
|
const ESTIMATE_CAVEAT =
|
|
97
98
|
'An estimate, not a guarantee: the drain re-lists live state when it runs, and pods, budgets and permissions can change until then. ' +
|
|
98
|
-
'
|
|
99
|
+
'Drain normally waits up to 2 minutes for evictions and pod deletion, retrying when a disruption budget temporarily blocks eviction. ' +
|
|
100
|
+
'Older Radar versions may return before pods are deleted. A pod covered by more than one budget is refused outright.'
|
|
99
101
|
|
|
100
102
|
interface DrainPlanContentProps {
|
|
101
103
|
nodeName: string
|
|
@@ -217,7 +219,9 @@ export function DrainPlanContent({
|
|
|
217
219
|
{current.pods.map((p) => (
|
|
218
220
|
<tr key={`${p.namespace}/${p.name}`} className="border-b border-theme-border last:border-b-0 align-top">
|
|
219
221
|
<td className="px-2 py-1.5 whitespace-nowrap">
|
|
220
|
-
<Badge severity={OUTCOME_SEVERITY[p.outcome]} size="sm">
|
|
222
|
+
<Badge severity={p.outcome === 'skip' && p.expectedSkip === false ? 'warning' : OUTCOME_SEVERITY[p.outcome]} size="sm">
|
|
223
|
+
{p.outcome === 'skip' && p.expectedSkip !== undefined ? p.expectedSkip ? 'left by design' : 'requires attention' : OUTCOME_LABEL[p.outcome]}
|
|
224
|
+
</Badge>
|
|
221
225
|
</td>
|
|
222
226
|
<td className="px-2 py-1.5 font-mono whitespace-nowrap text-theme-text-primary">
|
|
223
227
|
{p.namespace}/{p.name}
|
|
@@ -266,3 +266,10 @@ it("drops a refresh from an apply made before going back to a newer review", asy
|
|
|
266
266
|
expect.objectContaining({ reviewedResourceVersion: "3" }),
|
|
267
267
|
);
|
|
268
268
|
});
|
|
269
|
+
|
|
270
|
+
it("uses the accent token for a generic ConfigMap Edit control", () => {
|
|
271
|
+
render({ onSave: async () => {}, onPreview: async () => reviewed("1") });
|
|
272
|
+
const edit = [...host.querySelectorAll("button")].find((b) => b.textContent?.trim() === "Edit")!;
|
|
273
|
+
expect(edit.classList.contains("text-accent-text")).toBe(true);
|
|
274
|
+
expect(edit.className).not.toContain("text-blue-");
|
|
275
|
+
});
|
|
@@ -552,7 +552,7 @@ export function EditableYamlView({
|
|
|
552
552
|
{!readOnly && (
|
|
553
553
|
<button
|
|
554
554
|
onClick={handleStartEdit}
|
|
555
|
-
className="flex items-center gap-1 px-2 py-1 text-xs text-
|
|
555
|
+
className="flex items-center gap-1 px-2 py-1 text-xs text-accent-text hover:text-accent-light hover:bg-theme-elevated rounded"
|
|
556
556
|
>
|
|
557
557
|
<Pencil className="w-3.5 h-3.5" />
|
|
558
558
|
Edit
|
|
@@ -28,7 +28,7 @@ import { DialogPortal } from '../ui/DialogPortal'
|
|
|
28
28
|
import type { SelectedResource, WorkloadRevision } from '../../types'
|
|
29
29
|
import type { RadarUpgradeRequirement } from '../../types/fetch-error'
|
|
30
30
|
import { displayKindName } from '../ui/drawer-components'
|
|
31
|
-
import { getDefaultContainerName } from '../resources/resource-utils'
|
|
31
|
+
import { getDefaultContainerName, getExecContainerNames } from '../resources/resource-utils'
|
|
32
32
|
import { SetImageDialog, type SetImageOwnership } from './SetImageDialog'
|
|
33
33
|
import { DrainPlanDialog, DEFAULT_DRAIN_DIALOG_OPTIONS, type DrainDialogOptions, type DrainPlan } from './DrainPlanDialog'
|
|
34
34
|
import type { WorkloadImageInventory, WorkloadImageUpdate } from '../../types/core'
|
|
@@ -42,6 +42,7 @@ import { formatRelativeAgeTime } from '../../utils/format'
|
|
|
42
42
|
|
|
43
43
|
interface ResourceActionsBarProps {
|
|
44
44
|
resource: SelectedResource
|
|
45
|
+
leadingActions?: React.ReactNode
|
|
45
46
|
data: any
|
|
46
47
|
onClose?: () => void
|
|
47
48
|
hideLogs?: boolean
|
|
@@ -80,6 +81,7 @@ interface ResourceActionsBarProps {
|
|
|
80
81
|
cascadeDependents?: CascadeDependent[]
|
|
81
82
|
cascadeLoading?: boolean
|
|
82
83
|
cascadeRootResolved?: boolean
|
|
84
|
+
cascadePreviewFailed?: boolean
|
|
83
85
|
|
|
84
86
|
// Workload restart
|
|
85
87
|
onRestart?: (params: { kind: string; namespace: string; name: string }, callbacks?: { onSuccess?: () => void; onError?: (err: unknown) => void }) => void
|
|
@@ -155,13 +157,13 @@ interface ResourceActionsBarProps {
|
|
|
155
157
|
}
|
|
156
158
|
|
|
157
159
|
export function ResourceActionsBar({
|
|
158
|
-
resource, data, onClose, hideLogs, showYaml, onToggleYaml,
|
|
160
|
+
resource, data, leadingActions, onClose, hideLogs, showYaml, onToggleYaml,
|
|
159
161
|
onCompareTo,
|
|
160
162
|
onCompareAcrossClusters,
|
|
161
163
|
canExec, canViewLogs, canPortForward,
|
|
162
164
|
onOpenTerminal, onOpenLogs: openLogs, onOpenWorkloadLogs: openWorkloadLogs, onCopyCommand,
|
|
163
165
|
renderPortForward,
|
|
164
|
-
onDelete, isDeleting, cascadeDependents, cascadeLoading, cascadeRootResolved,
|
|
166
|
+
onDelete, isDeleting, cascadeDependents, cascadeLoading, cascadeRootResolved, cascadePreviewFailed,
|
|
165
167
|
onRestart, isRestarting,
|
|
166
168
|
onLoadImages, onSetImages, isSettingImages, imageOwnership,
|
|
167
169
|
revisions: revisionsList, revisionsLoading, revisionsError, onRollback, isRollingBack,
|
|
@@ -252,7 +254,7 @@ export function ResourceActionsBar({
|
|
|
252
254
|
namespace: resource.namespace,
|
|
253
255
|
podName: resource.name,
|
|
254
256
|
containerName: getDefaultContainerName(data) || containers[0],
|
|
255
|
-
containers,
|
|
257
|
+
containers: getExecContainerNames(data),
|
|
256
258
|
})
|
|
257
259
|
}
|
|
258
260
|
}
|
|
@@ -291,6 +293,8 @@ export function ResourceActionsBar({
|
|
|
291
293
|
|
|
292
294
|
return (
|
|
293
295
|
<div className="flex items-center gap-1.5 px-4 py-2 flex-wrap">
|
|
296
|
+
<div className="flex min-w-0 flex-wrap items-center gap-1.5 empty:hidden" data-action-group="object">
|
|
297
|
+
{leadingActions}
|
|
294
298
|
{/* Kind-specific actions (left) */}
|
|
295
299
|
{kind === 'pods' && (
|
|
296
300
|
<>
|
|
@@ -366,7 +370,7 @@ export function ResourceActionsBar({
|
|
|
366
370
|
onUncordonNode && (
|
|
367
371
|
<button
|
|
368
372
|
onClick={() => onUncordonNode({ name: resource.name })}
|
|
369
|
-
disabled={isUncordoningNode}
|
|
373
|
+
disabled={isUncordoningNode || isDrainingNode}
|
|
370
374
|
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-white bg-emerald-600 hover:bg-emerald-700 rounded-lg transition-colors disabled:opacity-50"
|
|
371
375
|
>
|
|
372
376
|
<Play className={`w-3.5 h-3.5 ${isUncordoningNode ? 'animate-pulse' : ''}`} />
|
|
@@ -586,8 +590,8 @@ export function ResourceActionsBar({
|
|
|
586
590
|
</button>
|
|
587
591
|
)}
|
|
588
592
|
|
|
589
|
-
|
|
590
|
-
<div className="flex-1"
|
|
593
|
+
</div>
|
|
594
|
+
<div className="ml-auto flex flex-wrap items-center gap-1.5" data-action-group="utility">
|
|
591
595
|
|
|
592
596
|
{/* Universal actions (right-aligned). The AI/Diagnose action is NOT here — it
|
|
593
597
|
lives in the detail header chrome (see WorkloadView), set apart from these
|
|
@@ -673,6 +677,8 @@ export function ResourceActionsBar({
|
|
|
673
677
|
</Tooltip>
|
|
674
678
|
)}
|
|
675
679
|
|
|
680
|
+
</div>
|
|
681
|
+
|
|
676
682
|
<ForceDeleteConfirmDialog
|
|
677
683
|
open={showDeleteConfirm}
|
|
678
684
|
onClose={() => setShowDeleteConfirm(false)}
|
|
@@ -684,6 +690,7 @@ export function ResourceActionsBar({
|
|
|
684
690
|
cascadeDependents={cascadeDependents}
|
|
685
691
|
cascadeLoading={cascadeLoading}
|
|
686
692
|
cascadeRootResolved={cascadeRootResolved}
|
|
693
|
+
cascadePreviewFailed={cascadePreviewFailed}
|
|
687
694
|
/>
|
|
688
695
|
|
|
689
696
|
{onLoadImages && onSetImages && (
|
|
@@ -193,6 +193,7 @@ import {
|
|
|
193
193
|
CNPGDatabaseRenderer,
|
|
194
194
|
CNPGPublicationRenderer,
|
|
195
195
|
CNPGSubscriptionRenderer,
|
|
196
|
+
CNPGDatabaseRoleRenderer,
|
|
196
197
|
CNPGImageCatalogRenderer,
|
|
197
198
|
KyvernoGlobalContextRenderer,
|
|
198
199
|
KyvernoUpdateRequestRenderer,
|
|
@@ -313,6 +314,7 @@ import type { ScalerDiagnosis } from '../resources/renderers/WorkloadRenderer'
|
|
|
313
314
|
* When an override is not provided, the base (shared) renderer is used.
|
|
314
315
|
*/
|
|
315
316
|
export interface RendererOverrides {
|
|
317
|
+
FluxHelmReleaseRenderer?: React.ComponentType<React.ComponentProps<typeof FluxHelmReleaseRenderer>>
|
|
316
318
|
JobRenderer?: React.ComponentType<{ data: any; onNavigate?: (ref: ResourceRef) => void }>
|
|
317
319
|
JobSetRenderer?: React.ComponentType<{ data: any; onNavigate?: (ref: ResourceRef) => void }>
|
|
318
320
|
RayJobRenderer?: React.ComponentType<{ data: any; onNavigate?: (ref: ResourceRef) => void }>
|
|
@@ -454,7 +456,7 @@ const KNOWN_KINDS = new Set([
|
|
|
454
456
|
'rollouts', 'analysisruns', 'analysistemplates', 'clusteranalysistemplates', 'experiments', 'certificates', 'workflows', 'persistentvolumes',
|
|
455
457
|
'storageclasses', 'certificaterequests', 'clusterissuers', 'issuers',
|
|
456
458
|
'orders', 'challenges',
|
|
457
|
-
'gateways', 'gatewayclasses', 'httproutes', 'grpcroutes', 'tcproutes', 'tlsroutes', 'sealedsecrets', 'workflowtemplates', 'clusterworkflowtemplates',
|
|
459
|
+
'gateways', 'gatewayclasses', 'httproutes', 'grpcroutes', 'tcproutes', 'tlsroutes', 'udproutes', 'sealedsecrets', 'workflowtemplates', 'clusterworkflowtemplates',
|
|
458
460
|
'networkpolicies', 'networkpolicy',
|
|
459
461
|
'globalnetworkpolicies', 'globalnetworkpolicy',
|
|
460
462
|
'stagednetworkpolicies', 'stagednetworkpolicy',
|
|
@@ -494,7 +496,7 @@ const KNOWN_KINDS = new Set([
|
|
|
494
496
|
'externalsecrets', 'clusterexternalsecrets', 'secretstores', 'clustersecretstores',
|
|
495
497
|
'clusters', 'scheduledbackups', 'poolers', 'objectstores',
|
|
496
498
|
'globalcontextentries', 'updaterequests', 'ephemeralreports', 'clusterephemeralreports',
|
|
497
|
-
'databases', 'publications', 'imagecatalogs', 'clusterimagecatalogs',
|
|
499
|
+
'databases', 'publications', 'imagecatalogs', 'clusterimagecatalogs', 'databaseroles',
|
|
498
500
|
'virtualservices', 'destinationrules', 'serviceentries',
|
|
499
501
|
'peerauthentications', 'authorizationpolicies',
|
|
500
502
|
'mutatingwebhookconfigurations', 'validatingwebhookconfigurations',
|
|
@@ -702,7 +704,7 @@ export function ResourceRendererDispatch({
|
|
|
702
704
|
const isGroupGatedKind =
|
|
703
705
|
kind === 'clusters' || kind === 'backups' || kind === 'scheduledbackups' || kind === 'poolers'
|
|
704
706
|
|| kind === 'objectstores' || kind === 'databases' || kind === 'publications'
|
|
705
|
-
|| kind === 'subscriptions' || kind === 'imagecatalogs' || kind === 'clusterimagecatalogs' || kind === 'jobsets'
|
|
707
|
+
|| kind === 'subscriptions' || kind === 'imagecatalogs' || kind === 'clusterimagecatalogs' || kind === 'databaseroles' || kind === 'jobsets'
|
|
706
708
|
|| kind === 'policies' || kind === 'rollouts' || kind === 'experiments'
|
|
707
709
|
|| kind === 'rayjobs' || kind === 'rayclusters' || kind === 'rayservices' || kind === 'admissionchecks' || kind === 'provisioningrequests'
|
|
708
710
|
|| kind === 'machines' || kind === 'machinesets' || kind === 'workloads' || kind === 'localqueues' || kind === 'clusterqueues'
|
|
@@ -713,7 +715,7 @@ export function ResourceRendererDispatch({
|
|
|
713
715
|
|| ((kind === 'scheduledbackups' || kind === 'poolers') && isCNPGApiVersion)
|
|
714
716
|
|| (kind === 'objectstores' && isApiGroup(data?.apiVersion, 'barmancloud.cnpg.io'))
|
|
715
717
|
|| ((kind === 'databases' || kind === 'publications' || kind === 'imagecatalogs'
|
|
716
|
-
|| kind === 'clusterimagecatalogs') && isCNPGApiVersion)
|
|
718
|
+
|| kind === 'clusterimagecatalogs' || kind === 'databaseroles') && isCNPGApiVersion)
|
|
717
719
|
|| (kind === 'subscriptions'
|
|
718
720
|
&& (isCNPGApiVersion || isApiGroup(data?.apiVersion, 'messaging.knative.dev')))
|
|
719
721
|
|| (kind === 'policies' && isApiGroup(data?.apiVersion, 'kyverno.io'))
|
|
@@ -754,6 +756,7 @@ export function ResourceRendererDispatch({
|
|
|
754
756
|
const isKnownKind = KNOWN_KINDS.has(kind) || isCrossplaneMR || isCrossplaneClaim || isCrossplaneXR
|
|
755
757
|
|
|
756
758
|
const JobComp = rendererOverrides?.JobRenderer ?? JobRenderer
|
|
759
|
+
const FluxHelmReleaseComp = rendererOverrides?.FluxHelmReleaseRenderer ?? FluxHelmReleaseRenderer
|
|
757
760
|
const JobSetComp = rendererOverrides?.JobSetRenderer ?? JobSetRenderer
|
|
758
761
|
const RayJobComp = rendererOverrides?.RayJobRenderer ?? RayJobRenderer
|
|
759
762
|
const RayClusterComp = rendererOverrides?.RayClusterRenderer ?? RayClusterRenderer
|
|
@@ -800,7 +803,7 @@ export function ResourceRendererDispatch({
|
|
|
800
803
|
{kind !== 'events' && <EventsSection fullscreen={!!renderSidebar} events={events || []} updates={updates || []} isLoading={eventsLoading ?? false} eventsError={eventsError ?? null} updatesError={updatesError ?? null} hint={eventsHint} />}
|
|
801
804
|
<LabelsSection data={data} />
|
|
802
805
|
<AnnotationsSection data={data} />
|
|
803
|
-
<MetadataSection data={data} />
|
|
806
|
+
<MetadataSection data={data} onNavigate={onNavigate} />
|
|
804
807
|
</>
|
|
805
808
|
)
|
|
806
809
|
|
|
@@ -860,6 +863,7 @@ export function ResourceRendererDispatch({
|
|
|
860
863
|
{kind === 'grpcroutes' && <GRPCRouteRenderer data={data} onNavigate={onNavigate} />}
|
|
861
864
|
{kind === 'tcproutes' && <SimpleRouteRenderer data={data} kind="TCPRoute" onNavigate={onNavigate} />}
|
|
862
865
|
{kind === 'tlsroutes' && <SimpleRouteRenderer data={data} kind="TLSRoute" onNavigate={onNavigate} />}
|
|
866
|
+
{kind === 'udproutes' && <SimpleRouteRenderer data={data} kind="UDPRoute" onNavigate={onNavigate} />}
|
|
863
867
|
{kind === 'sealedsecrets' && <SealedSecretRenderer data={data} onNavigate={onNavigate} />}
|
|
864
868
|
{(kind === 'workflowtemplates' || kind === 'clusterworkflowtemplates') && <WorkflowTemplateRenderer data={data} />}
|
|
865
869
|
{isCoreNetworkPolicy && <NetworkPolicyRenderer data={data} />}
|
|
@@ -877,7 +881,7 @@ export function ResourceRendererDispatch({
|
|
|
877
881
|
{kind === 'ocirepositories' && <OCIRepositoryRenderer data={data} />}
|
|
878
882
|
{kind === 'helmrepositories' && <HelmRepositoryRenderer data={data} />}
|
|
879
883
|
{kind === 'kustomizations' && <KustomizationRenderer data={data} onNavigate={onNavigate} />}
|
|
880
|
-
{kind === 'helmreleases' && <
|
|
884
|
+
{kind === 'helmreleases' && <FluxHelmReleaseComp data={data} onNavigate={onNavigate} />}
|
|
881
885
|
{kind === 'alerts' && <AlertRenderer data={data} />}
|
|
882
886
|
{kind === 'applications' && <ArgoApplicationRenderer data={data} />}
|
|
883
887
|
{kind === 'nodepools' && <KarpenterNodePoolComp data={data} onNavigate={onNavigate} />}
|
|
@@ -926,6 +930,7 @@ export function ResourceRendererDispatch({
|
|
|
926
930
|
{kind === 'databases' && isApiGroup(data.apiVersion, CNPG_GROUP) && <CNPGDatabaseComp data={data} onNavigate={onNavigate} />}
|
|
927
931
|
{kind === 'publications' && isApiGroup(data.apiVersion, CNPG_GROUP) && <CNPGPublicationComp data={data} onNavigate={onNavigate} />}
|
|
928
932
|
{kind === 'subscriptions' && isApiGroup(data.apiVersion, CNPG_GROUP) && <CNPGSubscriptionComp data={data} onNavigate={onNavigate} />}
|
|
933
|
+
{kind === 'databaseroles' && isApiGroup(data.apiVersion, CNPG_GROUP) && <CNPGDatabaseRoleRenderer data={data} onNavigate={onNavigate} />}
|
|
929
934
|
{(kind === 'imagecatalogs' || kind === 'clusterimagecatalogs') && isApiGroup(data.apiVersion, CNPG_GROUP) && <CNPGImageCatalogComp data={data} onNavigate={onNavigate} />}
|
|
930
935
|
{kind === 'backups' && isApiGroup(data.apiVersion, CNPG_GROUP) && <CNPGBackupRenderer data={data} onNavigate={onNavigate} />}
|
|
931
936
|
{kind === 'backups' && isApiGroup(data.apiVersion, 'velero.io') && <VeleroBackupComp data={data} onNavigate={onNavigate} />}
|
|
@@ -971,8 +976,8 @@ export function ResourceRendererDispatch({
|
|
|
971
976
|
{kind === 'azuremanagedcontrolplanes' && <AzureManagedControlPlaneRenderer data={data} onNavigate={onNavigate} />}
|
|
972
977
|
{kind === 'azuremanagedmachinepools' && <AzureManagedMachinePoolRenderer data={data} onNavigate={onNavigate} />}
|
|
973
978
|
{kind === 'azuremachines' && <AzureMachineRenderer data={data} onNavigate={onNavigate} />}
|
|
974
|
-
{kind === 'virtualservices' && <IstioVirtualServiceRenderer data={data} onNavigate={onNavigate} />}
|
|
975
|
-
{kind === 'destinationrules' && <IstioDestinationRuleRenderer data={data} onNavigate={onNavigate} />}
|
|
979
|
+
{kind === 'virtualservices' && <IstioVirtualServiceRenderer data={data} onNavigate={onNavigate} serviceDomain={relationships?.istioServiceDomain} />}
|
|
980
|
+
{kind === 'destinationrules' && <IstioDestinationRuleRenderer data={data} onNavigate={onNavigate} serviceDomain={relationships?.istioServiceDomain} />}
|
|
976
981
|
{kind === 'serviceentries' && <IstioServiceEntryRenderer data={data} />}
|
|
977
982
|
{kind === 'peerauthentications' && <IstioPeerAuthenticationRenderer data={data} />}
|
|
978
983
|
{kind === 'authorizationpolicies' && <IstioAuthorizationPolicyRenderer data={data} />}
|
|
@@ -1190,7 +1195,7 @@ export function getResourceStatus(kind: string, data: any): { text: string; colo
|
|
|
1190
1195
|
return getGatewayStatus(data)
|
|
1191
1196
|
}
|
|
1192
1197
|
if (k === 'gatewayclasses') return getGatewayClassStatus(data)
|
|
1193
|
-
if (k === 'httproutes' || k === 'grpcroutes' || k === 'tcproutes' || k === 'tlsroutes') return getRouteStatus(data)
|
|
1198
|
+
if (k === 'httproutes' || k === 'grpcroutes' || k === 'tcproutes' || k === 'tlsroutes' || k === 'udproutes') return getRouteStatus(data)
|
|
1194
1199
|
if (k === 'sealedsecrets') return getSealedSecretStatus(data)
|
|
1195
1200
|
if (k === 'poddisruptionbudgets') return getPDBStatus(data)
|
|
1196
1201
|
if (k === 'gitrepositories') return getGitRepositoryStatus(data)
|
|
@@ -1247,7 +1252,7 @@ export function getResourceStatus(kind: string, data: any): { text: string; colo
|
|
|
1247
1252
|
}
|
|
1248
1253
|
// Group-guarded for the same reason as the renderers: these plurals are
|
|
1249
1254
|
// generic enough that another operator can serve them.
|
|
1250
|
-
if ((k === 'databases' || k === 'publications' || k === 'subscriptions') && isApiGroup(data.apiVersion, CNPG_GROUP)) {
|
|
1255
|
+
if ((k === 'databases' || k === 'publications' || k === 'subscriptions' || k === 'databaseroles') && isApiGroup(data.apiVersion, CNPG_GROUP)) {
|
|
1251
1256
|
return getCNPGDeclarativeStatus(data)
|
|
1252
1257
|
}
|
|
1253
1258
|
if (k === 'globalcontextentries') return getKyvernoGlobalContextStatus(data)
|
|
@@ -11,3 +11,5 @@ export { CreateResourceDialog, type CreateResourceDialogProps, type ApplyResult
|
|
|
11
11
|
export { HelmManagedByChip, ManagedByChip, type HelmOwnerRef } from './ManagedByChip'
|
|
12
12
|
export { DetailShell, type DetailShellProps, type DetailShellTab } from './DetailShell'
|
|
13
13
|
export { classifyDiffLine, hasDiffBodyChange, DiffLine } from './UnifiedDiff'
|
|
14
|
+
|
|
15
|
+
export { ActionConfirmDialog, type ActionConfirmDialogProps, type ActionConfirmResult, type ActionWrite } from './ActionConfirmDialog'
|
|
@@ -23,4 +23,73 @@ describe("DiffViewer", () => {
|
|
|
23
23
|
expect(html).toContain("Old value:");
|
|
24
24
|
expect(html).toContain("New value:");
|
|
25
25
|
});
|
|
26
|
+
|
|
27
|
+
it("renders values kept out of the feed as a badge, not as struck-through text", () => {
|
|
28
|
+
const html = renderToStaticMarkup(
|
|
29
|
+
<DiffViewer
|
|
30
|
+
diff={{
|
|
31
|
+
summary: "pod template annotation checksum/config changed",
|
|
32
|
+
fields: [
|
|
33
|
+
{ path: "spec.template.metadata.annotations[checksum/config]", oldValue: "(value hidden)", newValue: "(value hidden)" },
|
|
34
|
+
{ path: "spec.template.metadata.annotations[example.com/new]", oldValue: null, newValue: "(set)" },
|
|
35
|
+
{ path: "spec.template.metadata.annotations[example.com/old]", oldValue: "(set)", newValue: null },
|
|
36
|
+
{ path: "spec.template.spec.affinity", oldValue: "changed", newValue: "changed" },
|
|
37
|
+
],
|
|
38
|
+
}}
|
|
39
|
+
/>,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
expect(html).not.toContain("line-through");
|
|
43
|
+
expect(html).not.toContain("(value hidden)");
|
|
44
|
+
expect(html).not.toContain("(set)");
|
|
45
|
+
expect(html.match(/value hidden/g)).toHaveLength(4);
|
|
46
|
+
expect(html).toContain("Set");
|
|
47
|
+
expect(html).toContain("Removed");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("names key changes instead of striking through an unchanged key list", () => {
|
|
51
|
+
const html = renderToStaticMarkup(
|
|
52
|
+
<DiffViewer
|
|
53
|
+
diff={{
|
|
54
|
+
summary: "data modified keys: [password]",
|
|
55
|
+
fields: [{ path: "data (modified keys)", oldValue: ["password"], newValue: ["password"] }],
|
|
56
|
+
}}
|
|
57
|
+
/>,
|
|
58
|
+
);
|
|
59
|
+
expect(html).toContain("keys changed: <span class=\"font-mono\">password</span>");
|
|
60
|
+
expect(html).not.toContain("line-through");
|
|
61
|
+
expect(html).not.toContain("Old value:");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("merges added, removed and modified key lists of one field into one row", () => {
|
|
65
|
+
const html = renderToStaticMarkup(
|
|
66
|
+
<DiffViewer
|
|
67
|
+
diff={{
|
|
68
|
+
summary: "",
|
|
69
|
+
fields: [
|
|
70
|
+
{ path: "data (added keys)", oldValue: null, newValue: ["token"] },
|
|
71
|
+
{ path: "data (removed keys)", oldValue: ["old"], newValue: null },
|
|
72
|
+
{ path: "data (modified keys)", oldValue: ["a", "b"], newValue: ["a", "b"] },
|
|
73
|
+
{ path: "stringData (added keys)", oldValue: null, newValue: ["extra"] },
|
|
74
|
+
{ path: "type", oldValue: "Opaque", newValue: "kubernetes.io/tls" },
|
|
75
|
+
],
|
|
76
|
+
}}
|
|
77
|
+
/>,
|
|
78
|
+
);
|
|
79
|
+
expect(html.match(/>data</g)).toHaveLength(1);
|
|
80
|
+
expect(html).toMatch(/keys changed: <span[^>]*>a, b<\/span>.*added: <span[^>]*>token<\/span>.*removed: <span[^>]*>old<\/span>/);
|
|
81
|
+
expect(html).toContain(">stringData<");
|
|
82
|
+
expect(html).toContain("added: <span class=\"font-mono\">extra</span>");
|
|
83
|
+
// Ordinary fields keep the old→new rendering.
|
|
84
|
+
expect(html).toContain("Old value:");
|
|
85
|
+
expect(html).toContain("kubernetes.io/tls");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("keeps the old→new rendering when a key-list path does not carry a list", () => {
|
|
89
|
+
const html = renderToStaticMarkup(
|
|
90
|
+
<DiffViewer diff={{ summary: "", fields: [{ path: "data (modified keys)", oldValue: "x", newValue: "y" }] }} />,
|
|
91
|
+
);
|
|
92
|
+
expect(html).toContain("Old value:");
|
|
93
|
+
expect(html).not.toContain("keys changed");
|
|
94
|
+
});
|
|
26
95
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { memo } from 'react'
|
|
2
2
|
import { clsx } from 'clsx'
|
|
3
|
-
import { ArrowRight, Plus, Minus, RefreshCw } from 'lucide-react'
|
|
3
|
+
import { ArrowRight, EyeOff, Plus, Minus, RefreshCw } from 'lucide-react'
|
|
4
4
|
import type { DiffInfo, FieldChange } from '../../types'
|
|
5
5
|
|
|
6
6
|
interface DiffViewerProps {
|
|
@@ -34,19 +34,128 @@ export const DiffViewer = memo(function DiffViewer({ diff, compact = false }: Di
|
|
|
34
34
|
|
|
35
35
|
{/* Field changes */}
|
|
36
36
|
<div className="space-y-1.5">
|
|
37
|
-
{diff.fields.map((
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
{diffRows(diff.fields).map((row, idx) =>
|
|
38
|
+
row.kind === 'keys' ? (
|
|
39
|
+
<KeyListRow key={`${row.field}-${idx}`} row={row} />
|
|
40
|
+
) : (
|
|
41
|
+
<FieldChangeRow key={`${row.field.path}-${idx}`} field={row.field} />
|
|
42
|
+
),
|
|
43
|
+
)}
|
|
40
44
|
</div>
|
|
41
45
|
</div>
|
|
42
46
|
)
|
|
43
47
|
})
|
|
44
48
|
|
|
49
|
+
// Secret and ConfigMap diffs report which keys changed as name lists, under
|
|
50
|
+
// paths like "data (modified keys)", never values. Rendered as an old→new
|
|
51
|
+
// pair, a modified key reads as removed and re-added, so key lists of one
|
|
52
|
+
// field are merged into a single row that names what happened to each key.
|
|
53
|
+
const KEY_LIST_PATH = /^(.+) \((added|removed|modified) keys\)$/
|
|
54
|
+
|
|
55
|
+
type KeyChange = 'added' | 'removed' | 'modified'
|
|
56
|
+
|
|
57
|
+
interface KeyListDiffRow {
|
|
58
|
+
kind: 'keys'
|
|
59
|
+
field: string
|
|
60
|
+
keys: Record<KeyChange, string[]>
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type DiffRow = KeyListDiffRow | { kind: 'field'; field: FieldChange }
|
|
64
|
+
|
|
65
|
+
function keyNames(value: unknown): string[] | null {
|
|
66
|
+
if (value === null || value === undefined) return []
|
|
67
|
+
return Array.isArray(value) && value.every((item) => typeof item === 'string') ? value : null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function diffRows(fields: FieldChange[]): DiffRow[] {
|
|
71
|
+
const rows: DiffRow[] = []
|
|
72
|
+
const byField = new Map<string, KeyListDiffRow>()
|
|
73
|
+
for (const field of fields) {
|
|
74
|
+
const match = KEY_LIST_PATH.exec(field.path)
|
|
75
|
+
const oldKeys = keyNames(field.oldValue)
|
|
76
|
+
const newKeys = keyNames(field.newValue)
|
|
77
|
+
if (!match || !oldKeys || !newKeys) {
|
|
78
|
+
rows.push({ kind: 'field', field })
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
const [, base, change] = match
|
|
82
|
+
let row = byField.get(base)
|
|
83
|
+
if (!row) {
|
|
84
|
+
row = { kind: 'keys', field: base, keys: { added: [], removed: [], modified: [] } }
|
|
85
|
+
byField.set(base, row)
|
|
86
|
+
rows.push(row)
|
|
87
|
+
}
|
|
88
|
+
const list = row.keys[change as KeyChange]
|
|
89
|
+
for (const key of [...oldKeys, ...newKeys]) {
|
|
90
|
+
if (!list.includes(key)) list.push(key)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return rows
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const KEY_CHANGE_LABEL: Record<KeyChange, string> = {
|
|
97
|
+
modified: 'keys changed',
|
|
98
|
+
added: 'added',
|
|
99
|
+
removed: 'removed',
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function KeyListRow({ row }: { row: KeyListDiffRow }) {
|
|
103
|
+
const parts = (['modified', 'added', 'removed'] as const).filter((change) => row.keys[change].length > 0)
|
|
104
|
+
return (
|
|
105
|
+
<div className="rounded bg-theme-surface/50 border border-theme-border px-3 py-2">
|
|
106
|
+
<div className="text-xs font-mono text-theme-text-tertiary mb-1 break-words [overflow-wrap:anywhere]">{row.field}</div>
|
|
107
|
+
<div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 text-sm text-theme-text-secondary">
|
|
108
|
+
{parts.map((change, idx) => (
|
|
109
|
+
<span key={change} className="min-w-0 [overflow-wrap:anywhere]">
|
|
110
|
+
{idx > 0 ? <span aria-hidden className="mr-2 text-theme-text-tertiary">·</span> : null}
|
|
111
|
+
{KEY_CHANGE_LABEL[change]}: <span className="font-mono">{row.keys[change].join(', ')}</span>
|
|
112
|
+
</span>
|
|
113
|
+
))}
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
45
119
|
interface FieldChangeRowProps {
|
|
46
120
|
field: FieldChange
|
|
47
121
|
}
|
|
48
122
|
|
|
123
|
+
// The backend never sends values it keeps out of the feed (secrets, config
|
|
124
|
+
// blobs, large structures); it sends one of these markers instead. Older
|
|
125
|
+
// events carry a bare "changed" on both sides.
|
|
126
|
+
const HIDDEN_VALUE = '(value hidden)'
|
|
127
|
+
const HIDDEN_SET = '(set)'
|
|
128
|
+
|
|
129
|
+
function hiddenChange(field: FieldChange): 'changed' | 'set' | 'removed' | null {
|
|
130
|
+
const { oldValue, newValue } = field
|
|
131
|
+
const absent = (v: unknown) => v === null || v === undefined
|
|
132
|
+
if ((oldValue === HIDDEN_VALUE && newValue === HIDDEN_VALUE) || (oldValue === 'changed' && newValue === 'changed')) {
|
|
133
|
+
return 'changed'
|
|
134
|
+
}
|
|
135
|
+
if (absent(oldValue) && newValue === HIDDEN_SET) return 'set'
|
|
136
|
+
if (oldValue === HIDDEN_SET && absent(newValue)) return 'removed'
|
|
137
|
+
return null
|
|
138
|
+
}
|
|
139
|
+
|
|
49
140
|
function FieldChangeRow({ field }: FieldChangeRowProps) {
|
|
141
|
+
const hidden = hiddenChange(field)
|
|
142
|
+
if (hidden) {
|
|
143
|
+
const Icon = hidden === 'set' ? Plus : hidden === 'removed' ? Minus : RefreshCw
|
|
144
|
+
return (
|
|
145
|
+
<div className="rounded bg-theme-surface/50 border border-theme-border px-3 py-2">
|
|
146
|
+
<div className="text-xs font-mono text-theme-text-tertiary mb-1 break-words [overflow-wrap:anywhere]">{field.path}</div>
|
|
147
|
+
<div className="flex flex-wrap items-center gap-2 text-sm text-theme-text-secondary">
|
|
148
|
+
<Icon className="w-3.5 h-3.5 shrink-0" aria-hidden />
|
|
149
|
+
<span>{hidden === 'set' ? 'Set' : hidden === 'removed' ? 'Removed' : 'Changed'}</span>
|
|
150
|
+
<span className="inline-flex items-center gap-1 rounded-full bg-theme-elevated px-2 py-0.5 text-xs text-theme-text-tertiary">
|
|
151
|
+
<EyeOff className="w-3 h-3" aria-hidden />
|
|
152
|
+
value hidden
|
|
153
|
+
</span>
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
50
159
|
const isAdded = field.oldValue === null || field.oldValue === undefined
|
|
51
160
|
const isRemoved = field.newValue === null || field.newValue === undefined
|
|
52
161
|
const isModified = !isAdded && !isRemoved
|
|
@@ -675,7 +675,7 @@ function calculateInterestingnessWithBreakdown(lane: ResourceLane): ScoreBreakdo
|
|
|
675
675
|
// Core workloads
|
|
676
676
|
Deployment: 50, Rollout: 50, StatefulSet: 50, DaemonSet: 50,
|
|
677
677
|
Service: 45, Ingress: 45, Gateway: 45,
|
|
678
|
-
HTTPRoute: 42, GRPCRoute: 42, TCPRoute: 42, TLSRoute: 42,
|
|
678
|
+
HTTPRoute: 42, GRPCRoute: 42, TCPRoute: 42, TLSRoute: 42, UDPRoute: 42,
|
|
679
679
|
Job: 40, CronJob: 40, Workflow: 40, CronWorkflow: 40,
|
|
680
680
|
Pod: 30,
|
|
681
681
|
HorizontalPodAutoscaler: 25,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
2
|
import {
|
|
3
|
+
isRolloutEvent,
|
|
3
4
|
buildHealthSpans,
|
|
4
5
|
sweepAggregateHealth,
|
|
5
6
|
formatAggregateHealthTooltip,
|
|
@@ -217,3 +218,50 @@ describe('buildLaneMemberSpans (leaf gathering)', () => {
|
|
|
217
218
|
expect(members.map((m) => m.name)).toEqual(['web'])
|
|
218
219
|
})
|
|
219
220
|
})
|
|
221
|
+
|
|
222
|
+
describe('isRolloutEvent', () => {
|
|
223
|
+
type F = { path: string; oldValue?: unknown; newValue?: unknown }
|
|
224
|
+
const ev = (kind: string, fields: F[], summary = '') => ({
|
|
225
|
+
id: 'e', source: 'informer', eventType: 'update', kind, namespace: 'shop', name: 'web',
|
|
226
|
+
timestamp: new Date(0).toISOString(),
|
|
227
|
+
diff: { summary, fields: fields.map((f) => ({ oldValue: 1, newValue: 2, ...f })) },
|
|
228
|
+
}) as any
|
|
229
|
+
|
|
230
|
+
it('does not treat a pod-template edit alone as a rollout (OnDelete and paused workloads do not roll)', () => {
|
|
231
|
+
expect(isRolloutEvent(ev('Deployment', [{ path: 'spec.template.metadata.annotations[kubectl.kubernetes.io/restartedAt]' }], 'pod template annotation kubectl.kubernetes.io/restartedAt added (new pod template revision)'))).toBe(false)
|
|
232
|
+
expect(isRolloutEvent(ev('DaemonSet', [{ path: 'spec.template.metadata.annotations[checksum/config]' }]))).toBe(false)
|
|
233
|
+
expect(isRolloutEvent(ev('Rollout', [{ path: 'spec.template.spec.containers[].image' }], 'image: web:2'))).toBe(false)
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it('treats updated-count movement with steady readiness as a rollout', () => {
|
|
237
|
+
expect(isRolloutEvent(ev('Deployment', [{ path: 'status.updatedReplicas', oldValue: 1, newValue: 2 }]))).toBe(true)
|
|
238
|
+
expect(isRolloutEvent(ev('DaemonSet', [{ path: 'status.updatedNumberScheduled', oldValue: 1, newValue: 2 }]))).toBe(true)
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it('keeps degraded health visible when readiness moves in the same update', () => {
|
|
242
|
+
// A pod swap, a lost pod and a scale-up under failing readiness are
|
|
243
|
+
// indistinguishable from the counts alone.
|
|
244
|
+
expect(isRolloutEvent(ev('DaemonSet', [
|
|
245
|
+
{ path: 'status.numberReady', oldValue: 4, newValue: 3 },
|
|
246
|
+
{ path: 'status.updatedNumberScheduled', oldValue: 1, newValue: 2 },
|
|
247
|
+
]))).toBe(false)
|
|
248
|
+
expect(isRolloutEvent(ev('Deployment', [
|
|
249
|
+
{ path: 'status.readyReplicas', oldValue: 3, newValue: 2 },
|
|
250
|
+
{ path: 'status.updatedReplicas', oldValue: 3, newValue: 5 },
|
|
251
|
+
]))).toBe(false)
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
it('treats the reset at the start of a rollout as a rollout', () => {
|
|
255
|
+
expect(isRolloutEvent(ev('Deployment', [{ path: 'status.updatedReplicas', oldValue: 3, newValue: 0 }], 'updated: 3→0'))).toBe(true)
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('does not treat a lost pod or unrelated changes as a rollout', () => {
|
|
259
|
+
expect(isRolloutEvent(ev('Deployment', [
|
|
260
|
+
{ path: 'status.readyReplicas', oldValue: 3, newValue: 2 },
|
|
261
|
+
{ path: 'status.updatedReplicas', oldValue: 3, newValue: 2 },
|
|
262
|
+
]))).toBe(false)
|
|
263
|
+
expect(isRolloutEvent(ev('Deployment', [{ path: 'status.readyReplicas', oldValue: 3, newValue: 1 }]))).toBe(false)
|
|
264
|
+
expect(isRolloutEvent(ev('Deployment', [{ path: 'spec.strategy.rollingUpdate.maxUnavailable' }], 'updated: wording without the field'))).toBe(false)
|
|
265
|
+
expect(isRolloutEvent(ev('Service', [{ path: 'status.updatedReplicas', oldValue: 1, newValue: 2 }]))).toBe(false)
|
|
266
|
+
})
|
|
267
|
+
})
|
|
@@ -57,28 +57,23 @@ export interface HealthSpanResult {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
|
-
* Check if an event
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
60
|
+
* Check if an event shows a rollout in progress (expected degradation): a
|
|
61
|
+
* deployment-like workload whose updated-replica count moved while its ready
|
|
62
|
+
* count held. Decided from the diff's field paths, not from summary wording.
|
|
63
|
+
*
|
|
64
|
+
* The rule errs toward keeping degraded health visible. A pod-template edit
|
|
65
|
+
* alone does not count (OnDelete and paused workloads take it without
|
|
66
|
+
* rolling), and an update that also moves the ready count does not count
|
|
67
|
+
* either: a lost pod and a scale-up under failing readiness look the same as
|
|
68
|
+
* a pod swap from the counts alone.
|
|
65
69
|
*/
|
|
66
|
-
function isRolloutEvent(event: TimelineEvent): boolean {
|
|
70
|
+
export function isRolloutEvent(event: TimelineEvent): boolean {
|
|
67
71
|
// Only deployment-like workload controllers can have rollout transitions.
|
|
68
72
|
if (!isDeploymentLikeWorkloadKind(event.kind)) return false
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
// "updated:" typically indicates replica count changes during rollout
|
|
74
|
-
if (summary.includes('updated:')) return true
|
|
75
|
-
// Image changes trigger rollouts
|
|
76
|
-
if (summary.includes('image(') || summary.includes('image:')) return true
|
|
77
|
-
// Spec template changes often indicate rollout
|
|
78
|
-
if (summary.includes('template')) return true
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return false
|
|
73
|
+
const paths = new Set((event.diff?.fields ?? []).map((field) => field.path))
|
|
74
|
+
const updatedMoved = paths.has('status.updatedReplicas') || paths.has('status.updatedNumberScheduled')
|
|
75
|
+
const readyMoved = paths.has('status.readyReplicas') || paths.has('status.numberReady')
|
|
76
|
+
return updatedMoved && !readyMoved
|
|
82
77
|
}
|
|
83
78
|
|
|
84
79
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { baseSubtitle } from './K8sResourceNode'
|
|
2
|
+
import { baseSubtitle, knativeDomainsTooltipLines } from './K8sResourceNode'
|
|
3
3
|
|
|
4
4
|
describe('baseSubtitle', () => {
|
|
5
5
|
it('describes workflow templates without dropping newer topology kinds', () => {
|
|
@@ -18,6 +18,23 @@ describe('baseSubtitle', () => {
|
|
|
18
18
|
})).toBe('ClusterIP :80 +2 more')
|
|
19
19
|
})
|
|
20
20
|
|
|
21
|
+
it('describes a domain-mapped Knative Service by its custom domain', () => {
|
|
22
|
+
expect(baseSubtitle('KnativeService', { url: 'http://api.team.svc.cluster.local' })).toBe('api.team.svc.cluster.local')
|
|
23
|
+
expect(baseSubtitle('KnativeService', { url: 'http://api.team.svc.cluster.local', mappedDomains: ['api.example.com'] })).toBe('api.example.com')
|
|
24
|
+
expect(baseSubtitle('KnativeService', { mappedDomains: ['a.example.com', 'b.example.com'] })).toBe('a.example.com +1')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('labels a mapped Knative Service URL by what it is', () => {
|
|
28
|
+
const text = (data: Record<string, unknown>) => knativeDomainsTooltipLines(data).map((l) => l.text)
|
|
29
|
+
// Private: the Service URL is the internal address, shown once.
|
|
30
|
+
expect(text({ mappedDomains: ['api.example.com'], url: 'http://api.team.svc.cluster.local', internalUrl: 'http://api.team.svc.cluster.local' }))
|
|
31
|
+
.toEqual(['api.example.com', 'Internal: http://api.team.svc.cluster.local'])
|
|
32
|
+
// Public: its own URL isn't internal.
|
|
33
|
+
expect(text({ mappedDomains: ['shop.example.com'], url: 'https://shop.team.example.org', internalUrl: 'http://shop.team.svc.cluster.local' }))
|
|
34
|
+
.toEqual(['shop.example.com', 'Service URL: https://shop.team.example.org', 'Internal: http://shop.team.svc.cluster.local'])
|
|
35
|
+
expect(text({ url: 'https://shop.team.example.org' })).toEqual([])
|
|
36
|
+
})
|
|
37
|
+
|
|
21
38
|
it('describes a Calico staged deletion as a removal, not as selecting everything', () => {
|
|
22
39
|
// A staged deletion carries no selector, because the Calico API forbids one.
|
|
23
40
|
// Falling through to the empty-selector default would label the broadest
|