fix(ui): Backfill proposals changes (#13350)

This commit is contained in:
Saketh Varma 2025-04-30 14:15:25 -05:00 committed by GitHub
parent afa9209f5d
commit 3154289dd8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 380 additions and 18 deletions

View File

@ -1,3 +1,4 @@
import { LoadingOutlined } from '@ant-design/icons';
import { Dropdown, Text } from '@components';
import { isEqual } from 'lodash';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
@ -23,6 +24,9 @@ import SelectLabelRenderer from '@components/components/Select/private/SelectLab
import useSelectDropdown from '@components/components/Select/private/hooks/useSelectDropdown';
import { SelectOption, SelectProps } from '@components/components/Select/types';
import NoResultsFoundPlaceholder from '@app/searchV2/searchBarV2/components/NoResultsFoundPlaceholder';
import { LoadingWrapper } from '@src/app/entityV2/shared/tabs/Incident/AcrylComponents/styledComponents';
export const selectDefaults: SelectProps = {
options: [],
label: '',
@ -75,6 +79,7 @@ export const SimpleSelect = ({
position,
applyHoverWidth,
ignoreMaxHeight = selectDefaults.ignoreMaxHeight,
isLoading = false,
...props
}: SelectProps) => {
const [searchQuery, setSearchQuery] = useState('');
@ -183,6 +188,13 @@ export const SimpleSelect = ({
size={size}
/>
)}
{isLoading ? (
<LoadingWrapper>
<LoadingOutlined />
</LoadingWrapper>
) : (
!filteredOptions.length && <NoResultsFoundPlaceholder />
)}
<OptionList style={optionListStyle} data-testid={optionListTestId}>
{showSelectAll && isMultiSelect && (
<DropdownSelectAllOption

View File

@ -55,6 +55,7 @@ export interface SelectProps<OptionType extends SelectOption = SelectOption> {
position?: OptionPosition;
applyHoverWidth?: boolean;
ignoreMaxHeight?: boolean;
isLoading?: boolean;
}
export interface SelectStyleProps {

View File

@ -15,13 +15,16 @@ const CardSkeleton = styled(Skeleton.Input)`
}
`;
export default function SearchFiltersLoadingSection() {
interface Props {
noOfLoadingSkeletons?: number;
}
export default function SearchFiltersLoadingSection({ noOfLoadingSkeletons = 4 }: Props) {
return (
<Container>
<CardSkeleton active size="default" />
<CardSkeleton active size="default" />
<CardSkeleton active size="default" />
<CardSkeleton active size="default" />
{Array.from({ length: noOfLoadingSkeletons }).map(() => (
<CardSkeleton active size="default" />
))}
</Container>
);
}

View File

@ -8,6 +8,7 @@ import {
PlatformIcon,
canCreateViewFromFilters,
combineAggregations,
deduplicateAggregations,
filterEmptyAggregations,
filterOptionsWithSearch,
getFilterDisplayName,
@ -26,7 +27,7 @@ import { dataPlatform, dataPlatformInstance, dataset1, glossaryTerm1, user1 } fr
import { DATE_TYPE_URN } from '@src/app/shared/constants';
import { getTestEntityRegistry } from '@utils/test-utils/TestPageContainer';
import { EntityType } from '@types';
import { AggregationMetadata, EntityType } from '@types';
describe('filter utils - getNewFilters', () => {
it('should get the correct list of filters when adding filters where the filter field did not already exist', () => {
@ -358,6 +359,81 @@ describe('filter utils - filterEmptyAggregations', () => {
});
});
describe('deduplicateAggregations() deduplicateAggregations method', () => {
// Happy Path Tests
describe('Happy Paths', () => {
it('should return an empty array when both baseAggs and secondaryAggs are empty', () => {
const baseAggs: AggregationMetadata[] = [];
const secondaryAggs: AggregationMetadata[] = [];
const result = deduplicateAggregations(baseAggs, secondaryAggs);
expect(result).toEqual([]);
});
it('should return secondaryAggs when baseAggs is empty', () => {
const baseAggs: AggregationMetadata[] = [];
const secondaryAggs: AggregationMetadata[] = [
{ count: 0, value: 'value1' },
{ count: 0, value: 'value2' },
];
const result = deduplicateAggregations(baseAggs, secondaryAggs);
expect(result).toEqual(secondaryAggs);
});
it('should return an empty array when all secondaryAggs are in baseAggs', () => {
const baseAggs: AggregationMetadata[] = [
{ count: 0, value: 'value1' },
{ count: 0, value: 'value2' },
];
const secondaryAggs: AggregationMetadata[] = [
{ count: 1, value: 'value1' },
{ count: 2, value: 'value2' },
];
const result = deduplicateAggregations(baseAggs, secondaryAggs);
expect(result).toEqual([]);
});
it('should return only the unique secondaryAggs not present in baseAggs', () => {
const baseAggs: AggregationMetadata[] = [{ count: 0, value: 'value1' }];
const secondaryAggs: AggregationMetadata[] = [
{ count: 0, value: 'value1' },
{ count: 2, value: 'value2' },
];
const result = deduplicateAggregations(baseAggs, secondaryAggs);
expect(result).toEqual([{ count: 2, value: 'value2' }]);
});
});
// Edge Case Tests
describe('Edge Cases', () => {
it('should handle case sensitivity correctly', () => {
const baseAggs: AggregationMetadata[] = [{ count: 0, value: 'Value1' }];
const secondaryAggs: AggregationMetadata[] = [
{ count: 0, value: 'value1' },
{ count: 0, value: 'Value2' },
];
const result = deduplicateAggregations(baseAggs, secondaryAggs);
expect(result).toEqual([
{ count: 0, value: 'value1' },
{ count: 0, value: 'Value2' },
]);
});
it('should handle large arrays efficiently', () => {
const baseAggs: AggregationMetadata[] = Array.from({ length: 1000 }, (_, i) => ({
count: 0,
value: `value${i}`,
}));
const secondaryAggs: AggregationMetadata[] = Array.from({ length: 2000 }, (_, i) => ({
count: 0,
value: `value${i}`,
}));
const result = deduplicateAggregations(baseAggs, secondaryAggs);
expect(result.length).toBe(1000);
expect(result[0]).toEqual({ count: 0, value: 'value1000' });
});
});
});
describe('filter utils - getFilterOptions', () => {
const originalAggs = [
{ value: 'aditya', count: 10 },

View File

@ -1,16 +1,22 @@
import { useEffect, useMemo } from 'react';
import { filterEmptyAggregations, getNewFilters, getNumActiveFiltersForFilter } from '@app/searchV2/filters/utils';
import {
deduplicateAggregations,
filterEmptyAggregations,
getNewFilters,
getNumActiveFiltersForFilter,
} from '@app/searchV2/filters/utils';
import useGetSearchQueryInputs from '@app/searchV2/useGetSearchQueryInputs';
import { ENTITY_FILTER_NAME } from '@app/searchV2/utils/constants';
import { useAggregateAcrossEntitiesLazyQuery } from '@src/graphql/search.generated';
import { FacetFilterInput, FacetMetadata } from '@types';
import { EntityType, FacetFilterInput, FacetMetadata } from '@types';
interface Props {
filter: FacetMetadata;
activeFilters: FacetFilterInput[];
onChangeFilters: (newFilters: FacetFilterInput[]) => void;
aggregationsEntityTypes?: Array<EntityType>;
shouldUseAggregationsFromFilter?: boolean;
}
@ -18,6 +24,7 @@ export default function useSearchFilterDropdown({
filter,
activeFilters,
onChangeFilters,
aggregationsEntityTypes,
shouldUseAggregationsFromFilter,
}: Props) {
const numActiveFilters = getNumActiveFiltersForFilter(activeFilters, filter);
@ -36,7 +43,7 @@ export default function useSearchFilterDropdown({
aggregateAcrossEntities({
variables: {
input: {
types: filter.field === ENTITY_FILTER_NAME ? null : entityFilters,
types: aggregationsEntityTypes || (filter.field === ENTITY_FILTER_NAME ? null : entityFilters),
query,
orFilters,
viewUrn,
@ -45,11 +52,27 @@ export default function useSearchFilterDropdown({
},
});
}
}, [aggregateAcrossEntities, entityFilters, filter.field, orFilters, query, viewUrn, shouldFetchAggregations]);
}, [
aggregateAcrossEntities,
entityFilters,
filter.field,
orFilters,
query,
viewUrn,
shouldFetchAggregations,
aggregationsEntityTypes,
]);
const fetchedAggregations =
data?.aggregateAcrossEntities?.facets?.find((f) => f.field === filter.field)?.aggregations || [];
const searchAggregations = filter.aggregations;
const activeAggregations = searchAggregations.filter((agg) =>
activeFilters.find((f) => f.values?.includes(agg.value) || f.value === agg.value),
);
const aggregations = shouldFetchAggregations
? data?.aggregateAcrossEntities?.facets?.[0]?.aggregations
: filter.aggregations;
? [...fetchedAggregations, ...deduplicateAggregations(fetchedAggregations, activeAggregations)]
: searchAggregations;
const finalAggregations = filterEmptyAggregations(aggregations || [], activeFilters);

View File

@ -293,6 +293,12 @@ export function filterEmptyAggregations(aggregations: AggregationMetadata[], act
});
}
// Filters out values from the secondary aggregations that are present in the base aggregations.
export const deduplicateAggregations = (baseAggs: AggregationMetadata[], secondaryAggs: AggregationMetadata[]) => {
const baseValues = baseAggs.map((agg) => agg.value);
return secondaryAggs.filter((agg) => !baseValues.includes(agg.value));
};
export function sortFacets(facetA: FacetMetadata, facetB: FacetMetadata, sortedFacetFields: string[]) {
if (sortedFacetFields.indexOf(facetA.field) === -1) return 1;
if (sortedFacetFields.indexOf(facetB.field) === -1) return -1;

View File

@ -12,6 +12,7 @@ import {
} from '@app/searchV2/filters/value/utils';
import { ENTITY_SUB_TYPE_FILTER_NAME, FILTER_DELIMITER } from '@app/searchV2/utils/constants';
import { useEntityRegistry } from '@app/useEntityRegistry';
import { EntityType } from '@src/types.generated';
interface Props {
field: FilterField;
@ -23,6 +24,7 @@ interface Props {
includeSubTypes?: boolean;
includeCount?: boolean;
className?: string;
aggregationsEntityTypes?: Array<EntityType>;
}
export default function EntityTypeMenu({
@ -35,6 +37,7 @@ export default function EntityTypeMenu({
includeSubTypes = true,
includeCount = false,
className,
aggregationsEntityTypes,
}: Props) {
const entityRegistry = useEntityRegistry();
const { displayName } = field;
@ -43,7 +46,12 @@ export default function EntityTypeMenu({
const [searchQuery, setSearchQuery] = useState<string | undefined>(undefined);
// Here we optionally load the aggregation options, which are the options that are displayed by default.
const { options: aggOptions, loading: aggLoading } = useLoadAggregationOptions(field, true, includeCount);
const { options: aggOptions, loading: aggLoading } = useLoadAggregationOptions({
field,
visible: true,
includeCounts: includeCount,
aggregationsEntityTypes,
});
const allOptions = [...defaultOptions, ...deduplicateOptions(defaultOptions, aggOptions)];

View File

@ -11,6 +11,7 @@ import {
useLoadAggregationOptions,
} from '@app/searchV2/filters/value/utils';
import { useEntityRegistry } from '@app/useEntityRegistry';
import { EntityType } from '@src/types.generated';
interface Props {
field: FilterField;
@ -21,6 +22,7 @@ interface Props {
type?: 'card' | 'default';
includeCount?: boolean;
className?: string;
aggregationsEntityTypes?: Array<EntityType>;
}
export default function EnumValueMenu({
@ -32,6 +34,7 @@ export default function EnumValueMenu({
onChangeValues,
onApply,
className,
aggregationsEntityTypes,
}: Props) {
const entityRegistry = useEntityRegistry();
const displayName = useFilterDisplayName(field);
@ -40,7 +43,12 @@ export default function EnumValueMenu({
const [searchQuery, setSearchQuery] = useState<string | undefined>(undefined);
// Here we optionally load the aggregation options, which are the options that are displayed by default.
const { options: aggOptions, loading: aggLoading } = useLoadAggregationOptions(field, true, includeCount);
const { options: aggOptions, loading: aggLoading } = useLoadAggregationOptions({
field,
visible: true,
includeCounts: includeCount,
aggregationsEntityTypes,
});
const allOptions = [...defaultOptions, ...deduplicateOptions(defaultOptions, aggOptions)];

View File

@ -10,7 +10,7 @@ import EntityValueMenu from '@app/searchV2/filters/value/EntityValueMenu';
import EnumValueMenu from '@app/searchV2/filters/value/EnumValueMenu';
import TextValueMenu from '@app/searchV2/filters/value/TextValueMenu';
import TimeBucketMenu from '@app/searchV2/filters/value/TimeBucketMenu';
import { FacetFilterInput } from '@src/types.generated';
import { EntityType, FacetFilterInput } from '@src/types.generated';
interface Props {
field: FilterField;
@ -22,6 +22,7 @@ interface Props {
includeCount?: boolean;
className?: string;
manuallyUpdateFilters?: (newValues: FacetFilterInput[]) => void;
aggregationsEntityTypes?: Array<EntityType>;
}
export default function ValueMenu({
@ -34,6 +35,7 @@ export default function ValueMenu({
includeCount,
className,
manuallyUpdateFilters,
aggregationsEntityTypes,
}: Props) {
const [stagedSelectedValues, setStagedSelectedValues] = useState<FilterValue[]>(values || []);
const visibilityRef = useRef<boolean>(visible);
@ -122,6 +124,7 @@ export default function ValueMenu({
className={className}
onChangeValues={setStagedSelectedValues}
onApply={() => onChangeValues(stagedSelectedValues)}
aggregationsEntityTypes={aggregationsEntityTypes}
/>
);
case FieldType.ENUM:
@ -135,6 +138,7 @@ export default function ValueMenu({
className={className}
onChangeValues={setStagedSelectedValues}
onApply={() => onChangeValues(stagedSelectedValues)}
aggregationsEntityTypes={aggregationsEntityTypes}
/>
);
case FieldType.BUCKETED_TIMESTAMP:

View File

@ -4,7 +4,7 @@ import React, { useState } from 'react';
import { FilterField, FilterValue, FilterValueOption } from '@app/searchV2/filters/types';
import ValueMenu from '@app/searchV2/filters/value/ValueMenu';
import { FacetFilterInput } from '@src/types.generated';
import { EntityType, FacetFilterInput } from '@src/types.generated';
interface Props {
field: FilterField;
@ -14,6 +14,7 @@ interface Props {
children?: any;
className?: string;
manuallyUpdateFilters?: (newValues: FacetFilterInput[]) => void;
aggregationsEntityTypes?: Array<EntityType>;
}
export default function ValueSelector({
@ -24,6 +25,7 @@ export default function ValueSelector({
children,
className,
manuallyUpdateFilters,
aggregationsEntityTypes,
}: Props) {
const [isMenuOpen, setIsMenuOpen] = useState(false);
@ -53,6 +55,7 @@ export default function ValueSelector({
includeCount
className={className}
manuallyUpdateFilters={onManuallyUpdateFilters}
aggregationsEntityTypes={aggregationsEntityTypes}
/>
)}
>

View File

@ -48,7 +48,17 @@ export const mapFilterCountsToZero = (options: FilterValueOption[]) => {
*
* TODO: Determine if we need to provide an option context that would help with filtering.
*/
export const useLoadAggregationOptions = (field: FilterField, visible: boolean, includeCounts: boolean) => {
export const useLoadAggregationOptions = ({
field,
visible,
includeCounts,
aggregationsEntityTypes,
}: {
field: FilterField;
visible: boolean;
includeCounts: boolean;
aggregationsEntityTypes?: Array<EntityType>;
}) => {
const { entityFilters, query, orFilters, viewUrn } = useGetSearchQueryInputs(
useMemo(() => [field.field], [field.field]),
);
@ -61,7 +71,7 @@ export const useLoadAggregationOptions = (field: FilterField, visible: boolean,
searchFlags: {
maxAggValues: MAX_AGGREGATION_COUNT,
},
types: field.field === ENTITY_FILTER_NAME ? null : entityFilters,
types: aggregationsEntityTypes || (field.field === ENTITY_FILTER_NAME ? null : entityFilters),
orFilters,
viewUrn,
},

View File

@ -0,0 +1,108 @@
import { Button } from 'antd';
import { CaretDown } from 'phosphor-react';
import React from 'react';
import styled, { CSSProperties } from 'styled-components';
import { Pill } from '@src/alchemy-components';
import { IconWrapper } from '@src/app/searchV2/filters/SearchFilterView';
import { FilterPredicate } from '@src/app/searchV2/filters/types';
import useFilterDropdown from '@src/app/searchV2/filters/useSearchFilterDropdown';
import { getFilterDropdownIcon, useFilterDisplayName } from '@src/app/searchV2/filters/utils';
import ValueSelector from '@src/app/searchV2/filters/value/ValueSelector';
import { formatNumber } from '@src/app/shared/formatNumber';
import { EntityType, FacetFilterInput, FacetMetadata } from '@src/types.generated';
export type FilterLabels = {
[key: string]: {
displayName: string;
icon?: React.ReactElement;
};
};
interface Props {
filter: FacetMetadata;
activeFilters: FacetFilterInput[];
onChangeFilters: (newFilters: FacetFilterInput[]) => void;
filterPredicates: FilterPredicate[];
labelStyle?: CSSProperties;
customFilterLabels?: FilterLabels;
aggregationsEntityTypes?: Array<EntityType>;
}
const FilterLabel = styled(Button)<{ $isActive: boolean }>`
display: flex;
align-items: center;
padding: 8px;
height: 36px;
border-radius: 8px;
background-color: white;
color: #6b7280;
font-size: 14px;
font-weight: 500;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: all 0.2s ease;
gap: 8px;
margin: 0 4px;
border: 1px solid #ebecf0;
&:hover,
&:focus {
color: inherit;
border-color: #ebecf0;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}
${(props) =>
props.$isActive &&
`
color: #374066;
font-weight: 600;
`}
`;
export default function Filter({
filter,
filterPredicates,
activeFilters,
onChangeFilters,
labelStyle,
customFilterLabels,
aggregationsEntityTypes,
}: Props) {
const { finalAggregations, updateFilters, numActiveFilters } = useFilterDropdown({
filter,
activeFilters,
onChangeFilters,
aggregationsEntityTypes,
});
const currentFilterPredicate = filterPredicates?.find((obj) => obj.field.field === filter.field) as FilterPredicate;
// TODO: Have config for the value labels as well
const labelConfig = customFilterLabels?.[filter.field];
const filterIcon = labelConfig ? labelConfig.icon : getFilterDropdownIcon(filter.field);
const entityFilterName = useFilterDisplayName(filter, currentFilterPredicate?.field?.displayName);
const displayName = labelConfig ? labelConfig.displayName : entityFilterName;
return (
<ValueSelector
field={currentFilterPredicate?.field}
values={currentFilterPredicate?.values}
defaultOptions={finalAggregations}
onChangeValues={updateFilters}
aggregationsEntityTypes={aggregationsEntityTypes}
>
<FilterLabel
$isActive={!!numActiveFilters}
style={labelStyle}
data-testid={`filter-dropdown-${displayName?.replace(/\s/g, '-')}`}
>
{filterIcon && <IconWrapper>{filterIcon}</IconWrapper>}
{displayName} {!!numActiveFilters && <Pill size="xs" label={formatNumber(numActiveFilters)} />}
<CaretDown style={{ fontSize: '14px', height: '14px' }} />
</FilterLabel>
</ValueSelector>
);
}

View File

@ -0,0 +1,69 @@
import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import Filter, { FilterLabels } from '@app/sharedV2/filters/Filter';
import FiltersLoadingSection from '@src/app/searchV2/filters/SearchFiltersLoadingSection';
import { FilterPredicate } from '@src/app/searchV2/filters/types';
import { convertToAvailableFilterPredictes } from '@src/app/searchV2/filters/utils';
import { EntityType, FacetFilterInput, FacetMetadata } from '@src/types.generated';
const Section = styled.div<{ removePadding?: boolean }>`
margin-bottom: 10px;
position: relative;
height: 40px;
display: flex;
`;
interface Props {
name: string;
loading: boolean;
availableFilters: FacetMetadata[];
activeFilters: FacetFilterInput[];
onChangeFilters: (newFilters: FacetFilterInput[]) => void;
aggregationsEntityTypes?: Array<EntityType>;
customFilterLabels?: FilterLabels;
noOfLoadingSkeletons?: number;
}
export default function FilterSection({
name,
loading,
availableFilters,
activeFilters,
onChangeFilters,
aggregationsEntityTypes,
customFilterLabels,
noOfLoadingSkeletons,
}: Props) {
const [finalAvailableFilters, setFinalAvailableFilters] = useState(availableFilters);
useEffect(() => {
if (!loading && finalAvailableFilters !== availableFilters) {
setFinalAvailableFilters(availableFilters);
}
}, [availableFilters, loading, finalAvailableFilters]);
const filterPredicates: FilterPredicate[] = convertToAvailableFilterPredictes(
activeFilters,
finalAvailableFilters || [],
);
return (
<Section id={`${name}-filters-section`} data-testid={`${name}-filters-section`}>
{loading && !finalAvailableFilters?.length && (
<FiltersLoadingSection noOfLoadingSkeletons={noOfLoadingSkeletons} />
)}
{finalAvailableFilters?.map((filter) => (
<Filter
key={filter.field}
filter={filter}
activeFilters={activeFilters}
onChangeFilters={onChangeFilters}
filterPredicates={filterPredicates}
customFilterLabels={customFilterLabels}
aggregationsEntityTypes={aggregationsEntityTypes}
/>
))}
</Section>
);
}

View File

@ -0,0 +1,26 @@
import * as QueryString from 'query-string';
import filtersToQueryStringParams from '@app/searchV2/utils/filtersToQueryStringParams';
import { FacetFilterInput } from '@src/types.generated';
export const navigateWithFilters = ({
filters,
history,
location,
}: {
filters?: Array<FacetFilterInput>;
history: any;
location: any;
}) => {
const search = QueryString.stringify(
{
...filtersToQueryStringParams(filters || []),
},
{ arrayFormat: 'comma' },
);
history.replace({
pathname: location.pathname, // Keep the current pathname
search,
});
};

View File

@ -1148,6 +1148,11 @@ fragment facetFields on FacetMetadata {
...parentDomainsFields
}
}
... on Dataset {
platform {
...platformFields
}
}
... on Container {
platform {
...platformFields