@skyhook-io/radar-app 1.8.6 → 1.8.7
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 +7 -1
- package/package.json +11 -9
- package/src/App.tsx +221 -181
- package/src/RadarApp.tsx +91 -20
- package/src/api/client.delta.test.ts +89 -0
- package/src/api/client.deltaSync.test.ts +216 -0
- package/src/api/client.ts +262 -34
- package/src/api/diagnose.ts +288 -0
- package/src/api/timelineSource.test.ts +217 -0
- package/src/api/timelineSource.ts +580 -0
- package/src/components/ConnectionErrorView.tsx +174 -70
- package/src/components/ContextSwitcher.tsx +13 -5
- package/src/components/applications/ApplicationsView.tsx +327 -26
- package/src/components/audit/AuditSettingsDialog.tsx +2 -2
- package/src/components/curl/ServiceCurlButton.tsx +2 -2
- package/src/components/diagnose/AISettings.tsx +121 -0
- package/src/components/diagnose/DiagnoseContext.tsx +491 -0
- package/src/components/diagnose/DiagnoseSurface.tsx +385 -0
- package/src/components/diagnose/Home.tsx +163 -0
- package/src/components/diagnose/InvestigationView.tsx +604 -0
- package/src/components/diagnose/LocalDiagnoseAction.tsx +150 -0
- package/src/components/diagnose/launch.ts +65 -0
- package/src/components/diagnose/parts.tsx +1689 -0
- package/src/components/dock/BottomDock.tsx +2 -3
- package/src/components/dock/WorkloadLogsTab.tsx +21 -5
- package/src/components/execution/BatchExecutionView.test.ts +170 -0
- package/src/components/execution/BatchExecutionView.tsx +1329 -0
- package/src/components/execution/batch-run-actions.test.ts +48 -0
- package/src/components/execution/batch-run-actions.ts +24 -0
- package/src/components/execution/batch-timeline.test.ts +57 -0
- package/src/components/execution/batch-timeline.ts +46 -0
- package/src/components/execution/execution-definition.test.ts +208 -0
- package/src/components/execution/execution-definition.ts +245 -0
- package/src/components/helm/ChartBrowser.tsx +2 -3
- package/src/components/helm/HelmReleaseDrawer.test.ts +17 -0
- package/src/components/helm/HelmReleaseDrawer.tsx +376 -43
- package/src/components/helm/HelmView.tsx +2 -3
- package/src/components/helm/InstallWizard.tsx +3 -5
- package/src/components/helm/TrackChartSourceDialog.tsx +48 -4
- package/src/components/helm/ValuesDiffPreview.tsx +15 -4
- package/src/components/home/HomeView.tsx +17 -15
- package/src/components/home/MCPSetupDialog.tsx +1 -1
- package/src/components/home/mcpToolCatalog.ts +2 -2
- package/src/components/issues/IssuesPane.tsx +9 -1
- package/src/components/logs/ScheduledWorkloadLogsViewer.tsx +135 -0
- package/src/components/portforward/PortForwardButton.tsx +2 -2
- package/src/components/portforward/PortForwardManager.tsx +19 -14
- package/src/components/resource/PrometheusChartsGrid.tsx +6 -80
- package/src/components/resource/RightsizingStrip.tsx +0 -3
- package/src/components/resources/ImageFilesystemModal.tsx +2 -2
- package/src/components/resources/PodFilesystemModal.tsx +2 -3
- package/src/components/resources/ResourceDetailDrawer.tsx +2 -0
- package/src/components/resources/ResourcesView.tsx +13 -4
- package/src/components/resources/renderers/CronWorkflowRenderer.tsx +1 -0
- package/src/components/resources/renderers/index.ts +1 -0
- package/src/components/settings/SettingsDialog.tsx +80 -23
- package/src/components/shared/LargeClusterNamespacePicker.tsx +2 -2
- package/src/components/timeline/LocalTimelineScrubber.tsx +212 -0
- package/src/components/timeline/RetainedTimelineScrubber.tsx +311 -0
- package/src/components/timeline/TimelineList.tsx +51 -10
- package/src/components/timeline/TimelineView.tsx +701 -15
- package/src/components/timeline/TimelineView.urlparams.test.ts +294 -0
- package/src/components/traffic/TrafficFilterSidebar.tsx +2 -2
- package/src/components/traffic/TrafficView.tsx +8 -4
- package/src/components/ui/CommandPalette.tsx +2 -2
- package/src/components/workload/WorkloadView.tsx +319 -35
- package/src/context/ConnectionContext.tsx +109 -11
- package/src/context/DiagnoseCustomization.tsx +42 -0
- package/src/context/TimelineSource.tsx +50 -0
- package/src/hooks/useClusterLoadState.ts +73 -0
- package/src/hooks/useEventSource.ts +6 -0
- package/src/index.css +157 -0
- package/src/index.ts +12 -0
- package/src/types/clusterLoadState.ts +33 -0
- package/src/utils/navigation.ts +10 -0
|
@@ -3,12 +3,40 @@ import { DialogPortal } from '@skyhook-io/k8s-ui/components/ui/DialogPortal'
|
|
|
3
3
|
import { X, Plus, Trash2, Link2, AlertTriangle } from 'lucide-react'
|
|
4
4
|
import { clsx } from 'clsx'
|
|
5
5
|
import { useHelmOCISources, useAddOCISource, useRemoveOCISource, useClusterInfo } from '../../api/client'
|
|
6
|
+
import { Input } from '@skyhook-io/k8s-ui'
|
|
7
|
+
import type { UpgradeInfo } from '../../types'
|
|
6
8
|
|
|
7
9
|
interface TrackChartSourceDialogProps {
|
|
8
10
|
open: boolean
|
|
9
11
|
onClose: () => void
|
|
10
12
|
/** Chart name of the release this was opened from, for the example prompt. */
|
|
11
13
|
chartName?: string
|
|
14
|
+
sourceIssue?: UpgradeInfo['sourceIssue']
|
|
15
|
+
sourceError?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getSourceIssueCopy(sourceIssue: UpgradeInfo['sourceIssue'], sourceError?: string) {
|
|
19
|
+
switch (sourceIssue) {
|
|
20
|
+
case 'repo_index_error':
|
|
21
|
+
return {
|
|
22
|
+
title: 'A Helm repo index failed',
|
|
23
|
+
body: 'Fix, refresh, or remove the broken Helm repo index. If this release came from OCI, add its registry prefix and Radar will check OCI before reporting that repo error.',
|
|
24
|
+
}
|
|
25
|
+
case 'ambiguous_repository':
|
|
26
|
+
return {
|
|
27
|
+
title: 'Multiple Helm repos match',
|
|
28
|
+
body: 'Radar will not guess between classic repos. Adding an OCI prefix will not enable direct upgrades while those repos remain ambiguous.',
|
|
29
|
+
}
|
|
30
|
+
case 'untracked':
|
|
31
|
+
return {
|
|
32
|
+
title: 'Source not tracked',
|
|
33
|
+
body: 'Helm does not record the install source. Add the OCI prefix that contains this chart.',
|
|
34
|
+
}
|
|
35
|
+
default:
|
|
36
|
+
return sourceError
|
|
37
|
+
? { title: 'Source unresolved', body: sourceError }
|
|
38
|
+
: undefined
|
|
39
|
+
}
|
|
12
40
|
}
|
|
13
41
|
|
|
14
42
|
// TrackChartSourceDialog lets the user register an OCI chart-source prefix — the
|
|
@@ -16,7 +44,7 @@ interface TrackChartSourceDialogProps {
|
|
|
16
44
|
// installed from, so for charts published to an OCI registry (and not managed by
|
|
17
45
|
// GitOps) Radar can only track upgrades once the user declares where they live.
|
|
18
46
|
// Registering a registry/org prefix lets Radar probe "<prefix>/<chartName>".
|
|
19
|
-
export function TrackChartSourceDialog({ open, onClose, chartName }: TrackChartSourceDialogProps) {
|
|
47
|
+
export function TrackChartSourceDialog({ open, onClose, chartName, sourceIssue, sourceError }: TrackChartSourceDialogProps) {
|
|
20
48
|
const [value, setValue] = useState('')
|
|
21
49
|
const { data: sources } = useHelmOCISources()
|
|
22
50
|
const { data: clusterInfo } = useClusterInfo()
|
|
@@ -30,6 +58,11 @@ export function TrackChartSourceDialog({ open, onClose, chartName }: TrackChartS
|
|
|
30
58
|
|
|
31
59
|
const trimmed = value.trim()
|
|
32
60
|
const invalid = trimmed !== '' && !trimmed.startsWith('oci://')
|
|
61
|
+
const normalizedInput = trimmed.replace(/\/+$/, '')
|
|
62
|
+
const normalizedChartName = chartName?.trim().replace(/^\/+|\/+$/g, '') ?? ''
|
|
63
|
+
const probedRef = normalizedInput && normalizedChartName ? `${normalizedInput}/${normalizedChartName}` : ''
|
|
64
|
+
const looksLikeFullChartRef = Boolean(normalizedInput && normalizedChartName && normalizedInput.endsWith(`/${normalizedChartName}`))
|
|
65
|
+
const sourceIssueCopy = getSourceIssueCopy(sourceIssue, sourceError)
|
|
33
66
|
|
|
34
67
|
const handleAdd = () => {
|
|
35
68
|
if (!trimmed || invalid) return
|
|
@@ -58,13 +91,22 @@ export function TrackChartSourceDialog({ open, onClose, chartName }: TrackChartS
|
|
|
58
91
|
</div>
|
|
59
92
|
|
|
60
93
|
<div className="p-4 space-y-4">
|
|
94
|
+
{sourceIssueCopy && (
|
|
95
|
+
<div className="flex items-start gap-2 rounded-lg border border-theme-border bg-theme-elevated px-3 py-2 text-sm">
|
|
96
|
+
<AlertTriangle className="w-4 h-4 shrink-0 mt-0.5 text-theme-text-secondary" />
|
|
97
|
+
<div>
|
|
98
|
+
<p className="font-medium text-theme-text-primary">{sourceIssueCopy.title}</p>
|
|
99
|
+
<p className="text-theme-text-secondary">{sourceIssueCopy.body}</p>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
)}
|
|
103
|
+
|
|
61
104
|
<div>
|
|
62
105
|
<label className="block text-sm font-medium text-theme-text-secondary mb-2">
|
|
63
106
|
OCI registry prefix
|
|
64
107
|
</label>
|
|
65
108
|
<div className="flex gap-2">
|
|
66
|
-
<
|
|
67
|
-
type="text"
|
|
109
|
+
<Input
|
|
68
110
|
value={value}
|
|
69
111
|
onChange={(e) => setValue(e.target.value)}
|
|
70
112
|
onKeyDown={(e) => e.key === 'Enter' && handleAdd()}
|
|
@@ -87,8 +129,10 @@ export function TrackChartSourceDialog({ open, onClose, chartName }: TrackChartS
|
|
|
87
129
|
<p className="mt-1 text-xs text-theme-text-tertiary">
|
|
88
130
|
{invalid
|
|
89
131
|
? 'Must be an oci:// reference.'
|
|
132
|
+
: looksLikeFullChartRef
|
|
133
|
+
? `This looks like a full chart ref. Radar would probe "${probedRef}"; remove the final "/${normalizedChartName}" if "${normalizedChartName}" is the chart name.`
|
|
90
134
|
: chartName
|
|
91
|
-
? `Radar will
|
|
135
|
+
? `Radar will probe "${probedRef || `<prefix>/${normalizedChartName}`}".`
|
|
92
136
|
: 'Radar probes <prefix>/<chartName> for each untracked release.'}
|
|
93
137
|
</p>
|
|
94
138
|
</div>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useEffect, useRef } from 'react'
|
|
2
|
+
import { createPortal } from 'react-dom'
|
|
2
3
|
import { X, Play, Loader2, FileText, AlertTriangle } from 'lucide-react'
|
|
3
4
|
import { clsx } from 'clsx'
|
|
4
5
|
import type { ValuesPreviewResponse } from '../../types'
|
|
@@ -8,6 +9,8 @@ interface ValuesDiffPreviewProps {
|
|
|
8
9
|
onClose: () => void
|
|
9
10
|
onApply: () => void
|
|
10
11
|
isApplying: boolean
|
|
12
|
+
title?: string
|
|
13
|
+
applyLabel?: string
|
|
11
14
|
}
|
|
12
15
|
|
|
13
16
|
export function ValuesDiffPreview({
|
|
@@ -15,6 +18,8 @@ export function ValuesDiffPreview({
|
|
|
15
18
|
onClose,
|
|
16
19
|
onApply,
|
|
17
20
|
isApplying,
|
|
21
|
+
title = 'Preview Changes',
|
|
22
|
+
applyLabel = 'Apply Changes',
|
|
18
23
|
}: ValuesDiffPreviewProps) {
|
|
19
24
|
const dialogRef = useRef<HTMLDivElement>(null)
|
|
20
25
|
|
|
@@ -40,7 +45,10 @@ export function ValuesDiffPreview({
|
|
|
40
45
|
const hasChanges = previewData.manifestDiff.trim().length > 0 &&
|
|
41
46
|
previewData.manifestDiff.split('\n').some(line => line.startsWith('+') || line.startsWith('-'))
|
|
42
47
|
|
|
43
|
-
|
|
48
|
+
// Portal to body so the modal escapes any CSS-transformed ancestor (the release
|
|
49
|
+
// drawer, the upgrade ConfirmDialog) and layers on top rather than being trapped
|
|
50
|
+
// in a lower stacking context.
|
|
51
|
+
return createPortal(
|
|
44
52
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
|
45
53
|
{/* Backdrop */}
|
|
46
54
|
<div
|
|
@@ -51,6 +59,8 @@ export function ValuesDiffPreview({
|
|
|
51
59
|
{/* Dialog */}
|
|
52
60
|
<div
|
|
53
61
|
ref={dialogRef}
|
|
62
|
+
role="dialog"
|
|
63
|
+
aria-modal="true"
|
|
54
64
|
tabIndex={-1}
|
|
55
65
|
className="relative dialog max-w-4xl w-full mx-4 max-h-[85vh] flex flex-col outline-none"
|
|
56
66
|
>
|
|
@@ -58,7 +68,7 @@ export function ValuesDiffPreview({
|
|
|
58
68
|
<div className="flex items-center justify-between px-4 py-3 border-b border-theme-border shrink-0">
|
|
59
69
|
<div className="flex items-center gap-2">
|
|
60
70
|
<FileText className="w-5 h-5 text-blue-400" />
|
|
61
|
-
<h3 className="text-lg font-semibold text-theme-text-primary">
|
|
71
|
+
<h3 className="text-lg font-semibold text-theme-text-primary">{title}</h3>
|
|
62
72
|
</div>
|
|
63
73
|
<button
|
|
64
74
|
onClick={onClose}
|
|
@@ -120,12 +130,13 @@ export function ValuesDiffPreview({
|
|
|
120
130
|
) : (
|
|
121
131
|
<Play className="w-4 h-4" />
|
|
122
132
|
)}
|
|
123
|
-
|
|
133
|
+
{applyLabel}
|
|
124
134
|
</button>
|
|
125
135
|
</div>
|
|
126
136
|
</div>
|
|
127
137
|
</div>
|
|
128
|
-
</div
|
|
138
|
+
</div>,
|
|
139
|
+
document.body
|
|
129
140
|
)
|
|
130
141
|
}
|
|
131
142
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useMemo, type ReactNode } from 'react'
|
|
2
2
|
import { useDashboard, useDashboardCRDs, useDashboardHelm, useIssues, type IssuesResponse } from '../../api/client'
|
|
3
3
|
import { useConnection } from '../../context/ConnectionContext'
|
|
4
|
+
import type { ClusterLoadState } from '../../types/clusterLoadState'
|
|
4
5
|
import type { ExtendedMainView, Topology, SelectedResource } from '../../types'
|
|
5
6
|
import { TopologyPreview } from './TopologyPreview'
|
|
6
7
|
import { HelmSummary } from './HelmSummary'
|
|
@@ -10,6 +11,7 @@ import { CertificateHealthCard } from './CertificateHealthCard'
|
|
|
10
11
|
import { NetworkPolicyCoverageCard } from './NetworkPolicyCoverageCard'
|
|
11
12
|
import { CostCard } from './CostCard'
|
|
12
13
|
import { GitOpsControllersCard } from './GitOpsControllersCard'
|
|
14
|
+
import { Tooltip } from '../ui/Tooltip'
|
|
13
15
|
import {
|
|
14
16
|
AuditCard,
|
|
15
17
|
FreshnessControl,
|
|
@@ -17,6 +19,7 @@ import {
|
|
|
17
19
|
StatusDot,
|
|
18
20
|
categoryLabel,
|
|
19
21
|
groupLabel,
|
|
22
|
+
issueTiming,
|
|
20
23
|
subjectRef,
|
|
21
24
|
type Issue,
|
|
22
25
|
} from '@skyhook-io/k8s-ui'
|
|
@@ -31,6 +34,7 @@ interface HomeViewProps {
|
|
|
31
34
|
onNavigateToView: (view: ExtendedMainView, params?: Record<string, string>) => void
|
|
32
35
|
onNavigateToResourceKind: (kind: string, group?: string, filters?: Record<string, string[]>) => void
|
|
33
36
|
onNavigateToResource: (resource: SelectedResource) => void
|
|
37
|
+
fallbackClusterLoadState?: ClusterLoadState
|
|
34
38
|
/**
|
|
35
39
|
* Optional override for the Certificate Health card's click. When an embedded
|
|
36
40
|
* host (Radar Cloud) takes Certs over with its own fleet page, it passes this
|
|
@@ -40,7 +44,7 @@ interface HomeViewProps {
|
|
|
40
44
|
onNavigateToCerts?: () => void
|
|
41
45
|
}
|
|
42
46
|
|
|
43
|
-
export function HomeView({ namespaces, topology, onNavigateToView, onNavigateToResourceKind, onNavigateToResource, onNavigateToCerts }: HomeViewProps) {
|
|
47
|
+
export function HomeView({ namespaces, topology, fallbackClusterLoadState, onNavigateToView, onNavigateToResourceKind, onNavigateToResource, onNavigateToCerts }: HomeViewProps) {
|
|
44
48
|
const { data, isLoading, error, dataUpdatedAt, refetch } = useDashboard(namespaces)
|
|
45
49
|
const { connection } = useConnection()
|
|
46
50
|
const { data: issuesData, isLoading: issuesLoading, isFetching: issuesFetching, error: issuesError } = useIssues(namespaces)
|
|
@@ -67,7 +71,7 @@ export function HomeView({ namespaces, topology, onNavigateToView, onNavigateToR
|
|
|
67
71
|
const { data: helmData } = useDashboardHelm(namespaces)
|
|
68
72
|
|
|
69
73
|
if (isLoading) {
|
|
70
|
-
return <PaneLoader label="Loading dashboard…" className="flex-1" />
|
|
74
|
+
return <PaneLoader label="Loading dashboard…" className="flex-1 min-h-0" />
|
|
71
75
|
}
|
|
72
76
|
|
|
73
77
|
if (error || !data) {
|
|
@@ -94,19 +98,13 @@ export function HomeView({ namespaces, topology, onNavigateToView, onNavigateToR
|
|
|
94
98
|
)
|
|
95
99
|
}
|
|
96
100
|
|
|
97
|
-
const stillLoading = data.deferredLoading || (data.partialData && data.partialData.length > 0)
|
|
98
|
-
|
|
99
101
|
return (
|
|
100
102
|
<div className="flex-1 overflow-y-auto">
|
|
101
103
|
<div className="max-w-[1600px] mx-auto px-6 py-6 space-y-6">
|
|
102
|
-
{
|
|
103
|
-
<div className="flex items-center gap-2 text-
|
|
104
|
-
<Loader2 className="
|
|
105
|
-
<span>
|
|
106
|
-
{data.partialData && data.partialData.length > 0
|
|
107
|
-
? `Still loading: ${data.partialData.join(', ')}`
|
|
108
|
-
: 'Loading remaining resources…'}
|
|
109
|
-
</span>
|
|
104
|
+
{fallbackClusterLoadState?.loading && (
|
|
105
|
+
<div className="flex items-center gap-2 text-sm text-theme-text-tertiary">
|
|
106
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
107
|
+
<span>{fallbackClusterLoadState.message}</span>
|
|
110
108
|
</div>
|
|
111
109
|
)}
|
|
112
110
|
{/* Row 1: Cluster Health Card (combined health + resource counts) */}
|
|
@@ -360,7 +358,7 @@ function ProblemsPanel({
|
|
|
360
358
|
{issues.map((issue) => {
|
|
361
359
|
const ref = subjectRef(issue)
|
|
362
360
|
const age = issue.first_seen ? formatCompactAge(issue.first_seen) : ''
|
|
363
|
-
const
|
|
361
|
+
const timing = issueTiming(issue)
|
|
364
362
|
|
|
365
363
|
return (
|
|
366
364
|
<button
|
|
@@ -378,10 +376,14 @@ function ProblemsPanel({
|
|
|
378
376
|
<div className="flex items-center gap-1.5">
|
|
379
377
|
<span className="text-[10px] text-theme-text-tertiary bg-theme-elevated px-1 py-0.5 rounded">{issue.kind}</span>
|
|
380
378
|
<span className="text-xs text-theme-text-primary truncate font-medium">{issue.name}</span>
|
|
381
|
-
{(age ||
|
|
379
|
+
{(age || timing) && (
|
|
382
380
|
<span className="ml-auto flex shrink-0 items-center gap-1">
|
|
383
381
|
{age && <span className="text-[10px] text-theme-text-tertiary tabular-nums">{age}</span>}
|
|
384
|
-
{
|
|
382
|
+
{timing && (
|
|
383
|
+
<Tooltip content={timing.tooltip} delay={100}>
|
|
384
|
+
<span className="badge-sm text-[10px] text-theme-text-secondary">{timing.chip}</span>
|
|
385
|
+
</Tooltip>
|
|
386
|
+
)}
|
|
385
387
|
</span>
|
|
386
388
|
)}
|
|
387
389
|
</div>
|
|
@@ -204,7 +204,7 @@ export function MCPSetupDialog({ open, onClose, mcpUrl }: MCPSetupDialogProps) {
|
|
|
204
204
|
<a href="https://modelcontextprotocol.io" target="_blank" rel="noopener noreferrer" className="text-purple-400 hover:text-purple-300 underline underline-offset-2">
|
|
205
205
|
Model Context Protocol
|
|
206
206
|
</a>{' '}
|
|
207
|
-
(MCP) server that lets AI
|
|
207
|
+
(MCP) server that lets AI agents query your cluster through Radar.
|
|
208
208
|
Unlike raw kubectl access, Radar gives your AI pre-processed, enriched data —
|
|
209
209
|
topology graphs, health assessments, deduplicated events, filtered logs — so it
|
|
210
210
|
can understand your cluster state quickly without burning through context on
|
|
@@ -237,9 +237,9 @@ export const MCP_TOOL_CATALOG: MCPToolInfo[] = [
|
|
|
237
237
|
},
|
|
238
238
|
{
|
|
239
239
|
name: 'get_workload_logs',
|
|
240
|
-
desc: 'Aggregated, filtered logs across all pods of a workload (Deployment, StatefulSet, or
|
|
240
|
+
desc: 'Aggregated, filtered logs across all pods of a workload (Deployment, StatefulSet, DaemonSet, Job, or Argo Workflow) — collected concurrently, filtered for errors/warnings, and deduplicated.',
|
|
241
241
|
params: [
|
|
242
|
-
{ arg: 'kind', desc: 'deployment (default), statefulset, or
|
|
242
|
+
{ arg: 'kind', desc: 'deployment (default), statefulset, daemonset, job, or workflow' },
|
|
243
243
|
{ arg: 'namespace', required: true, desc: 'workload namespace' },
|
|
244
244
|
{ arg: 'name', required: true, desc: 'workload name' },
|
|
245
245
|
{ arg: 'container', desc: 'specific container (defaults to all)' },
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
type SummaryTone,
|
|
16
16
|
} from '@skyhook-io/k8s-ui'
|
|
17
17
|
import { AlertTriangle } from 'lucide-react'
|
|
18
|
+
import { IssueDiagnoseButton } from '../diagnose/LocalDiagnoseAction'
|
|
18
19
|
|
|
19
20
|
const SEVERITY_TONE: Record<IssueSeverity, SummaryTone> = { critical: 'error', warning: 'warning' }
|
|
20
21
|
|
|
@@ -136,7 +137,14 @@ export function IssuesPane({ namespaces, onNavigateToResource }: IssuesPaneProps
|
|
|
136
137
|
) : (
|
|
137
138
|
/* anyData = the query resolved, i.e. the cluster is reachable; an empty
|
|
138
139
|
list then means "nothing broken" rather than "not connected". */
|
|
139
|
-
<IssuesView
|
|
140
|
+
<IssuesView
|
|
141
|
+
issues={shown}
|
|
142
|
+
anyData={!!data}
|
|
143
|
+
onResourceClick={onResourceClick}
|
|
144
|
+
renderActions={({ issue }) => (
|
|
145
|
+
<IssueDiagnoseButton kind={issue.kind} namespace={issue.namespace ?? ''} name={issue.name} />
|
|
146
|
+
)}
|
|
147
|
+
/>
|
|
140
148
|
)}
|
|
141
149
|
</div>
|
|
142
150
|
)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
2
|
+
import { Loader2, Terminal } from 'lucide-react'
|
|
3
|
+
import { clsx } from 'clsx'
|
|
4
|
+
import { useWorkloadRuns, type WorkloadRun } from '../../api/client'
|
|
5
|
+
import { WorkloadLogsViewer } from './WorkloadLogsViewer'
|
|
6
|
+
import { pickDefaultRun, workloadRunKey } from '../execution/BatchExecutionView'
|
|
7
|
+
|
|
8
|
+
interface ScheduledWorkloadLogsViewerProps {
|
|
9
|
+
kind: string
|
|
10
|
+
namespace: string
|
|
11
|
+
name: string
|
|
12
|
+
selectedRunKey?: string
|
|
13
|
+
onSelectRun?: (runKey: string) => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const EMPTY_RUNS: WorkloadRun[] = []
|
|
17
|
+
|
|
18
|
+
export function ScheduledWorkloadLogsViewer({ kind, namespace, name, selectedRunKey, onSelectRun }: ScheduledWorkloadLogsViewerProps) {
|
|
19
|
+
const clusterScoped = kind === 'ClusterWorkflowTemplate' || kind === 'clusterworkflowtemplates'
|
|
20
|
+
const runsQuery = useWorkloadRuns(kind, namespace, name, true, { clusterScoped, refetchActive: true })
|
|
21
|
+
const runs = runsQuery.data?.runs ?? EMPTY_RUNS
|
|
22
|
+
const defaultRun = useMemo(() => pickDefaultRun(runs), [runs])
|
|
23
|
+
const [localRunKey, setLocalRunKey] = useState('')
|
|
24
|
+
const effectiveRunKey = selectedRunKey ?? localRunKey
|
|
25
|
+
const selectRun = onSelectRun ?? setLocalRunKey
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (!runsQuery.data) return
|
|
29
|
+
if (runs.length === 0) {
|
|
30
|
+
if (effectiveRunKey) selectRun('')
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
if (!runs.some(run => workloadRunKey(run) === effectiveRunKey)) {
|
|
34
|
+
selectRun(workloadRunKey(defaultRun ?? runs[0]))
|
|
35
|
+
}
|
|
36
|
+
}, [runsQuery.data, runs, effectiveRunKey, defaultRun, selectRun])
|
|
37
|
+
|
|
38
|
+
const selectedRun = runs.find(run => workloadRunKey(run) === effectiveRunKey) ?? defaultRun
|
|
39
|
+
|
|
40
|
+
if (runsQuery.isLoading) {
|
|
41
|
+
return (
|
|
42
|
+
<div className="flex h-full items-center justify-center text-theme-text-tertiary">
|
|
43
|
+
<div className="flex items-center gap-2">
|
|
44
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
45
|
+
<span>Loading runs...</span>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (runsQuery.error) {
|
|
52
|
+
return (
|
|
53
|
+
<div className="flex h-full flex-col items-center justify-center gap-2 text-theme-text-tertiary">
|
|
54
|
+
<Terminal className="h-8 w-8" />
|
|
55
|
+
<span>{runsQuery.error instanceof Error ? runsQuery.error.message : 'Failed to load runs'}</span>
|
|
56
|
+
</div>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!selectedRun) {
|
|
61
|
+
return (
|
|
62
|
+
<div className="flex h-full flex-col items-center justify-center gap-2 text-theme-text-tertiary">
|
|
63
|
+
<Terminal className="h-8 w-8" />
|
|
64
|
+
<span>No retained runs found</span>
|
|
65
|
+
</div>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div className="flex h-full min-h-0 flex-col">
|
|
71
|
+
<div className="shrink-0 border-b border-theme-border bg-theme-surface px-3 py-2">
|
|
72
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
73
|
+
<span className="text-xs font-medium text-theme-text-secondary">Run</span>
|
|
74
|
+
<select
|
|
75
|
+
value={workloadRunKey(selectedRun)}
|
|
76
|
+
onChange={(event) => selectRun(event.target.value)}
|
|
77
|
+
className="min-w-0 max-w-full rounded-md border border-theme-border bg-theme-elevated px-2 py-1 text-sm text-theme-text-primary"
|
|
78
|
+
>
|
|
79
|
+
{runs.map(run => (
|
|
80
|
+
<option key={workloadRunKey(run)} value={workloadRunKey(run)}>
|
|
81
|
+
{formatRunOption(run, clusterScoped)}
|
|
82
|
+
</option>
|
|
83
|
+
))}
|
|
84
|
+
</select>
|
|
85
|
+
<span className={clsx('badge-sm', phaseBadgeClass(selectedRun.phase))}>
|
|
86
|
+
{selectedRun.phase}
|
|
87
|
+
</span>
|
|
88
|
+
<span className="text-xs text-theme-text-tertiary">
|
|
89
|
+
{formatRunTime(selectedRun)}
|
|
90
|
+
</span>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
<div className="min-h-0 flex-1">
|
|
94
|
+
<WorkloadLogsViewer
|
|
95
|
+
key={`${selectedRun.kind}/${selectedRun.namespace}/${selectedRun.name}`}
|
|
96
|
+
kind={selectedRun.kind}
|
|
97
|
+
namespace={selectedRun.namespace}
|
|
98
|
+
name={selectedRun.name}
|
|
99
|
+
autoStream={selectedRun.active}
|
|
100
|
+
/>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function phaseBadgeClass(phase: string): string {
|
|
107
|
+
switch (phase) {
|
|
108
|
+
case 'Succeeded':
|
|
109
|
+
return 'status-healthy'
|
|
110
|
+
case 'Running':
|
|
111
|
+
return 'status-neutral'
|
|
112
|
+
case 'Failed':
|
|
113
|
+
case 'Error':
|
|
114
|
+
return 'status-unhealthy'
|
|
115
|
+
case 'Pending':
|
|
116
|
+
return 'status-degraded'
|
|
117
|
+
default:
|
|
118
|
+
return 'status-unknown'
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function formatRunTime(run: WorkloadRun): string {
|
|
123
|
+
const raw = run.startedAt || run.scheduledAt || run.finishedAt
|
|
124
|
+
if (!raw) return ''
|
|
125
|
+
return new Date(raw).toLocaleString()
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function formatRunOption(run: WorkloadRun, showNamespace: boolean): string {
|
|
129
|
+
const bits = [showNamespace ? `${run.namespace}/${run.name}` : run.name, run.phase]
|
|
130
|
+
if (run.progress) bits.push(run.progress)
|
|
131
|
+
else if (run.desired) bits.push(`${run.succeeded ?? 0}/${run.desired}`)
|
|
132
|
+
const work = run.podTotal ? `${run.podSucceeded ?? 0}/${run.podTotal} pods` : ''
|
|
133
|
+
if (work) bits.push(work)
|
|
134
|
+
return bits.join(' · ')
|
|
135
|
+
}
|
|
@@ -7,6 +7,7 @@ import { useStartPortForward } from './PortForwardManager'
|
|
|
7
7
|
import { useIsLocalDeployment } from '../../contexts/CapabilitiesContext'
|
|
8
8
|
import { validatePort } from '@skyhook-io/k8s-ui/utils/validators'
|
|
9
9
|
import { Tooltip } from '../ui/Tooltip'
|
|
10
|
+
import { Input } from '@skyhook-io/k8s-ui'
|
|
10
11
|
|
|
11
12
|
interface PortForwardButtonProps {
|
|
12
13
|
type: 'pod' | 'service'
|
|
@@ -123,9 +124,8 @@ function KubectlCommandDialog({
|
|
|
123
124
|
<div className="flex flex-col gap-1">
|
|
124
125
|
<div className="flex items-center gap-2 text-sm text-theme-text-secondary">
|
|
125
126
|
<label htmlFor="local-port">Local port:</label>
|
|
126
|
-
<
|
|
127
|
+
<Input
|
|
127
128
|
id="local-port"
|
|
128
|
-
type="text"
|
|
129
129
|
inputMode="numeric"
|
|
130
130
|
value={portInput}
|
|
131
131
|
onChange={(e) => setPortInput(e.target.value)}
|
|
@@ -644,7 +644,7 @@ export function PortForwardPanel() {
|
|
|
644
644
|
onMouseEnter={onPanelHoverEnter}
|
|
645
645
|
onMouseLeave={onPanelHoverLeave}
|
|
646
646
|
className={clsx(
|
|
647
|
-
'fixed z-[51] w-
|
|
647
|
+
'fixed z-[51] w-[26rem] max-w-[calc(100vw-2rem)]',
|
|
648
648
|
'transition-opacity duration-150 ease-out',
|
|
649
649
|
isPanelOpen
|
|
650
650
|
? 'opacity-100 pointer-events-auto'
|
|
@@ -726,14 +726,16 @@ export function PortForwardPanel() {
|
|
|
726
726
|
</div>
|
|
727
727
|
) : (
|
|
728
728
|
<div className="divide-y divide-theme-border">
|
|
729
|
-
{activeSessions.map((session) =>
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
729
|
+
{activeSessions.map((session) => {
|
|
730
|
+
const resourceName = session.serviceName || session.podName
|
|
731
|
+
return (
|
|
732
|
+
<div
|
|
733
|
+
key={session.id}
|
|
734
|
+
className={clsx(
|
|
735
|
+
'p-3 space-y-1',
|
|
736
|
+
session.status === 'error' ? 'bg-red-500/10' : 'hover:bg-theme-elevated'
|
|
737
|
+
)}
|
|
738
|
+
>
|
|
737
739
|
{/* Row 1: status dot + name | stop button */}
|
|
738
740
|
<div className="flex items-start justify-between gap-2">
|
|
739
741
|
<div className="flex items-start gap-2 min-w-0 flex-1">
|
|
@@ -743,9 +745,11 @@ export function PortForwardPanel() {
|
|
|
743
745
|
session.status === 'running' ? 'bg-green-500' : 'bg-red-500'
|
|
744
746
|
)}
|
|
745
747
|
/>
|
|
746
|
-
<
|
|
747
|
-
|
|
748
|
-
|
|
748
|
+
<Tooltip content={resourceName} delay={300} position="bottom" disabled={!isPanelOpen} wrapperClassName="min-w-0 flex-1">
|
|
749
|
+
<span className="block min-w-0 truncate text-sm font-medium text-theme-text-primary">
|
|
750
|
+
{resourceName}
|
|
751
|
+
</span>
|
|
752
|
+
</Tooltip>
|
|
749
753
|
{session.status === 'error' && (
|
|
750
754
|
<span className={clsx('badge-sm shrink-0', SEVERITY_BADGE.error)}>Failed</span>
|
|
751
755
|
)}
|
|
@@ -916,8 +920,9 @@ export function PortForwardPanel() {
|
|
|
916
920
|
</div>
|
|
917
921
|
</div>
|
|
918
922
|
)}
|
|
919
|
-
|
|
920
|
-
|
|
923
|
+
</div>
|
|
924
|
+
)
|
|
925
|
+
})}
|
|
921
926
|
</div>
|
|
922
927
|
)}
|
|
923
928
|
</div>
|