Skip to content

pia-team/transformation-ui

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

41 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Transformation UI

A developer-facing tool for managing TMF630-compliant transformation definitions. Built with Next.js 15, React 19, TypeScript, and Tailwind CSS.

Part of the PIA Transformation Suite — works alongside transformation-app (backend API).


Screenshots

Dashboard Transformations List
Dashboard Transformations
Transformation Detail New Transformation
Detail New
Playground Login
Playground Login

What is Transformation UI?

Purpose

Transformation UI is a web-based management tool for creating, editing, testing, and monitoring data transformation definitions. These transformations convert data from one format to another using the JSLT language (a JSON query and transformation language, similar to XSLT but for JSON).

Who is it for?

  • Developers who need to write and test JSLT transformation rules
  • Integration engineers who map data between different TMF630-compliant APIs
  • DevOps teams who need to monitor transformation engine status

Key Concepts

Concept Description
Transformation Definition A named, versioned rule that describes how to convert input JSON into output JSON using JSLT
Transformation ID Unique identifier for each transformation (e.g. order-to-invoice, customer-mapping)
JSLT JSON transformation language — you write JSLT expressions to transform JSON data
Engine The backend service that executes JSLT transformations (currently only JSLT engine is available)
Version Each transformation is versioned. Edits create new versions automatically

How it connects to the backend

┌─────────────────┐         ┌─────────────────────┐         ┌──────────────┐
│                 │  REST   │                     │  JSLT   │              │
│ Transformation  │ ──────► │ transformation-app   │ ──────► │ JSLT Engine  │
│ UI (this app)   │  API    │ (Spring Boot API)   │ execute │              │
│                 │ ◄────── │                     │ ◄────── │              │
└─────────────────┘         └─────────────────────┘         └──────────────┘

Pages & Usage Guide

Authentication & Roles

When you first open the app, you'll be redirected to the Login page. The app uses OpenID Connect (OIDC) Single Sign-On (SSO) — click the "Single Sign-On" button and enter your credentials.

OIDC is optional at startup. If OIDC_ISSUER is not configured, the app still starts — the login page shows an "OIDC not configured" warning and the Sign-In button is disabled. This prevents runtime crashes like InvalidEndpoints when the OIDC provider is unavailable.

There are 3 roles that control what you can do:

Role Can View Can Create/Edit Can Delete
Reader ✅ All pages
Writer ✅ All pages ✅ Create & edit transformations
Admin ✅ All pages ✅ Create & edit ✅ Delete transformations

Your role is displayed at the bottom of the sidebar, under your email address.


1. Login Page (/login)

Login

The landing page for unauthenticated users.

  • Left panel — Branding area showing the app name and key features (JSLT Editor, Version Tracking, Live Playground)
  • Right panel — "Single Sign-On" button that redirects to the OIDC provider
  • Top-right — Language switcher (EN/TR) and dark mode toggle are available even before login
  • After successful login, you're automatically redirected to the Dashboard

2. Dashboard (/)

Dashboard

The home page after login. Gives you a quick overview of the system.

What you'll see:

  • Total Transformations — Number of transformation definitions registered in the system
  • Last 24h Activity — How many transformations were modified in the last 24 hours
  • Active Engines — Number of available transformation engines (currently 1: JSLT)
  • Engine Status — Shows which engines are online and available
  • Quick Actions — Shortcut buttons:
    • + New Transformation — Jump directly to the creation form
    • Quick Test — Open the Playground
    • View All — Go to the full transformations list
  • Recently Created — Last 5 newly created transformation definitions
  • Recently Updated — Last 5 recently modified transformations
  • Activity Feed — Combined timeline of all recent create/update events, sorted by time

How to use it: This is your starting point. Glance at the KPI cards to understand the current state, use Quick Actions to jump to common tasks, or click on any transformation in the recent lists to view its details.


3. Transformations List (/transformations)

Transformations

The main list of all transformation definitions in the system.

Table columns:

Column Description
Transformation ID Unique name (e.g. order-to-invoice). Click to open detail page
Version Current version number (auto-incremented on each edit)
Engine Which engine processes this transformation (e.g. JSLT)
Description Short text describing what the transformation does
Created By Email of the person who created it
Created On Creation date/time
Modified On Last modification date/time

