mirror of
https://github.com/datahub-project/datahub.git
synced 2025-12-12 18:47:45 +00:00
feat(business glossary): search, browse and entity page for business glossary terms (#2538)
This commit is contained in:
parent
e3c190e772
commit
89535d7dea
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -294,6 +294,11 @@ type GlossaryTerm implements Entity {
|
||||
"""
|
||||
urn: String!
|
||||
|
||||
"""
|
||||
Ownership metadata of the dataset
|
||||
"""
|
||||
ownership: Ownership
|
||||
|
||||
"""
|
||||
GMS Entity Type
|
||||
"""
|
||||
|
||||
@ -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;
|
||||
}, []);
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
@ -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' }} />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user