@skyhook-io/radar-app 1.12.2 → 1.13.0

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 (60) hide show
  1. package/package.json +6 -6
  2. package/src/App.tsx +35 -15
  3. package/src/api/client.images.test.ts +63 -0
  4. package/src/api/client.ts +246 -39
  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/cost/ApplicationCostTab.test.ts +45 -0
  16. package/src/components/cost/ApplicationCostTab.tsx +115 -64
  17. package/src/components/cost/CostTrendChart.test.ts +65 -0
  18. package/src/components/cost/CostTrendChart.tsx +107 -17
  19. package/src/components/cost/CostView.test.ts +36 -1
  20. package/src/components/cost/CostView.tsx +133 -51
  21. package/src/components/cost/CurrentAllocationUse.tsx +8 -4
  22. package/src/components/cost/WorkloadCostTab.test.ts +50 -0
  23. package/src/components/cost/WorkloadCostTab.tsx +109 -61
  24. package/src/components/cost/source.test.ts +33 -0
  25. package/src/components/cost/source.ts +100 -0
  26. package/src/components/diagnose/DiagnoseSurface.tsx +14 -10
  27. package/src/components/diagnose/parts.test.tsx +12 -4
  28. package/src/components/diagnose/parts.tsx +19 -15
  29. package/src/components/gitops/GitOpsView.tsx +8 -3
  30. package/src/components/helm/HelmReleaseDrawer.tsx +4 -3
  31. package/src/components/helm/OwnedResources.tsx +10 -2
  32. package/src/components/home/ClusterHealthCard.test.ts +31 -0
  33. package/src/components/home/ClusterHealthCard.tsx +60 -1
  34. package/src/components/home/CostCard.tsx +3 -2
  35. package/src/components/home/HomeView.tsx +36 -11
  36. package/src/components/home/MCPSetupDialog.tsx +5 -4
  37. package/src/components/home/RadarVersionLine.test.tsx +145 -0
  38. package/src/components/home/RadarVersionLine.tsx +137 -0
  39. package/src/components/resources/PodFilesystemModal.tsx +54 -2
  40. package/src/components/resources/ResourcesView.tsx +31 -8
  41. package/src/components/resources/renderers/WorkloadRenderer.tsx +13 -5
  42. package/src/components/rightsizing/RightsizingScanView.tsx +36 -12
  43. package/src/components/rightsizing/copy.test.ts +19 -0
  44. package/src/components/settings/SettingsDialog.tsx +687 -107
  45. package/src/components/settings/settings-state.test.ts +42 -0
  46. package/src/components/settings/settings-state.ts +39 -0
  47. package/src/components/ui/ErrorBoundary.test.tsx +55 -0
  48. package/src/components/ui/ErrorBoundary.tsx +17 -2
  49. package/src/components/ui/UpdateNotification.test.tsx +49 -0
  50. package/src/components/ui/UpdateNotification.tsx +6 -2
  51. package/src/components/workload/WorkloadView.test.ts +60 -0
  52. package/src/components/workload/WorkloadView.tsx +270 -29
  53. package/src/contexts/CapabilitiesContext.test.tsx +29 -0
  54. package/src/contexts/CapabilitiesContext.tsx +7 -3
  55. package/src/index.css +11 -1
  56. package/src/utils/navigation.test.ts +45 -0
  57. package/src/utils/navigation.ts +5 -5
  58. package/src/utils/topology-selection.ts +3 -2
  59. package/src/utils/version.test.ts +37 -0
  60. package/src/utils/version.ts +56 -0
@@ -1,5 +1,5 @@
1
1
  import { apiUrl, getAuthHeaders, getCredentialsMode } from '../api/config'
2
- import { apiVersionToGroup, kindToPlural } from '@skyhook-io/k8s-ui/utils/navigation'
2
+ import { apiVersionToGroup, kindToPluralWithGroup } from '@skyhook-io/k8s-ui/utils/navigation'
3
3
  import type { SelectedResource, Topology } from '@skyhook-io/k8s-ui/types/core'
4
4
  import type { SearchHit } from '../api/client'
5
5
 
@@ -14,7 +14,7 @@ export function searchHitToSelectedResource(hit: SearchHit): SelectedResource {
14
14
  }
15
15
 
16
16
  // Re-export shared navigation utilities from @skyhook-io/k8s-ui.
