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
89 changes: 89 additions & 0 deletions airflow-core/docs/howto/customize-ui.rst
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,95 @@ After

.. image:: ../img/change-site-title/example_instance_name_configuration.png

.. _customizing-ui-theme:

Customizing UI theme
--------------------

We can provide a JSON configuration to customize the UI.

.. important::

- Currently only the ``brand`` color palette can be customized.
- You must supply ``50``-``950`` OKLCH color values for ``brand`` color.
- OKLCH colors must have next format ``oklch(l c h)`` For more info see :ref:`config:api__theme`

.. note::

Modifying the ``brand`` color palette you also modify the navbar/sidebar.

To customize the UI, simply:

1. Add the configuration option of ``theme`` under the ``[api]`` section inside ``airflow.cfg``:

.. code-block::

[api]

theme = {
"tokens": {
"colors": {
"brand": {
"50": { "value": "oklch(0.971 0.013 17.38)" },
"100": { "value": "oklch(0.936 0.032 17.717)" },
"200": { "value": "oklch(0.885 0.062 18.334)" },
"300": { "value": "oklch(0.808 0.114 19.571)" },
"400": { "value": "oklch(0.704 0.191 22.216)" },
"500": { "value": "oklch(0.637 0.237 25.331)" },
"600": { "value": "oklch(0.577 0.245 27.325)" },
"700": { "value": "oklch(0.505 0.213 27.518)" },
"800": { "value": "oklch(0.444 0.177 26.899)" },
"900": { "value": "oklch(0.396 0.141 25.723)" },
"950": { "value": "oklch(0.258 0.092 26.042)" }
}
}
}
}


.. note::

The whitespace, particularly on the last line, is important so a multi-line value works properly. More details can be found in the
the `configparser docs <https://docs.python.org/3/library/configparser.html#supported-ini-file-structure>`_.

2. Alternatively, you can set a custom title using the environment variable:

.. code-block::

AIRFLOW__API__THEME='{
"tokens": {
"colors": {
"brand": {
"50": { "value": "oklch(0.971 0.013 17.38)" },
"100": { "value": "oklch(0.936 0.032 17.717)" },
"200": { "value": "oklch(0.885 0.062 18.334)" },
"300": { "value": "oklch(0.808 0.114 19.571)" },
"400": { "value": "oklch(0.704 0.191 22.216)" },
"500": { "value": "oklch(0.637 0.237 25.331)" },
"600": { "value": "oklch(0.577 0.245 27.325)" },
"700": { "value": "oklch(0.505 0.213 27.518)" },
"800": { "value": "oklch(0.444 0.177 26.899)" },
"900": { "value": "oklch(0.396 0.141 25.723)" },
"950": { "value": "oklch(0.258 0.092 26.042)" }
}
}
}
}'


Screenshots
^^^^^^^^^^^

Light Mode
""""""""""

.. image:: ../img/change-theme/exmaple_theme_configuration_light_mode.png

Dark Mode
"""""""""

.. image:: ../img/change-theme/exmaple_theme_configuration_dark_mode.png

|

Adding Dashboard Alert Messages
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
71 changes: 71 additions & 0 deletions airflow-core/src/airflow/api_fastapi/common/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
# under the License.
from __future__ import annotations

import re
from dataclasses import dataclass
from datetime import timedelta
from enum import Enum
Expand All @@ -28,6 +29,9 @@
BaseModel,
BeforeValidator,
ConfigDict,
field_validator,
model_serializer,
model_validator,
)

from airflow._shared.timezones import timezone
Expand Down Expand Up @@ -107,3 +111,70 @@ class UIAlert(BaseModel):

text: str
category: Literal["info", "warning", "error"]


class OklchColor(BaseModel):
"""Validates OKLCH color format from string oklch(l c h)."""

lightness: float
chroma: float
hue: float

@model_validator(mode="before")
@classmethod
def parse_oklch_string(cls, data):
if isinstance(data, str):
oklch_regex_pattern = r"^oklch\((-?\d+(?:\.\d+)?) (-?\d+(?:\.\d+)?) (-?\d+(?:\.\d+)?)\)$"
match = re.match(oklch_regex_pattern, data)

if not match:
raise ValueError(f"Invalid OKLCH format: {data} Expected format oklch(l c h)")

ligthness_str, chroma_str, hue_str = match.groups()

return {
"lightness": float(ligthness_str),
"chroma": float(chroma_str),
"hue": float(hue_str),
}
return data

@field_validator("lightness")
@classmethod
def validate_lightness(cls, value: float) -> float:
if value < 0 or value > 1:
raise ValueError(f"Invalid lightness: {value} Must be between 0 and 1")
return value

@field_validator("chroma")
@classmethod
def validate_chroma(cls, value: float) -> float:
if value < 0 or value > 0.5:
raise ValueError(f"Invalid chroma: {value} Must be between 0 and 0.5")
return value

@field_validator("hue")
@classmethod
def validate_hue(cls, value: float) -> float:
if value < 0 or value > 360:
raise ValueError(f"Invalid hue: {value} Must be between 0 and 360")
return value

@model_serializer(mode="plain")
def serialize_model(self) -> str:
return f"oklch({self.lightness} {self.chroma} {self.hue})"


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

tokens: dict[
Literal["colors"],
dict[
Literal["brand"],
dict[
Literal["50", "100", "200", "300", "400", "500", "600", "700", "800", "900", "950"],
dict[Literal["value"], OklchColor],
],
],
]
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

from pydantic import BaseModel

from airflow.api_fastapi.common.types import UIAlert
from airflow.api_fastapi.common.types import Theme, UIAlert


