Sitelet https://github.com/codingprotocols/podscape/pull/89/files
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions go-core/internal/providers/detect.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type ProviderSet struct {
Keda bool `json:"keda"`
Cilium bool `json:"cilium"`
HubbleRelay bool `json:"hubbleRelay"`
Ambassador bool `json:"ambassador"`
}

// Detect probes the cluster's API group list and IngressClass resources to
Expand Down Expand Up @@ -69,6 +70,8 @@ func Detect(disco discovery.DiscoveryInterface, ingressClasses []networkingv1.In
ps.Keda = true
case "cilium.io":
ps.Cilium = true
case "getambassador.io":
ps.Ambassador = true
}
}

Expand Down
9 changes: 9 additions & 0 deletions src/renderer/components/core/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,15 @@ export default function Sidebar(): JSX.Element {
</NavGroup>
)}

{providers.ambassador && (
<NavGroup title="Ambassador">
<NavItem label="Mappings" section="ambassador-mappings" icon={ICONS.route} {...navProps} />
<NavItem label="Hosts" section="ambassador-hosts" icon={ICONS.ingress} {...navProps} />
<NavItem label="TLS Contexts" section="ambassador-tlscontexts" icon={ICONS.secret} {...navProps} />
<NavItem label="TCP Mappings" section="ambassador-tcpmappings" icon={ICONS.portforward} {...navProps} />
</NavGroup>
)}

</nav>

</div>
Expand Down
9 changes: 9 additions & 0 deletions src/renderer/components/panels/ProviderResourcePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ const SECTION_TO_CRD: Partial<Record<ResourceKind, string>> = {
'keda-scaledjobs': 'scaledjobs.keda.sh',
'keda-triggerauthentications': 'triggerauthentications.keda.sh',
'keda-clustertriggerauthentications': 'clustertriggerauthentications.keda.sh',
// Ambassador
'ambassador-mappings': 'mappings.getambassador.io',
'ambassador-hosts': 'hosts.getambassador.io',
'ambassador-tlscontexts': 'tlscontexts.getambassador.io',
'ambassador-tcpmappings': 'tcpmappings.getambassador.io',
}

const SECTION_LABELS: Partial<Record<ResourceKind, string>> = {
Expand Down Expand Up @@ -66,6 +71,10 @@ const SECTION_LABELS: Partial<Record<ResourceKind, string>> = {
'keda-scaledjobs': 'Scaled Jobs',
'keda-triggerauthentications': 'Trigger Authentications',
'keda-clustertriggerauthentications': 'Cluster Trigger Authentications',
'ambassador-mappings': 'Mappings',
'ambassador-hosts': 'Hosts',
'ambassador-tlscontexts': 'TLS Contexts',
'ambassador-tcpmappings': 'TCP Mappings',
}

/** Sections whose resources are cluster-scoped — always fetch without namespace filter. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useAppStore } from '../../../store'
import { useShallow } from 'zustand/react/shallow'
import { canVerb } from '../../../store/slices/clusterSlice'
import YAMLViewer from '../../common/YAMLViewer'
import { X } from 'lucide-react'
import TimeSeriesChart, { PrometheusTimeRangeBar } from '../../advanced/TimeSeriesChart'
import { nodeCpuQuery, nodeMemoryQuery } from '../../../utils/prometheusQueries'

Expand Down Expand Up @@ -108,7 +109,7 @@ export default function NodeDetail({ node }: Props): JSX.Element {
}

return (
<div className="flex flex-col w-full h-full overflow-y-auto">
<div className="flex flex-col w-full h-full overflow-hidden">
<div className="px-6 py-6 border-b border-slate-100 dark:border-white/5 bg-white/5 shrink-0">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-2xl bg-blue-600/10 dark:bg-blue-500/20 flex items-center justify-center shrink-0">
Expand Down Expand Up @@ -162,6 +163,7 @@ export default function NodeDetail({ node }: Props): JSX.Element {
<p className="mt-2 text-[10px] text-red-500 font-mono break-all">{actionError}</p>
)}
</div>
<div className="flex-1 overflow-y-auto">

{/* Resource usage — live metrics if available, allocatable otherwise */}
<div className="px-6 py-5 border-b border-slate-100 dark:border-white/5">
Expand Down Expand Up @@ -341,13 +343,15 @@ export default function NodeDetail({ node }: Props): JSX.Element {
</div>
)}

