@skyhook-io/k8s-ui 1.8.9 → 1.8.11
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 +5 -4
- 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 +569 -0
- package/src/components/applications/ApplicationDetail.tsx +2517 -450
- package/src/components/applications/ApplicationsList.tsx +4 -1
- package/src/components/applications/ApplicationsView.tsx +101 -18
- 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 +81 -51
- package/src/components/checks/severity.ts +39 -45
- package/src/components/cluster-switcher/ClusterSwitcher.tsx +2 -2
- package/src/components/compare/CompareResourcePicker.tsx +2 -2
- package/src/components/gitops/GitOpsGraphFilterRail.tsx +2 -1
- package/src/components/gitops/GitOpsTableView.tsx +14 -59
- package/src/components/gitops/ManagedResourcesList.tsx +8 -11
- package/src/components/gitops/SyncCountdown.tsx +26 -17
- package/src/components/gitops/SyncOptionsDialog.tsx +2 -2
- package/src/components/gitops/insights/GitOpsInsightViews.tsx +277 -104
- package/src/components/gitops/insights/insights-helpers.test.ts +86 -1
- package/src/components/gitops/insights/insights-helpers.ts +69 -1
- package/src/components/gitops/tree/GitOpsTreeGraph.tsx +5 -2
- package/src/components/issues/IssuesView.tsx +219 -143
- package/src/components/issues/ResourceIssuesSection.test.tsx +40 -0
- package/src/components/issues/ResourceIssuesSection.tsx +28 -128
- package/src/components/issues/index.ts +3 -2
- package/src/components/issues/issue-timing.ts +53 -0
- package/src/components/issues/issues.test.ts +146 -4
- package/src/components/issues/severity.ts +29 -32
- 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 +107 -0
- package/src/components/resources/ResourcesSidebar.tsx +70 -52
- package/src/components/resources/ResourcesView.selected-kind.test.ts +143 -0
- package/src/components/resources/ResourcesView.tsx +243 -53
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +99 -0
- package/src/components/resources/renderers/JobRenderer.tsx +3 -1
- package/src/components/resources/renderers/MetricsUnavailableNotice.tsx +47 -0
- package/src/components/resources/renderers/NodeRenderer.test.tsx +103 -0
- package/src/components/resources/renderers/NodeRenderer.tsx +20 -12
- package/src/components/resources/renderers/PVCRenderer.test.tsx +23 -0
- package/src/components/resources/renderers/PVCRenderer.tsx +2 -2
- package/src/components/resources/renderers/PodRenderer.test.tsx +101 -0
- package/src/components/resources/renderers/PodRenderer.tsx +81 -70
- package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +2 -2
- package/src/components/resources/renderers/SbomReportRenderer.tsx +2 -2
- package/src/components/resources/renderers/SecretRenderer.tsx +2 -0
- package/src/components/resources/renderers/ServiceRenderer.tsx +58 -40
- 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-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 +140 -108
- package/src/components/shared/ResourceRendererDispatch.tsx +33 -4
- package/src/components/timeline/TimelineList.tsx +371 -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 +64 -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/TopologyFilterSidebar.tsx +9 -0
- package/src/components/topology/TopologyGraph.tsx +89 -16
- package/src/components/topology/TopologySearch.tsx +2 -2
- package/src/components/topology/layout.ts +10 -2
- package/src/components/topology/topology.css +0 -92
- package/src/components/ui/Badge.tsx +5 -0
- package/src/components/ui/BoardSkeleton.tsx +47 -0
- package/src/components/ui/CardSection.tsx +117 -0
- 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/Toast.tsx +1 -1
- package/src/components/ui/drawer-components.tsx +39 -26
- package/src/components/ui/index.ts +6 -0
- package/src/components/ui/severity-tone.ts +63 -0
- package/src/components/workload/ResourceDetailDrawer.tsx +6 -2
- package/src/components/workload/WorkloadView.tsx +2682 -472
- package/src/components/workload/index.ts +1 -1
- package/src/theme/variables.css +15 -1
- package/src/types/core.ts +57 -4
- package/src/utils/api-resources.test.ts +54 -0
- package/src/utils/api-resources.ts +68 -1
- package/src/utils/application-topology.test.ts +198 -0
- package/src/utils/application-topology.ts +261 -0
- package/src/utils/applications.test.ts +167 -1
- package/src/utils/applications.ts +1157 -393
- package/src/utils/index.ts +2 -0
- package/src/utils/navigation.test.ts +32 -1
- package/src/utils/navigation.ts +70 -0
- package/src/utils/resource-hierarchy.test.ts +990 -1
- package/src/utils/resource-hierarchy.ts +905 -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
- package/tsconfig.json +2 -2
|
@@ -217,6 +217,8 @@ export function SecretRenderer({ data, certificateInfo, resourceData, onSaveSecr
|
|
|
217
217
|
ref={textareaRef}
|
|
218
218
|
value={editValue}
|
|
219
219
|
onChange={(e) => setEditValue(e.target.value)}
|
|
220
|
+
autoCorrect="off"
|
|
221
|
+
autoCapitalize="off"
|
|
220
222
|
className="w-full bg-theme-base rounded p-2 text-xs text-theme-text-secondary font-mono border border-blue-500/50 focus:border-blue-500 focus:outline-none resize-none overflow-hidden whitespace-pre-wrap"
|
|
221
223
|
style={{ minHeight: '60px' }}
|
|
222
224
|
disabled={isSaving}
|
|
@@ -3,6 +3,15 @@ import { Globe, Clock, Radio } from 'lucide-react'
|
|
|
3
3
|
import { Section, PropertyList, Property, KeyValueBadgeList, CopyHandler, AlertBanner } from '../../ui/drawer-components'
|
|
4
4
|
import type { ResourceRef } from '../../../types'
|
|
5
5
|
|
|
6
|
+
export interface ServicePortRenderProps {
|
|
7
|
+
namespace: string
|
|
8
|
+
serviceName: string
|
|
9
|
+
port: number
|
|
10
|
+
protocol: string
|
|
11
|
+
name?: string
|
|
12
|
+
appProtocol?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
6
15
|
interface ServiceRendererProps {
|
|
7
16
|
data: any
|
|
8
17
|
onCopy: CopyHandler
|
|
@@ -10,11 +19,8 @@ interface ServiceRendererProps {
|
|
|
10
19
|
endpointSlices?: any[]
|
|
11
20
|
endpointSlicesLoading?: boolean
|
|
12
21
|
onNavigate?: (ref: ResourceRef) => void
|
|
13
|
-
renderPortAction?: (props:
|
|
14
|
-
|
|
15
|
-
* (e.g. an inline probe panel). Lets a host attach a port-scoped panel in the
|
|
16
|
-
* drawer flow rather than as a separate overlay. */
|
|
17
|
-
renderPortPanel?: (props: { namespace: string; serviceName: string; port: number; protocol: string; name?: string; appProtocol?: string }) => ReactNode
|
|
22
|
+
renderPortAction?: (props: ServicePortRenderProps) => ReactNode
|
|
23
|
+
renderPortPanel?: (props: ServicePortRenderProps) => ReactNode
|
|
18
24
|
}
|
|
19
25
|
|
|
20
26
|
function endpointSliceAddressCount(slice: any): number {
|
|
@@ -37,7 +43,6 @@ export function ServiceRenderer({ data, onCopy, copied, endpointSlices, endpoint
|
|
|
37
43
|
const ports = spec.ports || []
|
|
38
44
|
const lbIngress = data.status?.loadBalancer?.ingress || []
|
|
39
45
|
const namespace = data.metadata?.namespace
|
|
40
|
-
const serviceName = data.metadata?.name
|
|
41
46
|
|
|
42
47
|
const isLoadBalancer = spec.type === 'LoadBalancer'
|
|
43
48
|
const isExternalName = spec.type === 'ExternalName'
|
|
@@ -96,40 +101,7 @@ export function ServiceRenderer({ data, onCopy, copied, endpointSlices, endpoint
|
|
|
96
101
|
|
|
97
102
|
{ports.length > 0 && (
|
|
98
103
|
<Section title="Ports" defaultExpanded>
|
|
99
|
-
<
|
|
100
|
-
{ports.map((port: any, i: number) => (
|
|
101
|
-
<div key={`${port.port}-${port.protocol || 'TCP'}`} className="card-inner text-sm">
|
|
102
|
-
<div className="flex items-center justify-between gap-2">
|
|
103
|
-
<div className="flex items-baseline gap-x-2 gap-y-0.5 min-w-0 flex-wrap">
|
|
104
|
-
<span className="text-theme-text-primary font-medium">{port.name || `port-${i + 1}`}</span>
|
|
105
|
-
<span className="text-xs text-theme-text-tertiary">{port.protocol || 'TCP'}</span>
|
|
106
|
-
<span className="text-xs text-theme-text-secondary font-mono">
|
|
107
|
-
{port.port}{port.targetPort != null && port.targetPort !== port.port ? ` → ${port.targetPort}` : ''}
|
|
108
|
-
{port.nodePort ? ` (NodePort: ${port.nodePort})` : ''}
|
|
109
|
-
</span>
|
|
110
|
-
</div>
|
|
111
|
-
<div className="flex items-center gap-2 shrink-0">
|
|
112
|
-
{renderPortAction?.({
|
|
113
|
-
namespace,
|
|
114
|
-
serviceName,
|
|
115
|
-
port: port.port,
|
|
116
|
-
protocol: port.protocol || 'TCP',
|
|
117
|
-
name: port.name,
|
|
118
|
-
appProtocol: port.appProtocol,
|
|
119
|
-
})}
|
|
120
|
-
</div>
|
|
121
|
-
</div>
|
|
122
|
-
{renderPortPanel?.({
|
|
123
|
-
namespace,
|
|
124
|
-
serviceName,
|
|
125
|
-
port: port.port,
|
|
126
|
-
protocol: port.protocol || 'TCP',
|
|
127
|
-
name: port.name,
|
|
128
|
-
appProtocol: port.appProtocol,
|
|
129
|
-
})}
|
|
130
|
-
</div>
|
|
131
|
-
))}
|
|
132
|
-
</div>
|
|
104
|
+
<ServicePortCards service={data} renderPortAction={renderPortAction} renderPortPanel={renderPortPanel} />
|
|
133
105
|
</Section>
|
|
134
106
|
)}
|
|
135
107
|
|
|
@@ -184,3 +156,49 @@ export function ServiceRenderer({ data, onCopy, copied, endpointSlices, endpoint
|
|
|
184
156
|
</>
|
|
185
157
|
)
|
|
186
158
|
}
|
|
159
|
+
|
|
160
|
+
export function ServicePortCards({
|
|
161
|
+
service,
|
|
162
|
+
renderPortAction,
|
|
163
|
+
renderPortPanel,
|
|
164
|
+
}: {
|
|
165
|
+
service: any
|
|
166
|
+
renderPortAction?: (props: ServicePortRenderProps) => ReactNode
|
|
167
|
+
renderPortPanel?: (props: ServicePortRenderProps) => ReactNode
|
|
168
|
+
}) {
|
|
169
|
+
const ports = service?.spec?.ports || []
|
|
170
|
+
const namespace = service?.metadata?.namespace || ''
|
|
171
|
+
const serviceName = service?.metadata?.name || ''
|
|
172
|
+
return (
|
|
173
|
+
<div className="space-y-2">
|
|
174
|
+
{ports.map((port: any, i: number) => {
|
|
175
|
+
const props: ServicePortRenderProps = {
|
|
176
|
+
namespace,
|
|
177
|
+
serviceName,
|
|
178
|
+
port: port.port,
|
|
179
|
+
protocol: port.protocol || 'TCP',
|
|
180
|
+
name: port.name,
|
|
181
|
+
appProtocol: port.appProtocol,
|
|
182
|
+
}
|
|
183
|
+
return (
|
|
184
|
+
<div key={`${serviceName}-${port.name || i}-${port.port}-${port.protocol || 'TCP'}`} className="card-inner text-sm">
|
|
185
|
+
<div className="flex items-center justify-between gap-2">
|
|
186
|
+
<div className="flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-0.5">
|
|
187
|
+
<span className="font-medium text-theme-text-primary">{port.name || `port-${i + 1}`}</span>
|
|
188
|
+
<span className="text-xs text-theme-text-tertiary">{port.protocol || 'TCP'}</span>
|
|
189
|
+
<span className="font-mono text-xs text-theme-text-secondary">
|
|
190
|
+
{port.port}{port.targetPort != null && port.targetPort !== port.port ? ` → ${port.targetPort}` : ''}
|
|
191
|
+
{port.nodePort ? ` (NodePort: ${port.nodePort})` : ''}
|
|
192
|
+
</span>
|
|
193
|
+
</div>
|
|
194
|
+
<div className="flex shrink-0 items-center gap-2">
|
|
195
|
+
{renderPortAction?.(props)}
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
{renderPortPanel?.(props)}
|
|
199
|
+
</div>
|
|
200
|
+
)
|
|
201
|
+
})}
|
|
202
|
+
</div>
|
|
203
|
+
)
|
|
204
|
+
}
|
|
@@ -2,6 +2,7 @@ import { useState, useMemo } from 'react'
|
|
|
2
2
|
import { Shield, Package, ChevronDown, ChevronRight, ChevronUp, ExternalLink, Search, ArrowUpDown, Wrench } from 'lucide-react'
|
|
3
3
|
import { clsx } from 'clsx'
|
|
4
4
|
import { Section, PropertyList, Property } from '../../ui/drawer-components'
|
|
5
|
+
import { Input } from '../../ui/Input'
|
|
5
6
|
import { formatAge } from '../resource-utils'
|
|
6
7
|
import { SEVERITY_BADGE_COLORS, SEVERITY_BAR_COLORS, SEVERITY_TEXT_COLORS, SEVERITY_ORDER, TrivyAlertBanner, formatTrivyImage } from './trivy-shared'
|
|
7
8
|
|
|
@@ -222,8 +223,7 @@ export function VulnerabilityReportRenderer({ data }: VulnerabilityReportRendere
|
|
|
222
223
|
<div className="flex items-center gap-2 mb-2 px-1">
|
|
223
224
|
<div className="relative flex-1">
|
|
224
225
|
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3 h-3 text-theme-text-tertiary" />
|
|
225
|
-
<
|
|
226
|
-
type="text"
|
|
226
|
+
<Input
|
|
227
227
|
placeholder="Filter by CVE or package..."
|
|
228
228
|
value={searchTerm}
|
|
229
229
|
onChange={(e) => { setSearchTerm(e.target.value); setShowAll(false) }}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { renderToString } from 'react-dom/server'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { WorkflowRenderer } from './WorkflowRenderer'
|
|
5
|
+
|
|
6
|
+
describe('WorkflowRenderer', () => {
|
|
7
|
+
it('does not turn retained failed attempts into a failed workflow', () => {
|
|
8
|
+
const html = renderToString(<WorkflowRenderer data={{
|
|
9
|
+
metadata: { name: 'retry-workflow' },
|
|
10
|
+
status: {
|
|
11
|
+
phase: 'Succeeded',
|
|
12
|
+
nodes: {
|
|
13
|
+
root: { name: 'retry-workflow', displayName: 'retry-workflow', templateName: 'main', type: 'Retry', phase: 'Succeeded', children: ['attempt'] },
|
|
14
|
+
attempt: { displayName: 'retry-workflow(0)', type: 'Pod', phase: 'Failed', message: 'first attempt failed' },
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
}} />)
|
|
18
|
+
|
|
19
|
+
expect(html).toContain('Workflow Completed Successfully')
|
|
20
|
+
expect(html).not.toContain('Workflow Issues')
|
|
21
|
+
expect(html).toContain('Execution (2 nodes)')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('lists every failed node and bounds long problem summaries', () => {
|
|
25
|
+
const longMessage = 'x'.repeat(500)
|
|
26
|
+
const html = renderToString(<WorkflowRenderer data={{
|
|
27
|
+
metadata: { name: 'failed-workflow' },
|
|
28
|
+
status: {
|
|
29
|
+
phase: 'Failed',
|
|
30
|
+
message: 'workflow failed',
|
|
31
|
+
nodes: {
|
|
32
|
+
root: { name: 'failed-workflow', displayName: 'failed-workflow', templateName: 'main', type: 'DAG', phase: 'Failed', children: ['with-message', 'without-message'] },
|
|
33
|
+
'with-message': { displayName: 'prepare', type: 'Pod', phase: 'Failed', message: longMessage },
|
|
34
|
+
'without-message': { displayName: 'publish', type: 'Pod', phase: 'Failed' },
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
}} />)
|
|
38
|
+
|
|
39
|
+
expect(html).toContain('Workflow Issues')
|
|
40
|
+
expect(html).toContain('publish failed')
|
|
41
|
+
expect(html).toContain(`prepare: ${'x'.repeat(290)}…`)
|
|
42
|
+
})
|
|
43
|
+
})
|
|
@@ -1,23 +1,16 @@
|
|
|
1
1
|
import { Play, Clock, CheckCircle, XCircle, Loader2, SkipForward, PauseCircle } from 'lucide-react'
|
|
2
2
|
import { clsx } from 'clsx'
|
|
3
|
-
import { Section, PropertyList, Property, ConditionsSection, AlertBanner } from '../../ui/drawer-components'
|
|
3
|
+
import { Section, PropertyList, Property, ConditionsSection, AlertBanner, ResourceLink } from '../../ui/drawer-components'
|
|
4
4
|
import { formatAge, formatDuration } from '../resource-utils'
|
|
5
|
+
import { buildWorkflowExecutionModel, flattenWorkflowExecution, WorkflowExecutionModel, WorkflowExecutionNode, WorkflowTemplateReference } from '../../../utils/workflow-execution'
|
|
6
|
+
import { SEVERITY_TEXT } from '../../../utils/badge-colors'
|
|
5
7
|
|
|
6
8
|
interface WorkflowRendererProps {
|
|
7
9
|
data: any
|
|
10
|
+
onNavigate?: (ref: { kind: string; namespace: string; name: string; group?: string }) => void
|
|
8
11
|
}
|
|
9
12
|
|
|
10
|
-
|
|
11
|
-
id: string
|
|
12
|
-
displayName: string
|
|
13
|
-
phase: string
|
|
14
|
-
startedAt: string | null
|
|
15
|
-
finishedAt: string | null
|
|
16
|
-
message: string | null
|
|
17
|
-
nodeType: string
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function getStepDuration(step: WorkflowStep): string | null {
|
|
13
|
+
function getStepDuration(step: WorkflowExecutionNode): string | null {
|
|
21
14
|
if (!step.startedAt) return null
|
|
22
15
|
const start = new Date(step.startedAt)
|
|
23
16
|
const end = step.finishedAt ? new Date(step.finishedAt) : new Date()
|
|
@@ -29,15 +22,16 @@ function StepStatusIcon({ phase, nodeType }: { phase: string; nodeType?: string
|
|
|
29
22
|
return <SkipForward className="w-4 h-4 text-theme-text-tertiary shrink-0" />
|
|
30
23
|
}
|
|
31
24
|
if (nodeType === 'Suspend' && phase !== 'Succeeded') {
|
|
32
|
-
return <PauseCircle className=
|
|
25
|
+
return <PauseCircle className={clsx('h-4 w-4 shrink-0', SEVERITY_TEXT.warning)} />
|
|
33
26
|
}
|
|
34
27
|
switch (phase) {
|
|
35
28
|
case 'Succeeded':
|
|
36
|
-
return <CheckCircle className=
|
|
29
|
+
return <CheckCircle className={clsx('h-4 w-4 shrink-0', SEVERITY_TEXT.success)} />
|
|
37
30
|
case 'Failed':
|
|
38
|
-
|
|
31
|
+
case 'Error':
|
|
32
|
+
return <XCircle className={clsx('h-4 w-4 shrink-0', SEVERITY_TEXT.error)} />
|
|
39
33
|
case 'Running':
|
|
40
|
-
return <Loader2 className=
|
|
34
|
+
return <Loader2 className={clsx('h-4 w-4 shrink-0 animate-spin', SEVERITY_TEXT.warning)} />
|
|
41
35
|
default:
|
|
42
36
|
return <Clock className="w-4 h-4 text-theme-text-tertiary shrink-0" />
|
|
43
37
|
}
|
|
@@ -74,66 +68,41 @@ function formatEstimatedDuration(seconds: number): string {
|
|
|
74
68
|
return remainingMinutes > 0 ? `${hours}h ${remainingMinutes}m` : `${hours}h`
|
|
75
69
|
}
|
|
76
70
|
|
|
77
|
-
function getWorkflowProblems(data: any): string[] {
|
|
71
|
+
function getWorkflowProblems(data: any, execution: WorkflowExecutionModel): string[] {
|
|
78
72
|
const problems: string[] = []
|
|
79
73
|
const status = data.status || {}
|
|
80
74
|
const phase = status.phase
|
|
81
75
|
|
|
82
76
|
if (phase === 'Failed') {
|
|
83
|
-
problems.push(status.message || 'Workflow failed')
|
|
77
|
+
problems.push(workflowProblemSummary(status.message || 'Workflow failed'))
|
|
84
78
|
} else if (phase === 'Error') {
|
|
85
|
-
problems.push(status.message || 'Workflow error')
|
|
79
|
+
problems.push(workflowProblemSummary(status.message || 'Workflow error'))
|
|
86
80
|
}
|
|
87
81
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
82
|
+
const failedNodes = phase === 'Failed' || phase === 'Error'
|
|
83
|
+
? execution.focusPaths.map((path) => path.terminal).filter((node, index, nodes) =>
|
|
84
|
+
(node.phase === 'Failed' || node.phase === 'Error') && nodes.findIndex((candidate) => candidate.id === node.id) === index,
|
|
85
|
+
)
|
|
86
|
+
: []
|
|
92
87
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (withMessages.length > 0) {
|
|
96
|
-
for (const n of withMessages as any[]) {
|
|
97
|
-
problems.push(`${n.displayName}: ${n.message}`)
|
|
98
|
-
}
|
|
99
|
-
} else {
|
|
100
|
-
problems.push(`Failed steps: ${failedNodes.map((n: any) => n.displayName).join(', ')}`)
|
|
101
|
-
}
|
|
88
|
+
for (const node of failedNodes) {
|
|
89
|
+
problems.push(workflowProblemSummary(node.message ? `${node.displayLabel}: ${node.message}` : `${node.displayLabel} failed`))
|
|
102
90
|
}
|
|
103
91
|
|
|
104
92
|
return problems
|
|
105
93
|
}
|
|
106
94
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
function extractSteps(data: any): WorkflowStep[] {
|
|
110
|
-
const nodes = data.status?.nodes || {}
|
|
111
|
-
const steps: WorkflowStep[] = Object.entries(nodes)
|
|
112
|
-
.filter(([, node]: [string, any]) => VISIBLE_NODE_TYPES.has(node.type))
|
|
113
|
-
.map(([id, node]: [string, any]) => ({
|
|
114
|
-
id,
|
|
115
|
-
displayName: node.displayName || id,
|
|
116
|
-
phase: node.phase || (node.type === 'Skipped' ? 'Skipped' : 'Pending'),
|
|
117
|
-
startedAt: node.startedAt || null,
|
|
118
|
-
finishedAt: node.finishedAt || null,
|
|
119
|
-
message: node.message || null,
|
|
120
|
-
nodeType: node.type,
|
|
121
|
-
}))
|
|
122
|
-
|
|
123
|
-
steps.sort((a, b) => {
|
|
124
|
-
if (!a.startedAt && !b.startedAt) return 0
|
|
125
|
-
if (!a.startedAt) return 1
|
|
126
|
-
if (!b.startedAt) return -1
|
|
127
|
-
return new Date(a.startedAt).getTime() - new Date(b.startedAt).getTime()
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
return steps
|
|
95
|
+
function workflowProblemSummary(message: string): string {
|
|
96
|
+
return message.length > 300 ? `${message.slice(0, 299)}…` : message
|
|
131
97
|
}
|
|
132
98
|
|
|
133
|
-
export function WorkflowRenderer({ data }: WorkflowRendererProps) {
|
|
99
|
+
export function WorkflowRenderer({ data, onNavigate }: WorkflowRendererProps) {
|
|
134
100
|
const status = data.status || {}
|
|
135
101
|
const spec = data.spec || {}
|
|
136
102
|
const phase = status.phase || 'Unknown'
|
|
103
|
+
const execution = buildWorkflowExecutionModel(data)
|
|
104
|
+
const executionNodes = execution.visibleSteps
|
|
105
|
+
const stepRows = flattenWorkflowExecution(execution)
|
|
137
106
|
|
|
138
107
|
// Compute duration
|
|
139
108
|
const startedAt = status.startedAt ? new Date(status.startedAt) : null
|
|
@@ -144,18 +113,13 @@ export function WorkflowRenderer({ data }: WorkflowRendererProps) {
|
|
|
144
113
|
? formatDuration(Date.now() - startedAt.getTime(), true) + ' (running)'
|
|
145
114
|
: null
|
|
146
115
|
|
|
147
|
-
|
|
148
|
-
const problems = getWorkflowProblems(data)
|
|
116
|
+
const problems = [...new Set(getWorkflowProblems(data, execution))]
|
|
149
117
|
const hasProblems = problems.length > 0
|
|
150
118
|
|
|
151
|
-
// Extract steps
|
|
152
|
-
const steps = extractSteps(data)
|
|
153
|
-
|
|
154
119
|
// Arguments
|
|
155
120
|
const parameters = spec.arguments?.parameters || []
|
|
156
121
|
|
|
157
|
-
|
|
158
|
-
const templateName = spec.workflowTemplateRef?.name || null
|
|
122
|
+
const workflowTemplateRef = execution.templateRefs.find((ref) => ref.source === 'workflow')
|
|
159
123
|
|
|
160
124
|
// Estimated duration
|
|
161
125
|
const estimatedDuration = status.estimatedDuration
|
|
@@ -183,7 +147,7 @@ export function WorkflowRenderer({ data }: WorkflowRendererProps) {
|
|
|
183
147
|
{duration && <Property label="Duration" value={duration} />}
|
|
184
148
|
{status.startedAt && <Property label="Started" value={formatAge(status.startedAt)} />}
|
|
185
149
|
<Property label="Finished" value={status.finishedAt ? formatAge(status.finishedAt) : 'Running...'} />
|
|
186
|
-
{
|
|
150
|
+
{workflowTemplateRef && <Property label="Definition" value={<TemplateRefLink refInfo={workflowTemplateRef} onNavigate={onNavigate} />} />}
|
|
187
151
|
{status.progress && (
|
|
188
152
|
<Property label="Progress" value={status.progress} />
|
|
189
153
|
)}
|
|
@@ -203,33 +167,38 @@ export function WorkflowRenderer({ data }: WorkflowRendererProps) {
|
|
|
203
167
|
</PropertyList>
|
|
204
168
|
</Section>
|
|
205
169
|
|
|
206
|
-
{
|
|
207
|
-
|
|
208
|
-
<Section title={`Steps (${steps.length})`} defaultExpanded>
|
|
170
|
+
{executionNodes.length > 0 && (
|
|
171
|
+
<Section title={`Execution (${executionNodes.length} ${executionNodes.length === 1 ? 'node' : 'nodes'})`} defaultExpanded>
|
|
209
172
|
<div className="space-y-1.5">
|
|
210
|
-
{
|
|
173
|
+
{stepRows.map(({ node: step, depth }) => {
|
|
211
174
|
const isFailed = step.phase === 'Failed' || step.phase === 'Error'
|
|
212
|
-
const isSkipped = step.
|
|
213
|
-
const isSuspend = step.
|
|
175
|
+
const isSkipped = step.type === 'Skipped' || step.phase === 'Skipped'
|
|
176
|
+
const isSuspend = step.type === 'Suspend'
|
|
214
177
|
return (
|
|
215
178
|
<div key={step.id} className={clsx(
|
|
216
179
|
'text-sm card-inner px-3 py-2',
|
|
217
|
-
isFailed && 'border-l-2 border-
|
|
218
|
-
)}>
|
|
180
|
+
isFailed && 'border-l-2 border-l-[var(--color-error)]'
|
|
181
|
+
)} style={{ marginLeft: `${Math.min(depth, 6) * 12}px` }}>
|
|
219
182
|
<div className="flex items-center gap-2">
|
|
220
|
-
<StepStatusIcon phase={step.phase} nodeType={step.
|
|
183
|
+
<StepStatusIcon phase={step.phase} nodeType={step.type} />
|
|
221
184
|
<span className={clsx(
|
|
222
185
|
'flex-1',
|
|
223
|
-
isSkipped ? 'text-theme-text-tertiary' : isSuspend ?
|
|
186
|
+
isSkipped ? 'text-theme-text-tertiary' : isSuspend ? SEVERITY_TEXT.warning : 'text-theme-text-primary'
|
|
224
187
|
)}>
|
|
225
|
-
{step.
|
|
188
|
+
{step.displayLabel}
|
|
226
189
|
{isSkipped && <span className="ml-1 text-xs text-theme-text-tertiary">(skipped)</span>}
|
|
227
|
-
{isSuspend && <span className=
|
|
190
|
+
{isSuspend && <span className={clsx('ml-1 text-xs opacity-70', SEVERITY_TEXT.warning)}>(suspend)</span>}
|
|
228
191
|
</span>
|
|
192
|
+
<span className="text-xs text-theme-text-tertiary">{step.displayType}</span>
|
|
229
193
|
<span className="text-xs text-theme-text-secondary">{getStepDuration(step) || '-'}</span>
|
|
230
194
|
</div>
|
|
231
195
|
{isFailed && step.message && (
|
|
232
|
-
<div className=
|
|
196
|
+
<div className={clsx('mt-1 ml-6 line-clamp-2 break-all text-xs', SEVERITY_TEXT.error)} title={step.message}>{step.message}</div>
|
|
197
|
+
)}
|
|
198
|
+
{step.templateRef && (
|
|
199
|
+
<div className="mt-1 ml-6 text-xs text-theme-text-tertiary">
|
|
200
|
+
Uses <TemplateRefLink refInfo={step.templateRef} onNavigate={onNavigate} />
|
|
201
|
+
</div>
|
|
233
202
|
)}
|
|
234
203
|
</div>
|
|
235
204
|
)
|
|
@@ -254,3 +223,16 @@ export function WorkflowRenderer({ data }: WorkflowRendererProps) {
|
|
|
254
223
|
</>
|
|
255
224
|
)
|
|
256
225
|
}
|
|
226
|
+
|
|
227
|
+
function TemplateRefLink({ refInfo, onNavigate }: { refInfo: WorkflowTemplateReference; onNavigate?: WorkflowRendererProps['onNavigate'] }) {
|
|
228
|
+
return (
|
|
229
|
+
<ResourceLink
|
|
230
|
+
name={refInfo.name}
|
|
231
|
+
kind={refInfo.resourceKind}
|
|
232
|
+
namespace={refInfo.namespace}
|
|
233
|
+
group="argoproj.io"
|
|
234
|
+
label={refInfo.clusterScope ? `${refInfo.name} (cluster)` : refInfo.name}
|
|
235
|
+
onNavigate={onNavigate}
|
|
236
|
+
/>
|
|
237
|
+
)
|
|
238
|
+
}
|
|
@@ -23,6 +23,7 @@ export * from './CNPGScheduledBackupRenderer'
|
|
|
23
23
|
export * from './ConfigAuditReportRenderer'
|
|
24
24
|
export * from './ConfigMapRenderer'
|
|
25
25
|
export * from './CronJobRenderer'
|
|
26
|
+
export * from './CronWorkflowRenderer'
|
|
26
27
|
export * from './eso-cells'
|
|
27
28
|
export * from './EventRenderer'
|
|
28
29
|
export * from './EndpointSliceRenderer'
|
|
@@ -300,6 +300,6 @@ export function getClusterProvider(resource: any): string {
|
|
|
300
300
|
/** Parse CAPI compound condition messages ("* Foo: bar * Baz: qux") into a structured list */
|
|
301
301
|
export function parseCAPIConditionMessage(message: string): string[] | null {
|
|
302
302
|
if (!message || !message.includes('*')) return null
|
|
303
|
-
const items = message.split(
|
|
303
|
+
const items = message.split('*').map(s => s.trim()).filter(Boolean)
|
|
304
304
|
return items.length > 1 ? items : null
|
|
305
305
|
}
|
|
@@ -1406,6 +1406,36 @@ export function getWorkflowTemplate(workflow: any): string | null {
|
|
|
1406
1406
|
return workflow.spec?.workflowTemplateRef?.name || null
|
|
1407
1407
|
}
|
|
1408
1408
|
|
|
1409
|
+
export function getCronWorkflowStatus(cwf: any): StatusBadge {
|
|
1410
|
+
const spec = cwf.spec || {}
|
|
1411
|
+
const status = cwf.status || {}
|
|
1412
|
+
const active = Array.isArray(status.active) ? status.active.length : 0
|
|
1413
|
+
if (spec.suspend === true) {
|
|
1414
|
+
return { text: 'Suspended', color: healthColors.degraded, level: 'degraded' }
|
|
1415
|
+
}
|
|
1416
|
+
if (active > 0) {
|
|
1417
|
+
return { text: `${active} active`, color: healthColors.neutral, level: 'neutral' }
|
|
1418
|
+
}
|
|
1419
|
+
if (status.lastScheduledTime) {
|
|
1420
|
+
return { text: 'Idle', color: healthColors.neutral, level: 'neutral' }
|
|
1421
|
+
}
|
|
1422
|
+
return { text: 'Never run', color: healthColors.unknown, level: 'unknown' }
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
export function getCronWorkflowSchedule(cwf: any): string {
|
|
1426
|
+
const schedules = cwf.spec?.schedules
|
|
1427
|
+
if (Array.isArray(schedules) && schedules.length > 0) return schedules.join(', ')
|
|
1428
|
+
return cwf.spec?.schedule || '-'
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
export function getCronWorkflowLastRun(cwf: any): string {
|
|
1432
|
+
return cwf.status?.lastScheduledTime ? formatAge(cwf.status.lastScheduledTime) : 'Never'
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
export function getCronWorkflowTemplate(cwf: any): string {
|
|
1436
|
+
return cwf.spec?.workflowSpec?.workflowTemplateRef?.name || cwf.spec?.workflowSpec?.entrypoint || '-'
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1409
1439
|
// ============================================================================
|
|
1410
1440
|
// CERT-MANAGER UTILITIES — re-exported from resource-utils-certmanager.ts
|
|
1411
1441
|
// ============================================================================
|
|
@@ -1866,6 +1896,7 @@ export function getCellFilterValue(resource: any, column: string, kind: string):
|
|
|
1866
1896
|
if (kindLower === 'persistentvolumes') return getPVStatus(resource).text
|
|
1867
1897
|
if (kindLower === 'rollouts') return getRolloutStatus(resource).text
|
|
1868
1898
|
if (kindLower === 'workflows') return getWorkflowStatus(resource).text
|
|
1899
|
+
if (kindLower === 'cronworkflows') return getCronWorkflowStatus(resource).text
|
|
1869
1900
|
if (kindLower === 'hpas' || kindLower === 'horizontalpodautoscalers') return getHPAStatus(resource).text
|
|
1870
1901
|
if (kindLower === 'gateways') return getGatewayStatus(resource).text
|
|
1871
1902
|
if (kindLower === 'gatewayclasses') return getGatewayClassStatus(resource).text
|
|
@@ -52,9 +52,9 @@ export function DetailShell<TId extends string = string>({
|
|
|
52
52
|
const visibleTabs = tabs.filter((t) => !t.hidden)
|
|
53
53
|
|
|
54
54
|
return (
|
|
55
|
-
<div className="flex flex-col h-full w-full bg-theme-
|
|
55
|
+
<div className="flex flex-col h-full w-full bg-theme-base">
|
|
56
56
|
{/* Header */}
|
|
57
|
-
<div className="shrink-0 border-b border-theme-border bg-theme-
|
|
57
|
+
<div className="shrink-0 border-b border-theme-border bg-theme-base">
|
|
58
58
|
{!compactHeader && (
|
|
59
59
|
<>
|
|
60
60
|
{breadcrumb && <div className="px-6 pt-2.5">{breadcrumb}</div>}
|