class ConfigResponse(BaseModel):
Expand All @@ -35,3 +35,4 @@ class ConfigResponse(BaseModel):
dashboard_alert: list[UIAlert]
show_external_log_redirect: bool
external_log_name: str | None = None
theme: Theme
Original file line number Diff line number Diff line change
Expand Up @@ -1329,6 +1329,8 @@ components:
- type: string
- type: 'null'
title: External Log Name
theme:
$ref: '#/components/schemas/Theme'
type: object
required:
- page_size
Expand All @@ -1341,6 +1343,7 @@ components:
- test_connection
- dashboard_alert
- show_external_log_redirect
- theme
title: ConfigResponse
description: configuration serializer.
ConnectionHookFieldBehavior:
Expand Down Expand Up @@ -2297,6 +2300,8 @@ components:
- type
title: NodeResponse
description: Node serializer for responses.
OklchColor:
type: string
ReprocessBehavior:
type: string
enum:
Expand Down Expand Up @@ -2650,6 +2655,43 @@ components:
- name
title: TeamResponse
description: Base serializer for Team.
Theme:
properties:
tokens:
additionalProperties:
additionalProperties:
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
propertyNames:
const: brand
type: object
propertyNames:
const: colors
type: object
title: Tokens
type: object
required:
- tokens
title: Theme
description: JSON to modify Chakra's theme.
TriggerResponse:
properties:
id:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
# under the License.
from __future__ import annotations

from json import loads
from typing import Any

from fastapi import Depends, status
Expand All @@ -31,6 +32,27 @@

config_router = AirflowRouter(tags=["Config"])

THEME_FALLBACK = """
{
"tokens": {
"colors": {
"brand": {
"50": { "value": "oklch(0.98 0.006 248.717)" },
"100": { "value": "oklch(0.962 0.012 249.46)" },
"200": { "value": "oklch(0.923 0.023 255.082)" },
"300": { "value": "oklch(0.865 0.039 252.42)" },
"400": { "value": "oklch(0.705 0.066 256.378)" },
"500": { "value": "oklch(0.575 0.08 257.759)" },
"600": { "value": "oklch(0.469 0.084 257.657)" },
"700": { "value": "oklch(0.399 0.084 257.85)" },
"800": { "value": "oklch(0.324 0.072 260.329)" },
"900": { "value": "oklch(0.259 0.062 265.566)" },
"950": { "value": "oklch(0.179 0.05 265.487)" }
}
}
}
}
"""

API_CONFIG_KEYS = [
"enable_swagger_ui",
Expand Down Expand Up @@ -59,6 +81,7 @@ def get_configs() -> ConfigResponse:
"dashboard_alert": [alert for alert in DASHBOARD_UIALERTS if isinstance(alert, UIAlert)],
"show_external_log_redirect": task_log_reader.supports_external_link,
"external_log_name": getattr(task_log_reader.log_handler, "log_name", None),
"theme": loads(conf.get("api", "theme", fallback=THEME_FALLBACK)),
}

config.update({key: value for key, value in additional_config.items()})
Expand Down
60 changes: 60 additions & 0 deletions airflow-core/src/airflow/config_templates/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1359,6 +1359,66 @@ api:
type: string
example: ~
default:
theme:
description: |
JSON config to customize the Chakra UI theme.
Currently only supports ``brand`` color customization.

Must supply ``50``-``950`` OKLCH color values for ``brand`` color.
For usage see :ref:`customizing-ui-theme`

.. important::
``oklch(l c h)`` must follow next format:

- l (lightness) : ``float`` Must be between 0 and 1
- c (chroma) : ``float`` Must be between 0 and 0.5
- h (hue) : ``float`` Must be between 0 and 360

Note: As shown below, you can split your json config over multiple lines by indenting.
See configparser documentation for an example:
https://docs.python.org/3/library/configparser.html#supported-ini-file-structure.
version_added: ~
type: string
example: >
{
"tokens": {
"colors": {
"brand": {
"50": { "value": "oklch(0.971 0.013 17.38)" },
"100": { "value": "oklch(0.936 0.032 17.717)" },
"200": { "value": "oklch(0.885 0.062 18.334)" },
"300": { "value": "oklch(0.808 0.114 19.571)" },
"400": { "value": "oklch(0.704 0.191 22.216)" },
"500": { "value": "oklch(0.637 0.237 25.331)" },
"600": { "value": "oklch(0.577 0.245 27.325)" },
"700": { "value": "oklch(0.505 0.213 27.518)" },
"800": { "value": "oklch(0.444 0.177 26.899)" },
"900": { "value": "oklch(0.396 0.141 25.723)" },
"950": { "value": "oklch(0.258 0.092 26.042)" }
}
}
}
}
default: >
{{
"tokens": {{
"colors": {{
"brand": {{
"50": {{ "value": "oklch(0.98 0.006 248.717)" }},
"100": {{ "value": "oklch(0.962 0.012 249.460)" }},
"200": {{ "value": "oklch(0.923 0.023 255.082)" }},
"300": {{ "value": "oklch(0.865 0.039 252.420)" }},
"400": {{ "value": "oklch(0.705 0.066 256.378)" }},
"500": {{ "value": "oklch(0.575 0.08 257.759)" }},
"600": {{ "value": "oklch(0.469 0.084 257.657)" }},
"700": {{ "value": "oklch(0.399 0.084 257.850)" }},
"800": {{ "value": "oklch(0.324 0.072 260.329)" }},
"900": {{ "value": "oklch(0.259 0.062 265.566)" }},
"950": {{ "value": "oklch(0.179 0.05 265.487)" }}
}}
}}
}}
}}
enable_swagger_ui:
description: |
Boolean for running SwaggerUI in the webserver.
Expand Down
Loading
Loading