From d9dda1b059f2bbbef31e92009aba40fbc75df7e2 Mon Sep 17 00:00:00 2001 From: v-tarasevich-blitz-brain Date: Fri, 22 Aug 2025 16:59:33 +0300 Subject: [PATCH] feat(customHomePage/assetCollection): add dynamic filters (#14435) Co-authored-by: Chris Collins --- .../module/UpsertPageModuleResolver.java | 6 + .../types/module/PageModuleParamsMapper.java | 6 + .../src/main/resources/module.graphql | 18 +++ .../assetCollection/AssetCollectionModal.tsx | 71 +++++++++- .../assetCollection/AssetCollectionModule.tsx | 74 +++++++++- .../modules/assetCollection/AssetsSection.tsx | 40 ++++-- .../DynamicSelectAssetsTab.tsx | 27 ++++ .../assetCollection/ManualSelectAssetsTab.tsx | 107 ++++++++++++++ .../assetCollection/SelectAssetsSection.tsx | 134 ++++++------------ .../modules/assetCollection/constants.ts | 2 + .../form/HierarchyViewModuleForm.tsx | 2 +- .../RelatedEntitiesSection.tsx | 2 +- .../selectAssets/SelectAssetsSection.tsx | 6 +- .../ButtonTabs/ButtonTabs.tsx} | 10 +- .../ButtonTabs}/TabButtons.tsx | 2 +- .../ButtonTabs}/types.ts | 0 .../components => shared/Form}/FormItem.tsx | 0 .../utils/__tests__/filterUtils.test.ts | 44 +++++- .../src/app/searchV2/utils/filterUtils.ts | 4 + .../builder/__tests__/utils.test.tsx | 24 +++- .../sharedV2/queryBuilder/builder/utils.ts | 4 + .../src/graphql/template.graphql | 1 + .../module/AssetCollectionModuleParams.pdl | 9 ++ 23 files changed, 472 insertions(+), 121 deletions(-) create mode 100644 datahub-web-react/src/app/homeV3/modules/assetCollection/DynamicSelectAssetsTab.tsx create mode 100644 datahub-web-react/src/app/homeV3/modules/assetCollection/ManualSelectAssetsTab.tsx create mode 100644 datahub-web-react/src/app/homeV3/modules/assetCollection/constants.ts rename datahub-web-react/src/app/homeV3/modules/{hierarchyViewModule/components/form/sections/selectAssets/assetTypeTabs/AssetTypeTabs.tsx => shared/ButtonTabs/ButtonTabs.tsx} (73%) rename datahub-web-react/src/app/homeV3/modules/{hierarchyViewModule/components/form/sections/selectAssets/assetTypeTabs => shared/ButtonTabs}/TabButtons.tsx (93%) rename datahub-web-react/src/app/homeV3/modules/{hierarchyViewModule/components/form/sections/selectAssets/assetTypeTabs => shared/ButtonTabs}/types.ts (100%) rename datahub-web-react/src/app/homeV3/modules/{hierarchyViewModule/components/form/components => shared/Form}/FormItem.tsx (100%) diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/module/UpsertPageModuleResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/module/UpsertPageModuleResolver.java index 236d8b75ad..9226f3fe67 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/module/UpsertPageModuleResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/module/UpsertPageModuleResolver.java @@ -122,6 +122,12 @@ public class UpsertPageModuleResolver implements DataFetcher { (urn): urn is string => typeof urn === 'string', ); const urn = initialState?.urn; + + const currentDynamicFilterLogicalPredicate: LogicalPredicate | undefined = useMemo( + () => + initialState?.properties?.params?.assetCollectionParams?.dynamicFilterJson + ? JSON.parse(initialState.properties.params.assetCollectionParams.dynamicFilterJson) + : undefined, + [initialState?.properties?.params?.assetCollectionParams?.dynamicFilterJson], + ); + + const currentSelectAssetType = useMemo(() => { + if (currentAssets.length === 0 && currentDynamicFilterLogicalPredicate) { + return SELECT_ASSET_TYPE_DYNAMIC; + } + return SELECT_ASSET_TYPE_MANUAL; + }, [currentDynamicFilterLogicalPredicate, currentAssets]); + + const [selectAssetType, setSelectAssetType] = useState(currentSelectAssetType); + + const [dynamicFilter, setDynamicFilter] = useState( + currentDynamicFilterLogicalPredicate, + ); + const [selectedAssetUrns, setSelectedAssetUrns] = useState(currentAssets); const nameValue = Form.useWatch('name', form); - const isDisabled = !nameValue?.trim() || !selectedAssetUrns.length; + const isDisabled = useMemo(() => { + if (!nameValue?.trim()) return true; + + switch (selectAssetType) { + case SELECT_ASSET_TYPE_MANUAL: + return !selectedAssetUrns.length; + case SELECT_ASSET_TYPE_DYNAMIC: + return isEmptyLogicalPredicate(dynamicFilter); + default: + return true; + } + }, [nameValue, selectAssetType, selectedAssetUrns, dynamicFilter]); const handleUpsertAssetCollectionModule = () => { + const getAssetCollectionParams = () => { + switch (selectAssetType) { + case SELECT_ASSET_TYPE_MANUAL: + return { + assetUrns: selectedAssetUrns, + dynamicFilterJson: undefined, + }; + case SELECT_ASSET_TYPE_DYNAMIC: + return { + assetUrns: [], + dynamicFilterJson: JSON.stringify(dynamicFilter), + }; + default: + return {}; + } + }; + form.validateFields().then((values) => { const { name } = values; + upsertModule({ urn, name, @@ -42,9 +96,7 @@ const AssetCollectionModal = () => { // scope: initialState?.properties.visibility.scope || undefined, type: DataHubPageModuleType.AssetCollection, params: { - assetCollectionParams: { - assetUrns: selectedAssetUrns, - }, + assetCollectionParams: getAssetCollectionParams(), }, }); close(); @@ -61,7 +113,14 @@ const AssetCollectionModal = () => { > - + ); diff --git a/datahub-web-react/src/app/homeV3/modules/assetCollection/AssetCollectionModule.tsx b/datahub-web-react/src/app/homeV3/modules/assetCollection/AssetCollectionModule.tsx index 79ab2fc701..ed72987875 100644 --- a/datahub-web-react/src/app/homeV3/modules/assetCollection/AssetCollectionModule.tsx +++ b/datahub-web-react/src/app/homeV3/modules/assetCollection/AssetCollectionModule.tsx @@ -5,6 +5,9 @@ import EmptyContent from '@app/homeV3/module/components/EmptyContent'; import EntityItem from '@app/homeV3/module/components/EntityItem'; import LargeModule from '@app/homeV3/module/components/LargeModule'; import { ModuleProps } from '@app/homeV3/module/types'; +import { excludeEmptyAndFilters } from '@app/searchV2/utils/filterUtils'; +import { LogicalPredicate } from '@app/sharedV2/queryBuilder/builder/types'; +import { convertLogicalPredicateToOrFilters } from '@app/sharedV2/queryBuilder/builder/utils'; import { useGetSearchResultsForMultipleQuery } from '@graphql/search.generated'; import { DataHubPageModuleType, Entity } from '@types'; @@ -20,19 +23,72 @@ const AssetCollectionModule = (props: ModuleProps) => { [props.module.properties.params.assetCollectionParams?.assetUrns], ); + const dynamicFilterLogicalPredicate: LogicalPredicate | undefined = useMemo( + () => + props.module.properties.params.assetCollectionParams?.dynamicFilterJson + ? JSON.parse(props.module.properties.params.assetCollectionParams?.dynamicFilterJson) + : undefined, + [props.module.properties.params.assetCollectionParams?.dynamicFilterJson], + ); + + const shouldFetchByDynamicFilter = useMemo( + () => assetUrns.length === 0 && !!dynamicFilterLogicalPredicate, + [assetUrns, dynamicFilterLogicalPredicate], + ); + + const dynamicOrFilters = useMemo(() => { + if (dynamicFilterLogicalPredicate) { + const orFilters = excludeEmptyAndFilters(convertLogicalPredicateToOrFilters(dynamicFilterLogicalPredicate)); + return orFilters; + } + return undefined; + }, [dynamicFilterLogicalPredicate]); + + const totalForInfiniteScroll = useMemo( + () => (shouldFetchByDynamicFilter ? undefined : assetUrns.length), + [shouldFetchByDynamicFilter, assetUrns], + ); + const { loading, refetch } = useGetSearchResultsForMultipleQuery({ variables: { input: { start: 0, count: DEFAULT_PAGE_SIZE, query: '*', - filters: [{ field: 'urn', values: assetUrns }], + ...(shouldFetchByDynamicFilter + ? { orFilters: dynamicOrFilters } + : { + filters: [{ field: 'urn', values: assetUrns }], + }), }, }, - skip: assetUrns.length === 0, + skip: assetUrns.length === 0 && !dynamicOrFilters?.length, }); - const fetchEntities = useCallback( + const fetchEntitiesByDynamicFilter = useCallback( + async (start: number, count: number): Promise => { + if (!dynamicOrFilters?.length) return []; + + const result = await refetch({ + input: { + start, + count, + query: '*', + orFilters: dynamicOrFilters, + }, + }); + + const results = + result.data?.searchAcrossEntities?.searchResults + ?.map((res) => res.entity) + .filter((entity): entity is Entity => !!entity) || []; + + return results; + }, + [dynamicOrFilters, refetch], + ); + + const fetchEntitiesByAssetUrns = useCallback( async (start: number, count: number): Promise => { if (assetUrns.length === 0) return []; // urn slicing is done at the front-end to maintain the order of assets to show with pagination @@ -57,6 +113,16 @@ const AssetCollectionModule = (props: ModuleProps) => { [assetUrns, refetch], ); + const fetchEntities = useCallback( + async (start: number, count: number): Promise => { + if (shouldFetchByDynamicFilter) { + return fetchEntitiesByDynamicFilter(start, count); + } + return fetchEntitiesByAssetUrns(start, count); + }, + [fetchEntitiesByDynamicFilter, fetchEntitiesByAssetUrns, shouldFetchByDynamicFilter], + ); + return ( @@ -73,7 +139,7 @@ const AssetCollectionModule = (props: ModuleProps) => { description="Edit the module and add assets to see them in this list" /> } - totalItemCount={assetUrns.length} + totalItemCount={totalForInfiniteScroll} /> ); diff --git a/datahub-web-react/src/app/homeV3/modules/assetCollection/AssetsSection.tsx b/datahub-web-react/src/app/homeV3/modules/assetCollection/AssetsSection.tsx index 43b75dfcfe..a547d2d5da 100644 --- a/datahub-web-react/src/app/homeV3/modules/assetCollection/AssetsSection.tsx +++ b/datahub-web-react/src/app/homeV3/modules/assetCollection/AssetsSection.tsx @@ -1,10 +1,12 @@ import { colors } from '@components'; import { Divider } from 'antd'; -import React from 'react'; +import React, { useMemo } from 'react'; import styled from 'styled-components'; import SelectAssetsSection from '@app/homeV3/modules/assetCollection/SelectAssetsSection'; import SelectedAssetsSection from '@app/homeV3/modules/assetCollection/SelectedAssetsSection'; +import { SELECT_ASSET_TYPE_MANUAL } from '@app/homeV3/modules/assetCollection/constants'; +import { LogicalPredicate } from '@app/sharedV2/queryBuilder/builder/types'; const Container = styled.div` display: flex; @@ -27,26 +29,48 @@ export const VerticalDivider = styled(Divider)` `; type Props = { + selectAssetType: string; + setSelectAssetType: (newSelectAssetType: string) => void; selectedAssetUrns: string[]; setSelectedAssetUrns: React.Dispatch>; + dynamicFilter: LogicalPredicate | null | undefined; + setDynamicFilter: (newDynamicFilter: LogicalPredicate | null | undefined) => void; }; -const AssetsSection = ({ selectedAssetUrns, setSelectedAssetUrns }: Props) => { +const AssetsSection = ({ + selectAssetType, + setSelectAssetType, + selectedAssetUrns, + setSelectedAssetUrns, + dynamicFilter, + setDynamicFilter, +}: Props) => { + const shouldShowSelectedAssetsSection = useMemo( + () => selectAssetType === SELECT_ASSET_TYPE_MANUAL, + [selectAssetType], + ); + return ( - - - + {shouldShowSelectedAssetsSection && ( + + + + )} ); }; diff --git a/datahub-web-react/src/app/homeV3/modules/assetCollection/DynamicSelectAssetsTab.tsx b/datahub-web-react/src/app/homeV3/modules/assetCollection/DynamicSelectAssetsTab.tsx new file mode 100644 index 0000000000..3cee4b281a --- /dev/null +++ b/datahub-web-react/src/app/homeV3/modules/assetCollection/DynamicSelectAssetsTab.tsx @@ -0,0 +1,27 @@ +import React from 'react'; + +import LogicalFiltersBuilder from '@app/sharedV2/queryBuilder/LogicalFiltersBuilder'; +import { LogicalOperatorType, LogicalPredicate } from '@app/sharedV2/queryBuilder/builder/types'; +import { properties } from '@app/sharedV2/queryBuilder/properties'; + +const EMPTY_FILTER = { + operator: LogicalOperatorType.AND, + operands: [], +}; + +type Props = { + dynamicFilter: LogicalPredicate | null | undefined; + setDynamicFilter: (newDynamicFilter: LogicalPredicate | null | undefined) => void; +}; + +const DynamicSelectAssetsTab = ({ dynamicFilter, setDynamicFilter }: Props) => { + return ( + + ); +}; + +export default DynamicSelectAssetsTab; diff --git a/datahub-web-react/src/app/homeV3/modules/assetCollection/ManualSelectAssetsTab.tsx b/datahub-web-react/src/app/homeV3/modules/assetCollection/ManualSelectAssetsTab.tsx new file mode 100644 index 0000000000..983fa956d7 --- /dev/null +++ b/datahub-web-react/src/app/homeV3/modules/assetCollection/ManualSelectAssetsTab.tsx @@ -0,0 +1,107 @@ +import { Checkbox, Loader, SearchBar, Text } from '@components'; +import React, { useState } from 'react'; +import styled from 'styled-components'; + +import EntityItem from '@app/homeV3/module/components/EntityItem'; +import AssetFilters from '@app/homeV3/modules/assetCollection/AssetFilters'; +import EmptySection from '@app/homeV3/modules/assetCollection/EmptySection'; +import useGetAssetResults from '@app/homeV3/modules/assetCollection/useGetAssetResults'; +import { LoaderContainer } from '@app/homeV3/styledComponents'; +import { getEntityDisplayType } from '@app/searchV2/autoCompleteV2/utils'; +import useAppliedFilters from '@app/searchV2/filtersV2/context/useAppliedFilters'; +import { useEntityRegistryV2 } from '@app/useEntityRegistry'; + +import { DataHubPageModuleType, Entity } from '@types'; + +const ItemDetailsContainer = styled.div` + display: flex; + align-items: center; +`; + +const ResultsContainer = styled.div` + margin: 0 -16px 0 -8px; + position: relative; + max-height: 300px; + padding-right: 8px; +`; + +const ScrollableResultsContainer = styled.div` + max-height: inherit; + overflow-y: auto; +`; + +type Props = { + selectedAssetUrns: string[]; + setSelectedAssetUrns: React.Dispatch>; +}; + +const ManualSelectAssetsTab = ({ selectedAssetUrns, setSelectedAssetUrns }: Props) => { + const entityRegistry = useEntityRegistryV2(); + + const [searchQuery, setSearchQuery] = useState(); + const { appliedFilters, updateFieldFilters } = useAppliedFilters(); + const { entities, loading } = useGetAssetResults({ searchQuery, appliedFilters }); + + const handleSearchChange = (value: string) => { + setSearchQuery(value); + }; + + const handleCheckboxChange = (urn: string) => { + setSelectedAssetUrns((prev) => (prev.includes(urn) ? prev.filter((u) => u !== urn) : [...prev, urn])); + }; + + const customDetailsRenderer = (entity: Entity) => { + const displayType = getEntityDisplayType(entity, entityRegistry); + + return ( + + + {displayType} + + handleCheckboxChange(entity.urn)} + /> + + ); + }; + + let content; + if (loading) { + content = ( + + + + ); + } else if (entities && entities.length > 0) { + content = entities?.map((entity) => ( + + )); + } else { + content = ; + } + + return ( + <> + + + + {content} + + + ); +}; + +export default ManualSelectAssetsTab; diff --git a/datahub-web-react/src/app/homeV3/modules/assetCollection/SelectAssetsSection.tsx b/datahub-web-react/src/app/homeV3/modules/assetCollection/SelectAssetsSection.tsx index e3f778a0dd..75f2b7dc9d 100644 --- a/datahub-web-react/src/app/homeV3/modules/assetCollection/SelectAssetsSection.tsx +++ b/datahub-web-react/src/app/homeV3/modules/assetCollection/SelectAssetsSection.tsx @@ -1,17 +1,13 @@ -import { Checkbox, Loader, SearchBar, Text } from '@components'; -import React, { useState } from 'react'; +import { Text } from '@components'; +import React, { useCallback } from 'react'; import styled from 'styled-components'; -import EntityItem from '@app/homeV3/module/components/EntityItem'; -import AssetFilters from '@app/homeV3/modules/assetCollection/AssetFilters'; -import EmptySection from '@app/homeV3/modules/assetCollection/EmptySection'; -import useGetAssetResults from '@app/homeV3/modules/assetCollection/useGetAssetResults'; -import { LoaderContainer } from '@app/homeV3/styledComponents'; -import { getEntityDisplayType } from '@app/searchV2/autoCompleteV2/utils'; -import useAppliedFilters from '@app/searchV2/filtersV2/context/useAppliedFilters'; -import { useEntityRegistryV2 } from '@app/useEntityRegistry'; - -import { DataHubPageModuleType, Entity } from '@types'; +import DynamicSelectAssetsTab from '@app/homeV3/modules/assetCollection/DynamicSelectAssetsTab'; +import ManualSelectAssetsTab from '@app/homeV3/modules/assetCollection/ManualSelectAssetsTab'; +import { SELECT_ASSET_TYPE_DYNAMIC, SELECT_ASSET_TYPE_MANUAL } from '@app/homeV3/modules/assetCollection/constants'; +import ButtonTabs from '@app/homeV3/modules/shared/ButtonTabs/ButtonTabs'; +import { Tab } from '@app/homeV3/modules/shared/ButtonTabs/types'; +import { LogicalPredicate } from '@app/sharedV2/queryBuilder/builder/types'; const AssetsSection = styled.div` display: flex; @@ -19,96 +15,56 @@ const AssetsSection = styled.div` gap: 8px; `; -const ItemDetailsContainer = styled.div` - display: flex; - align-items: center; -`; - -const ResultsContainer = styled.div` - margin: 0 -16px 0 -8px; - position: relative; - max-height: 300px; - padding-right: 8px; -`; - -const ScrollableResultsContainer = styled.div` - max-height: inherit; - overflow-y: auto; -`; - type Props = { + selectAssetType: string; + setSelectAssetType: (newSelectAssetType: string) => void; selectedAssetUrns: string[]; setSelectedAssetUrns: React.Dispatch>; + dynamicFilter: LogicalPredicate | null | undefined; + setDynamicFilter: (newDynamicFilter: LogicalPredicate | null | undefined) => void; }; -const SelectAssetsSection = ({ selectedAssetUrns, setSelectedAssetUrns }: Props) => { - const entityRegistry = useEntityRegistryV2(); - - const [searchQuery, setSearchQuery] = useState(); - const { appliedFilters, updateFieldFilters } = useAppliedFilters(); - const { entities, loading } = useGetAssetResults({ searchQuery, appliedFilters }); - - const handleSearchChange = (value: string) => { - setSearchQuery(value); - }; - - const handleCheckboxChange = (urn: string) => { - setSelectedAssetUrns((prev) => (prev.includes(urn) ? prev.filter((u) => u !== urn) : [...prev, urn])); - }; - - const customDetailsRenderer = (entity: Entity) => { - const displayType = getEntityDisplayType(entity, entityRegistry); - - return ( - - - {displayType} - - handleCheckboxChange(entity.urn)} +const SelectAssetsSection = ({ + selectAssetType, + setSelectAssetType, + selectedAssetUrns, + setSelectedAssetUrns, + dynamicFilter, + setDynamicFilter, +}: Props) => { + const tabs: Tab[] = [ + { + key: SELECT_ASSET_TYPE_MANUAL, + label: 'Select Assets', + content: ( + - - ); - }; + ), + }, + { + key: SELECT_ASSET_TYPE_DYNAMIC, + label: 'Dynamic Filter', + content: , + }, + ]; - let content; - if (loading) { - content = ( - - - - ); - } else if (entities && entities.length > 0) { - content = entities?.map((entity) => ( - - )); - } else { - content = ; - } + const onTabChanged = useCallback( + (newActiveTabKey: string) => { + if (newActiveTabKey === SELECT_ASSET_TYPE_MANUAL || newActiveTabKey === SELECT_ASSET_TYPE_DYNAMIC) { + setSelectAssetType?.(newActiveTabKey); + } + }, + [setSelectAssetType], + ); return ( Search and Select Assets - - - - {content} - + ); }; diff --git a/datahub-web-react/src/app/homeV3/modules/assetCollection/constants.ts b/datahub-web-react/src/app/homeV3/modules/assetCollection/constants.ts new file mode 100644 index 0000000000..93b9dfca5e --- /dev/null +++ b/datahub-web-react/src/app/homeV3/modules/assetCollection/constants.ts @@ -0,0 +1,2 @@ +export const SELECT_ASSET_TYPE_MANUAL = 'manual'; +export const SELECT_ASSET_TYPE_DYNAMIC = 'dynamic'; diff --git a/datahub-web-react/src/app/homeV3/modules/hierarchyViewModule/components/form/HierarchyViewModuleForm.tsx b/datahub-web-react/src/app/homeV3/modules/hierarchyViewModule/components/form/HierarchyViewModuleForm.tsx index ed57afabaa..465395fa25 100644 --- a/datahub-web-react/src/app/homeV3/modules/hierarchyViewModule/components/form/HierarchyViewModuleForm.tsx +++ b/datahub-web-react/src/app/homeV3/modules/hierarchyViewModule/components/form/HierarchyViewModuleForm.tsx @@ -2,9 +2,9 @@ import { Input } from '@components'; import React from 'react'; import styled from 'styled-components'; -import FormItem from '@app/homeV3/modules/hierarchyViewModule/components/form/components/FormItem'; import RelatedEntitiesSection from '@app/homeV3/modules/hierarchyViewModule/components/form/sections/relatedEntities/RelatedEntitiesSection'; import SelectAssetsSection from '@app/homeV3/modules/hierarchyViewModule/components/form/sections/selectAssets/SelectAssetsSection'; +import FormItem from '@app/homeV3/modules/shared/Form/FormItem'; const FormWrapper = styled.div` display: flex; diff --git a/datahub-web-react/src/app/homeV3/modules/hierarchyViewModule/components/form/sections/relatedEntities/RelatedEntitiesSection.tsx b/datahub-web-react/src/app/homeV3/modules/hierarchyViewModule/components/form/sections/relatedEntities/RelatedEntitiesSection.tsx index b7f267ea7f..b68a3e1fd9 100644 --- a/datahub-web-react/src/app/homeV3/modules/hierarchyViewModule/components/form/sections/relatedEntities/RelatedEntitiesSection.tsx +++ b/datahub-web-react/src/app/homeV3/modules/hierarchyViewModule/components/form/sections/relatedEntities/RelatedEntitiesSection.tsx @@ -2,13 +2,13 @@ import { Form } from 'antd'; import React, { useCallback } from 'react'; import { useHierarchyFormContext } from '@app/homeV3/modules/hierarchyViewModule/components/form/HierarchyFormContext'; -import FormItem from '@app/homeV3/modules/hierarchyViewModule/components/form/components/FormItem'; import { FORM_FIELD_RELATED_ENTITIES_FILTER, FORM_FIELD_SHOW_RELATED_ENTITIES, } from '@app/homeV3/modules/hierarchyViewModule/components/form/constants'; import ShowRelatedEntitiesSwitch from '@app/homeV3/modules/hierarchyViewModule/components/form/sections/relatedEntities/components/ShowRelatedEntitiesToggler'; import { HierarchyForm } from '@app/homeV3/modules/hierarchyViewModule/components/form/types'; +import FormItem from '@app/homeV3/modules/shared/Form/FormItem'; import LogicalFiltersBuilder from '@app/sharedV2/queryBuilder/LogicalFiltersBuilder'; import { LogicalOperatorType, LogicalPredicate } from '@app/sharedV2/queryBuilder/builder/types'; import { properties } from '@app/sharedV2/queryBuilder/properties'; diff --git a/datahub-web-react/src/app/homeV3/modules/hierarchyViewModule/components/form/sections/selectAssets/SelectAssetsSection.tsx b/datahub-web-react/src/app/homeV3/modules/hierarchyViewModule/components/form/sections/selectAssets/SelectAssetsSection.tsx index 11222d4b6d..0eb41c2b94 100644 --- a/datahub-web-react/src/app/homeV3/modules/hierarchyViewModule/components/form/sections/selectAssets/SelectAssetsSection.tsx +++ b/datahub-web-react/src/app/homeV3/modules/hierarchyViewModule/components/form/sections/selectAssets/SelectAssetsSection.tsx @@ -5,11 +5,11 @@ import styled from 'styled-components'; import DomainsSelectableTreeView from '@app/homeV3/modules/hierarchyViewModule/components/domains/DomainsSelectableTreeView'; import { useHierarchyFormContext } from '@app/homeV3/modules/hierarchyViewModule/components/form/HierarchyFormContext'; -import FormItem from '@app/homeV3/modules/hierarchyViewModule/components/form/components/FormItem'; import { FORM_FIELD_ASSET_TYPE } from '@app/homeV3/modules/hierarchyViewModule/components/form/constants'; -import EntityTypeTabs from '@app/homeV3/modules/hierarchyViewModule/components/form/sections/selectAssets/assetTypeTabs/AssetTypeTabs'; import GlossarySelectableTreeView from '@app/homeV3/modules/hierarchyViewModule/components/glossary/GlossarySelectableTreeView'; import { ASSET_TYPE_DOMAINS, ASSET_TYPE_GLOSSARY } from '@app/homeV3/modules/hierarchyViewModule/constants'; +import ButtonTabs from '@app/homeV3/modules/shared/ButtonTabs/ButtonTabs'; +import FormItem from '@app/homeV3/modules/shared/Form/FormItem'; const Wrapper = styled.div``; @@ -56,7 +56,7 @@ export default function SelectAssetsSection() { Search and Select Assets - + ); diff --git a/datahub-web-react/src/app/homeV3/modules/hierarchyViewModule/components/form/sections/selectAssets/assetTypeTabs/AssetTypeTabs.tsx b/datahub-web-react/src/app/homeV3/modules/shared/ButtonTabs/ButtonTabs.tsx similarity index 73% rename from datahub-web-react/src/app/homeV3/modules/hierarchyViewModule/components/form/sections/selectAssets/assetTypeTabs/AssetTypeTabs.tsx rename to datahub-web-react/src/app/homeV3/modules/shared/ButtonTabs/ButtonTabs.tsx index f625cbd2b0..8b7004433e 100644 --- a/datahub-web-react/src/app/homeV3/modules/hierarchyViewModule/components/form/sections/selectAssets/assetTypeTabs/AssetTypeTabs.tsx +++ b/datahub-web-react/src/app/homeV3/modules/shared/ButtonTabs/ButtonTabs.tsx @@ -1,8 +1,8 @@ import React, { useCallback, useState } from 'react'; import styled from 'styled-components'; -import { TabButtons } from '@app/homeV3/modules/hierarchyViewModule/components/form/sections/selectAssets/assetTypeTabs/TabButtons'; -import { Tab } from '@app/homeV3/modules/hierarchyViewModule/components/form/sections/selectAssets/assetTypeTabs/types'; +import { TabButtons } from '@app/homeV3/modules/shared/ButtonTabs/TabButtons'; +import { Tab } from '@app/homeV3/modules/shared/ButtonTabs/types'; const TabContentWrapper = styled.div<{ $visible?: boolean }>` ${(props) => !props.$visible && 'display: none;'} @@ -11,10 +11,10 @@ const TabContentWrapper = styled.div<{ $visible?: boolean }>` interface Props { tabs: Tab[]; defaultKey?: string; - onTabClick: (key: string) => void; + onTabClick?: (key: string) => void; } -export default function EntityTypeTabs({ tabs, defaultKey, onTabClick }: Props) { +export default function ButtonTabs({ tabs, defaultKey, onTabClick }: Props) { const [activeKey, setActiveKey] = useState(defaultKey ?? tabs?.[0]?.key); const [renderedKeys, setRenderedKeys] = useState(activeKey ? [activeKey] : []); @@ -22,7 +22,7 @@ export default function EntityTypeTabs({ tabs, defaultKey, onTabClick }: Props) (key: string) => { setActiveKey(key); setRenderedKeys((prev) => [...new Set([...prev, key])]); - onTabClick(key); + onTabClick?.(key); }, [onTabClick], ); diff --git a/datahub-web-react/src/app/homeV3/modules/hierarchyViewModule/components/form/sections/selectAssets/assetTypeTabs/TabButtons.tsx b/datahub-web-react/src/app/homeV3/modules/shared/ButtonTabs/TabButtons.tsx similarity index 93% rename from datahub-web-react/src/app/homeV3/modules/hierarchyViewModule/components/form/sections/selectAssets/assetTypeTabs/TabButtons.tsx rename to datahub-web-react/src/app/homeV3/modules/shared/ButtonTabs/TabButtons.tsx index d5a340dfd4..1ee5be16bb 100644 --- a/datahub-web-react/src/app/homeV3/modules/hierarchyViewModule/components/form/sections/selectAssets/assetTypeTabs/TabButtons.tsx +++ b/datahub-web-react/src/app/homeV3/modules/shared/ButtonTabs/TabButtons.tsx @@ -2,7 +2,7 @@ import { Button, colors } from '@components'; import React, { useCallback } from 'react'; import styled from 'styled-components'; -import { Tab } from '@app/homeV3/modules/hierarchyViewModule/components/form/sections/selectAssets/assetTypeTabs/types'; +import { Tab } from '@app/homeV3/modules/shared/ButtonTabs/types'; const StyledButton = styled(Button)<{ $active?: boolean }>` width: 100%; diff --git a/datahub-web-react/src/app/homeV3/modules/hierarchyViewModule/components/form/sections/selectAssets/assetTypeTabs/types.ts b/datahub-web-react/src/app/homeV3/modules/shared/ButtonTabs/types.ts similarity index 100% rename from datahub-web-react/src/app/homeV3/modules/hierarchyViewModule/components/form/sections/selectAssets/assetTypeTabs/types.ts rename to datahub-web-react/src/app/homeV3/modules/shared/ButtonTabs/types.ts diff --git a/datahub-web-react/src/app/homeV3/modules/hierarchyViewModule/components/form/components/FormItem.tsx b/datahub-web-react/src/app/homeV3/modules/shared/Form/FormItem.tsx similarity index 100% rename from datahub-web-react/src/app/homeV3/modules/hierarchyViewModule/components/form/components/FormItem.tsx rename to datahub-web-react/src/app/homeV3/modules/shared/Form/FormItem.tsx diff --git a/datahub-web-react/src/app/searchV2/utils/__tests__/filterUtils.test.ts b/datahub-web-react/src/app/searchV2/utils/__tests__/filterUtils.test.ts index 0edd997f2e..c1edfc9967 100644 --- a/datahub-web-react/src/app/searchV2/utils/__tests__/filterUtils.test.ts +++ b/datahub-web-react/src/app/searchV2/utils/__tests__/filterUtils.test.ts @@ -1,5 +1,9 @@ import { QuickFilterField } from '@app/searchV2/autoComplete/quickFilters/utils'; -import { getAutoCompleteInputFromQuickFilter, getFiltersWithQuickFilter } from '@app/searchV2/utils/filterUtils'; +import { + excludeEmptyAndFilters, + getAutoCompleteInputFromQuickFilter, + getFiltersWithQuickFilter, +} from '@app/searchV2/utils/filterUtils'; describe('getAutoCompleteInputFromQuickFilter', () => { it('should create a platform filter if the selected quick filter is a platform', () => { @@ -43,3 +47,41 @@ describe('getFiltersWithQuickFilter', () => { expect(filterResult).toMatchObject([]); }); }); + +describe('excludeEmptyAndFilters', () => { + it('should handle filter out empty filters', () => { + const result = excludeEmptyAndFilters(undefined); + + expect(result).toBeUndefined(); + }); + + it('should handle empty array', () => { + const result = excludeEmptyAndFilters([]); + + expect(result).toMatchObject([]); + }); + + it('should handle array of filled filters', () => { + const result = excludeEmptyAndFilters([ + { and: [{ field: 'test', values: ['test'] }] }, + { and: [{ field: 'test2', values: ['test2'] }] }, + ]); + + expect(result).toMatchObject([ + { and: [{ field: 'test', values: ['test'] }] }, + { and: [{ field: 'test2', values: ['test2'] }] }, + ]); + }); + + it('should handle mixed empty and filled filters', () => { + const result = excludeEmptyAndFilters([{ and: [] }, { and: [{ field: 'test', values: ['test'] }] }]); + + expect(result).toMatchObject([{ and: [{ field: 'test', values: ['test'] }] }]); + }); + + it('should handle array of empty filters', () => { + const result = excludeEmptyAndFilters([{ and: [] }, { and: [] }]); + + expect(result).toMatchObject([]); + }); +}); diff --git a/datahub-web-react/src/app/searchV2/utils/filterUtils.ts b/datahub-web-react/src/app/searchV2/utils/filterUtils.ts index e2e5054283..ca406d2ad0 100644 --- a/datahub-web-react/src/app/searchV2/utils/filterUtils.ts +++ b/datahub-web-react/src/app/searchV2/utils/filterUtils.ts @@ -214,3 +214,7 @@ export function combineOrFilters(orFilter1: AndFilterInput[], orFilter2: AndFilt return mergedFilter; } + +export function excludeEmptyAndFilters(filters: AndFilterInput[] | undefined): AndFilterInput[] | undefined { + return filters?.filter((filter) => filter.and?.length); +} diff --git a/datahub-web-react/src/app/sharedV2/queryBuilder/builder/__tests__/utils.test.tsx b/datahub-web-react/src/app/sharedV2/queryBuilder/builder/__tests__/utils.test.tsx index 197b87a302..1d68e1c772 100644 --- a/datahub-web-react/src/app/sharedV2/queryBuilder/builder/__tests__/utils.test.tsx +++ b/datahub-web-react/src/app/sharedV2/queryBuilder/builder/__tests__/utils.test.tsx @@ -1,5 +1,9 @@ -import { LogicalOperatorType } from '@app/sharedV2/queryBuilder/builder/types'; -import { convertLogicalPredicateToOrFilters, isLogicalPredicate } from '@app/sharedV2/queryBuilder/builder/utils'; +import { LogicalOperatorType, LogicalPredicate } from '@app/sharedV2/queryBuilder/builder/types'; +import { + convertLogicalPredicateToOrFilters, + isEmptyLogicalPredicate, + isLogicalPredicate, +} from '@app/sharedV2/queryBuilder/builder/utils'; describe('utils', () => { describe('isLogicalPredicate', () => { @@ -205,6 +209,10 @@ describe('utils', () => { ]; const EMPTY_LOGICAL_PREDICATE = {}; + const LOGICAL_PREDICATE_WITH_EMPTY_OPERANDS = { + operator: LogicalOperatorType.AND, + operands: [], + }; const LOGICAL_PREDICATE_WITH_UNKNOWN_OPERATION = { operator: 'UNKNOWN', @@ -235,4 +243,16 @@ describe('utils', () => { expect(convertLogicalPredicateToOrFilters(LOGICAL_PREDICATE_WITH_UNKNOWN_OPERATION)).toEqual(undefined); }); }); + + describe('isEmptyLogicalPredicate', () => { + it('should handle not empty logical predicate', () => { + expect(isEmptyLogicalPredicate(BASIC_AND_LOGICAL_PREDICATE)).toBeFalsy(); + }); + it('should handle empty logical predicate', () => { + expect(isEmptyLogicalPredicate(EMPTY_LOGICAL_PREDICATE as LogicalPredicate)).toBeTruthy(); + }); + it('should handle logical predicate with empty', () => { + expect(isEmptyLogicalPredicate(LOGICAL_PREDICATE_WITH_EMPTY_OPERANDS)).toBeTruthy(); + }); + }); }); diff --git a/datahub-web-react/src/app/sharedV2/queryBuilder/builder/utils.ts b/datahub-web-react/src/app/sharedV2/queryBuilder/builder/utils.ts index 52bfafb294..e3768b468a 100644 --- a/datahub-web-react/src/app/sharedV2/queryBuilder/builder/utils.ts +++ b/datahub-web-react/src/app/sharedV2/queryBuilder/builder/utils.ts @@ -109,3 +109,7 @@ export const convertToLogicalPredicate = (predicate: LogicalPredicate | Property // Already is a logical predicate. return predicate as LogicalPredicate; }; + +export function isEmptyLogicalPredicate(predicate: LogicalPredicate | null | undefined) { + return !predicate?.operands?.length; +} diff --git a/datahub-web-react/src/graphql/template.graphql b/datahub-web-react/src/graphql/template.graphql index ffdc41eb51..99f6b680e1 100644 --- a/datahub-web-react/src/graphql/template.graphql +++ b/datahub-web-react/src/graphql/template.graphql @@ -36,6 +36,7 @@ fragment PageModule on DataHubPageModule { } assetCollectionParams { assetUrns + dynamicFilterJson } linkParams { linkUrl diff --git a/metadata-models/src/main/pegasus/com/linkedin/module/AssetCollectionModuleParams.pdl b/metadata-models/src/main/pegasus/com/linkedin/module/AssetCollectionModuleParams.pdl index 8a0cb5a6eb..6a470f1f16 100644 --- a/metadata-models/src/main/pegasus/com/linkedin/module/AssetCollectionModuleParams.pdl +++ b/metadata-models/src/main/pegasus/com/linkedin/module/AssetCollectionModuleParams.pdl @@ -7,4 +7,13 @@ import com.linkedin.common.Urn */ record AssetCollectionModuleParams { assetUrns: array[Urn] + + /** + * Optional dynamic filters + * + * The stringified json representing the logical predicate built in the UI to select assets. + * This predicate is turned into orFilters to send through graphql since graphql doesn't support + * arbitrary nesting. This string is used to restore the UI for this logical predicate. + */ + dynamicFilterJson: optional string } \ No newline at end of file