Features:

  • Search — Type in the search bar to filter by transformation ID (searches as you type)
  • Sort — Click any column header to sort ascending/descending. Uses TMF630 sort convention via @pia-team/pia-ui-tmf630-query-core (-field desc, field asc)
  • TMF630 Filters — Click the filter icon to open the advanced filter panel (powered by @pia-team/pia-ui-tmf630-search). Supports the full TMF630 QueryDSL operator set:
    • Field (e.g. transformationId, createdOn, engine)
    • Operator (e.g. eq, contains, gte, between, isnull, regex, and more)
    • Value (text, date/time picker, enum dropdown depending on field type)
    • Date/time values are serialized with local timezone offset (ISO-8601, e.g. 2026-03-15T14:30:00+03:00)
    • Active filters appear as removable chips below the search bar
  • Pagination — TMF630 pagination headers (X-Total-Count, Content-Range, HTTP 200/206) parsed via parseTMF630Headers from the shared library. Navigate between pages using the controls at the bottom (10, 20, 50, 100 per page)
  • Export — Download the current (filtered) list as CSV or JSON
  • + New Transformation button — Opens the creation form (visible to Writers and Admins only)

How to use it: Use search and filters to find the transformation you need, then click on its ID to view/edit. Sort by "Modified On" descending to see the most recently changed ones first.


4. Transformation Detail (/transformations/[id])

Detail

Detailed view of a single transformation definition. Has 3 tabs:

Tab 1: Definition (default)

The main editing interface.

  • Header — Shows transformation ID, current version, engine type, creation/modification dates
  • JSLT Definition — Monaco code editor showing the JSLT transformation code. Syntax highlighting and auto-indentation are built in
  • Description — Editable text field for documentation
  • Context Variables — Optional JSON context that can be passed to the transformation engine
  • Test Section — Built-in testing without leaving the page:
    1. Enter sample Input JSON in the input editor
    2. Click Test to execute the transformation
    3. See the Output Result instantly
  • Save — After editing the JSLT code or description, click Save. This creates a new version automatically
  • Conflict Detection — If someone else edited the same transformation while you were editing, a conflict dialog appears letting you choose which version to keep

Tab 2: Version History

  • Shows a list of all previous versions of this transformation
  • Each version shows: version number, who modified it, when, and what changed
  • You can view the JSLT code of any past version

Tab 3: Playground

  • Same as the global Playground page, but pre-loaded with this transformation's definition
  • Useful for quick testing without switching pages

How to use it: Open a transformation from the list → review/edit the JSLT code → test with sample input → save when ready. The version history gives you a full audit trail.


5. New Transformation (/transformations/new)

New

Form for creating a brand new transformation definition. Only visible to Writer and Admin roles.

Form fields:

Field Required Description
Transformation ID Unique name (e.g. order-to-invoice). Cannot be changed after creation
Engine Select the transformation engine (currently: JSLT)
Description What this transformation does
JSLT Definition The transformation code in JSLT syntax. Written in a Monaco code editor
Context Variables Optional JSON context variables

Built-in testing before save:

  1. Write your JSLT definition in the code editor
  2. Enter sample Input JSON
  3. Click Test to execute and see the result immediately
  4. If the result is correct, click Save to create the transformation

Validation:

  • Transformation ID must be unique (server-side check)
  • JSLT definition cannot be empty
  • If validation fails, error messages appear next to the relevant fields

How to use it: Fill in the ID and engine, write your JSLT code, test it with sample data, then save. After saving, you'll be redirected to the detail page of your new transformation.


6. Playground (/playground)

Playground

A standalone JSLT testing sandbox — no need to create or save anything.

Layout (3 panels):

Panel Purpose
JSLT Definition (left) Write or paste your JSLT transformation code
Input JSON (center) The source JSON data you want to transform
Output Result (right) The transformation result (read-only, appears after executing)

How to use it:

  1. Select an Engine from the dropdown (default: JSLT)
  2. Optionally select an existing Transformation to load its definition
  3. Write or paste a JSLT expression in the definition panel
  4. Paste or type the Input JSON you want to transform
  5. Click the ▶ Run button
  6. The Output panel shows the result or error message
  7. Execution time is displayed next to the Run button

Additional features:

  • Load existing transformation — Dropdown to pick a saved transformation and load its JSLT code
  • Version selector — If you loaded an existing transformation, switch between its versions
  • Context Variables — Expand the context panel to add JSON variables accessible in the JSLT code
  • Format/Copy — Quick action buttons to format JSON or copy content
  • Reset — Clear all panels and start fresh

Use cases:

  • Experimenting with JSLT syntax before creating a real transformation
  • Debugging a transformation that's not producing expected output
  • Learning JSLT by trying different expressions on sample data

Sidebar Navigation

The sidebar appears on all authenticated pages:

  • Collapse/Expand — Click the toggle button (top-right of sidebar) to collapse it to icon-only mode. Hover to temporarily expand
  • Navigation links — Dashboard, Transformations, Playground
  • Bottom section — Language switcher (EN/TR), dark mode toggle, user info (email + role), logout button
  • Prefetch — Hovering over "Transformations" link pre-fetches the data for faster navigation

