Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@ private SearchUtils() {}
EntityType.DATA_PRODUCT,
EntityType.DOMAIN,
EntityType.BUSINESS_ATTRIBUTE,
EntityType.APPLICATION);
EntityType.APPLICATION,
EntityType.STRUCTURED_PROPERTY);

/** Entities that are part of browse by default */
public static final List<EntityType> BROWSE_ENTITY_TYPES =
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
package com.linkedin.datahub.graphql.types.structuredproperty;

import static com.linkedin.metadata.Constants.*;
import static com.linkedin.metadata.Constants.STRUCTURED_PROPERTY_ENTITY_NAME;

import com.google.common.collect.ImmutableSet;
import com.linkedin.common.urn.Urn;
import com.linkedin.common.urn.UrnUtils;
import com.linkedin.datahub.graphql.QueryContext;
import com.linkedin.datahub.graphql.generated.AutoCompleteResults;
import com.linkedin.datahub.graphql.generated.Entity;
import com.linkedin.datahub.graphql.generated.EntityType;
import com.linkedin.datahub.graphql.generated.FacetFilterInput;
import com.linkedin.datahub.graphql.generated.SearchResults;
import com.linkedin.datahub.graphql.generated.StructuredPropertyEntity;
import com.linkedin.datahub.graphql.types.SearchableEntityType;
import com.linkedin.datahub.graphql.types.mappers.AutoCompleteResultsMapper;
import com.linkedin.entity.EntityResponse;
import com.linkedin.entity.client.EntityClient;
import com.linkedin.metadata.query.AutoCompleteResult;
import com.linkedin.metadata.query.filter.Filter;
import graphql.execution.DataFetcherResult;
import java.util.ArrayList;
import java.util.HashSet;
Expand All @@ -20,11 +28,14 @@
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.NotImplementedException;