</div>

{/* YAML viewer modal */}
{(yamlLoading || yaml !== null || yamlError !== null) && (
<div className="fixed inset-0 z-[60] bg-slate-900/40 backdrop-blur-md flex items-center justify-center p-8 animate-in fade-in duration-200">
<div className="bg-white dark:bg-[hsl(var(--bg-dark))] rounded-3xl shadow-2xl border border-slate-200 dark:border-white/10 w-full max-w-5xl h-full max-h-[90vh] flex flex-col overflow-hidden">
<div className="flex items-center justify-between px-8 py-5 border-b border-slate-100 dark:border-white/10 bg-white/5 backdrop-blur-xl shrink-0">
<h3 className="text-sm font-black text-slate-900 dark:text-white uppercase tracking-widest">{yamlLoading ? 'Loading YAML…' : `YAML — ${node.metadata.name}`}</h3>
<button onClick={() => { ++yamlFetchIdRef.current; setYaml(null); setYamlError(null); setYamlLoading(false) }} className="w-8 h-8 flex items-center justify-center rounded-full hover:bg-white/10 text-slate-400 transition-colors">✕</button>
<button type="button" onClick={() => { ++yamlFetchIdRef.current; setYaml(null); setYamlError(null); setYamlLoading(false) }} className="w-8 h-8 flex items-center justify-center rounded-full hover:bg-slate-200 dark:hover:bg-slate-800 text-slate-400 transition-colors" aria-label="Close"><X className="w-4 h-4" /></button>
</div>
<div className="flex-1 min-h-0">
{yamlError ? (
Expand Down
51 changes: 27 additions & 24 deletions src/renderer/components/resource-details/network/IngressDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { formatAge } from '../../../types'
import { useAppStore } from '../../../store'
import { useShallow } from 'zustand/react/shallow'
import YAMLViewer from '../../common/YAMLViewer'
import { X } from 'lucide-react'

interface Props { ingress: KubeIngress }

Expand Down Expand Up @@ -209,7 +210,7 @@ export default function IngressDetail({ ingress: ing }: Props): JSX.Element {
}, [tab, ing.metadata.uid, selectedContext, ns])

return (
<div className="flex flex-col w-full h-full overflow-y-auto">
<div className="flex flex-col w-full h-full overflow-hidden">
{/* Header */}
<div className="px-6 py-6 border-b border-slate-100 dark:border-white/5 bg-white/5 shrink-0">
<div className="flex items-start gap-2">
Expand Down Expand Up @@ -341,35 +342,37 @@ export default function IngressDetail({ ingress: ing }: Props): JSX.Element {
)}

{tab === 'events' && (
<div className="px-4 py-3 flex-1">
<div className="flex items-center justify-between mb-3">
<div className="flex flex-col flex-1 min-h-0">
<div className="flex items-center justify-between px-4 pt-3 pb-2 shrink-0">
<h4 className="text-xs font-medium text-slate-400 dark:text-slate-500 uppercase tracking-wider">Events</h4>
<button onClick={loadEvents} disabled={eventsLoading}
className="text-xs px-2 py-1 rounded bg-white/5 text-slate-400 dark:text-slate-500 border border-slate-100 dark:border-slate-800 hover:bg-white/10 disabled:opacity-50">
{eventsLoading ? '…' : 'Refresh'}
</button>
</div>
{eventsLoading ? (
<div className="flex items-center justify-center h-24">
<div className="w-5 h-5 border-2 border-gray-700 border-t-blue-500 rounded-full animate-spin" />
</div>
) : events.length === 0 ? (
<p className="text-xs text-slate-500 dark:text-slate-400 text-center py-8">No events found</p>
) : (
<div className="space-y-2">
{events.map((e, i) => (
<div key={e.metadata.uid || i} className={`rounded p-2 text-xs ${e.type === 'Warning' ? 'bg-yellow-500/10 border border-yellow-500/20' : 'bg-white/5 border border-slate-100 dark:border-slate-800'
}`}>
<div className="flex items-center justify-between gap-2 mb-1">
<span className={`font-medium ${e.type === 'Warning' ? 'text-yellow-300' : 'text-slate-600 dark:text-slate-300'}`}>{e.reason}</span>
<span className="text-slate-500 dark:text-slate-400 text-[10px]">{e.count ? `×${e.count}` : ''}</span>
<div className="flex-1 overflow-y-auto px-4 pb-3">
{eventsLoading ? (
<div className="flex items-center justify-center h-24">
<div className="w-5 h-5 border-2 border-gray-700 border-t-blue-500 rounded-full animate-spin" />
</div>
) : events.length === 0 ? (
<p className="text-xs text-slate-500 dark:text-slate-400 text-center py-8">No events found</p>
) : (
<div className="space-y-2">
{events.map((e, i) => (
<div key={e.metadata.uid || i} className={`rounded p-2 text-xs ${e.type === 'Warning' ? 'bg-yellow-500/10 border border-yellow-500/20' : 'bg-white/5 border border-slate-100 dark:border-slate-800'
}`}>
<div className="flex items-center justify-between gap-2 mb-1">
<span className={`font-medium ${e.type === 'Warning' ? 'text-yellow-300' : 'text-slate-600 dark:text-slate-300'}`}>{e.reason}</span>
<span className="text-slate-500 dark:text-slate-400 text-[10px]">{e.count ? `×${e.count}` : ''}</span>
</div>
<p className="text-slate-400 dark:text-slate-500 leading-relaxed">{e.message}</p>
{e.lastTimestamp && <p className="text-slate-500 dark:text-slate-400 mt-1">{formatAge(e.lastTimestamp)} ago</p>}
</div>
<p className="text-slate-400 dark:text-slate-500 leading-relaxed">{e.message}</p>
{e.lastTimestamp && <p className="text-slate-500 dark:text-slate-400 mt-1">{formatAge(e.lastTimestamp)} ago</p>}
</div>
))}
</div>
)}
))}
</div>
)}
</div>
</div>
)}

Expand All @@ -384,7 +387,7 @@ export default function IngressDetail({ ingress: ing }: Props): JSX.Element {
<div className="bg-white dark:bg-[hsl(var(--bg-dark))] rounded-3xl shadow-2xl border border-slate-200 dark:border-white/10 w-full max-w-5xl h-full max-h-[90vh] flex flex-col overflow-hidden">
<div className="flex items-center justify-between px-8 py-5 border-b border-slate-100 dark:border-white/10 bg-white/5 backdrop-blur-xl shrink-0">
<h3 className="text-sm font-black text-slate-900 dark:text-white uppercase tracking-widest">{yamlLoading ? 'Loading YAML…' : `YAML — ${ing.metadata.name}`}</h3>
<button onClick={() => { ++yamlFetchIdRef.current; setYaml(null); setYamlError(null); setYamlLoading(false) }} className="w-8 h-8 flex items-center justify-center rounded-full hover:bg-white/10 text-slate-400 transition-colors">✕</button>
<button type="button" onClick={() => { ++yamlFetchIdRef.current; setYaml(null); setYamlError(null); setYamlLoading(false) }} className="w-8 h-8 flex items-center justify-center rounded-full hover:bg-slate-200 dark:hover:bg-slate-800 text-slate-400 transition-colors" aria-label="Close"><X className="w-4 h-4" /></button>
</div>
<div className="flex-1 min-h-0">
{yamlError ? (
Expand Down
1 change: 1 addition & 0 deletions src/renderer/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const PROVIDER_SECTIONS = new Set<ResourceKind>([
'traefik-tlsoptions', 'traefik-tlsstores', 'traefik-serverstransporttcps',
'nginx-virtualservers', 'nginx-virtualserverroutes', 'nginx-policies', 'nginx-transportservers',
'keda-scaledobjects', 'keda-scaledjobs', 'keda-triggerauthentications', 'keda-clustertriggerauthentications',
'ambassador-mappings', 'ambassador-hosts', 'ambassador-tlscontexts', 'ambassador-tcpmappings',
])

// ─── Labels ───────────────────────────────────────────────────────────────────
Expand Down
5 changes: 3 additions & 2 deletions src/renderer/store/slices/clusterSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,8 @@ export const createClusterSlice: StoreSlice<ClusterSlice> = (set, get) => {
const isProviderSection = currentSection.startsWith('istio-') ||
currentSection.startsWith('traefik-') ||
currentSection.startsWith('nginx-') ||
currentSection.startsWith('keda-')
currentSection.startsWith('keda-') ||
currentSection.startsWith('ambassador-')
set({
selectedContext: name, isProduction: isProd, loadingNamespaces: true, loadingResources: true,
namespaces: [], selectedNamespace: null, selectedResource: null, error: null,
Expand All @@ -223,7 +224,7 @@ export const createClusterSlice: StoreSlice<ClusterSlice> = (set, get) => {
metricsError: null,
// Reset provider detection so stale flags from the old cluster don't
// briefly show sidebar groups that don't exist in the new cluster.
providers: { istio: false, traefik: false, nginxInc: false, nginxCommunity: false, keda: false, cilium: false, hubbleRelay: false },
providers: { istio: false, traefik: false, nginxInc: false, nginxCommunity: false, keda: false, cilium: false, hubbleRelay: false, ambassador: false },
// Navigate away from provider-specific sections so ProviderResourcePanel
// doesn't attempt a fetch against a cluster that may lack those CRDs.
...(isProviderSection ? { section: 'dashboard' as const } : {}),
Expand Down
6 changes: 3 additions & 3 deletions src/renderer/store/slices/providersSlice.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ describe('providersSlice', () => {
beforeEach(() => {
state = {
selectedContext: 'ctx-a',
providers: { istio: false, traefik: false, nginxInc: false, nginxCommunity: false, keda: false, cilium: false, hubbleRelay: false },
providers: { istio: false, traefik: false, nginxInc: false, nginxCommunity: false, keda: false, cilium: false, hubbleRelay: false, ambassador: false },
providersLoading: false,
}
set = vi.fn((update: any) => {
Expand Down Expand Up @@ -67,7 +67,7 @@ describe('providersSlice', () => {
expect(set).toHaveBeenCalledWith({ providersLoading: true })
// Error path: reset to defaults
expect(set).toHaveBeenCalledWith({
providers: { istio: false, traefik: false, nginxInc: false, nginxCommunity: false, keda: false, cilium: false, hubbleRelay: false },
providers: { istio: false, traefik: false, nginxInc: false, nginxCommunity: false, keda: false, cilium: false, hubbleRelay: false, ambassador: false },
providersLoading: false,
})
})
Expand Down Expand Up @@ -96,6 +96,6 @@ describe('providersSlice', () => {
expect(set).toHaveBeenCalledWith({ providersLoading: true })
expect(set).toHaveBeenCalledWith({ providersLoading: false })
// providers state must not have been updated.
expect(state.providers).toEqual({ istio: false, traefik: false, nginxInc: false, nginxCommunity: false, keda: false, cilium: false, hubbleRelay: false })
expect(state.providers).toEqual({ istio: false, traefik: false, nginxInc: false, nginxCommunity: false, keda: false, cilium: false, hubbleRelay: false, ambassador: false })
})
})
1 change: 1 addition & 0 deletions src/renderer/store/slices/providersSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const defaultProviders: ProviderSet = {
keda: false,
cilium: false,
hubbleRelay: false,
ambassador: false,
}

export const createProvidersSlice: StoreSlice<ProvidersSlice> = (set, get) => {
Expand Down
1 change: 1 addition & 0 deletions src/renderer/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,5 @@ export interface ProviderSet {
keda: boolean
cilium: boolean
hubbleRelay: boolean
ambassador: boolean
}
5 changes: 5 additions & 0 deletions src/renderer/types/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,8 @@ export type ResourceKind =
| 'keda-scaledjobs'
| 'keda-triggerauthentications'
| 'keda-clustertriggerauthentications'
// Ambassador API gateway
| 'ambassador-mappings'
| 'ambassador-hosts'
| 'ambassador-tlscontexts'
| 'ambassador-tcpmappings'
Loading