Committers will exercise their judgement on what endpoints should exist in the public airflow/api_fastapi/public versus the private airflow/api_fastapi/ui
airflow/ui is our React frontend powered. Dependencies are managed by pnpm and dev/build processes by Vite.
Make sure you are using recent versions of pnpm>=9 and node>=20. breeze start-airflow will build the UI automatically.
Adding the --dev-mode flag will automatically run the vite dev server for hot reloading the UI during local development.
In certain WSL environments, you will need to set CHOKIDAR_USEPOLLING=true in your environment variables for hot reloading to work.
Follow the pnpm docs to install pnpm locally. Follow the nvm docs to manage your node version, which needs to be v22 or higher.
# install dependencies
pnpm install
# Run vite dev server for local development.
# The dev server will run on a different port than Airflow. To use the UI, access it through wherever your Airflow webserver is running, usually 8080 or 28080.
# Trying to use the UI from the Vite port (5173) will lead to auth errors.
pnpm dev
# Generate production build files will be at airflow/ui/dist
pnpm build
# Format code in .ts, .tsx, .json, .css, .html files
pnpm format
# Check JS/TS code in .ts, .tsx, .html files and report any errors/warnings
pnpm lint
# Check JS/TS code in .ts, .tsx, .html files and report any errors/warnings and fix them if possible
pnpm lint:fix
# Run tests for all .test.ts, test.tsx files
pnpm test
# Run coverage
pnpm coverage
# Generate queries and types from the REST API OpenAPI spec
pnpm codegen/distbuild files/public/i18n/localesinternationalization files/openapi-genautogenerated types and queries based on the public REST API openapi spec. Do not manually edit./ruleslinting rules for javascript and typescript code/src/assetsstatic assets for the UI like icons/src/componentsshared components across the UI/src/constantsconstants used across the UI like managing search parameters/src/utilsutility functions used across the UI/src/layoutscommon React layouts used by many pages/src/pagesindividual pages for the UI/src/contextcontext providers that wrap around whole pages or the whole app/src/querieswrappers around autogenerated react query hooks to handle specific side effects/src/mocksmock data for testing/src/main.tsxentry point for the UI/src/router.tsxthe router for the UI, update this to add new pages or routes/src/theme.tsthe theme for the UI, update this to change the colors, fonts, etc./src/queryClient.tsthe query client for the UI, update this to change the default options for the API requests
The outline for this document in GitHub is available at top-right corner button (with 3-dots and 3 lines).
In order to create a more modern UI, we use React. If you are unfamiliar with React then it is recommended to check out their documentation to understand components and jsx syntax.
We are using Chakra UI as a component and styling library. Notably, all styling is done in a theme file or
inline when defining a component. There are a few shorthand style props like px instead of padding-right, padding-left.
To make this work, all Chakra styling and css styling are completely separate.
We use React Query as our state management library. It is recommended to check out their documentation to understand how to use it.
We also have a codegen tool that automatically generates the queries and types based on the REST API openapi spec.
When fetching data, try to use the query key pattern to avoid fetching the same data multiple times.
Linting, Formatting, and Testing
Before committing your changes, it's best to quickly test your code with the following commands to avoid CI failures:
# Linting
pnpm lint
# Formatting
pnpm format
# Testing
pnpm testStyles in Chakra
Avoid using raw hex colors. Use the theme file for all styling.
Try to use Chakra's semantic tokens for colors whenever possible.
For example, instead of using red.500, use red.focusRing or for text, use fg.error.
Effects
If you find yourself calling useEffect, you should double check if you really need it. Check out React's documentation on useEffect for more information. If you still need an effect, please leave a comment on the PR to explain why you need it.
Testing Requirements
All new components must include tests. Create a .test.tsx file alongside your component:
# Component structure
src/components/MyComponent.tsx
src/components/MyComponent.test.tsxWhat to test:
- Component renders with different props
- User interactions (clicks, inputs, etc.)
- Loading, error, and empty states
- API responses (mock with MSW)
What NOT to test:
- Third-party library internals (Chakra UI, React Router)
- Implementation details (internal state names)
- Auto-generated code (
openapi-gen/)
// Example test structure
import { render, screen, waitFor } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { Wrapper } from 'src/utils/Wrapper';
describe('MyComponent', () => {
it('renders correctly', () => {
render(<MyComponent value="test" />, { wrapper: Wrapper });
expect(screen.getByText('test')).toBeInTheDocument();
});
it('handles loading state', async () => {
render(<MyComponent />, { wrapper: Wrapper });
expect(screen.getByText('Loading...')).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText('Loaded')).toBeInTheDocument();
});
});
});State Management Patterns
Choose the right state management for your use case:
- URL State (
useSearchParams): Filters, pagination, sort order - anything that should be shareable via URL - Local State (
useState): Modal open/closed, form inputs, temporary UI state - Local Storage (
useLocalStorage): User preferences that persist across sessions (theme, table view mode, column visibility) - Server State (React Query): API data - never duplicate in useState
import { useLocalStorage } from 'usehooks-ts';
// ✅ GOOD: Filter state in URL for shareability
const [searchParams, setSearchParams] = useSearchParams();
const filter = searchParams.get('filter') ?? 'all';
// ✅ GOOD: Modal state is local and temporary
const [isModalOpen, setIsModalOpen] = useState(false);
// ✅ GOOD: User preference persists across sessions
const [viewMode, setViewMode] = useLocalStorage<'table' | 'card'>('dags-view-mode', 'table');
// ✅ GOOD: API data managed by React Query
const { data: dags, isLoading, error } = useDags();
// ❌ BAD: Don't duplicate server state in useState
const [dags, setDags] = useState([]);
useEffect(() => { fetchDags().then(setDags); }, []);
// ❌ BAD: Don't duplicate localStorage in useState
const [viewMode, setViewMode] = useLocalStorage('view-mode', 'table');
const [localViewMode, setLocalViewMode] = useState(viewMode); // Unnecessary!
// Just use viewMode directly!
// ❌ BAD: Don't create state for calculated values
const [items, setItems] = useState([1, 2, 3, 4, 5]);
const [itemCount, setItemCount] = useState(items.length); // Unnecessary!
// Calculate during render instead:
const itemCount = items.length;
// ❌ BAD: Don't create state for derived data
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState(''); // Unnecessary!
// Calculate during render:
const fullName = `${firstName} ${lastName}`;Error and Loading States
Always handle all states from async operations:
// ❌ BAD: Only handles happy path
const { data } = useDags();
return <div>{data.map(...)}</div>; // Crashes if data is undefined!
// ✅ GOOD: Handles all states
const { data, isLoading, error } = useDags();
if (isLoading) {
return <Spinner />;
}
if (error) {
return <ErrorAlert error={error} />;
}
if (!data || data.length === 0) {
return <EmptyState message="No Dags found" />;
}
return <div>{data.map(...)}</div>;If you happen to add API endpoints you can head to Adding API endpoints.