17
- export { kindToPlural, pluralToKind, refToSelectedResource, apiVersionToGroup } from '@skyhook-io/k8s-ui/utils/navigation'
17
+ export { kindToPlural, kindToPluralWithGroup, pluralToKind, refToSelectedResource, apiVersionToGroup } from '@skyhook-io/k8s-ui/utils/navigation'
18
18
  export type { NavigateToResource } from '@skyhook-io/k8s-ui/utils/navigation'
19
19
 
20
20
  const NETWORK_POLICY_TOPOLOGY_KINDS = new Set([
@@ -48,7 +48,7 @@ export function getNetworkPolicyResourceTarget(topology: Topology | null): { kin
48
48
 
49
49
  const group = networkPolicyGroup(node)
50
50
  const target = {
51
- kind: kindToPlural(node.kind),
51
+ kind: kindToPluralWithGroup(node.kind, group ?? ''),
52
52
  ...(group ? { group } : {}),
53
53
  }
54
54
  targets.set(`${target.kind}\u0000${target.group ?? ''}`, target)
@@ -96,13 +96,13 @@ export function resourcePath(resource: SelectedResource): string {
96
96
  }
97
97
  if (resource.group) params.set('apiGroup', resource.group)
98
98
  const query = params.toString()
99
- return `/resources/${kindToPlural(resource.kind)}${query ? `?${query}` : ''}`
99
+ return `/resources/${kindToPluralWithGroup(resource.kind, resource.group ?? '')}${query ? `?${query}` : ''}`
100
100
  }
101
101
 
102
102
  const FULLSCREEN_RESOURCE_KINDS = new Set(['pods', 'deployments', 'statefulsets', 'daemonsets', 'jobs', 'cronjobs', 'nodes'])
103
103
 
104
104
  export function relatedResourcePath(resource: SelectedResource): string {
105
- const apiKind = kindToPlural(resource.kind).toLowerCase()
105
+ const apiKind = kindToPluralWithGroup(resource.kind, resource.group ?? '').toLowerCase()
106
106
  if (FULLSCREEN_RESOURCE_KINDS.has(apiKind)) {
107
107
  return buildWorkloadPath({ ...resource, kind: apiKind })
108
108
  }
@@ -1,5 +1,5 @@
1
1
  import type { SelectedResource, Topology, TopologyNode } from '@skyhook-io/k8s-ui/types/core'
2
- import { apiVersionToGroup, kindToPlural } from '@skyhook-io/k8s-ui/utils/navigation'
2
+ import { apiVersionToGroup, kindToPluralWithGroup } from '@skyhook-io/k8s-ui/utils/navigation'
3
3
 
4
4
  function topologyNodeGroup(node: TopologyNode): string | undefined {
5
5
  const apiVersionGroup = apiVersionToGroup(node.data.apiVersion as string | undefined)
@@ -18,10 +18,11 @@ export function findSelectedTopologyNode(
18
18
  selectedResource: SelectedResource,
19
19
  ): TopologyNode | undefined {
20
20
  const namespace = selectedResource.namespace || ''
21
+ const selectedKind = kindToPluralWithGroup(selectedResource.kind, selectedResource.group ?? '')
21
22
  const candidates = topology?.nodes.filter(node =>
22
23
  ((node.data.namespace as string) || '') === namespace &&
23
24
  node.name === selectedResource.name &&
24
- (kindToPlural(node.kind) === selectedResource.kind || node.kind === selectedResource.kind),
25
+ (kindToPluralWithGroup(node.kind, topologyNodeGroup(node) ?? '') === selectedKind || node.kind === selectedResource.kind),
25
26
  ) ?? []
26
27
 
27
28
  if (candidates.length === 0) return undefined
@@ -0,0 +1,37 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ getVersionUpdateStatus,
4
+ IN_CLUSTER_UPGRADE_URL,
5
+ parseMajorMinor,
6
+ versionUpdateURL,
7
+ } from './version'
8
+
9
+ describe('Radar version helpers', () => {
10
+ it('parses major and minor versions with an optional v prefix', () => {
11
+ expect(parseMajorMinor('v1.12.3')).toEqual({ major: 1, minor: 12 })
12
+ expect(parseMajorMinor('dev')).toBeNull()
13
+ })
14
+
15
+ it('grades available updates by distance from the latest release', () => {
16
+ expect(getVersionUpdateStatus('1.12.0', '1.12.1')).toEqual({ tier: 'patch' })
17
+ expect(getVersionUpdateStatus('1.11.4', '1.12.1')).toEqual({ tier: 'minor', minorVersionsBehind: 1 })
18
+ expect(getVersionUpdateStatus('1.10.4', '1.12.1')).toEqual({ tier: 'minor', minorVersionsBehind: 2 })
19
+ expect(getVersionUpdateStatus('1.9.4', '1.12.1')).toEqual({ tier: 'stale', minorVersionsBehind: 3 })
20
+ expect(getVersionUpdateStatus('0.12.4', '1.12.1')).toEqual({ tier: 'stale', majorVersionBehind: true })
21
+ expect(getVersionUpdateStatus('1.12.1-rc.1', '1.12.1')).toEqual({ tier: 'patch' })
22
+ })
23
+
24
+ it('does not flag invalid, current, or newer local builds', () => {
25
+ expect(getVersionUpdateStatus('dev', '1.12.1')).toEqual({ tier: 'none' })
26
+ expect(getVersionUpdateStatus('1.12.1', '1.12.1')).toEqual({ tier: 'none' })
27
+ expect(getVersionUpdateStatus('1.13.0', '1.12.1')).toEqual({ tier: 'none' })
28
+ expect(getVersionUpdateStatus('2.0.0', '1.12.1')).toEqual({ tier: 'none' })
29
+ })
30
+
31
+ it('routes in-cluster upgrades to instructions instead of release notes', () => {
32
+ const releaseURL = 'https://github.com/skyhook-io/radar/releases/tag/v1.2.4'
33
+ expect(versionUpdateURL('in-cluster', releaseURL)).toBe(IN_CLUSTER_UPGRADE_URL)
34
+ expect(versionUpdateURL('local', releaseURL)).toBe(releaseURL)
35
+ expect(versionUpdateURL('cloud', releaseURL)).toBeUndefined()
36
+ })
37
+ })
@@ -0,0 +1,56 @@
1
+ import type { DeploymentMode } from '../types'
2
+
3
+ export interface MajorMinorVersion {
4
+ major: number
5
+ minor: number
6
+ }
7
+
8
+ export type VersionUpdateTier = 'none' | 'patch' | 'minor' | 'stale'
9
+
10
+ export interface VersionUpdateStatus {
11
+ tier: VersionUpdateTier
12
+ minorVersionsBehind?: number
13
+ majorVersionBehind?: boolean
14
+ }
15
+
16
+ export const IN_CLUSTER_UPGRADE_URL = 'https://radarhq.io/docs/configuration/in-cluster'
17
+
18
+ export function versionUpdateURL(deploymentMode: DeploymentMode, releaseURL?: string): string | undefined {
19
+ if (deploymentMode === 'cloud') return undefined
20
+ return deploymentMode === 'in-cluster' ? IN_CLUSTER_UPGRADE_URL : releaseURL
21
+ }
22
+
23
+ export function parseMajorMinor(version: string): MajorMinorVersion | null {
24
+ const match = /^v?(\d+)\.(\d+)/.exec(version.trim())
25
+ if (!match) return null
26
+ return { major: Number(match[1]), minor: Number(match[2]) }
27
+ }
28
+
29
+ function parseVersion(version: string): [major: number, minor: number, patch: number, prerelease: boolean] | null {
30
+ const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(version.trim())
31
+ if (!match) return null
32
+ return [Number(match[1]), Number(match[2]), Number(match[3]), !!match[4]]
33
+ }
34
+
35
+ export function getVersionUpdateStatus(current: string, latest?: string): VersionUpdateStatus {
36
+ if (!latest) return { tier: 'none' }
37
+
38
+ const currentVersion = parseVersion(current)
39
+ const latestVersion = parseVersion(latest)
40
+ if (!currentVersion || !latestVersion) return { tier: 'none' }
41
+
42
+ const [currentMajor, currentMinor, currentPatch, currentPrerelease] = currentVersion
43
+ const [latestMajor, latestMinor, latestPatch, latestPrerelease] = latestVersion
44
+
45
+ if (latestMajor < currentMajor) return { tier: 'none' }
46
+ if (latestMajor > currentMajor) return { tier: 'stale', majorVersionBehind: true }
47
+
48
+ const minorVersionsBehind = latestMinor - currentMinor
49
+ if (minorVersionsBehind >= 3) return { tier: 'stale', minorVersionsBehind }
50
+ if (minorVersionsBehind > 0) return { tier: 'minor', minorVersionsBehind }
51
+ if (minorVersionsBehind < 0 || latestPatch < currentPatch) return { tier: 'none' }
52
+ if (latestPatch === currentPatch) {
53
+ return currentPrerelease && !latestPrerelease ? { tier: 'patch' } : { tier: 'none' }
54
+ }
55
+ return { tier: 'patch' }
56
+ }