@skyhook-io/k8s-ui 1.5.2 → 1.5.4
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/components/audit/AuditAlerts.tsx +6 -0
- package/src/components/audit/AuditFindingsTable.tsx +83 -33
- package/src/components/gitops/ManagedResourcesList.tsx +2 -1
- package/src/components/logs/JsonLogLine.tsx +10 -7
- package/src/components/logs/LogCore.tsx +160 -63
- package/src/components/logs/LogToolbarSelects.tsx +17 -6
- package/src/components/logs/LogsViewer.tsx +8 -7
- package/src/components/logs/StructuredLogLine.tsx +46 -47
- package/src/components/logs/WorkloadLogsViewer.tsx +44 -34
- package/src/components/logs/log-palette.ts +222 -0
- package/src/components/logs/useLogBuffer.ts +6 -1
- package/src/components/resources/ResourcesView.tsx +183 -52
- package/src/components/resources/renderers/CNPGClusterRenderer.tsx +3 -2
- package/src/components/resources/renderers/CertificateRenderer.tsx +4 -3
- package/src/components/resources/renderers/ClusterComplianceReportRenderer.tsx +2 -1
- package/src/components/resources/renderers/ClusterExternalSecretRenderer.tsx +4 -3
- package/src/components/resources/renderers/ExposedSecretReportRenderer.tsx +2 -1
- package/src/components/resources/renderers/KnativeRevisionRenderer.tsx +2 -1
- package/src/components/resources/renderers/KyvernoPolicyReportRenderer.tsx +4 -3
- package/src/components/resources/renderers/PrometheusRuleRenderer.tsx +3 -2
- package/src/components/resources/renderers/SecretRenderer.tsx +10 -4
- package/src/components/resources/renderers/trivy-shared.tsx +3 -2
- package/src/components/resources/resource-utils-istio.ts +4 -3
- package/src/components/resources/resource-utils.ts +10 -2
- package/src/components/timeline/TimelineList.tsx +2 -1
- package/src/components/topology/GroupNode.tsx +2 -1
- package/src/components/topology/TopologyGraph.tsx +2 -1
- package/src/components/ui/Badge.tsx +2 -1
- package/src/components/ui/ClusterName.tsx +95 -0
- package/src/components/ui/EmptyState.tsx +118 -0
- package/src/components/ui/FilterPill.tsx +98 -0
- package/src/components/ui/ForceDeleteConfirmDialog.tsx +2 -1
- package/src/components/ui/ResourceBar.tsx +1 -1
- package/src/components/ui/Tooltip.tsx +43 -0
- package/src/components/ui/drawer-components.tsx +11 -0
- package/src/components/ui/index.ts +7 -0
- package/src/components/ui/provider-logos/aws-dark.png +0 -0
- package/src/components/ui/provider-logos/aws.png +0 -0
- package/src/components/ui/provider-logos/azure.svg +23 -0
- package/src/components/ui/provider-logos/gcp.png +0 -0
- package/src/components/ui/status-tone.test.ts +53 -0
- package/src/components/ui/status-tone.tsx +104 -0
- package/src/components/workload/WorkloadView.tsx +3 -3
- package/src/theme/components.css +7 -0
- package/src/utils/badge-colors.ts +1 -0
- package/src/utils/context-name.test.ts +106 -0
- package/src/utils/context-name.ts +25 -6
- package/src/utils/index.ts +1 -0
- package/src/utils/navigation.ts +4 -8
- package/src/utils/pluralize.test.ts +98 -0
- package/src/utils/pluralize.ts +60 -0
package/package.json
CHANGED
|
@@ -11,6 +11,12 @@ export interface AuditFinding {
|
|
|
11
11
|
category: string
|
|
12
12
|
severity: string
|
|
13
13
|
message: string
|
|
14
|
+
/** Cluster context — set when findings come from multiple clusters
|
|
15
|
+
* (cross-cluster aggregation). Rendered as a Cluster column when the
|
|
16
|
+
* host passes `multiCluster` to AuditFindingsTable. Grouped as one
|
|
17
|
+
* optional object so id and name stay in lockstep — `name` is
|
|
18
|
+
* meaningless without `id`. */
|
|
19
|
+
cluster?: { id: string; name: string }
|
|
14
20
|
}
|
|
15
21
|
|
|
16
22
|
interface AuditAlertsProps {
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { useState, useMemo, useRef, useEffect } from 'react'
|
|
2
|
-
import { ShieldAlert, AlertTriangle, ChevronRight, Search, ExternalLink, MoreHorizontal, EyeOff, Layers } from 'lucide-react'
|
|
2
|
+
import { ShieldAlert, AlertTriangle, ChevronRight, CheckCircle2, Search, ExternalLink, MoreHorizontal, EyeOff, Layers } from 'lucide-react'
|
|
3
3
|
import { clsx } from 'clsx'
|
|
4
4
|
import type { AuditFinding } from './AuditAlerts'
|
|
5
5
|
import { SEVERITY_TEXT, BP_CATEGORY_BADGE, DEFAULT_BADGE_COLOR } from '../../utils/badge-colors'
|
|
6
|
+
import { EmptyState } from '../ui/EmptyState'
|
|
7
|
+
import { FilterPill } from '../ui/FilterPill'
|
|
8
|
+
import { pluralize } from '../../utils/pluralize'
|
|
6
9
|
|
|
7
10
|
const CATEGORIES = ['Security', 'Reliability', 'Efficiency'] as const
|
|
8
11
|
const SEVERITIES = ['danger', 'warning'] as const
|
|
@@ -32,9 +35,19 @@ export interface AuditFindingsTableProps {
|
|
|
32
35
|
onHideCheck?: (checkID: string, title: string) => void
|
|
33
36
|
onHideCategory?: (category: string) => void
|
|
34
37
|
onHideNamespace?: (namespace: string) => void
|
|
38
|
+
/** When true, render a Cluster column on flat findings rows. Set this
|
|
39
|
+
* when findings come from multiple clusters (cross-cluster aggregation).
|
|
40
|
+
* Each finding's clusterId/clusterName fields populate the column.
|
|
41
|
+
* (Currently flat-rows only; grouped views don't surface a Cluster
|
|
42
|
+
* column today.) */
|
|
43
|
+
multiCluster?: boolean
|
|
44
|
+
/** Click-through for cluster-name links in the multi-cluster view.
|
|
45
|
+
* Defaults to no-op; multi-cluster hosts pass a navigator that opens
|
|
46
|
+
* the cluster's per-cluster audit page. */
|
|
47
|
+
onClusterClick?: (clusterId: string) => void
|
|
35
48
|
}
|
|
36
49
|
|
|
37
|
-
export function AuditFindingsTable({ groups, findings, checks, onResourceClick, onHideCheck, onHideCategory, onHideNamespace }: AuditFindingsTableProps) {
|
|
50
|
+
export function AuditFindingsTable({ groups, findings, checks, onResourceClick, onHideCheck, onHideCategory, onHideNamespace, multiCluster, onClusterClick }: AuditFindingsTableProps) {
|
|
38
51
|
const [categoryFilter, setCategoryFilter] = useState<string | null>(null)
|
|
39
52
|
const [severityFilter, setSeverityFilter] = useState<string | null>(null)
|
|
40
53
|
const [frameworkFilter, setFrameworkFilter] = useState<string | null>(null)
|
|
@@ -230,22 +243,28 @@ export function AuditFindingsTable({ groups, findings, checks, onResourceClick,
|
|
|
230
243
|
)}
|
|
231
244
|
</div>
|
|
232
245
|
|
|
233
|
-
{/* Row 2: Filter chips */}
|
|
234
|
-
<div className="flex flex-wrap items-center gap-1">
|
|
235
|
-
<
|
|
236
|
-
<span className="w-px h-
|
|
246
|
+
{/* Row 2: Filter chips — three groups separated by dividers (All | Categories | Severities | Frameworks). */}
|
|
247
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
248
|
+
<FilterPill label="All" active={!categoryFilter && !severityFilter && !frameworkFilter} onClick={() => { setCategoryFilter(null); setSeverityFilter(null); setFrameworkFilter(null) }} />
|
|
249
|
+
<span className="w-px h-5 bg-theme-border mx-2" />
|
|
237
250
|
{CATEGORIES.map(cat => (
|
|
238
|
-
<
|
|
251
|
+
<FilterPill key={cat} label={cat} active={categoryFilter === cat} onClick={() => setCategoryFilter(categoryFilter === cat ? null : cat)} />
|
|
239
252
|
))}
|
|
240
|
-
<span className="w-px h-
|
|
253
|
+
<span className="w-px h-5 bg-theme-border mx-2" />
|
|
241
254
|
{SEVERITIES.map(sev => (
|
|
242
|
-
<
|
|
255
|
+
<FilterPill
|
|
256
|
+
key={sev}
|
|
257
|
+
label={sev === 'danger' ? 'Critical' : 'Warning'}
|
|
258
|
+
active={severityFilter === sev}
|
|
259
|
+
tone={sev === 'danger' ? 'danger' : 'warn'}
|
|
260
|
+
onClick={() => setSeverityFilter(severityFilter === sev ? null : sev)}
|
|
261
|
+
/>
|
|
243
262
|
))}
|
|
244
263
|
{frameworks.length > 0 && (
|
|
245
264
|
<>
|
|
246
|
-
<span className="w-px h-
|
|
265
|
+
<span className="w-px h-5 bg-theme-border mx-2" />
|
|
247
266
|
{frameworks.map(fw => (
|
|
248
|
-
<
|
|
267
|
+
<FilterPill key={fw} label={fw} active={frameworkFilter === fw} onClick={() => setFrameworkFilter(frameworkFilter === fw ? null : fw)} />
|
|
249
268
|
))}
|
|
250
269
|
</>
|
|
251
270
|
)}
|
|
@@ -254,9 +273,33 @@ export function AuditFindingsTable({ groups, findings, checks, onResourceClick,
|
|
|
254
273
|
|
|
255
274
|
{/* Content */}
|
|
256
275
|
{isEmpty ? (
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
276
|
+
totalEmpty ? (
|
|
277
|
+
// Healthy state — communicates "you're winning", not "no data".
|
|
278
|
+
// Replaces a grey one-liner that conflated both meanings.
|
|
279
|
+
<EmptyState
|
|
280
|
+
tone="healthy"
|
|
281
|
+
variant="card"
|
|
282
|
+
icon={CheckCircle2}
|
|
283
|
+
headline="All checks passing"
|
|
284
|
+
body="No issues found across these resources."
|
|
285
|
+
/>
|
|
286
|
+
) : (
|
|
287
|
+
<EmptyState
|
|
288
|
+
tone="filtered"
|
|
289
|
+
variant="card"
|
|
290
|
+
headline="No findings match the current filters"
|
|
291
|
+
body="Try clearing one or more filters to see more results."
|
|
292
|
+
action={
|
|
293
|
+
<button
|
|
294
|
+
type="button"
|
|
295
|
+
onClick={() => { setCategoryFilter(null); setSeverityFilter(null); setFrameworkFilter(null); setSearchTerm('') }}
|
|
296
|
+
className="badge badge-sm border border-theme-border bg-theme-elevated text-theme-text-primary hover:bg-theme-hover transition-colors"
|
|
297
|
+
>
|
|
298
|
+
Clear all filters
|
|
299
|
+
</button>
|
|
300
|
+
}
|
|
301
|
+
/>
|
|
302
|
+
)
|
|
260
303
|
) : namespacedGroups ? (
|
|
261
304
|
/* Namespace-grouped view */
|
|
262
305
|
<div className="flex flex-col gap-1">
|
|
@@ -272,7 +315,7 @@ export function AuditFindingsTable({ groups, findings, checks, onResourceClick,
|
|
|
272
315
|
>
|
|
273
316
|
<ChevronRight className={clsx('w-4 h-4 text-theme-text-tertiary shrink-0 transition-transform duration-200', nsExpanded && 'rotate-90')} />
|
|
274
317
|
<span className="text-sm font-semibold text-theme-text-primary">{ns}</span>
|
|
275
|
-
<span className="text-xs text-theme-text-tertiary">{nsGroups.length
|
|
318
|
+
<span className="text-xs text-theme-text-tertiary">{pluralize(nsGroups.length, 'resource')}</span>
|
|
276
319
|
<span className="flex-1" />
|
|
277
320
|
<div className="flex items-center gap-3 shrink-0">
|
|
278
321
|
{nsDanger > 0 && <span className={clsx('text-xs font-semibold tabular-nums', SEVERITY_TEXT.error)}>{nsDanger} critical</span>}
|
|
@@ -309,7 +352,13 @@ export function AuditFindingsTable({ groups, findings, checks, onResourceClick,
|
|
|
309
352
|
/* Flat fallback (per-resource view) */
|
|
310
353
|
<div className="flex flex-col gap-1">
|
|
311
354
|
{filteredFindings?.map((f, i) => (
|
|
312
|
-
<FlatFindingRow
|
|
355
|
+
<FlatFindingRow
|
|
356
|
+
key={`${f.cluster?.id ?? ''}-${f.checkID}-${i}`}
|
|
357
|
+
finding={f}
|
|
358
|
+
onResourceClick={onResourceClick}
|
|
359
|
+
showCluster={multiCluster}
|
|
360
|
+
onClusterClick={onClusterClick}
|
|
361
|
+
/>
|
|
313
362
|
))}
|
|
314
363
|
</div>
|
|
315
364
|
)}
|
|
@@ -425,7 +474,7 @@ function ResourceGroupRow({ group: g, checks, expanded, onToggle, onResourceClic
|
|
|
425
474
|
)
|
|
426
475
|
}
|
|
427
476
|
|
|
428
|
-
function FlatFindingRow({ finding, onResourceClick }: { finding: AuditFinding; onResourceClick?: (kind: string, namespace: string, name: string) => void }) {
|
|
477
|
+
function FlatFindingRow({ finding, onResourceClick, showCluster, onClusterClick }: { finding: AuditFinding; onResourceClick?: (kind: string, namespace: string, name: string) => void; showCluster?: boolean; onClusterClick?: (clusterId: string) => void }) {
|
|
429
478
|
const isDanger = finding.severity === 'danger'
|
|
430
479
|
const severityColor = isDanger ? SEVERITY_TEXT.error : SEVERITY_TEXT.warning
|
|
431
480
|
|
|
@@ -436,6 +485,23 @@ function FlatFindingRow({ finding, onResourceClick }: { finding: AuditFinding; o
|
|
|
436
485
|
) : (
|
|
437
486
|
<AlertTriangle className={clsx('w-4 h-4 shrink-0', severityColor)} />
|
|
438
487
|
)}
|
|
488
|
+
{showCluster && finding.cluster && (
|
|
489
|
+
// Cluster column for multi-cluster contexts. Renders before
|
|
490
|
+
// the kind/namespace/name path so cluster scope is established
|
|
491
|
+
// first in the read order.
|
|
492
|
+
onClusterClick ? (
|
|
493
|
+
<button
|
|
494
|
+
onClick={() => onClusterClick(finding.cluster!.id)}
|
|
495
|
+
className="text-xs text-[var(--color-radar-accent)] hover:underline shrink-0 max-w-[160px] truncate text-left"
|
|
496
|
+
>
|
|
497
|
+
{finding.cluster.name}
|
|
498
|
+
</button>
|
|
499
|
+
) : (
|
|
500
|
+
<span className="text-xs text-theme-text-secondary shrink-0 max-w-[160px] truncate">
|
|
501
|
+
{finding.cluster.name}
|
|
502
|
+
</span>
|
|
503
|
+
)
|
|
504
|
+
)}
|
|
439
505
|
{onResourceClick ? (
|
|
440
506
|
<button
|
|
441
507
|
onClick={() => onResourceClick(finding.kind, finding.namespace, finding.name)}
|
|
@@ -465,22 +531,6 @@ function SummaryBadge({ label, count, color }: { label: string; count: number; c
|
|
|
465
531
|
)
|
|
466
532
|
}
|
|
467
533
|
|
|
468
|
-
function FilterChip({ label, active, onClick }: { label: string; active: boolean; onClick: () => void }) {
|
|
469
|
-
return (
|
|
470
|
-
<button
|
|
471
|
-
onClick={onClick}
|
|
472
|
-
className={clsx(
|
|
473
|
-
'px-2 py-0.5 text-xs rounded-md transition-colors focus-visible:ring-2 focus-visible:ring-theme-text-primary/20 focus-visible:outline-none',
|
|
474
|
-
active
|
|
475
|
-
? 'bg-theme-text-primary/10 text-theme-text-primary font-medium'
|
|
476
|
-
: 'text-theme-text-tertiary hover:text-theme-text-secondary hover:bg-theme-hover'
|
|
477
|
-
)}
|
|
478
|
-
>
|
|
479
|
-
{label}
|
|
480
|
-
</button>
|
|
481
|
-
)
|
|
482
|
-
}
|
|
483
|
-
|
|
484
534
|
interface ContextMenuItem {
|
|
485
535
|
label: string
|
|
486
536
|
onClick: () => void
|
|
@@ -3,6 +3,7 @@ import { ChevronDown, ChevronRight, Package, ExternalLink } from 'lucide-react'
|
|
|
3
3
|
import { clsx } from 'clsx'
|
|
4
4
|
import type { ManagedResource, GitOpsHealthStatus } from '../../types/gitops'
|
|
5
5
|
import { groupManagedResourcesByKind } from '../../types/gitops'
|
|
6
|
+
import { pluralize } from '../../utils/pluralize'
|
|
6
7
|
|
|
7
8
|
interface ManagedResourcesListProps {
|
|
8
9
|
resources: ManagedResource[]
|
|
@@ -204,7 +205,7 @@ export function InventoryCount({ count, healthy, unhealthy }: { count: number; h
|
|
|
204
205
|
|
|
205
206
|
return (
|
|
206
207
|
<span className="text-xs text-theme-text-secondary">
|
|
207
|
-
{count
|
|
208
|
+
{pluralize(count, 'resource')}
|
|
208
209
|
</span>
|
|
209
210
|
)
|
|
210
211
|
}
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import { useState, useMemo } from 'react'
|
|
2
2
|
import { ChevronRight, ChevronDown } from 'lucide-react'
|
|
3
|
-
import { getLevelColor } from '../../utils/log-format'
|
|
4
3
|
import type { LogLevel } from './useLogBuffer'
|
|
4
|
+
import { getLogPalette, getLogLevelColor } from './log-palette'
|
|
5
5
|
|
|
6
6
|
interface JsonLogLineProps {
|
|
7
7
|
content: string
|
|
8
8
|
level: LogLevel
|
|
9
9
|
wordWrap: boolean
|
|
10
|
+
/** Dark/light palette selector. Defaults to `true` for the dark-by-default viewer. */
|
|
11
|
+
isDark?: boolean
|
|
10
12
|
}
|
|
11
13
|
|
|
12
|
-
export function JsonLogLine({ content, level, wordWrap }: JsonLogLineProps) {
|
|
14
|
+
export function JsonLogLine({ content, level, wordWrap, isDark = true }: JsonLogLineProps) {
|
|
13
15
|
const [expanded, setExpanded] = useState(false)
|
|
14
|
-
const
|
|
16
|
+
const palette = useMemo(() => getLogPalette(isDark), [isDark])
|
|
17
|
+
const levelColor = getLogLevelColor(level, isDark)
|
|
15
18
|
|
|
16
19
|
const parsed = useMemo(() => {
|
|
17
20
|
try {
|
|
@@ -36,17 +39,17 @@ export function JsonLogLine({ content, level, wordWrap }: JsonLogLineProps) {
|
|
|
36
39
|
<span className={levelColor}>
|
|
37
40
|
<button
|
|
38
41
|
onClick={() => setExpanded(!expanded)}
|
|
39
|
-
className=
|
|
42
|
+
className={`inline-flex items-center gap-0.5 ${palette.hoverSurface} rounded px-0.5 -ml-0.5 align-middle`}
|
|
40
43
|
>
|
|
41
44
|
{expanded
|
|
42
|
-
? <ChevronDown className=
|
|
43
|
-
: <ChevronRight className=
|
|
45
|
+
? <ChevronDown className={`w-3 h-3 shrink-0 ${palette.textTertiary}`} />
|
|
46
|
+
: <ChevronRight className={`w-3 h-3 shrink-0 ${palette.textTertiary}`} />
|
|
44
47
|
}
|
|
45
48
|
</button>
|
|
46
49
|
{!expanded ? (
|
|
47
50
|
<span className={wordWrap ? 'whitespace-pre-wrap break-all' : 'whitespace-pre'}>
|
|
48
51
|
{summary}
|
|
49
|
-
<span className=
|
|
52
|
+
<span className={`${palette.textTertiary} ml-1`}>{`{${fieldCount} fields}`}</span>
|
|
50
53
|
</span>
|
|
51
54
|
) : (
|
|
52
55
|
<span className={`block ml-4 ${wordWrap ? 'whitespace-pre-wrap break-all' : 'whitespace-pre'}`}>
|