blob: cdbb8840c8d02c7c7217c897a70e22796f8bf982 [file] [log] [blame]
.. 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>`__.