From 22e3d6960e05b94f6e2d6c290f555182d98041be Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sun, 21 Jun 2026 18:58:01 +0200 Subject: [PATCH] feat(v5): add app configuration inspector Add a sheet-based dashboard inspector with overview, networking, and advanced tabs for applications. Switch Caddy proxy actions to the generic Flux ingress apply and stop payloads. --- app/Actions/V5/Proxy/StartCaddyIngress.php | 17 +- app/Actions/V5/Proxy/StopCaddyIngress.php | 2 +- app/Services/Flux/FluxClient.php | 18 +- resources/js/v5/Pages/Dashboard.tsx | 169 ++++++++++++++++++ resources/js/v5/components/ui/sheet.tsx | 15 +- resources/js/v5/components/ui/tabs.tsx | 63 +++++++ tests/Feature/V5/DashboardTest.php | 56 ++++-- .../Unit/V5/CaddyIngressConfigurationTest.php | 11 +- 8 files changed, 317 insertions(+), 34 deletions(-) create mode 100644 resources/js/v5/components/ui/tabs.tsx diff --git a/app/Actions/V5/Proxy/StartCaddyIngress.php b/app/Actions/V5/Proxy/StartCaddyIngress.php index d11f089c1..3b2e8b6da 100644 --- a/app/Actions/V5/Proxy/StartCaddyIngress.php +++ b/app/Actions/V5/Proxy/StartCaddyIngress.php @@ -27,7 +27,7 @@ class StartCaddyIngress } $configuration = GenerateCaddyIngressConfiguration::run($this->applications($server)); - $output = $this->fluxClient->applyCaddyIngress($hostId, $configuration['caddyfile'], $configuration['apps']); + $output = $this->fluxClient->applyIngress($hostId, 'caddy', $configuration['caddyfile'], $this->ingressApps($configuration['apps'])); if ($server->exists) { $server->update([ @@ -39,6 +39,21 @@ class StartCaddyIngress return $output; } + /** + * @param array $apps + * @return array + */ + private function ingressApps(array $apps): array + { + return array_map( + fn (array $app): array => [ + 'name' => $app['name'], + 'config' => $app['caddyfile'], + ], + $apps + ); + } + /** * @return Collection */ diff --git a/app/Actions/V5/Proxy/StopCaddyIngress.php b/app/Actions/V5/Proxy/StopCaddyIngress.php index 12ca01421..79645236d 100644 --- a/app/Actions/V5/Proxy/StopCaddyIngress.php +++ b/app/Actions/V5/Proxy/StopCaddyIngress.php @@ -20,7 +20,7 @@ class StopCaddyIngress return 'Server is missing its Flux host id.'; } - $output = $this->fluxClient->stopCaddyIngress($hostId); + $output = $this->fluxClient->stopIngress($hostId, 'caddy'); if ($server->exists) { $server->update(['ingress_status' => 'exited']); diff --git a/app/Services/Flux/FluxClient.php b/app/Services/Flux/FluxClient.php index 006cae06f..53644b17f 100644 --- a/app/Services/Flux/FluxClient.php +++ b/app/Services/Flux/FluxClient.php @@ -22,27 +22,29 @@ class FluxClient } /** - * @param array $apps + * @param array $apps */ - public function applyCaddyIngress(string $hostId, string $caddyfile, array $apps = [], string $meshNetwork = 'coolify-default-mesh'): string + public function applyIngress(string $hostId, string $kind, string $config, array $apps = [], string $meshNetwork = 'coolify-default-mesh'): string { $payload = $this->dispatch($hostId, [ - 'type' => 'apply_caddy_ingress', - 'caddyfile' => $caddyfile, + 'type' => 'ingress.apply', + 'kind' => $kind, + 'config' => $config, 'apps' => $apps, 'mesh_network' => $meshNetwork, ]); - return $this->output($payload, 'Caddy ingress applied.'); + return $this->output($payload, 'Ingress applied.'); } - public function stopCaddyIngress(string $hostId): string + public function stopIngress(string $hostId, string $kind): string { $payload = $this->dispatch($hostId, [ - 'type' => 'stop_caddy_ingress', + 'type' => 'ingress.stop', + 'kind' => $kind, ]); - return $this->output($payload, 'Caddy ingress stopped.'); + return $this->output($payload, 'Ingress stopped.'); } /** diff --git a/resources/js/v5/Pages/Dashboard.tsx b/resources/js/v5/Pages/Dashboard.tsx index 6fc9862fe..bbdd64489 100644 --- a/resources/js/v5/Pages/Dashboard.tsx +++ b/resources/js/v5/Pages/Dashboard.tsx @@ -6,6 +6,9 @@ import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Field, FieldLabel } from '@/components/ui/field'; import { Input } from '@/components/ui/input'; +import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@/components/ui/sheet'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Textarea } from '@/components/ui/textarea'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { resolveCanvasNodeLayout, resolveCanvasNodePosition, type CanvasNodeBounds } from '@/lib/canvas-collision'; import { csrfToken } from '@/lib/csrf'; @@ -152,6 +155,7 @@ export default function Dashboard({ const [connections, setConnections] = useState(initialResourceConnections); const [selectedConnectionId, setSelectedConnectionId] = useState(null); const [selectedApplicationId, setSelectedApplicationId] = useState(null); + const [selectedInspectorApplicationId, setSelectedInspectorApplicationId] = useState(null); const [connectionPortInput, setConnectionPortInput] = useState>({}); const [draftConnection, setDraftConnection] = useState(null); const [viewport, setViewport] = useState({ x: 0, y: 0, zoom: 1 }); @@ -173,6 +177,10 @@ export default function Dashboard({ }), [applications], ); + const selectedInspectorApplication = useMemo( + () => applications.find((application) => application.id === selectedInspectorApplicationId) ?? null, + [applications, selectedInspectorApplicationId], + ); useEffect(() => { const settledResources = settleCanvasResources(initialApplications, caddyIngresses); @@ -183,6 +191,7 @@ export default function Dashboard({ setSelectedNginxServerId((currentServerId) => currentServerId || nginxServers[0]?.id || ''); setSelectedConnectionId(null); setSelectedApplicationId(null); + setSelectedInspectorApplicationId(null); centerOnCanvasNodes(settledResources.applications, settledResources.ingresses); }, [initialApplications, caddyIngresses, initialResourceConnections, selectedProjectUuid, selectedEnvironmentUuid]); @@ -1029,6 +1038,12 @@ function normalizeConnection(connection: V5ResourceConnection): CanvasConnection setSelectedApplicationId(null); } + function openApplicationInspector(event: MouseEvent, application: V5Application): void { + event.stopPropagation(); + setSelectedApplicationId(application.id); + setSelectedInspectorApplicationId(application.id); + } + function connectionTargetFromPointer(event: PointerEvent): HTMLElement | null { const pointerTarget = document.elementFromPoint(event.clientX, event.clientY) as HTMLElement | null; @@ -1586,6 +1601,7 @@ function normalizeConnection(connection: V5ResourceConnection): CanvasConnection transform: `translate3d(${application.canvasX}px, ${application.canvasY}px, 0)`, }} onPointerDown={(event) => startApplicationDrag(event, application)} + onDoubleClick={(event) => openApplicationInspector(event, application)} > {CONNECTOR_SIDES.map((side) => (