mirror of
https://github.com/datahub-project/datahub.git
synced 2025-11-02 11:49:23 +00:00
feat(business-glossary): Business glossary relationship UI (#3129)
This commit is contained in:
parent
db3784b407
commit
daf7a8f37e
@ -33,6 +33,9 @@ public class GlossaryTermInfoMapper implements ModelMapper<com.linkedin.glossary
|
||||
if (glossaryTermInfo.hasCustomProperties()) {
|
||||
glossaryTermInfoResult.setCustomProperties(StringMapMapper.map(glossaryTermInfo.getCustomProperties()));
|
||||
}
|
||||
if (glossaryTermInfo.hasRawSchema()) {
|
||||
glossaryTermInfoResult.setRawSchema(glossaryTermInfo.getRawSchema());
|
||||
}
|
||||
return glossaryTermInfoResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,6 +22,7 @@ import {
|
||||
import { GetTagDocument } from './graphql/tag.generated';
|
||||
import { GetMlModelDocument } from './graphql/mlModel.generated';
|
||||
import { GetMlModelGroupDocument } from './graphql/mlModelGroup.generated';
|
||||
import { GetGlossaryTermDocument, GetGlossaryTermQuery } from './graphql/glossaryTerm.generated';
|
||||
|
||||
const user1 = {
|
||||
username: 'sdas',
|
||||
@ -558,6 +559,98 @@ const glossaryTerm1 = {
|
||||
},
|
||||
} as GlossaryTerm;
|
||||
|
||||
const glossaryTerm2 = {
|
||||
urn: 'urn:li:glossaryTerm:example.glossaryterm1',
|
||||
type: 'GLOSSARY_TERM',
|
||||
name: 'glossaryterm1',
|
||||
hierarchicalName: 'example.glossaryterm1',
|
||||
ownership: null,
|
||||
glossaryTermInfo: {
|
||||
definition: 'is A relation glossary term 1',
|
||||
termSource: 'INTERNAL',
|
||||
sourceRef: 'TERM_SOURCE_SAXO',
|
||||
sourceUrl: '',
|
||||
rawSchema: 'sample proto schema',
|
||||
customProperties: [
|
||||
{
|
||||
key: 'keyProperty',
|
||||
value: 'valueProperty',
|
||||
__typename: 'StringMapEntry',
|
||||
},
|
||||
],
|
||||
__typename: 'GlossaryTermInfo',
|
||||
},
|
||||
isRealtedTerms: {
|
||||
start: 0,
|
||||
count: 0,
|
||||
total: 0,
|
||||
relationships: [
|
||||
{
|
||||
entity: {
|
||||
urn: 'urn:li:glossaryTerm:schema.Field16Schema_v1',
|
||||
__typename: 'GlossaryTerm',
|
||||
},
|
||||
},
|
||||
],
|
||||
__typename: 'EntityRelationshipsResult',
|
||||
},
|
||||
hasRelatedTerms: {
|
||||
start: 0,
|
||||
count: 0,
|
||||
total: 0,
|
||||
relationships: [
|
||||
{
|
||||
entity: {
|
||||
urn: 'urn:li:glossaryTerm:example.glossaryterm2',
|
||||
__typename: 'GlossaryTerm',
|
||||
},
|
||||
},
|
||||
],
|
||||
__typename: 'EntityRelationshipsResult',
|
||||
},
|
||||
__typename: 'GlossaryTerm',
|
||||
};
|
||||
|
||||
const glossaryTerm3 = {
|
||||
urn: 'urn:li:glossaryTerm:example.glossaryterm2',
|
||||
type: 'GLOSSARY_TERM',
|
||||
name: 'glossaryterm2',
|
||||
hierarchicalName: 'example.glossaryterm2',
|
||||
ownership: null,
|
||||
glossaryTermInfo: {
|
||||
definition: 'has A relation glossary term 2',
|
||||
termSource: 'INTERNAL',
|
||||
sourceRef: 'TERM_SOURCE_SAXO',
|
||||
sourceUrl: '',
|
||||
rawSchema: 'sample proto schema',
|
||||
customProperties: [
|
||||
{
|
||||
key: 'keyProperty',
|
||||
value: 'valueProperty',
|
||||
__typename: 'StringMapEntry',
|
||||
},
|
||||
],
|
||||
__typename: 'GlossaryTermInfo',
|
||||
},
|
||||
glossaryRelatedTerms: {
|
||||
isRelatedTerms: null,
|
||||
hasRelatedTerms: [
|
||||
{
|
||||
urn: 'urn:li:glossaryTerm:example.glossaryterm3',
|
||||
name: 'glossaryterm3',
|
||||
__typename: 'GlossaryTerm',
|
||||
},
|
||||
{
|
||||
urn: 'urn:li:glossaryTerm:example.glossaryterm4',
|
||||
name: 'glossaryterm4',
|
||||
__typename: 'GlossaryTerm',
|
||||
},
|
||||
],
|
||||
__typename: 'GlossaryRelatedTerms',
|
||||
},
|
||||
__typename: 'GlossaryTerm',
|
||||
} as GlossaryTerm;
|
||||
|
||||
const sampleTag = {
|
||||
urn: 'urn:li:tag:abc-sample-tag',
|
||||
name: 'abc-sample-tag',
|
||||
@ -1351,6 +1444,32 @@ export const mocks = [
|
||||
} as GetSearchResultsQuery,
|
||||
},
|
||||
},
|
||||
{
|
||||
request: {
|
||||
query: GetGlossaryTermDocument,
|
||||
variables: {
|
||||
urn: 'urn:li:glossaryTerm:example.glossaryterm1',
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
glossaryTerm: { ...glossaryTerm2 },
|
||||
} as GetGlossaryTermQuery,
|
||||
},
|
||||
},
|
||||
{
|
||||
request: {
|
||||
query: GetGlossaryTermDocument,
|
||||
variables: {
|
||||
urn: 'urn:li:glossaryTerm:example.glossaryterm2',
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
glossaryTerm: { ...glossaryTerm3 },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
request: {
|
||||
query: GetSearchResultsDocument,
|
||||
|
||||
@ -0,0 +1,72 @@
|
||||
import { Menu } from 'antd';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import GlossaryRelatedTermsResult from './GlossaryRelatedTermsResult';
|
||||
|
||||
export type Props = {
|
||||
glossaryTerm: any;
|
||||
};
|
||||
|
||||
export enum RelatedTermTypes {
|
||||
hasRelatedTerms = 'Composed Of',
|
||||
isRelatedTerms = 'Defined in',
|
||||
}
|
||||
|
||||
const DetailWrapper = styled.div`
|
||||
display: inline-flex;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const MenuWrapper = styled.div`
|
||||
border: 2px solid #f5f5f5;
|
||||
`;
|
||||
|
||||
const Content = styled.div`
|
||||
margin-left: 32px;
|
||||
flex-grow: 1;
|
||||
`;
|
||||
|
||||
export default function GlossayRelatedTerms({ glossaryTerm }: Props) {
|
||||
const [selectedKey, setSelectedKey] = useState('');
|
||||
const menuOptionsArray = Object.keys(RelatedTermTypes);
|
||||
|
||||
useEffect(() => {
|
||||
if (menuOptionsArray && menuOptionsArray.length > 0 && selectedKey.length === 0) {
|
||||
setSelectedKey(menuOptionsArray[0]);
|
||||
}
|
||||
}, [menuOptionsArray, selectedKey]);
|
||||
|
||||
const onMenuClick = ({ key }) => {
|
||||
setSelectedKey(key);
|
||||
};
|
||||
|
||||
return (
|
||||
<DetailWrapper>
|
||||
<MenuWrapper>
|
||||
<Menu
|
||||
selectable={false}
|
||||
mode="inline"
|
||||
style={{ width: 256 }}
|
||||
selectedKeys={[selectedKey]}
|
||||
onClick={(key) => {
|
||||
onMenuClick(key);
|
||||
}}
|
||||
>
|
||||
{menuOptionsArray.map((option) => (
|
||||
<Menu.Item data-testid={option} key={option}>
|
||||
{RelatedTermTypes[option]}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
<Content>
|
||||
{selectedKey && (
|
||||
<GlossaryRelatedTermsResult
|
||||
glossaryRelatedTermType={RelatedTermTypes[selectedKey]}
|
||||
glossaryRelatedTermResult={glossaryTerm[selectedKey]?.relationships || []}
|
||||
/>
|
||||
)}
|
||||
</Content>
|
||||
</DetailWrapper>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,89 @@
|
||||
import { QueryResult } from '@apollo/client';
|
||||
import { Divider, List, Typography } from 'antd';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { GetGlossaryTermQuery, useGetGlossaryTermQuery } from '../../../../graphql/glossaryTerm.generated';
|
||||
import { EntityType, Exact } from '../../../../types.generated';
|
||||
import { Message } from '../../../shared/Message';
|
||||
import { useEntityRegistry } from '../../../useEntityRegistry';
|
||||
import { PreviewType } from '../../Entity';
|
||||
|
||||
export type Props = {
|
||||
glossaryRelatedTermType: string;
|
||||
glossaryRelatedTermResult: Array<any>;
|
||||
};
|
||||
|
||||
const ListContainer = styled.div`
|
||||
display: default;
|
||||
flex-grow: default;
|
||||
`;
|
||||
|
||||
const TitleContainer = styled.div`
|
||||
margin-bottom: 30px;
|
||||
`;
|
||||
|
||||
const ListItem = styled.div`
|
||||
margin: 40px;
|
||||
padding-bottom: 5px;
|
||||
`;
|
||||
|
||||
const Profile = styled.div`
|
||||
marging-bottom: 20px;
|
||||
`;
|
||||
|
||||
const messageStyle = { marginTop: '10%' };
|
||||
|
||||
export default function GlossaryRelatedTermsResult({ glossaryRelatedTermType, glossaryRelatedTermResult }: Props) {
|
||||
const entityRegistry = useEntityRegistry();
|
||||
const glossaryRelatedTermUrns: Array<string> = [];
|
||||
glossaryRelatedTermResult.forEach((item: any) => {
|
||||
glossaryRelatedTermUrns.push(item?.entity?.urn);
|
||||
});
|
||||
const glossaryTermInfo: QueryResult<GetGlossaryTermQuery, Exact<{ urn: string }>>[] = [];
|
||||
|
||||
for (let i = 0; i < glossaryRelatedTermUrns.length; i++) {
|
||||
glossaryTermInfo.push(
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
useGetGlossaryTermQuery({
|
||||
variables: {
|
||||
urn: glossaryRelatedTermUrns[i],
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const contentLoading = glossaryTermInfo.some((item) => {
|
||||
return item.loading;
|
||||
});
|
||||
return (
|
||||
<>
|
||||
{contentLoading ? (
|
||||
<Message type="loading" content="Loading..." style={messageStyle} />
|
||||
) : (
|
||||
<ListContainer>
|
||||
<TitleContainer>
|
||||
<Typography.Title level={3}>{glossaryRelatedTermType}</Typography.Title>
|
||||
<Divider />
|
||||
</TitleContainer>
|
||||
<List
|
||||
dataSource={glossaryTermInfo}
|
||||
renderItem={(item) => {
|
||||
return (
|
||||
<ListItem>
|
||||
<Profile>
|
||||
{entityRegistry.renderPreview(
|
||||
EntityType.GlossaryTerm,
|
||||
PreviewType.PREVIEW,
|
||||
item?.data?.glossaryTerm,
|
||||
)}
|
||||
</Profile>
|
||||
<Divider />
|
||||
</ListItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</ListContainer>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -15,7 +15,7 @@ export default function GlossaryTermHeader({ definition, sourceRef, sourceUrl, o
|
||||
const entityRegistry = useEntityRegistry();
|
||||
return (
|
||||
<>
|
||||
<Space direction="vertical" size="middle">
|
||||
<Space direction="vertical" size="middle" style={{ marginBottom: '15px' }}>
|
||||
<Typography.Paragraph>{definition}</Typography.Paragraph>
|
||||
<Space split={<Divider type="vertical" />}>
|
||||
<Typography.Text>Source</Typography.Text>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Alert } from 'antd';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useGetGlossaryTermQuery } from '../../../../graphql/glossaryTerm.generated';
|
||||
import { GetGlossaryTermQuery, useGetGlossaryTermQuery } from '../../../../graphql/glossaryTerm.generated';
|
||||
import { EntityType, GlossaryTerm, SearchResult } from '../../../../types.generated';
|
||||
import { useGetEntitySearchResults } from '../../../../utils/customGraphQL/useGetEntitySearchResults';
|
||||
import { EntityProfile } from '../../../shared/EntityProfile';
|
||||
@ -9,16 +9,20 @@ import useUserParams from '../../../shared/entitySearch/routingUtils/useUserPara
|
||||
import { Message } from '../../../shared/Message';
|
||||
import { useEntityRegistry } from '../../../useEntityRegistry';
|
||||
import { Properties as PropertiesView } from '../../shared/Properties';
|
||||
import GlossayRelatedTerms from './GlossaryRelatedTerms';
|
||||
import GlossaryTermHeader from './GlossaryTermHeader';
|
||||
import SchemaView from './SchemaView';
|
||||
|
||||
const messageStyle = { marginTop: '10%' };
|
||||
|
||||
export enum TabType {
|
||||
RelatedEntity = 'Related Entities',
|
||||
RelatedGlossaryTerms = 'Related Terms',
|
||||
Schema = 'Schema',
|
||||
Properties = 'Properties',
|
||||
}
|
||||
|
||||
const ENABLED_TAB_TYPES = [TabType.Properties, TabType.RelatedEntity];
|
||||
const ENABLED_TAB_TYPES = [TabType.Properties, TabType.RelatedEntity, TabType.RelatedGlossaryTerms, TabType.Schema];
|
||||
|
||||
export default function GlossaryTermProfile() {
|
||||
const { urn } = useUserParams();
|
||||
@ -56,17 +60,27 @@ export default function GlossaryTermProfile() {
|
||||
return filteredSearchResult;
|
||||
}, [entitySearchResult]);
|
||||
|
||||
const getTabs = ({ glossaryTermInfo }: GlossaryTerm) => {
|
||||
const getTabs = ({ glossaryTerm }: GetGlossaryTermQuery) => {
|
||||
return [
|
||||
{
|
||||
name: TabType.RelatedEntity,
|
||||
path: TabType.RelatedEntity.toLocaleLowerCase(),
|
||||
content: <RelatedEntityResults searchResult={entitySearchForDetails} />,
|
||||
},
|
||||
{
|
||||
name: TabType.RelatedGlossaryTerms,
|
||||
path: TabType.RelatedGlossaryTerms.toLocaleLowerCase(),
|
||||
content: <GlossayRelatedTerms glossaryTerm={glossaryTerm || {}} />,
|
||||
},
|
||||
{
|
||||
name: TabType.Schema,
|
||||
path: TabType.Schema.toLocaleLowerCase(),
|
||||
content: <SchemaView rawSchema={glossaryTerm?.glossaryTermInfo?.rawSchema || ''} />,
|
||||
},
|
||||
{
|
||||
name: TabType.Properties,
|
||||
path: TabType.Properties.toLocaleLowerCase(),
|
||||
content: <PropertiesView properties={glossaryTermInfo.customProperties || []} />,
|
||||
content: <PropertiesView properties={glossaryTerm?.glossaryTermInfo?.customProperties || []} />,
|
||||
},
|
||||
].filter((tab) => ENABLED_TAB_TYPES.includes(tab.name));
|
||||
};
|
||||
@ -94,7 +108,7 @@ export default function GlossaryTermProfile() {
|
||||
title={data.glossaryTerm.name}
|
||||
tags={null}
|
||||
header={getHeader(data?.glossaryTerm as GlossaryTerm)}
|
||||
tabs={getTabs(data.glossaryTerm as GlossaryTerm)}
|
||||
tabs={getTabs(data)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import { Empty, Typography } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export type Props = {
|
||||
rawSchema: string | null;
|
||||
};
|
||||
|
||||
const Content = styled.div`
|
||||
margin-left: 32px;
|
||||
flex-grow: 1;
|
||||
`;
|
||||
|
||||
export default function SchemaView({ rawSchema }: Props) {
|
||||
return (
|
||||
<>
|
||||
{rawSchema && rawSchema.length > 0 ? (
|
||||
<Typography.Text data-testid="schema-raw-view">
|
||||
<pre>
|
||||
<code>{rawSchema}</code>
|
||||
</pre>
|
||||
</Typography.Text>
|
||||
) : (
|
||||
<Content>
|
||||
<Empty description="No Schema" image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
</Content>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import TestPageContainer from '../../../../../utils/test-utils/TestPageContainer';
|
||||
import GlossaryRelatedTerms from '../GlossaryRelatedTerms';
|
||||
import { mocks } from '../../../../../Mocks';
|
||||
|
||||
const glossaryRelatedTermData = {
|
||||
isRealtedTerms: {
|
||||
start: 0,
|
||||
count: 0,
|
||||
total: 0,
|
||||
relationships: [
|
||||
{
|
||||
entity: {
|
||||
urn: 'urn:li:glossaryTerm:schema.Field16Schema_v1',
|
||||
__typename: 'GlossaryTerm',
|
||||
},
|
||||
},
|
||||
],
|
||||
__typename: 'EntityRelationshipsResult',
|
||||
},
|
||||
hasRelatedTerms: {
|
||||
start: 0,
|
||||
count: 0,
|
||||
total: 0,
|
||||
relationships: [
|
||||
{
|
||||
entity: {
|
||||
urn: 'urn:li:glossaryTerm:example.glossaryterm2',
|
||||
__typename: 'GlossaryTerm',
|
||||
},
|
||||
},
|
||||
],
|
||||
__typename: 'EntityRelationshipsResult',
|
||||
},
|
||||
};
|
||||
|
||||
describe('Glossary Related Terms', () => {
|
||||
it('renders and print hasRelatedTerms detail by default', async () => {
|
||||
const { getByText } = render(
|
||||
<MockedProvider mocks={mocks} addTypename={false}>
|
||||
<TestPageContainer>
|
||||
<GlossaryRelatedTerms glossaryTerm={glossaryRelatedTermData} />
|
||||
</TestPageContainer>
|
||||
</MockedProvider>,
|
||||
);
|
||||
expect(getByText('Composed Of')).toBeInTheDocument();
|
||||
expect(getByText('Defined in')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@ -45,7 +45,7 @@ const PreviewImage = styled(Image)`
|
||||
`;
|
||||
|
||||
const styles = {
|
||||
row: { width: '100%', marginBottom: '0px' },
|
||||
row: { width: '100%', marginBottom: '20px' },
|
||||
leftColumn: { maxWidth: '75%' },
|
||||
rightColumn: { maxWidth: '25%' },
|
||||
name: { fontSize: '18px' },
|
||||
|
||||
@ -35,6 +35,7 @@ export const RoutedTabs = ({ defaultPath, tabs, onTabChange, ...props }: Props)
|
||||
<div>
|
||||
<Tabs
|
||||
defaultActiveKey={activePath}
|
||||
activeKey={activePath}
|
||||
size="large"
|
||||
onTabClick={(tab: string) => onTabChange && onTabChange(tab)}
|
||||
onChange={(newPath) => history.push(`${url}/${newPath}`)}
|
||||
|
||||
@ -4,7 +4,7 @@ query getGlossaryTerm($urn: String!, $start: Int, $count: Int) {
|
||||
type
|
||||
name
|
||||
hierarchicalName
|
||||
isRealtedTerms: relationships(types: ["IsA"], direction: OUTGOING, start: $start, count: $count) {
|
||||
isRelatedTerms: relationships(types: ["IsA"], direction: OUTGOING, start: $start, count: $count) {
|
||||
start
|
||||
count
|
||||
total
|
||||
@ -16,7 +16,7 @@ query getGlossaryTerm($urn: String!, $start: Int, $count: Int) {
|
||||
}
|
||||
}
|
||||
}
|
||||
hasRealtedTerms: relationships(types: ["HasA"], direction: OUTGOING, start: $start, count: $count) {
|
||||
hasRelatedTerms: relationships(types: ["HasA"], direction: OUTGOING, start: $start, count: $count) {
|
||||
start
|
||||
count
|
||||
total
|
||||
@ -36,6 +36,7 @@ query getGlossaryTerm($urn: String!, $start: Int, $count: Int) {
|
||||
termSource
|
||||
sourceRef
|
||||
sourceUrl
|
||||
rawSchema
|
||||
customProperties {
|
||||
key
|
||||
value
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user