feat(customHomePage/assetCollection): add dynamic filters (#14435)

Co-authored-by: Chris Collins <chriscollins3456@gmail.com>
This commit is contained in:
v-tarasevich-blitz-brain 2025-08-22 16:59:33 +03:00 committed by GitHub
parent b3fafc38be
commit d9dda1b059
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 472 additions and 121 deletions

View File

@ -122,6 +122,12 @@ public class UpsertPageModuleResolver implements DataFetcher<CompletableFuture<D
UrnArray urnArray = new UrnArray(urns);
assetCollectionParams.setAssetUrns(urnArray);
if (paramsInput.getAssetCollectionParams().getDynamicFilterJson() != null) {
assetCollectionParams.setDynamicFilterJson(
paramsInput.getAssetCollectionParams().getDynamicFilterJson());
}
gmsParams.setAssetCollectionParams(assetCollectionParams);
}

View File

@ -68,6 +68,12 @@ public class PageModuleParamsMapper
.collect(Collectors.toList());
assetCollectionParams.setAssetUrns(assetUrnStrings);
if (params.getAssetCollectionParams().getDynamicFilterJson() != null) {
assetCollectionParams.setDynamicFilterJson(
params.getAssetCollectionParams().getDynamicFilterJson());
}
result.setAssetCollectionParams(assetCollectionParams);
}

View File

@ -133,6 +133,15 @@ input AssetCollectionModuleParamsInput {
The list of asset urns for the asset collection module
"""
assetUrns: [String!]!
"""
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: String
}
"""
@ -311,6 +320,15 @@ type AssetCollectionModuleParams {
The list of asset urns for the asset collection module
"""
assetUrns: [String!]!
"""
Optional dynamic filter
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: String
}
"""

View File

@ -1,11 +1,14 @@
import { Form } from 'antd';
import React, { useState } from 'react';
import React, { useMemo, useState } from 'react';
import styled from 'styled-components';
import { usePageTemplateContext } from '@app/homeV3/context/PageTemplateContext';
import BaseModuleModal from '@app/homeV3/moduleModals/common/BaseModuleModal';
import ModuleDetailsForm from '@app/homeV3/moduleModals/common/ModuleDetailsForm';
import AssetsSection from '@app/homeV3/modules/assetCollection/AssetsSection';
import { SELECT_ASSET_TYPE_DYNAMIC, SELECT_ASSET_TYPE_MANUAL } from '@app/homeV3/modules/assetCollection/constants';
import { LogicalPredicate } from '@app/sharedV2/queryBuilder/builder/types';
import { isEmptyLogicalPredicate } from '@app/sharedV2/queryBuilder/builder/utils';
import { DataHubPageModuleType } from '@types';
@ -26,15 +29,66 @@ const AssetCollectionModal = () => {
(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<string>(currentSelectAssetType);
const [dynamicFilter, setDynamicFilter] = useState<LogicalPredicate | null | undefined>(
currentDynamicFilterLogicalPredicate,
);
const [selectedAssetUrns, setSelectedAssetUrns] = useState<string[]>(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 = () => {
>
<ModalContent>
<ModuleDetailsForm form={form} formValues={{ name: currentName }} />
<AssetsSection selectedAssetUrns={selectedAssetUrns} setSelectedAssetUrns={setSelectedAssetUrns} />
<AssetsSection
selectAssetType={selectAssetType}
setSelectAssetType={setSelectAssetType}
selectedAssetUrns={selectedAssetUrns}
setSelectedAssetUrns={setSelectedAssetUrns}
dynamicFilter={dynamicFilter}
setDynamicFilter={setDynamicFilter}
/>
</ModalContent>
</BaseModuleModal>
);

View File

@ -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<Entity[]> => {
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<Entity[]> => {
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<Entity[]> => {
if (shouldFetchByDynamicFilter) {
return fetchEntitiesByDynamicFilter(start, count);
}
return fetchEntitiesByAssetUrns(start, count);
},
[fetchEntitiesByDynamicFilter, fetchEntitiesByAssetUrns, shouldFetchByDynamicFilter],
);
return (
<LargeModule {...props} loading={loading}>
<InfiniteScrollList<Entity>
@ -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}
/>
</LargeModule>
);

View File

@ -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<React.SetStateAction<string[]>>;
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 (
<Container>
<LeftSection>
<SelectAssetsSection
selectAssetType={selectAssetType}
setSelectAssetType={setSelectAssetType}
selectedAssetUrns={selectedAssetUrns}
setSelectedAssetUrns={setSelectedAssetUrns}
dynamicFilter={dynamicFilter}
setDynamicFilter={setDynamicFilter}
/>
</LeftSection>
<VerticalDivider type="vertical" />
<RightSection>
<SelectedAssetsSection
selectedAssetUrns={selectedAssetUrns}
setSelectedAssetUrns={setSelectedAssetUrns}
/>
</RightSection>
{shouldShowSelectedAssetsSection && (
<RightSection>
<SelectedAssetsSection
selectedAssetUrns={selectedAssetUrns}
setSelectedAssetUrns={setSelectedAssetUrns}
/>
</RightSection>
)}
</Container>
);
};

View File

@ -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 (
<LogicalFiltersBuilder
filters={dynamicFilter ?? EMPTY_FILTER}
onChangeFilters={setDynamicFilter}
properties={properties}
/>
);
};
export default DynamicSelectAssetsTab;

View File

@ -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<React.SetStateAction<string[]>>;
};
const ManualSelectAssetsTab = ({ selectedAssetUrns, setSelectedAssetUrns }: Props) => {
const entityRegistry = useEntityRegistryV2();
const [searchQuery, setSearchQuery] = useState<string | undefined>();
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 (
<ItemDetailsContainer>
<Text color="gray" size="sm">
{displayType}
</Text>
<Checkbox
size="xs"
isChecked={selectedAssetUrns?.includes(entity.urn)}
onCheckboxChange={() => handleCheckboxChange(entity.urn)}
/>
</ItemDetailsContainer>
);
};
let content;
if (loading) {
content = (
<LoaderContainer>
<Loader />
</LoaderContainer>
);
} else if (entities && entities.length > 0) {
content = entities?.map((entity) => (
<EntityItem
entity={entity}
key={entity.urn}
customDetailsRenderer={customDetailsRenderer}
moduleType={DataHubPageModuleType.AssetCollection}
padding="8px 0 8px 8px"
navigateOnlyOnNameClick
/>
));
} else {
content = <EmptySection />;
}
return (
<>
<SearchBar value={searchQuery} onChange={handleSearchChange} />
<AssetFilters
searchQuery={searchQuery}
appliedFilters={appliedFilters}
updateFieldFilters={updateFieldFilters}
/>
<ResultsContainer>
<ScrollableResultsContainer>{content}</ScrollableResultsContainer>
</ResultsContainer>
</>
);
};
export default ManualSelectAssetsTab;

