@skyhook-io/k8s-ui 1.10.3 → 1.10.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyhook-io/k8s-ui",
3
- "version": "1.10.3",
3
+ "version": "1.10.4",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/skyhook-io/radar",
@@ -7,10 +7,11 @@
7
7
  */
8
8
  import ELK from 'elkjs/lib/elk-api.js'
9
9
  import elkWorkerAlgorithm from 'elkjs/lib/elk-worker.min.js?url'
10
+ import { assetUrl } from '../../utils/asset-url'
10
11
 
11
12
  // Create ELK with explicit worker URL (runs the algorithm in our worker context)
12
13
  const elk = new ELK({
13
- workerUrl: elkWorkerAlgorithm,
14
+ workerUrl: assetUrl(elkWorkerAlgorithm),
14
15
  })
15
16
 
16
17
  // ELK options for laying out nodes within a single group
package/src/types/core.ts CHANGED
@@ -92,6 +92,21 @@ export interface Capabilities {
92
92
  resources?: ResourcePermissions // Per-resource-type permissions
93
93
  authEnabled?: boolean // Auth is enabled on the backend
94
94
  username?: string // Authenticated user's username (when auth enabled)
95
+ // Which Cloud-connect lane this deployment gets. Optional on the wire:
96
+ // older backends don't advertise it — consumers fall back to wizard links.
97
+ cloudConnect?: CloudConnectCapability
98
+ }
99
+
100
+ // CloudConnectCapability picks the Cloud funnel's connect lane: 'driver'
101
+ // means the in-product connect flow can run on this server (local, no auth,
102
+ // no existing tunnel); 'wizard' routes to the Hub's connect wizard at appUrl.
103
+ export interface CloudConnectCapability {
104
+ lane: 'driver' | 'wizard'
105
+ appUrl: string
106
+ // Hub API origin the connect dialog reads its live copy from. Absent means
107
+ // the server decided this deployment must not fetch — consumers render their
108
+ // compiled-in copy instead.
109
+ apiUrl?: string
95
110
  }
96
111
 
