@skyhook-io/radar-app 1.3.2 → 1.3.3
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 +1 -1
- package/src/App.tsx +111 -58
- package/src/api/client.ts +29 -1
- package/src/components/ConnectionErrorView.tsx +2 -2
- package/src/components/helm/ChartBrowser.tsx +7 -3
- package/src/components/helm/InstallWizard.tsx +1 -1
- package/src/components/helm/RoleGatedPanel.tsx +2 -2
- package/src/components/home/ClusterHealthCard.tsx +1 -1
- package/src/components/home/HomeView.tsx +14 -3
- package/src/components/home/MCPSetupDialog.tsx +4 -4
- package/src/components/issues/IssuesPane.tsx +78 -0
- package/src/components/portforward/PortForwardButton.tsx +1 -1
- package/src/components/portforward/PortForwardManager.tsx +1 -1
- package/src/components/resource/PrometheusCharts.tsx +18 -159
- package/src/components/resources/ImageFilesystemModal.tsx +1 -2
- package/src/components/resources/renderers/WorkloadRenderer.tsx +6 -2
- package/src/components/settings/MyPermissionsDialog.tsx +1 -1
- package/src/components/settings/SettingsDialog.tsx +22 -2
- package/src/components/timeline/TimelineSwimlanes.tsx +8 -1311
- package/src/components/ui/Markdown.tsx +1 -1
- package/src/components/ui/UpdateNotification.tsx +1 -1
- package/src/components/workload/WorkloadView.tsx +188 -6
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import { useState, useMemo } from 'react'
|
|
2
|
-
import { clsx } from 'clsx'
|
|
3
|
-
import { BarChart3, Wifi, WifiOff, Loader2 } from 'lucide-react'
|
|
4
2
|
import {
|
|
5
|
-
|
|
3
|
+
PrometheusChartsView,
|
|
6
4
|
MetricsSummary as BaseMetricsSummary,
|
|
7
|
-
SeriesLegend,
|
|
8
5
|
type TimeSeries,
|
|
9
6
|
type ReferenceLine,
|
|
10
7
|
} from '@skyhook-io/k8s-ui/components/charts'
|
|
@@ -95,7 +92,6 @@ export function PrometheusCharts({ kind, namespace, name, showEmptyState = false
|
|
|
95
92
|
const { data: status, isLoading: statusLoading } = usePrometheusStatus()
|
|
96
93
|
const connectMutation = usePrometheusConnect()
|
|
97
94
|
|
|
98
|
-
const categories = kind === 'Node' ? NODE_CATEGORIES : WORKLOAD_CATEGORIES
|
|
99
95
|
const [activeCategory, setActiveCategory] = useState<PrometheusMetricCategory>('cpu')
|
|
100
96
|
const [timeRange, setTimeRange] = useState<PrometheusTimeRange>('1h')
|
|
101
97
|
|
|
@@ -116,161 +112,24 @@ export function PrometheusCharts({ kind, namespace, name, showEmptyState = false
|
|
|
116
112
|
return computeRequestLimitLines(resource, kind, activeCategory)
|
|
117
113
|
}, [resource, kind, activeCategory])
|
|
118
114
|
|
|
119
|
-
if (!isSupported) {
|
|
120
|
-
return null
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Loading state — checking Prometheus availability (only show when explicitly requested)
|
|
124
|
-
if (statusLoading) {
|
|
125
|
-
if (!showEmptyState) return null
|
|
126
|
-
return (
|
|
127
|
-
<div className="flex items-center justify-center py-12 text-theme-text-tertiary">
|
|
128
|
-
<Loader2 className="w-5 h-5 animate-spin mr-2" />
|
|
129
|
-
Checking Prometheus availability...
|
|
130
|
-
</div>
|
|
131
|
-
)
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// When embedded in Overview (showEmptyState=false), hide when not connected or no data
|
|
135
|
-
if (!showEmptyState) {
|
|
136
|
-
if (!isConnected) return null
|
|
137
|
-
if (!metricsLoading && !metricsError && !metrics?.result?.series?.length) return null
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (!isConnected) {
|
|
141
|
-
return (
|
|
142
|
-
<div className="flex flex-col items-center justify-center py-12 gap-4">
|
|
143
|
-
<WifiOff className="w-10 h-10 text-theme-text-quaternary" />
|
|
144
|
-
<div className="text-center">
|
|
145
|
-
<p className="text-sm text-theme-text-secondary mb-1">Prometheus not connected</p>
|
|
146
|
-
<p className="text-xs text-theme-text-tertiary mb-4">
|
|
147
|
-
{status?.error || 'Connect to view historical CPU, memory, and network metrics'}
|
|
148
|
-
</p>
|
|
149
|
-
<button
|
|
150
|
-
onClick={() => connectMutation.mutate()}
|
|
151
|
-
disabled={connectMutation.isPending}
|
|
152
|
-
className="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg btn-brand"
|
|
153
|
-
>
|
|
154
|
-
{connectMutation.isPending ? (
|
|
155
|
-
<Loader2 className="w-4 h-4 animate-spin" />
|
|
156
|
-
) : (
|
|
157
|
-
<Wifi className="w-4 h-4" />
|
|
158
|
-
)}
|
|
159
|
-
Discover Prometheus
|
|
160
|
-
</button>
|
|
161
|
-
</div>
|
|
162
|
-
</div>
|
|
163
|
-
)
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const activeCategoryDef = categories.find(c => c.key === activeCategory) || categories[0]
|
|
167
|
-
|
|
168
115
|
return (
|
|
169
|
-
<
|
|
170
|
-
{
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
{cat.label}
|
|
187
|
-
</button>
|
|
188
|
-
))}
|
|
189
|
-
</div>
|
|
190
|
-
|
|
191
|
-
{/* Time range selector */}
|
|
192
|
-
<select
|
|
193
|
-
value={timeRange}
|
|
194
|
-
onChange={e => {
|
|
195
|
-
const next = e.target.value as PrometheusTimeRange
|
|
196
|
-
setTimeRange(next)
|
|
197
|
-
onTimeRangeChange?.(next)
|
|
198
|
-
}}
|
|
199
|
-
className="px-2 py-1 text-xs rounded-md bg-theme-elevated border border-theme-border text-theme-text-secondary focus:outline-none focus:ring-1 focus:ring-blue-500/50"
|
|
200
|
-
>
|
|
201
|
-
{TIME_RANGES.map(tr => (
|
|
202
|
-
<option key={tr.value} value={tr.value}>{tr.label}</option>
|
|
203
|
-
))}
|
|
204
|
-
</select>
|
|
205
|
-
</div>
|
|
206
|
-
|
|
207
|
-
{/* Chart area — fixed min-height prevents layout shift while loading */}
|
|
208
|
-
<div className="min-h-[280px] p-4">
|
|
209
|
-
{metricsLoading ? (
|
|
210
|
-
<div className="flex items-center justify-center min-h-[240px] text-theme-text-tertiary">
|
|
211
|
-
<Loader2 className="w-5 h-5 animate-spin mr-2" />
|
|
212
|
-
Loading metrics...
|
|
213
|
-
</div>
|
|
214
|
-
) : metricsError ? (
|
|
215
|
-
<div className="flex items-center justify-center h-full text-red-400 text-sm">
|
|
216
|
-
Failed to load metrics: {(metricsError as Error).message}
|
|
217
|
-
</div>
|
|
218
|
-
) : metrics?.result?.series?.length ? (
|
|
219
|
-
<div className="h-full flex flex-col gap-4">
|
|
220
|
-
{/* Summary stats */}
|
|
221
|
-
<MetricsSummary
|
|
222
|
-
series={metrics.result.series}
|
|
223
|
-
category={activeCategoryDef}
|
|
224
|
-
unit={metrics.unit}
|
|
225
|
-
/>
|
|
226
|
-
|
|
227
|
-
{/* Main chart */}
|
|
228
|
-
<div className="flex-1 min-h-0">
|
|
229
|
-
<AreaChart
|
|
230
|
-
series={metrics.result.series}
|
|
231
|
-
color={activeCategoryDef.chartColor}
|
|
232
|
-
fillColor={activeCategoryDef.fillColor}
|
|
233
|
-
unit={metrics.unit}
|
|
234
|
-
referenceLines={referenceLines}
|
|
235
|
-
/>
|
|
236
|
-
</div>
|
|
237
|
-
|
|
238
|
-
{/* Per-pod legend for workload-level queries */}
|
|
239
|
-
{metrics.result.series.length > 1 && (
|
|
240
|
-
<SeriesLegend series={metrics.result.series} color={activeCategoryDef.chartColor} />
|
|
241
|
-
)}
|
|
242
|
-
</div>
|
|
243
|
-
) : (
|
|
244
|
-
<div className="flex flex-col items-center justify-center h-full text-theme-text-tertiary">
|
|
245
|
-
<BarChart3 className="w-8 h-8 mb-2 opacity-40" />
|
|
246
|
-
<p className="text-sm">No data for this time range</p>
|
|
247
|
-
<p className="text-xs text-theme-text-quaternary mt-1">
|
|
248
|
-
Try a different time range or check that metrics are being collected
|
|
249
|
-
</p>
|
|
250
|
-
{metrics?.hint && (
|
|
251
|
-
<p className="mt-3 px-3 py-2 w-full max-w-lg text-xs text-yellow-700 dark:text-yellow-400 bg-yellow-500/10 border border-yellow-500/30 rounded">
|
|
252
|
-
{metrics.hint}
|
|
253
|
-
</p>
|
|
254
|
-
)}
|
|
255
|
-
{metrics?.query && (
|
|
256
|
-
<details className="mt-3 w-full max-w-lg text-left">
|
|
257
|
-
<summary className="text-xs text-theme-text-quaternary cursor-pointer hover:text-theme-text-tertiary">
|
|
258
|
-
Diagnostics: show PromQL query
|
|
259
|
-
</summary>
|
|
260
|
-
<div className="mt-2 p-2 bg-theme-base border border-theme-border rounded text-xs font-mono text-theme-text-secondary break-all">
|
|
261
|
-
{metrics.query}
|
|
262
|
-
</div>
|
|
263
|
-
<p className="mt-1.5 text-xs text-theme-text-quaternary">
|
|
264
|
-
This query returned no results. Verify in your Prometheus UI that the metric names and labels
|
|
265
|
-
({activeCategoryDef.key === 'cpu' ? 'pod, namespace, container' : 'pod, namespace'}) exist.
|
|
266
|
-
Custom label relabeling in your Prometheus configuration may require adjustments.
|
|
267
|
-
</p>
|
|
268
|
-
</details>
|
|
269
|
-
)}
|
|
270
|
-
</div>
|
|
271
|
-
)}
|
|
272
|
-
</div>
|
|
273
|
-
</div>
|
|
116
|
+
<PrometheusChartsView
|
|
117
|
+
kind={kind}
|
|
118
|
+
showEmptyState={showEmptyState}
|
|
119
|
+
statusLoading={statusLoading}
|
|
120
|
+
isConnected={isConnected}
|
|
121
|
+
statusError={status?.error}
|
|
122
|
+
onConnect={() => connectMutation.mutate()}
|
|
123
|
+
connecting={connectMutation.isPending}
|
|
124
|
+
category={activeCategory}
|
|
125
|
+
onCategoryChange={setActiveCategory}
|
|
126
|
+
range={timeRange}
|
|
127
|
+
onRangeChange={(r) => { setTimeRange(r); onTimeRangeChange?.(r) }}
|
|
128
|
+
metrics={metrics}
|
|
129
|
+
metricsLoading={metricsLoading}
|
|
130
|
+
metricsError={(metricsError as Error) ?? null}
|
|
131
|
+
referenceLines={referenceLines}
|
|
132
|
+
/>
|
|
274
133
|
)
|
|
275
134
|
}
|
|
276
135
|
|
|
@@ -444,7 +444,7 @@ function AuthenticationHelp({ image, registryType, onRetry }: AuthenticationHelp
|
|
|
444
444
|
</p>
|
|
445
445
|
|
|
446
446
|
<p className="text-xs text-theme-text-tertiary text-center max-w-md mb-6">
|
|
447
|
-
Registry: <span className="
|
|
447
|
+
Registry: <span className="inline-code">{registry}</span>
|
|
448
448
|
{registryType && registryType !== 'generic' && (
|
|
449
449
|
<> ({formatAuthMethod(registryType)})</>
|
|
450
450
|
)}
|
|
@@ -743,4 +743,3 @@ function FileTreeNode({ node, depth, defaultExpanded = true, image, namespace, p
|
|
|
743
743
|
</div>
|
|
744
744
|
)
|
|
745
745
|
}
|
|
746
|
-
|
|
@@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom'
|
|
|
3
3
|
import { useScaleWorkload } from '../../../api/client'
|
|
4
4
|
import { useRBACSubject } from '../../../api/rbac'
|
|
5
5
|
import { useQueryClient } from '@tanstack/react-query'
|
|
6
|
+
import type { Relationships, ResourceRef } from '../../../types'
|
|
6
7
|
|
|
7
8
|
// Map plural lowercase kind to singular PascalCase for ownerReferences matching
|
|
8
9
|
function getOwnerKind(kind: string): string {
|
|
@@ -19,10 +20,12 @@ function getOwnerKind(kind: string): string {
|
|
|
19
20
|
interface WorkloadRendererProps {
|
|
20
21
|
kind: string
|
|
21
22
|
data: any
|
|
22
|
-
onNavigate?: (ref:
|
|
23
|
+
onNavigate?: (ref: ResourceRef) => void
|
|
24
|
+
relationships?: Relationships
|
|
25
|
+
scaleBlockedBy?: ResourceRef[]
|
|
23
26
|
}
|
|
24
27
|
|
|
25
|
-
export function WorkloadRenderer({ kind, data, onNavigate }: WorkloadRendererProps) {
|
|
28
|
+
export function WorkloadRenderer({ kind, data, onNavigate, scaleBlockedBy }: WorkloadRendererProps) {
|
|
26
29
|
const navigate = useNavigate()
|
|
27
30
|
const queryClient = useQueryClient()
|
|
28
31
|
const scaleMutation = useScaleWorkload()
|
|
@@ -47,6 +50,7 @@ export function WorkloadRenderer({ kind, data, onNavigate }: WorkloadRendererPro
|
|
|
47
50
|
rbacData={rbacData ?? null}
|
|
48
51
|
rbacLoading={rbacLoading}
|
|
49
52
|
rbacError={rbacError as Error | null}
|
|
53
|
+
scaleBlockedBy={scaleBlockedBy}
|
|
50
54
|
onScale={async (replicas) => {
|
|
51
55
|
await scaleMutation.mutateAsync({
|
|
52
56
|
kind,
|
|
@@ -109,7 +109,7 @@ export function MyPermissionsDialog({ open, onClose }: MyPermissionsDialogProps)
|
|
|
109
109
|
|
|
110
110
|
<p className="text-xs text-theme-text-tertiary">
|
|
111
111
|
Computed by the Kubernetes API via{' '}
|
|
112
|
-
<code className="
|
|
112
|
+
<code className="inline-code">SelfSubjectRulesReview</code>.
|
|
113
113
|
Shows what you can do in <span className="text-theme-text-secondary">{namespace}</span>,
|
|
114
114
|
plus any cluster-scoped rules that apply everywhere.
|
|
115
115
|
</p>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useState, useEffect, useRef, useCallback } from 'react'
|
|
2
2
|
import { createPortal } from 'react-dom'
|
|
3
|
-
import { Settings, X, RotateCcw, Loader2, Copy, Check, Pin } from 'lucide-react'
|
|
3
|
+
import { Settings, X, RotateCcw, Loader2, Copy, Check, Pin, Shield } from 'lucide-react'
|
|
4
4
|
import { clsx } from 'clsx'
|
|
5
5
|
import { useAnimatedUnmount } from '../../hooks/useAnimatedUnmount'
|
|
6
6
|
import { TRANSITION_BACKDROP, TRANSITION_PANEL } from '../../utils/animation'
|
|
@@ -28,9 +28,10 @@ interface ConfigResponse {
|
|
|
28
28
|
interface SettingsDialogProps {
|
|
29
29
|
open: boolean
|
|
30
30
|
onClose: () => void
|
|
31
|
+
onShowMyPermissions?: () => void
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
export function SettingsDialog({ open, onClose }: SettingsDialogProps) {
|
|
34
|
+
export function SettingsDialog({ open, onClose, onShowMyPermissions }: SettingsDialogProps) {
|
|
34
35
|
const dialogRef = useRef<HTMLDivElement>(null)
|
|
35
36
|
const { shouldRender, isOpen } = useAnimatedUnmount(open, 200)
|
|
36
37
|
const [configData, setConfigData] = useState<ConfigResponse | null>(null)
|
|
@@ -167,6 +168,25 @@ export function SettingsDialog({ open, onClose }: SettingsDialogProps) {
|
|
|
167
168
|
{loadError}
|
|
168
169
|
</div>
|
|
169
170
|
)}
|
|
171
|
+
{onShowMyPermissions && (
|
|
172
|
+
<div className="mb-4 rounded-md border border-theme-border bg-theme-elevated/50 p-3">
|
|
173
|
+
<div className="flex items-center justify-between gap-3">
|
|
174
|
+
<div className="min-w-0">
|
|
175
|
+
<h3 className="text-sm font-medium text-theme-text-primary">My permissions</h3>
|
|
176
|
+
<p className="mt-0.5 text-xs text-theme-text-tertiary">
|
|
177
|
+
View what your current identity can do in this cluster.
|
|
178
|
+
</p>
|
|
179
|
+
</div>
|
|
180
|
+
<button
|
|
181
|
+
onClick={onShowMyPermissions}
|
|
182
|
+
className="shrink-0 flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-hover rounded-md transition-colors"
|
|
183
|
+
>
|
|
184
|
+
<Shield className="w-3.5 h-3.5" />
|
|
185
|
+
Open
|
|
186
|
+
</button>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
)}
|
|
170
190
|
<StartupConfigTab
|
|
171
191
|
config={editedConfig}
|
|
172
192
|
effectiveConfig={configData?.effective}
|