Conductor UI v2
The open-source React UI for Conductor. It ships as both a standalone web application and an npm library that enterprise packages can extend via a plugin system.
Running locally
Prerequisites
- Node.js 18+
- pnpm 10.x — we use pnpm 10 (not v11) since pnpm v11 requires Node.js v22+. The exact version is pinned via
packageManagerinpackage.json. Run once to activate it:corepack enable - A running Conductor server (default:
http://localhost:8080)
Setup
pnpm install
Configure the backend URL in .env (see .env for defaults):
VITE_WF_SERVER=http://localhost:8080
Start the dev server
pnpm dev
The app will be available at http://localhost:1234.
Runtime configuration
The app reads runtime config from public/context.js, which is loaded at startup (not bundled). Copy the example and edit as needed:
cp public/context.js.example public/context.js
This file sets feature flags (window.conductor) and auth config (window.authConfig) without requiring a rebuild.
Available scripts
| Script | Description |
|---|---|
pnpm dev |
Start dev server with HMR |
pnpm build |
Build standalone app to dist/ |
pnpm build:lib |
Build npm library to dist/ |
pnpm build:all |
Build both app and library |
pnpm lint |
Run ESLint |
pnpm lint:fix |
Run ESLint with auto-fix |
pnpm prettier:check |
Check formatting |
pnpm prettier:write |
Auto-format all files |
pnpm typecheck |
Type-check without emitting |
pnpm test |
Run Vitest unit tests (single pass) |
pnpm test:watch |
Run Vitest in watch mode |
pnpm test:coverage |
Run Vitest with v8 coverage report |
pnpm test:e2e |
Run Playwright UI tests (mocked backend, headless) |
pnpm test:e2e:ui |
Open the Playwright interactive UI |
pnpm test:e2e:headed |
Run UI tests in a visible browser |
pnpm test:e2e:debug |
Step through UI tests in the Playwright debugger |
pnpm test:e2e:integration |
Run integration tests against a live backend |
pnpm test:e2e:integration:ui |
Integration tests in Playwright interactive UI |
pnpm test:e2e:integration:headed |
Integration tests in a visible browser |
Testing
Unit tests (Vitest)
Tests live alongside source files as *.test.{ts,tsx} and run in jsdom.
They cover utilities, state machines, and component logic without a browser or server.
pnpm test # single run
pnpm test:watch # re-runs on file change
pnpm test:coverage # produces coverage/index.html
E2E tests (Playwright)
E2E tests live in e2e/ and are run by Playwright against a real Chromium
browser. Every test mocks the Conductor backend with page.route(), so no
running Conductor server is required — the suite works entirely against the
built-in Vite dev server.
First-time setup
Install the Playwright browser binaries (one-time per machine):
pnpm exec playwright install --with-deps chromium
Running locally
# Headless (fastest) — reuses a running dev server on :1234 if one exists
pnpm test:e2e
# Interactive Playwright UI — best for writing and debugging tests
pnpm test:e2e:ui
# Watch the browser run the tests
pnpm test:e2e:headed
# Step through a single test with the Playwright debugger
pnpm test:e2e:debug
# Run one file
pnpm test:e2e e2e/smoke.spec.ts
# Run tests whose name matches a pattern
pnpm test:e2e --grep "navigates to"
If pnpm dev is already running on port 1234, Playwright reuses that server.
If nothing is running, it starts a dev server automatically for the test run.
Running in CI
Set CI=true (GitHub Actions does this automatically) and run:
pnpm exec playwright install --with-deps chromium
pnpm test:e2e
With CI=true the config:
- Always starts a fresh dev server (never reuses an existing one)
- Retries each failing test up to 2 times before marking it failed
- Uses a single worker to avoid resource contention
Example GitHub Actions job:
- name: Install Playwright browsers
run: pnpm exec playwright install --with-deps chromium
- name: Run E2E tests
run: pnpm test:e2e
- name: Upload Playwright report
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
retention-days: 7
Integration tests (Playwright + live backend)
Integration tests live in e2e/integration/ and use a separate config,
playwright.integration.config.ts. They talk to a real Conductor server and
verify the full stack end-to-end: the API client creates test data, the
browser navigates through the UI, and assertions confirm the data is rendered
correctly. Docker is managed automatically — no manual server management is
required.
How it works
- Global setup (
e2e/integration/global-setup.ts) checks whether a Conductor server is already listening on port 8000. If not, it builds theconductor:serverDocker image if needed (uses layer cache after first run), then startsdocker/docker-compose-ui-e2e.yamland waits for the/healthendpoint to return 200 (up to 4 minutes to account for cold JVM starts). - The app is built with
vite buildand then served withvite preview, withVITE_WF_SERVER=http://localhost:8000passed to the preview server so its/apiproxy forwards requests to the Docker backend. Tests run against the production bundle — the same artifact that gets deployed. - Each test file uses
e2e/integration/api-client.tsto create isolated test data (unique names per run) and cleans up inafterAll. - Global teardown stops the Docker stack only if setup started it — a backend you started yourself before running the tests is left untouched.
Running integration tests locally
Prerequisites: Docker must be running.
pnpm test:e2e:integration
This single command does everything automatically:
- Builds the
conductor:serverDocker image if it does not already exist locally — slow the first time (~5–10 min) but Docker's layer cache makes subsequent runs fast (~30s) unless server-side code has changed - Starts Postgres + the Conductor server via Docker Compose
(
docker/docker-compose-ui-e2e.yaml) and waits up to 4 minutes for the backend/healthendpoint to respond - Builds the UI (
pnpm build) withVITE_WF_SERVER=http://localhost:8000 - Starts
vite previewto serve the production bundle on port 1234, with its/apiproxy forwarding to the Docker backend - Runs the Playwright test suite against
http://localhost:1234 - Stops the Docker stack when the tests finish
Common options
# Interactive Playwright UI — step through tests visually, great for debugging
pnpm test:e2e:integration:ui
# Watch the browser execute the tests in real time
pnpm test:e2e:integration:headed
# Run a single spec file
pnpm test:e2e:integration e2e/integration/workflows.spec.ts
# Run tests whose name matches a pattern
pnpm test:e2e:integration --grep "appears in the"
# Skip Docker management if you already have a Conductor backend running
# on port 8000 (e.g. started with docker compose separately)
SKIP_DOCKER=true pnpm test:e2e:integration
# Keep the Docker stack running after the tests finish (faster re-runs)
SKIP_DOCKER_TEARDOWN=true pnpm test:e2e:integration
# Point the tests at a backend running on a non-default URL
CONDUCTOR_SERVER_URL=http://localhost:9000 pnpm test:e2e:integration
Faster iteration after the first run
On subsequent runs, if you keep the Docker stack alive with
SKIP_DOCKER_TEARDOWN=true, you can skip the Docker startup wait on the next
run because the setup script detects the backend is already healthy:
# First run — starts Docker, runs tests, leaves stack running
SKIP_DOCKER_TEARDOWN=true pnpm test:e2e:integration
# Subsequent runs — backend already up, jumps straight to build + test
pnpm test:e2e:integration
To stop the stack manually when you are done:
docker compose -p conductor-ui-e2e -f docker/docker-compose-ui-e2e.yaml down
Running integration tests in CI
pnpm test:e2e:integration automatically builds the app and starts vite preview
before running the tests, so no explicit build step is needed in CI.
- name: Install Playwright browsers
run: pnpm exec playwright install --with-deps chromium
- name: Run integration tests
# Global setup builds the conductor:server image automatically on first run.
# The Playwright webServer config then runs `pnpm build && pnpm preview`.
run: pnpm test:e2e:integration
- name: Upload integration report
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-integration-report
path: playwright-integration-report/
retention-days: 7
If you cache the Docker image between CI runs (e.g. using GitHub Actions
docker/build-push-action with cache-to/cache-from), the server build
step drops from ~10 minutes to ~30 seconds on cache hits.
Using as a library
Install directly from a tagged release of this repository. The &path:/ui-next
argument tells the package manager to use the ui-next/ subdirectory as the
package root:
# pnpm (recommended)
pnpm add "conductor-oss/conductor#<tag>&path:/ui-next"
# npm / yarn
npm install "conductor-oss/conductor#<tag>&path:/ui-next"
Or pin the version in package.json:
"conductor-ui": "conductor-oss/conductor#v1.0.0&path:/ui-next"
Replace <tag> / v1.0.0 with the release tag you want to consume
(e.g. v3.2.1). Available tags:
https://github.com/conductor-oss/conductor/releases
Import styles in your app entry point:
import "conductor-ui/styles.css"; // component styles
import "conductor-ui/global.css"; // global body/font styles (optional)
Extending with plugins
The plugin system lets you register additional routes, sidebar items, task forms, auth providers, and more without modifying the core package.
import { pluginRegistry, App } from "conductor-ui";
// Register a custom sidebar item
pluginRegistry.registerSidebarItem({
position: { target: "root", after: "definitionsSubMenu" },
item: {
id: "myFeature",
title: "My Feature",
icon: <MyIcon />,
linkTo: "/my-feature",
shortcuts: [],
hidden: false,
position: 350,
},
});
// Register a custom route
pluginRegistry.registerRoutes([
{
path: "/my-feature",
element: <MyFeaturePage />,
},
]);
// Render the app
function Root() {
return <App />;
}
Plugin extension points
| Extension | Method | Description |
|---|---|---|
| Routes | registerRoutes(routes) |
Add authenticated routes |
| Public routes | registerPublicRoutes(routes) |
Add unauthenticated routes |
| Sidebar items | registerSidebarItem(reg) |
Inject items into the sidebar |
| Task forms | registerTaskForm(reg) |
Custom forms for task types in the workflow editor |
| Task menu items | registerTaskMenuItem(reg) |
Add task types to the "Add Task" menu |
| Auth provider | registerAuthProvider(reg) |
Replace the auth implementation |
| Search provider | registerSearchProvider(reg) |
Add results to global search |
Sidebar item positioning
Sidebar items use numeric positions so plugins can inject between core items without collisions. The core OSS positions are exported for reference:
import { CORE_SIDEBAR_POSITIONS } from "conductor-ui";
// CORE_SIDEBAR_POSITIONS.ROOT:
// executionsSubMenu: 100
// runWorkflow: 200
// definitionsSubMenu:300
// helpMenu: 400
// swaggerItem: 500
pluginRegistry.registerSidebarItem({
position: { target: "root" },
item: {
id: "myItem",
position: 350, // between definitionsSubMenu (300) and helpMenu (400)
// ...
},
});
Project structure
src/
├── components/ # Shared UI components
│ └── Sidebar/ # Sidebar with plugin-injectable menu
├── pages/ # Route-level page components
├── plugins/ # Plugin registry and fetch utilities
├── shared/ # Auth state machine and context
├── theme/ # MUI theme provider
├── types/ # Shared TypeScript types
└── utils/ # Feature flags, constants, helpers
public/
├── context.js # Runtime config (gitignored, not bundled)
└── context.js.example
Peer dependencies
When consuming as a library, the following must be provided by the host app:
react^18react-dom^18react-router/react-router-dom^7@mui/material,@mui/icons-material,@mui/system,@mui/x-date-pickers@emotion/react,@emotion/styled