Features

  • Dashboard — KPI cards (total transformations, 24h activity), engine status, recent activity feed, quick actions
  • Transformations List — Paginated table with search, sort, column toggle, inline filters
  • Transformation Detail — View/edit transformation definitions with Monaco code editor (JSLT syntax)
  • Create Transformation — Form with validation (Zod), JSLT code editor, live preview
  • Playground — Interactive JSLT testing sandbox with input/output panels
  • Authentication — OIDC SSO via NextAuth.js, role-based access (reader/writer/admin)
  • i18n — English & Turkish (next-intl), switchable at runtime
  • Dark Mode — System-aware theme toggle (next-themes)
  • Responsive — Collapsible sidebar, mobile-friendly layout

TMF630 Search Configuration (search-config.json)

The advanced filter panel on the Transformations List page is fully configurable via a JSON file — no code changes required. Admins can control which fields are searchable, which operators are available per field, and how values are displayed.

Where it lives

Environment Location
Local dev transformation-ui/search-config.json (project root)
Docker Compose mvnx-compose/transformation-ui/search-config.json (volume-mounted)
Kubernetes ConfigMap transformation-ui-env-js-configsearch-config.json key

The file is served at runtime via GET /api/search-config. The SearchConfigProvider in the layout fetches it once on app load.

New to search-config.json? See the comprehensive Creating a search-config.json — Complete Guide in the pia-ui-components README. It covers all field types, all 22 TMF630 operators, operator presets, validation rules, and a step-by-step walkthrough for new projects.

JSON Schema (IDE Autocomplete)

The config file references a JSON Schema bundled in @pia-team/pia-ui-tmf630-query-core. This gives you autocomplete, hover docs, and validation in VS Code / Cursor:

{
  "$schema": "node_modules/@pia-team/pia-ui-tmf630-query-core/search-config.schema.json",
  "fields": { ... }
}

The $schema key is ignored at runtime — it only activates IDE support.

Configuration Format

{
  "displayPattern": "dd/MM/yyyy HH:mm",
  "fields": {
    "transformationId": {
      "operatorSet": "text-search",
      "validation": { "maxLength": 255 }
    },
    "engine": {
      "type": "enum",
      "operatorSet": "selection",
      "values": [
        { "displayName": "JSLT", "serverValue": "JSLT" }
      ]
    },
    "createdBy": {
      "type": "email",
      "operators": ["eq", "ne", "contains", "containsi", "startswith", "startswithi", "in", "nin"]
    },
    "createdOn": {
      "type": "offsetDateTime",
      "operatorSet": "date-range",
      "displayFormat": "date",
      "displayPattern": "dd/MM/yyyy"
    },
    "modifiedBy": {
      "type": "email",
      "operatorSet": "text-search",
      "nullable": true
    },
    "modifiedOn": {
      "type": "offsetDateTime",
      "operatorSet": "date-range",
      "displayFormat": "datetime",
      "nullable": true
    },
    "transformationVersion": {
      "displayName": "Version",
      "type": "numeric",
      "operatorSet": "numeric",
      "validation": { "min": 1 }
    }
  },
  "defaults": {
    "defaultField": "transformationId",
    "defaultOperator": "containsi"
  },
  "responseFields": [
    "transformationId", "engine", "createdBy", "createdOn",
    "modifiedBy", "modifiedOn", "transformationVersion"
  ]
}

Field Properties

Property Type Required Description
type string No Field type: text (default), numeric, enum, email, url, date, dateTime, offsetDateTime, instant
displayName string No Label shown in the UI. Auto-derived from field key if omitted (e.g., createdOn → "Created On")
displayFormat string No For temporal types only: "date" (date picker) or "datetime" (datetime picker)
responseDisplayFormat string No For temporal types only: "date" or "datetime". Controls table column display independently from filter picker. Falls back to displayFormat if omitted.
operatorSet string No Named operator preset (see table below). Auto-derived from type if omitted
operators string[] No Explicit operator list. Overrides operatorSet when provided (advanced usage)
nullable boolean No If true, appends isnull and isnotnull operators to whichever operator set is active
values array No Enum options: [{ "displayName": "...", "serverValue": "..." }]. Only for type: "enum"
validation object No Client-side validation: maxLength, minLength, min, max, pattern, patternMessage, required

Operator Presets

Presets simplify configuration — each preset is a curated list of TMF630 operators appropriate for the field type.

