| .. Licensed to the Apache Software Foundation (ASF) under one |
| or more contributor license agreements. See the NOTICE file |
| distributed with this work for additional information |
| regarding copyright ownership. The ASF licenses this file |
| to you under the Apache License, Version 2.0 (the |
| "License"); you may not use this file except in compliance |
| with the License. You may obtain a copy of the License at |
| |
| .. http://www.apache.org/licenses/LICENSE-2.0 |
| |
| .. Unless required by applicable law or agreed to in writing, |
| software distributed under the License is distributed on an |
| "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| KIND, either express or implied. See the License for the |
| specific language governing permissions and limitations |
| under the License. |
| |
| 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 <https://vitejs.dev/guide/>`__. |
| 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. |
| |
| System Requirements |
| ------------------- |
| |
| Building the Airflow UI requires at least 8GB of system RAM (6GB available). |
| |
| By default, the build process allocates 4GB of heap memory to Node.js. If you encounter |
| out-of-memory errors during the build, you can increase this by setting the ``NODE_OPTIONS`` |
| environment variable: |
| |
| .. code-block:: bash |
| |
| # Increase to 8GB |
| export NODE_OPTIONS="--max-old-space-size=8192" |
| breeze start-airflow |
| |
| pnpm commands |
| ------------- |
| |
| Follow the `pnpm docs <https://pnpm.io/installation>`__ to install pnpm locally. |
| Follow the `nvm docs <https://github.com/nvm-sh/nvm>`__ to manage your node version, which needs to be v22 or higher. |
| |
| .. code-block:: bash |
| |
| # 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 <https://reactjs.org/>`__. |
| 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 <https://chakra-ui.com/>`__ 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 <https://tanstack.com/query/latest/docs/framework/react/overview>`__ 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: |
| |
| .. code-block:: bash |
| |
| # 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 <https://www.chakra-ui.com/docs/theming/colors#semantic-tokens/>`__ for colors whenever possible. |
| For example, instead of using ``red.500``, use ``red.focusRing`` or for text, use ``fg.error``. |
| |
| .. code-block:: typescript |
| // ❌ BAD: Directly using color number |
| <Box border="1px solid" borderColor="gray.800" /> |
| |
| // ✅ Good: Using color semantic token |
| <Box border="1px solid" borderColor="gray.subtle" /> |
| |
| // ✅ BEST: Using semantic tokens for both color values |
| <Box border="1px solid" borderColor="border.inverted" /> |
| |
| **Effects** |
| |
| If you find yourself calling useEffect, you should double check if you really need it. |
| Check out React's documentation on `useEffect <https://react.dev/learn/you-might-not-need-an-effect>`__ 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: |
| |
| .. code-block:: bash |
| |
| # 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/``) |
| |
| .. code-block:: typescript |
| |
| // 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 |
| |
| .. code-block:: typescript |
| |
| 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: |
| |
| .. code-block:: typescript |
| |
| // ❌ 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 <16_adding_api_endpoints.rst>`__. |