@skyhook-io/radar-app 1.0.1 → 1.1.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 +6 -6
- package/src/App.tsx +144 -25
- package/src/api/client.ts +158 -8
- package/src/components/dock/DockContext.tsx +1 -0
- package/src/components/dock/index.ts +1 -1
- package/src/components/gitops/GitOpsView.tsx +2441 -0
- package/src/components/gitops/RollbackDialog.tsx +107 -0
- package/src/components/gitops/SyncOptionsDialog.tsx +144 -0
- package/src/components/helm/HelmReleaseDrawer.tsx +20 -3
- package/src/components/helm/HelmView.tsx +9 -1
- package/src/components/helm/OwnedResources.tsx +2 -2
- package/src/components/home/GitOpsControllersCard.tsx +108 -0
- package/src/components/home/HomeView.tsx +9 -1
- package/src/components/resources/ResourcesView.tsx +27 -2
- package/src/components/resources/resource-utils.ts +2 -1
- package/src/components/timeline/TimelineSwimlanes.tsx +20 -6
- package/src/components/ui/CommandPalette.tsx +6 -4
- package/src/components/ui/ShortcutHelpOverlay.tsx +2 -1
- package/src/components/ui/UpdateNotification.tsx +36 -19
- package/src/components/workload/WorkloadView.tsx +126 -10
- package/src/types.ts +2 -0
- package/src/utils/navigation.ts +14 -1
|
@@ -2,11 +2,12 @@ import { useState, useMemo, useRef, useEffect, useCallback } from 'react'
|
|
|
2
2
|
import { TRANSITION_BACKDROP, TRANSITION_PANEL } from '../../utils/animation'
|
|
3
3
|
import { Search, X, ChevronRight } from 'lucide-react'
|
|
4
4
|
import { Home, Network, List, Clock, Package, Activity, Sun, Stethoscope, DollarSign, ShieldCheck } from 'lucide-react'
|
|
5
|
+
import { GitBranch } from 'lucide-react'
|
|
5
6
|
import { clsx } from 'clsx'
|
|
6
7
|
import { useNamespaces, useContexts } from '../../api/client'
|
|
7
8
|
import { CORE_RESOURCES, useAPIResources } from '../../api/apiResources'
|
|
8
9
|
import { getResourceIcon } from '../../utils/resource-icons'
|
|
9
|
-
type MainView = 'home' | 'topology' | 'resources' | 'timeline' | 'helm' | 'traffic' | 'cost' | 'audit'
|
|
10
|
+
type MainView = 'home' | 'topology' | 'resources' | 'timeline' | 'helm' | 'traffic' | 'cost' | 'audit' | 'gitops'
|
|
10
11
|
|
|
11
12
|
interface CommandPaletteProps {
|
|
12
13
|
onClose: () => void
|
|
@@ -154,9 +155,10 @@ export function CommandPalette({
|
|
|
154
155
|
{ view: 'resources', label: 'Resources', icon: List, shortcut: '3' },
|
|
155
156
|
{ view: 'timeline', label: 'Timeline', icon: Clock, shortcut: '4' },
|
|
156
157
|
{ view: 'helm', label: 'Helm', icon: Package, shortcut: '5' },
|
|
157
|
-
{ view: '
|
|
158
|
-
{ view: '
|
|
159
|
-
{ view: '
|
|
158
|
+
{ view: 'gitops', label: 'GitOps', icon: GitBranch, shortcut: '6' },
|
|
159
|
+
{ view: 'traffic', label: 'Traffic', icon: Activity, shortcut: '7' },
|
|
160
|
+
{ view: 'cost', label: 'Cost', icon: DollarSign, shortcut: '8' },
|
|
161
|
+
{ view: 'audit', label: 'Audit', icon: ShieldCheck, shortcut: '9' },
|
|
160
162
|
]
|
|
161
163
|
for (const v of viewEntries) {
|
|
162
164
|
result.push({
|
|
@@ -19,7 +19,7 @@ const CONTEXT_CATEGORIES: ShortcutCategory[] = ['Drawer', 'Dock']
|
|
|
19
19
|
|
|
20
20
|
// Preferred ordering within the view section
|
|
21
21
|
const VIEW_CATEGORY_ORDER: ShortcutCategory[] = [
|
|
22
|
-
'Search', 'Table', 'Resource Actions', 'Topology', 'Timeline', 'Helm',
|
|
22
|
+
'Search', 'Table', 'Resource Actions', 'Topology', 'Timeline', 'Helm', 'GitOps',
|
|
23
23
|
]
|
|
24
24
|
|
|
25
25
|
const VIEW_LABELS: Record<string, string> = {
|
|
@@ -28,6 +28,7 @@ const VIEW_LABELS: Record<string, string> = {
|
|
|
28
28
|
resources: 'Resources',
|
|
29
29
|
timeline: 'Timeline',
|
|
30
30
|
helm: 'Helm',
|
|
31
|
+
gitops: 'GitOps',
|
|
31
32
|
traffic: 'Traffic',
|
|
32
33
|
}
|
|
33
34
|
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
useApplyDesktopUpdate,
|
|
9
9
|
} from '../../api/client'
|
|
10
10
|
import type { DesktopUpdateState } from '../../api/client'
|
|
11
|
+
import { WithTooltip } from './Tooltip'
|
|
11
12
|
|
|
12
13
|
const DISMISSED_KEY = 'radar-update-dismissed'
|
|
13
14
|
|
|
@@ -117,7 +118,7 @@ export function UpdateNotification() {
|
|
|
117
118
|
<UpdateIcon state={effectiveState} />
|
|
118
119
|
</div>
|
|
119
120
|
<div className="flex-1 min-w-0">
|
|
120
|
-
<h4 className="text-sm font-medium text-theme-text-primary">
|
|
121
|
+
<h4 className="text-sm font-medium text-theme-text-primary pr-6">
|
|
121
122
|
<UpdateTitle state={effectiveState} />
|
|
122
123
|
</h4>
|
|
123
124
|
<p className="text-xs text-theme-text-secondary mt-1">
|
|
@@ -140,15 +141,30 @@ export function UpdateNotification() {
|
|
|
140
141
|
|
|
141
142
|
{/* CLI: show update command with copy button for package managers */}
|
|
142
143
|
{!isDesktop && versionInfo.updateCommand ? (
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
144
|
+
<>
|
|
145
|
+
<WithTooltip tip={versionInfo.updateCommand} delay={100}>
|
|
146
|
+
<button
|
|
147
|
+
onClick={handleCopyCommand}
|
|
148
|
+
className="flex items-center gap-2 mt-2 px-2 py-1.5 bg-theme-elevated rounded font-mono text-theme-text-primary hover:bg-theme-surface-hover transition-colors w-full"
|
|
149
|
+
>
|
|
150
|
+
<code className="flex-1 text-left truncate text-[11px]">{versionInfo.updateCommand}</code>
|
|
151
|
+
<CopyIcon copied={copied} failed={copyFailed} />
|
|
152
|
+
</button>
|
|
153
|
+
</WithTooltip>
|
|
154
|
+
{/* Direct installs may have placed the binary somewhere the install
|
|
155
|
+
script won't touch — surface a download link as a fallback. */}
|
|
156
|
+
{versionInfo.installMethod === 'direct' && versionInfo.releaseUrl && (
|
|
157
|
+
<a
|
|
158
|
+
href={versionInfo.releaseUrl}
|
|
159
|
+
target="_blank"
|
|
160
|
+
rel="noopener noreferrer"
|
|
161
|
+
className="inline-flex items-center gap-1 mt-1.5 text-xs text-theme-text-tertiary hover:text-theme-text-secondary hover:underline"
|
|
162
|
+
>
|
|
163
|
+
or download from GitHub →
|
|
164
|
+
</a>
|
|
165
|
+
)}
|
|
166
|
+
</>
|
|
150
167
|
) : (
|
|
151
|
-
/* Direct download - show release link */
|
|
152
168
|
!isDesktop && versionInfo.releaseUrl && (
|
|
153
169
|
<a
|
|
154
170
|
href={versionInfo.releaseUrl}
|
|
@@ -161,17 +177,18 @@ export function UpdateNotification() {
|
|
|
161
177
|
)
|
|
162
178
|
)}
|
|
163
179
|
</div>
|
|
164
|
-
{/* Don't show dismiss during active update */}
|
|
165
|
-
{effectiveState !== 'downloading' && effectiveState !== 'applying' && (
|
|
166
|
-
<button
|
|
167
|
-
onClick={handleDismiss}
|
|
168
|
-
className="p-1 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded shrink-0"
|
|
169
|
-
aria-label="Dismiss"
|
|
170
|
-
>
|
|
171
|
-
<X className="w-4 h-4" />
|
|
172
|
-
</button>
|
|
173
|
-
)}
|
|
174
180
|
</div>
|
|
181
|
+
{/* Dismiss is absolute so it doesn't compress the chip's width.
|
|
182
|
+
fixed on the parent already establishes the positioning context. */}
|
|
183
|
+
{effectiveState !== 'downloading' && effectiveState !== 'applying' && (
|
|
184
|
+
<button
|
|
185
|
+
onClick={handleDismiss}
|
|
186
|
+
className="absolute top-2 right-2 p-1 text-theme-text-secondary hover:text-theme-text-primary hover:bg-theme-elevated rounded"
|
|
187
|
+
aria-label="Dismiss"
|
|
188
|
+
>
|
|
189
|
+
<X className="w-4 h-4" />
|
|
190
|
+
</button>
|
|
191
|
+
)}
|
|
175
192
|
</div>
|
|
176
193
|
)
|
|
177
194
|
}
|
|
@@ -6,9 +6,11 @@ import { Terminal } from 'lucide-react'
|
|
|
6
6
|
import {
|
|
7
7
|
WorkloadView as BaseWorkloadView,
|
|
8
8
|
type RendererOverrides,
|
|
9
|
+
type GitOpsOwnerRef,
|
|
10
|
+
gitOpsRouteForOwner,
|
|
9
11
|
} from '@skyhook-io/k8s-ui'
|
|
10
12
|
import type { SelectedResource, ResourceRef, ResolvedEnvFrom } from '../../types'
|
|
11
|
-
import { kindToPlural, type NavigateToResource } from '../../utils/navigation'
|
|
13
|
+
import { kindToPlural, buildWorkloadPath, type NavigateToResource } from '../../utils/navigation'
|
|
12
14
|
import {
|
|
13
15
|
useChanges, useResourceWithRelationships, usePodLogs, useTopology, useUpdateResource,
|
|
14
16
|
useDeleteResource, useTriggerCronJob, useSuspendCronJob, useResumeCronJob,
|
|
@@ -21,7 +23,7 @@ import {
|
|
|
21
23
|
fetchJSON,
|
|
22
24
|
} from '../../api/client'
|
|
23
25
|
import { PrometheusCharts, isPrometheusSupported } from '../resource/PrometheusCharts'
|
|
24
|
-
import { useResourceAudit } from '../../api/client'
|
|
26
|
+
import { useResourceAudit, useResources } from '../../api/client'
|
|
25
27
|
import { AuditAlerts } from '@skyhook-io/k8s-ui'
|
|
26
28
|
import { WorkloadLogsViewer } from '../logs/WorkloadLogsViewer'
|
|
27
29
|
import { LogsViewer } from '../logs/LogsViewer'
|
|
@@ -35,6 +37,7 @@ import { ServiceRenderer } from '../resources/renderers/ServiceRenderer'
|
|
|
35
37
|
import { WorkloadRenderer } from '../resources/renderers/WorkloadRenderer'
|
|
36
38
|
import { CreateResourceDialog } from '../shared/CreateResourceDialog'
|
|
37
39
|
import { cleanYamlForDuplicate } from '../../utils/skeleton-yaml'
|
|
40
|
+
import { useDesktopDownload } from '../../hooks/useDesktopDownload'
|
|
38
41
|
|
|
39
42
|
type TabType = 'overview' | 'timeline' | 'logs' | 'metrics' | 'yaml'
|
|
40
43
|
|
|
@@ -54,13 +57,19 @@ interface WorkloadViewRouteProps {
|
|
|
54
57
|
export function WorkloadViewRoute({ onNavigateToResource }: WorkloadViewRouteProps) {
|
|
55
58
|
const location = useLocation()
|
|
56
59
|
const navigate = useNavigate()
|
|
60
|
+
const [searchParams] = useSearchParams()
|
|
57
61
|
|
|
58
|
-
// Parse /workload/:kind/:ns/:name from pathname
|
|
62
|
+
// Parse /workload/:kind/:ns/:name from pathname. Segments are URL-encoded by
|
|
63
|
+
// buildWorkloadPath; names can also contain literal slashes (e.g. some CRD names),
|
|
64
|
+
// which survive encoding as %2F and reassemble correctly here.
|
|
59
65
|
const parts = location.pathname.replace(/^\//, '').split('/')
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const
|
|
66
|
+
const decode = (s: string): string => {
|
|
67
|
+
try { return decodeURIComponent(s) } catch { return s }
|
|
68
|
+
}
|
|
69
|
+
const kind = decode(parts[1] ?? '')
|
|
70
|
+
const namespace = decode(parts[2] ?? '')
|
|
71
|
+
const name = parts.slice(3).map(decode).join('/')
|
|
72
|
+
const group = searchParams.get('apiGroup') || ''
|
|
64
73
|
|
|
65
74
|
if (!kind || !namespace || !name) {
|
|
66
75
|
return (
|
|
@@ -79,8 +88,7 @@ export function WorkloadViewRoute({ onNavigateToResource }: WorkloadViewRoutePro
|
|
|
79
88
|
}, [navigate])
|
|
80
89
|
|
|
81
90
|
const handleNavigate = useCallback((resource: SelectedResource) => {
|
|
82
|
-
|
|
83
|
-
navigate(`/workload/${resource.kind}/${resource.namespace}/${resource.name}`)
|
|
91
|
+
navigate(buildWorkloadPath(resource))
|
|
84
92
|
}, [navigate])
|
|
85
93
|
|
|
86
94
|
return (
|
|
@@ -88,6 +96,7 @@ export function WorkloadViewRoute({ onNavigateToResource }: WorkloadViewRoutePro
|
|
|
88
96
|
kind={kind}
|
|
89
97
|
namespace={namespace}
|
|
90
98
|
name={name}
|
|
99
|
+
group={group}
|
|
91
100
|
onBack={handleBack}
|
|
92
101
|
onNavigateToResource={onNavigateToResource || handleNavigate}
|
|
93
102
|
/>
|
|
@@ -315,11 +324,22 @@ export function WorkloadView({
|
|
|
315
324
|
const canUpdateSecrets = useCanUpdateSecrets()
|
|
316
325
|
const updateResource = useUpdateResource()
|
|
317
326
|
const actionsBarProps = useActionsBarProps(kindProp, namespace, name)
|
|
327
|
+
const desktopDownload = useDesktopDownload()
|
|
318
328
|
|
|
319
329
|
const handleUpdateResource = useCallback(async (params: { kind: string; namespace: string; name: string; yaml: string }) => {
|
|
320
330
|
await updateResource.mutateAsync(params)
|
|
321
331
|
}, [updateResource])
|
|
322
332
|
|
|
333
|
+
const navigateRouter = useNavigate()
|
|
334
|
+
const handleOpenGitOpsResource = useCallback(
|
|
335
|
+
(ref: GitOpsOwnerRef) => navigateRouter(gitOpsRouteForOwner(ref)),
|
|
336
|
+
[navigateRouter],
|
|
337
|
+
)
|
|
338
|
+
const handleNavigateGitOpsPath = useCallback(
|
|
339
|
+
(path: string) => navigateRouter(path),
|
|
340
|
+
[navigateRouter],
|
|
341
|
+
)
|
|
342
|
+
|
|
323
343
|
// Duplicate dialog
|
|
324
344
|
const [duplicateDialogOpen, setDuplicateDialogOpen] = useState(false)
|
|
325
345
|
const [duplicateYaml, setDuplicateYaml] = useState('')
|
|
@@ -370,12 +390,18 @@ export function WorkloadView({
|
|
|
370
390
|
isPrometheusSupported(kind) && !(kind === 'Pod' && res?.status?.phase === 'Pending')
|
|
371
391
|
}
|
|
372
392
|
onDuplicate={handleDuplicate}
|
|
393
|
+
onDownload={desktopDownload}
|
|
373
394
|
actionsBarProps={actionsBarProps}
|
|
374
395
|
rendererOverrides={rendererOverrides}
|
|
375
396
|
resolvedEnvFrom={resolvedEnvFrom}
|
|
376
397
|
renderOverviewExtra={({ kind: k, namespace: ns, name: n }) => (
|
|
377
|
-
|
|
398
|
+
<>
|
|
399
|
+
<AuditSection kind={k} namespace={ns} name={n} />
|
|
400
|
+
<FluxSourceConsumersSection kind={k} namespace={ns} name={n} />
|
|
401
|
+
</>
|
|
378
402
|
)}
|
|
403
|
+
onOpenGitOpsResource={handleOpenGitOpsResource}
|
|
404
|
+
onNavigateGitOpsPath={handleNavigateGitOpsPath}
|
|
379
405
|
/>
|
|
380
406
|
<CreateResourceDialog
|
|
381
407
|
open={duplicateDialogOpen}
|
|
@@ -546,3 +572,93 @@ function AuditSection({ kind, namespace, name }: { kind: string; namespace: stri
|
|
|
546
572
|
if (!findings || findings.length === 0) return null
|
|
547
573
|
return <AuditAlerts findings={findings} onViewAll={() => navigate('/audit')} />
|
|
548
574
|
}
|
|
575
|
+
|
|
576
|
+
// FluxSourceConsumersSection lists the reconcilers (Kustomization, HelmRelease)
|
|
577
|
+
// that reference this Flux source CR — the inverse of `spec.sourceRef`. Renders
|
|
578
|
+
// only when the focused resource is a Flux source kind; otherwise null. Sources
|
|
579
|
+
// can have many consumers (one repo feeding multiple apps), so this answers
|
|
580
|
+
// "if I edit this source, what gets affected on the next reconcile?".
|
|
581
|
+
//
|
|
582
|
+
// Filtering happens client-side off the namespaced reconciler lists — these
|
|
583
|
+
// are typically small (tens, not thousands) and the dynamic informer cache
|
|
584
|
+
// makes the request cheap. If a cluster ever has thousands of HelmReleases,
|
|
585
|
+
// a dedicated /api/gitops/consumers endpoint would be the right move; today
|
|
586
|
+
// it'd be premature.
|
|
587
|
+
// Outer component is cheap — it does only the kind check and decides whether
|
|
588
|
+
// to mount the data-fetching child. Without this split, useResources would
|
|
589
|
+
// fire two API calls on EVERY workload drawer open (Pod, Deployment, Service,
|
|
590
|
+
// …), since the hook has no `enabled` flag and can't be conditionally called
|
|
591
|
+
// (Rules of Hooks). The hooks only need to run when the focused resource is
|
|
592
|
+
// actually a Flux source CR.
|
|
593
|
+
function FluxSourceConsumersSection({ kind, namespace, name }: { kind: string; namespace: string; name: string }) {
|
|
594
|
+
// The inner WorkloadView de-pluralizes the URL's plural form, which gives
|
|
595
|
+
// "Gitrepository" (single-uppercase) rather than the wire-correct
|
|
596
|
+
// "GitRepository" — so we match lowercase. spec.sourceRef.kind on consumers
|
|
597
|
+
// is always wire-correct, so we look that up separately.
|
|
598
|
+
const sourceKind = FLUX_SOURCE_KIND_BY_LOWER.get(kind.toLowerCase()) ?? null
|
|
599
|
+
if (!sourceKind) return null
|
|
600
|
+
return <FluxSourceConsumersInner sourceKind={sourceKind} namespace={namespace} name={name} />
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function FluxSourceConsumersInner({ sourceKind, namespace, name }: { sourceKind: string; namespace: string; name: string }) {
|
|
604
|
+
const navigate = useNavigate()
|
|
605
|
+
const { data: kustomizations } = useResources<any>('kustomizations', undefined, 'kustomize.toolkit.fluxcd.io')
|
|
606
|
+
const { data: helmReleases } = useResources<any>('helmreleases', undefined, 'helm.toolkit.fluxcd.io')
|
|
607
|
+
|
|
608
|
+
const consumers: Array<{ kind: 'Kustomization' | 'HelmRelease'; namespace: string; name: string; plural: string }> = []
|
|
609
|
+
for (const k of kustomizations ?? []) {
|
|
610
|
+
const ref = k?.spec?.sourceRef ?? {}
|
|
611
|
+
const refNs = ref.namespace || k?.metadata?.namespace
|
|
612
|
+
if (ref.kind === sourceKind && ref.name === name && refNs === namespace) {
|
|
613
|
+
consumers.push({ kind: 'Kustomization', namespace: k.metadata.namespace, name: k.metadata.name, plural: 'kustomizations' })
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
for (const h of helmReleases ?? []) {
|
|
617
|
+
const ref = h?.spec?.chart?.spec?.sourceRef ?? {}
|
|
618
|
+
const refNs = ref.namespace || h?.metadata?.namespace
|
|
619
|
+
if (ref.kind === sourceKind && ref.name === name && refNs === namespace) {
|
|
620
|
+
consumers.push({ kind: 'HelmRelease', namespace: h.metadata.namespace, name: h.metadata.name, plural: 'helmreleases' })
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (consumers.length === 0) {
|
|
625
|
+
return (
|
|
626
|
+
<div className="rounded-lg border border-theme-border bg-theme-elevated/40 p-3 text-xs text-theme-text-tertiary">
|
|
627
|
+
Consumed by — no Kustomization or HelmRelease references this source.
|
|
628
|
+
</div>
|
|
629
|
+
)
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
return (
|
|
633
|
+
<div className="rounded-lg border border-theme-border bg-theme-elevated/40 p-3">
|
|
634
|
+
<h3 className="mb-2 text-xs font-medium text-theme-text-secondary">
|
|
635
|
+
Consumed by ({consumers.length})
|
|
636
|
+
</h3>
|
|
637
|
+
<div className="flex flex-wrap gap-1.5">
|
|
638
|
+
{consumers.map((c) => (
|
|
639
|
+
<button
|
|
640
|
+
key={`${c.kind}/${c.namespace}/${c.name}`}
|
|
641
|
+
onClick={() => navigate(`/gitops/detail/${c.plural}/${encodeURIComponent(c.namespace)}/${encodeURIComponent(c.name)}`)}
|
|
642
|
+
className="inline-flex items-center gap-1.5 rounded border border-theme-border bg-theme-surface px-1.5 py-0.5 text-[11px] text-theme-text-secondary hover:border-skyhook-500/60 hover:text-skyhook-500 transition-colors"
|
|
643
|
+
title={`${c.kind} ${c.namespace}/${c.name}`}
|
|
644
|
+
>
|
|
645
|
+
<span className="text-theme-text-tertiary">{c.kind === 'HelmRelease' ? 'HR' : 'K'}</span>
|
|
646
|
+
<span>{c.namespace}/{c.name}</span>
|
|
647
|
+
</button>
|
|
648
|
+
))}
|
|
649
|
+
</div>
|
|
650
|
+
</div>
|
|
651
|
+
)
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// FLUX_SOURCE_KIND_BY_LOWER maps lowercase kind (what the inner WorkloadView
|
|
655
|
+
// produces via its plural-to-singular fallback) to the wire-correct
|
|
656
|
+
// PascalCase form that consumers carry in spec.sourceRef.kind. HelmChart is
|
|
657
|
+
// intentionally absent — it's an auto-generated internal CR, not something
|
|
658
|
+
// users create or point reconcilers at directly.
|
|
659
|
+
const FLUX_SOURCE_KIND_BY_LOWER = new Map<string, string>([
|
|
660
|
+
['gitrepository', 'GitRepository'],
|
|
661
|
+
['helmrepository', 'HelmRepository'],
|
|
662
|
+
['ocirepository', 'OCIRepository'],
|
|
663
|
+
['bucket', 'Bucket'],
|
|
664
|
+
])
|
package/src/types.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
// Re-export all types from the shared @skyhook-io/k8s-ui package.
|
|
2
2
|
// This file exists for backward compatibility — new code should import from '@skyhook-io/k8s-ui' directly.
|
|
3
3
|
export * from '@skyhook-io/k8s-ui/types/core'
|
|
4
|
+
export * from '@skyhook-io/k8s-ui/types/gitops-tree'
|
|
5
|
+
export * from '@skyhook-io/k8s-ui/types/gitops-insights'
|
package/src/utils/navigation.ts
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
import { apiUrl, getAuthHeaders, getCredentialsMode } from '../api/config'
|
|
2
|
+
import type { SelectedResource } from '@skyhook-io/k8s-ui/types/core'
|
|
2
3
|
|
|
3
4
|
// Re-export shared navigation utilities from @skyhook-io/k8s-ui.
|
|
4
|
-
export { kindToPlural, pluralToKind, refToSelectedResource } from '@skyhook-io/k8s-ui/utils/navigation'
|
|
5
|
+
export { kindToPlural, pluralToKind, refToSelectedResource, apiVersionToGroup } from '@skyhook-io/k8s-ui/utils/navigation'
|
|
5
6
|
export type { NavigateToResource } from '@skyhook-io/k8s-ui/utils/navigation'
|
|
6
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Build a /workload/:kind/:namespace/:name URL, preserving the API group as a
|
|
10
|
+
* query param so the WorkloadView can resolve CRDs with colliding kind names.
|
|
11
|
+
*/
|
|
12
|
+
export function buildWorkloadPath(resource: SelectedResource): string {
|
|
13
|
+
const kind = encodeURIComponent(resource.kind)
|
|
14
|
+
const namespace = encodeURIComponent(resource.namespace)
|
|
15
|
+
const name = encodeURIComponent(resource.name)
|
|
16
|
+
const base = `/workload/${kind}/${namespace}/${name}`
|
|
17
|
+
return resource.group ? `${base}?apiGroup=${encodeURIComponent(resource.group)}` : base
|
|
18
|
+
}
|
|
19
|
+
|
|
7
20
|
// radar-specific: open URL in system browser (desktop app support)
|
|
8
21
|
export function openExternal(url: string): void {
|
|
9
22
|
fetch(apiUrl('/desktop/open-url'), {
|