Preset Name Operators Auto-used by types
text-search eq, ne, contains, containsi, startswith, startswithi, endswith, endswithi, in, nin text, email, url
text-exact eq, ne, eqi, nei, in, nin
selection eq, ne, in, nin enum
date-range eq, ne, gt, gte, lt, lte, between date, dateTime, offsetDateTime, instant
numeric eq, ne, gt, gte, lt, lte, between, in, nin numeric

Operator Override Priority

The system follows a 3-level priority chain:

1. operators: [...]     (highest — explicit list, full admin control)
2. operatorSet: "..."   (named preset)
3. auto-derived         (lowest — based on field type)

nullable: true always appends isnull and isnotnull regardless of which level is active.

Usage Scenarios

Scenario 1 — Basic (auto-derived, no overrides)

Just specify the field type; operators are auto-derived:

{
  "fields": {
    "name": {},
    "createdOn": { "type": "offsetDateTime", "displayFormat": "date" }
  }
}

name gets text-search operators automatically. createdOn gets date-range operators.

Scenario 2 — Named preset override

Admin wants to restrict a text field to exact-match operators only:

{
  "fields": {
    "code": {
      "operatorSet": "text-exact"
    }
  }
}

Result: eq, ne, eqi, nei, in, nin (no contains, startswith, endswith).

Scenario 3 — Explicit operator list (advanced)

Admin wants full control over exactly which operators are available:

{
  "fields": {
    "createdBy": {
      "type": "email",
      "operators": ["eq", "ne", "contains", "containsi", "startswith", "startswithi", "in", "nin"]
    }
  }
}

Result: only these 8 operators appear in the dropdown. endswith/endswithi are excluded.

Scenario 4 — Nullable fields

Backend DTO field can be null (e.g., modifiedOn is null for newly created records):

{
  "fields": {
    "modifiedOn": {
      "type": "offsetDateTime",
      "operatorSet": "date-range",
      "displayFormat": "datetime",
      "nullable": true
    }
  }
}

Result: eq, ne, gt, gte, lt, lte, between, isnull, isnotnull.

Scenario 5 — Enum with dynamic values

Engine values are loaded dynamically from the backend API. Static values serve as a fallback:

{
  "fields": {
    "engine": {
      "type": "enum",
      "operatorSet": "selection",
      "values": [
        { "displayName": "JSLT", "serverValue": "JSLT" }
      ]
    }
  }
}

At runtime, page.tsx overrides enumOptions with fresh data from GET /api/proxy/engines. If the API is down, the static values are shown.

Scenario 6 — Client-side validation

{
  "fields": {
    "transformationId": {
      "validation": { "maxLength": 255 }
    },
    "transformationVersion": {
      "type": "numeric",
      "validation": { "min": 1, "max": 99999 }
    },
    "customField": {
      "validation": {
        "pattern": "^[A-Z]{3}-\\d+$",
        "patternMessage": "Must match format ABC-123"
      }
    }
  }
}

Date/Time Handling

Config type Filter Picker Table Column Wire format sent to backend
offsetDateTime + displayFormat: "date" + displayPattern: "dd/MM/yyyy" Date-only 19/02/2026 2026-03-15T00:00:00+03:00 (auto day-boundary expansion)
offsetDateTime + displayFormat: "date" + responseDisplayFormat: "datetime" Date-only Date + time 2026-03-15T00:00:00+03:00
offsetDateTime + displayFormat: "datetime" + displayPattern: "dd/MM/yyyy HH:mm" Date + time 19/02/2026 14:30 2026-03-15T14:30:00+03:00
instant Date + time Date + time 2026-03-15T11:30:00Z (converted to UTC)

Table column display is driven by displayPattern from search-config.json. If a field has its own displayPattern, that is used; otherwise the global (context-level) displayPattern applies. If neither is set, Intl.DateTimeFormat locale-based formatting is used as fallback. | date | Date-only | Date-only | 2026-03-15 |

The displayPattern at the root level (e.g., "dd/MM/yyyy HH:mm") is inherited by all temporal fields. Individual fields can override it (e.g., createdOn uses "dd/MM/yyyy" for date-only display). The pattern controls both filter chip display and table column formatting. See the Display Pattern Tokens section in pia-ui-components README for all supported tokens (yyyy, MM, dd, HH, hh, mm, ss, a).

Automatic day-boundary expansion: When displayFormat: "date" is set on a temporal field (like createdOn with type offsetDateTime), the library automatically expands operators to day-boundary ranges. For example, createdOn.eq=2026-03-15 becomes createdOn.gte=2026-03-15T00:00:00+03:00 & createdOn.lt=2026-03-16T00:00:00+03:00. This works for both flat query params and JsonPath filters — no extra code needed. See the full expansion table in the pia-ui-components README.

