feat(business glossary): search, browse and entity page for business glossary terms (#2538)

This commit is contained in:
Lal Rishav 2021-05-13 09:18:49 +05:30 committed by GitHub
parent e3c190e772
commit 89535d7dea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 241 additions and 8 deletions

View File

@ -3,6 +3,7 @@ package com.linkedin.datahub.graphql.types.glossary;
import com.linkedin.common.urn.GlossaryTermUrn;
import java.net.URISyntaxException;
import java.util.regex.Pattern;
public class GlossaryTermUtils {
@ -15,4 +16,12 @@ public class GlossaryTermUtils {
throw new RuntimeException(String.format("Failed to retrieve glossary with urn %s, invalid urn", urnStr));
}
}
public static String getGlossaryTermName(String hierarchicalName) {
if (hierarchicalName.contains(".")) {
String[] nodes = hierarchicalName.split(Pattern.quote("."));
return nodes[nodes.length - 1];
}
return hierarchicalName;
}
}

View File

@ -5,6 +5,8 @@ import javax.annotation.Nonnull;
import com.linkedin.datahub.graphql.generated.GlossaryTerm;
import com.linkedin.datahub.graphql.generated.EntityType;
import com.linkedin.datahub.graphql.types.mappers.ModelMapper;
import com.linkedin.datahub.graphql.types.common.mappers.OwnershipMapper;
import com.linkedin.datahub.graphql.types.glossary.GlossaryTermUtils;
/**
* Maps Pegasus {@link RecordTemplate} objects to objects conforming to the GQL schema.
@ -24,8 +26,11 @@ public class GlossaryTermMapper implements ModelMapper<com.linkedin.glossary.Glo
com.linkedin.datahub.graphql.generated.GlossaryTerm result = new com.linkedin.datahub.graphql.generated.GlossaryTerm();
result.setUrn(glossaryTerm.getUrn().toString());
result.setType(EntityType.GLOSSARY_TERM);
result.setName(glossaryTerm.getUrn().getNameEntity());
result.setName(GlossaryTermUtils.getGlossaryTermName(glossaryTerm.getUrn().getNameEntity()));
result.setGlossaryTermInfo(GlossaryTermInfoMapper.map(glossaryTerm.getGlossaryTermInfo()));
if (glossaryTerm.hasOwnership()) {
result.setOwnership(OwnershipMapper.map(glossaryTerm.getOwnership()));
}
return result;
}
}

View File

@ -294,6 +294,11 @@ type GlossaryTerm implements Entity {
"""
urn: String!
"""
Ownership metadata of the dataset
"""
ownership: Ownership
"""
GMS Entity Type
"""

View File

@ -24,6 +24,7 @@ import defaultThemeConfig from './conf/theme/theme_light.config.json';
import { PageRoutes } from './conf/Global';
import { isLoggedInVar } from './app/auth/checkAuthStatus';
import { GlobalCfg } from './conf';
import { GlossaryTermEntity } from './app/entity/glossaryTerm/GlossaryTermEntity';
// Enable to use the Apollo MockProvider instead of a real HTTP client
const MOCK_MODE = false;
@ -93,6 +94,7 @@ const App: React.VFC = () => {
register.register(new TagEntity());
register.register(new DataFlowEntity());
register.register(new DataJobEntity());
register.register(new GlossaryTermEntity());
return register;
}, []);

View File

@ -8,7 +8,7 @@ import {
GetSearchResultsQuery,
} from './graphql/search.generated';
import { GetUserDocument } from './graphql/user.generated';
import { Dataset, DataFlow, DataJob, EntityType, PlatformType } from './types.generated';
import { Dataset, DataFlow, DataJob, GlossaryTerm, EntityType, PlatformType } from './types.generated';
import { GetTagDocument } from './graphql/tag.generated';
const user1 = {
@ -412,6 +412,36 @@ export const dataset7WithSelfReferentialLineage = {
],
},
};
const glossaryTerm1 = {
urn: 'urn:li:glossaryTerm:1',
type: EntityType.GlossaryTerm,
name: 'Another glossary term',
ownership: {
owners: [
{
owner: {
...user1,
},
type: 'DATAOWNER',
},
{
owner: {
...user2,
},
type: 'DELEGATE',
},
],
lastModified: {
time: 0,
},
},
glossaryTermInfo: {
definition: 'New glossary term',
termSource: 'termSource',
sourceRef: 'sourceRef',
sourceURI: 'sourceURI',
},
} as GlossaryTerm;
const sampleTag = {
urn: 'urn:li:tag:abc-sample-tag',
@ -866,6 +896,59 @@ export const mocks = [
} as GetSearchResultsQuery,
},
},
{
request: {
query: GetSearchResultsDocument,
variables: {
input: {
type: 'GLOSSARY_TERM',
query: 'tags:abc-sample-tag',
start: 0,
count: 1,
filters: [],
},
},
},
result: {
data: {
__typename: 'Query',
search: {
__typename: 'SearchResults',
start: 0,
count: 1,
total: 1,
searchResults: [
{
entity: {
__typename: 'GLOSSARY_TERM',
...glossaryTerm1,
},
matchedFields: [],
},
],
facets: [
{
field: 'origin',
aggregations: [
{
value: 'PROD',
count: 3,
},
],
},
{
field: 'platform',
aggregations: [
{ value: 'hdfs', count: 1 },
{ value: 'mysql', count: 1 },
{ value: 'kafka', count: 1 },
],
},
],
},
} as GetSearchResultsQuery,
},
},
{
request: {
query: GetSearchResultsDocument,

View File

@ -0,0 +1,58 @@
import * as React from 'react';
import { BookFilled, BookOutlined } from '@ant-design/icons';
import { EntityType, GlossaryTerm } from '../../../types.generated';
import { Entity, IconStyleType, PreviewType } from '../Entity';
import { Preview } from './preview/Preview';
/**
* Definition of the DataHub Dataset entity.
*/
export class GlossaryTermEntity implements Entity<GlossaryTerm> {
type: EntityType = EntityType.GlossaryTerm;
icon = (fontSize: number, styleType: IconStyleType) => {
if (styleType === IconStyleType.TAB_VIEW) {
return <BookOutlined style={{ fontSize }} />;
}
if (styleType === IconStyleType.HIGHLIGHT) {
return <BookFilled style={{ fontSize, color: '#B37FEB' }} />;
}
return (
<BookOutlined
style={{
fontSize,
color: '#BFBFBF',
}}
/>
);
};
isSearchEnabled = () => true;
isBrowseEnabled = () => true;
getAutoCompleteFieldName = () => 'name';
isLineageEnabled = () => false;
getPathName = () => 'glossary';
getCollectionName = () => 'Business Glossary';
renderProfile = (urn: string) => <div>Coming soon.... {urn}</div>;
renderSearch = () => <div />;
renderPreview = (_: PreviewType, data: GlossaryTerm) => {
return (
<Preview
urn={data?.urn}
name={data?.name}
definition={data?.glossaryTermInfo?.definition}
owners={data?.ownership?.owners}
/>
);
};
}

