@skyhook-io/radar-app 1.12.2 → 1.12.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.
Files changed (41) hide show
  1. package/package.json +2 -2
  2. package/src/App.tsx +34 -15
  3. package/src/api/client.images.test.ts +63 -0
  4. package/src/api/client.ts +208 -33
  5. package/src/api/client.yaml.test.ts +3 -3
  6. package/src/api/version-check.test.ts +78 -0
  7. package/src/components/CloudConnectFlow.tsx +46 -26
  8. package/src/components/CloudFunnelButton.tsx +166 -129
  9. package/src/components/ConnectionErrorView.test.tsx +21 -1
  10. package/src/components/ConnectionErrorView.tsx +7 -8
  11. package/src/components/applications/ApplicationsView.tsx +10 -9
  12. package/src/components/audit/AuditView.tsx +6 -3
  13. package/src/components/audit/UpgradeReadinessView.test.ts +26 -2
  14. package/src/components/audit/UpgradeReadinessView.tsx +19 -9
  15. package/src/components/diagnose/DiagnoseSurface.tsx +14 -10
  16. package/src/components/gitops/GitOpsView.tsx +8 -3
  17. package/src/components/helm/HelmReleaseDrawer.tsx +4 -3
  18. package/src/components/helm/OwnedResources.tsx +10 -2
  19. package/src/components/home/ClusterHealthCard.test.ts +31 -0
  20. package/src/components/home/ClusterHealthCard.tsx +60 -1
  21. package/src/components/home/HomeView.tsx +36 -11
  22. package/src/components/home/MCPSetupDialog.tsx +5 -4
  23. package/src/components/home/RadarVersionLine.test.tsx +145 -0
  24. package/src/components/home/RadarVersionLine.tsx +137 -0
  25. package/src/components/resources/PodFilesystemModal.tsx +54 -2
  26. package/src/components/resources/ResourcesView.tsx +31 -8
  27. package/src/components/resources/renderers/WorkloadRenderer.tsx +13 -5
  28. package/src/components/settings/SettingsDialog.tsx +21 -4
  29. package/src/components/ui/ErrorBoundary.test.tsx +55 -0
  30. package/src/components/ui/ErrorBoundary.tsx +17 -2
  31. package/src/components/ui/UpdateNotification.test.tsx +49 -0
  32. package/src/components/ui/UpdateNotification.tsx +6 -2
  33. package/src/components/workload/WorkloadView.test.ts +60 -0
  34. package/src/components/workload/WorkloadView.tsx +270 -29
  35. package/src/contexts/CapabilitiesContext.test.tsx +29 -0
  36. package/src/contexts/CapabilitiesContext.tsx +7 -3
  37. package/src/utils/navigation.test.ts +45 -0
  38. package/src/utils/navigation.ts +5 -5
  39. package/src/utils/topology-selection.ts +3 -2
  40. package/src/utils/version.test.ts +37 -0
  41. package/src/utils/version.ts +56 -0
@@ -3,20 +3,35 @@ import { AlertTriangle, RefreshCw } from 'lucide-react'
3
3
 
4
4
  interface Props {
5
5
  children: ReactNode
6
+ /**
7
+ * Clears a caught error whenever this value changes - pass whatever selects
8
+ * the children, typically the route. A boundary latches: nothing resets
9
+ * hasError on its own, so without this a view that throws keeps the fallback
10
+ * on screen and navigating away cannot escape it. Preferred over remounting
11
+ * via `key`, which would also discard the children's state on every change
12
+ * while nothing is wrong.
13
+ */
14
+ resetKey: string | number
6
15
  }
7
16
 
8
17
  interface State {
9
18
  hasError: boolean
10
19
  error: Error | null
20
+ resetKey: string | number
11
21
  }
12
22
 
