Skip to content
Closed
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
48 changes: 48 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,54 @@ After

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

Customizing side/navbar color
-----------------------------

We can provide a color to generate and apply a custom color palette for the UI's side/navbar.

The color is used as the base for generating a cohesive theme that adapts text, backgrounds, and accent colors automatically.

.. important::

- The color must be provided as a six-digit hexadecimal value in the form ``#RRGGBB``
- Invalid or missing values the theme to the built-in default color scheme.

.. note::

Both pure white ``#ffffff`` and pure black ``#000000`` will generate the same color palette (grayscale).
They have no hue or color saturation. Their only difference is brightness, not color tone.

To make this change, simply:

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

.. code-block::

[api]

theme = "#ff0000"


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

.. code-block::

AIRFLOW__API__THEME = "#ff0000"


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.
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,4 @@ class ConfigResponse(BaseModel):
dashboard_alert: list[UIAlert]
show_external_log_redirect: bool
external_log_name: str | None = None
theme: str
Original file line number Diff line number Diff line change
Expand Up @@ -1245,6 +1245,9 @@ components:
- type: string
- type: 'null'
title: External Log Name
theme:
type: string
title: Theme
type: object
required:
- page_size
Expand All @@ -1257,6 +1260,7 @@ components:
- test_connection
- dashboard_alert
- show_external_log_redirect
- theme
title: ConfigResponse
description: configuration serializer.
ConnectionHookFieldBehavior:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,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": conf.get("api", "theme", fallback=""),
}

config.update({key: value for key, value in additional_config.items()})
Expand Down
8 changes: 8 additions & 0 deletions airflow-core/src/airflow/config_templates/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1360,6 +1360,14 @@ api:
type: string
example: ~
default:
theme:
description: |
Creates and sets a custom color palette for the side/navbar based on a color.
It expects a hex six-digit form. If the value is not provided UI will use default theme.
version_added: ~
type: string
example: "#dce7f5"
default:
enable_swagger_ui:
description: |
Boolean for running SwaggerUI in the webserver.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6983,10 +6983,14 @@ export const $ConfigResponse = {
}
],
title: 'External Log Name'
},
theme: {
type: 'string',
title: 'Theme'
}
},
type: 'object',
required: ['page_size', 'auto_refresh_interval', 'hide_paused_dags_by_default', 'instance_name', 'enable_swagger_ui', 'require_confirmation_dag_change', 'default_wrap', 'test_connection', 'dashboard_alert', 'show_external_log_redirect'],
required: ['page_size', 'auto_refresh_interval', 'hide_paused_dags_by_default', 'instance_name', 'enable_swagger_ui', 'require_confirmation_dag_change', 'default_wrap', 'test_connection', 'dashboard_alert', 'show_external_log_redirect', 'theme'],
title: 'ConfigResponse',
description: 'configuration serializer.'
} as const;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1749,6 +1749,7 @@ export type ConfigResponse = {
dashboard_alert: Array<UIAlert>;
show_external_log_redirect: boolean;
external_log_name?: string | null;
theme: string;
};

/**
Expand Down
4 changes: 3 additions & 1 deletion airflow-core/src/airflow/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@
"use-debounce": "^10.0.4",
"usehooks-ts": "^3.1.1",
"yaml": "^2.6.1",
"zustand": "^5.0.4"
"zustand": "^5.0.4",
"culori": "^4.0.2",
"@types/culori": "^4.0.1"
},
"devDependencies": {
"@7nohe/openapi-react-query-codegen": "^1.6.2",
Expand Down
17 changes: 17 additions & 0 deletions airflow-core/src/airflow/ui/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*!
* 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.
*/
import { ChakraProvider } from "@chakra-ui/react";
import { useMemo, type PropsWithChildren } from "react";

import { useConfig } from "src/queries/useConfig";
import { createTheme } from "src/theme";
import { generatePalette } from "src/utils/generatePalette";

export const ChakraCustomProvider = ({ children }: PropsWithChildren) => {
const getTheme = useConfig("theme");

const system = useMemo(() => {
if (typeof getTheme === "undefined") {
return undefined;
}

const theme = typeof getTheme === "string" ? getTheme : "";

return createTheme(generatePalette(theme));
}, [getTheme]);

return system && <ChakraProvider value={system}>{children}</ChakraProvider>;
};
20 changes: 20 additions & 0 deletions airflow-core/src/airflow/ui/src/context/chakraCustom/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*!
* 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.
*/

export * from "./ChakraCustomProvider";
15 changes: 7 additions & 8 deletions airflow-core/src/airflow/ui/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
* specific language governing permissions and limitations
* under the License.
*/
import { ChakraProvider } from "@chakra-ui/react";
import { QueryClientProvider } from "@tanstack/react-query";
import axios, { type AxiosError } from "axios";
import { StrictMode } from "react";
Expand All @@ -29,14 +28,14 @@ import * as ReactRouterDOM from "react-router-dom";
import * as ReactJSXRuntime from "react/jsx-runtime";

import type { HTTPExceptionResponse } from "openapi/requests/types.gen";
import { ChakraCustomProvider } from "src/context/chakraCustom";
import { ColorModeProvider } from "src/context/colorMode";
import { TimezoneProvider } from "src/context/timezone";
import { router } from "src/router";
import { getRedirectPath } from "src/utils/links.ts";

import i18n from "./i18n/config";
import { client } from "./queryClient";
import { system } from "./theme";

// Set React, ReactDOM, and ReactJSXRuntime on globalThis to share them with the dynamically imported React plugins.
// Only one instance of React should be used.
Expand Down Expand Up @@ -69,15 +68,15 @@ axios.interceptors.response.use(
createRoot(document.querySelector("#root") as HTMLDivElement).render(
<StrictMode>
<I18nextProvider i18n={i18n}>
<ChakraProvider value={system}>
<ColorModeProvider>
<QueryClientProvider client={client}>
<QueryClientProvider client={client}>
<ChakraCustomProvider>
<ColorModeProvider>
<TimezoneProvider>
<RouterProvider router={router} />
</TimezoneProvider>
</QueryClientProvider>
</ColorModeProvider>
</ChakraProvider>
</ColorModeProvider>
</ChakraCustomProvider>
</QueryClientProvider>
</I18nextProvider>
</StrictMode>,
);
1 change: 1 addition & 0 deletions airflow-core/src/airflow/ui/src/mocks/handlers/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const handlers: Array<HttpHandler> = [
page_size: 15,
require_confirmation_dag_change: false,
test_connection: "Disabled",
theme: "",
}),
),
];
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import type { TaskInstanceResponse } from "openapi/requests/types.gen";
import { TaskTrySelect } from "src/components/TaskTrySelect";
import { Button, Menu, Select, Tooltip } from "src/components/ui";
import { SearchParamsKeys } from "src/constants/searchParams";
import { system } from "src/theme";
import { createTheme } from "src/theme";
import { type LogLevel, logLevelColorMapping, logLevelOptions } from "src/utils/logs";

type Props = {
Expand Down Expand Up @@ -85,6 +85,7 @@ export const TaskLogHeader = ({
const sources = searchParams.getAll(SearchParamsKeys.SOURCE);
const logLevels = searchParams.getAll(SearchParamsKeys.LOG_LEVEL);
const hasLogLevels = logLevels.length > 0;
const system = createTheme();

// Have select zIndex greater than modal zIndex in fullscreen so that
// select options are displayed.
Expand Down
Loading