Operator-Aware Value Inputs

The filter panel automatically adapts the value input based on the selected operator. This is handled by the library's FilterRow component with built-in components from @pia-team/pia-ui-tmf630-search:

Operator Input Type Component Value Example
between Dual from/to inputs BetweenValueInput ["2026-01-01", "2026-12-31"] Date range, version range
in, nin (enum) Checkbox dropdown MultiSelectInput ["JSLT", "XSLT"] Select multiple engines
in, nin (text) Tag/chip input TagValueInput ["Alice", "Bob"] Multiple user names
isnull, isnotnull No input Hidden "true" Check for null modifiedOn
All others Single input <input> / DatePicker / Select "Alice" Standard text/date/enum

The renderValueInput prop in page.tsx handles custom rendering (date pickers, enum selects) and receives betweenIndex (0 = from, 1 = to) and displayFormat for dual inputs:

renderValueInput={({ value, onChange, placeholder, className, type, enumOptions, displayFormat, betweenIndex }) =>
  type === "date" ? (
    <DatePicker
      value={value || undefined}
      onChange={onChange}
      placeholder={
        betweenIndex === 0 ? "From" :
        betweenIndex === 1 ? "To" :
        placeholder
      }
      className={className}
      mode={displayFormat === "datetime" ? "datetime" : "date"}
    />
  ) : type === "enum" && enumOptions?.length ? (
    <Select value={value || undefined} onValueChange={onChange}>
      <SelectTrigger className={className}>
        <SelectValue placeholder={placeholder || "Select..."} />
      </SelectTrigger>
      <SelectContent>
        {enumOptions.map((opt) => (
          <SelectItem key={opt.value} value={opt.value}>
            {opt.label}
          </SelectItem>
        ))}
      </SelectContent>
    </Select>
  ) : (
    <input
      type={type === "numeric" ? "number" : "text"}
      value={value}
      onChange={(e) => onChange(e.target.value)}
      placeholder={
        betweenIndex === 0 ? "From" :
        betweenIndex === 1 ? "To" :
        placeholder
      }
      className={className}
    />
  )
}

Multi-value operators are serialized as repeated query parameters per the TMF630 toolkit specification:

# between
birthdate.between=1990-01-01&birthdate.between=1999-12-31

# in
surname.in=Doe&surname.in=Brown

# isnull (no value needed)
name.isnull=true

Tech Stack

Category Technology
Framework Next.js 15 (App Router, standalone output)
Language TypeScript 5
UI React 19, Tailwind CSS 3, Radix UI, Lucide icons
State TanStack React Query v5
Auth NextAuth.js v5 (Generic OIDC)
Forms React Hook Form + Zod validation
Editor Monaco Editor (@monaco-editor/react)
TMF630 @pia-team/pia-ui-tmf630-query-core (types, filter/sort serialization, pagination headers)
Filters @pia-team/pia-ui-tmf630-search (TMF630 QueryDSL filter panel, chips, headless hook)
i18n next-intl
Testing Vitest + Testing Library (unit), Playwright (e2e)

Project Structure

transformation-ui/
├── src/
│   ├── app/                    # Next.js App Router pages
│   │   ├── (auth)/login/       # Login page
│   │   ├── (main)/             # Authenticated layout (sidebar)
│   │   │   ├── page.tsx        # Dashboard
│   │   │   ├── transformations/
│   │   │   │   ├── page.tsx    # List
│   │   │   │   ├── [id]/       # Detail
│   │   │   │   └── new/        # Create
│   │   │   └── playground/     # JSLT playground
│   │   └── api/
│   │       ├── auth/           # NextAuth endpoints
│   │       ├── config/         # Public env config (client)
│   │       └── proxy/          # API proxy to backend
│   ├── components/             # Shared UI components
│   │   ├── layout/             # Sidebar, breadcrumb, page header
│   │   ├── shared/             # Theme toggle, language switcher
│   │   └── ui/                 # Radix-based primitives (button, dialog, etc.)
│   ├── features/               # Feature modules
│   │   ├── auth/               # Auth hooks, user menu
│   │   ├── dashboard/          # KPI cards, activity feed
│   │   └── transformations/    # List, detail, form, hooks
│   ├── instrumentation.ts       # Loads env.json into process.env at Node.js runtime
│   ├── lib/                    # Shared utilities
│   │   ├── api/                # API client, endpoints, types (re-exports TMF630 types from @pia-team/pia-ui-tmf630-query-core)
│   │   ├── env/                # Environment config (context, reader)
│   │   └── providers/          # Auth, query, theme, toast providers
│   └── messages/               # i18n translation files (en.json, tr.json)
├── env.json                    # Runtime config (gitignored). Secrets via env vars in production
├── next.config.ts              # Reads env.json → process.env at build/startup
├── Dockerfile                  # Dev Docker build
├── Dockerfile_release          # Production multi-stage build
└── .github/workflows/          # CI/CD (GitHub Actions)