View File

@ -0,0 +1,28 @@
import React from 'react';
import { BookOutlined } from '@ant-design/icons';
import { EntityType, Owner } from '../../../../types.generated';
import DefaultPreviewCard from '../../../preview/DefaultPreviewCard';
import { useEntityRegistry } from '../../../useEntityRegistry';
export const Preview = ({
urn,
name,
definition,
owners,
}: {
urn: string;
name: string;
definition?: string | null;
owners?: Array<Owner> | null;
}): JSX.Element => {
const entityRegistry = useEntityRegistry();
return (
<DefaultPreviewCard
url={`/${entityRegistry.getPathName(EntityType.GlossaryTerm)}/${urn}`}
name={name || ''}
description={definition || ''}
owners={owners}
logoComponent={<BookOutlined style={{ fontSize: '72px' }} />}
/>
);
};

View File

@ -0,0 +1,20 @@
import { render } from '@testing-library/react';
import React from 'react';
import TestPageContainer from '../../../../../utils/test-utils/TestPageContainer';
import { Preview } from '../Preview';
describe('Preview', () => {
it('renders', () => {
const { getByText } = render(
<TestPageContainer>
<Preview
urn="urn:li:glossaryTerm:instruments.FinancialInstrument_v1"
name="name"
definition="definition"
owners={null}
/>
</TestPageContainer>,
);
expect(getByText('definition')).toBeInTheDocument();
});
});

View File

@ -10,6 +10,7 @@ import TagGroup from '../shared/tags/TagGroup';
interface Props {
name: string;
logoUrl?: string;
logoComponent?: JSX.Element;
url: string;
description: string;
type?: string;
@ -46,6 +47,7 @@ const styles = {
export default function DefaultPreviewCard({
name,
logoUrl,
logoComponent,
url,
description,
type,
@ -62,16 +64,19 @@ export default function DefaultPreviewCard({
<Space direction="vertical" align="start" size={28} style={styles.leftColumn}>
<Link to={url}>
<Space direction="horizontal" size={20} align="center">
{logoUrl && <PreviewImage src={logoUrl} preview />}
{logoUrl ? <PreviewImage src={logoUrl} preview /> : logoComponent || ''}
<Space direction="vertical" size={8}>
<Typography.Text strong style={styles.name}>
{name}
</Typography.Text>
<Space split={<Divider type="vertical" />} size={16}>
<Typography.Text>{type}</Typography.Text>
<Typography.Text strong>{platform}</Typography.Text>
{qualifier && <Tag>{qualifier}</Tag>}
</Space>
{(type || platform || qualifier) && (
<Space split={<Divider type="vertical" />} size={16}>
<Typography.Text>{type}</Typography.Text>
<Typography.Text strong>{platform}</Typography.Text>
{qualifier && <Tag>{qualifier}</Tag>}
</Space>
)}
</Space>
</Space>
</Link>

View File

@ -48,6 +48,22 @@ query getBrowseResults($input: BrowseInput!) {
...globalTagsFields
}
}
... on GlossaryTerm {
name
ownership {
...ownershipFields
}
glossaryTermInfo {
definition
termSource
sourceRef
sourceUrl
customProperties {
key
value
}
}
}
... on Chart {
urn
type

View File

@ -12,6 +12,7 @@ import { EntityRegistryContext } from '../../entityRegistryContext';
import { TagEntity } from '../../app/entity/tag/Tag';
import defaultThemeConfig from '../../conf/theme/theme_light.config.json';
import { GlossaryTermEntity } from '../../app/entity/glossaryTerm/GlossaryTermEntity';
type Props = {
children: React.ReactNode;
@ -26,6 +27,7 @@ export function getTestEntityRegistry() {
entityRegistry.register(new TagEntity());
entityRegistry.register(new DataFlowEntity());
entityRegistry.register(new DataJobEntity());
entityRegistry.register(new GlossaryTermEntity());
return entityRegistry;
}