openova/products/catalyst/bootstrap/ui/vite.config.ts
e3mrah 2c6595a378
feat(openova-flow): npm workspaces + FlowPage canvas real-adapter rewire (Agent #5) (#1399)
Lands the OpenovaFlow Foundation end-to-end so the catalyst-ui FlowPage
consumes the new openova-flow-server's merged multi-region SSE stream
(`GET /api/v1/flows/{deploymentId}/stream`) and renders the per-region
adapter-flux emissions directly via @openova/flow-canvas. Closes the
revert from PR #1394 and unblocks the prov #34 multi-region 2-bubble
demo (fsn1 + hel1 each install bp-gateway-api → two bubbles).

# What ships

## A. npm workspaces at repo root

  • New `package.json` declares `openova-monorepo` private root with
    three workspaces: products/openova-flow/{core,canvas} +
    products/catalyst/bootstrap/ui.
  • Root `package-lock.json` resolves @openova/flow-* as workspace
    symlinks into the hoisted node_modules tree.
  • react / react-dom / d3-* are now hoisted into the monorepo's root
    node_modules, so flow-canvas's bare `import 'react'` resolves via
    standard upward-walking node_modules — no per-package sibling
    node_modules required (the root cause of PR #1389's build break).

## B. Catalyst-ui consumes @openova/flow-* via file: deps

  • catalyst-ui's `package.json` adds `@openova/flow-core` and
    `@openova/flow-canvas` as `file:../../../openova-flow/{core,canvas}`
    deps so `npm ci` from within catalyst-ui (today's CI path) keeps
    working without needing root-level `npm ci -ws`.
  • Vite `resolve.alias` + tsconfig `paths` bind `@openova/flow-core`
    and `@openova/flow-canvas` to the source-only `./src/index.ts`
    entry points. `dedupe: ['react', 'react-dom']` guards against
    double-instancing.
  • `tsconfig.app.json` `include` adds the two flow-package src trees
    so tsc covers them with catalyst-ui's strict settings (instead of
    each package's standalone `tsc -p tsconfig.json`, which lacks the
    React/d3 node_modules siblings).

## C. New SSE consumer + bridge

  • `src/lib/openflow-adapter-sse.ts` — `useFlowStream` React hook +
    pure `reduceFlowMessage` reducer. Consumes the contract verbatim
    (snapshot / upsert-flow / upsert-nodes / upsert-rels / delete-nodes
    / delete-rels). Owns the EventSource lifecycle, GET /snapshot
    pre-paint, capped exponential reconnect.
  • `src/lib/flow-bridge.ts` — catalyst-specific glue:
    `CATALYST_STATUS_PALETTE` (mirrors `--bubble-*` CSS tokens onto
    `StatusTone`), `flowStateToArrays` (Map→Array materialiser),
    `regionDescriptorsFromFlow` (derives FlowCanvas regions from live
    region tags + optional wizard-store augmentation), and
    `rollupFlowStatus` (provisioning-status rollup on the new
    contract).
  • NOT a Job-shape bridge — the legacy Job adapter from PR #1389
    is gone. catalyst-ui never goes through Catalyst's legacy Job model
    again; the SSE stream IS the source of truth.

## D. FlowPage.tsx rewired

  • Drives `FlowCanvas` from `@openova/flow-canvas` directly off the
    new hook.
  • Multi-region support comes for free: per-region adapter-flux tags
    every emitted FlowNode with `region: '<location-code>'`; the
    canvas's swimlane layout buckets by `region`. Single-region
    provisions render identically to before via a synthetic
    fallback descriptor.
  • Embedded mode preserved for JobDetail.

## E. Containerfile preserves CI build

  • COPY products/openova-flow/{core,canvas}/{package.json,src/}
    BEFORE `npm ci` so `file:` deps validate. Subsequent
    `COPY products/` layers the rest (CONTRACT.md etc.) in.

# Tests

  • 23 new tests, 0 regressions on adjacent areas:
    - `openflow-adapter-sse.test.ts` (6) — reducer covers all 6
      FlowMessage variants including delete-nodes' rel-prune cascade
      AND a multi-region merge case (fsn1 + hel1 both install
      bp-gateway-api).
    - `flow-bridge.test.ts` (10) — palette completeness, Map→Array
      ordering, region descriptor derivation/fallback, status rollup
      including group-exclusion and terminal-failure detection.
    - `FlowPage.test.tsx` (7) — empty-state mount, StatusStrip, no
      legacy mode toggle, embedded variant.
  • flow-core: 20/20 passing; flow-canvas: 9/9 passing.
  • Vitest full suite: 1130 pass / 87 fail (87 fails are pre-existing
    on main and unrelated — PinInput6, ProvisionPage, etc.). Baseline
    on main is 1052 pass / 88 fail / 27 failed files; this PR brings
    78 new passing tests and lowers failing files from 27 → 18.

# Constraints honoured (Rule 7)

  • NO `vite build` / `next build` / `npm run build` / `npx playwright
    test` / `npx playwright install`. Only `tsc --noEmit` + `vitest
    run` + `npm install --package-lock-only`.
  • NO `kubectl apply` / chart manifests touched (Rule 11).
  • NO hardcoded URLs / regions / k3s flags. Endpoint composed from
    `API_BASE`; regions derived from live FlowNode tags; deploymentId
    from `useParams` (Rule 18).
  • Two-repo discipline: openova-io/openova only (Rule 21).
  • Conventional commit + Claude co-author footer (Rule 20).
  • isolation:"worktree" — work landed in a dedicated worktree.

# Canonical-seam citations (ARCHITECT-FIRST)

  1. PR #1389's `flow-bridge.ts` — reference for the shape of a
     catalyst-ui→@openova/flow contract layer. NOT conflated: that
     bridge translated legacy Catalyst Jobs into FlowNodes; this one
     consumes the new SSE FlowMessage stream directly with no Job
     intermediary.
  2. `useDeploymentEvents.ts` (line 526+, `openStream` + `onerror`
     reconnect + capped retry) — canonical SSE consumer pattern in
     this codebase. `useFlowStream` mirrors it (capped exponential
     backoff, idempotent reducer over replayed buffered events).

# Definition of Done — post-merge verification plan

  1. CI green (catalyst-build builds the new Containerfile path).
  2. `curl -k -b /tmp/cz-cookie-prov27.txt
     'https://console.openova.io/sovereign/api/v1/flows/5a175e0a88c99cec/snapshot' | jq`
     → nodes[] contains BOTH `fsn1/bp-gateway-api` AND `hel1/bp-gateway-api`.
  3. Browser test: navigate to
     `https://console.openova.io/sovereign/provision/5a175e0a88c99cec/jobs/install-gateway-api`
     → expect TWO bubbles (one per region).
  4. If snapshot is empty, inspect emitter DaemonSets:
     `kubectl --context=omantel get pods -n openova-flow`.

Co-authored-by: hatiyildiz <269457768+hatiyildiz@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 16:59:07 +04:00

113 lines
4.5 KiB
TypeScript

/// <reference types="vitest" />
import type { Plugin } from 'vite'
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
import { resolve } from 'path'
import { spawnSync } from 'node:child_process'
const REPO_ROOT = resolve(__dirname, '../../../..')
/**
* Vite plugin that re-runs scripts/build-catalog.mjs whenever any
* platform/<name>/blueprint.yaml or products/<name>/blueprint.yaml changes
* during dev. Keeps the wizard's StepComponents grid live-reloading without
* requiring a manual `npm run build:catalog` after every Blueprint edit.
*
* Build mode runs the same script via `prebuild` in package.json, so this
* plugin only matters in dev.
*/
function rebuildCatalogOnYamlChange(): Plugin {
const catalogScript = resolve(__dirname, 'scripts/build-catalog.mjs')
function runCatalog(reason: string) {
const r = spawnSync(process.execPath, [catalogScript], { stdio: 'inherit' })
if (r.status !== 0) {
// eslint-disable-next-line no-console
console.error(`[build-catalog] failed (${reason})`)
}
}
return {
name: 'catalyst-rebuild-catalog',
apply: 'serve',
configureServer(server) {
// Watch the entire monorepo's platform/ + products/ for blueprint.yaml
// changes. Vite's chokidar instance handles dedup + glob.
server.watcher.add([
resolve(REPO_ROOT, 'platform/**/blueprint.yaml'),
resolve(REPO_ROOT, 'products/**/blueprint.yaml'),
])
const isBlueprintFile = (path: string) => /(?:^|\/)blueprint\.yaml$/.test(path)
const handle = (event: string) => (path: string) => {
if (!isBlueprintFile(path)) return
runCatalog(`${event} ${path}`)
server.ws.send({ type: 'full-reload', path: '*' })
}
server.watcher.on('add', handle('add'))
server.watcher.on('change', handle('change'))
server.watcher.on('unlink', handle('unlink'))
},
}
}
export default defineConfig({
// base: '/' — path-agnostic for all deployment contexts.
//
// On Sovereigns (console.<sov>.omani.works/) the HTTPRoute passes
// through to nginx root; assets must be at /assets/*, not
// /sovereign/assets/*. On contabo (console.openova.io/sovereign/*),
// the Traefik strip-sovereign Middleware strips the /sovereign prefix
// BEFORE forwarding to nginx, so nginx still sees /assets/* — both
// cases resolve correctly with base: '/'.
//
// Issue #596: the previous base: '/sovereign/' caused blank pages on
// every Sovereign cluster because the browser requested
// /sovereign/assets/index-*.js but nginx (serving dist at /) returned 404.
base: '/',
plugins: [tailwindcss(), react(), rebuildCatalogOnYamlChange()],
resolve: {
alias: {
// OpenovaFlow Foundation — the @openova/* packages live in
// products/openova-flow/{core,canvas}/. They publish source-only
// entry points (./src/index.ts) so Vite + Vitest + tsc all bind
// straight to TS source. Workspaces (root package.json) hoist
// react/react-dom/d3-* into the monorepo's node_modules tree so
// the canvas source's bare imports resolve via standard
// upward-walking node_modules — no per-package node_modules
// sibling needed. Per-package aliases are listed BEFORE the '@'
// alias because @rollup/plugin-alias matches whole-name (so
// ordering is academic) but the documented convention is
// "longer key first" defensively.
'@openova/flow-core': resolve(__dirname, '../../../openova-flow/core/src/index.ts'),
'@openova/flow-canvas': resolve(__dirname, '../../../openova-flow/canvas/src/index.ts'),
'@': resolve(__dirname, './src'),
},
// Workspaces install one copy of react/react-dom at the monorepo
// root; dedupe makes sure both the catalyst-ui bundle AND the
// flow-canvas source bind to the same instance (otherwise React
// throws "invalid hook call" when two copies coexist).
dedupe: ['react', 'react-dom'],
},
server: {
port: 5173,
proxy: {
// With base: '/', API_BASE = '/api' in dev too. Direct proxy to
// catalyst-api on localhost:8080 — no prefix rewrite needed.
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
},
},
},
// Vitest config — drives `npm run test` in this package. The test runner
// shares Vite's `resolve.alias` so `@/...` imports work in tests too.
test: {
environment: 'jsdom',
globals: false,
css: false,
include: ['src/**/*.test.{ts,tsx}'],
setupFiles: ['src/test/setup.ts'],
},
})