Environment Configuration

Configuration uses two layers:

Layer Contains Committed to git?
env.json All config (URLs, roles, OIDC, secrets for dev) No (gitignored)
Environment variables Overrides (e.g. NEXT_PUBLIC_BASE_PATH in Docker) Never

Priority rule: Environment variables always take precedence over env.json. If a value is set in both, the env var wins.

env.json Structure

{
  "api": {
    "baseUrl": "", // Client API base URL (empty = same-origin proxy)
    "proxyTarget": "http://...", // Backend API URL for server-side proxy
  },
  "basePath": "/transformation-ui", // Next.js basePath (set when behind nginx)
  "oidc": {
    "issuer": "https://host/realms/myrealm", // OIDC issuer URL (uses .well-known discovery)
    "clientId": "federation", // OIDC client ID
    "clientSecret": "...", // OIDC client secret (dev only — use env var in production)
    "providerName": "SSO Login", // Display name for the provider (optional)
  },
  "roles": {
    "path": "realm_access.roles", // JWT claim path for roles
    "read": "ebu-federation-reader", // Reader role name
    "write": "ebu-federation-writer", // Writer role name
    "admin": "ebu-federation-admin", // Admin role name
  },
  "nextAuth": {
    "secret": "...", // Session encryption key (dev only — use env var in production)
    "url": "https://...", // External app URL (only needed in Docker/production behind proxy)
    "trustHost": "true", // Set "true" when behind reverse proxy or in local dev
  },
  "node": {
    "tlsRejectUnauthorized": false, // Set false for self-signed certs (dev only!)
  },
}

Required Secrets

These values are required for the app to function. They can be provided via environment variables or in env.json:

env.json key Environment Variable Description
oidc.clientSecret OIDC_CLIENT_SECRET OIDC client secret from your identity provider
nextAuth.secret NEXTAUTH_SECRET Session encryption key (generate with openssl rand -base64 32)

Production: Use environment variables or a secret manager (Kubernetes Secrets, etc.) — never commit secrets to version control.

Local / Docker Compose: Secrets can be placed in env.json for convenience since this file is gitignored and volume-mounted at runtime.

Important: AUTH_BASE_PATH

Do NOT set AUTH_BASE_PATH manually. The auth basePath is auto-derived from NEXT_PUBLIC_BASE_PATH:

basePath = AUTH_BASE_PATH || `${NEXT_PUBLIC_BASE_PATH}/api/auth`

Setting AUTH_BASE_PATH=/api/auth while NEXT_PUBLIC_BASE_PATH=/transformation-ui causes all auth endpoints to return 400 Bad Request. This happens because the route handler prepends the Next.js basePath to the URL (/transformation-ui/api/auth/session), but Auth.js expects its basePath (/api/auth) to match — they don't, so every request fails.

Correct behavior: Leave AUTH_BASE_PATH unset → the code derives /transformation-ui/api/auth automatically.

How env.json Works

env.json + Environment Variables
  │
  ├─► next.config.ts          Reads at build/startup → sets process.env.*
  │     │                     (env vars take precedence over env.json)
  │     ├─ OIDC_*             OIDC config (issuer, clientId, etc.)
  │     └─ NEXTAUTH_*         Auth config (secret, url, trustHost)
  │
  ├─► instrumentation.ts      Re-reads env.json at Node.js runtime
  │                            (ensures env vars are available for Auth.js)
  │
  ├─► /api/config route        Serves ONLY public fields to client
  │     └─ env-context.tsx     React context fetches from /api/config
  │
  └─► /api/proxy route         Uses API_PROXY_TARGET for backend calls

Fallback when env.json is missing: The /api/config route builds the public config from process.env variables (NEXT_PUBLIC_API_BASE_URL, NEXT_PUBLIC_BASE_PATH, OIDC_ISSUER, OIDC_PROVIDER_NAME, ROLE_*). This ensures the client receives OIDC status and basePath even in Docker/Kubernetes deployments where env.json is not mounted.

Security: Secrets are never sent to the browser. The /api/config endpoint only serves filtered public fields. In production, secrets should come from environment variables rather than env.json.


DevOps Configuration

Local Development

# 1. Clone & install
git clone https://github.com/pia-team/transformation-ui.git
cd transformation-ui
npm install

