mirror of
https://github.com/datahub-project/datahub.git
synced 2025-12-12 18:47:45 +00:00
feat(embedded search results): support custom endpoints in embedded search result (#3986)
This commit is contained in:
parent
9366a47f88
commit
eac3c62667
@ -2,27 +2,59 @@ import React, { useState } from 'react';
|
||||
import * as QueryString from 'query-string';
|
||||
import { useHistory, useLocation, useParams } from 'react-router';
|
||||
import { message } from 'antd';
|
||||
import { ApolloError } from '@apollo/client';
|
||||
|
||||
import { useEntityRegistry } from '../../../../../useEntityRegistry';
|
||||
import { EntityType, FacetFilterInput } from '../../../../../../types.generated';
|
||||
import { EntityType, FacetFilterInput, FacetMetadata, Maybe, Scalars } from '../../../../../../types.generated';
|
||||
import useFilters from '../../../../../search/utils/useFilters';
|
||||
import { ENTITY_FILTER_NAME } from '../../../../../search/utils/constants';
|
||||
import { useGetSearchResultsForMultipleQuery } from '../../../../../../graphql/search.generated';
|
||||
import { SearchCfg } from '../../../../../../conf';
|
||||
import { navigateToEntitySearchUrl } from './navigateToEntitySearchUrl';
|
||||
import { EmbeddedListSearchResults } from './EmbeddedListSearchResults';
|
||||
import EmbeddedListSearchHeader from './EmbeddedListSearchHeader';
|
||||
import { useGetSearchResultsForMultipleQuery } from '../../../../../../graphql/search.generated';
|
||||
import { GetSearchResultsParams, SearchResultInterface } from './types';
|
||||
|
||||
// this extracts the response from useGetSearchResultsForMultipleQuery into a common interface other search endpoints can also produce
|
||||
function useWrappedSearchResults(params: GetSearchResultsParams) {
|
||||
const { data, loading, error } = useGetSearchResultsForMultipleQuery(params);
|
||||
return { data: data?.searchAcrossEntities, loading, error };
|
||||
}
|
||||
|
||||
type SearchPageParams = {
|
||||
type?: string;
|
||||
};
|
||||
|
||||
type SearchResultsInterface = {
|
||||
/** The offset of the result set */
|
||||
start: Scalars['Int'];
|
||||
/** The number of entities included in the result set */
|
||||
count: Scalars['Int'];
|
||||
/** The total number of search results matching the query and filters */
|
||||
total: Scalars['Int'];
|
||||
/** The search result entities */
|
||||
searchResults: Array<SearchResultInterface>;
|
||||
/** Candidate facet aggregations used for search filtering */
|
||||
facets?: Maybe<Array<FacetMetadata>>;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
emptySearchQuery?: string | null;
|
||||
fixedFilter?: FacetFilterInput | null;
|
||||
placeholderText?: string | null;
|
||||
useGetSearchResults?: (params: GetSearchResultsParams) => {
|
||||
data: SearchResultsInterface | undefined | null;
|
||||
loading: boolean;
|
||||
error: ApolloError | undefined;
|
||||
};
|
||||
};
|
||||
|
||||
export const EmbeddedListSearch = ({ emptySearchQuery, fixedFilter, placeholderText }: Props) => {
|
||||
export const EmbeddedListSearch = ({
|
||||
emptySearchQuery,
|
||||
fixedFilter,
|
||||
placeholderText,
|
||||
useGetSearchResults = useWrappedSearchResults,
|
||||
}: Props) => {
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const entityRegistry = useEntityRegistry();
|
||||
@ -42,7 +74,7 @@ export const EmbeddedListSearch = ({ emptySearchQuery, fixedFilter, placeholderT
|
||||
|
||||
const [showFilters, setShowFilters] = useState(false);
|
||||
|
||||
const { data, loading, error } = useGetSearchResultsForMultipleQuery({
|
||||
const { data, loading, error } = useGetSearchResults({
|
||||
variables: {
|
||||
input: {
|
||||
types: entityFilters,
|
||||
@ -99,8 +131,7 @@ export const EmbeddedListSearch = ({ emptySearchQuery, fixedFilter, placeholderT
|
||||
};
|
||||
|
||||
// Filter out the persistent filter values
|
||||
const filteredFilters =
|
||||
data?.searchAcrossEntities?.facets?.filter((facet) => facet.field !== fixedFilter?.field) || [];
|
||||
const filteredFilters = data?.facets?.filter((facet) => facet.field !== fixedFilter?.field) || [];
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -112,7 +143,7 @@ export const EmbeddedListSearch = ({ emptySearchQuery, fixedFilter, placeholderT
|
||||
/>
|
||||
<EmbeddedListSearchResults
|
||||
loading={loading}
|
||||
searchResponse={data?.searchAcrossEntities}
|
||||
searchResponse={data}
|
||||
filters={filteredFilters}
|
||||
selectedFilters={filters}
|
||||
onChangeFilters={onChangeFilters}
|
||||
|
||||
@ -115,6 +115,13 @@ export const EmbeddedListSearchResults = ({
|
||||
entities={
|
||||
searchResponse?.searchResults?.map((searchResult) => searchResult.entity) || []
|
||||
}
|
||||
additionalPropertiesList={
|
||||
searchResponse?.searchResults?.map((searchResult) => ({
|
||||
// when we add impact analysis, we will want to pipe the path to each element to the result this
|
||||
// eslint-disable-next-line @typescript-eslint/dot-notation
|
||||
path: searchResult['path'],
|
||||
})) || []
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -0,0 +1,22 @@
|
||||
import {
|
||||
Entity,
|
||||
MatchedField,
|
||||
Maybe,
|
||||
SearchAcrossEntitiesInput,
|
||||
SearchInsight,
|
||||
} from '../../../../../../types.generated';
|
||||
|
||||
export type GetSearchResultsParams = {
|
||||
variables: {
|
||||
input: SearchAcrossEntitiesInput;
|
||||
};
|
||||
} & Record<string, any>;
|
||||
|
||||
export type SearchResultInterface = {
|
||||
entity: Entity;
|
||||
/** Insights about why the search result was matched */
|
||||
insights?: Maybe<Array<SearchInsight>>;
|
||||
/** Matched field hint */
|
||||
matchedFields: Array<MatchedField>;
|
||||
paths?: Array<Entity>;
|
||||
} & Record<string, any>;
|
||||
@ -11,6 +11,12 @@ export function urlEncodeUrn(urn: string) {
|
||||
);
|
||||
}
|
||||
|
||||
export function getNumberWithOrdinal(n) {
|
||||
const suffixes = ['th', 'st', 'nd', 'rd'];
|
||||
const v = n % 100;
|
||||
return n + (suffixes[(v - 20) % 10] || suffixes[v] || suffixes[0]);
|
||||
}
|
||||
|
||||
export function notEmpty<TValue>(value: TValue | null | undefined): value is TValue {
|
||||
return value !== null && value !== undefined;
|
||||
}
|
||||
|
||||
@ -1,13 +1,15 @@
|
||||
import { Image, Typography } from 'antd';
|
||||
import { Image, Tooltip, Typography } from 'antd';
|
||||
import React, { ReactNode } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
import { GlobalTags, Owner, GlossaryTerms, SearchInsight } from '../../types.generated';
|
||||
import { GlobalTags, Owner, GlossaryTerms, SearchInsight, Entity } from '../../types.generated';
|
||||
import { useEntityRegistry } from '../useEntityRegistry';
|
||||
import AvatarsGroup from '../shared/avatar/AvatarsGroup';
|
||||
import TagTermGroup from '../shared/tags/TagTermGroup';
|
||||
import { ANTD_GRAY } from '../entity/shared/constants';
|
||||
import NoMarkdownViewer from '../entity/shared/components/styled/StripMarkdownText';
|
||||
import { getNumberWithOrdinal } from '../entity/shared/utils';
|
||||
import { useEntityData } from '../entity/shared/EntityContext';
|
||||
|
||||
interface Props {
|
||||
name: string;
|
||||
@ -26,6 +28,9 @@ interface Props {
|
||||
dataTestID?: string;
|
||||
titleSizePx?: number;
|
||||
onClick?: () => void;
|
||||
// this is provided by the impact analysis view. it is used to display
|
||||
// how the listed node is connected to the source node
|
||||
path?: Entity[];
|
||||
}
|
||||
|
||||
const PreviewContainer = styled.div`
|
||||
@ -129,7 +134,11 @@ export default function DefaultPreviewCard({
|
||||
titleSizePx,
|
||||
dataTestID,
|
||||
onClick,
|
||||
path,
|
||||
}: Props) {
|
||||
// sometimes these lists will be rendered inside an entity container (for example, in the case of impact analysis)
|
||||
// in those cases, we may want to enrich the preview w/ context about the container entity
|
||||
const { entityData } = useEntityData();
|
||||
const entityRegistry = useEntityRegistry();
|
||||
const insightViews: Array<ReactNode> = [
|
||||
...(insights?.map((insight) => (
|
||||
@ -153,6 +162,18 @@ export default function DefaultPreviewCard({
|
||||
{platform && <PlatformText>{platform}</PlatformText>}
|
||||
{(logoUrl || logoComponent || platform) && <PlatformDivider />}
|
||||
<PlatformText>{type}</PlatformText>
|
||||
{path && (
|
||||
<span>
|
||||
<PlatformDivider />
|
||||
<Tooltip
|
||||
title={`This entity is a ${getNumberWithOrdinal(
|
||||
path?.length + 1,
|
||||
)} degree connection to ${entityData?.name || 'the source entity'}`}
|
||||
>
|
||||
<PlatformText>{getNumberWithOrdinal(path?.length + 1)}</PlatformText>
|
||||
</Tooltip>
|
||||
</span>
|
||||
)}
|
||||
</PlatformInfo>
|
||||
<EntityTitle onClick={onClick} $titleSizePx={titleSizePx}>
|
||||
{name || ' '}
|
||||
|
||||
@ -38,18 +38,37 @@ const ThinDivider = styled(Divider)`
|
||||
margin: 0px;
|
||||
`;
|
||||
|
||||
type AdditionalProperties = {
|
||||
path?: Entity[];
|
||||
};
|
||||
|
||||
type Props = {
|
||||
// additional data about the search result that is not part of the entity used to enrich the
|
||||
// presentation of the entity. For example, metadata about how the entity is related for the case
|
||||
// of impact analysis
|
||||
additionalPropertiesList?: Array<AdditionalProperties>;
|
||||
entities: Array<Entity>;
|
||||
onClick?: (index: number) => void;
|
||||
};
|
||||
|
||||
export const EntityNameList = ({ entities, onClick }: Props) => {
|
||||
export const EntityNameList = ({ additionalPropertiesList, entities, onClick }: Props) => {
|
||||
const entityRegistry = useEntityRegistry();
|
||||
if (
|
||||
additionalPropertiesList?.length !== undefined &&
|
||||
additionalPropertiesList.length > 0 &&
|
||||
additionalPropertiesList?.length !== entities.length
|
||||
) {
|
||||
console.warn(
|
||||
'Warning: additionalPropertiesList length provided to EntityNameList does not match entity array length',
|
||||
{ additionalPropertiesList, entities },
|
||||
);
|
||||
}
|
||||
return (
|
||||
<StyledList
|
||||
bordered
|
||||
dataSource={entities}
|
||||
renderItem={(entity, index) => {
|
||||
const additionalProperties = additionalPropertiesList?.[index];
|
||||
const genericProps = entityRegistry.getGenericEntityProperties(entity.type, entity);
|
||||
const platformLogoUrl = genericProps?.platform?.properties?.logoUrl;
|
||||
const platformName =
|
||||
@ -73,6 +92,7 @@ export const EntityNameList = ({ entities, onClick }: Props) => {
|
||||
tags={genericProps?.globalTags || undefined}
|
||||
glossaryTerms={genericProps?.glossaryTerms || undefined}
|
||||
onClick={() => onClick?.(index)}
|
||||
path={additionalProperties?.path}
|
||||
/>
|
||||
</ListItem>
|
||||
<ThinDivider />
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user