@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.
@@ -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
- AreaChart,
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
- <div className="flex flex-col h-full">
170
- {/* Toolbar */}
171
- <div className="shrink-0 flex items-center justify-between px-4 py-2.5 border-b border-theme-border bg-theme-surface/50">
172
- {/* Category tabs */}
173
- <div className="flex items-center gap-1">
174
- <BarChart3 className="w-4 h-4 text-theme-text-tertiary mr-2" />
175
- {categories.map(cat => (
176
- <button
177
- key={cat.key}
178
- onClick={() => setActiveCategory(cat.key)}
179
- className={clsx(
180
- 'px-2.5 py-1 text-xs font-medium rounded-md transition-colors',
181
- activeCategory === cat.key
182
- ? 'bg-theme-elevated text-theme-text-primary shadow-sm'
183
- : 'text-theme-text-tertiary hover:text-theme-text-secondary hover:bg-theme-elevated/50'
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="font-mono text-theme-text-secondary">{registry}</span>
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: { kind: string; namespace: string; name: string }) => void
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="bg-theme-elevated px-1 rounded">SelfSubjectRulesReview</code>.
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}