# 2. Create env.json (copy from template below)
# See "env.json for Local Development" section

# 3. Start dev server
npm run dev
# → http://localhost:3000/transformation-ui
#   (root "/" auto-redirects to basePath)

env.json for Local Development

For local development, secrets can be included in env.json for convenience (the file is gitignored):

{
  "api": {
    "baseUrl": "",
    "proxyTarget": "https://localhost:8443"
  },
  "basePath": "/transformation-ui",
  "oidc": {
    "issuer": "https://mvnx.compose.test:8443/keycloak/realms/dsync",
    "clientId": "federation",
    "clientSecret": "<your-client-secret>",
    "providerName": "SSO Login"
  },
  "roles": {
    "path": "realm_access.roles",
    "read": "ebu-federation-reader",
    "write": "ebu-federation-writer",
    "admin": "ebu-federation-admin"
  },
  "nextAuth": {
    "secret": "<generate-with: openssl rand -base64 32>",
    "trustHost": "true"
  },
  "node": {
    "tlsRejectUnauthorized": false
  }
}

Secrets in env.json are acceptable here because the file is listed in .gitignore and only used on your local machine.

Note: nextAuth.url is intentionally omitted — Auth.js auto-detects the URL via trustHost: true in local dev, which avoids env-url-basepath-mismatch warnings and logout/redirect issues.

Docker Compose (mvnx-compose)

In the mvnx-compose project, env.json is volume-mounted into the container. The only required Docker Compose environment variable is NEXT_PUBLIC_BASE_PATH (needed at build time for middleware):

# docker-compose.yml
transformation-ui:
  image: ghcr.io/pia-team/transformation-ui:1.2.0
  environment:
    - NEXT_PUBLIC_BASE_PATH=/transformation-ui
  volumes:
    - ./transformation-ui/env.json:/app/env.json:ro
  depends_on:
    keycloak: { condition: service_healthy }
    nginx: { condition: service_healthy }
    transformation-app: { condition: service_healthy }

env.json for Docker Compose

All configuration including secrets is in env.json (volume-mounted, not committed to the transformation-ui repo):

{
  "api": {
    "baseUrl": "",
    "proxyTarget": "http://transformation-app:8080/transformation/v1"
  },
  "basePath": "/transformation-ui",
  "oidc": {
    "issuer": "https://mvnx.compose.test:8443/keycloak/realms/dsync",
    "clientId": "federation",
    "clientSecret": "<your-client-secret>",
    "providerName": "SSO Login"
  },
  "roles": {
    "path": "realm_access.roles",
    "read": "ebu-federation-reader",
    "write": "ebu-federation-writer",
    "admin": "ebu-federation-admin"
  },
  "nextAuth": {
    "secret": "<generate-with: openssl rand -base64 32>",
    "url": "https://mvnx.compose.test:8443/transformation-ui",
    "trustHost": "true"
  },
  "node": {
    "tlsRejectUnauthorized": false
  }
}

Note: In Docker Compose, nextAuth.url is required because Auth.js needs to know the external HTTPS URL for correct callback/redirect URL generation behind nginx.

Key differences from local:

Field Local Docker Compose
api.proxyTarget https://localhost:8443 (via nginx on host) http://transformation-app:8080/transformation/v1 (direct)
basePath "/transformation-ui" "/transformation-ui"
nextAuth.url (omitted — auto-detected) "https://mvnx.compose.test:8443/transformation-ui"
trustHost "true" "true"

Nginx Configuration

The nginx reverse proxy passes requests to the Next.js server. Large proxy buffers are required because Auth.js OIDC callback responses can have very large headers:

location /transformation-ui {
    set $transformation_ui_upstream http://transformation-ui:3000;
    proxy_pass         $transformation_ui_upstream;
    proxy_set_header   Host              $host:8443;
    proxy_set_header   X-Real-IP         $remote_addr;
    proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header   X-Forwarded-Proto https;
    proxy_set_header   X-Forwarded-Host  $host:8443;
    proxy_set_header   X-Forwarded-Port  8443;
    proxy_buffering    off;
    proxy_buffer_size        128k;
    proxy_buffers            4 256k;
    proxy_busy_buffers_size  256k;
    proxy_http_version 1.1;
    proxy_set_header   Upgrade           $http_upgrade;
    proxy_set_header   Connection        "upgrade";
}

Important: Without the increased proxy_buffer_size / proxy_buffers, OIDC login callbacks will fail with 502 Bad Gateway (upstream sent too big header).


CI/CD — Docker Image Release

Uses GitHub Actions to build and push Docker images to GitHub Container Registry (GHCR).

Release Process

# 1. Ensure you're on develop and up-to-date
git checkout develop
git pull

