@skyhook-io/radar-app 1.14.6 → 1.15.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.
- package/package.json +1 -1
- package/src/App.tsx +51 -6
- package/src/api/client.admission.test.ts +34 -0
- package/src/api/client.ts +37 -3
- package/src/api/usage-data.fixtures.ts +33 -0
- package/src/api/usage-data.ts +165 -0
- package/src/components/CloudFunnelButton.test.tsx +2 -2
- package/src/components/CloudFunnelButton.tsx +99 -55
- package/src/components/SelfManagedStart.tsx +96 -0
- package/src/components/cloudConnectHandoff.test.ts +1 -1
- package/src/components/cloudConnectHandoff.ts +1 -1
- package/src/components/diagnose/ActivityTurn.tsx +3 -2
- package/src/components/diagnose/ApplyDialog.tsx +4 -2
- package/src/components/dialog-names.test.ts +69 -0
- package/src/components/execution/BatchExecutionView.tsx +1 -1
- package/src/components/execution/JobSetAdmission.test.tsx +160 -37
- package/src/components/execution/JobSetAdmission.tsx +16 -7
- package/src/components/helm/TrackChartSourceDialog.tsx +5 -3
- package/src/components/home/HomeView.tsx +6 -2
- package/src/components/home/RadarVersionLine.test.tsx +14 -0
- package/src/components/home/RadarVersionLine.tsx +28 -3
- package/src/components/nav/PrimaryNavRail.test.tsx +10 -1
- package/src/components/nav/PrimaryNavRail.tsx +25 -3
- package/src/components/resources/renderers/JobAdmissionRenderers.test.tsx +18 -0
- package/src/components/resources/renderers/JobAdmissionRenderers.tsx +18 -0
- package/src/components/resources/renderers/RayJobRenderer.test.tsx +40 -0
- package/src/components/resources/renderers/RayJobRenderer.tsx +45 -0
- package/src/components/settings/PrivacySection.test.tsx +84 -0
- package/src/components/settings/PrivacySection.tsx +140 -0
- package/src/components/settings/SettingsDialog.tsx +13 -52
- package/src/components/settings/controls.tsx +55 -0
- package/src/components/settings/settings-state.ts +1 -0
- package/src/components/ui/ErrorBoundary.tsx +5 -0
- package/src/components/ui/Omnibar.tsx +16 -0
- package/src/components/ui/UpdateNotification.tsx +13 -9
- package/src/components/ui/command-items.ts +14 -0
- package/src/components/usage-data/UsageDataAsk.test.tsx +41 -0
- package/src/components/usage-data/UsageDataAsk.tsx +106 -0
- package/src/components/usage-data/UsageDataPrompt.test.ts +53 -0
- package/src/components/usage-data/UsageDataPrompt.tsx +148 -0
- package/src/components/whats-new/WhatsNew.test.ts +110 -0
- package/src/components/whats-new/WhatsNew.tsx +423 -0
- package/src/components/whats-new/WhatsNewDialog.test.tsx +229 -0
- package/src/components/whats-new/check-whats-new.test.ts +65 -0
- package/src/components/whats-new/releaseNotes.ts +121 -0
- package/src/components/workload/WorkloadView.tsx +11 -1
- package/src/k8s-ui-exports.test.ts +73 -0
- package/src/utils/navigation.test.ts +17 -1
- package/src/utils/navigation.ts +16 -0
- package/src/utils/version.ts +11 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
import { mkdtempSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { describe, expect, it } from 'vitest'
|
|
6
|
+
|
|
7
|
+
// scripts/check-whats-new.sh gates every minor release on having notes; these
|
|
8
|
+
// pin what counts as an entry, against catalogs written to a temp dir.
|
|
9
|
+
const script = join(__dirname, '..', '..', '..', '..', 'scripts', 'check-whats-new.sh')
|
|
10
|
+
|
|
11
|
+
function check(version: string, catalog: string) {
|
|
12
|
+
const file = join(mkdtempSync(join(tmpdir(), 'whats-new-')), 'releaseNotes.ts')
|
|
13
|
+
writeFileSync(file, catalog)
|
|
14
|
+
return spawnSync('bash', [script, version], { env: { ...process.env, WHATS_NEW_CATALOG: file }, encoding: 'utf8' }).status
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const empty = `export const RELEASE_NOTES: ReleaseNotes[] = []\n`
|
|
18
|
+
const withEntry = (entry: string, before = '') => `${before}export const RELEASE_NOTES: ReleaseNotes[] = [\n${entry}\n]\n\nexport function after() {}\n`
|
|
19
|
+
|
|
20
|
+
describe('check-whats-new.sh', () => {
|
|
21
|
+
it('lets patches and prereleases through without notes', () => {
|
|
22
|
+
expect(check('v1.15.1', empty)).toBe(0)
|
|
23
|
+
expect(check('v1.15.0-rc.1', empty)).toBe(0)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('refuses a minor or major release without notes, including one with build metadata', () => {
|
|
27
|
+
expect(check('v1.15.0', empty)).toBe(1)
|
|
28
|
+
expect(check('v2.0.0', empty)).toBe(1)
|
|
29
|
+
expect(check('v1.15.0+build.1', empty)).toBe(1)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('refuses a version that is not vX.Y.Z', () => {
|
|
33
|
+
expect(check('v1.15', empty)).toBe(1)
|
|
34
|
+
expect(check('latest', empty)).toBe(1)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('accepts a real entry, wherever version sits in it', () => {
|
|
38
|
+
const entry = ` {\n releaseUrl: 'https://github.com/skyhook-io/radar/releases/tag/v1.15.0', version: 'v1.15.0',\n highlights: [],\n },`
|
|
39
|
+
expect(check('v1.15.0', withEntry(entry))).toBe(0)
|
|
40
|
+
expect(check('1.15.0+build.1', withEntry(entry))).toBe(0)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('does not count comments, objects outside the catalog, or look-alike properties', () => {
|
|
44
|
+
const decoys = `// { version: 'v1.16.0' },\n/* { version: 'v1.17.0' } */\nconst draft = { version: 'v1.18.0' }\n`
|
|
45
|
+
const catalog = withEntry(` // { version: 'v1.20.0' },\n { notversion: 'v1.19.0' },`, decoys)
|
|
46
|
+
for (const v of ['v1.16.0', 'v1.17.0', 'v1.18.0', 'v1.19.0', 'v1.20.0']) expect(check(v, catalog), v).toBe(1)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('ignores an object after an empty catalog, even with a semicolon', () => {
|
|
50
|
+
expect(check('v1.15.0', `export const RELEASE_NOTES: ReleaseNotes[] = [];\nconst draft = { version: 'v1.15.0' };\n`)).toBe(1)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('does not count the words "version:" inside another entry\'s text', () => {
|
|
54
|
+
const entry = ` {\n version: 'v1.14.0',\n improvements: ["Set version: 'v1.15.0' in your configuration"],\n },`
|
|
55
|
+
expect(check('v1.15.0', withEntry(entry))).toBe(1)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('accepts a value on the line after its key, quoted keys, and space before the colon', () => {
|
|
59
|
+
expect(check('v1.15.0', withEntry(` {\n version:\n 'v1.15.0',\n },`))).toBe(0)
|
|
60
|
+
expect(check('v1.15.0', withEntry(` { version: "v1.15.0" },`))).toBe(0)
|
|
61
|
+
expect(check('v1.15.0', withEntry(` { "version": "v1.15.0" },`))).toBe(0)
|
|
62
|
+
expect(check('v1.15.0', withEntry(` { version : 'v1.15.0' },`))).toBe(0)
|
|
63
|
+
expect(check('v1.15.0', withEntry(` {\n "version" :\n "v1.15.0",\n },`))).toBe(0)
|
|
64
|
+
})
|
|
65
|
+
})
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { LucideIcon } from 'lucide-react'
|
|
2
|
+
import { Bot, GitCompareArrows, Layers, Network, Scale } from 'lucide-react'
|
|
3
|
+
import { compareVersions } from '../../utils/version'
|
|
4
|
+
|
|
5
|
+
export interface ReleaseHighlight {
|
|
6
|
+
id: string
|
|
7
|
+
icon: LucideIcon
|
|
8
|
+
title: string
|
|
9
|
+
description: string
|
|
10
|
+
/** In-app route the highlight's call to action opens. */
|
|
11
|
+
path?: string
|
|
12
|
+
cta?: string
|
|
13
|
+
/** Icon tile color, so the cards read apart at a glance. The lead card always uses the accent. */
|
|
14
|
+
tone?: HighlightTone
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type HighlightTone = 'violet' | 'teal' | 'emerald' | 'indigo'
|
|
18
|
+
|
|
19
|
+
export interface ReleaseNotes {
|
|
20
|
+
version: string
|
|
21
|
+
/** The first highlight leads the dialog at full width. */
|
|
22
|
+
highlights: ReleaseHighlight[]
|
|
23
|
+
improvements: string[]
|
|
24
|
+
releaseUrl: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// One entry per release that has something to announce. A version without its
|
|
28
|
+
// own entry shows the newest entry at or below it, so a patch upgrade (or one
|
|
29
|
+
// that skips the release with notes) still gets them. Every minor and major
|
|
30
|
+
// release must have its own entry: scripts/check-whats-new.sh refuses to
|
|
31
|
+
// release one without it.
|
|
32
|
+
export const RELEASE_NOTES: ReleaseNotes[] = [
|
|
33
|
+
{
|
|
34
|
+
version: 'v1.15.0',
|
|
35
|
+
releaseUrl: 'https://github.com/skyhook-io/radar/releases/tag/v1.15.0',
|
|
36
|
+
highlights: [
|
|
37
|
+
{
|
|
38
|
+
id: 'ai-investigations',
|
|
39
|
+
icon: Bot,
|
|
40
|
+
title: 'AI investigations that show their work',
|
|
41
|
+
// No link: the investigations workspace redirects home when this run
|
|
42
|
+
// mode can't host local agents.
|
|
43
|
+
description: 'A clear verdict and the story of what broke, with the charts, logs and config behind it placed where the agent cites them. Now also runs on OpenCode, including AWS Bedrock.',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: 'gitops-manifest-diff',
|
|
47
|
+
icon: GitCompareArrows,
|
|
48
|
+
title: 'Full manifest diffs for Argo CD resources',
|
|
49
|
+
description: 'Compare the Git-rendered and live manifest of a drifted Argo CD resource, side by side or unified.',
|
|
50
|
+
tone: 'violet',
|
|
51
|
+
path: '/gitops',
|
|
52
|
+
cta: 'Open GitOps',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: 'rightsizing-risks-first',
|
|
56
|
+
icon: Scale,
|
|
57
|
+
title: 'Rightsizing and cost data in MCP',
|
|
58
|
+
description: 'Agents get new optimized tools. Risks (OOM, limit conflicts and throttling) lead before cost-saving, in the UI as well.',
|
|
59
|
+
tone: 'emerald',
|
|
60
|
+
path: '/cost/rightsizing',
|
|
61
|
+
cta: 'Open Rightsizing',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'batch-ai-workloads',
|
|
65
|
+
icon: Layers,
|
|
66
|
+
title: 'Deeper batch + AI/ML workloads support',
|
|
67
|
+
description: 'Dedicated pages for Kueue, JobSet and Ray resources, and Job and JobSet details now show why a queued job has no Pods.',
|
|
68
|
+
tone: 'teal',
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: 'rollout-traffic-topology',
|
|
72
|
+
icon: Network,
|
|
73
|
+
title: 'Canary and blue-green in Topology',
|
|
74
|
+
description: 'See canary and stable Pods, which Services route to each side, and the canary traffic weight.',
|
|
75
|
+
tone: 'indigo',
|
|
76
|
+
path: '/topology',
|
|
77
|
+
cta: 'Open Topology',
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
improvements: [
|
|
81
|
+
// The grid fills row by row: pair lines of similar length.
|
|
82
|
+
'Resource tables remember your sort for each kind',
|
|
83
|
+
'Install Helm charts from OCI registries',
|
|
84
|
+
'Checks flag expired and expiring TLS certificates',
|
|
85
|
+
'Radar Desktop keeps your preferences across restarts',
|
|
86
|
+
'Issues show where a failing Pod differs from its template',
|
|
87
|
+
'PVC details say why usage data is unavailable',
|
|
88
|
+
'Issues catch Strimzi connector and admission webhook failures',
|
|
89
|
+
'Bind the web UI to a specific IP with --listen-address',
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
export function releaseNotesFor(
|
|
95
|
+
version: string | undefined,
|
|
96
|
+
catalog: ReleaseNotes[] = RELEASE_NOTES,
|
|
97
|
+
): ReleaseNotes | undefined {
|
|
98
|
+
if (!version) return undefined
|
|
99
|
+
const normalized = version.startsWith('v') ? version : `v${version}`
|
|
100
|
+
return catalog.find(notes => notes.version === normalized)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** The release line a catalog entry covers, e.g. v1.15 for v1.15.0: its patches show the same notes. */
|
|
104
|
+
export function releaseLine(version: string): string {
|
|
105
|
+
return version.replace(/^(v\d+\.\d+)\.\d+$/, '$1')
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The newest notes for a release at or below `version` — what that version's user has access to. */
|
|
109
|
+
export function latestReleaseNotesFor(
|
|
110
|
+
version: string | undefined,
|
|
111
|
+
catalog: ReleaseNotes[] = RELEASE_NOTES,
|
|
112
|
+
): ReleaseNotes | undefined {
|
|
113
|
+
if (!version) return undefined
|
|
114
|
+
let latest: ReleaseNotes | undefined
|
|
115
|
+
for (const notes of catalog) {
|
|
116
|
+
const atOrBelow = compareVersions(notes.version, version)
|
|
117
|
+
if (atOrBelow === null || atOrBelow > 0) continue
|
|
118
|
+
if (!latest || (compareVersions(notes.version, latest.version) ?? 0) > 0) latest = notes
|
|
119
|
+
}
|
|
120
|
+
return latest
|
|
121
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { RayJobRenderer } from '../resources/renderers/RayJobRenderer'
|
|
2
|
+
import { JobRenderer, JobSetRenderer } from '../resources/renderers/JobAdmissionRenderers'
|
|
1
3
|
import { RayClusterRenderer } from '../resources/renderers/RayClusterRenderer'
|
|
2
4
|
import { RayServiceRenderer } from '../resources/renderers/RayServiceRenderer'
|
|
3
5
|
import { KueueWorkloadRenderer } from '../resources/renderers/KueueWorkloadRenderer'
|
|
@@ -171,6 +173,9 @@ export function supportsBatchExecution(kind: string, apiKind: string, group?: st
|
|
|
171
173
|
|
|
172
174
|
// Stable reference — web renderer wrappers inject platform hooks internally
|
|
173
175
|
const rendererOverrides: RendererOverrides = {
|
|
176
|
+
RayJobRenderer,
|
|
177
|
+
JobRenderer,
|
|
178
|
+
JobSetRenderer,
|
|
174
179
|
RayServiceRenderer,
|
|
175
180
|
RayClusterRenderer,
|
|
176
181
|
KueueWorkloadRenderer,
|
|
@@ -607,6 +612,11 @@ export function WorkloadView({
|
|
|
607
612
|
const refetchResourceAndRuns = useCallback(async () => {
|
|
608
613
|
await Promise.all([
|
|
609
614
|
refetchResource(),
|
|
615
|
+
...(apiKind === 'rayjobs' && effectiveGroup === 'ray.io' ? [
|
|
616
|
+
queryClient.refetchQueries({ queryKey: ['resource', 'jobs', namespace, name, 'batch'], type: 'active' }),
|
|
617
|
+
...(resource?.spec?.clusterSelector?.['ray.io/cluster'] ? [queryClient.refetchQueries({ queryKey: ['resource', 'rayclusters', namespace, resource.spec.clusterSelector['ray.io/cluster'], 'ray.io'], type: 'active' })] : []),
|
|
618
|
+
] : []),
|
|
619
|
+
queryClient.refetchQueries({ queryKey: ['kueue-admission', effectiveGroup, apiKind, namespace, name], type: 'active' }),
|
|
610
620
|
queryClient.refetchQueries({
|
|
611
621
|
queryKey: ['workload-runs', apiKind, namespace, name],
|
|
612
622
|
}),
|
|
@@ -615,7 +625,7 @@ export function WorkloadView({
|
|
|
615
625
|
...(apiKind === 'rayclusters' ? { type: 'active' as const } : {}),
|
|
616
626
|
}),
|
|
617
627
|
])
|
|
618
|
-
}, [apiKind, name, namespace, queryClient, refetchResource])
|
|
628
|
+
}, [apiKind, effectiveGroup, name, namespace, queryClient, refetchResource, resource?.spec?.clusterSelector])
|
|
619
629
|
const podWorkloadOwner = useMemo(
|
|
620
630
|
() => podWorkloadOwnerFromRelationships(apiKind, namespace, relationships, resource),
|
|
621
631
|
[apiKind, namespace, relationships, resource],
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import { describe, expect, test } from 'vitest'
|
|
3
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
|
|
4
|
+
import { dirname, join, resolve } from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
|
|
7
|
+
// Radar's own build aliases @skyhook-io/k8s-ui to its source, so a deep
|
|
8
|
+
// import that the package's `exports` map can't serve still builds here and
|
|
9
|
+
// only fails in a consumer that installs the published packages (Radar Hub).
|
|
10
|
+
// This resolves every deep import the way Node and bundlers do: the most
|
|
11
|
+
// specific matching key wins, and an array target resolves to its first entry
|
|
12
|
+
// without falling back when that file doesn't exist.
|
|
13
|
+
|
|
14
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
15
|
+
const k8sUiRoot = resolve(here, '../../packages/k8s-ui')
|
|
16
|
+
const exportsMap: Record<string, string | string[]> = JSON.parse(
|
|
17
|
+
readFileSync(join(k8sUiRoot, 'package.json'), 'utf8'),
|
|
18
|
+
).exports
|
|
19
|
+
|
|
20
|
+
function resolveExport(subpath: string): string | undefined {
|
|
21
|
+
const key = `./${subpath}`
|
|
22
|
+
let target = exportsMap[key]
|
|
23
|
+
let star: string | undefined
|
|
24
|
+
if (target === undefined) {
|
|
25
|
+
let best = ''
|
|
26
|
+
for (const pattern of Object.keys(exportsMap)) {
|
|
27
|
+
const i = pattern.indexOf('*')
|
|
28
|
+
if (i < 0) continue
|
|
29
|
+
const prefix = pattern.slice(0, i)
|
|
30
|
+
const suffix = pattern.slice(i + 1)
|
|
31
|
+
if (key.startsWith(prefix) && key.endsWith(suffix) && key.length >= prefix.length + suffix.length && prefix.length > best.length) {
|
|
32
|
+
best = prefix
|
|
33
|
+
target = exportsMap[pattern]
|
|
34
|
+
star = key.slice(prefix.length, key.length - suffix.length)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (target === undefined) return undefined
|
|
39
|
+
const first = Array.isArray(target) ? target[0] : target
|
|
40
|
+
return join(k8sUiRoot, star === undefined ? first : first.split('*').join(star))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Everything radar-app publishes that can import from k8s-ui, stylesheets
|
|
44
|
+
// included (vitest doesn't load CSS, so these are read from disk).
|
|
45
|
+
function sourceFiles(dir: string): string[] {
|
|
46
|
+
return readdirSync(dir).flatMap((name) => {
|
|
47
|
+
const path = join(dir, name)
|
|
48
|
+
if (statSync(path).isDirectory()) return sourceFiles(path)
|
|
49
|
+
return /\.(ts|tsx|css)$/.test(name) && !/\.test\.(ts|tsx)$/.test(name) ? [path] : []
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe('@skyhook-io/k8s-ui deep imports', () => {
|
|
54
|
+
const imports = new Set<string>()
|
|
55
|
+
for (const file of sourceFiles(here)) {
|
|
56
|
+
// Any quoted specifier: `from` imports, side-effect and dynamic imports,
|
|
57
|
+
// and stylesheet @imports all resolve through the same map.
|
|
58
|
+
for (const m of readFileSync(file, 'utf8').matchAll(/['"]@skyhook-io\/k8s-ui\/([^'"]+)['"]/g)) {
|
|
59
|
+
imports.add(m[1])
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
test('finds the deep imports it checks, stylesheets included', () => {
|
|
64
|
+
expect(imports.size).toBeGreaterThan(10)
|
|
65
|
+
expect(imports).toContain('theme/variables.css')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test.each([...imports].sort())('%s resolves through the published exports map', (subpath) => {
|
|
69
|
+
const file = resolveExport(subpath)
|
|
70
|
+
expect(file, `no exports entry serves "@skyhook-io/k8s-ui/${subpath}"`).toBeDefined()
|
|
71
|
+
expect(existsSync(file!), `"@skyhook-io/k8s-ui/${subpath}" resolves to missing ${file}`).toBe(true)
|
|
72
|
+
})
|
|
73
|
+
})
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
initNavigationMap,
|
|
5
5
|
resetNavigationMap,
|
|
6
6
|
} from '@skyhook-io/k8s-ui/utils/navigation'
|
|
7
|
-
import { getNetworkPolicyResourceTarget, relatedResourcePath, resourcePath } from './navigation'
|
|
7
|
+
import { getNetworkPolicyResourceTarget, relatedResourcePath, resourcePath, withCrossViewParams } from './navigation'
|
|
8
8
|
|
|
9
9
|
afterEach(resetNavigationMap)
|
|
10
10
|
|
|
@@ -60,3 +60,19 @@ it('uses the Kubernetes kind for a Calico aggregate target', () => {
|
|
|
60
60
|
expect(getNetworkPolicyResourceTarget({nodes: [{id: 'policy', kind: 'CalicoNetworkPolicy', name: 'allow', status: 'healthy', data: {apiVersion: 'crd.projectcalico.org/v1', resourceKind: 'NetworkPolicy'}}], edges: []} as unknown as Topology))
|
|
61
61
|
.toEqual({kind: 'networkpolicies', group: 'crd.projectcalico.org'})
|
|
62
62
|
})
|
|
63
|
+
|
|
64
|
+
describe('withCrossViewParams', () => {
|
|
65
|
+
it('carries the namespace pick and open investigation onto a bare path', () => {
|
|
66
|
+
expect(withCrossViewParams('/gitops/argo/argocd/app', '?namespaces=a,b&ai-run=r1&tab=yaml'))
|
|
67
|
+
.toBe('/gitops/argo/argocd/app?namespaces=a%2Cb&ai-run=r1')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('keeps what the path sets itself, and its hash', () => {
|
|
71
|
+
expect(withCrossViewParams('/resources/pods?namespaces=x#top', '?namespaces=a&ai-run=r1'))
|
|
72
|
+
.toBe('/resources/pods?namespaces=x&ai-run=r1#top')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('leaves the path alone when nothing is scoped', () => {
|
|
76
|
+
expect(withCrossViewParams('/helm', '')).toBe('/helm')
|
|
77
|
+
})
|
|
78
|
+
})
|
package/src/utils/navigation.ts
CHANGED
|
@@ -133,3 +133,19 @@ export function openExternal(url: string): void {
|
|
|
133
133
|
window.open(url, '_blank')
|
|
134
134
|
})
|
|
135
135
|
}
|
|
136
|
+
|
|
137
|
+
// setMainView's cross-view params. A navigation whose search omits
|
|
138
|
+
// ?namespaces= reads as an empty pick to App's URL sync, which clears the
|
|
139
|
+
// user's namespace scope.
|
|
140
|
+
const CROSS_VIEW_PARAMS = ['namespaces', 'ai-run'] as const
|
|
141
|
+
|
|
142
|
+
/** Carries the current cross-view params onto an in-app path that doesn't set them itself. */
|
|
143
|
+
export function withCrossViewParams(path: string, currentSearch: string): string {
|
|
144
|
+
const destination = new URL(path, 'http://radar.invalid')
|
|
145
|
+
const current = new URLSearchParams(currentSearch)
|
|
146
|
+
for (const key of CROSS_VIEW_PARAMS) {
|
|
147
|
+
const value = current.get(key)
|
|
148
|
+
if (value && !destination.searchParams.has(key)) destination.searchParams.set(key, value)
|
|
149
|
+
}
|
|
150
|
+
return `${destination.pathname}${destination.search}${destination.hash}`
|
|
151
|
+
}
|
package/src/utils/version.ts
CHANGED
|
@@ -32,6 +32,17 @@ function parseVersion(version: string): [major: number, minor: number, patch: nu
|
|
|
32
32
|
return [Number(match[1]), Number(match[2]), Number(match[3]), !!match[4]]
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/** Semver order (a prerelease sorts before its release); null when either side isn't a version. */
|
|
36
|
+
export function compareVersions(a: string, b: string): number | null {
|
|
37
|
+
const x = parseVersion(a)
|
|
38
|
+
const y = parseVersion(b)
|
|
39
|
+
if (!x || !y) return null
|
|
40
|
+
for (let i = 0; i < 3; i++) {
|
|
41
|
+
if (x[i] !== y[i]) return (x[i] as number) - (y[i] as number)
|
|
42
|
+
}
|
|
43
|
+
return Number(y[3]) - Number(x[3])
|
|
44
|
+
}
|
|
45
|
+
|
|
35
46
|
export function getVersionUpdateStatus(current: string, latest?: string): VersionUpdateStatus {
|
|
36
47
|
if (!latest) return { tier: 'none' }
|
|
37
48
|
|