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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions airflow-core/docs/howto/customize-ui.rst
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ We can provide a JSON configuration to customize the UI.
.. important::

- You can customize the ``brand``, ``gray``, ``black``, and ``white`` color tokens, ``globalCss``, and the navigation icon via ``icon`` (and ``icon_dark_mode``).
- All top-level fields (``tokens``, ``globalCss``, ``icon``, ``icon_dark_mode``) are **optional** — you can supply any combination, including an empty ``{}`` to restore OSS defaults.
- All color tokens are **optional** — you can override any subset without supplying the others.
- ``brand`` and ``gray`` each accept an 11-shade scale with keys ``50``–``950``.
- ``black`` and ``white`` each accept a single color: ``{ "value": "oklch(...)" }``.
Expand Down
1 change: 1 addition & 0 deletions airflow-core/newsfragments/64552.improvement.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Allow UI theme config with only CSS overrides, icon only, or empty ``{}`` to restore OSS defaults. The ``tokens`` field is now optional in the theme configuration.
8 changes: 2 additions & 6 deletions airflow-core/src/airflow/api_fastapi/common/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from dataclasses import dataclass
from datetime import timedelta
from enum import Enum
from typing import Annotated, Any, Literal
from typing import Annotated, Literal

from pydantic import (
AfterValidator,
Expand Down Expand Up @@ -208,15 +208,11 @@ def check_at_least_one_color(self) -> ThemeColors:
raise ValueError("At least one color token must be provided: brand, gray, black, or white")
return self

@model_serializer(mode="wrap")
def serialize_model(self, handler: Any) -> dict:
return {k: v for k, v in handler(self).items() if v is not None}


class Theme(BaseModel):
"""JSON to modify Chakra's theme."""

tokens: dict[Literal["colors"], ThemeColors]
tokens: dict[Literal["colors"], ThemeColors] | None = None
globalCss: dict[str, dict] | None = None
icon: ThemeIconType = None
icon_dark_mode: ThemeIconType = None
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,17 @@
# under the License.
from __future__ import annotations

from pydantic import ConfigDict, field_serializer

from airflow.api_fastapi.common.types import Theme, UIAlert
from airflow.api_fastapi.core_api.base import BaseModel


class ConfigResponse(BaseModel):
"""configuration serializer."""

model_config = ConfigDict(json_schema_mode_override="validation")

fallback_page_limit: int
auto_refresh_interval: int
hide_paused_dags_by_default: bool
Expand All @@ -36,3 +40,9 @@ class ConfigResponse(BaseModel):
external_log_name: str | None = None
theme: Theme | None
multi_team: bool

@field_serializer("theme")
def serialize_theme(self, theme: Theme | None) -> dict | None:
if theme is None:
return None
return theme.model_dump(exclude_none=True)
Original file line number Diff line number Diff line change
Expand Up @@ -3272,11 +3272,13 @@ components:
Theme:
properties:
tokens:
additionalProperties:
$ref: '#/components/schemas/ThemeColors'
propertyNames:
const: colors
type: object
anyOf:
- additionalProperties:
$ref: '#/components/schemas/ThemeColors'
propertyNames:
const: colors
type: object
- type: 'null'
title: Tokens
globalCss:
anyOf:
Expand All @@ -3297,13 +3299,80 @@ components:
- type: 'null'
title: Icon Dark Mode
type: object
required:
- tokens
title: Theme
description: JSON to modify Chakra's theme.
ThemeColors:
additionalProperties: true
properties:
brand:
anyOf:
- additionalProperties:
additionalProperties:
$ref: '#/components/schemas/OklchColor'
propertyNames:
const: value
type: object
propertyNames:
enum:
- '50'
- '100'
- '200'
- '300'
- '400'
- '500'
- '600'
- '700'
- '800'
- '900'
- '950'
type: object
- type: 'null'
title: Brand
gray:
anyOf:
- additionalProperties:
additionalProperties:
$ref: '#/components/schemas/OklchColor'
propertyNames:
const: value
type: object
propertyNames:
enum:
- '50'
- '100'
- '200'
- '300'
- '400'
- '500'
- '600'
- '700'
- '800'
- '900'
- '950'
type: object
- type: 'null'
title: Gray
black:
anyOf:
- additionalProperties:
$ref: '#/components/schemas/OklchColor'
propertyNames:
const: value
type: object
- type: 'null'
title: Black
white:
anyOf:
- additionalProperties:
$ref: '#/components/schemas/OklchColor'
propertyNames:
const: value
type: object
- type: 'null'
title: White
type: object
title: ThemeColors
description: Color tokens for the UI theme. All fields are optional; at least
one must be provided.
TokenType:
type: string
enum:
Expand Down
109 changes: 99 additions & 10 deletions airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8895,13 +8895,20 @@ export const $TeamResponse = {
export const $Theme = {
properties: {
tokens: {
additionalProperties: {
'$ref': '#/components/schemas/ThemeColors'
},
propertyNames: {
const: 'colors'
},
type: 'object',
anyOf: [
{
additionalProperties: {
'$ref': '#/components/schemas/ThemeColors'
},
propertyNames: {
const: 'colors'
},
type: 'object'
},
{
type: 'null'
}
],
title: 'Tokens'
},
globalCss: {
Expand Down Expand Up @@ -8943,14 +8950,96 @@ export const $Theme = {
}
},
type: 'object',
required: ['tokens'],
title: 'Theme',
description: "JSON to modify Chakra's theme."
} as const;

export const $ThemeColors = {
additionalProperties: true,
type: 'object'
properties: {
brand: {
anyOf: [
{
additionalProperties: {
additionalProperties: {
'$ref': '#/components/schemas/OklchColor'
},
propertyNames: {
const: 'value'
},
type: 'object'
},
propertyNames: {
enum: ['50', '100', '200', '300', '400', '500', '600', '700', '800', '900', '950']
},
type: 'object'
},
{
type: 'null'
}
],
title: 'Brand'
},
gray: {
anyOf: [
{
additionalProperties: {
additionalProperties: {
'$ref': '#/components/schemas/OklchColor'
},
propertyNames: {
const: 'value'
},
type: 'object'
},
propertyNames: {
enum: ['50', '100', '200', '300', '400', '500', '600', '700', '800', '900', '950']
},
type: 'object'
},
{
type: 'null'
}
],
title: 'Gray'
},
black: {
anyOf: [
{
additionalProperties: {
'$ref': '#/components/schemas/OklchColor'
},
propertyNames: {
const: 'value'
},
type: 'object'
},
{
type: 'null'
}
],
title: 'Black'
},
white: {
anyOf: [
{
additionalProperties: {
'$ref': '#/components/schemas/OklchColor'
},
propertyNames: {
const: 'value'
},
type: 'object'
},
{
type: 'null'
}
],
title: 'White'
}
},
type: 'object',
title: 'ThemeColors',
description: 'Color tokens for the UI theme. All fields are optional; at least one must be provided.'
} as const;

export const $TokenType = {
Expand Down
26 changes: 22 additions & 4 deletions airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2199,9 +2199,9 @@ export type TeamResponse = {
* JSON to modify Chakra's theme.
*/
export type Theme = {
tokens: {
[key: string]: ThemeColors;
};
tokens?: {
[key: string]: ThemeColors;
} | null;
globalCss?: {
[key: string]: {
[key: string]: unknown;
Expand All @@ -2211,8 +2211,26 @@ export type Theme = {
icon_dark_mode?: string | null;
};

/**
* Color tokens for the UI theme. All fields are optional; at least one must be provided.
*/
export type ThemeColors = {
[key: string]: unknown;
brand?: {
[key: string]: {
[key: string]: OklchColor;
};
} | null;
gray?: {
[key: string]: {
[key: string]: OklchColor;
};
} | null;
black?: {
[key: string]: OklchColor;
} | null;
white?: {
[key: string]: OklchColor;
} | null;
};

/**
Expand Down
20 changes: 12 additions & 8 deletions airflow-core/src/airflow/ui/src/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -406,16 +406,20 @@ const defaultAirflowTheme = {
export const createTheme = (userTheme?: Theme) => {
const defaultAirflowConfig = defineConfig({ theme: defaultAirflowTheme });

const userConfig = defineConfig(
userTheme
? {
theme: { tokens: userTheme.tokens },
const userConfig = userTheme
? defineConfig({
...(userTheme.tokens !== undefined && {
theme: { tokens: userTheme.tokens as Record<string, unknown> },
}),
...(userTheme.globalCss !== undefined && {
globalCss: userTheme.globalCss as Record<string, SystemStyleObject>,
}
: {},
);
}),
})
: undefined;

const mergedConfig = mergeConfigs(defaultConfig, defaultAirflowConfig, userConfig);
const mergedConfig = userConfig
? mergeConfigs(defaultConfig, defaultAirflowConfig, userConfig)
: mergeConfigs(defaultConfig, defaultAirflowConfig);

return createSystem(mergedConfig);
};
Expand Down
Loading
Loading