@RequiredArgsConstructor
public class StructuredPropertyType
implements com.linkedin.datahub.graphql.types.EntityType<StructuredPropertyEntity, String> {
implements com.linkedin.datahub.graphql.types.EntityType<StructuredPropertyEntity, String>,
SearchableEntityType<StructuredPropertyEntity, String> {

public static final Set<String> ASPECTS_TO_FETCH =
ImmutableSet.of(
Expand Down Expand Up @@ -77,4 +88,30 @@ public List<DataFetcherResult<StructuredPropertyEntity>> batchLoad(
throw new RuntimeException("Failed to batch load Queries", e);
}
}

@Override
public AutoCompleteResults autoComplete(
@Nonnull String query,
@Nullable String field,
@Nullable Filter filters,
@Nullable Integer limit,
@Nonnull final QueryContext context)
throws Exception {
final AutoCompleteResult result =
_entityClient.autoComplete(
context.getOperationContext(), STRUCTURED_PROPERTY_ENTITY_NAME, query, filters, limit);
return AutoCompleteResultsMapper.map(context, result);
}

@Override
public SearchResults search(
@Nonnull String query,
@Nullable List<FacetFilterInput> filters,
int start,
@Nullable Integer count,
@Nonnull final QueryContext context)
throws Exception {
throw new NotImplementedException(
"Searchable type (deprecated) not implemented on Structured Property entity type");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function InfiniteScrollList<T>({

return (
<>
{items.length === 0 && !loading && emptyState}
{items.length === 0 && totalItemCount === 0 && !loading && emptyState}
{items.map((item) => renderItem(item))}
{hasMore && <ObserverContainer ref={observerRef} />}
{items.length > 0 && showLoader && loading && <Loader size="sm" alignItems="center" />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useInfiniteScroll } from '@components/components/InfiniteScrollList/use

const flushPromises = () => new Promise(setImmediate);

// Mock IntersectionObserver
const mockIntersectionObserver = vi.fn((callback) => {
const observerInstance = {
observe: (element: Element) => {
Expand Down Expand Up @@ -51,7 +52,7 @@ describe('useInfiniteScroll hook', () => {
expect(result.current.hasMore).toBe(true);
});

it('sets hasMore=false when less than pageSize items are fetched and no totalItemCount provided', async () => {
it('sets hasMore=false when fewer items than pageSize are returned', async () => {
const dataBatches = [[1, 2]];
const fetchData = createFetchDataMock(dataBatches);

Expand Down Expand Up @@ -85,25 +86,95 @@ describe('useInfiniteScroll hook', () => {
expect(result.current.hasMore).toBe(false);
});

it('does not load more data if already loading or hasMore is false', async () => {
it('does not fetch more when already loading or hasMore is false', async () => {
const dataBatches = [[1, 2, 3]];
const fetchData = createFetchDataMock(dataBatches);

const { result, waitForNextUpdate } = renderHook(() => useInfiniteScroll({ fetchData, pageSize }));

await waitForNextUpdate();

expect(result.current.loading).toBe(false);
expect(result.current.hasMore).toBe(true);

// set hasMore=false to trigger early return
act(() => {
result.current.observerRef.current = document.createElement('div');
result.current.hasMore = false as any;
});

await flushPromises();

expect(fetchData).toHaveBeenCalledTimes(1);
expect(fetchData).toHaveBeenCalledTimes(1);
});

it('does not crash if fetchData returns non-array', async () => {
const fetchData = vi.fn(() => Promise.resolve(null as any));
const { result, waitForNextUpdate } = renderHook(() => useInfiniteScroll({ fetchData, pageSize }));

await waitForNextUpdate();

expect(result.current.items).toEqual([]);
expect(result.current.hasMore).toBe(true);
});

it('prepends a new item correctly and prevents duplicates', async () => {
const dataBatches = [[1, 2, 3]];
const fetchData = createFetchDataMock(dataBatches);

const { result, waitForNextUpdate } = renderHook(() => useInfiniteScroll({ fetchData, pageSize }));

// Prepend before initial load
act(() => {
result.current.prependItem(0);
});

await waitForNextUpdate();

expect(result.current.items).toEqual([0, 1, 2, 3]);

// Attempt to prepend the same item again
act(() => {
result.current.prependItem(0);
});

expect(result.current.items).toEqual([0, 1, 2, 3]);
});

it('does not prepend null or undefined', async () => {
const fetchData = vi.fn(() => Promise.resolve([1, 2]));
const { result, waitForNextUpdate } = renderHook(() => useInfiniteScroll({ fetchData, pageSize }));

await waitForNextUpdate();

act(() => {
result.current.prependItem(undefined as any);
result.current.prependItem(null as any);
});

expect(result.current.items).toEqual([1, 2]);
});

it('removes items correctly using predicate', async () => {
const fetchData = vi.fn(() => Promise.resolve([1, 2, 3]));
const { result, waitForNextUpdate } = renderHook(() => useInfiniteScroll({ fetchData, pageSize }));

await waitForNextUpdate();

act(() => {
result.current.removeItem((item) => item === 2);
});

expect(result.current.items).toEqual([1, 3]);
});

it('updates items correctly using predicate', async () => {
const fetchData = vi.fn(() => Promise.resolve([1, 2, 3]));
const { result, waitForNextUpdate } = renderHook(() => useInfiniteScroll({ fetchData, pageSize }));

await waitForNextUpdate();

act(() => {
result.current.updateItem(99, (item) => item === 2);
});

expect(result.current.items).toEqual([1, 99, 3]);
});

it('cleans up IntersectionObserver on unmount', async () => {
Expand Down Expand Up @@ -135,8 +206,6 @@ describe('useInfiniteScroll hook', () => {

await waitForNextUpdate();

expect(observeMock).toHaveBeenCalled();

unmount();

expect(unobserveMock).toHaveBeenCalled();
Expand All @@ -151,4 +220,25 @@ describe('useInfiniteScroll hook', () => {
expect(typeof result.current.observerRef).toBe('object');
expect(result.current.observerRef.current).toBeNull();
});

it('resets items and startIndex on resetTrigger', async () => {
const dataBatches = [[1, 2, 3]];
const fetchData = createFetchDataMock(dataBatches);

let trigger = 1;
const { result, waitForNextUpdate, rerender } = renderHook(
({ resetTrigger }) => useInfiniteScroll({ fetchData, pageSize, resetTrigger }),
{ initialProps: { resetTrigger: trigger } },
);

await waitForNextUpdate();
expect(result.current.items).toEqual([1, 2, 3]);

// Trigger reset
trigger = 2;
rerender({ resetTrigger: trigger });
expect(result.current.items).toEqual([]);
expect(result.current.hasMore).toBe(true);
expect(result.current.loading).toBe(false);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ import styled from 'styled-components';

export const ObserverContainer = styled.div`
height: 1px;
margintop: 1px;
margin-top: 1px;
`;
Loading
Loading