Skip to content

Latest commit

 

History

History
285 lines (197 loc) · 10.4 KB

File metadata and controls

285 lines (197 loc) · 10.4 KB

Node.js Environment Setup

Contributing to the REST API in Airflow

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

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.

pnpm commands

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

Project Structure

  • /dist build files
  • /public/i18n/locales internationalization files
  • /openapi-gen autogenerated types and queries based on the public REST API openapi spec. Do not manually edit.
  • /rules linting rules for javascript and typescript code
  • /src/assets static assets for the UI like icons
  • /src/components shared components across the UI
  • /src/constants constants used across the UI like managing search parameters
  • /src/utils utility functions used across the UI
  • /src/layouts common React layouts used by many pages
  • /src/pages individual pages for the UI
  • /src/context context providers that wrap around whole pages or the whole app
  • /src/queries wrappers around autogenerated react query hooks to handle specific side effects
  • /src/mocks mock data for testing
  • /src/main.tsx entry point for the UI
  • /src/router.tsx the router for the UI, update this to add new pages or routes
  • /src/theme.ts the theme for the UI, update this to change the colors, fonts, etc.
  • /src/queryClient.ts the 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).

React, JSX and Chakra

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.

React Query

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.

Best Practices

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 test

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

What 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.