feat(business-glossary): Business glossary relationship UI (#3129)

This commit is contained in:
Lal Rishav 2021-08-20 00:55:14 +05:30 committed by GitHub
parent db3784b407
commit daf7a8f37e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 390 additions and 10 deletions

View File

@ -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;
}
}
}

View File

@ -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,

View File

@ -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>
);
}

View File

@ -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>
)}
</>
);
}

View File

@ -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>

View File

@ -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)}
/>
)}
</>

View File

@ -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>
)}
</>
);
}

View File

@ -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();
});
});

View File

@ -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' },

View File

@ -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}`)}

View File

@ -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