Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
61 changes: 61 additions & 0 deletions .github/workflows/ui-next-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: UI v2 CI

on:
pull_request:
branches:
- main
paths:
- "ui-next/**"

permissions:
contents: read

jobs:
lint-format-test:
name: Lint, Format & Test
runs-on: ubuntu-latest
defaults:
run:
working-directory: ui-next

steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.32.0

- name: Get pnpm store directory
id: pnpm-cache
run: echo "store=$(pnpm store path)" >> $GITHUB_OUTPUT

- name: Cache pnpm store
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-cache.outputs.store }}
key: ${{ runner.os }}-pnpm-${{ hashFiles('ui-next/pnpm-lock.yaml') }}
restore-keys: ${{ runner.os }}-pnpm-

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Prettier check
run: pnpm prettier:check

- name: Lint
run: pnpm lint

- name: Type check
run: pnpm typecheck

- name: Test
run: pnpm test

- name: Build
run: pnpm build
Comment thread Fixed
8 changes: 8 additions & 0 deletions ui-next/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# OSS Conductor UI – defaults for local dev

# Backend API (Conductor server). Default: local server.
VITE_WF_SERVER=http://localhost:8080

# Optional
# VITE_PUBLIC_URL=/
# GENERATE_SOURCEMAP=false
26 changes: 26 additions & 0 deletions ui-next/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Dependencies
node_modules

# Local env (may contain secrets)
.env.local
.env.*.local

# Build output
dist
build
storybook-static

# Test / Playwright
/test-results/
/playwright-report/
/playwright/.cache/

# Turbo
.turbo

# Vite
.vite

# IDE / OS
.DS_Store

7 changes: 7 additions & 0 deletions ui-next/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Hoist these packages so they are directly importable without needing explicit
# devDependency declarations for each transitive package.
public-hoist-pattern[]=@mui/*
public-hoist-pattern[]=@use-gesture/*
public-hoist-pattern[]=@eslint/*
public-hoist-pattern[]=globals
public-hoist-pattern[]=monaco-editor
10 changes: 10 additions & 0 deletions ui-next/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
build
storybook-static
/test-results/
/playwright-report/
/playwright/.cache/
tests-examples
playwright
public/context.js
/dist/
pnpm-lock.yaml
1 change: 1 addition & 0 deletions ui-next/.prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
171 changes: 171 additions & 0 deletions ui-next/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
# Conductor UI v2

The open-source React UI for [Conductor](https://github.com/conductor-oss/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 22+
- [pnpm](https://pnpm.io/) 10.32.0 (`corepack use pnpm@10.32.0`)
- A running Conductor server (default: `http://localhost:8080`)

### Setup

```bash
pnpm install
```

Configure the backend URL in `.env` (see `.env` for defaults):

```bash
VITE_WF_SERVER=http://localhost:8080
```

### Start the dev server

```bash
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:

```bash
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 unit tests |
| `pnpm test:watch` | Run tests in watch mode |
| `pnpm test:coverage` | Run tests with coverage report |

## Using as an npm library

Install the package:

```bash
npm install conductor-ui
```

Import styles in your app entry point:

```tsx
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.

```tsx
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:

```tsx
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` ^18
- `react-dom` ^18
- `react-router` / `react-router-dom` ^7
- `@mui/material`, `@mui/icons-material`, `@mui/system`, `@mui/x-date-pickers`
- `@emotion/react`, `@emotion/styled`
105 changes: 105 additions & 0 deletions ui-next/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import eslintReact from "@eslint-react/eslint-plugin";
import js from "@eslint/js";
import vitest from "@vitest/eslint-plugin";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import { globalIgnores } from "eslint/config";
import globals from "globals";
import tseslint from "typescript-eslint";

const commonRules = {
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unused-vars": [
"error",
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
],
// TODO: Remove this and fix types properly
"@typescript-eslint/ban-ts-comment": "warn",
// Prevent direct imports from date-fns and date-fns-tz except in utils/date.ts
"no-restricted-imports": [
"error",
{
patterns: [
{
group: ["date-fns"],
message:
"Direct imports from 'date-fns' are not allowed. Please import from 'src/utils/date' instead.",
},
{
group: ["date-fns-tz"],
message:
"Direct imports from 'date-fns-tz' are not allowed. Please import from 'src/utils/date' instead.",
},
],
},
],
};

const baseConfig = {
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs["recommended-latest"],
reactRefresh.configs.vite,
eslintReact.configs.recommended,
],
languageOptions: {
ecmaVersion: 2020,
sourceType: "module",
globals: {
...globals.browser,
...globals.node,
},
},
rules: {
"no-undef": "error",
...commonRules,
},
};

export default tseslint.config([
globalIgnores(["dist", "node_modules"]),

// Test files (Vitest + testing globals)
{
files: [
"**/__tests__/**/*.{js,jsx,ts,tsx}",
"**/*.{test,spec}.{js,jsx,ts,tsx}",
],
...baseConfig,
plugins: { vitest, ...baseConfig.plugins },
rules: {
...vitest.configs.recommended.rules,
...commonRules,
},
},

// JSX files (allow PropTypes)
{
files: ["**/*.jsx"],
...baseConfig,
rules: {
...baseConfig.rules,
"react/prop-types": "off",
"@eslint-react/no-prop-types": "off",
},
},

// Non-test files (TS/TSX)
{
files: ["**/*.{js,ts,tsx}"],
ignores: [
"**/__tests__/**/*.{js,jsx,ts,tsx}",
"**/*.{test,spec}.{js,jsx,ts,tsx}",
],
...baseConfig,
},

// Allow date-fns and date-fns-tz imports in utils/date.ts
{
files: ["src/utils/date.ts"],
rules: {
"no-restricted-imports": "off",
},
},
]);
Loading
Loading