View File

@ -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<React.SetStateAction<string[]>>;
dynamicFilter: LogicalPredicate | null | undefined;
setDynamicFilter: (newDynamicFilter: LogicalPredicate | null | undefined) => void;
};
const SelectAssetsSection = ({ selectedAssetUrns, setSelectedAssetUrns }: Props) => {
const entityRegistry = useEntityRegistryV2();
const [searchQuery, setSearchQuery] = useState<string | undefined>();
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 (
<ItemDetailsContainer>
<Text color="gray" size="sm">
{displayType}
</Text>
<Checkbox
size="xs"
isChecked={selectedAssetUrns?.includes(entity.urn)}
onCheckboxChange={() => handleCheckboxChange(entity.urn)}
const SelectAssetsSection = ({
selectAssetType,
setSelectAssetType,
selectedAssetUrns,
setSelectedAssetUrns,
dynamicFilter,
setDynamicFilter,
}: Props) => {
const tabs: Tab[] = [
{
key: SELECT_ASSET_TYPE_MANUAL,
label: 'Select Assets',
content: (
<ManualSelectAssetsTab
selectedAssetUrns={selectedAssetUrns}
setSelectedAssetUrns={setSelectedAssetUrns}
/>
</ItemDetailsContainer>
);
};
),
},
{
key: SELECT_ASSET_TYPE_DYNAMIC,
label: 'Dynamic Filter',
content: <DynamicSelectAssetsTab dynamicFilter={dynamicFilter} setDynamicFilter={setDynamicFilter} />,
},
];
let content;
if (loading) {
content = (
<LoaderContainer>
<Loader />
</LoaderContainer>
);
} else if (entities && entities.length > 0) {
content = entities?.map((entity) => (
<EntityItem
entity={entity}
key={entity.urn}
customDetailsRenderer={customDetailsRenderer}
moduleType={DataHubPageModuleType.AssetCollection}
padding="8px 0 8px 8px"
navigateOnlyOnNameClick
/>
));
} else {
content = <EmptySection />;
}
const onTabChanged = useCallback(
(newActiveTabKey: string) => {
if (newActiveTabKey === SELECT_ASSET_TYPE_MANUAL || newActiveTabKey === SELECT_ASSET_TYPE_DYNAMIC) {
setSelectAssetType?.(newActiveTabKey);
}
},
[setSelectAssetType],
);
return (
<AssetsSection>
<Text color="gray" weight="bold">
Search and Select Assets
</Text>
<SearchBar value={searchQuery} onChange={handleSearchChange} />
<AssetFilters
searchQuery={searchQuery}
appliedFilters={appliedFilters}
updateFieldFilters={updateFieldFilters}
/>
<ResultsContainer>
<ScrollableResultsContainer>{content}</ScrollableResultsContainer>
</ResultsContainer>
<ButtonTabs tabs={tabs} onTabClick={onTabChanged} defaultKey={selectAssetType} />
</AssetsSection>
);
};

View File

@ -0,0 +1,2 @@
export const SELECT_ASSET_TYPE_MANUAL = 'manual';
export const SELECT_ASSET_TYPE_DYNAMIC = 'dynamic';

View File

@ -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;

View File

@ -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';

View File

@ -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
</Text>
<FormItem name={FORM_FIELD_ASSET_TYPE}>
<EntityTypeTabs tabs={tabs} onTabClick={onTabClick} defaultKey={assetType ?? defaultAssetsType} />
<ButtonTabs tabs={tabs} onTabClick={onTabClick} defaultKey={assetType ?? defaultAssetsType} />
</FormItem>
</Wrapper>
);

View File

@ -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<string | undefined>(defaultKey ?? tabs?.[0]?.key);
const [renderedKeys, setRenderedKeys] = useState<string[]>(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],
);

View File

@ -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%;

View File

@ -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([]);
});
});

View File

@ -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);
}

View File

@ -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();
});
});
});

View File

@ -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;
}

View File

@ -36,6 +36,7 @@ fragment PageModule on DataHubPageModule {
}
assetCollectionParams {
assetUrns
dynamicFilterJson
}
linkParams {
linkUrl

View File

@ -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
}