# 2. Create and push a semver tag
git tag 1.1.0
git push origin 1.1.0

# 3. On GitHub:
#    → Releases → Draft a new Release
#    → Select tag → Generate Release Notes → Publish Release

# 4. GitHub Actions builds & pushes the image automatically
#    → ghcr.io/pia-team/transformation-ui:1.1.0

Using the Docker Image

# In docker-compose.yml (instead of local build):
transformation-ui:
  image: ghcr.io/pia-team/transformation-ui:1.2.0
  environment:
    - NEXT_PUBLIC_BASE_PATH=/transformation-ui
  volumes:
    - ./transformation-ui/env.json:/app/env.json:ro

Kubernetes / Production

Configuration is split between Deployment env vars and a ConfigMap-mounted env.json.

Deployment Environment Variables

All URL-based variables must include the basePath (/transformation-ui):

env:
  # --- Auth / OIDC ---
  - name: OIDC_ISSUER
    value: "https://diam.dev.mvnx-gcu.com/realms/orbitant-realm"
  - name: OIDC_CLIENT_ID
    value: "orbitant-backend-client"
  - name: OIDC_CLIENT_SECRET
    valueFrom:
      secretKeyRef:
        name: transformation-suite
        key: APPLICATION_S2S_CLIENT_SECRET
  - name: NEXTAUTH_SECRET
    valueFrom:
      secretKeyRef:
        name: transformation-suite
        key: APPLICATION_S2S_CLIENT_SECRET
  - name: AUTH_TRUST_HOST
    value: "true"

  # --- URLs (must include basePath!) ---
  - name: AUTH_URL
    value: "https://transformation-ui.dev.mvnx-gcu.com/transformation-ui"
  - name: NEXTAUTH_URL
    value: "https://transformation-ui.dev.mvnx-gcu.com/transformation-ui"
  - name: NEXT_PUBLIC_APP_URL
    value: "https://transformation-ui.dev.mvnx-gcu.com/transformation-ui"
  - name: NEXT_PUBLIC_BASE_PATH
    value: "/transformation-ui"
  - name: NEXT_PUBLIC_API_BASE_URL
    value: "https://transformation-ui.dev.mvnx-gcu.com/transformation-ui/"

  # --- DO NOT set AUTH_BASE_PATH (auto-derived, see warning above) ---

  # --- Performance ---
  - name: NODE_OPTIONS
    value: "--max-http-header-size=1000000"
  - name: NODE_TLS_REJECT_UNAUTHORIZED
    value: "1"

env.json ConfigMap (mounted at /app/env.json)

{
  "api": {
    "baseUrl": "https://transformation-ui.dev.mvnx-gcu.com/transformation-ui/",
    "proxyTarget": "https://transformations.dev.mvnx-gcu.com/transformation/v1"
  },
  "basePath": "/transformation-ui",
  "publicUrl": "https://transformation-ui.dev.mvnx-gcu.com/transformation-ui",
  "oidc": {
    "issuer": "https://diam.dev.mvnx-gcu.com/realms/orbitant-realm",
    "clientId": "orbitant-backend-client",
    "providerName": "SSO Login"
  },
  "roles": {
    "path": "realm_access.roles",
    "read": "mvnx-reader",
    "write": "mvnx-writer",
    "admin": "mvnx-admin"
  },
  "nextAuth": {
    "url": "https://transformation-ui.dev.mvnx-gcu.com/transformation-ui",
    "trustHost": "true"
  },
  "node": {
    "tlsRejectUnauthorized": true
  }
}

Critical consistency rules:

  1. AUTH_URL, NEXTAUTH_URL, NEXT_PUBLIC_APP_URL, and nextAuth.url must all include the basePath (e.g. /transformation-ui)
  2. nextAuth.url must not end with /api/auth — Auth.js appends that automatically
  3. NEXT_PUBLIC_BASE_PATH in the Deployment must match basePath in the ConfigMap
  4. NODE_TLS_REJECT_UNAUTHORIZED=1 in the Deployment and tlsRejectUnauthorized: true in the ConfigMap must be consistent
  5. Never set AUTH_BASE_PATH — it conflicts with the basePath prepending logic in the route handler

Available Scripts

Command Description
npm run dev Start development server
npm run build Production build
npm start Start production server
npm run lint ESLint check
npm run lint:fix ESLint auto-fix
npm run format Prettier format
npm run test Run unit tests (Vitest)
npm run test:watch Unit tests in watch mode
npm run test:coverage Unit tests with coverage
npm run test:e2e Run E2E tests (Playwright)

License

Private — PIA Team

About

transformation-suite

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages