Saltar a contenido

Cortex Panel — runtime frontend

Cómo el shell React consume manifests y dashboards CUS por HTTP. Para declarar UI desde Python, usa la guía Plugins.

Ver ADR 003, ADR 018 y contexto del proyecto.

Arquitectura npm

Paquete Rol
@cortex/panel-core Headless: tipos, PanelProvider, nav, routing, PanelApiClient
@cortex/panel-shadcn Skin shadcn/ui + Tailwind v4: shell, widgets, DashboardRenderer
@cortex/web createPanelShellApp — rutas, provider, API client
framework/web-shadcn Demo Vite → :5175
flowchart TB
  subgraph demo [Demo Vite]
    SH[framework/web-shadcn]
  end
  subgraph web ["@cortex/web"]
    W["createPanelShellApp"]
  end
  subgraph skin ["@cortex/panel-shadcn"]
    SK[widgets + PanelShell]
  end
  subgraph core ["@cortex/panel-core"]
    C["PanelProvider navUtils PanelApiClient"]
  end
  SH --> W --> SK --> C

Arranque local

npm install              # raíz — workspaces
npm run dev:web-shadcn   # http://localhost:5175
npm run build:web-shadcn

Vite hace proxy de /apihttp://localhost:8000. La API debe estar en marcha (uv run cortex-api).

Flujo runtime

  1. En bootstrap, el plugin registra UI (register_resources o register_dashboards).
  2. El shell carga rutas con GET /api/v1/panels/routes.
  3. ModuleScreen resuelve pantalla → GET /api/v1/ui/dashboards/{dashboardId}.
  4. WidgetRenderer instancia el componente según widget.type.
sequenceDiagram
  participant API as FastAPI
  participant Shell as PanelProvider
  participant M as ModuleScreen
  participant W as Widgets

  Shell->>API: GET /api/v1/panels/{panelId}
  M->>API: GET manifest + dashboard
  M->>W: layout + configs
  W->>API: fetchPath datos y forms

Componentes clave

Componente Responsabilidad
PanelProvider Contexto API, registry, refresh entre widgets
PanelShell Sidebar, header, slots de render hooks
ModuleScreen Pantalla activa → dashboard
DashboardRenderer Grid de widgets
WidgetRenderer Lookup registry.get(type)

Jerarquía de títulos

El shell shadcn aplica un contrato de un título visible por pantalla (ver ADR 018):

Capa Responsabilidad
PanelShell h1 = screens[].title de la pantalla activa
ModuleScreen h2 solo si theme.shellOwnsPageTitle es false
DashboardRenderer h3 solo si layout.hideTitle es false
Widgets (data-table, form, infolist) CardTitle solo si config.title está definido

Flags en PanelConfiguration.theme:

Flag Default (shadcn) Efecto
shellOwnsPageTitle true El header del shell muestra el título; ModuleScreen omite h2
showPanelSubtitle true Muestra panel.title como subtítulo gris sobre el h1

En Python, emit_dashboard(..., hide_title=True) suprime el título del dashboard; el título vive en screens[].title. ResourceBuilder lo aplica por defecto en list/create/edit/view.

flowchart TB
  subgraph titles [Jerarquía de títulos]
    Shell["PanelShell h1 screen.title"]
    Module["ModuleScreen h2 opcional"]
    Dash["DashboardRenderer h3 si no hideTitle"]
    Widget["Widget CardTitle si config.title"]
  end
  Shell --> Module --> Dash --> Widget

Resolución de pantallas

ModuleScreen y PanelShell usan matchModuleScreen de @cortex/panel-core:

  1. Calculan la ruta relativa con relativeModulePath(pathname, moduleUrl).
  2. Evalúan todas las entradas de manifest.screens con dashboardId.
  3. Eligen la pantalla con mayor especificidad (scoreScreenPath): segmentos literales antes que parámetros (:id).

Ejemplo: la ruta relativa create coincide con path: "create" y con path: ":id", pero gana la literal.

import { matchModuleScreen, relativeModulePath } from '@cortex/panel-core';

const relative = relativeModulePath('/panel/clients/create', '/panel/clients');
const { screen, params } = matchModuleScreen(manifest, relative);
// screen.path === 'create'

Si una acción CREATE navega a create, el manifest debe declarar una pantalla con path: "create" (o usar ResourceBuilder, que la genera automáticamente).

PanelApiClient

Rutas habituales (packages/cortex-panel-core/src/panelApiClient.ts):

Método Endpoint
listPanelRoutes() GET /api/v1/panels/routes
getPanel(panelId) GET /api/v1/panels/{panelId}
getModuleManifest(...) GET /api/v1/panels/{panelId}/modules/{moduleId}/manifest
getDashboard(id) GET /api/v1/ui/dashboards/{id}
getForm(formId) GET /api/v1/forms/{formId}
fetchPath(path) Cualquier ruta de datos del plugin

Widget registry

import { createDefaultRegistry } from '@cortex/panel-shadcn';

const registry = createDefaultRegistry();
registry.register('mi-widget', MiWidget);

Catálogo de tipos: Widgets (plugins).

Shell mínimo

import { createPanelShellApp } from '@cortex/web';
import { PanelShell, createDefaultRegistry } from '@cortex/panel-shadcn';

export const { AppPanelProvider, AppPanelRoutes } = createPanelShellApp({
  PanelShell,
  createDefaultRegistry,
});

Multi-módulo en un panel

GET /api/v1/panels/panel devuelve modules[], navGroups[] y navigation[] — sidebar agrupado con orden por navSort. Varios plugins contribuyen módulos sin compartir código React.

flowchart TB
  subgraph panelOperativo [Panel panel]
    Shell[PanelShell sidebar único]
    B[cortex_plugin_booking]
    C[cortex_plugin_clients]
    P[cortex_plugin_pricing]
  end
  Shell --> B
  Shell --> C
  Shell --> P
  B --> API_B["/api/v1/booking/"]
  C --> API_C["/api/v1/clients/"]
  P --> API_P["/api/v1/pricing/"]

Panel Control

Módulo embebido cortex_framework.control — rutas /control/, API /api/v1/control/plugins. No es plugin pip.

Validación CUS

cortex_framework.panel.validate — schemas en framework/cortex_framework/panel/schemas/.

Siguiente lectura