13
23
  export class ErrorBoundary extends Component<Props, State> {
14
- state: State = { hasError: false, error: null }
24
+ state: State = { hasError: false, error: null, resetKey: this.props.resetKey }
15
25
 
16
- static getDerivedStateFromError(error: Error): State {
26
+ static getDerivedStateFromError(error: Error): Pick<State, 'hasError' | 'error'> {
17
27
  return { hasError: true, error }
18
28
  }
19
29
 
30
+ static getDerivedStateFromProps(props: Props, state: State): State | null {
31
+ if (Object.is(props.resetKey, state.resetKey)) return null
32
+ return { hasError: false, error: null, resetKey: props.resetKey }
33
+ }
34
+
20
35
  componentDidCatch(error: Error, info: React.ErrorInfo) {
21
36
  console.error('[ErrorBoundary]', error, info.componentStack)
22
37
  }
@@ -0,0 +1,49 @@
1
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
2
+ import { renderToString } from 'react-dom/server'
3
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
4
+
5
+ let deploymentMode = 'local'
6
+
7
+ vi.mock('../../api/client', () => ({
8
+ useCapabilities: () => ({ data: { deployment: { mode: deploymentMode } } }),
9
+ useVersionCheck: () => ({
10
+ data: {
11
+ currentVersion: '1.2.3',
12
+ latestVersion: '1.3.0',
13
+ updateAvailable: true,
14
+ installMethod: 'direct',
15
+ releaseUrl: 'https://github.com/skyhook-io/radar/releases/tag/v1.3.0',
16
+ },
17
+ }),
18
+ useStartDesktopUpdate: () => ({ mutate: vi.fn(), isPending: false }),
19
+ useDesktopUpdateStatus: () => ({ data: undefined }),
20
+ useApplyDesktopUpdate: () => ({ mutate: vi.fn() }),
21
+ }))
22
+
23
+ import { UpdateNotification } from './UpdateNotification'
24
+
25
+ function renderNotification() {
26
+ const client = new QueryClient()
27
+ return renderToString(
28
+ <QueryClientProvider client={client}>
29
+ <UpdateNotification />
30
+ </QueryClientProvider>,
31
+ )
32
+ }
33
+
34
+ describe('UpdateNotification', () => {
35
+ beforeEach(() => {
36
+ deploymentMode = 'local'
37
+ })
38
+
39
+ it('keeps the update popup for local installations', () => {
40
+ const html = renderNotification()
41
+ expect(html).toContain('Update Available')
42
+ expect(html).toContain('1.3.0')
43
+ })
44
+
45
+ it('suppresses the update popup for shared in-cluster viewers', () => {
46
+ deploymentMode = 'in-cluster'
47
+ expect(renderNotification()).toBe('')
48
+ })
49
+ })
@@ -3,6 +3,7 @@ import { Download, X, Copy, Check, RotateCw, ArrowDownToLine, Loader2 } from 'lu
3
3
  import { useQueryClient } from '@tanstack/react-query'
4
4
  import {
5
5
  useVersionCheck,
6
+ useCapabilities,
6
7
  useStartDesktopUpdate,
7
8
  useDesktopUpdateStatus,
8
9
  useApplyDesktopUpdate,
@@ -14,6 +15,8 @@ const DISMISSED_KEY = 'radar-update-dismissed'
14
15
 
15
16
  export function UpdateNotification() {
16
17
  const queryClient = useQueryClient()
18
+ const { data: capabilities } = useCapabilities()
19
+ const deploymentMode = capabilities ? (capabilities.deployment?.mode ?? 'local') : undefined
17
20
  const { data: versionInfo } = useVersionCheck()
18
21
  const [dismissed, setDismissed] = useState(false)
19
22
  const [copied, setCopied] = useState(false)
@@ -98,8 +101,9 @@ export function UpdateNotification() {
98
101
  })
99
102
  }
100
103
 
101
- // Don't show if no update available, dismissed, or error
102
- if (!versionInfo?.updateAvailable || dismissed) {
104
+ // Shared in-cluster viewers get a persistent Home notice instead of a
105
+ // floating action prompt they may not be able to act on.
106
+ if (!versionInfo?.updateAvailable || dismissed || deploymentMode === undefined || deploymentMode === 'in-cluster' || deploymentMode === 'cloud') {
103
107
  return null
104
108
  }
105
109
 
@@ -0,0 +1,60 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { gitOpsOwnerFromRelationships } from '@skyhook-io/k8s-ui'
4
+ import type { Relationships } from '../../types'
5
+ import { findInheritedGitOpsLookupRef } from './WorkloadView'
6
+
7
+ describe('findInheritedGitOpsLookupRef', () => {
8
+ it('follows a referenced ReplicaSet to its parent workload for inherited ownership', () => {
9
+ const relationships: Relationships = {
10
+ managedBy: [
11
+ {
12
+ kind: 'Deployment',
13
+ group: 'apps',
14
+ namespace: 'prod',
15
+ name: 'web',
16
+ },
17
+ ],
18
+ }
19
+
20
+ expect(
21
+ findInheritedGitOpsLookupRef(relationships, null, {
22
+ kind: 'ReplicaSet',
23
+ group: 'apps',
24
+ namespace: 'prod',
25
+ name: 'web-7b8d9',
26
+ }),
27
+ ).toEqual({
28
+ kind: 'Deployment',
29
+ group: 'apps',
30
+ namespace: 'prod',
31
+ name: 'web',
32
+ })
33
+ })
34
+
35
+ it('does not fetch a parent when the target has a direct GitOps owner', () => {
36
+ const relationships: Relationships = {
37
+ managedBy: [
38
+ {
39
+ kind: 'Application',
40
+ group: 'argoproj.io',
41
+ namespace: 'argocd',
42
+ name: 'web',
43
+ },
44
+ ],
45
+ }
46
+
47
+ expect(
48
+ findInheritedGitOpsLookupRef(
49
+ relationships,
50
+ gitOpsOwnerFromRelationships(relationships),
51
+ {
52
+ kind: 'Deployment',
53
+ group: 'apps',
54
+ namespace: 'prod',
55
+ name: 'web',
56
+ },
57
+ ),
58
+ ).toBeNull()
59
+ })
60
+ })