@skyhook-io/radar-app 0.1.6 → 0.1.9
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 +6 -6
- package/src/api/client.ts +102 -10
- package/src/components/ContextSwitcher.tsx +98 -357
- package/src/components/audit/AuditView.tsx +3 -10
- package/src/components/cost/CostView.tsx +2 -8
- package/src/components/helm/ChartBrowser.tsx +16 -6
- package/src/components/helm/HelmReleaseDrawer.tsx +53 -36
- package/src/components/helm/HelmView.tsx +2 -3
- package/src/components/helm/InstallWizard.tsx +5 -7
- package/src/components/helm/ManifestDiffViewer.tsx +2 -5
- package/src/components/helm/ManifestViewer.tsx +2 -5
- package/src/components/helm/RoleGatedPanel.tsx +47 -0
- package/src/components/helm/ValuesViewer.tsx +5 -8
- package/src/components/home/HelmSummary.tsx +12 -0
- package/src/components/home/HomeView.tsx +3 -10
- package/src/components/resources/ImageFilesystemModal.tsx +6 -5
- package/src/components/resources/PodFilesystemModal.tsx +2 -6
- package/src/components/timeline/TimelineSwimlanes.tsx +2 -7
- package/src/components/traffic/TrafficView.tsx +5 -7
- package/src/components/traffic/TrafficWizard.tsx +7 -12
- package/src/index.ts +6 -0
package/package.json
CHANGED
package/src/App.tsx
CHANGED
|
@@ -37,7 +37,7 @@ import { useNamespaces, useSwitchContext, useAuthMe } from './api/client'
|
|
|
37
37
|
import { routePath, apiUrl, getAuthHeaders, getCredentialsMode } from './api/config'
|
|
38
38
|
import { KeyboardShortcutProvider, useRegisterShortcut, useRegisterShortcuts } from './hooks/useKeyboardShortcuts'
|
|
39
39
|
import { useAnimatedUnmount } from './hooks/useAnimatedUnmount'
|
|
40
|
-
import
|
|
40
|
+
import radarLoadingIcon from '@skyhook-io/k8s-ui/assets/radar/radar-icon-loading.svg'
|
|
41
41
|
import { RefreshCw, Network, List, Clock, Package, Sun, Moon, Activity, Home, Star, Search, Bug, Settings, SquareTerminal, ShieldCheck } from 'lucide-react'
|
|
42
42
|
import { useTheme } from './context/ThemeContext'
|
|
43
43
|
import { Tooltip } from './components/ui/Tooltip'
|
|
@@ -141,8 +141,8 @@ function AuthBarrier({ authMode }: { authMode: string }) {
|
|
|
141
141
|
return (
|
|
142
142
|
<div className="flex-1 flex items-center justify-center bg-theme-base">
|
|
143
143
|
<div className="flex flex-col items-center gap-4">
|
|
144
|
-
<
|
|
145
|
-
<p className="text-sm text-theme-text-secondary">Redirecting to login
|
|
144
|
+
<img src={radarLoadingIcon} alt="" aria-hidden className="w-11 h-11" />
|
|
145
|
+
<p className="text-sm text-theme-text-secondary">Redirecting to login…</p>
|
|
146
146
|
</div>
|
|
147
147
|
</div>
|
|
148
148
|
)
|
|
@@ -942,10 +942,10 @@ function AppInner() {
|
|
|
942
942
|
{!isSwitching && !(authMe?.authEnabled && !authMe?.username) && connection.state === 'connecting' && (
|
|
943
943
|
<div className="flex-1 flex items-center justify-center bg-theme-base">
|
|
944
944
|
<div className="flex flex-col items-center gap-4 text-theme-text-secondary">
|
|
945
|
-
<
|
|
945
|
+
<img src={radarLoadingIcon} alt="" aria-hidden className="w-11 h-11" />
|
|
946
946
|
<div className="text-center">
|
|
947
947
|
<p className="font-medium text-theme-text-primary">Connecting to cluster</p>
|
|
948
|
-
<p className="text-sm text-theme-text-secondary mt-1">{connection.context || 'Loading
|
|
948
|
+
<p className="text-sm text-theme-text-secondary mt-1">{connection.context || 'Loading…'}</p>
|
|
949
949
|
{connection.progressMessage && (
|
|
950
950
|
<p className="text-xs text-theme-text-tertiary animate-pulse mt-3">
|
|
951
951
|
{connection.progressMessage}
|
|
@@ -960,7 +960,7 @@ function AppInner() {
|
|
|
960
960
|
{isSwitching && (
|
|
961
961
|
<div className="flex-1 flex items-center justify-center bg-theme-base">
|
|
962
962
|
<div className="flex flex-col items-center gap-4 text-theme-text-secondary">
|
|
963
|
-
<
|
|
963
|
+
<img src={radarLoadingIcon} alt="" aria-hidden className="w-11 h-11" />
|
|
964
964
|
<div className="text-center">
|
|
965
965
|
<div className="text-sm font-medium text-theme-text-primary">Switching context</div>
|
|
966
966
|
{targetContext && (
|
package/src/api/client.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useQuery, useMutation, useQueryClient, skipToken } from '@tanstack/react-query'
|
|
2
2
|
import { showApiError, showApiSuccess } from '../components/ui/Toast'
|
|
3
|
+
import { useCanHelmWrite } from '../contexts/CapabilitiesContext'
|
|
3
4
|
import type {
|
|
4
5
|
Topology,
|
|
5
6
|
ClusterInfo,
|
|
@@ -212,7 +213,13 @@ export interface DashboardHelmRelease {
|
|
|
212
213
|
export interface DashboardHelmSummary {
|
|
213
214
|
total: number
|
|
214
215
|
releases: DashboardHelmRelease[]
|
|
215
|
-
restricted?: boolean // True when user lacks permissions to list Helm releases
|
|
216
|
+
restricted?: boolean // True when user lacks permissions to list Helm releases (RBAC-denied)
|
|
217
|
+
// error + errorCode populated when the Helm read failed for a non-RBAC
|
|
218
|
+
// reason (client not initialized, unconfigured, network). Surfaced
|
|
219
|
+
// via the dashboard widget so empty results aren't mistaken for
|
|
220
|
+
// "this cluster has zero releases."
|
|
221
|
+
error?: string
|
|
222
|
+
errorCode?: string
|
|
216
223
|
}
|
|
217
224
|
|
|
218
225
|
export interface DashboardCRDCount {
|
|
@@ -264,6 +271,7 @@ export interface DashboardResponse {
|
|
|
264
271
|
audit: DashboardAudit | null
|
|
265
272
|
nodeVersionSkew: { versions: Record<string, string[]>; minVersion: string; maxVersion: string } | null
|
|
266
273
|
deferredLoading?: boolean // True while deferred informers (secrets, events, etc.) are still syncing
|
|
274
|
+
partialData?: string[] // Resource kinds still loading after first paint (slow-cluster fallback)
|
|
267
275
|
accessRestricted?: boolean // True when user has no namespace access (RBAC)
|
|
268
276
|
}
|
|
269
277
|
|
|
@@ -654,11 +662,16 @@ export function useNamespaceCapabilities(namespace: string | undefined, globalCa
|
|
|
654
662
|
}
|
|
655
663
|
|
|
656
664
|
// Auth
|
|
665
|
+
export type CloudRole = 'owner' | 'member' | 'viewer'
|
|
666
|
+
|
|
657
667
|
export interface AuthMe {
|
|
658
668
|
authEnabled: boolean
|
|
659
669
|
authMode?: string
|
|
660
670
|
username?: string
|
|
661
671
|
groups?: string[]
|
|
672
|
+
/** Pre-computed Cloud tier from `cloud:<tier>` group prefix.
|
|
673
|
+
* Absent when not running under Cloud (OSS, OIDC, no role group). */
|
|
674
|
+
cloudRole?: CloudRole
|
|
662
675
|
}
|
|
663
676
|
|
|
664
677
|
export function useAuthMe() {
|
|
@@ -669,6 +682,81 @@ export function useAuthMe() {
|
|
|
669
682
|
})
|
|
670
683
|
}
|
|
671
684
|
|
|
685
|
+
// Tier ordering for Cloud-role gates. Mirrors radar OSS pkg/auth
|
|
686
|
+
// CloudRole.AtLeast — the SPA must agree with the backend on what
|
|
687
|
+
// "member-or-higher" means; otherwise we'd hide a button the
|
|
688
|
+
// backend would happily honor (or vice versa).
|
|
689
|
+
const CLOUD_ROLE_RANK: Record<string, number> = { viewer: 1, member: 2, owner: 3 }
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* useCloudRole returns the caller's Cloud tier (`owner` / `member` /
|
|
693
|
+
* `viewer`) and a `canAtLeast(min)` gate. When no Cloud role is
|
|
694
|
+
* present (OSS, OIDC, no role group, OR auth/me is still loading),
|
|
695
|
+
* `canAtLeast` returns true — the gate is strictly additive for
|
|
696
|
+
* Cloud-attributed users, mirroring the backend's `requireCloudRole`
|
|
697
|
+
* semantics. Use for passive content gating (panels, sections); use
|
|
698
|
+
* `useCanHelmAct` (or similar) for *click-prone* surfaces where you
|
|
699
|
+
* need fail-closed behavior during the auth/me round-trip to prevent
|
|
700
|
+
* a viewer from clicking through during the loading window.
|
|
701
|
+
*
|
|
702
|
+
* Why optimistic during load: the gated empty state ("Your role can't
|
|
703
|
+
* view…") rendered briefly to OSS / kubectl-plugin users before
|
|
704
|
+
* auth/me resolves is a worse regression than a Cloud viewer seeing
|
|
705
|
+
* a content tab populate for a tick before being gated out. Click-
|
|
706
|
+
* prevention belongs in the action-button hook, not here.
|
|
707
|
+
*/
|
|
708
|
+
export function useCloudRole() {
|
|
709
|
+
const { data, isLoading } = useAuthMe()
|
|
710
|
+
const role = data?.cloudRole
|
|
711
|
+
return {
|
|
712
|
+
role,
|
|
713
|
+
isLoading,
|
|
714
|
+
isCloudUser: !!role,
|
|
715
|
+
canAtLeast: (min: CloudRole) => {
|
|
716
|
+
if (!role) return true // not Cloud-attributed (incl. still-loading) → no gate
|
|
717
|
+
return (CLOUD_ROLE_RANK[role] ?? 0) >= (CLOUD_ROLE_RANK[min] ?? 0)
|
|
718
|
+
},
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* useCanHelmAct combines the K8s capability gate (rbac.helm=true) and
|
|
724
|
+
* the Cloud role gate (member+) into a single answer for any Helm
|
|
725
|
+
* write or sensitive-read button. Returns { allowed, reason } so the
|
|
726
|
+
* tooltip can explain which gate failed.
|
|
727
|
+
*
|
|
728
|
+
* Cloud role check runs FIRST so the message is actionable for Cloud
|
|
729
|
+
* users — telling them "Helm write permissions required" is wrong if
|
|
730
|
+
* the chart is fine and the actual gate is their viewer role.
|
|
731
|
+
*/
|
|
732
|
+
export function useCanHelmAct(): { allowed: boolean; reason?: string } {
|
|
733
|
+
const helmWrite = useCanHelmWrite()
|
|
734
|
+
const { role, canAtLeast, isLoading } = useCloudRole()
|
|
735
|
+
// Fail-closed for action buttons during the auth/me round-trip:
|
|
736
|
+
// a Cloud viewer who clicks during loading would otherwise fire a
|
|
737
|
+
// real request that gets 403'd. For OSS / kubectl-plugin the
|
|
738
|
+
// round-trip is sub-ms so this is imperceptible; for Cloud it
|
|
739
|
+
// prevents the click-through window. Distinct from useCloudRole's
|
|
740
|
+
// canAtLeast (which is optimistic during loading) because passive
|
|
741
|
+
// content gates don't have a click-handler to misfire.
|
|
742
|
+
if (isLoading) {
|
|
743
|
+
return { allowed: false, reason: 'Loading permissions…' }
|
|
744
|
+
}
|
|
745
|
+
if (!canAtLeast('member')) {
|
|
746
|
+
return {
|
|
747
|
+
allowed: false,
|
|
748
|
+
reason: `Your Radar Cloud role (${role ?? 'unknown'}) cannot run Helm operations. Ask a member or owner.`,
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
if (!helmWrite) {
|
|
752
|
+
return {
|
|
753
|
+
allowed: false,
|
|
754
|
+
reason: 'Helm write permissions required. Set rbac.helm=true in the Radar Helm chart values.',
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
return { allowed: true }
|
|
758
|
+
}
|
|
759
|
+
|
|
672
760
|
// Namespaces
|
|
673
761
|
export function useNamespaces() {
|
|
674
762
|
return useQuery<Namespace[]>({
|
|
@@ -1652,8 +1740,11 @@ export function useHelmRelease(namespace: string, name: string) {
|
|
|
1652
1740
|
})
|
|
1653
1741
|
}
|
|
1654
1742
|
|
|
1655
|
-
// Get manifest for a Helm release (optionally at a specific revision)
|
|
1656
|
-
|
|
1743
|
+
// Get manifest for a Helm release (optionally at a specific revision).
|
|
1744
|
+
// `enabled` lets callers skip the query when the user's Cloud role
|
|
1745
|
+
// would 403 the read — saves a round-trip and avoids a transient
|
|
1746
|
+
// "error" state that the role-gated empty panel doesn't need.
|
|
1747
|
+
export function useHelmManifest(namespace: string, name: string, revision?: number, enabled = true) {
|
|
1657
1748
|
const params = revision ? `?revision=${revision}` : ''
|
|
1658
1749
|
return useQuery<string>({
|
|
1659
1750
|
queryKey: ['helm-manifest', namespace, name, revision],
|
|
@@ -1665,34 +1756,35 @@ export function useHelmManifest(namespace: string, name: string, revision?: numb
|
|
|
1665
1756
|
}
|
|
1666
1757
|
return response.text()
|
|
1667
1758
|
},
|
|
1668
|
-
enabled: Boolean(namespace && name),
|
|
1759
|
+
enabled: Boolean(namespace && name && enabled),
|
|
1669
1760
|
staleTime: 60000, // 1 minute
|
|
1670
1761
|
})
|
|
1671
1762
|
}
|
|
1672
1763
|
|
|
1673
|
-
// Get values for a Helm release
|
|
1674
|
-
export function useHelmValues(namespace: string, name: string, allValues?: boolean) {
|
|
1764
|
+
// Get values for a Helm release. `enabled` see useHelmManifest.
|
|
1765
|
+
export function useHelmValues(namespace: string, name: string, allValues?: boolean, enabled = true) {
|
|
1675
1766
|
const params = allValues ? '?all=true' : ''
|
|
1676
1767
|
return useQuery<HelmValues>({
|
|
1677
1768
|
queryKey: ['helm-values', namespace, name, allValues],
|
|
1678
1769
|
queryFn: () => fetchJSON(`/helm/releases/${namespace}/${name}/values${params}`),
|
|
1679
|
-
enabled: Boolean(namespace && name),
|
|
1770
|
+
enabled: Boolean(namespace && name && enabled),
|
|
1680
1771
|
staleTime: 60000,
|
|
1681
1772
|
})
|
|
1682
1773
|
}
|
|
1683
1774
|
|
|
1684
|
-
// Get diff between two revisions
|
|
1775
|
+
// Get diff between two revisions. `enabled` see useHelmManifest.
|
|
1685
1776
|
export function useHelmManifestDiff(
|
|
1686
1777
|
namespace: string,
|
|
1687
1778
|
name: string,
|
|
1688
1779
|
revision1: number,
|
|
1689
|
-
revision2: number
|
|
1780
|
+
revision2: number,
|
|
1781
|
+
enabled = true,
|
|
1690
1782
|
) {
|
|
1691
1783
|
return useQuery<ManifestDiff>({
|
|
1692
1784
|
queryKey: ['helm-diff', namespace, name, revision1, revision2],
|
|
1693
1785
|
queryFn: () =>
|
|
1694
1786
|
fetchJSON(`/helm/releases/${namespace}/${name}/diff?revision1=${revision1}&revision2=${revision2}`),
|
|
1695
|
-
enabled: Boolean(namespace && name && revision1 > 0 && revision2 > 0 && revision1 !== revision2),
|
|
1787
|
+
enabled: Boolean(namespace && name && revision1 > 0 && revision2 > 0 && revision1 !== revision2 && enabled),
|
|
1696
1788
|
staleTime: 60000,
|
|
1697
1789
|
})
|
|
1698
1790
|
}
|