feat: Show Registered Services on Ingress Proxy page
Replace "Instance Routes" with "Registered Services" on the Central Ingress page. Services are fetched from /api/v1/services and display name, domain, backend, source, reach level, and backend type. Central no longer manages instances directly — it shows services registered by Wild Cloud, Wild Works, and itself. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,12 +4,12 @@ import { Button } from './ui/button';
|
||||
import { Input, Label } from './ui';
|
||||
import { Alert, AlertDescription } from './ui/alert';
|
||||
import { Textarea } from './ui/textarea';
|
||||
import { Network, Loader2, AlertCircle, CheckCircle, Play, RotateCw, Plus, Trash2, X, Check, Activity, ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { Network, Loader2, AlertCircle, CheckCircle, Play, RotateCw, Plus, Trash2, X, Check, Activity, ChevronDown, ChevronUp, Globe, Server } from 'lucide-react';
|
||||
import { Badge } from './ui/badge';
|
||||
import { useConfig } from '../hooks';
|
||||
import { useHaproxy } from '../hooks/useHaproxy';
|
||||
import { useServices } from '../hooks/useServices';
|
||||
import type { HAProxyCustomRoute } from '../types';
|
||||
import type { HaproxyInstanceRoute } from '../services/api/networking';
|
||||
import { haproxyApi } from '../services/api/networking';
|
||||
|
||||
export function IngressProxyComponent() {
|
||||
@@ -27,6 +27,8 @@ export function IngressProxyComponent() {
|
||||
isRestarting: isHaproxyRestarting,
|
||||
} = useHaproxy();
|
||||
|
||||
const { services, isLoading: isServicesLoading } = useServices();
|
||||
|
||||
const [showAddCustomRoute, setShowAddCustomRoute] = useState(false);
|
||||
const [newCustomRoute, setNewCustomRoute] = useState<HAProxyCustomRoute>({ name: '', port: 0, backend: '' });
|
||||
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
||||
@@ -133,7 +135,7 @@ export function IngressProxyComponent() {
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle>Instance Routes</CardTitle>
|
||||
<CardTitle>Registered Services</CardTitle>
|
||||
{isHaproxyLoading ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
||||
) : (
|
||||
@@ -145,37 +147,39 @@ export function IngressProxyComponent() {
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{(() => {
|
||||
const routes: HaproxyInstanceRoute[] = haproxyStatus?.routes ?? [];
|
||||
return routes.length > 0 ? (
|
||||
{isServicesLoading ? (
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />Loading services...
|
||||
</div>
|
||||
) : services.length > 0 ? (
|
||||
<div className="border rounded-lg divide-y text-sm">
|
||||
{routes.map(r => (
|
||||
<div key={r.name} className="px-3 py-2">
|
||||
{services.map(s => (
|
||||
<div key={s.name} className="px-3 py-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="h-3.5 w-3.5 text-green-500 shrink-0" />
|
||||
<span className="font-mono text-xs">*.{r.domain}</span>
|
||||
</div>
|
||||
<span className="font-mono text-xs text-muted-foreground">{r.backendIP}</span>
|
||||
</div>
|
||||
{r.extraDomains && r.extraDomains.length > 0 && (
|
||||
<div className="mt-1 ml-5 flex flex-wrap gap-1">
|
||||
{r.extraDomains.map(d => (
|
||||
<span key={d} className="font-mono text-xs text-muted-foreground bg-muted rounded px-1.5 py-0.5">
|
||||
{d}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
{s.reach === 'public' ? (
|
||||
<Globe className="h-3.5 w-3.5 text-blue-500 shrink-0" />
|
||||
) : (
|
||||
<Server className="h-3.5 w-3.5 text-green-500 shrink-0" />
|
||||
)}
|
||||
<span className="font-medium">{s.name}</span>
|
||||
<span className="font-mono text-xs text-muted-foreground">{s.domain}</span>
|
||||
</div>
|
||||
<span className="font-mono text-xs text-muted-foreground">{s.backend.address}</span>
|
||||
</div>
|
||||
<div className="mt-1 ml-5 flex flex-wrap gap-2 text-xs text-muted-foreground">
|
||||
<span>source: <span className="text-foreground">{s.source}</span></span>
|
||||
<span>reach: <span className="text-foreground">{s.reach}</span></span>
|
||||
<span>type: <span className="text-foreground">{s.backend.type}</span></span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
No instances registered — click Generate & Apply to configure.
|
||||
No services registered — click Generate & Apply to configure.
|
||||
</p>
|
||||
);
|
||||
})()}
|
||||
)}
|
||||
|
||||
{haproxyGenerateData && (
|
||||
<Alert className="border-green-200 bg-green-50 dark:border-green-800 dark:bg-green-950/20">
|
||||
|
||||
17
web/src/hooks/useServices.ts
Normal file
17
web/src/hooks/useServices.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { servicesApi } from '../services/api/networking';
|
||||
|
||||
export const useServices = () => {
|
||||
const query = useQuery({
|
||||
queryKey: ['services'],
|
||||
queryFn: () => servicesApi.list(),
|
||||
refetchInterval: 30000,
|
||||
staleTime: 10000,
|
||||
});
|
||||
|
||||
return {
|
||||
services: query.data?.services ?? [],
|
||||
isLoading: query.isLoading,
|
||||
error: query.error,
|
||||
};
|
||||
};
|
||||
@@ -1,8 +1,8 @@
|
||||
export { apiClient, ApiError } from './client';
|
||||
export * from './types';
|
||||
export { dnsmasqApi } from './dnsmasq';
|
||||
export { haproxyApi, ddnsApi, dhcpApi, secretsApi, crowdSecApi } from './networking';
|
||||
export type { HaproxyStatus, HaproxyGenerateResult, DdnsStatus, DhcpLease, DhcpStaticLease, BackendStat, CrowdSecStatus, CrowdSecMachine, CrowdSecBouncer, CrowdSecDecision, CrowdSecProvisionResult } from './networking';
|
||||
export { servicesApi, haproxyApi, ddnsApi, dhcpApi, secretsApi, crowdSecApi } from './networking';
|
||||
export type { RegisteredService, HaproxyStatus, HaproxyGenerateResult, DdnsStatus, DhcpLease, DhcpStaticLease, BackendStat, CrowdSecStatus, CrowdSecMachine, CrowdSecBouncer, CrowdSecDecision, CrowdSecProvisionResult } from './networking';
|
||||
export { vpnApi } from './vpn';
|
||||
export type { VpnStatus, VpnConfig, VpnPeer, VpnPeerConfig } from './vpn';
|
||||
export { certApi } from './cert';
|
||||
|
||||
@@ -1,5 +1,26 @@
|
||||
import { apiClient } from './client';
|
||||
|
||||
// Services
|
||||
|
||||
export interface RegisteredService {
|
||||
name: string;
|
||||
source: string;
|
||||
domain: string;
|
||||
backend: {
|
||||
address: string;
|
||||
type: string;
|
||||
health?: string;
|
||||
};
|
||||
reach: string;
|
||||
tls?: string;
|
||||
}
|
||||
|
||||
export const servicesApi = {
|
||||
async list(): Promise<{ services: RegisteredService[] }> {
|
||||
return apiClient.get('/api/v1/services');
|
||||
},
|
||||
};
|
||||
|
||||
// HAProxy
|
||||
|
||||
export interface HaproxyInstanceRoute {
|
||||
|
||||
Reference in New Issue
Block a user