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 { Input, Label } from './ui';
|
||||||
import { Alert, AlertDescription } from './ui/alert';
|
import { Alert, AlertDescription } from './ui/alert';
|
||||||
import { Textarea } from './ui/textarea';
|
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 { Badge } from './ui/badge';
|
||||||
import { useConfig } from '../hooks';
|
import { useConfig } from '../hooks';
|
||||||
import { useHaproxy } from '../hooks/useHaproxy';
|
import { useHaproxy } from '../hooks/useHaproxy';
|
||||||
|
import { useServices } from '../hooks/useServices';
|
||||||
import type { HAProxyCustomRoute } from '../types';
|
import type { HAProxyCustomRoute } from '../types';
|
||||||
import type { HaproxyInstanceRoute } from '../services/api/networking';
|
|
||||||
import { haproxyApi } from '../services/api/networking';
|
import { haproxyApi } from '../services/api/networking';
|
||||||
|
|
||||||
export function IngressProxyComponent() {
|
export function IngressProxyComponent() {
|
||||||
@@ -27,6 +27,8 @@ export function IngressProxyComponent() {
|
|||||||
isRestarting: isHaproxyRestarting,
|
isRestarting: isHaproxyRestarting,
|
||||||
} = useHaproxy();
|
} = useHaproxy();
|
||||||
|
|
||||||
|
const { services, isLoading: isServicesLoading } = useServices();
|
||||||
|
|
||||||
const [showAddCustomRoute, setShowAddCustomRoute] = useState(false);
|
const [showAddCustomRoute, setShowAddCustomRoute] = useState(false);
|
||||||
const [newCustomRoute, setNewCustomRoute] = useState<HAProxyCustomRoute>({ name: '', port: 0, backend: '' });
|
const [newCustomRoute, setNewCustomRoute] = useState<HAProxyCustomRoute>({ name: '', port: 0, backend: '' });
|
||||||
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
||||||
@@ -133,7 +135,7 @@ export function IngressProxyComponent() {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<CardTitle>Instance Routes</CardTitle>
|
<CardTitle>Registered Services</CardTitle>
|
||||||
{isHaproxyLoading ? (
|
{isHaproxyLoading ? (
|
||||||
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
||||||
) : (
|
) : (
|
||||||
@@ -145,37 +147,39 @@ export function IngressProxyComponent() {
|
|||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
{(() => {
|
{isServicesLoading ? (
|
||||||
const routes: HaproxyInstanceRoute[] = haproxyStatus?.routes ?? [];
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
return routes.length > 0 ? (
|
<Loader2 className="h-4 w-4 animate-spin" />Loading services...
|
||||||
<div className="border rounded-lg divide-y text-sm">
|
</div>
|
||||||
{routes.map(r => (
|
) : services.length > 0 ? (
|
||||||
<div key={r.name} className="px-3 py-2">
|
<div className="border rounded-lg divide-y text-sm">
|
||||||
<div className="flex items-center justify-between">
|
{services.map(s => (
|
||||||
<div className="flex items-center gap-2">
|
<div key={s.name} className="px-3 py-2">
|
||||||
<CheckCircle className="h-3.5 w-3.5 text-green-500 shrink-0" />
|
<div className="flex items-center justify-between">
|
||||||
<span className="font-mono text-xs">*.{r.domain}</span>
|
<div className="flex items-center gap-2">
|
||||||
</div>
|
{s.reach === 'public' ? (
|
||||||
<span className="font-mono text-xs text-muted-foreground">{r.backendIP}</span>
|
<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>
|
</div>
|
||||||
{r.extraDomains && r.extraDomains.length > 0 && (
|
<span className="font-mono text-xs text-muted-foreground">{s.backend.address}</span>
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
<div className="mt-1 ml-5 flex flex-wrap gap-2 text-xs text-muted-foreground">
|
||||||
</div>
|
<span>source: <span className="text-foreground">{s.source}</span></span>
|
||||||
) : (
|
<span>reach: <span className="text-foreground">{s.reach}</span></span>
|
||||||
<p className="text-xs text-muted-foreground">
|
<span>type: <span className="text-foreground">{s.backend.type}</span></span>
|
||||||
No instances registered — click Generate & Apply to configure.
|
</div>
|
||||||
</p>
|
</div>
|
||||||
);
|
))}
|
||||||
})()}
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
No services registered — click Generate & Apply to configure.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
{haproxyGenerateData && (
|
{haproxyGenerateData && (
|
||||||
<Alert className="border-green-200 bg-green-50 dark:border-green-800 dark:bg-green-950/20">
|
<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 { apiClient, ApiError } from './client';
|
||||||
export * from './types';
|
export * from './types';
|
||||||
export { dnsmasqApi } from './dnsmasq';
|
export { dnsmasqApi } from './dnsmasq';
|
||||||
export { haproxyApi, ddnsApi, dhcpApi, secretsApi, crowdSecApi } from './networking';
|
export { servicesApi, haproxyApi, ddnsApi, dhcpApi, secretsApi, crowdSecApi } from './networking';
|
||||||
export type { HaproxyStatus, HaproxyGenerateResult, DdnsStatus, DhcpLease, DhcpStaticLease, BackendStat, CrowdSecStatus, CrowdSecMachine, CrowdSecBouncer, CrowdSecDecision, CrowdSecProvisionResult } from './networking';
|
export type { RegisteredService, HaproxyStatus, HaproxyGenerateResult, DdnsStatus, DhcpLease, DhcpStaticLease, BackendStat, CrowdSecStatus, CrowdSecMachine, CrowdSecBouncer, CrowdSecDecision, CrowdSecProvisionResult } from './networking';
|
||||||
export { vpnApi } from './vpn';
|
export { vpnApi } from './vpn';
|
||||||
export type { VpnStatus, VpnConfig, VpnPeer, VpnPeerConfig } from './vpn';
|
export type { VpnStatus, VpnConfig, VpnPeer, VpnPeerConfig } from './vpn';
|
||||||
export { certApi } from './cert';
|
export { certApi } from './cert';
|
||||||
|
|||||||
@@ -1,5 +1,26 @@
|
|||||||
import { apiClient } from './client';
|
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
|
// HAProxy
|
||||||
|
|
||||||
export interface HaproxyInstanceRoute {
|
export interface HaproxyInstanceRoute {
|
||||||
|
|||||||
Reference in New Issue
Block a user