Skip to content

Commit 6fa5692

Browse files
add business attribute entity to v2 UI
1 parent 946f649 commit 6fa5692

6 files changed

Lines changed: 366 additions & 0 deletions

File tree

datahub-web-react/src/app/buildEntityRegistryV2.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { QueryEntity } from './entityV2/query/QueryEntity';
2424
import { SchemaFieldEntity } from './entityV2/schemaField/SchemaFieldEntity';
2525
import { StructuredPropertyEntity } from './entityV2/structuredProperty/StructuredPropertyEntity';
2626
import { DataProcessInstanceEntity } from './entityV2/dataProcessInstance/DataProcessInstanceEntity';
27+
import { BusinessAttributeEntity } from './entityV2/businessAttribute/BusinessAttributeEntity';
2728

2829
export default function buildEntityRegistryV2() {
2930
const registry = new EntityRegistry();
@@ -52,5 +53,6 @@ export default function buildEntityRegistryV2() {
5253
registry.register(new SchemaFieldEntity());
5354
registry.register(new StructuredPropertyEntity());
5455
registry.register(new DataProcessInstanceEntity());
56+
registry.register(new BusinessAttributeEntity());
5557
return registry;
5658
}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import * as React from 'react';
2+
import { GlobalOutlined } from '@ant-design/icons';
3+
import { BusinessAttribute, EntityType, SearchResult } from '../../../types.generated';
4+
import { Entity, EntityCapabilityType, IconStyleType, PreviewType } from '../Entity';
5+
import { getDataForEntityType } from '../shared/containers/profile/utils';
6+
import { EntityProfile } from '../shared/containers/profile/EntityProfile';
7+
import { useGetBusinessAttributeQuery } from '../../../graphql/businessAttribute.generated';
8+
import { EntityMenuItems } from '../shared/EntityDropdown/EntityMenuActions';
9+
import { DocumentationTab } from '../shared/tabs/Documentation/DocumentationTab';
10+
import { PropertiesTab } from '../shared/tabs/Properties/PropertiesTab';
11+
import { SidebarAboutSection } from '../shared/containers/profile/sidebar/AboutSection/SidebarAboutSection';
12+
import { SidebarOwnerSection } from '../shared/containers/profile/sidebar/Ownership/sidebar/SidebarOwnerSection';
13+
import { SidebarTagsSection } from '../shared/containers/profile/sidebar/SidebarTagsSection';
14+
import { Preview } from './preview/Preview';
15+
import { PageRoutes } from '../../../conf/Global';
16+
import BusinessAttributeRelatedEntity from './profile/BusinessAttributeRelatedEntity';
17+
import { BusinessAttributeDataTypeSection } from './profile/BusinessAttributeDataTypeSection';
18+
19+
/**
20+
* Definition of datahub Business Attribute Entity
21+
*/
22+
/* eslint-disable @typescript-eslint/no-unused-vars */
23+
export class BusinessAttributeEntity implements Entity<BusinessAttribute> {
24+
type: EntityType = EntityType.BusinessAttribute;
25+
26+
icon = (fontSize?: number, styleType?: IconStyleType, color?: string) => {
27+
if (styleType === IconStyleType.TAB_VIEW) {
28+
return <GlobalOutlined style={{ fontSize, color }} />;
29+
}
30+
31+
if (styleType === IconStyleType.HIGHLIGHT) {
32+
return <GlobalOutlined style={{ fontSize, color: color || '#B37FEB' }} />;
33+
}
34+
35+
if (styleType === IconStyleType.SVG) {
36+
// TODO: Update the returned path value to the correct svg icon path
37+
return (
38+
<path d="M832 64H192c-17.7 0-32 14.3-32 32v832c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V96c0-17.7-14.3-32-32-32zm-600 72h560v208H232V136zm560 480H232V408h560v208zm0 272H232V680h560v208zM304 240a40 40 0 1080 0 40 40 0 10-80 0zm0 272a40 40 0 1080 0 40 40 0 10-80 0zm0 272a40 40 0 1080 0 40 40 0 10-80 0z" />
39+
);
40+
}
41+
42+
return (
43+
<GlobalOutlined
44+
style={{
45+
fontSize,
46+
color: color || '#BFBFBF',
47+
}}
48+
/>
49+
);
50+
};
51+
52+
displayName = (data: BusinessAttribute) => {
53+
return data?.properties?.name || data?.urn;
54+
};
55+
56+
getPathName = () => 'business-attribute';
57+
58+
getEntityName = () => 'Business Attribute';
59+
60+
getCollectionName = () => 'Business Attributes';
61+
62+
getGraphName = () => 'businessAttribute';
63+
64+
getCustomCardUrlPath = () => PageRoutes.BUSINESS_ATTRIBUTE;
65+
66+
isBrowseEnabled = () => false;
67+
68+
isLineageEnabled = () => false;
69+
70+
isSearchEnabled = () => true;
71+
72+
getOverridePropertiesFromEntity = (data: BusinessAttribute) => {
73+
return {
74+
name: data.properties?.name,
75+
};
76+
};
77+
78+
getGenericEntityProperties = (data: BusinessAttribute) => {
79+
return getDataForEntityType({
80+
data,
81+
entityType: this.type,
82+
getOverrideProperties: this.getOverridePropertiesFromEntity,
83+
});
84+
};
85+
86+
renderPreview = (previewType: PreviewType, data: BusinessAttribute) => {
87+
return (
88+
<Preview
89+
previewType={previewType}
90+
urn={data.urn}
91+
name={this.displayName(data)}
92+
description={data.properties?.description || ''}
93+
owners={data.ownership?.owners}
94+
/>
95+
);
96+
};
97+
98+
renderProfile = (urn: string) => {
99+
return (
100+
<EntityProfile
101+
urn={urn}
102+
entityType={EntityType.BusinessAttribute}
103+
useEntityQuery={useGetBusinessAttributeQuery as any}
104+
headerDropdownItems={new Set([EntityMenuItems.DELETE])}
105+
isNameEditable
106+
tabs={[
107+
{
108+
name: 'Documentation',
109+
component: DocumentationTab,
110+
},
111+
{
112+
name: 'Related Entities',
113+
component: BusinessAttributeRelatedEntity,
114+
},
115+
{
116+
name: 'Properties',
117+
component: PropertiesTab,
118+
},
119+
]}
120+
sidebarSections={[
121+
{
122+
component: SidebarAboutSection,
123+
},
124+
{
125+
component: BusinessAttributeDataTypeSection,
126+
},
127+
{
128+
component: SidebarOwnerSection,
129+
},
130+
{
131+
component: SidebarTagsSection,
132+
properties: {
133+
hasTags: true,
134+
hasTerms: true,
135+
customTagPath: 'properties.tags',
136+
customTermPath: 'properties.glossaryTerms',
137+
},
138+
},
139+
]}
140+
getOverrideProperties={this.getOverridePropertiesFromEntity}
141+
/>
142+
);
143+
};
144+
145+
renderSearch = (result: SearchResult) => {
146+
return this.renderPreview(PreviewType.SEARCH, result.entity as BusinessAttribute);
147+
};
148+
149+
supportedCapabilities = () => {
150+
return new Set([
151+
EntityCapabilityType.OWNERS,
152+
EntityCapabilityType.TAGS,
153+
EntityCapabilityType.GLOSSARY_TERMS,
154+
// EntityCapabilityType.BUSINESS_ATTRIBUTES,
155+
]);
156+
};
157+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import React from 'react';
2+
import { GlobalOutlined } from '@ant-design/icons';
3+
import { EntityType, Owner } from '../../../../types.generated';
4+
import DefaultPreviewCard from '../../../preview/DefaultPreviewCard';
5+
import { useEntityRegistry } from '../../../useEntityRegistry';
6+
import { IconStyleType, PreviewType } from '../../Entity';
7+
import UrlButton from '../../shared/UrlButton';
8+
import { getRelatedEntitiesUrl } from '../../../businessAttribute/businessAttributeUtils';
9+
10+
export const Preview = ({
11+
urn,
12+
name,
13+
description,
14+
owners,
15+
previewType,
16+
}: {
17+
urn: string;
18+
name: string;
19+
description?: string | null;
20+
owners?: Array<Owner> | null;
21+
previewType: PreviewType;
22+
}): JSX.Element => {
23+
const entityRegistry = useEntityRegistry();
24+
return (
25+
<DefaultPreviewCard
26+
previewType={previewType}
27+
url={entityRegistry.getEntityUrl(EntityType.BusinessAttribute, urn)}
28+
name={name || ''}
29+
urn={urn}
30+
description={description || ''}
31+
owners={owners}
32+
logoComponent={<GlobalOutlined style={{ fontSize: '20px' }} />}
33+
type="Business Attribute"
34+
typeIcon={entityRegistry.getIcon(EntityType.BusinessAttribute, 14, IconStyleType.ACCENT)}
35+
entityTitleSuffix={
36+
<UrlButton href={getRelatedEntitiesUrl(entityRegistry, urn)}>View Related Entities</UrlButton>
37+
}
38+
/>
39+
);
40+
};
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { MockedProvider } from '@apollo/client/testing';
2+
import { render } from '@testing-library/react';
3+
import React from 'react';
4+
import { mocks } from '../../../../../Mocks';
5+
import TestPageContainer from '../../../../../utils/test-utils/TestPageContainer';
6+
import { Preview } from '../Preview';
7+
import { PreviewType } from '../../../Entity';
8+
9+
describe('Preview', () => {
10+
it('renders', () => {
11+
const { getByText } = render(
12+
<MockedProvider mocks={mocks} addTypename={false}>
13+
<TestPageContainer>
14+
<Preview
15+
urn="urn:li:businessAttribute:ba1"
16+
name="name"
17+
description="definition"
18+
owners={null}
19+
previewType={PreviewType.PREVIEW}
20+
/>
21+
</TestPageContainer>
22+
</MockedProvider>,
23+
);
24+
expect(getByText('definition')).toBeInTheDocument();
25+
});
26+
});
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { Button, message, Select } from 'antd';
2+
import { EditOutlined } from '@ant-design/icons';
3+
import React, { useEffect, useState } from 'react';
4+
import styled from 'styled-components';
5+
import { useEntityData, useRefetch } from '@src/app/entity/shared/EntityContext';
6+
import { SidebarHeader } from '../../shared/containers/profile/sidebar/SidebarHeader';
7+
import { useUpdateBusinessAttributeMutation } from '../../../../graphql/businessAttribute.generated';
8+
import { SchemaFieldDataType } from '../../../businessAttribute/businessAttributeUtils';
9+
10+
interface Props {
11+
readOnly?: boolean;
12+
}
13+
14+
const DataTypeSelect = styled(Select)`
15+
&& {
16+
width: 100%;
17+
margin-top: 1em;
18+
margin-bottom: 1em;
19+
}
20+
`;
21+
// Ensures that any newly added datatype is automatically included in the user dropdown.
22+
const DATA_TYPES = Object.values(SchemaFieldDataType);
23+
export const BusinessAttributeDataTypeSection = ({ readOnly }: Props) => {
24+
const { urn, entityData } = useEntityData();
25+
const [originalDescription, setOriginalDescription] = useState<string | null>(null);
26+
const [isEditing, setEditing] = useState(false);
27+
const refetch = useRefetch();
28+
29+
useEffect(() => {
30+
if (entityData?.properties?.businessAttributeDataType) {
31+
setOriginalDescription(entityData?.properties?.businessAttributeDataType);
32+
}
33+
}, [entityData]);
34+
35+
const [updateBusinessAttribute] = useUpdateBusinessAttributeMutation();
36+
37+
const handleChange = (value) => {
38+
if (value === originalDescription) {
39+
setEditing(false);
40+
return;
41+
}
42+
43+
updateBusinessAttribute({ variables: { urn, input: { type: value } } })
44+
.then(() => {
45+
setEditing(false);
46+
setOriginalDescription(value);
47+
message.success({ content: 'Data Type Updated', duration: 2 });
48+
refetch();
49+
})
50+
.catch((e: unknown) => {
51+
message.destroy();
52+
if (e instanceof Error) {
53+
message.error({ content: `Failed to update Data Type: \n ${e.message || ''}`, duration: 3 });
54+
}
55+
});
56+
};
57+
58+
// Toggle editing mode
59+
const handleEditClick = () => {
60+
setEditing(!isEditing);
61+
};
62+
63+
return (
64+
<div>
65+
<SidebarHeader
66+
title="Data Type"
67+
actions={
68+
!readOnly && (
69+
<Button
70+
data-testid="edit-data-type-button"
71+
onClick={handleEditClick}
72+
type="text"
73+
shape="circle"
74+
>
75+
<EditOutlined />
76+
</Button>
77+
)
78+
}
79+
/>
80+
{originalDescription}
81+
{isEditing && (
82+
<DataTypeSelect
83+
data-testid="add-data-type-option"
84+
placeholder="A data type for business attribute"
85+
onChange={handleChange}
86+
>
87+
{DATA_TYPES.map((dataType: SchemaFieldDataType) => (
88+
<Select.Option key={dataType} value={dataType}>
89+
{dataType}
90+
</Select.Option>
91+
))}
92+
</DataTypeSelect>
93+
)}
94+
</div>
95+
);
96+
};
97+
98+
export default BusinessAttributeDataTypeSection;
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import * as React from 'react';
2+
import { useEntityData } from '@src/app/entity/shared/EntityContext';
3+
import { UnionType } from '../../../search/utils/constants';
4+
import { EmbeddedListSearchSection } from '../../shared/components/styled/search/EmbeddedListSearchSection';
5+
6+
export default function BusinessAttributeRelatedEntity() {
7+
const { entityData } = useEntityData();
8+
9+
const entityUrn = entityData?.urn;
10+
11+
const fixedOrFilters =
12+
(entityUrn && [
13+
{
14+
field: 'businessAttribute',
15+
values: [entityUrn],
16+
},
17+
]) ||
18+
[];
19+
20+
entityData?.isAChildren?.relationships?.forEach((businessAttribute) => {
21+
const childUrn = businessAttribute.entity?.urn;
22+
23+
if (childUrn) {
24+
fixedOrFilters.push({
25+
field: 'businessAttributes',
26+
values: [childUrn],
27+
});
28+
}
29+
});
30+
31+
return (
32+
<EmbeddedListSearchSection
33+
fixedFilters={{
34+
unionType: UnionType.OR,
35+
filters: fixedOrFilters,
36+
}}
37+
emptySearchQuery="*"
38+
placeholderText="Filter entities..."
39+
skipCache
40+
applyView
41+
/>
42+
);
43+
}

0 commit comments

Comments
 (0)