@skyhook-io/k8s-ui 1.7.2 → 1.7.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 +3 -3
- package/src/components/audit/AuditFindingsTable.tsx +7 -0
- package/src/components/checks/ChecksView.tsx +899 -0
- package/src/components/checks/checks.test.ts +40 -0
- package/src/components/checks/index.ts +3 -0
- package/src/components/checks/severity.ts +63 -0
- package/src/components/checks/types.ts +138 -0
- package/src/components/gitops/GitOpsTableView.tsx +246 -28
- package/src/components/resources/ResourcesView.tsx +61 -0
- package/src/components/resources/renderers/NamespaceRenderer.tsx +2 -5
- package/src/components/resources/renderers/PodRenderer.tsx +6 -7
- package/src/components/resources/renderers/RBACErrorSection.test.tsx +62 -0
- package/src/components/resources/renderers/RBACErrorSection.tsx +72 -0
- package/src/components/resources/renderers/RoleRenderer.tsx +2 -7
- package/src/components/resources/renderers/ServiceAccountRenderer.tsx +2 -7
- package/src/components/resources/renderers/WorkloadRenderer.tsx +6 -5
- package/src/components/ui/ClusterName.tsx +15 -7
- package/src/index.ts +5 -0
- package/src/utils/resource-icons.ts +1 -1
|
@@ -1776,6 +1776,13 @@ interface ResourcesViewProps {
|
|
|
1776
1776
|
* namespace+name only.
|
|
1777
1777
|
*/
|
|
1778
1778
|
resolveRowCluster?: (resource: any) => { id: string; name: string } | undefined
|
|
1779
|
+
/**
|
|
1780
|
+
* Clears the global namespace selection (the header NamespaceSwitcher state).
|
|
1781
|
+
* When wired, the "Clear filters" button also drops the active namespaces;
|
|
1782
|
+
* otherwise it only resets the view-local filter state. Host-owned because
|
|
1783
|
+
* the switcher lives outside this component and may persist server-side.
|
|
1784
|
+
*/
|
|
1785
|
+
onClearNamespaces?: () => void
|
|
1779
1786
|
}
|
|
1780
1787
|
|
|
1781
1788
|
// Default selected kind
|
|
@@ -1922,6 +1929,7 @@ export function ResourcesView({
|
|
|
1922
1929
|
onRowSelect,
|
|
1923
1930
|
onCompareSubmit,
|
|
1924
1931
|
resolveRowCluster,
|
|
1932
|
+
onClearNamespaces,
|
|
1925
1933
|
}: ResourcesViewProps) {
|
|
1926
1934
|
const initialFilters = getInitialFiltersFromURL()
|
|
1927
1935
|
const [selectedKind, setSelectedKind] = useState<SelectedKindInfo>(() => getInitialKindFromURL(basePath, defaultKind, locationPathname, locationSearch))
|
|
@@ -2684,6 +2692,25 @@ export function ResourcesView({
|
|
|
2684
2692
|
navigate({ pathname: newPath, search: queryStr }, { replace: !pushHistory })
|
|
2685
2693
|
}, [navigate, basePath])
|
|
2686
2694
|
|
|
2695
|
+
const clearAllFilters = useCallback(() => {
|
|
2696
|
+
setSearchTerm('')
|
|
2697
|
+
setColumnFilters({})
|
|
2698
|
+
setProblemFilters([])
|
|
2699
|
+
setLabelSelector('')
|
|
2700
|
+
setOwnerKind('')
|
|
2701
|
+
setOwnerName('')
|
|
2702
|
+
setShowInactiveReplicaSets(false)
|
|
2703
|
+
// Filter-only URL params. Path + kind + namespace + other cross-view
|
|
2704
|
+
// params are out of scope here; the host's onClearNamespaces (and its
|
|
2705
|
+
// own state→URL sync) owns namespace cleanup.
|
|
2706
|
+
const params = new URLSearchParams(window.location.search)
|
|
2707
|
+
for (const key of ['search', 'filters', 'problems', 'labels', 'ownerKind', 'ownerName', 'showInactive']) {
|
|
2708
|
+
params.delete(key)
|
|
2709
|
+
}
|
|
2710
|
+
navigate({ pathname: window.location.pathname, search: params.toString() }, { replace: true })
|
|
2711
|
+
onClearNamespaces?.()
|
|
2712
|
+
}, [navigate, onClearNamespaces])
|
|
2713
|
+
|
|
2687
2714
|
// Update URL when any filter changes
|
|
2688
2715
|
useEffect(() => {
|
|
2689
2716
|
// Skip URL update if we're syncing FROM the URL (e.g., browser back button)
|
|
@@ -3505,6 +3532,17 @@ export function ResourcesView({
|
|
|
3505
3532
|
|
|
3506
3533
|
// Check if any filters are active
|
|
3507
3534
|
const hasOwnerFilter = ownerKind !== '' && ownerName !== ''
|
|
3535
|
+
// Namespace contribution gated on a host-wired clearer: without it the
|
|
3536
|
+
// Clear filters button can't drop the namespace, so showing it would be
|
|
3537
|
+
// a no-op for that case.
|
|
3538
|
+
const hasAnyFilter =
|
|
3539
|
+
!!searchTerm ||
|
|
3540
|
+
!!labelSelector ||
|
|
3541
|
+
hasOwnerFilter ||
|
|
3542
|
+
problemFilters.length > 0 ||
|
|
3543
|
+
Object.values(columnFilters).some((vals) => vals.length > 0) ||
|
|
3544
|
+
showInactiveReplicaSets ||
|
|
3545
|
+
(!!onClearNamespaces && namespaces.length > 0)
|
|
3508
3546
|
|
|
3509
3547
|
|
|
3510
3548
|
// Toggle problem filter
|
|
@@ -3799,6 +3837,19 @@ export function ResourcesView({
|
|
|
3799
3837
|
</span>
|
|
3800
3838
|
)}
|
|
3801
3839
|
|
|
3840
|
+
{hasAnyFilter && (
|
|
3841
|
+
<Tooltip content={!!onClearNamespaces && namespaces.length > 0 ? 'Reset all filters and the active namespace' : 'Reset all filters'}>
|
|
3842
|
+
<button
|
|
3843
|
+
type="button"
|
|
3844
|
+
onClick={clearAllFilters}
|
|
3845
|
+
className="flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated transition-colors"
|
|
3846
|
+
>
|
|
3847
|
+
<RotateCcw className="w-3.5 h-3.5" />
|
|
3848
|
+
<span>Clear filters</span>
|
|
3849
|
+
</button>
|
|
3850
|
+
</Tooltip>
|
|
3851
|
+
)}
|
|
3852
|
+
|
|
3802
3853
|
{lastUpdated && <LastUpdatedLabel lastUpdated={lastUpdated} />}
|
|
3803
3854
|
{/* Column picker */}
|
|
3804
3855
|
<div className="relative" ref={columnPickerRef}>
|
|
@@ -3978,6 +4029,16 @@ export function ResourcesView({
|
|
|
3978
4029
|
</div>
|
|
3979
4030
|
)
|
|
3980
4031
|
})()}
|
|
4032
|
+
{hasAnyFilter && (
|
|
4033
|
+
<button
|
|
4034
|
+
type="button"
|
|
4035
|
+
onClick={clearAllFilters}
|
|
4036
|
+
className="flex items-center gap-1.5 mt-3 px-3 py-1.5 text-sm rounded-md bg-theme-elevated hover:bg-theme-border text-theme-text-secondary hover:text-theme-text-primary transition-colors"
|
|
4037
|
+
>
|
|
4038
|
+
<RotateCcw className="w-3.5 h-3.5" />
|
|
4039
|
+
Clear filters
|
|
4040
|
+
</button>
|
|
4041
|
+
)}
|
|
3981
4042
|
</div>
|
|
3982
4043
|
) : (
|
|
3983
4044
|
<MetricsContext.Provider value={metricsLookup}>
|
|
@@ -3,6 +3,7 @@ import { clsx } from 'clsx'
|
|
|
3
3
|
import { Section, PropertyList, Property, ResourceLink } from '../../ui/drawer-components'
|
|
4
4
|
import type { RBACNamespaceResponse, RBACBindingWithSubjects, RBACSubject, ResourceRef } from '../../../types'
|
|
5
5
|
import { rbacKindBadgeClass } from '../../../utils/rbac-badges'
|
|
6
|
+
import { RBACErrorSection } from './RBACErrorSection'
|
|
6
7
|
import { SEVERITY_TEXT, SEVERITY_DOT } from '../../../utils/badge-colors'
|
|
7
8
|
import { parseCPUToNanocores, parseMemoryToBytes } from '../../../utils/format'
|
|
8
9
|
|
|
@@ -197,11 +198,7 @@ function NamespaceRBACSection({
|
|
|
197
198
|
)
|
|
198
199
|
}
|
|
199
200
|
if (error) {
|
|
200
|
-
return
|
|
201
|
-
<Section title="RBAC" icon={Shield}>
|
|
202
|
-
<div className="text-sm text-red-400">Could not load RBAC summary: {error.message}</div>
|
|
203
|
-
</Section>
|
|
204
|
-
)
|
|
201
|
+
return <RBACErrorSection title="RBAC" error={error} errorPrefix="Could not load RBAC summary" />
|
|
205
202
|
}
|
|
206
203
|
if (!rbacData) return null
|
|
207
204
|
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
rbacApiGroupBadgeClass,
|
|
11
11
|
} from '../../../utils/rbac-badges'
|
|
12
12
|
import { detectBlastRadius, rulePermissivenessScore } from '../../../utils/rbac-blast-radius'
|
|
13
|
+
import { RBACErrorSection, isRBACUnavailable } from './RBACErrorSection'
|
|
13
14
|
import type { ResolvedEnvFrom, RBACSubjectResponse, RBACPolicyRule } from '../../../types'
|
|
14
15
|
import { Tooltip } from '../../ui/Tooltip'
|
|
15
16
|
import { MetricsChart } from '../../ui/MetricsChart'
|
|
@@ -877,13 +878,11 @@ function PodPermissionsSection({
|
|
|
877
878
|
)
|
|
878
879
|
}
|
|
879
880
|
if (error) {
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
</Section>
|
|
886
|
-
)
|
|
881
|
+
// Permissions is a bonus section here; when RBAC is simply not available
|
|
882
|
+
// (cluster-static) or forbidden, hide it rather than repeat a note on every
|
|
883
|
+
// Pod. Genuine faults still surface.
|
|
884
|
+
if (isRBACUnavailable(error)) return null
|
|
885
|
+
return <RBACErrorSection title={title} error={error} />
|
|
887
886
|
}
|
|
888
887
|
if (!rbacData) return null
|
|
889
888
|
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { renderToString } from 'react-dom/server'
|
|
3
|
+
import { RBACErrorSection, isRBACUnavailable } from './RBACErrorSection'
|
|
4
|
+
|
|
5
|
+
function err(message: string, status?: number): Error {
|
|
6
|
+
return Object.assign(new Error(message), status === undefined ? {} : { status })
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe('isRBACUnavailable', () => {
|
|
10
|
+
it('is true for the RBAC-cache 503 and any 403 (expected, non-actionable)', () => {
|
|
11
|
+
expect(isRBACUnavailable(err('RBAC cache not available', 503))).toBe(true)
|
|
12
|
+
expect(isRBACUnavailable(err('forbidden', 403))).toBe(true)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('is false for genuine faults so they still surface', () => {
|
|
16
|
+
expect(isRBACUnavailable(err('Not connected to cluster', 503))).toBe(false)
|
|
17
|
+
expect(isRBACUnavailable(err('boom', 500))).toBe(false)
|
|
18
|
+
expect(isRBACUnavailable(err('Failed to fetch'))).toBe(false)
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
describe('RBACErrorSection', () => {
|
|
23
|
+
it('renders 503 (SA cannot read RBAC) as a calm note, not a red error', () => {
|
|
24
|
+
const html = renderToString(
|
|
25
|
+
<RBACErrorSection title="Permissions" error={err('RBAC cache not available', 503)} />,
|
|
26
|
+
)
|
|
27
|
+
expect(html).toContain('RBAC visibility')
|
|
28
|
+
expect(html).not.toContain('text-red-400')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('renders a non-RBAC 503 (e.g. cluster disconnect) in red, not the calm RBAC note', () => {
|
|
32
|
+
const html = renderToString(
|
|
33
|
+
<RBACErrorSection title="Permissions" error={err('Not connected to cluster', 503)} />,
|
|
34
|
+
)
|
|
35
|
+
expect(html).toContain('text-red-400')
|
|
36
|
+
expect(html).not.toContain('RBAC visibility')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('renders 403 (viewer lacks permission) as a calm note, not a red error', () => {
|
|
40
|
+
const html = renderToString(
|
|
41
|
+
<RBACErrorSection title="Permissions" error={err('forbidden', 403)} />,
|
|
42
|
+
)
|
|
43
|
+
expect(html).toContain('permission to view RBAC bindings')
|
|
44
|
+
expect(html).not.toContain('text-red-400')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('renders genuine failures (500 / no status) in red with the prefix', () => {
|
|
48
|
+
const html = renderToString(
|
|
49
|
+
<RBACErrorSection title="Permissions" error={err('boom', 500)} />,
|
|
50
|
+
)
|
|
51
|
+
expect(html).toContain('text-red-400')
|
|
52
|
+
expect(html).toContain('Could not load permissions')
|
|
53
|
+
expect(html).toContain('boom')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('honors a custom errorPrefix for genuine failures', () => {
|
|
57
|
+
const html = renderToString(
|
|
58
|
+
<RBACErrorSection title="Bindings" error={err('boom')} errorPrefix="Could not load RBAC data" />,
|
|
59
|
+
)
|
|
60
|
+
expect(html).toContain('Could not load RBAC data')
|
|
61
|
+
})
|
|
62
|
+
})
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Shield } from 'lucide-react'
|
|
2
|
+
import type { ComponentType } from 'react'
|
|
3
|
+
import { Section } from '../../ui/drawer-components'
|
|
4
|
+
|
|
5
|
+
interface RBACErrorSectionProps {
|
|
6
|
+
title: string
|
|
7
|
+
error: Error
|
|
8
|
+
// Matches the icon of the section's success/loading state (Shield for most,
|
|
9
|
+
// Users for the Role bindings section) so the error state isn't jarring.
|
|
10
|
+
icon?: ComponentType<{ className?: string }>
|
|
11
|
+
// Prefix for the genuine-error line; copy differs slightly across renderers
|
|
12
|
+
// ("permissions" on Pod/Workload, "RBAC data" on ServiceAccount).
|
|
13
|
+
errorPrefix?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const errorStatus = (error: Error): number | undefined => (error as { status?: number }).status
|
|
17
|
+
|
|
18
|
+
// 503 because Radar's SA can't read RBAC, so the informers never synced — a
|
|
19
|
+
// cluster-static config state (same on every resource), not a failure. The message
|
|
20
|
+
// check distinguishes it from a generic connectivity 503 ("Not connected to
|
|
21
|
+
// cluster"), which is a real fault and must stay loud (red).
|
|
22
|
+
const isRBACCacheUnavailable = (error: Error): boolean =>
|
|
23
|
+
errorStatus(error) === 503 && error.message.includes('RBAC cache')
|
|
24
|
+
|
|
25
|
+
// 403 because the requesting user lacks list permission on bindings.
|
|
26
|
+
const isRBACForbidden = (error: Error): boolean => errorStatus(error) === 403
|
|
27
|
+
|
|
28
|
+
// True for the two expected, non-actionable RBAC states above. Surfaces that treat
|
|
29
|
+
// the RBAC section as a bonus (Pod/Workload Permissions) hide it entirely for these.
|
|
30
|
+
// Genuine faults — connectivity 503, 500, network errors — are deliberately NOT
|
|
31
|
+
// included, so they still surface rather than being silently dropped.
|
|
32
|
+
export function isRBACUnavailable(error: Error): boolean {
|
|
33
|
+
return isRBACForbidden(error) || isRBACCacheUnavailable(error)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// RBACErrorSection renders each expected state as a calm note (distinct copy per
|
|
37
|
+
// state) and reserves the red treatment for genuine failures. It shares the two
|
|
38
|
+
// sub-predicates with isRBACUnavailable so the "what counts as unavailable" rule
|
|
39
|
+
// has a single source of truth and can't drift.
|
|
40
|
+
export function RBACErrorSection({
|
|
41
|
+
title,
|
|
42
|
+
error,
|
|
43
|
+
icon = Shield,
|
|
44
|
+
errorPrefix = 'Could not load permissions',
|
|
45
|
+
}: RBACErrorSectionProps) {
|
|
46
|
+
if (isRBACCacheUnavailable(error)) {
|
|
47
|
+
return (
|
|
48
|
+
<Section title={title} icon={icon}>
|
|
49
|
+
<div className="text-sm text-theme-text-tertiary">
|
|
50
|
+
RBAC visibility isn’t available — the identity Radar connects with can’t read
|
|
51
|
+
RBAC resources in this cluster.
|
|
52
|
+
</div>
|
|
53
|
+
</Section>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
if (isRBACForbidden(error)) {
|
|
57
|
+
return (
|
|
58
|
+
<Section title={title} icon={icon}>
|
|
59
|
+
<div className="text-sm text-theme-text-tertiary">
|
|
60
|
+
You don’t have permission to view RBAC bindings here.
|
|
61
|
+
</div>
|
|
62
|
+
</Section>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
return (
|
|
66
|
+
<Section title={title} icon={icon}>
|
|
67
|
+
<div className="text-sm text-red-400">
|
|
68
|
+
{errorPrefix}: {error.message}
|
|
69
|
+
</div>
|
|
70
|
+
</Section>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
@@ -3,6 +3,7 @@ import { clsx } from 'clsx'
|
|
|
3
3
|
import { Section, PropertyList, Property, AlertBanner, ResourceLink } from '../../ui/drawer-components'
|
|
4
4
|
import type { RBACRoleResponse, RBACSubject, ResourceRef } from '../../../types'
|
|
5
5
|
import { rbacKindBadgeClass, rbacVerbBadgeClass } from '../../../utils/rbac-badges'
|
|
6
|
+
import { RBACErrorSection } from './RBACErrorSection'
|
|
6
7
|
|
|
7
8
|
interface RoleRendererProps {
|
|
8
9
|
data: any
|
|
@@ -216,13 +217,7 @@ function RoleBindingsSection({ rbacRoleData, loading, error, onNavigate }: RoleB
|
|
|
216
217
|
)
|
|
217
218
|
}
|
|
218
219
|
if (error) {
|
|
219
|
-
return
|
|
220
|
-
<Section title="Bindings" icon={Users}>
|
|
221
|
-
<div className="text-sm text-red-400">
|
|
222
|
-
Could not load bindings: {error.message}
|
|
223
|
-
</div>
|
|
224
|
-
</Section>
|
|
225
|
-
)
|
|
220
|
+
return <RBACErrorSection title="Bindings" error={error} icon={Users} errorPrefix="Could not load bindings" />
|
|
226
221
|
}
|
|
227
222
|
if (!rbacRoleData) return null
|
|
228
223
|
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
rbacKindBadgeClass,
|
|
17
17
|
} from '../../../utils/rbac-badges'
|
|
18
18
|
import { RBAC_BLAST_ESCALATION_VERBS } from '../../../utils/rbac-blast-radius'
|
|
19
|
+
import { RBACErrorSection } from './RBACErrorSection'
|
|
19
20
|
import { BADGE_SEVERITY_COLORS } from '../../ui/Badge'
|
|
20
21
|
|
|
21
22
|
interface ServiceAccountRendererProps {
|
|
@@ -194,13 +195,7 @@ function RBACSections({ rbacData, loading, error, onNavigate }: RBACSectionsProp
|
|
|
194
195
|
)
|
|
195
196
|
}
|
|
196
197
|
if (error) {
|
|
197
|
-
return
|
|
198
|
-
<Section title="Bindings" icon={Shield}>
|
|
199
|
-
<div className="text-sm text-red-400">
|
|
200
|
-
Could not load RBAC data: {error.message}
|
|
201
|
-
</div>
|
|
202
|
-
</Section>
|
|
203
|
-
)
|
|
198
|
+
return <RBACErrorSection title="Bindings" error={error} errorPrefix="Could not load RBAC data" />
|
|
204
199
|
}
|
|
205
200
|
if (!rbacData) return null
|
|
206
201
|
|
|
@@ -5,6 +5,7 @@ import { Section, PropertyList, Property, ConditionsSection, PodTemplateSection,
|
|
|
5
5
|
import { DialogPortal } from '../../ui/DialogPortal'
|
|
6
6
|
import type { RBACSubjectResponse, RBACPolicyRule } from '../../../types'
|
|
7
7
|
import { detectBlastRadius, rulePermissivenessScore } from '../../../utils/rbac-blast-radius'
|
|
8
|
+
import { RBACErrorSection, isRBACUnavailable } from './RBACErrorSection'
|
|
8
9
|
import {
|
|
9
10
|
rbacVerbBadgeClass,
|
|
10
11
|
rbacResourceBadgeClass,
|
|
@@ -379,11 +380,11 @@ function WorkloadPermissionsSection({
|
|
|
379
380
|
)
|
|
380
381
|
}
|
|
381
382
|
if (error) {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
383
|
+
// Permissions is a bonus section here; when RBAC is simply not available
|
|
384
|
+
// (cluster-static) or forbidden, hide it rather than repeat a note on every
|
|
385
|
+
// workload. Genuine faults still surface.
|
|
386
|
+
if (isRBACUnavailable(error)) return null
|
|
387
|
+
return <RBACErrorSection title={title} error={error} />
|
|
387
388
|
}
|
|
388
389
|
if (!rbacData) return null
|
|
389
390
|
|
|
@@ -91,10 +91,18 @@ export function ClusterName({ name, variant = 'inline', noBadge, fallbackBadge,
|
|
|
91
91
|
const showFallback = !noBadge && !hasProvider && fallbackBadge != null
|
|
92
92
|
const showRegion = parsed.region !== null && variant === 'stacked'
|
|
93
93
|
const collapsed = parsed.raw !== parsed.clusterName
|
|
94
|
-
// Tooltip
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
|
|
94
|
+
// The styled <Tooltip> wrapper is gated ONLY on `collapsed` — a stable,
|
|
95
|
+
// string-derived fact. Truncation must NOT gate the wrapper: wrapping
|
|
96
|
+
// changes the layout box MiddleEllipsis measures, so a truncated↔untruncated
|
|
97
|
+
// flip oscillates through its ResizeObserver (the exact feedback its
|
|
98
|
+
// onTruncatedChange note warns against). For the truncation case we disclose
|
|
99
|
+
// the full name via MiddleEllipsis's native `title` instead — an attribute,
|
|
100
|
+
// not a layout change — so the measurement can't feed back.
|
|
101
|
+
const showRawTooltip = !noTooltip && collapsed
|
|
102
|
+
// Don't also set a native title when the raw is already shown via the
|
|
103
|
+
// wrapper (collapsed) — that would double up. Only the non-collapsed
|
|
104
|
+
// truncation path uses it; there raw === clusterName.
|
|
105
|
+
const truncationTitle = !noTooltip && !collapsed && truncated ? parsed.clusterName : undefined
|
|
98
106
|
|
|
99
107
|
const body = (
|
|
100
108
|
<span className={['inline-flex items-center gap-1.5 min-w-0', className ?? ''].join(' ')}>
|
|
@@ -102,7 +110,7 @@ export function ClusterName({ name, variant = 'inline', noBadge, fallbackBadge,
|
|
|
102
110
|
{showFallback && fallbackBadge}
|
|
103
111
|
{variant === 'stacked' ? (
|
|
104
112
|
<span className="flex flex-col min-w-0 flex-1">
|
|
105
|
-
<MiddleEllipsis text={parsed.clusterName} onTruncatedChange={onTruncatedChange} />
|
|
113
|
+
<MiddleEllipsis text={parsed.clusterName} title={truncationTitle} onTruncatedChange={onTruncatedChange} />
|
|
106
114
|
{showRegion && (
|
|
107
115
|
<span className="text-[10px] text-theme-text-tertiary truncate">
|
|
108
116
|
{parsed.provider} · {parsed.region}
|
|
@@ -110,12 +118,12 @@ export function ClusterName({ name, variant = 'inline', noBadge, fallbackBadge,
|
|
|
110
118
|
)}
|
|
111
119
|
</span>
|
|
112
120
|
) : (
|
|
113
|
-
<MiddleEllipsis text={parsed.clusterName} onTruncatedChange={onTruncatedChange} />
|
|
121
|
+
<MiddleEllipsis text={parsed.clusterName} title={truncationTitle} onTruncatedChange={onTruncatedChange} />
|
|
114
122
|
)}
|
|
115
123
|
</span>
|
|
116
124
|
)
|
|
117
125
|
|
|
118
|
-
if (!
|
|
126
|
+
if (!showRawTooltip) return body
|
|
119
127
|
|
|
120
128
|
return (
|
|
121
129
|
<Tooltip content={parsed.raw} delay={250}>
|
package/src/index.ts
CHANGED
|
@@ -40,6 +40,11 @@ export * from './components/topology'
|
|
|
40
40
|
// Cluster audit (AuditCard, AuditAlerts, AuditFindingsTable)
|
|
41
41
|
export * from './components/audit'
|
|
42
42
|
|
|
43
|
+
// Checks remediation queue (ChecksView, shared types + severity vocabulary).
|
|
44
|
+
// Host-agnostic: Hub feeds fleet-resolved data, OSS can feed a single-cluster
|
|
45
|
+
// resolve.
|
|
46
|
+
export * from './components/checks'
|
|
47
|
+
|
|
43
48
|
// Cluster switcher (shared trigger+dropdown for OSS Radar and Radar Hub)
|
|
44
49
|
export * from './components/cluster-switcher'
|
|
45
50
|
|