From e8731bb5fc66cbfb650b418ae8618819583bba69 Mon Sep 17 00:00:00 2001 From: GUAN MING Date: Fri, 22 Aug 2025 12:10:49 +0800 Subject: [PATCH 1/2] Add utility function for generic filter counting --- .../DagsList/DagsFilters/DagsFilters.tsx | 27 +--- .../ui/src/pages/Events/EventsFilters.tsx | 3 +- .../ui/src/pages/Events/filterUtils.ts | 73 --------- .../airflow/ui/src/utils/filterUtils.test.ts | 138 ++++++++++++++++++ .../src/airflow/ui/src/utils/filterUtils.ts | 39 +++++ 5 files changed, 179 insertions(+), 101 deletions(-) delete mode 100644 airflow-core/src/airflow/ui/src/pages/Events/filterUtils.ts create mode 100644 airflow-core/src/airflow/ui/src/utils/filterUtils.test.ts create mode 100644 airflow-core/src/airflow/ui/src/utils/filterUtils.ts diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.tsx b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.tsx index f4e6656a1b448..a218285ba4a7c 100644 --- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.tsx +++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.tsx @@ -26,6 +26,7 @@ import { ResetButton } from "src/components/ui"; import { SearchParamsKeys, type SearchParamsKeysType } from "src/constants/searchParams"; import { useConfig } from "src/queries/useConfig"; import { useDagTagsInfinite } from "src/queries/useDagTagsInfinite"; +import { getFilterCount } from "src/utils/filterUtils"; import { FavoriteFilter } from "./FavoriteFilter"; import { PausedFilter } from "./PausedFilter"; @@ -41,32 +42,6 @@ const { TAGS_MATCH_MODE: TAGS_MATCH_MODE_PARAM, }: SearchParamsKeysType = SearchParamsKeys; -type FilterOptions = { - selectedTags: Array; - showFavorites: string | null; - showPaused: string | null; - state: string | null; -}; - -const getFilterCount = ({ selectedTags, showFavorites, showPaused, state }: FilterOptions) => { - let count = 0; - - if (state !== null) { - count += 1; - } - if (showPaused !== null) { - count += 1; - } - if (selectedTags.length > 0) { - count += 1; - } - if (showFavorites !== null) { - count += 1; - } - - return count; -}; - export const DagsFilters = () => { const [searchParams, setSearchParams] = useSearchParams(); diff --git a/airflow-core/src/airflow/ui/src/pages/Events/EventsFilters.tsx b/airflow-core/src/airflow/ui/src/pages/Events/EventsFilters.tsx index 00a4b25c63c1c..75af3d4bc3674 100644 --- a/airflow-core/src/airflow/ui/src/pages/Events/EventsFilters.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Events/EventsFilters.tsx @@ -26,8 +26,7 @@ import { DateTimeInput } from "src/components/DateTimeInput"; import { SearchBar } from "src/components/SearchBar"; import { ResetButton } from "src/components/ui"; import { SearchParamsKeys } from "src/constants/searchParams"; - -import { getFilterCount } from "./filterUtils"; +import { getFilterCount } from "src/utils/filterUtils"; const { AFTER: AFTER_PARAM, diff --git a/airflow-core/src/airflow/ui/src/pages/Events/filterUtils.ts b/airflow-core/src/airflow/ui/src/pages/Events/filterUtils.ts deleted file mode 100644 index db448155b1457..0000000000000 --- a/airflow-core/src/airflow/ui/src/pages/Events/filterUtils.ts +++ /dev/null @@ -1,73 +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. - */ -export type FilterOptions = { - after: string | null; - before: string | null; - dagId: string | null; - eventType: string | null; - mapIndex: string | null; - runId: string | null; - taskId: string | null; - tryNumber: string | null; - user: string | null; -}; - -export const getFilterCount = ({ - after, - before, - dagId, - eventType, - mapIndex, - runId, - taskId, - tryNumber, - user, -}: FilterOptions) => { - let count = 0; - - if (after !== null) { - count += 1; - } - if (before !== null) { - count += 1; - } - if (dagId !== null) { - count += 1; - } - if (eventType !== null) { - count += 1; - } - if (mapIndex !== null) { - count += 1; - } - if (runId !== null) { - count += 1; - } - if (taskId !== null) { - count += 1; - } - if (tryNumber !== null) { - count += 1; - } - if (user !== null) { - count += 1; - } - - return count; -}; diff --git a/airflow-core/src/airflow/ui/src/utils/filterUtils.test.ts b/airflow-core/src/airflow/ui/src/utils/filterUtils.test.ts new file mode 100644 index 0000000000000..7fed600d09186 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/utils/filterUtils.test.ts @@ -0,0 +1,138 @@ +/*! + * 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 { describe, it, expect } from "vitest"; + +import { getFilterCount } from "./filterUtils"; + +describe("getFilterCount", () => { + it("counts non-undefined values correctly", () => { + const filters = { + count: 5, + enabled: true, + name: "test", + nullValue: undefined, + status: "active", + undefinedValue: undefined, + }; + + expect(getFilterCount(filters)).toBe(4); + }); + + it("handles array fields correctly", () => { + const filters = { + emptyTags: [], + nullValue: undefined, + status: "active", + tags: ["tag1", "tag2"], + }; + + expect(getFilterCount(filters)).toBe(2); + }); + + it("excludes specified fields", () => { + const filters = { + debug: "also-excluded", + internal: "should-be-excluded", + name: "test", + nullValue: undefined, + status: "active", + }; + + expect(getFilterCount(filters, { excludeFields: ["internal", "debug"] })).toBe(2); + }); + + it("handles array fields and excludes together", () => { + const filters = { + emptyTags: [], + internal: "excluded", + nullValue: undefined, + status: "active", + tags: ["tag1", "tag2"], + }; + + expect( + getFilterCount(filters, { + excludeFields: ["internal"], + }), + ).toBe(2); + }); + + it("returns 0 for empty filters", () => { + expect(getFilterCount({})).toBe(0); + }); + + it("returns 0 when all values are undefined/undefined", () => { + const filters = { + nullValue: undefined, + undefinedValue: undefined, + }; + + expect(getFilterCount(filters)).toBe(0); + }); + + it("handles boolean values correctly", () => { + const filters = { + disabled: false, + enabled: true, + nullValue: undefined, + }; + + expect(getFilterCount(filters)).toBe(2); + }); + + it("handles zero values correctly", () => { + const filters = { + amount: 0.0, + count: 0, + nullValue: undefined, + }; + + expect(getFilterCount(filters)).toBe(2); + }); + + it("handles empty strings correctly", () => { + const filters = { + emptyString: "", + nonEmptyString: "value", + nullValue: undefined, + }; + + expect(getFilterCount(filters)).toBe(2); + }); +}); + +it("handles complex filters pattern", () => { + const filters = { + after: "2024-01-01", + before: ["tag1", "tag2"], + dagId: "test-dag", + eventType: "event", + mapIndex: undefined, + runId: "test-run", + taskId: undefined, + tryNumber: undefined, + user: "admin", + }; + + expect( + getFilterCount(filters, { + excludeFields: ["eventType", "mapIndex"], + }), + ).toBe(5); +}); diff --git a/airflow-core/src/airflow/ui/src/utils/filterUtils.ts b/airflow-core/src/airflow/ui/src/utils/filterUtils.ts new file mode 100644 index 0000000000000..d902c2a1d558c --- /dev/null +++ b/airflow-core/src/airflow/ui/src/utils/filterUtils.ts @@ -0,0 +1,39 @@ +/*! + * 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 const getFilterCount = >( + filters: T, + options?: { + excludeFields?: Array; + }, +): number => { + const { excludeFields = [] } = options ?? {}; + + return Object.entries(filters).reduce((count, [key, value]) => { + if (excludeFields.includes(key)) { + return count; + } + + if (Array.isArray(value)) { + return count + (value.length > 0 ? 1 : 0); + } + + return count + (value !== null && value !== undefined ? 1 : 0); + }, 0); +}; From 6f728e231780a5113c9a0cc4ad0a8cbb5220365e Mon Sep 17 00:00:00 2001 From: GUAN MING Date: Fri, 22 Aug 2025 22:22:36 +0800 Subject: [PATCH 2/2] Remove excludeFields from getFilterCount --- .../airflow/ui/src/utils/filterUtils.test.ts | 34 +------------------ .../src/airflow/ui/src/utils/filterUtils.ts | 16 ++------- 2 files changed, 3 insertions(+), 47 deletions(-) diff --git a/airflow-core/src/airflow/ui/src/utils/filterUtils.test.ts b/airflow-core/src/airflow/ui/src/utils/filterUtils.test.ts index 7fed600d09186..f11b2ee6efa52 100644 --- a/airflow-core/src/airflow/ui/src/utils/filterUtils.test.ts +++ b/airflow-core/src/airflow/ui/src/utils/filterUtils.test.ts @@ -45,34 +45,6 @@ describe("getFilterCount", () => { expect(getFilterCount(filters)).toBe(2); }); - it("excludes specified fields", () => { - const filters = { - debug: "also-excluded", - internal: "should-be-excluded", - name: "test", - nullValue: undefined, - status: "active", - }; - - expect(getFilterCount(filters, { excludeFields: ["internal", "debug"] })).toBe(2); - }); - - it("handles array fields and excludes together", () => { - const filters = { - emptyTags: [], - internal: "excluded", - nullValue: undefined, - status: "active", - tags: ["tag1", "tag2"], - }; - - expect( - getFilterCount(filters, { - excludeFields: ["internal"], - }), - ).toBe(2); - }); - it("returns 0 for empty filters", () => { expect(getFilterCount({})).toBe(0); }); @@ -130,9 +102,5 @@ it("handles complex filters pattern", () => { user: "admin", }; - expect( - getFilterCount(filters, { - excludeFields: ["eventType", "mapIndex"], - }), - ).toBe(5); + expect(getFilterCount(filters)).toBe(6); }); diff --git a/airflow-core/src/airflow/ui/src/utils/filterUtils.ts b/airflow-core/src/airflow/ui/src/utils/filterUtils.ts index d902c2a1d558c..9df5a5a9995a8 100644 --- a/airflow-core/src/airflow/ui/src/utils/filterUtils.ts +++ b/airflow-core/src/airflow/ui/src/utils/filterUtils.ts @@ -17,23 +17,11 @@ * under the License. */ -export const getFilterCount = >( - filters: T, - options?: { - excludeFields?: Array; - }, -): number => { - const { excludeFields = [] } = options ?? {}; - - return Object.entries(filters).reduce((count, [key, value]) => { - if (excludeFields.includes(key)) { - return count; - } - +export const getFilterCount = (filters: Record): number => + Object.values(filters).reduce((count: number, value) => { if (Array.isArray(value)) { return count + (value.length > 0 ? 1 : 0); } return count + (value !== null && value !== undefined ? 1 : 0); }, 0); -};