@skyhook-io/k8s-ui 1.8.10 → 1.8.12
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/package.json +7 -6
- package/src/assets/gitops/argocd.png +0 -0
- package/src/assets/gitops/flux.svg +7 -0
- package/src/components/applications/AppChips.tsx +10 -0
- package/src/components/applications/ApplicationDetail.test.tsx +697 -0
- package/src/components/applications/ApplicationDetail.tsx +3269 -474
- package/src/components/applications/ApplicationsView.tsx +62 -10
- package/src/components/applications/applications-view.test.tsx +47 -0
- package/src/components/applications/index.ts +1 -1
- package/src/components/audit/AuditAlerts.tsx +14 -10
- package/src/components/checks/ChecksView.tsx +2 -3
- package/src/components/checks/severity.ts +14 -10
- package/src/components/cluster-switcher/ClusterSwitcher.tsx +2 -2
- package/src/components/compare/CompareResourcePicker.tsx +2 -2
- package/src/components/gitops/GitOpsDetailLayout.tsx +13 -3
- package/src/components/gitops/GitOpsGraphFilterRail.tsx +2 -1
- package/src/components/gitops/GitOpsTableView.tsx +13 -6
- package/src/components/gitops/ManagedResourcesList.tsx +8 -11
- package/src/components/gitops/SyncCountdown.tsx +26 -17
- package/src/components/gitops/SyncOptionsDialog.test.ts +48 -0
- package/src/components/gitops/SyncOptionsDialog.tsx +190 -46
- package/src/components/gitops/insights/ArgoResourceDiff.tsx +172 -0
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +474 -121
- package/src/components/gitops/insights/index.ts +1 -0
- package/src/components/gitops/insights/insights-helpers.test.ts +102 -1
- package/src/components/gitops/insights/insights-helpers.ts +74 -1
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +10 -2
- package/src/components/issues/IssuesView.tsx +91 -52
- package/src/components/issues/ResourceIssuesSection.test.tsx +39 -6
- package/src/components/issues/ResourceIssuesSection.tsx +46 -144
- package/src/components/issues/index.ts +3 -0
- package/src/components/issues/issue-timing.ts +53 -0
- package/src/components/issues/issues.test.ts +102 -1
- package/src/components/issues/severity.ts +3 -0
- package/src/components/logs/LogCore.tsx +14 -4
- package/src/components/logs/WorkloadLogsViewer.tsx +31 -6
- package/src/components/logs/useLogStream.ts +8 -1
- package/src/components/namespace-switcher/NamespacePicker.tsx +86 -107
- package/src/components/resources/ResourcesSidebar.test.tsx +71 -0
- package/src/components/resources/ResourcesSidebar.tsx +46 -46
- package/src/components/resources/ResourcesView.selected-kind.test.ts +143 -0
- package/src/components/resources/ResourcesView.tsx +233 -51
- package/src/components/resources/renderers/AWSMachineRenderer.tsx +3 -2
- package/src/components/resources/renderers/AWSManagedClusterRenderer.tsx +3 -2
- package/src/components/resources/renderers/AWSManagedControlPlaneRenderer.tsx +3 -2
- package/src/components/resources/renderers/AWSManagedMachinePoolRenderer.tsx +3 -2
- package/src/components/resources/renderers/AzureMachineRenderer.tsx +3 -2
- package/src/components/resources/renderers/AzureManagedControlPlaneRenderer.tsx +3 -2
- package/src/components/resources/renderers/AzureManagedMachinePoolRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIClusterRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIKubeadmControlPlaneRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIMachineDeploymentRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIMachineHealthCheckRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIMachinePoolRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIMachineRenderer.tsx +3 -2
- package/src/components/resources/renderers/CAPIMachineSetRenderer.tsx +3 -2
- package/src/components/resources/renderers/CertificateRenderer.tsx +3 -2
- package/src/components/resources/renderers/CertificateRequestRenderer.tsx +3 -2
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/CompositeRenderer.tsx +3 -2
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +99 -0
- package/src/components/resources/renderers/ExternalSecretRenderer.tsx +3 -2
- package/src/components/resources/renderers/GCPMachineRenderer.tsx +3 -2
- package/src/components/resources/renderers/GCPManagedControlPlaneRenderer.tsx +3 -2
- package/src/components/resources/renderers/GCPManagedMachinePoolRenderer.tsx +3 -2
- package/src/components/resources/renderers/GRPCRouteRenderer.tsx +7 -3
- package/src/components/resources/renderers/GatewayClassRenderer.tsx +3 -2
- package/src/components/resources/renderers/HTTPRouteRenderer.tsx +7 -3
- package/src/components/resources/renderers/JobRenderer.tsx +3 -1
- package/src/components/resources/renderers/KarpenterEC2NodeClassRenderer.tsx +3 -2
- package/src/components/resources/renderers/KarpenterNodeClaimRenderer.tsx +3 -2
- package/src/components/resources/renderers/KarpenterNodePoolRenderer.tsx +3 -2
- package/src/components/resources/renderers/KedaScaledJobRenderer.tsx +3 -2
- package/src/components/resources/renderers/KedaScaledObjectRenderer.tsx +3 -2
- package/src/components/resources/renderers/ManagedResourceRenderer.tsx +3 -2
- package/src/components/resources/renderers/PVCRenderer.tsx +3 -2
- package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +2 -2
- package/src/components/resources/renderers/SbomReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/SealedSecretRenderer.tsx +3 -2
- package/src/components/resources/renderers/SecretRenderer.tsx +2 -0
- package/src/components/resources/renderers/SecretStoreRenderer.tsx +3 -2
- package/src/components/resources/renderers/ServiceRenderer.tsx +58 -40
- package/src/components/resources/renderers/SimpleRouteRenderer.tsx +7 -3
- package/src/components/resources/renderers/VulnerabilityReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/WorkflowRenderer.test.tsx +43 -0
- package/src/components/resources/renderers/WorkflowRenderer.tsx +60 -78
- package/src/components/resources/renderers/index.ts +1 -0
- package/src/components/resources/resource-utils-argo.test.ts +18 -1
- package/src/components/resources/resource-utils-argo.ts +8 -0
- package/src/components/resources/resource-utils-capi.ts +1 -1
- package/src/components/resources/resource-utils.ts +31 -0
- package/src/components/shared/DetailShell.tsx +2 -2
- package/src/components/shared/ResourceActionsBar.tsx +142 -111
- package/src/components/shared/ResourceRendererDispatch.tsx +33 -4
- package/src/components/shared/UnifiedDiff.tsx +51 -0
- package/src/components/shared/index.ts +1 -0
- package/src/components/timeline/TimelineList.tsx +375 -354
- package/src/components/timeline/TimelineStrip.test.tsx +194 -0
- package/src/components/timeline/TimelineStrip.tsx +836 -0
- package/src/components/timeline/TimelineSwimlanes.test.tsx +1028 -0
- package/src/components/timeline/TimelineSwimlanes.tsx +2498 -593
- package/src/components/timeline/TimelineToolbar.test.tsx +320 -0
- package/src/components/timeline/TimelineToolbar.tsx +759 -0
- package/src/components/timeline/TimelineToolbarState.test.tsx +88 -0
- package/src/components/timeline/index.ts +23 -0
- package/src/components/timeline/scrubber-math.test.ts +235 -0
- package/src/components/timeline/scrubber-math.ts +249 -0
- package/src/components/timeline/shared.test.tsx +219 -0
- package/src/components/timeline/shared.tsx +232 -447
- package/src/components/timeline/timeline-filters.test.ts +196 -0
- package/src/components/timeline/timeline-filters.ts +180 -0
- package/src/components/timeline/timeline-lane-sort.test.ts +97 -0
- package/src/components/timeline/timeline-lane-sort.ts +77 -0
- package/src/components/timeline/timeline-live.test.ts +131 -0
- package/src/components/timeline/timeline-live.ts +77 -0
- package/src/components/topology/GroupNode.tsx +9 -2
- package/src/components/topology/K8sResourceNode.test.ts +11 -0
- package/src/components/topology/K8sResourceNode.tsx +387 -243
- package/src/components/topology/TopologyBreadcrumb.tsx +30 -0
- package/src/components/topology/TopologyControls.tsx +3 -1
- package/src/components/topology/TopologyFilterSidebar.tsx +9 -0
- package/src/components/topology/TopologyGraph.tsx +149 -90
- package/src/components/topology/TopologyOverlayBar.tsx +30 -0
- package/src/components/topology/TopologySearch.tsx +25 -17
- package/src/components/topology/index.ts +2 -0
- package/src/components/topology/layout.ts +10 -2
- package/src/components/topology/topology.css +0 -92
- package/src/components/ui/Badge.tsx +6 -1
- package/src/components/ui/CodeViewer.tsx +2 -2
- package/src/components/ui/Collapse.tsx +59 -0
- package/src/components/ui/ConfirmDialog.tsx +7 -3
- package/src/components/ui/DialogPortal.tsx +31 -9
- package/src/components/ui/Input.tsx +24 -0
- package/src/components/ui/MultiSelectPicker.test.tsx +101 -0
- package/src/components/ui/MultiSelectPicker.tsx +167 -0
- package/src/components/ui/PaneLoader.tsx +10 -2
- package/src/components/ui/ResourceBar.tsx +68 -25
- package/src/components/ui/SearchBox.tsx +18 -4
- package/src/components/ui/SearchPillInput.tsx +2 -2
- package/src/components/ui/SelectMenu.tsx +81 -0
- package/src/components/ui/drawer-components.tsx +48 -32
- package/src/components/ui/index.ts +6 -0
- package/src/components/ui/severity-tone.ts +11 -4
- package/src/components/workload/ResourceDetailDrawer.tsx +6 -2
- package/src/components/workload/WorkloadView.tsx +2706 -475
- package/src/components/workload/index.ts +1 -1
- package/src/theme/tailwind-theme.css +1 -0
- package/src/theme/variables.css +2 -1
- package/src/types/core.ts +57 -4
- package/src/types/gitops-insights.ts +69 -1
- package/src/types/gitops.ts +3 -3
- package/src/utils/application-history.test.ts +538 -0
- package/src/utils/application-history.ts +257 -0
- package/src/utils/application-topology.test.ts +198 -0
- package/src/utils/application-topology.ts +261 -0
- package/src/utils/applications.test.ts +201 -1
- package/src/utils/applications.ts +1182 -393
- package/src/utils/index.ts +3 -0
- package/src/utils/navigation.test.ts +32 -1
- package/src/utils/navigation.ts +70 -0
- package/src/utils/resource-hierarchy.test.ts +1024 -1
- package/src/utils/resource-hierarchy.ts +925 -267
- package/src/utils/resource-icons.ts +2 -0
- package/src/utils/topology-neighborhood.test.ts +202 -1
- package/src/utils/topology-neighborhood.ts +194 -13
- package/src/utils/workflow-execution.test.ts +556 -0
- package/src/utils/workflow-execution.ts +814 -0
- package/src/utils/workload-colors.ts +2 -3
|
@@ -0,0 +1,697 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { renderToString } from 'react-dom/server'
|
|
3
|
+
import { ApplicationDetail, type ApplicationDetailProps } from './ApplicationDetail'
|
|
4
|
+
import type { AppHistory, AppRow, AppSourceRef } from '../../utils/applications'
|
|
5
|
+
import { workloadKey } from '../../utils/topology-neighborhood'
|
|
6
|
+
|
|
7
|
+
const app: AppRow = {
|
|
8
|
+
key: "app:prod:checkout",
|
|
9
|
+
name: "checkout",
|
|
10
|
+
namespace: "prod",
|
|
11
|
+
health: "healthy",
|
|
12
|
+
workload_class: "mixed",
|
|
13
|
+
appVersion: "1.2.3",
|
|
14
|
+
workloads: [
|
|
15
|
+
{
|
|
16
|
+
kind: "Deployment",
|
|
17
|
+
namespace: "prod",
|
|
18
|
+
name: "checkout-api",
|
|
19
|
+
workload_class: "service",
|
|
20
|
+
health: "healthy",
|
|
21
|
+
ready: 3,
|
|
22
|
+
desired: 3,
|
|
23
|
+
restarts: 0,
|
|
24
|
+
version: "1.2.3",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
kind: "Deployment",
|
|
28
|
+
namespace: "prod",
|
|
29
|
+
name: "checkout-worker",
|
|
30
|
+
workload_class: "worker",
|
|
31
|
+
health: "healthy",
|
|
32
|
+
ready: 1,
|
|
33
|
+
desired: 1,
|
|
34
|
+
restarts: 0,
|
|
35
|
+
version: "1.2.3",
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
relationships: {
|
|
39
|
+
services: ["checkout-api"],
|
|
40
|
+
ingresses: ["checkout"],
|
|
41
|
+
configs: 2,
|
|
42
|
+
scalers: 1,
|
|
43
|
+
storage: 1,
|
|
44
|
+
networkPolicies: 1,
|
|
45
|
+
serviceRefs: [{ kind: 'Service', namespace: 'prod', name: 'checkout-api' }],
|
|
46
|
+
ingressRefs: [{ kind: 'Ingress', namespace: 'prod', name: 'checkout' }],
|
|
47
|
+
configRefs: [
|
|
48
|
+
{ kind: 'ConfigMap', namespace: 'prod', name: 'checkout-config' },
|
|
49
|
+
{ kind: 'Secret', namespace: 'prod', name: 'checkout-secret' },
|
|
50
|
+
],
|
|
51
|
+
scalerRefs: [{ kind: 'HorizontalPodAutoscaler', namespace: 'prod', name: 'checkout-api' }],
|
|
52
|
+
storageRefs: [{ kind: 'PersistentVolumeClaim', namespace: 'prod', name: 'checkout-data' }],
|
|
53
|
+
networkPolicyRefs: [{ kind: 'NetworkPolicy', namespace: 'prod', name: 'checkout-ingress' }],
|
|
54
|
+
},
|
|
55
|
+
events: [
|
|
56
|
+
{
|
|
57
|
+
type: "Warning",
|
|
58
|
+
reason: "BackOff",
|
|
59
|
+
object: "Deployment/checkout-api",
|
|
60
|
+
message: "Back-off restarting failed container",
|
|
61
|
+
count: 2,
|
|
62
|
+
lastSeen: "1m ago",
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
function renderDetail(props: Partial<ApplicationDetailProps> = {}) {
|
|
68
|
+
const componentProps = {
|
|
69
|
+
app,
|
|
70
|
+
onBack: () => {},
|
|
71
|
+
renderWorkload: (workload) => <div>Runtime for {workload.name}</div>,
|
|
72
|
+
renderOverviewIssues: () => null,
|
|
73
|
+
...props,
|
|
74
|
+
} as ApplicationDetailProps;
|
|
75
|
+
return renderToString(<ApplicationDetail {...componentProps} />);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
describe("ApplicationDetail shell", () => {
|
|
79
|
+
it("preserves the built-in issue surface when the host slot is omitted", () => {
|
|
80
|
+
const html = renderDetail({ renderOverviewIssues: undefined });
|
|
81
|
+
|
|
82
|
+
expect(html).toContain('Application views')
|
|
83
|
+
expect(html).toContain('Overview')
|
|
84
|
+
expect(html).toContain('Topology')
|
|
85
|
+
expect(html).toContain('History')
|
|
86
|
+
expect(html).toContain('BackOff on checkout-api')
|
|
87
|
+
expect(html).toContain('Application identity')
|
|
88
|
+
expect(html).toContain('Grouped from Kubernetes ownership')
|
|
89
|
+
expect(html).toContain('Entrypoints')
|
|
90
|
+
expect(html).toContain('Dependencies')
|
|
91
|
+
expect(html).toContain('svc<!-- -->/</span>checkout-api')
|
|
92
|
+
expect(html).toContain('ing<!-- -->/</span>checkout')
|
|
93
|
+
expect(html).toContain('Configuration')
|
|
94
|
+
expect(html).toContain('Autoscaling')
|
|
95
|
+
expect(html).toContain('Storage')
|
|
96
|
+
expect(html).toContain('Network policy')
|
|
97
|
+
expect(html).not.toContain('Related resources')
|
|
98
|
+
expect(html).toContain('Workloads')
|
|
99
|
+
expect(html).not.toContain('Runtime for')
|
|
100
|
+
expect(html).not.toContain('Components')
|
|
101
|
+
expect(html).not.toContain('Changes')
|
|
102
|
+
expect(html).not.toContain('YAML')
|
|
103
|
+
expect(html).not.toContain('Cost')
|
|
104
|
+
expect(html).not.toContain('>Deploy<')
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it("defaults to the application overview scope", () => {
|
|
108
|
+
const html = renderDetail();
|
|
109
|
+
|
|
110
|
+
expect(html).toContain("Application views");
|
|
111
|
+
expect(html).toContain("Overview");
|
|
112
|
+
expect(html).toContain("Topology");
|
|
113
|
+
expect(html).toContain("History");
|
|
114
|
+
expect(html).not.toContain("BackOff on checkout-api");
|
|
115
|
+
expect(html).toContain("Application identity");
|
|
116
|
+
expect(html).toContain("Entrypoints");
|
|
117
|
+
expect(html).toContain("Dependencies");
|
|
118
|
+
expect(html).toContain("svc<!-- -->/</span>checkout-api");
|
|
119
|
+
expect(html).toContain("ing<!-- -->/</span>checkout");
|
|
120
|
+
expect(html).toContain("Configuration");
|
|
121
|
+
expect(html).toContain("Autoscaling");
|
|
122
|
+
expect(html).toContain("Storage");
|
|
123
|
+
expect(html).not.toContain("Related resources");
|
|
124
|
+
expect(html).toContain("Workloads");
|
|
125
|
+
expect(html).not.toContain("Runtime for");
|
|
126
|
+
expect(html).not.toContain("Components");
|
|
127
|
+
expect(html).not.toContain("Changes");
|
|
128
|
+
expect(html).not.toContain("YAML");
|
|
129
|
+
expect(html).not.toContain("Cost");
|
|
130
|
+
expect(html).not.toContain(">Deploy<");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("shows workload scope without application view tabs when the URL names a workload", () => {
|
|
134
|
+
const selected = app.workloads[1];
|
|
135
|
+
const html = renderDetail({
|
|
136
|
+
selectedView: "overview",
|
|
137
|
+
onSelectView: () => {},
|
|
138
|
+
selectedWorkloadKey: workloadKey(selected),
|
|
139
|
+
onSelectWorkload: () => {},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
expect(html).toContain('Runtime for')
|
|
143
|
+
expect(html).toContain('checkout-worker')
|
|
144
|
+
expect(html).not.toContain('Application views')
|
|
145
|
+
expect(html).not.toContain('Overview')
|
|
146
|
+
expect(html).not.toContain('Topology')
|
|
147
|
+
expect(html).not.toContain('History')
|
|
148
|
+
expect(html).not.toContain('Application identity')
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it("renders the optional application cost view only when the host selects it", () => {
|
|
152
|
+
const withCostTab = renderDetail({
|
|
153
|
+
selectedView: "overview",
|
|
154
|
+
onSelectView: () => {},
|
|
155
|
+
selectedWorkloadKey: null,
|
|
156
|
+
onSelectWorkload: () => {},
|
|
157
|
+
renderCostView: () => <div>Application cost body</div>,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
expect(withCostTab).toContain("Cost");
|
|
161
|
+
expect(withCostTab).not.toContain("Application cost body");
|
|
162
|
+
|
|
163
|
+
const selectedCost = renderDetail({
|
|
164
|
+
selectedView: "overview",
|
|
165
|
+
onSelectView: () => {},
|
|
166
|
+
selectedWorkloadKey: null,
|
|
167
|
+
onSelectWorkload: () => {},
|
|
168
|
+
renderCostView: () => <div>Application cost body</div>,
|
|
169
|
+
costViewSelected: true,
|
|
170
|
+
onSelectCostView: () => {},
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
expect(selectedCost).toContain("Cost");
|
|
174
|
+
expect(selectedCost).toContain("Application cost body");
|
|
175
|
+
expect(selectedCost).not.toContain("Source & provenance");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("does not render a workload selector for a single-workload app", () => {
|
|
179
|
+
const singleApp: AppRow = {
|
|
180
|
+
...app,
|
|
181
|
+
workloads: [app.workloads[0]],
|
|
182
|
+
};
|
|
183
|
+
const selected = singleApp.workloads[0];
|
|
184
|
+
const html = renderDetail({
|
|
185
|
+
app: singleApp,
|
|
186
|
+
selectedWorkloadKey: workloadKey(selected),
|
|
187
|
+
onSelectWorkload: () => {},
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
expect(html).toContain("Runtime for");
|
|
191
|
+
expect(html).toContain("checkout-api");
|
|
192
|
+
expect(html).toContain("Workload");
|
|
193
|
+
expect(html).toContain("Deployment<!-- -->/<!-- -->checkout-api");
|
|
194
|
+
expect(html).not.toContain('aria-haspopup="listbox"');
|
|
195
|
+
expect(html).not.toContain("Application views");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("opens the workload directly for uncontrolled single-workload apps", () => {
|
|
199
|
+
const singleApp: AppRow = {
|
|
200
|
+
...app,
|
|
201
|
+
workloads: [app.workloads[0]],
|
|
202
|
+
};
|
|
203
|
+
const html = renderDetail({ app: singleApp });
|
|
204
|
+
|
|
205
|
+
expect(html).toContain("Runtime for");
|
|
206
|
+
expect(html).toContain("checkout-api");
|
|
207
|
+
expect(html).not.toContain("Application views");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("renders one filtered application chronology with workload and source actions", () => {
|
|
211
|
+
const html = renderDetail({
|
|
212
|
+
selectedView: "history",
|
|
213
|
+
onSelectView: () => {},
|
|
214
|
+
selectedWorkloadKey: null,
|
|
215
|
+
onSelectWorkload: () => {},
|
|
216
|
+
historyMode: 'local',
|
|
217
|
+
historyRange: 'all',
|
|
218
|
+
historyRangeOptions: [{ value: 'all', label: 'All observed' }],
|
|
219
|
+
onOpenTimeline: () => {},
|
|
220
|
+
historyItems: [
|
|
221
|
+
{
|
|
222
|
+
id: 'source-sync',
|
|
223
|
+
category: 'deployment',
|
|
224
|
+
title: 'Argo CD sync',
|
|
225
|
+
timestamp: '2026-07-13T10:00:00.000Z',
|
|
226
|
+
sourceRef: { type: 'gitops', tool: 'argocd', kind: 'Application', namespace: 'argocd', name: 'checkout' },
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
id: 'runtime-change',
|
|
230
|
+
category: 'change',
|
|
231
|
+
title: 'Deployment updated',
|
|
232
|
+
timestamp: '2026-07-13T09:00:00.000Z',
|
|
233
|
+
resource: { kind: 'Deployment', namespace: 'prod', name: 'checkout-api' },
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
id: 'problem',
|
|
237
|
+
category: 'problem',
|
|
238
|
+
title: 'BackOff',
|
|
239
|
+
timestamp: '2026-07-13T08:00:00.000Z',
|
|
240
|
+
resource: { kind: 'Pod', namespace: 'prod', name: 'checkout-api-abc12' },
|
|
241
|
+
},
|
|
242
|
+
],
|
|
243
|
+
onOpenSource: () => {},
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
expect(html).toContain("Application history");
|
|
247
|
+
expect(html).toContain("Deployments");
|
|
248
|
+
expect(html).toContain("Changes");
|
|
249
|
+
expect(html).toContain("Problems");
|
|
250
|
+
expect(html).toContain("Open full Timeline");
|
|
251
|
+
expect(html).toContain("Deployment");
|
|
252
|
+
expect(html).toContain("prod");
|
|
253
|
+
expect(html).toContain("checkout-api");
|
|
254
|
+
expect(html).toContain("View Argo CD application");
|
|
255
|
+
expect(html).toContain("events retained by this Radar instance");
|
|
256
|
+
expect(html).not.toContain("Current incidents");
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('reports retained event limits separately from recording gaps', () => {
|
|
260
|
+
const html = renderDetail({
|
|
261
|
+
selectedView: 'history',
|
|
262
|
+
onSelectView: () => {},
|
|
263
|
+
selectedWorkloadKey: null,
|
|
264
|
+
onSelectWorkload: () => {},
|
|
265
|
+
historyMode: 'retained',
|
|
266
|
+
historyRange: '7d',
|
|
267
|
+
historyRangeOptions: [{ value: '7d', label: '7 days' }],
|
|
268
|
+
historyRuntimeLimited: true,
|
|
269
|
+
historyItems: [{
|
|
270
|
+
id: 'runtime-change',
|
|
271
|
+
category: 'change',
|
|
272
|
+
title: 'Deployment updated',
|
|
273
|
+
timestamp: '2026-07-13T09:00:00.000Z',
|
|
274
|
+
}],
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
expect(html).toContain('Showing the 10,000 most recent events in these namespaces')
|
|
278
|
+
expect(html).not.toContain('Recording coverage is incomplete')
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
it('shows a single honest error state when runtime history fails without source history', () => {
|
|
282
|
+
const html = renderDetail({
|
|
283
|
+
selectedView: 'history',
|
|
284
|
+
onSelectView: () => {},
|
|
285
|
+
selectedWorkloadKey: null,
|
|
286
|
+
onSelectWorkload: () => {},
|
|
287
|
+
historyRuntimeError: true,
|
|
288
|
+
historyItems: [],
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
expect(html).toContain('Application changes could not be loaded.')
|
|
292
|
+
expect(html).not.toContain('Deployment-source history is still shown.')
|
|
293
|
+
expect(html).not.toContain('No application history in this range.')
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
it('marks retained runtime rows as stale when a refresh fails', () => {
|
|
297
|
+
const html = renderDetail({
|
|
298
|
+
selectedView: 'history',
|
|
299
|
+
onSelectView: () => {},
|
|
300
|
+
selectedWorkloadKey: null,
|
|
301
|
+
onSelectWorkload: () => {},
|
|
302
|
+
historyRuntimeError: true,
|
|
303
|
+
historyItems: [{
|
|
304
|
+
id: 'runtime-change',
|
|
305
|
+
category: 'change',
|
|
306
|
+
title: 'Deployment updated',
|
|
307
|
+
timestamp: '2026-07-13T09:00:00.000Z',
|
|
308
|
+
}],
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
expect(html).toContain('Application changes could not be refreshed.')
|
|
312
|
+
expect(html).toContain('Previously loaded changes may be stale.')
|
|
313
|
+
expect(html).toContain('Deployment updated')
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
it("previews the latest retained deployment change in Overview", () => {
|
|
317
|
+
const healthyApp: AppRow = { ...app, events: [] };
|
|
318
|
+
const history: AppHistory = {
|
|
319
|
+
appKey: healthyApp.key,
|
|
320
|
+
sourceRef: {
|
|
321
|
+
type: "gitops",
|
|
322
|
+
tool: "argocd",
|
|
323
|
+
group: "argoproj.io",
|
|
324
|
+
kind: "Application",
|
|
325
|
+
namespace: "argocd",
|
|
326
|
+
name: "checkout",
|
|
327
|
+
},
|
|
328
|
+
summary: {
|
|
329
|
+
state: "change",
|
|
330
|
+
title: "Argo CD sync",
|
|
331
|
+
detail: "Succeeded · abc123",
|
|
332
|
+
timestamp: "2026-07-08T12:00:00Z",
|
|
333
|
+
},
|
|
334
|
+
anchors: [
|
|
335
|
+
{
|
|
336
|
+
type: "gitops",
|
|
337
|
+
title: "Argo CD sync",
|
|
338
|
+
status: "Succeeded",
|
|
339
|
+
revision: "abc123",
|
|
340
|
+
timestamp: "2026-07-08T12:00:00Z",
|
|
341
|
+
},
|
|
342
|
+
],
|
|
343
|
+
};
|
|
344
|
+
const html = renderDetail({
|
|
345
|
+
app: healthyApp,
|
|
346
|
+
history,
|
|
347
|
+
onOpenSource: () => {},
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
expect(html).toContain('Latest change')
|
|
351
|
+
expect(html).toContain('Argo CD sync')
|
|
352
|
+
expect(html).toContain('History')
|
|
353
|
+
expect(html).toContain('View Argo CD application')
|
|
354
|
+
expect(html).not.toContain('No application issues detected')
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
it('shows an authoritative deployment source without duplicating its action in history', () => {
|
|
358
|
+
const sourceRef: AppSourceRef = { type: 'gitops', tool: 'flux', group: 'kustomize.toolkit.fluxcd.io', kind: 'Kustomization', namespace: 'flux-system', name: 'checkout' }
|
|
359
|
+
const sourceApp: AppRow = { ...app, events: [], sourceRef }
|
|
360
|
+
const history: AppHistory = {
|
|
361
|
+
appKey: sourceApp.key,
|
|
362
|
+
sourceRef,
|
|
363
|
+
summary: { state: 'change', title: 'Flux reconciliation', timestamp: '2026-07-08T12:00:00Z' },
|
|
364
|
+
}
|
|
365
|
+
const html = renderDetail({ app: sourceApp, history, onOpenSource: () => {} })
|
|
366
|
+
|
|
367
|
+
expect(html).toContain('Deployment source')
|
|
368
|
+
expect(html).toContain('Flux Kustomization')
|
|
369
|
+
expect(html).toContain('flux-system/checkout')
|
|
370
|
+
expect(html.match(/View Flux Kustomization/g)).toHaveLength(1)
|
|
371
|
+
expect(html).not.toContain('App key')
|
|
372
|
+
expect(html).not.toContain('Confidence')
|
|
373
|
+
expect(html).not.toContain('Source & provenance')
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
it('separates healthy runtime from degraded delivery and rolls up the application status', () => {
|
|
377
|
+
const sourceRef: AppSourceRef = { type: 'gitops', tool: 'argocd', group: 'argoproj.io', kind: 'Application', namespace: 'argocd', name: 'checkout' }
|
|
378
|
+
const html = renderDetail({
|
|
379
|
+
app: {
|
|
380
|
+
...app,
|
|
381
|
+
health: 'degraded',
|
|
382
|
+
runtimeHealth: 'healthy',
|
|
383
|
+
sourceRef,
|
|
384
|
+
sourceStatus: { sync: 'Synced', health: 'Degraded' },
|
|
385
|
+
},
|
|
386
|
+
onOpenSource: () => {},
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
expect(html).toContain('Degraded')
|
|
390
|
+
expect(html).toContain('Runtime')
|
|
391
|
+
expect(html).toContain('Healthy')
|
|
392
|
+
expect(html).toContain('4/4 ready')
|
|
393
|
+
expect(html).toContain('Delivery')
|
|
394
|
+
expect(html.match(/Synced/g)).toHaveLength(2)
|
|
395
|
+
expect((html.match(/Degraded/g) ?? []).length).toBeGreaterThanOrEqual(3)
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
it('warns when application workloads resolve to different deployment sources', () => {
|
|
399
|
+
const html = renderDetail({ app: { ...app, sourceConflict: true } })
|
|
400
|
+
|
|
401
|
+
expect(html).toContain('Deployment source')
|
|
402
|
+
expect(html).toContain('Multiple deployment sources detected')
|
|
403
|
+
expect(html).toContain('do not share one deployment manager')
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
it('distinguishes inferred application identity from declared identity', () => {
|
|
407
|
+
const inferred = renderDetail({
|
|
408
|
+
app: {
|
|
409
|
+
...app,
|
|
410
|
+
identity: { key: 'checkout', env: 'prod', confidence: 'medium', evidence: 'name stem checkout + shared image repo', source: 'name-stem' },
|
|
411
|
+
},
|
|
412
|
+
})
|
|
413
|
+
expect(inferred).toContain('Application identity')
|
|
414
|
+
expect(inferred).toContain('Inferred application boundary')
|
|
415
|
+
expect(inferred).toContain('a shared name + image')
|
|
416
|
+
|
|
417
|
+
const declared = renderDetail({
|
|
418
|
+
app: {
|
|
419
|
+
...app,
|
|
420
|
+
identity: { key: 'checkout', env: 'prod', confidence: 'high', evidence: 'app.skyhook.io/app=checkout', source: 'explicit' },
|
|
421
|
+
},
|
|
422
|
+
})
|
|
423
|
+
expect(declared).toContain('Identified by the app.skyhook.io/app annotation')
|
|
424
|
+
expect(declared).not.toContain('Inferred application boundary')
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
it('uses tier provenance when a grouped app has no cross-app identity', () => {
|
|
428
|
+
const html = renderDetail({ app: { ...app, tier: 5 } })
|
|
429
|
+
|
|
430
|
+
expect(html).toContain('Application identity')
|
|
431
|
+
expect(html).toContain('Grouped by Helm metadata')
|
|
432
|
+
expect(html).not.toContain('Grouped from Kubernetes ownership')
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
it('does not present completed init containers as a long-running workload state', () => {
|
|
436
|
+
const statefulApp: AppRow = {
|
|
437
|
+
...app,
|
|
438
|
+
workloads: [
|
|
439
|
+
{ ...app.workloads[0], kind: 'StatefulSet', name: 'redis-master', reason: 'Completed' },
|
|
440
|
+
{ ...app.workloads[1], kind: 'StatefulSet', name: 'redis-replicas', reason: 'Completed' },
|
|
441
|
+
],
|
|
442
|
+
}
|
|
443
|
+
const jobApp: AppRow = {
|
|
444
|
+
...app,
|
|
445
|
+
workloads: [
|
|
446
|
+
{ ...app.workloads[0], kind: 'Job', name: 'database-migration', workload_class: 'job', reason: 'Completed' },
|
|
447
|
+
{ ...app.workloads[1], kind: 'CronJob', name: 'database-migration-schedule', workload_class: 'job' },
|
|
448
|
+
],
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
expect(renderDetail({ app: statefulApp })).not.toMatch(/redis-master<\/button><div[^>]*>Completed/)
|
|
452
|
+
expect(renderDetail({ app: jobApp })).toMatch(/database-migration<\/button><div[^>]*>Completed/)
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
it("does not duplicate current incidents in the Overview history preview", () => {
|
|
456
|
+
const history: AppHistory = {
|
|
457
|
+
appKey: app.key,
|
|
458
|
+
summary: {
|
|
459
|
+
state: "incident",
|
|
460
|
+
title: "Current incident: BackOff on Deployment/checkout-api",
|
|
461
|
+
detail: "Back-off restarting failed container",
|
|
462
|
+
},
|
|
463
|
+
incidents: [
|
|
464
|
+
{
|
|
465
|
+
severity: "warning",
|
|
466
|
+
title: "BackOff on Deployment/checkout-api",
|
|
467
|
+
object: "Deployment/checkout-api",
|
|
468
|
+
},
|
|
469
|
+
],
|
|
470
|
+
};
|
|
471
|
+
const html = renderDetail({ history });
|
|
472
|
+
|
|
473
|
+
expect(html).not.toContain("BackOff on checkout-api");
|
|
474
|
+
expect(html).not.toContain(
|
|
475
|
+
"Current incident: BackOff on Deployment/checkout-api",
|
|
476
|
+
);
|
|
477
|
+
expect(html).not.toContain("Latest change");
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it('previews retained incidents in Overview when no current issue card exists', () => {
|
|
481
|
+
const healthyApp: AppRow = { ...app, events: [] }
|
|
482
|
+
const history: AppHistory = {
|
|
483
|
+
appKey: healthyApp.key,
|
|
484
|
+
summary: { state: 'incident', title: 'Current incident: FailedScheduling on Pod/checkout-api-abc', detail: '0/9 nodes are available' },
|
|
485
|
+
incidents: [{ severity: 'warning', title: 'FailedScheduling on Pod/checkout-api-abc', object: 'Pod/checkout-api-abc' }],
|
|
486
|
+
}
|
|
487
|
+
const html = renderDetail({
|
|
488
|
+
app: healthyApp,
|
|
489
|
+
history,
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
expect(html).toContain('Latest incident')
|
|
493
|
+
expect(html).toContain('Current incident: FailedScheduling on Pod/checkout-api-abc')
|
|
494
|
+
expect(html).toContain('View history')
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
it('does not duplicate retained incidents when the host renders current issues', () => {
|
|
498
|
+
const healthyApp: AppRow = { ...app, events: [] }
|
|
499
|
+
const history: AppHistory = {
|
|
500
|
+
appKey: healthyApp.key,
|
|
501
|
+
summary: { state: 'incident', title: 'Current incident: FailedScheduling on Pod/checkout-api-abc' },
|
|
502
|
+
}
|
|
503
|
+
const html = renderDetail({
|
|
504
|
+
app: healthyApp,
|
|
505
|
+
history,
|
|
506
|
+
renderOverviewIssues: () => <div>Operational Issues (1)</div>,
|
|
507
|
+
hasOverviewIssues: true,
|
|
508
|
+
})
|
|
509
|
+
|
|
510
|
+
expect(html).toContain('Operational Issues (1)')
|
|
511
|
+
expect(html).not.toContain('Latest incident')
|
|
512
|
+
expect(html).not.toContain('Current incident: FailedScheduling on Pod/checkout-api-abc')
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
it('does not report idle zero-replica workloads as application issues', () => {
|
|
516
|
+
const idleApp: AppRow = {
|
|
517
|
+
...app,
|
|
518
|
+
health: "healthy",
|
|
519
|
+
events: [],
|
|
520
|
+
workloads: [
|
|
521
|
+
{
|
|
522
|
+
kind: "CronJob",
|
|
523
|
+
namespace: "prod",
|
|
524
|
+
name: "checkout-cleanup",
|
|
525
|
+
workload_class: "job",
|
|
526
|
+
health: "neutral",
|
|
527
|
+
ready: 0,
|
|
528
|
+
desired: 0,
|
|
529
|
+
restarts: 0,
|
|
530
|
+
version: "1.2.3",
|
|
531
|
+
},
|
|
532
|
+
{
|
|
533
|
+
kind: "Job",
|
|
534
|
+
namespace: "prod",
|
|
535
|
+
name: "checkout-cleanup-123",
|
|
536
|
+
workload_class: "job",
|
|
537
|
+
health: "unknown",
|
|
538
|
+
ready: 0,
|
|
539
|
+
desired: 0,
|
|
540
|
+
restarts: 0,
|
|
541
|
+
version: "1.2.3",
|
|
542
|
+
},
|
|
543
|
+
],
|
|
544
|
+
};
|
|
545
|
+
const html = renderDetail({ app: idleApp });
|
|
546
|
+
|
|
547
|
+
expect(html).not.toContain('No application issues detected')
|
|
548
|
+
expect(html).not.toContain('needs attention')
|
|
549
|
+
})
|
|
550
|
+
|
|
551
|
+
it("uses the resource namespace when linking a history item to a workload", () => {
|
|
552
|
+
const ambiguousApp: AppRow = {
|
|
553
|
+
...app,
|
|
554
|
+
workloads: [
|
|
555
|
+
...app.workloads,
|
|
556
|
+
{
|
|
557
|
+
kind: "Deployment",
|
|
558
|
+
namespace: "canary",
|
|
559
|
+
name: "checkout-api",
|
|
560
|
+
workload_class: "service",
|
|
561
|
+
health: "healthy",
|
|
562
|
+
ready: 1,
|
|
563
|
+
desired: 1,
|
|
564
|
+
restarts: 0,
|
|
565
|
+
version: "1.2.3",
|
|
566
|
+
},
|
|
567
|
+
],
|
|
568
|
+
};
|
|
569
|
+
const html = renderDetail({
|
|
570
|
+
app: ambiguousApp,
|
|
571
|
+
selectedView: "history",
|
|
572
|
+
onSelectView: () => {},
|
|
573
|
+
selectedWorkloadKey: null,
|
|
574
|
+
onSelectWorkload: () => {},
|
|
575
|
+
historyItems: [{
|
|
576
|
+
id: 'prod-change',
|
|
577
|
+
category: 'change',
|
|
578
|
+
title: 'Deployment updated',
|
|
579
|
+
timestamp: '2026-07-13T09:00:00.000Z',
|
|
580
|
+
resource: { kind: 'Deployment', namespace: 'prod', name: 'checkout-api' },
|
|
581
|
+
}],
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
expect(html).toContain("Deployment");
|
|
585
|
+
expect(html).toContain("prod");
|
|
586
|
+
expect(html).toContain("checkout-api");
|
|
587
|
+
expect(html).not.toContain("canary/checkout-api");
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
it("shows batch activity and injected operational issues on the application overview", () => {
|
|
591
|
+
const batchApp: AppRow = {
|
|
592
|
+
key: "app:prod:batch",
|
|
593
|
+
name: "batch",
|
|
594
|
+
namespace: "prod",
|
|
595
|
+
health: "unhealthy",
|
|
596
|
+
workload_class: "job",
|
|
597
|
+
workloads: [
|
|
598
|
+
{
|
|
599
|
+
kind: "CronJob",
|
|
600
|
+
namespace: "prod",
|
|
601
|
+
name: "nightly",
|
|
602
|
+
workload_class: "job",
|
|
603
|
+
health: "healthy",
|
|
604
|
+
ready: 0,
|
|
605
|
+
desired: 0,
|
|
606
|
+
restarts: 0,
|
|
607
|
+
batch: {
|
|
608
|
+
schedule: "0 3 * * *",
|
|
609
|
+
retainedRuns: 3,
|
|
610
|
+
succeededRuns: 3,
|
|
611
|
+
failedRuns: 0,
|
|
612
|
+
activeRuns: 0,
|
|
613
|
+
latestRunName: "nightly-29720340",
|
|
614
|
+
latestRunPhase: "Succeeded",
|
|
615
|
+
latestStartedAt: "2026-07-05T10:25:00Z",
|
|
616
|
+
lastSuccessfulAt: "2026-07-05T10:29:00Z",
|
|
617
|
+
},
|
|
618
|
+
},
|
|
619
|
+
{
|
|
620
|
+
kind: "Job",
|
|
621
|
+
namespace: "prod",
|
|
622
|
+
name: "ad-hoc-import",
|
|
623
|
+
workload_class: "job",
|
|
624
|
+
health: "unhealthy",
|
|
625
|
+
ready: 0,
|
|
626
|
+
desired: 1,
|
|
627
|
+
restarts: 0,
|
|
628
|
+
batch: {
|
|
629
|
+
retainedRuns: 1,
|
|
630
|
+
succeededRuns: 0,
|
|
631
|
+
failedRuns: 1,
|
|
632
|
+
activeRuns: 0,
|
|
633
|
+
latestRunName: "ad-hoc-import",
|
|
634
|
+
latestRunPhase: "Failed",
|
|
635
|
+
latestStartedAt: "2026-07-05T09:25:00Z",
|
|
636
|
+
message: "Job has reached the specified backoff limit",
|
|
637
|
+
},
|
|
638
|
+
},
|
|
639
|
+
],
|
|
640
|
+
};
|
|
641
|
+
const html = renderDetail({
|
|
642
|
+
app: batchApp,
|
|
643
|
+
renderOverviewIssues: () => <div>Operational Issues (1)</div>,
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
expect(html).toContain("Application views");
|
|
647
|
+
expect(html).toContain("Operational Issues (1)");
|
|
648
|
+
expect(html).not.toContain("Batch activity");
|
|
649
|
+
const workloadsPanel = html.indexOf(">Workloads</h2>");
|
|
650
|
+
expect(workloadsPanel).toBeGreaterThanOrEqual(0);
|
|
651
|
+
expect(html).toContain("Batch resources");
|
|
652
|
+
expect(html).toContain("Active runs");
|
|
653
|
+
expect(html).toContain("Retained runs");
|
|
654
|
+
expect(html).toContain("1 CronJob / 1 Job");
|
|
655
|
+
expect(html).toContain("3 succeeded / 1 failed");
|
|
656
|
+
expect(html).toContain("nightly-29720340");
|
|
657
|
+
expect(html).toContain("ad-hoc-import");
|
|
658
|
+
expect(html).not.toContain("ad-hoc-import is down");
|
|
659
|
+
expect(html).not.toContain("Runtime for");
|
|
660
|
+
expect(html).toContain('2xl:grid-cols-4');
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
it("uses the delivery verdict in a successful batch application header", () => {
|
|
664
|
+
const batchApp: AppRow = {
|
|
665
|
+
key: "app:prod:batch",
|
|
666
|
+
name: "batch",
|
|
667
|
+
namespace: "prod",
|
|
668
|
+
health: "unhealthy",
|
|
669
|
+
runtimeHealth: "unhealthy",
|
|
670
|
+
workload_class: "job",
|
|
671
|
+
sourceStatus: { sync: "Synced", health: "Degraded" },
|
|
672
|
+
workloads: [
|
|
673
|
+
{
|
|
674
|
+
kind: "CronJob",
|
|
675
|
+
namespace: "prod",
|
|
676
|
+
name: "nightly",
|
|
677
|
+
workload_class: "job",
|
|
678
|
+
health: "unhealthy",
|
|
679
|
+
ready: 0,
|
|
680
|
+
desired: 0,
|
|
681
|
+
restarts: 0,
|
|
682
|
+
batch: {
|
|
683
|
+
retainedRuns: 2,
|
|
684
|
+
succeededRuns: 1,
|
|
685
|
+
failedRuns: 1,
|
|
686
|
+
activeRuns: 0,
|
|
687
|
+
latestRunPhase: "Succeeded",
|
|
688
|
+
},
|
|
689
|
+
},
|
|
690
|
+
],
|
|
691
|
+
};
|
|
692
|
+
const html = renderDetail({ app: batchApp });
|
|
693
|
+
|
|
694
|
+
expect(html).toMatch(/<span class="text-sm font-semibold">Degraded<\/span>/);
|
|
695
|
+
expect(html).toContain("Succeeded");
|
|
696
|
+
});
|
|
697
|
+
});
|