mirror of
https://github.com/datahub-project/datahub.git
synced 2025-12-13 02:57:03 +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 com.linkedin.common.urn.GlossaryTermUrn;
|
||||||
|
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
public class GlossaryTermUtils {
|
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));
|
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.GlossaryTerm;
|
||||||
import com.linkedin.datahub.graphql.generated.EntityType;
|
import com.linkedin.datahub.graphql.generated.EntityType;
|
||||||
import com.linkedin.datahub.graphql.types.mappers.ModelMapper;
|
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.
|
* 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();
|
com.linkedin.datahub.graphql.generated.GlossaryTerm result = new com.linkedin.datahub.graphql.generated.GlossaryTerm();
|
||||||
result.setUrn(glossaryTerm.getUrn().toString());
|
result.setUrn(glossaryTerm.getUrn().toString());
|
||||||
result.setType(EntityType.GLOSSARY_TERM);
|
result.setType(EntityType.GLOSSARY_TERM);
|
||||||
result.setName(glossaryTerm.getUrn().getNameEntity());
|
result.setName(GlossaryTermUtils.getGlossaryTermName(glossaryTerm.getUrn().getNameEntity()));
|
||||||
result.setGlossaryTermInfo(GlossaryTermInfoMapper.map(glossaryTerm.getGlossaryTermInfo()));
|
result.setGlossaryTermInfo(GlossaryTermInfoMapper.map(glossaryTerm.getGlossaryTermInfo()));
|
||||||
|
if (glossaryTerm.hasOwnership()) {
|
||||||
|
result.setOwnership(OwnershipMapper.map(glossaryTerm.getOwnership()));
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -294,6 +294,11 @@ type GlossaryTerm implements Entity {
|
|||||||
"""
|
"""
|
||||||
urn: String!
|
urn: String!
|
||||||
|
|
||||||
|
"""
|
||||||
|
Ownership metadata of the dataset
|
||||||
|
"""
|
||||||
|
ownership: Ownership
|
||||||
|
|
||||||
"""
|
"""
|
||||||
GMS Entity Type
|
GMS Entity Type
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import defaultThemeConfig from './conf/theme/theme_light.config.json';
|
|||||||
import { PageRoutes } from './conf/Global';
|
import { PageRoutes } from './conf/Global';
|
||||||
import { isLoggedInVar } from './app/auth/checkAuthStatus';
|
import { isLoggedInVar } from './app/auth/checkAuthStatus';
|
||||||
import { GlobalCfg } from './conf';
|
import { GlobalCfg } from './conf';
|
||||||
|
import { GlossaryTermEntity } from './app/entity/glossaryTerm/GlossaryTermEntity';
|
||||||
|
|
||||||
// Enable to use the Apollo MockProvider instead of a real HTTP client
|
// Enable to use the Apollo MockProvider instead of a real HTTP client
|
||||||
const MOCK_MODE = false;
|
const MOCK_MODE = false;
|
||||||
@ -93,6 +94,7 @@ const App: React.VFC = () => {
|
|||||||
register.register(new TagEntity());
|
register.register(new TagEntity());
|
||||||
register.register(new DataFlowEntity());
|
register.register(new DataFlowEntity());
|
||||||
register.register(new DataJobEntity());
|
register.register(new DataJobEntity());
|
||||||
|
register.register(new GlossaryTermEntity());
|
||||||
return register;
|
return register;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import {
|
|||||||
GetSearchResultsQuery,
|
GetSearchResultsQuery,
|
||||||
} from './graphql/search.generated';
|
} from './graphql/search.generated';
|
||||||
import { GetUserDocument } from './graphql/user.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';
|
import { GetTagDocument } from './graphql/tag.generated';
|
||||||
|
|
||||||
const user1 = {
|
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 = {
|
const sampleTag = {
|
||||||
urn: 'urn:li:tag:abc-sample-tag',
|
urn: 'urn:li:tag:abc-sample-tag',
|
||||||
@ -866,6 +896,59 @@ export const mocks = [
|
|||||||
} as GetSearchResultsQuery,
|
} 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: {
|
request: {
|
||||||
query: GetSearchResultsDocument,
|
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 {
|
interface Props {
|
||||||
name: string;
|
name: string;
|
||||||
logoUrl?: string;
|
logoUrl?: string;
|
||||||
|
logoComponent?: JSX.Element;
|
||||||
url: string;
|
url: string;
|
||||||
description: string;
|
description: string;
|
||||||
type?: string;
|
type?: string;
|
||||||
@ -46,6 +47,7 @@ const styles = {
|
|||||||
export default function DefaultPreviewCard({
|
export default function DefaultPreviewCard({
|
||||||
name,
|
name,
|
||||||
logoUrl,
|
logoUrl,
|
||||||
|
logoComponent,
|
||||||
url,
|
url,
|
||||||
description,
|
description,
|
||||||
type,
|
type,
|
||||||
@ -62,16 +64,19 @@ export default function DefaultPreviewCard({
|
|||||||
<Space direction="vertical" align="start" size={28} style={styles.leftColumn}>
|
<Space direction="vertical" align="start" size={28} style={styles.leftColumn}>
|
||||||
<Link to={url}>
|
<Link to={url}>
|
||||||
<Space direction="horizontal" size={20} align="center">
|
<Space direction="horizontal" size={20} align="center">
|
||||||
{logoUrl && <PreviewImage src={logoUrl} preview />}
|
{logoUrl ? <PreviewImage src={logoUrl} preview /> : logoComponent || ''}
|
||||||
|
|
||||||
<Space direction="vertical" size={8}>
|
<Space direction="vertical" size={8}>
|
||||||
<Typography.Text strong style={styles.name}>
|
<Typography.Text strong style={styles.name}>
|
||||||
{name}
|
{name}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<Space split={<Divider type="vertical" />} size={16}>
|
{(type || platform || qualifier) && (
|
||||||
<Typography.Text>{type}</Typography.Text>
|
<Space split={<Divider type="vertical" />} size={16}>
|
||||||
<Typography.Text strong>{platform}</Typography.Text>
|
<Typography.Text>{type}</Typography.Text>
|
||||||
{qualifier && <Tag>{qualifier}</Tag>}
|
<Typography.Text strong>{platform}</Typography.Text>
|
||||||
</Space>
|
{qualifier && <Tag>{qualifier}</Tag>}
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
</Space>
|
</Space>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@ -48,6 +48,22 @@ query getBrowseResults($input: BrowseInput!) {
|
|||||||
...globalTagsFields
|
...globalTagsFields
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
... on GlossaryTerm {
|
||||||
|
name
|
||||||
|
ownership {
|
||||||
|
...ownershipFields
|
||||||
|
}
|
||||||
|
glossaryTermInfo {
|
||||||
|
definition
|
||||||
|
termSource
|
||||||
|
sourceRef
|
||||||
|
sourceUrl
|
||||||
|
customProperties {
|
||||||
|
key
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
... on Chart {
|
... on Chart {
|
||||||
urn
|
urn
|
||||||
type
|
type
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import { EntityRegistryContext } from '../../entityRegistryContext';
|
|||||||
import { TagEntity } from '../../app/entity/tag/Tag';
|
import { TagEntity } from '../../app/entity/tag/Tag';
|
||||||
|
|
||||||
import defaultThemeConfig from '../../conf/theme/theme_light.config.json';
|
import defaultThemeConfig from '../../conf/theme/theme_light.config.json';
|
||||||
|
import { GlossaryTermEntity } from '../../app/entity/glossaryTerm/GlossaryTermEntity';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@ -26,6 +27,7 @@ export function getTestEntityRegistry() {
|
|||||||
entityRegistry.register(new TagEntity());
|
entityRegistry.register(new TagEntity());
|
||||||
entityRegistry.register(new DataFlowEntity());
|
entityRegistry.register(new DataFlowEntity());
|
||||||
entityRegistry.register(new DataJobEntity());
|
entityRegistry.register(new DataJobEntity());
|
||||||
|
entityRegistry.register(new GlossaryTermEntity());
|
||||||
return entityRegistry;
|
return entityRegistry;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user