This is an automated email from the ASF dual-hosted git repository.
kaxilnaik pushed a commit to branch v3-0-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/v3-0-test by this push:
new dbb6c5df74b feat: add `stats` ui endpoint (#49985)
dbb6c5df74b is described below
commit dbb6c5df74b3e4cdb80b8a3dcf35c86677d525ef
Author: Guan Ming(Wesley) Chiu <[email protected]>
AuthorDate: Wed Apr 30 21:37:50 2025 +0800
feat: add `stats` ui endpoint (#49985)
* feat: add `stats` ui endpoint
* fix: cross db sql
(cherry picked from commit e216dd640a30873242fef5c59dce9951d629bf0f)
---
.../core_api/datamodels/ui/dashboard.py | 9 ++
.../api_fastapi/core_api/openapi/_private_ui.yaml | 38 +++++
.../api_fastapi/core_api/routes/ui/dashboard.py | 54 ++++++-
.../src/airflow/ui/openapi-gen/queries/common.ts | 10 ++
.../ui/openapi-gen/queries/ensureQueryData.ts | 11 ++
.../src/airflow/ui/openapi-gen/queries/prefetch.ts | 11 ++
.../src/airflow/ui/openapi-gen/queries/queries.ts | 19 +++
.../src/airflow/ui/openapi-gen/queries/suspense.ts | 19 +++
.../airflow/ui/openapi-gen/requests/schemas.gen.ts | 25 +++
.../ui/openapi-gen/requests/services.gen.ts | 14 ++
.../airflow/ui/openapi-gen/requests/types.gen.ts | 22 +++
.../core_api/routes/ui/test_dashboard.py | 167 +++++++++++++++++++++
12 files changed, 398 insertions(+), 1 deletion(-)
diff --git
a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/dashboard.py
b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/dashboard.py
index ad806858828..bf2afa9fcd3 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/dashboard.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/dashboard.py
@@ -61,3 +61,12 @@ class HistoricalMetricDataResponse(BaseModel):
dag_run_types: DAGRunTypes
dag_run_states: DAGRunStates
task_instance_states: TaskInstanceStateCount
+
+
+class DashboardDagStatsResponse(BaseModel):
+ """Dashboard DAG Stats serializer for responses."""
+
+ active_dag_count: int
+ failed_dag_count: int
+ running_dag_count: int
+ queued_dag_count: int
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 f3237ddbe1f..5eead52583d 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
@@ -296,6 +296,22 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
+ /ui/dashboard/dag_stats:
+ get:
+ tags:
+ - Dashboard
+ summary: Dag Stats
+ description: Return basic DAG stats with counts of DAGs in various
states.
+ operationId: dag_stats
+ responses:
+ '200':
+ description: Successful Response
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/DashboardDagStatsResponse'
+ security:
+ - OAuth2PasswordBearer: []
/ui/structure/structure_data:
get:
tags:
@@ -1269,6 +1285,28 @@ components:
- bundle_url
title: DagVersionResponse
description: Dag Version serializer for responses.
+ DashboardDagStatsResponse:
+ properties:
+ active_dag_count:
+ type: integer
+ title: Active Dag Count
+ failed_dag_count:
+ type: integer
+ title: Failed Dag Count
+ running_dag_count:
+ type: integer
+ title: Running Dag Count
+ queued_dag_count:
+ type: integer
+ title: Queued Dag Count
+ type: object
+ required:
+ - active_dag_count
+ - failed_dag_count
+ - running_dag_count
+ - queued_dag_count
+ title: DashboardDagStatsResponse
+ description: Dashboard DAG Stats serializer for responses.
EdgeResponse:
properties:
source_id:
diff --git
a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/dashboard.py
b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/dashboard.py
index e7f6d42c9d4..1e8ff2e4f03 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/dashboard.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/dashboard.py
@@ -18,14 +18,19 @@ from __future__ import annotations
from fastapi import Depends, status
from sqlalchemy import func, select
+from sqlalchemy.sql.expression import case, false
from airflow.api_fastapi.auth.managers.models.resource_details import
DagAccessEntity
from airflow.api_fastapi.common.db.common import SessionDep
from airflow.api_fastapi.common.parameters import DateTimeQuery,
OptionalDateTimeQuery
from airflow.api_fastapi.common.router import AirflowRouter
-from airflow.api_fastapi.core_api.datamodels.ui.dashboard import
HistoricalMetricDataResponse
+from airflow.api_fastapi.core_api.datamodels.ui.dashboard import (
+ DashboardDagStatsResponse,
+ HistoricalMetricDataResponse,
+)
from airflow.api_fastapi.core_api.openapi.exceptions import
create_openapi_http_exception_doc
from airflow.api_fastapi.core_api.security import requires_access_dag
+from airflow.models.dag import DagModel
from airflow.models.dagrun import DagRun, DagRunType
from airflow.models.taskinstance import TaskInstance
from airflow.utils import timezone
@@ -97,3 +102,50 @@ def historical_metrics(
}
return
HistoricalMetricDataResponse.model_validate(historical_metrics_response)
+
+
+@dashboard_router.get(
+ "/dag_stats",
+ dependencies=[Depends(requires_access_dag(method="GET"))],
+)
+def dag_stats(
+ session: SessionDep,
+) -> DashboardDagStatsResponse:
+ """Return basic DAG stats with counts of DAGs in various states."""
+ latest_dates_subq = (
+ select(DagRun.dag_id,
func.max(DagRun.logical_date).label("max_logical_date"))
+ .where(DagRun.logical_date.is_not(None))
+ .group_by(DagRun.dag_id)
+ .subquery()
+ )
+
+ latest_runs = (
+ select(
+ DagModel.dag_id,
+ DagModel.is_paused,
+ DagRun.state,
+ )
+ .join(DagModel, DagRun.dag_id == DagModel.dag_id)
+ .join(
+ latest_dates_subq,
+ (DagRun.dag_id == latest_dates_subq.c.dag_id)
+ & (DagRun.logical_date == latest_dates_subq.c.max_logical_date),
+ )
+ .cte()
+ )
+
+ combined_query = select(
+ func.coalesce(func.sum(case((latest_runs.c.is_paused == false(), 1))),
0).label("active"),
+ func.coalesce(func.sum(case((latest_runs.c.state ==
DagRunState.FAILED, 1))), 0).label("failed"),
+ func.coalesce(func.sum(case((latest_runs.c.state ==
DagRunState.RUNNING, 1))), 0).label("running"),
+ func.coalesce(func.sum(case((latest_runs.c.state ==
DagRunState.QUEUED, 1))), 0).label("queued"),
+ ).select_from(latest_runs)
+
+ counts = session.execute(combined_query).first()
+
+ return DashboardDagStatsResponse(
+ active_dag_count=counts.active,
+ failed_dag_count=counts.failed,
+ running_dag_count=counts.running,
+ queued_dag_count=counts.queued,
+ )
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
b/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
index 378e696b229..a59e98c888b 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/common.ts
@@ -1711,6 +1711,16 @@ export const UseDashboardServiceHistoricalMetricsKeyFn =
(
},
queryKey?: Array<unknown>,
) => [useDashboardServiceHistoricalMetricsKey, ...(queryKey ?? [{ endDate,
startDate }])];
+export type DashboardServiceDagStatsDefaultResponse =
Awaited<ReturnType<typeof DashboardService.dagStats>>;
+export type DashboardServiceDagStatsQueryResult<
+ TData = DashboardServiceDagStatsDefaultResponse,
+ TError = unknown,
+> = UseQueryResult<TData, TError>;
+export const useDashboardServiceDagStatsKey = "DashboardServiceDagStats";
+export const UseDashboardServiceDagStatsKeyFn = (queryKey?: Array<unknown>) =>
[
+ useDashboardServiceDagStatsKey,
+ ...(queryKey ?? []),
+];
export type StructureServiceStructureDataDefaultResponse = Awaited<
ReturnType<typeof StructureService.structureData>
>;
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
index cd3a7cfa9ed..c97c8ac5c66 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/ensureQueryData.ts
@@ -2381,6 +2381,17 @@ export const
ensureUseDashboardServiceHistoricalMetricsData = (
queryKey: Common.UseDashboardServiceHistoricalMetricsKeyFn({ endDate,
startDate }),
queryFn: () => DashboardService.historicalMetrics({ endDate, startDate }),
});
+/**
+ * Dag Stats
+ * Return basic DAG stats with counts of DAGs in various states.
+ * @returns DashboardDagStatsResponse Successful Response
+ * @throws ApiError
+ */
+export const ensureUseDashboardServiceDagStatsData = (queryClient:
QueryClient) =>
+ queryClient.ensureQueryData({
+ queryKey: Common.UseDashboardServiceDagStatsKeyFn(),
+ queryFn: () => DashboardService.dagStats(),
+ });
/**
* Structure Data
* Get Structure Data.
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
index d3f010ceb9b..c9f715befa4 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/prefetch.ts
@@ -2381,6 +2381,17 @@ export const
prefetchUseDashboardServiceHistoricalMetrics = (
queryKey: Common.UseDashboardServiceHistoricalMetricsKeyFn({ endDate,
startDate }),
queryFn: () => DashboardService.historicalMetrics({ endDate, startDate }),
});
+/**
+ * Dag Stats
+ * Return basic DAG stats with counts of DAGs in various states.
+ * @returns DashboardDagStatsResponse Successful Response
+ * @throws ApiError
+ */
+export const prefetchUseDashboardServiceDagStats = (queryClient: QueryClient)
=>
+ queryClient.prefetchQuery({
+ queryKey: Common.UseDashboardServiceDagStatsKeyFn(),
+ queryFn: () => DashboardService.dagStats(),
+ });
/**
* Structure Data
* Get Structure Data.
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
index 0245b93dcb2..ae5dabdcd0d 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/queries.ts
@@ -2849,6 +2849,25 @@ export const useDashboardServiceHistoricalMetrics = <
queryFn: () => DashboardService.historicalMetrics({ endDate, startDate })
as TData,
...options,
});
+/**
+ * Dag Stats
+ * Return basic DAG stats with counts of DAGs in various states.
+ * @returns DashboardDagStatsResponse Successful Response
+ * @throws ApiError
+ */
+export const useDashboardServiceDagStats = <
+ TData = Common.DashboardServiceDagStatsDefaultResponse,
+ TError = unknown,
+ TQueryKey extends Array<unknown> = unknown[],
+>(
+ queryKey?: TQueryKey,
+ options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">,
+) =>
+ useQuery<TData, TError>({
+ queryKey: Common.UseDashboardServiceDagStatsKeyFn(queryKey),
+ queryFn: () => DashboardService.dagStats() as TData,
+ ...options,
+ });
/**
* Structure Data
* Get Structure Data.
diff --git a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
index ba4593ab925..00cd46c9d0e 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/queries/suspense.ts
@@ -2826,6 +2826,25 @@ export const
useDashboardServiceHistoricalMetricsSuspense = <
queryFn: () => DashboardService.historicalMetrics({ endDate, startDate })
as TData,
...options,
});
+/**
+ * Dag Stats
+ * Return basic DAG stats with counts of DAGs in various states.
+ * @returns DashboardDagStatsResponse Successful Response
+ * @throws ApiError
+ */
+export const useDashboardServiceDagStatsSuspense = <
+ TData = Common.DashboardServiceDagStatsDefaultResponse,
+ TError = unknown,
+ TQueryKey extends Array<unknown> = unknown[],
+>(
+ queryKey?: TQueryKey,
+ options?: Omit<UseQueryOptions<TData, TError>, "queryKey" | "queryFn">,
+) =>
+ useSuspenseQuery<TData, TError>({
+ queryKey: Common.UseDashboardServiceDagStatsKeyFn(queryKey),
+ queryFn: () => DashboardService.dagStats() as TData,
+ ...options,
+ });
/**
* Structure Data
* Get Structure Data.
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 481a5142df7..eb634bb0779 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
@@ -6315,6 +6315,31 @@ export const $DAGWithLatestDagRunsResponse = {
description: "DAG with latest dag runs response serializer.",
} as const;
+export const $DashboardDagStatsResponse = {
+ properties: {
+ active_dag_count: {
+ type: "integer",
+ title: "Active Dag Count",
+ },
+ failed_dag_count: {
+ type: "integer",
+ title: "Failed Dag Count",
+ },
+ running_dag_count: {
+ type: "integer",
+ title: "Running Dag Count",
+ },
+ queued_dag_count: {
+ type: "integer",
+ title: "Queued Dag Count",
+ },
+ },
+ type: "object",
+ required: ["active_dag_count", "failed_dag_count", "running_dag_count",
"queued_dag_count"],
+ title: "DashboardDagStatsResponse",
+ description: "Dashboard DAG Stats serializer for responses.",
+} as const;
+
export const $EdgeResponse = {
properties: {
source_id: {
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts
b/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts
index 385f8256e7f..bfd0077783a 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/services.gen.ts
@@ -211,6 +211,7 @@ import type {
GetDependenciesResponse,
HistoricalMetricsData,
HistoricalMetricsResponse,
+ DagStatsResponse2,
StructureDataData,
StructureDataResponse2,
GridDataData,
@@ -3499,6 +3500,19 @@ export class DashboardService {
},
});
}
+
+ /**
+ * Dag Stats
+ * Return basic DAG stats with counts of DAGs in various states.
+ * @returns DashboardDagStatsResponse Successful Response
+ * @throws ApiError
+ */
+ public static dagStats(): CancelablePromise<DagStatsResponse2> {
+ return __request(OpenAPI, {
+ method: "GET",
+ url: "/ui/dashboard/dag_stats",
+ });
+ }
}
export class StructureService {
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 8b626f7ffed..a2370b2cc77 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
@@ -1564,6 +1564,16 @@ export type DAGWithLatestDagRunsResponse = {
readonly file_token: string;
};
+/**
+ * Dashboard DAG Stats serializer for responses.
+ */
+export type DashboardDagStatsResponse = {
+ active_dag_count: number;
+ failed_dag_count: number;
+ running_dag_count: number;
+ queued_dag_count: number;
+};
+
/**
* Edge serializer for responses.
*/
@@ -2633,6 +2643,8 @@ export type HistoricalMetricsData = {
export type HistoricalMetricsResponse = HistoricalMetricDataResponse;
+export type DagStatsResponse2 = DashboardDagStatsResponse;
+
export type StructureDataData = {
dagId: string;
externalDependencies?: boolean;
@@ -5446,6 +5458,16 @@ export type $OpenApiTs = {
};
};
};
+ "/ui/dashboard/dag_stats": {
+ get: {
+ res: {
+ /**
+ * Successful Response
+ */
+ 200: DashboardDagStatsResponse;
+ };
+ };
+ };
"/ui/structure/structure_data": {
get: {
req: StructureDataData;
diff --git
a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_dashboard.py
b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_dashboard.py
index 2dd60058c2e..aab0dca59a7 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_dashboard.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_dashboard.py
@@ -99,6 +99,108 @@ def make_dag_runs(dag_maker, session, time_machine):
time_machine.move_to("2023-07-02T00:00:00+00:00", tick=False)
[email protected]
+def make_failed_dag_runs(dag_maker, session):
+ with dag_maker(
+ dag_id="test_failed_dag",
+ serialized=True,
+ session=session,
+ start_date=pendulum.DateTime(2023, 2, 1, 0, 0, 0, tzinfo=pendulum.UTC),
+ ):
+ EmptyOperator(task_id="task_1") >> EmptyOperator(task_id="task_2")
+
+ date = dag_maker.dag.start_date
+
+ dag_maker.create_dagrun(
+ run_id="run_1",
+ state=DagRunState.FAILED,
+ run_type=DagRunType.SCHEDULED,
+ logical_date=date,
+ start_date=date,
+ )
+
+ dag_maker.sync_dagbag_to_db()
+
+
[email protected]
+def make_queued_dag_runs(dag_maker, session):
+ with dag_maker(
+ dag_id="test_queued_dag",
+ serialized=True,
+ session=session,
+ start_date=pendulum.DateTime(2023, 2, 1, 0, 0, 0, tzinfo=pendulum.UTC),
+ ):
+ EmptyOperator(task_id="task_1") >> EmptyOperator(task_id="task_2")
+
+ date = dag_maker.dag.start_date
+
+ dag_maker.create_dagrun(
+ run_id="run_1",
+ state=DagRunState.QUEUED,
+ run_type=DagRunType.SCHEDULED,
+ logical_date=date,
+ start_date=date,
+ )
+
+ dag_maker.sync_dagbag_to_db()
+
+
[email protected]
+def make_multiple_dags(dag_maker, session):
+ with dag_maker(
+ dag_id="test_running_dag",
+ serialized=True,
+ session=session,
+ start_date=pendulum.DateTime(2023, 2, 1, 0, 0, 0, tzinfo=pendulum.UTC),
+ ):
+ EmptyOperator(task_id="task_1") >> EmptyOperator(task_id="task_2")
+
+ date = dag_maker.dag.start_date
+ dag_maker.create_dagrun(
+ run_id="run_1",
+ state=DagRunState.RUNNING,
+ run_type=DagRunType.SCHEDULED,
+ logical_date=date,
+ start_date=date,
+ )
+
+ with dag_maker(
+ dag_id="test_failed_dag",
+ serialized=True,
+ session=session,
+ start_date=pendulum.DateTime(2023, 2, 1, 0, 0, 0, tzinfo=pendulum.UTC),
+ ):
+ EmptyOperator(task_id="task_1") >> EmptyOperator(task_id="task_2")
+
+ date = dag_maker.dag.start_date
+ dag_maker.create_dagrun(
+ run_id="run_1",
+ state=DagRunState.FAILED,
+ run_type=DagRunType.SCHEDULED,
+ logical_date=date,
+ start_date=date,
+ )
+
+ with dag_maker(
+ dag_id="test_queued_dag",
+ serialized=True,
+ session=session,
+ start_date=pendulum.DateTime(2023, 2, 1, 0, 0, 0, tzinfo=pendulum.UTC),
+ ):
+ EmptyOperator(task_id="task_1") >> EmptyOperator(task_id="task_2")
+
+ date = dag_maker.dag.start_date
+ dag_maker.create_dagrun(
+ run_id="run_1",
+ state=DagRunState.QUEUED,
+ run_type=DagRunType.SCHEDULED,
+ logical_date=date,
+ start_date=date,
+ )
+
+ dag_maker.sync_dagbag_to_db()
+
+
class TestHistoricalMetricsDataEndpoint:
@pytest.mark.parametrize(
"params, expected",
@@ -188,3 +290,68 @@ class TestHistoricalMetricsDataEndpoint:
"/dashboard/historical_metrics_data", params={"start_date":
"2023-02-02T00:00"}
)
assert response.status_code == 403
+
+
+class TestDagStatsEndpoint:
+ @pytest.mark.usefixtures("freeze_time_for_dagruns", "make_multiple_dags")
+ def test_should_response_200_multiple_dags(self, test_client):
+ response = test_client.get("/dashboard/dag_stats")
+ assert response.status_code == 200
+ assert response.json() == {
+ "active_dag_count": 3,
+ "failed_dag_count": 1,
+ "running_dag_count": 1,
+ "queued_dag_count": 1,
+ }
+
+ @pytest.mark.usefixtures("freeze_time_for_dagruns", "make_dag_runs")
+ def test_should_response_200_single_dag(self, test_client):
+ response = test_client.get("/dashboard/dag_stats")
+ assert response.status_code == 200
+ assert response.json() == {
+ "active_dag_count": 1,
+ "failed_dag_count": 0,
+ "running_dag_count": 1,
+ "queued_dag_count": 0,
+ }
+
+ @pytest.mark.usefixtures("freeze_time_for_dagruns", "make_failed_dag_runs")
+ def test_should_response_200_failed_dag(self, test_client):
+ response = test_client.get("/dashboard/dag_stats")
+ assert response.status_code == 200
+ assert response.json() == {
+ "active_dag_count": 1,
+ "failed_dag_count": 1,
+ "running_dag_count": 0,
+ "queued_dag_count": 0,
+ }
+
+ @pytest.mark.usefixtures("freeze_time_for_dagruns", "make_queued_dag_runs")
+ def test_should_response_200_queued_dag(self, test_client):
+ response = test_client.get("/dashboard/dag_stats")
+ assert response.status_code == 200
+ assert response.json() == {
+ "active_dag_count": 1,
+ "failed_dag_count": 0,
+ "running_dag_count": 0,
+ "queued_dag_count": 1,
+ }
+
+ @pytest.mark.usefixtures("freeze_time_for_dagruns")
+ def test_should_response_200_no_dag_runs(self, test_client):
+ response = test_client.get("/dashboard/dag_stats")
+ assert response.status_code == 200
+ assert response.json() == {
+ "active_dag_count": 0,
+ "failed_dag_count": 0,
+ "running_dag_count": 0,
+ "queued_dag_count": 0,
+ }
+
+ def test_should_response_401(self, unauthenticated_test_client):
+ response = unauthenticated_test_client.get("/dashboard/dag_stats")
+ assert response.status_code == 401
+
+ def test_should_response_403(self, unauthorized_test_client):
+ response = unauthorized_test_client.get("/dashboard/dag_stats")
+ assert response.status_code == 403