feat(ui): Adding support for 'has siblings' filter behind a feature flag. (#12685)

Co-authored-by: John Joyce <john@Mac-4613.lan>
This commit is contained in:
John Joyce 2025-03-25 18:40:47 -07:00 committed by GitHub
parent 3e5a3928f8
commit 0c315d2c62
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 248 additions and 4 deletions

View File

@ -205,6 +205,7 @@ public class AppConfigResolver implements DataFetcher<CompletableFuture<AppConfi
.setShowNavBarRedesign(_featureFlags.isShowNavBarRedesign())
.setShowAutoCompleteResults(_featureFlags.isShowAutoCompleteResults())
.setEntityVersioningEnabled(_featureFlags.isEntityVersioning())
.setShowHasSiblingsFilter(_featureFlags.isShowHasSiblingsFilter())
.setShowSearchBarAutocompleteRedesign(
_featureFlags.isShowSearchBarAutocompleteRedesign())
.build();

View File

@ -599,6 +599,11 @@ type FeatureFlagsConfig {
"""
entityVersioningEnabled: Boolean!
"""
If turned on, show the "has siblings" filter in search
"""
showHasSiblingsFilter: Boolean!
"""
If turned on, show the redesigned search bar's autocomplete
"""

View File

@ -12,6 +12,7 @@ import {
LEGACY_ENTITY_FILTER_NAME,
SCHEMA_FIELD_ALIASES_FILTER_NAME,
} from './utils/constants';
import { useAppConfig } from '../useAppConfig';
const TOP_FILTERS = ['degree', ENTITY_FILTER_NAME, 'platform', 'tags', 'glossaryTerms', 'domains', 'owners'];
@ -30,6 +31,7 @@ interface Props {
}
export const SimpleSearchFilters = ({ facets, selectedFilters, onFilterSelect, loading }: Props) => {
const { config } = useAppConfig();
const [cachedProps, setCachedProps] = useState<{
facets: Array<FacetMetadata>;
selectedFilters: Array<FacetFilterInput>;
@ -87,6 +89,7 @@ export const SimpleSearchFilters = ({ facets, selectedFilters, onFilterSelect, l
filter: facet,
activeFilters: selectedFilters,
onChangeFilters: onFilterSelect,
config,
})
) : (
<SimpleSearchFilter

View File

@ -1,6 +1,7 @@
import { Divider } from 'antd';
import React from 'react';
import styled from 'styled-components';
import { useAppConfig } from '@src/app/useAppConfig';
import { FacetFilterInput, FacetMetadata } from '../../../types.generated';
import { useUserContext } from '../../context/useUserContext';
import {
@ -74,6 +75,7 @@ export default function BasicFilters({
showAdvancedFilters,
}: Props) {
const userContext = useUserContext();
const { config } = useAppConfig();
const selectedViewUrn = userContext?.localState?.selectedViewUrn;
const showSaveViewButton = activeFilters?.length > 0 && selectedViewUrn === undefined;
// only want Environment filter if there's 2 or more envs
@ -99,6 +101,7 @@ export default function BasicFilters({
filter,
activeFilters,
onChangeFilters,
config,
})
) : (
<SearchFilter

View File

@ -2,6 +2,7 @@ import { PlusOutlined } from '@ant-design/icons';
import { Dropdown } from 'antd';
import React, { useState } from 'react';
import styled from 'styled-components';
import { useAppConfig } from '@src/app/useAppConfig';
import { FacetFilterInput, FacetMetadata } from '../../../types.generated';
import MoreFilterOption from './MoreFilterOption';
import { getNumActiveFiltersForGroupOfFilters } from './utils';
@ -39,6 +40,7 @@ export default function MoreFilters({ filters, activeFilters, onChangeFilters }:
const [isMenuOpen, setIsMenuOpen] = useState(false);
const numActiveFilters = getNumActiveFiltersForGroupOfFilters(activeFilters, filters);
const filterRendererRegistry = useFilterRendererRegistry();
const { config } = useAppConfig();
function updateFiltersAndClose(newFilters: FacetFilterInput[]) {
onChangeFilters(newFilters);
@ -62,6 +64,7 @@ export default function MoreFilters({ filters, activeFilters, onChangeFilters }:
filter,
activeFilters,
onChangeFilters: updateFiltersAndClose,
config,
})
) : (
<MoreFilterOption

View File

@ -0,0 +1,72 @@
import React from 'react';
import { FilterScenarioType } from '../types';
import { BooleanSimpleSearchFilter } from '../shared/BooleanSimpleSearchFilter';
import BooleanMoreFilter from '../shared/BooleanMoreFilter';
import { FacetFilterInput, FacetMetadata, FacetFilter } from '../../../../../types.generated';
import BooleanSearchFilter from '../shared/BooleanSearchFilter';
export interface Props {
scenario: FilterScenarioType;
filter: FacetMetadata;
activeFilters: FacetFilterInput[];
onChangeFilters: (newFilters: FacetFilter[]) => void;
icon?: React.ReactNode;
}
export function HasSiblingsFilter({ icon, scenario, filter, activeFilters, onChangeFilters }: Props) {
const isSelected = activeFilters?.find((f) => f.field === 'hasSiblings')?.values?.includes('true');
const toggleFilter = () => {
let newFilters;
if (isSelected) {
newFilters = activeFilters.filter((f) => f.field !== 'hasSiblings');
} else {
newFilters = [...activeFilters, { field: 'hasSiblings', values: ['true'] }];
}
onChangeFilters(newFilters);
};
const aggregateCount = filter.aggregations.find((agg) => agg.value === 'true')?.count;
if (!aggregateCount) {
return null;
}
return (
<>
{scenario === FilterScenarioType.SEARCH_V1 && (
<BooleanSimpleSearchFilter
title="Siblings"
option="Has siblings"
isSelected={isSelected || false}
onSelect={toggleFilter}
defaultDisplayFilters
count={aggregateCount}
key="hasSiblings"
/>
)}
{scenario === FilterScenarioType.SEARCH_V2_PRIMARY && (
<BooleanSearchFilter
icon={icon}
title="Siblings"
option="Has siblings"
initialSelected={isSelected || false}
onUpdate={toggleFilter}
count={aggregateCount}
key="hasSiblings"
/>
)}
{scenario === FilterScenarioType.SEARCH_V2_SECONDARY && (
<BooleanMoreFilter
icon={icon}
title="Siblings"
option="Has siblings"
initialSelected={isSelected || false}
onUpdate={toggleFilter}
count={aggregateCount}
key="hasSiblings"
/>
)}
</>
);
}

View File

@ -0,0 +1,25 @@
import React from 'react';
import { BuildOutlined } from '@ant-design/icons';
import { FilterRenderer } from '../FilterRenderer';
import { FilterRenderProps } from '../types';
import { HasSiblingsFilter } from './HasSiblingsFilter';
export class HasSiblingsRenderer implements FilterRenderer {
field = 'hasSiblings';
render = (props: FilterRenderProps) => {
if (!props.config?.featureFlags?.showHasSiblingsFilter) {
return <></>;
}
return <HasSiblingsFilter {...props} icon={this.icon()} />;
};
icon = () => <BuildOutlined />;
valueLabel = (value: string) => {
if (value === 'true') {
return <>Has Siblings</>;
}
return <>Has No Siblings</>;
};
}

View File

@ -1,4 +1,4 @@
import { FacetFilter, FacetFilterInput, FacetMetadata } from '../../../../types.generated';
import { AppConfig, FacetFilter, FacetFilterInput, FacetMetadata } from '../../../../types.generated';
/**
* The scenario in which filter rendering is required.
@ -25,5 +25,6 @@ export interface FilterRenderProps {
scenario: FilterScenarioType;
filter: FacetMetadata;
activeFilters: FacetFilterInput[];
config?: AppConfig;
onChangeFilters: (newFilters: FacetFilter[]) => void;
}

View File

@ -1,10 +1,11 @@
import FilterRendererRegistry from './FilterRendererRegistry';
import { HasActiveIncidentsRenderer } from './incident/HasActiveIncidentsRenderer';
import { HasSiblingsRenderer } from './siblings/HasSiblingsRenderer';
/**
* Configure the render registry.
*/
const RENDERERS = [new HasActiveIncidentsRenderer()];
const RENDERERS = [new HasActiveIncidentsRenderer(), new HasSiblingsRenderer()];
const REGISTRY = new FilterRendererRegistry();
RENDERERS.forEach((renderer) => REGISTRY.register(renderer));

View File

@ -11,6 +11,7 @@ import {
ENTITY_SUB_TYPE_FILTER_NAME,
DEGREE_FILTER_NAME,
} from './utils/constants';
import { useAppConfig } from '../useAppConfig';
import { SCHEMA_FIELD_ALIASES_FILTER_NAME } from '../search/utils/constants';
const TOP_FILTERS = ['degree', ENTITY_FILTER_NAME, 'platform', 'tags', 'glossaryTerms', 'domains', 'owners'];
@ -30,6 +31,7 @@ interface Props {
}
export const SimpleSearchFilters = ({ facets, selectedFilters, onFilterSelect, loading }: Props) => {
const { config } = useAppConfig();
const [cachedProps, setCachedProps] = useState<{
facets: Array<FacetMetadata>;
selectedFilters: Array<FacetFilterInput>;
@ -87,6 +89,7 @@ export const SimpleSearchFilters = ({ facets, selectedFilters, onFilterSelect, l
filter: facet,
activeFilters: selectedFilters,
onChangeFilters: onFilterSelect,
config,
})
) : (
<SimpleSearchFilter

View File

@ -2,6 +2,7 @@ import { CaretDownFilled } from '@ant-design/icons';
import { Dropdown } from 'antd';
import React, { useState } from 'react';
import styled from 'styled-components';
import { useAppConfig } from '@src/app/useAppConfig';
import { FacetFilterInput, FacetMetadata } from '../../../types.generated';
import MoreFilterOption from './MoreFilterOption';
import { getNumActiveFiltersForGroupOfFilters } from './utils';
@ -34,6 +35,7 @@ export default function MoreFilters({ filters, filterPredicates, activeFilters,
const [isMenuOpen, setIsMenuOpen] = useState(false);
const numActiveFilters = getNumActiveFiltersForGroupOfFilters(activeFilters, filters);
const filterRendererRegistry = useFilterRendererRegistry();
const { config } = useAppConfig();
function updateFiltersAndClose(newFilters: FacetFilterInput[]) {
onChangeFilters(newFilters);
@ -57,6 +59,7 @@ export default function MoreFilters({ filters, filterPredicates, activeFilters,
filter,
activeFilters,
onChangeFilters: updateFiltersAndClose,
config,
})
) : (
<MoreFilterOption

View File

@ -2,6 +2,7 @@ import React from 'react';
import styled from 'styled-components';
import { Divider } from 'antd';
import { SlidersOutlined } from '@ant-design/icons';
import { useAppConfig } from '@src/app/useAppConfig';
import { FacetFilterInput, FacetMetadata } from '../../../types.generated';
import { useUserContext } from '../../context/useUserContext';
import { ORIGIN_FILTER_NAME, UnionType } from '../utils/constants';
@ -68,6 +69,7 @@ export default function SearchFilterOptions({
// If we move view select down, then move this down into a sibling component.
const userContext = useUserContext();
const filterRendererRegistry = useFilterRendererRegistry();
const { config } = useAppConfig();
const fieldsWithCustomRenderers = Array.from(filterRendererRegistry.fieldNameToRenderer.keys());
const selectedViewUrn = userContext?.localState?.selectedViewUrn;
const showSaveViewButton = activeFilters?.length > 0 && selectedViewUrn === undefined;
@ -131,6 +133,7 @@ export default function SearchFilterOptions({
filter,
activeFilters,
onChangeFilters,
config,
})
) : (
<SearchFilter

View File

@ -14,6 +14,7 @@ import {
TagOutlined,
UserOutlined,
WarningOutlined,
BuildOutlined,
} from '@ant-design/icons';
import { BookmarkSimple, Globe } from '@phosphor-icons/react';
import { EntityType } from '../../../../types.generated';
@ -42,6 +43,7 @@ import {
STRUCTURED_PROPERTIES_FILTER_NAME,
TAGS_FILTER_NAME,
TYPE_NAMES_FILTER_NAME,
HAS_SIBLINGS_FILTER_NAME,
} from '../../utils/constants';
import { FieldType, FilterField } from '../types';
@ -201,6 +203,13 @@ export const STRUCTURED_PROPERTY_FILTER: FilterField = {
icon: <Icon component={TableIcon} />,
};
export const HAS_SIBLINGS_FILTER: FilterField = {
field: HAS_SIBLINGS_FILTER_NAME,
displayName: FIELD_TO_LABEL[HAS_SIBLINGS_FILTER_NAME],
type: FieldType.BOOLEAN,
icon: <BuildOutlined />,
};
const DAY_IN_MILLIS = 24 * 60 * 60 * 1000;
export const LAST_MODIFIED_FILTER: FilterField = {
@ -272,6 +281,7 @@ export const DEFAULT_FILTER_FIELDS: FilterField[] = [
HAS_FAILING_ASSERTIONS_FILTER,
ORIGIN_FILTER,
DATA_PLATFORM_INSTANCE_FILTER,
HAS_SIBLINGS_FILTER,
];
export const VIEW_BUILDER_FIELDS: FilterField[] = [

View File

@ -1,5 +1,10 @@
import { HasFailingAssertionsRenderer } from './assertion/HasFailingAssertionsRenderer';
import { FilterRenderer } from './FilterRenderer';
import { HasActiveIncidentsRenderer } from './incident/HasActiveIncidentsRenderer';
import { HasSiblingsRenderer } from './siblings/HasSiblingsRenderer';
export const renderers: Array<FilterRenderer> = [new HasFailingAssertionsRenderer(), new HasActiveIncidentsRenderer()];
export const renderers: Array<FilterRenderer> = [
new HasFailingAssertionsRenderer(),
new HasActiveIncidentsRenderer(),
new HasSiblingsRenderer(),
];

View File

@ -0,0 +1,72 @@
import React from 'react';
import { FilterScenarioType } from '../types';
import { BooleanSimpleSearchFilter } from '../shared/BooleanSimpleSearchFilter';
import BooleanMoreFilter from '../shared/BooleanMoreFilter';
import { FacetFilterInput, FacetMetadata, FacetFilter } from '../../../../../types.generated';
import BooleanSearchFilter from '../shared/BooleanSearchFilter';
export interface Props {
scenario: FilterScenarioType;
filter: FacetMetadata;
activeFilters: FacetFilterInput[];
onChangeFilters: (newFilters: FacetFilter[]) => void;
icon?: React.ReactNode;
}
export function HasSiblingsFilter({ icon, scenario, filter, activeFilters, onChangeFilters }: Props) {
const isSelected = activeFilters?.find((f) => f.field === 'hasSiblings')?.values?.includes('true');
const toggleFilter = () => {
let newFilters;
if (isSelected) {
newFilters = activeFilters.filter((f) => f.field !== 'hasSiblings');
} else {
newFilters = [...activeFilters, { field: 'hasSiblings', values: ['true'] }];
}
onChangeFilters(newFilters);
};
const aggregateCount = filter.aggregations.find((agg) => agg.value === 'true')?.count;
if (!aggregateCount) {
return null;
}
return (
<>
{scenario === FilterScenarioType.SEARCH_V1 && (
<BooleanSimpleSearchFilter
title="Siblings"
option="Has siblings"
isSelected={isSelected || false}
onSelect={toggleFilter}
defaultDisplayFilters
count={aggregateCount}
key="hasSiblings"
/>
)}
{scenario === FilterScenarioType.SEARCH_V2_PRIMARY && (
<BooleanSearchFilter
icon={icon}
title="Siblings"
option="Has siblings"
initialSelected={isSelected || false}
onUpdate={toggleFilter}
count={aggregateCount}
key="hasSiblings"
/>
)}
{scenario === FilterScenarioType.SEARCH_V2_SECONDARY && (
<BooleanMoreFilter
icon={icon}
title="Siblings"
option="Has siblings"
initialSelected={isSelected || false}
onUpdate={toggleFilter}
count={aggregateCount}
key="hasSiblings"
/>
)}
</>
);
}

View File

@ -0,0 +1,25 @@
import React from 'react';
import { BuildOutlined } from '@ant-design/icons';
import { FilterRenderer } from '../FilterRenderer';
import { FilterRenderProps } from '../types';
import { HasSiblingsFilter } from './HasSiblingsFilter';
export class HasSiblingsRenderer implements FilterRenderer {
field = 'hasSiblings';
render = (props: FilterRenderProps) => {
if (!props.config?.featureFlags?.showHasSiblingsFilter) {
return <></>;
}
return <HasSiblingsFilter {...props} icon={this.icon()} />;
};
icon = () => <BuildOutlined />;
valueLabel = (value: string) => {
if (value === 'true') {
return <>Has Siblings</>;
}
return <>Has No Siblings</>;
};
}

View File

@ -1,4 +1,4 @@
import { FacetFilter, FacetFilterInput, FacetMetadata } from '../../../../types.generated';
import { AppConfig, FacetFilter, FacetFilterInput, FacetMetadata } from '../../../../types.generated';
/**
* The scenario in which filter rendering is required.
@ -25,5 +25,6 @@ export interface FilterRenderProps {
scenario: FilterScenarioType;
filter: FacetMetadata;
activeFilters: FacetFilterInput[];
config?: AppConfig;
onChangeFilters: (newFilters: FacetFilter[]) => void;
}

View File

@ -35,6 +35,7 @@ export const DEGREE_FILTER_NAME = 'degree';
export const BROWSE_PATH_V2_FILTER_NAME = 'browsePathV2';
export const HAS_ACTIVE_INCIDENTS_FILTER_NAME = 'hasActiveIncidents';
export const HAS_FAILING_ASSERTIONS_FILTER_NAME = 'hasFailingAssertions';
export const HAS_SIBLINGS_FILTER_NAME = 'hasSiblings';
export const CHART_TYPE_FILTER_NAME = 'type';
export const LAST_MODIFIED_FILTER_NAME = 'lastModifiedAt';
export const STRUCTURED_PROPERTIES_FILTER_NAME = 'structuredProperties.';
@ -114,6 +115,7 @@ export const FIELD_TO_LABEL = {
platformInstance: 'Platform Instance',
hasActiveIncidents: 'Has Active Incidents',
hasFailingAssertions: 'Has Failing Assertions',
hasSiblings: 'Has Siblings',
[BROWSE_PATH_V2_FILTER_NAME]: 'Path',
[LAST_MODIFIED_FILTER_NAME]: 'Last Modified (In Source)',
[STRUCTURED_PROPERTIES_FILTER_NAME]: 'Structured Property',

View File

@ -67,6 +67,7 @@ export const DEFAULT_APP_CONFIG = {
showNavBarRedesign: false,
showAutoCompleteResults: false,
entityVersioningEnabled: false,
showHasSiblingsFilter: false,
showSearchBarAutocompleteRedesign: false,
},
chromeExtensionConfig: {

View File

@ -83,6 +83,7 @@ query appConfig {
showNavBarRedesign
showAutoCompleteResults
entityVersioningEnabled
showHasSiblingsFilter
showSearchBarAutocompleteRedesign
}
chromeExtensionConfig {

View File

@ -22,6 +22,8 @@ record Siblings {
"fieldName": "siblings",
"fieldType": "URN",
"queryByDefault": false,
"hasValuesFieldName": "hasSiblings",
"addHasValuesToFilters": true,
}
}
siblings: array[Urn]

View File

@ -36,5 +36,6 @@ public class FeatureFlags {
private boolean showAutoCompleteResults = false;
private boolean dataProcessInstanceEntityEnabled = true;
private boolean entityVersioning = false;
private boolean showHasSiblingsFilter = false;
private boolean showSearchBarAutocompleteRedesign = false;
}

View File

@ -506,6 +506,7 @@ featureFlags:
showNavBarRedesign: ${SHOW_NAV_BAR_REDESIGN:true} # If turned on, show the newly designed nav bar in the V2 experience
showAutoCompleteResults: ${SHOW_AUTO_COMPLETE_RESULTS:true} # If turned on, show the auto complete results in the search bar
entityVersioning: ${ENTITY_VERSIONING_ENABLED:false} # Enables entity versioning APIs, validators, and side effects
showHasSiblingsFilter: ${SHOW_HAS_SIBLINGS_FILTER:false} # If turned on, the "has siblings" filter will be visible in search results page
showSearchBarAutocompleteRedesign: ${SHOW_SEARCH_BAR_AUTOCOMPLETE_REDESIGN:false} # If turned on, show the redesigned search bar's autocomplete
entityChangeEvents: