This is an automated email from the ASF dual-hosted git repository.
bbovenzi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new ba7b407455c Add is_favorite to ui dags list (#56341)
ba7b407455c is described below
commit ba7b407455c2d6ead4eba54ad75ab1f0156056b3
Author: Brent Bovenzi <[email protected]>
AuthorDate: Thu Oct 9 16:31:24 2025 -0400
Add is_favorite to ui dags list (#56341)
* Add is_favorite to ui dags list
* Update dagdetails
---
.../api_fastapi/core_api/datamodels/dags.py | 1 +
.../api_fastapi/core_api/datamodels/ui/dags.py | 1 +
.../api_fastapi/core_api/openapi/_private_ui.yaml | 4 ++
.../core_api/openapi/v2-rest-api-generated.yaml | 4 ++
.../api_fastapi/core_api/routes/public/dags.py | 18 +++++-
.../airflow/api_fastapi/core_api/routes/ui/dags.py | 11 ++++
.../airflow/ui/openapi-gen/requests/schemas.gen.ts | 11 +++-
.../airflow/ui/openapi-gen/requests/types.gen.ts | 2 +
.../components/DagActions/FavoriteDagButton.tsx | 33 +++++------
.../src/airflow/ui/src/mocks/handlers/dags.ts | 4 ++
.../src/airflow/ui/src/pages/Dag/Header.tsx | 2 +-
.../airflow/ui/src/pages/DagsList/DagCard.test.tsx | 1 +
.../src/airflow/ui/src/pages/DagsList/DagCard.tsx | 2 +-
.../src/airflow/ui/src/pages/DagsList/DagsList.tsx | 5 +-
.../src/airflow/ui/src/queries/useFavoriteDag.ts | 33 -----------
.../airflow/ui/src/queries/useToggleFavoriteDag.ts | 66 ++++++++++++++++++++++
.../src/airflow/ui/src/queries/useUnfavoriteDag.ts | 33 -----------
.../core_api/routes/public/test_dags.py | 41 ++++++++++++++
.../api_fastapi/core_api/routes/ui/test_dags.py | 36 ++++++++++++
.../src/airflowctl/api/datamodels/generated.py | 1 +
20 files changed, 217 insertions(+), 92 deletions(-)
diff --git a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dags.py
b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dags.py
index 23aa4ec300f..2d14f29c41a 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dags.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/dags.py
@@ -157,6 +157,7 @@ class DAGDetailsResponse(DAGResponse):
last_parsed: datetime | None
default_args: abc.Mapping | None
owner_links: dict[str, str] | None = None
+ is_favorite: bool = False
@field_validator("timezone", mode="before")
@classmethod
diff --git
a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/dags.py
b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/dags.py
index e76c5414d6d..97221f6192d 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/dags.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/dags.py
@@ -29,6 +29,7 @@ class DAGWithLatestDagRunsResponse(DAGResponse):
asset_expression: dict | None
latest_dag_runs: list[DAGRunResponse]
pending_actions: list[HITLDetail]
+ is_favorite: bool
class DAGWithLatestDagRunsCollectionResponse(BaseModel):
diff --git
a/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml
b/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml
index 549ce5ae055..a3858d6fc2a 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml
+++ b/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml
@@ -1686,6 +1686,9 @@ components:
$ref: '#/components/schemas/HITLDetail'
type: array
title: Pending Actions
+ is_favorite:
+ type: boolean
+ title: Is Favorite
file_token:
type: string
title: File Token
@@ -1721,6 +1724,7 @@ components:
- asset_expression
- latest_dag_runs
- pending_actions
+ - is_favorite
- file_token
title: DAGWithLatestDagRunsResponse
description: DAG with latest dag runs response serializer.
diff --git
a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
index ac87cfdd7d0..88d5a1ff431 100644
---
a/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
+++
b/airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-generated.yaml
@@ -10017,6 +10017,10 @@ components:
type: object
- type: 'null'
title: Owner Links
+ is_favorite:
+ type: boolean
+ title: Is Favorite
+ default: false
file_token:
type: string
title: File Token
diff --git
a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dags.py
b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dags.py
index e62c12c5ecb..a50c1b35069 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dags.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/public/dags.py
@@ -209,7 +209,9 @@ def get_dag(
),
dependencies=[Depends(requires_access_dag(method="GET"))],
)
-def get_dag_details(dag_id: str, session: SessionDep, dag_bag: DagBagDep) ->
DAGDetailsResponse:
+def get_dag_details(
+ dag_id: str, session: SessionDep, dag_bag: DagBagDep, user: GetUserDep
+) -> DAGDetailsResponse:
"""Get details of DAG."""
dag = get_latest_version_of_dag(dag_bag, dag_id, session)
@@ -221,7 +223,19 @@ def get_dag_details(dag_id: str, session: SessionDep,
dag_bag: DagBagDep) -> DAG
if not key.startswith("_") and not hasattr(dag_model, key):
setattr(dag_model, key, value)
- return dag_model
+ # Check if this DAG is marked as favorite by the current user
+ user_id = str(user.get_id())
+ is_favorite = (
+ session.scalar(
+ select(DagFavorite.dag_id).where(DagFavorite.user_id == user_id,
DagFavorite.dag_id == dag_id)
+ )
+ is not None
+ )
+
+ # Add is_favorite field to the DAG model
+ setattr(dag_model, "is_favorite", is_favorite)
+
+ return DAGDetailsResponse.model_validate(dag_model)
@dags_router.patch(
diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/dags.py
b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/dags.py
index af4ed341f7f..ff20874b50f 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/dags.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/dags.py
@@ -61,10 +61,12 @@ from airflow.api_fastapi.core_api.datamodels.ui.dags import
(
)
from airflow.api_fastapi.core_api.openapi.exceptions import
create_openapi_http_exception_doc
from airflow.api_fastapi.core_api.security import (
+ GetUserDep,
ReadableDagsFilterDep,
requires_access_dag,
)
from airflow.models import DagModel, DagRun
+from airflow.models.dag_favorite import DagFavorite
from airflow.models.hitl import HITLDetail
from airflow.models.taskinstance import TaskInstance
from airflow.utils.state import TaskInstanceState
@@ -114,6 +116,7 @@ def get_dags(
has_pending_actions: QueryPendingActionsFilter,
readable_dags_filter: ReadableDagsFilterDep,
session: SessionDep,
+ user: GetUserDep,
dag_runs_limit: int = 10,
) -> DAGWithLatestDagRunsCollectionResponse:
"""Get DAGs with recent DagRun."""
@@ -153,6 +156,13 @@ def get_dags(
dags = [dag for dag in session.scalars(dags_select)]
+ # Fetch favorite status for each DAG for the current user
+ user_id = str(user.get_id())
+ favorites_select = select(DagFavorite.dag_id).where(
+ DagFavorite.user_id == user_id, DagFavorite.dag_id.in_([dag.dag_id for
dag in dags])
+ )
+ favorite_dag_ids = set(session.scalars(favorites_select))
+
# Populate the last 'dag_runs_limit' DagRuns for each DAG
recent_runs_subquery = (
select(
@@ -224,6 +234,7 @@ def get_dags(
"asset_expression": dag.asset_expression,
"latest_dag_runs": [],
"pending_actions": pending_actions_by_dag_id[dag.dag_id],
+ "is_favorite": dag.dag_id in favorite_dag_ids,
}
)
for dag in dags
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
index 7d344a546cc..23b53707db2 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
@@ -2041,6 +2041,11 @@ export const $DAGDetailsResponse = {
],
title: 'Owner Links'
},
+ is_favorite: {
+ type: 'boolean',
+ title: 'Is Favorite',
+ default: false
+ },
file_token: {
type: 'string',
title: 'File Token',
@@ -7343,6 +7348,10 @@ export const $DAGWithLatestDagRunsResponse = {
type: 'array',
title: 'Pending Actions'
},
+ is_favorite: {
+ type: 'boolean',
+ title: 'Is Favorite'
+ },
file_token: {
type: 'string',
title: 'File Token',
@@ -7351,7 +7360,7 @@ export const $DAGWithLatestDagRunsResponse = {
}
},
type: 'object',
- required: ['dag_id', 'dag_display_name', 'is_paused', 'is_stale',
'last_parsed_time', 'last_parse_duration', 'last_expired', 'bundle_name',
'bundle_version', 'relative_fileloc', 'fileloc', 'description',
'timetable_summary', 'timetable_description', 'tags', 'max_active_tasks',
'max_active_runs', 'max_consecutive_failed_dag_runs',
'has_task_concurrency_limits', 'has_import_errors', 'next_dagrun_logical_date',
'next_dagrun_data_interval_start', 'next_dagrun_data_interval_end', 'next_da
[...]
+ required: ['dag_id', 'dag_display_name', 'is_paused', 'is_stale',
'last_parsed_time', 'last_parse_duration', 'last_expired', 'bundle_name',
'bundle_version', 'relative_fileloc', 'fileloc', 'description',
'timetable_summary', 'timetable_description', 'tags', 'max_active_tasks',
'max_active_runs', 'max_consecutive_failed_dag_runs',
'has_task_concurrency_limits', 'has_import_errors', 'next_dagrun_logical_date',
'next_dagrun_data_interval_start', 'next_dagrun_data_interval_end', 'next_da
[...]
title: 'DAGWithLatestDagRunsResponse',
description: 'DAG with latest dag runs response serializer.'
} as const;
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
index 0a719c75122..63e325717c4 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
@@ -562,6 +562,7 @@ export type DAGDetailsResponse = {
owner_links?: {
[key: string]: (string);
} | null;
+ is_favorite?: boolean;
/**
* Return file token.
*/
@@ -1834,6 +1835,7 @@ export type DAGWithLatestDagRunsResponse = {
} | null;
latest_dag_runs: Array<DAGRunResponse>;
pending_actions: Array<HITLDetail>;
+ is_favorite: boolean;
/**
* Return file token.
*/
diff --git
a/airflow-core/src/airflow/ui/src/components/DagActions/FavoriteDagButton.tsx
b/airflow-core/src/airflow/ui/src/components/DagActions/FavoriteDagButton.tsx
index a548d0bf8f5..38024ecddde 100644
---
a/airflow-core/src/airflow/ui/src/components/DagActions/FavoriteDagButton.tsx
+++
b/airflow-core/src/airflow/ui/src/components/DagActions/FavoriteDagButton.tsx
@@ -17,44 +17,39 @@
* under the License.
*/
import { Box } from "@chakra-ui/react";
-import { useCallback, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { FiStar } from "react-icons/fi";
-import { useDagServiceGetDagsUi } from "openapi/queries";
-import { useFavoriteDag } from "src/queries/useFavoriteDag";
-import { useUnfavoriteDag } from "src/queries/useUnfavoriteDag";
+import { useToggleFavoriteDag } from "src/queries/useToggleFavoriteDag";
import ActionButton from "../ui/ActionButton";
type FavoriteDagButtonProps = {
readonly dagId: string;
+ readonly isFavorite?: boolean;
readonly withText?: boolean;
};
-export const FavoriteDagButton = ({ dagId, withText = true }:
FavoriteDagButtonProps) => {
+export const FavoriteDagButton = ({ dagId, isFavorite = false, withText = true
}: FavoriteDagButtonProps) => {
const { t: translate } = useTranslation("dags");
- const { data: favorites } = useDagServiceGetDagsUi({ isFavorite: true });
- const isFavorite = useMemo(
- () => favorites?.dags.some((fav) => fav.dag_id === dagId) ?? false,
- [favorites, dagId],
- );
-
- const { mutate: favoriteDag } = useFavoriteDag();
- const { mutate: unfavoriteDag } = useUnfavoriteDag();
-
- const onToggle = useCallback(() => {
- const mutationFn = isFavorite ? unfavoriteDag : favoriteDag;
+ const { isLoading, toggleFavorite } = useToggleFavoriteDag(dagId);
- mutationFn({ dagId });
- }, [dagId, isFavorite, favoriteDag, unfavoriteDag]);
+ const onToggle = () => toggleFavorite(isFavorite);
return (
<Box>
<ActionButton
actionName={isFavorite ? translate("unfavoriteDag") :
translate("favoriteDag")}
- icon={<FiStar style={{ fill: isFavorite ?
"var(--chakra-colors-brand-solid)" : "none" }} />}
+ icon={
+ <FiStar
+ style={{
+ fill: isFavorite ? "var(--chakra-colors-brand-solid)" : "none",
+ stroke: "var(--chakra-colors-brand-solid)",
+ }}
+ />
+ }
+ loading={isLoading}
onClick={onToggle}
text={isFavorite ? translate("unfavoriteDag") :
translate("favoriteDag")}
withText={withText}
diff --git a/airflow-core/src/airflow/ui/src/mocks/handlers/dags.ts
b/airflow-core/src/airflow/ui/src/mocks/handlers/dags.ts
index 35ba89a46ce..75158b012c4 100644
--- a/airflow-core/src/airflow/ui/src/mocks/handlers/dags.ts
+++ b/airflow-core/src/airflow/ui/src/mocks/handlers/dags.ts
@@ -32,6 +32,7 @@ export const handlers: Array<HttpHandler> = [
fileloc: "/airflow/dags/tutorial_taskflow_api.py",
has_import_errors: false,
has_task_concurrency_limits: false,
+ is_favorite: true,
is_paused: false,
is_stale: false,
last_parsed_time: "2025-01-13T07:34:01.593459Z",
@@ -69,6 +70,7 @@ export const handlers: Array<HttpHandler> = [
fileloc: "/airflow/dags/tutorial_taskflow_api_failed.py",
has_import_errors: false,
has_task_concurrency_limits: false,
+ is_favorite: false,
is_paused: false,
is_stale: false,
last_parsed_time: "2025-01-13T07:34:01.593459Z",
@@ -128,6 +130,7 @@ export const handlers: Array<HttpHandler> = [
fileloc: "/airflow/dags/tutorial_taskflow_api_failed.py",
has_import_errors: false,
has_task_concurrency_limits: false,
+ is_favorite: false,
is_paused: false,
is_stale: false,
last_expired: null,
@@ -155,6 +158,7 @@ export const handlers: Array<HttpHandler> = [
fileloc: "/airflow/dags/tutorial_taskflow_api_success.py",
has_import_errors: false,
has_task_concurrency_limits: false,
+ is_favorite: true,
is_paused: false,
is_stale: false,
last_expired: null,
diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Header.tsx
b/airflow-core/src/airflow/ui/src/pages/Dag/Header.tsx
index 1ca3b25c0de..8d55c0f0c39 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Header.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Header.tsx
@@ -128,7 +128,7 @@ export const Header = ({
text={translate("dag:header.buttons.dagDocs")}
/>
)}
- <FavoriteDagButton dagId={dag.dag_id} withText={true} />
+ <FavoriteDagButton dagId={dag.dag_id} isFavorite={dag.is_favorite}
withText />
<Menu.Root>
<Menu.Trigger asChild>
<Button aria-label={translate("dag:header.buttons.advanced")}
variant="outline">
diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.test.tsx
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.test.tsx
index 319095119bd..4f4438def45 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.test.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.test.tsx
@@ -66,6 +66,7 @@ const mockDag = {
fileloc: "/files/dags/nested_task_groups.py",
has_import_errors: false,
has_task_concurrency_limits: false,
+ is_favorite: false,
is_paused: false,
is_stale: false,
last_expired: null,
diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.tsx
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.tsx
index 3d3ddd902ad..0643bc8cc3f 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.tsx
@@ -72,7 +72,7 @@ export const DagCard = ({ dag }: Props) => {
isPaused={dag.is_paused}
withText={false}
/>
- <FavoriteDagButton dagId={dag.dag_id} withText={false} />
+ <FavoriteDagButton dagId={dag.dag_id} isFavorite={dag.is_favorite}
withText={false} />
<DeleteDagButton dagDisplayName={dag.dag_display_name}
dagId={dag.dag_id} withText={false} />
</HStack>
</Flex>
diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx
index e3f0a01ec87..ef3153876c3 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx
@@ -164,8 +164,9 @@ const createColumns = (
header: "",
},
{
- accessorKey: "favorite",
- cell: ({ row: { original } }) => <FavoriteDagButton
dagId={original.dag_id} withText={false} />,
+ cell: ({ row: { original } }) => (
+ <FavoriteDagButton dagId={original.dag_id}
isFavorite={original.is_favorite} withText={false} />
+ ),
enableHiding: false,
enableSorting: false,
header: "",
diff --git a/airflow-core/src/airflow/ui/src/queries/useFavoriteDag.ts
b/airflow-core/src/airflow/ui/src/queries/useFavoriteDag.ts
deleted file mode 100644
index 3f5f3aee4f4..00000000000
--- a/airflow-core/src/airflow/ui/src/queries/useFavoriteDag.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-/*!
- * 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 { useQueryClient } from "@tanstack/react-query";
-
-import { useDagServiceGetDagsUiKey, useDagServiceFavoriteDag } from
"openapi/queries";
-
-export const useFavoriteDag = () => {
- const queryClient = useQueryClient();
-
- const onSuccess = async () => {
- await queryClient.invalidateQueries({ queryKey:
[useDagServiceGetDagsUiKey] });
- };
-
- return useDagServiceFavoriteDag({
- onSuccess,
- });
-};
diff --git a/airflow-core/src/airflow/ui/src/queries/useToggleFavoriteDag.ts
b/airflow-core/src/airflow/ui/src/queries/useToggleFavoriteDag.ts
new file mode 100644
index 00000000000..d3cfaa7cf0c
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/queries/useToggleFavoriteDag.ts
@@ -0,0 +1,66 @@
+/*!
+ * 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 { useQueryClient } from "@tanstack/react-query";
+import { useCallback } from "react";
+
+import {
+ useDagServiceGetDagsUiKey,
+ useDagServiceFavoriteDag,
+ useDagServiceUnfavoriteDag,
+ UseDagServiceGetDagDetailsKeyFn,
+} from "openapi/queries";
+
+export const useToggleFavoriteDag = (dagId: string) => {
+ const queryClient = useQueryClient();
+
+ const onSuccess = useCallback(async () => {
+ // Invalidate the DAGs list query
+ await queryClient.invalidateQueries({
+ queryKey: [useDagServiceGetDagsUiKey, UseDagServiceGetDagDetailsKeyFn({
dagId }, [{ dagId }])],
+ });
+
+ // Invalidate the specific DAG details query for this DAG
+ await queryClient.invalidateQueries({
+ queryKey: UseDagServiceGetDagDetailsKeyFn({ dagId }, [{ dagId }]),
+ });
+ }, [queryClient, dagId]);
+
+ const favoriteMutation = useDagServiceFavoriteDag({
+ onSuccess,
+ });
+
+ const unfavoriteMutation = useDagServiceUnfavoriteDag({
+ onSuccess,
+ });
+
+ const toggleFavorite = useCallback(
+ (isFavorite: boolean) => {
+ const mutation = isFavorite ? unfavoriteMutation : favoriteMutation;
+
+ mutation.mutate({ dagId });
+ },
+ [dagId, favoriteMutation, unfavoriteMutation],
+ );
+
+ return {
+ error: favoriteMutation.error ?? unfavoriteMutation.error,
+ isLoading: favoriteMutation.isPending || unfavoriteMutation.isPending,
+ toggleFavorite,
+ };
+};
diff --git a/airflow-core/src/airflow/ui/src/queries/useUnfavoriteDag.ts
b/airflow-core/src/airflow/ui/src/queries/useUnfavoriteDag.ts
deleted file mode 100644
index 008e7dbb8e3..00000000000
--- a/airflow-core/src/airflow/ui/src/queries/useUnfavoriteDag.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-/*!
- * 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 { useQueryClient } from "@tanstack/react-query";
-
-import { useDagServiceGetDagsUiKey, useDagServiceUnfavoriteDag } from
"openapi/queries";
-
-export const useUnfavoriteDag = () => {
- const queryClient = useQueryClient();
-
- const onSuccess = async () => {
- await queryClient.invalidateQueries({ queryKey:
[useDagServiceGetDagsUiKey] });
- };
-
- return useDagServiceUnfavoriteDag({
- onSuccess,
- });
-};
diff --git
a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dags.py
b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dags.py
index 564a4b27348..a81eacd3fc4 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dags.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/public/test_dags.py
@@ -483,6 +483,21 @@ class TestGetDags(TestDagEndpoint):
assert body["total_entries"] == expected_total_entries
assert sorted([dag["dag_id"] for dag in body["dags"]]) ==
sorted(expected_ids)
+ def test_get_dags_filter_non_favorites(self, session, test_client):
+ """Test filtering DAGs by is_favorite=false."""
+ # Mark DAG1 as favorite
+ session.add(DagFavorite(user_id="test", dag_id=DAG1_ID))
+ session.commit()
+
+ response = test_client.get("/dags", params={"is_favorite": False})
+
+ assert response.status_code == 200
+ body = response.json()
+
+ # Should return only non-favorite DAGs (DAG2)
+ assert body["total_entries"] == 1
+ assert [dag["dag_id"] for dag in body["dags"]] == [DAG2_ID]
+
def test_get_dags_should_response_401(self, unauthenticated_test_client):
response = unauthenticated_test_client.get("/dags")
assert response.status_code == 401
@@ -862,6 +877,7 @@ class TestDagDetails(TestDagEndpoint):
"template_search_path": None,
"timetable_description": "Never, external triggers only",
"timezone": UTC_JSON_REPR,
+ "is_favorite": False,
}
assert res_json == expected
@@ -957,6 +973,7 @@ class TestDagDetails(TestDagEndpoint):
"template_search_path": None,
"timetable_description": "Never, external triggers only",
"timezone": UTC_JSON_REPR,
+ "is_favorite": False,
}
assert res_json == expected
@@ -968,6 +985,30 @@ class TestDagDetails(TestDagEndpoint):
response = unauthorized_test_client.get(f"/dags/{DAG1_ID}/details")
assert response.status_code == 403
+ def test_dag_details_includes_is_favorite_field(self, session,
test_client):
+ """Test that DAG details include the is_favorite field."""
+ # Mark DAG2 as favorite
+ session.add(DagFavorite(user_id="test", dag_id=DAG2_ID))
+ session.commit()
+
+ response = test_client.get(f"/dags/{DAG2_ID}/details")
+ assert response.status_code == 200
+ body = response.json()
+
+ # Verify is_favorite field is present and correct
+ assert "is_favorite" in body
+ assert isinstance(body["is_favorite"], bool)
+ assert body["is_favorite"] is True
+
+ # Test with non-favorite DAG
+ response = test_client.get(f"/dags/{DAG1_ID}/details")
+ assert response.status_code == 200
+ body = response.json()
+
+ assert "is_favorite" in body
+ assert isinstance(body["is_favorite"], bool)
+ assert body["is_favorite"] is False
+
class TestGetDag(TestDagEndpoint):
"""Unit tests for Get DAG."""
diff --git
a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_dags.py
b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_dags.py
index 5ebf2221d92..d27cd3fb08d 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_dags.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_dags.py
@@ -26,6 +26,7 @@ from fastapi.testclient import TestClient
from sqlalchemy.orm import Session
from airflow.models import DagRun
+from airflow.models.dag_favorite import DagFavorite
from airflow.models.hitl import HITLDetail
from airflow.sdk.timezone import utcnow
from airflow.utils.session import provide_session
@@ -278,3 +279,38 @@ class TestGetDagRuns(TestPublicDagEndpoint):
body = response.json()
assert body["total_entries"] == expected_dag_count
assert len(body["dags"]) == expected_dag_count
+
+ def test_is_favorite_field_with_multiple_favorites(self, test_client,
session):
+ """Test is_favorite field with multiple DAGs marked as favorites."""
+ # Mark both DAG1 and DAG2 as favorites
+ session.add(DagFavorite(user_id="test", dag_id=DAG1_ID))
+ session.add(DagFavorite(user_id="test", dag_id=DAG2_ID))
+ session.commit()
+
+ response = test_client.get("/dags")
+ assert response.status_code == 200
+ body = response.json()
+
+ # Count favorites in response
+ favorite_count = sum(1 for dag in body["dags"] if dag["is_favorite"])
+ assert favorite_count == 2
+
+ # Verify specific DAGs are marked as favorites
+ dag_favorites = {dag["dag_id"]: dag["is_favorite"] for dag in
body["dags"]}
+ assert dag_favorites[DAG1_ID] is True
+ assert dag_favorites[DAG2_ID] is True
+
+ def test_is_favorite_field_user_specific(self, test_client, session):
+ """Test that is_favorite field is user-specific."""
+ # Mark DAG1 as favorite for a different user
+ session.add(DagFavorite(user_id="other_user", dag_id=DAG1_ID))
+ session.commit()
+
+ # Request as the test user (not other_user)
+ response = test_client.get("/dags")
+ assert response.status_code == 200
+ body = response.json()
+
+ # Verify that DAG1 is not marked as favorite for the test user
+ dag1_data = next(dag for dag in body["dags"] if dag["dag_id"] ==
DAG1_ID)
+ assert dag1_data["is_favorite"] is False
diff --git a/airflow-ctl/src/airflowctl/api/datamodels/generated.py
b/airflow-ctl/src/airflowctl/api/datamodels/generated.py
index dc56b58aa5a..c965a5139a6 100644
--- a/airflow-ctl/src/airflowctl/api/datamodels/generated.py
+++ b/airflow-ctl/src/airflowctl/api/datamodels/generated.py
@@ -1302,6 +1302,7 @@ class DAGDetailsResponse(BaseModel):
last_parsed: Annotated[datetime | None, Field(title="Last Parsed")] = None
default_args: Annotated[dict[str, Any] | None, Field(title="Default
Args")] = None
owner_links: Annotated[dict[str, str] | None, Field(title="Owner Links")]
= None
+ is_favorite: Annotated[bool | None, Field(title="Is Favorite")] = False
file_token: Annotated[str, Field(description="Return file token.",
title="File Token")]
concurrency: Annotated[
int,