97
112
  export interface FeatureCapabilities {
@@ -0,0 +1,113 @@
1
+ import { describe, it, expect, afterEach } from 'vitest'
2
+ import { assetUrl } from './asset-url'
3
+
4
+ type RuntimeGlobal = typeof globalThis & {
5
+ __RADAR_RUNTIME_CONFIG__?: { assetBase?: string }
6
+ }
7
+
8
+ function setAssetBase(assetBase: string) {
9
+ ;(globalThis as RuntimeGlobal).__RADAR_RUNTIME_CONFIG__ = { assetBase }
10
+ }
11
+
12
+ /**
13
+ * Simulates a Worker scope, where `window` is absent and `location` is the
14
+ * worker script's own URL. jsdom defines `window`, so it has to be removed.
15
+ */
16
+ function asWorkerAt(pathname: string, run: () => void) {
17
+ const realWindow = globalThis.window
18
+ // @ts-expect-error — deliberately emulating a scope without `window`
19
+ delete globalThis.window
20
+ const realLocation = globalThis.location
21
+ Object.defineProperty(globalThis, 'location', {
22
+ value: { pathname },
23
+ configurable: true,
24
+ writable: true,
25
+ })
26
+ try {
27
+ run()
28
+ } finally {
29
+ Object.defineProperty(globalThis, 'location', {
30
+ value: realLocation,
31
+ configurable: true,
32
+ writable: true,
33
+ })
34
+ globalThis.window = realWindow
35
+ }
36
+ }
37
+
38
+ afterEach(() => {
39
+ delete (globalThis as RuntimeGlobal).__RADAR_RUNTIME_CONFIG__
40
+ })
41
+
42
+ describe('assetUrl at the root (no base path)', () => {
43
+ it('leaves absolute paths untouched', () => {
44
+ expect(assetUrl('/images/radar/radar-icon.svg')).toBe('/images/radar/radar-icon.svg')
45
+ })
46
+
47
+ it('makes Vite-relative asset paths absolute', () => {
48
+ expect(assetUrl('./assets/index-abc.js')).toBe('/assets/index-abc.js')
49
+ expect(assetUrl('assets/index-abc.js')).toBe('/assets/index-abc.js')
50
+ })
51
+
52
+ it('unwraps the webpack/Next StaticImageData shape', () => {
53
+ expect(assetUrl({ src: '/_next/static/x.png' })).toBe('/_next/static/x.png')
54
+ })
55
+
56
+ // An app route may contain an "/assets/" segment (e.g. a namespace named
57
+ // "assets"). That must not be mistaken for a base path.
58
+ it('ignores an /assets/ segment in the current app route', () => {
59
+ expect(assetUrl('/images/radar/radar-icon.svg')).toBe('/images/radar/radar-icon.svg')
60
+ })
61
+ })
62
+
63
+ describe('assetUrl under a base path', () => {
64
+ it('prefixes absolute and relative asset paths', () => {
65
+ setAssetBase('/radar')
66
+ expect(assetUrl('/images/radar/radar-icon.svg')).toBe('/radar/images/radar/radar-icon.svg')
67
+ expect(assetUrl('./assets/index-abc.js')).toBe('/radar/assets/index-abc.js')
68
+ expect(assetUrl('assets/index-abc.js')).toBe('/radar/assets/index-abc.js')
69
+ })
70
+
71
+ it('is idempotent for already-prefixed paths', () => {
72
+ setAssetBase('/radar')
73
+ expect(assetUrl('/radar/assets/index-abc.js')).toBe('/radar/assets/index-abc.js')
74
+ })
75
+
76
+ it('leaves protocol-relative URLs alone', () => {
77
+ setAssetBase('/radar')
78
+ expect(assetUrl('//cdn.example.com/x.png')).toBe('//cdn.example.com/x.png')
79
+ })
80
+
81
+ it('handles a nested base path', () => {
82
+ setAssetBase('/tools/radar')
83
+ expect(assetUrl('/favicon.svg')).toBe('/tools/radar/favicon.svg')
84
+ })
85
+ })
86
+
87
+ describe('assetUrl inside a Worker', () => {
88
+ it('recovers the base path from the worker script URL', () => {
89
+ asWorkerAt('/radar/assets/layout.worker-abc.js', () => {
90
+ expect(assetUrl('/images/x.svg')).toBe('/radar/images/x.svg')
91
+ })
92
+ })
93
+
94
+ it('resolves to the root when the worker is served from the root', () => {
95
+ asWorkerAt('/assets/layout.worker-abc.js', () => {
96
+ expect(assetUrl('/images/x.svg')).toBe('/images/x.svg')
97
+ })
98
+ })
99
+
100
+ // The base path may itself contain "/assets/" — split on the last one.
101
+ it('splits on the last /assets/ segment', () => {
102
+ asWorkerAt('/host/assets/radar/assets/layout.worker-abc.js', () => {
103
+ expect(assetUrl('/images/x.svg')).toBe('/host/assets/radar/images/x.svg')
104
+ })
105
+ })
106
+
107
+ it('prefers the injected config over the URL heuristic', () => {
108
+ setAssetBase('/radar')
109
+ asWorkerAt('/somewhere/assets/worker.js', () => {
110
+ expect(assetUrl('/images/x.svg')).toBe('/radar/images/x.svg')
111
+ })
112
+ })
113
+ })
@@ -9,5 +9,37 @@
9
9
  export type AssetImport = string | { src: string }
10
10
 
11
11
  export function assetUrl(asset: AssetImport): string {
12
- return typeof asset === 'string' ? asset : asset.src
12
+ const url = typeof asset === 'string' ? asset : asset.src
13
+ const assetBase = getRuntimeAssetBase()
14
+ if (!assetBase || url.startsWith('//') || url === assetBase || url.startsWith(`${assetBase}/`)) {
15
+ if (url.startsWith('./assets/')) return `/assets/${url.slice('./assets/'.length)}`
16
+ if (url.startsWith('assets/')) return `/${url}`
17
+ return url
18
+ }
19
+ if (url.startsWith('./')) return `${assetBase}/${url.slice(2)}`
20
+ if (!url.startsWith('/') && url.startsWith('assets/')) return `${assetBase}/${url}`
21
+ if (!url.startsWith('/')) return url
22
+ return `${assetBase}${url}`
23
+ }
24
+
25
+ function getRuntimeAssetBase(): string {
26
+ if (typeof globalThis === 'undefined') return ''
27
+ const runtime = (globalThis as typeof globalThis & {
28
+ __RADAR_RUNTIME_CONFIG__?: { assetBase?: string }
29
+ }).__RADAR_RUNTIME_CONFIG__
30
+ const configured = runtime?.assetBase?.replace(/\/+$/, '')
31
+ if (configured) return configured
32
+
33
+ // A Worker has its own global scope, so it never sees the runtime config the
34
+ // server injects into index.html — but its own script URL sits at
35
+ // `<base>/assets/<chunk>.js`, so the base can be recovered from it. Restricted
36
+ // to worker scopes on purpose: on the main thread `pathname` is an app route,
37
+ // which may legitimately contain an "/assets/" segment (a namespace or
38
+ // resource named "assets") and would yield a bogus prefix for every asset.
39
+ if (typeof window !== 'undefined') return ''
40
+ const pathname = globalThis.location?.pathname ?? ''
41
+ // lastIndexOf, not indexOf: the base path itself may contain "/assets/".
42
+ const assetsIndex = pathname.lastIndexOf('/assets/')
43
+ if (assetsIndex > 0) return pathname.slice(0, assetsIndex)
44
+ return ''
13
45
  }
@@ -1,5 +1,6 @@
1
1
  export * from './format'
2
2
  export * from './pluralize'
3
+ export * from './asset-url'
3
4
  export * from './badge-colors'
4
5
  export * from './resource-icons'
5
6
  export * from './navigation'