Skip to content

Commit 5417747

Browse files
feat(ui/backend): business attributes support for new ui (#15452)
1 parent 47f2653 commit 5417747

File tree

22 files changed

+615
-20
lines changed

22 files changed

+615
-20
lines changed

datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/entity/EntityPrivilegesResolver.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import com.linkedin.datahub.graphql.generated.Entity;
1515
import com.linkedin.datahub.graphql.generated.EntityPrivileges;
1616
import com.linkedin.datahub.graphql.resolvers.assertion.AssertionUtils;
17+
import com.linkedin.datahub.graphql.resolvers.businessattribute.BusinessAttributeAuthorizationUtils;
1718
import com.linkedin.datahub.graphql.resolvers.dataproduct.DataProductAuthorizationUtils;
1819
import com.linkedin.datahub.graphql.resolvers.incident.IncidentUtils;
1920
import com.linkedin.datahub.graphql.resolvers.mutate.DescriptionUtils;
@@ -52,6 +53,8 @@ public CompletableFuture<EntityPrivileges> get(DataFetchingEnvironment environme
5253
return GraphQLConcurrencyUtils.supplyAsync(
5354
() -> {
5455
switch (urn.getEntityType()) {
56+
case Constants.BUSINESS_ATTRIBUTE_ENTITY_NAME:
57+
return getBusinessAttributePrivileges(urn, context);
5558
case Constants.GLOSSARY_TERM_ENTITY_NAME:
5659
return getGlossaryTermPrivileges(urn, context);
5760
case Constants.GLOSSARY_NODE_ENTITY_NAME:
@@ -79,6 +82,14 @@ public CompletableFuture<EntityPrivileges> get(DataFetchingEnvironment environme
7982
"get");
8083
}
8184

85+
private EntityPrivileges getBusinessAttributePrivileges(Urn urn, QueryContext context) {
86+
final EntityPrivileges result = new EntityPrivileges();
87+
result.setCanManageEntity(
88+
BusinessAttributeAuthorizationUtils.canManageBusinessAttribute(context));
89+
addCommonPrivileges(result, urn, context);
90+
return result;
91+
}
92+
8293
private EntityPrivileges getGlossaryTermPrivileges(Urn termUrn, QueryContext context) {
8394
final EntityPrivileges result = new EntityPrivileges();
8495
addCommonPrivileges(result, termUrn, context);

datahub-graphql-core/src/main/resources/entity.graphql

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13996,6 +13996,11 @@ type BusinessAttribute implements Entity {
1399613996
List of relationships between the source Entity and some destination entities with a given types
1399713997
"""
1399813998
relationships(input: RelationshipsInput!): EntityRelationshipsResult
13999+
14000+
"""
14001+
Privileges given to a user relevant to this entity
14002+
"""
14003+
privileges: EntityPrivileges
1399914004
}
1400014005

1400114006
"""

datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/entity/EntityPrivilegesResolverTest.java

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import com.datahub.authentication.Authentication;
88
import com.linkedin.datahub.graphql.QueryContext;
9+
import com.linkedin.datahub.graphql.generated.BusinessAttribute;
910
import com.linkedin.datahub.graphql.generated.Chart;
1011
import com.linkedin.datahub.graphql.generated.Dashboard;
1112
import com.linkedin.datahub.graphql.generated.DataJob;
@@ -31,6 +32,7 @@ public class EntityPrivilegesResolverTest {
3132
final String dashboardUrn = "urn:li:dashboard:(looker,dashboards.1)";
3233
final String dataJobUrn =
3334
"urn:li:dataJob:(urn:li:dataFlow:(spark,test_machine.sparkTestApp,local),QueryExecId_31)";
35+
final String businessAttributeUrn = "urn:li:businessAttribute:testBusinessAttribute";
3436

3537
private DataFetchingEnvironment setUpTestWithPermissions(Entity entity) {
3638
QueryContext mockContext = getMockAllowContext();
@@ -238,4 +240,77 @@ public void testGetDataJobSuccessWithoutPermissions() throws Exception {
238240

239241
assertFalse(result.getCanEditLineage());
240242
}
243+
244+
@Test
245+
public void testGetBusinessAttributeSuccessWithPermissions() throws Exception {
246+
final BusinessAttribute businessAttribute = new BusinessAttribute();
247+
businessAttribute.setUrn(businessAttributeUrn);
248+
249+
EntityClient mockClient = Mockito.mock(EntityClient.class);
250+
DataFetchingEnvironment mockEnv = setUpTestWithPermissions(businessAttribute);
251+
252+
EntityPrivilegesResolver resolver = new EntityPrivilegesResolver(mockClient);
253+
EntityPrivileges result = resolver.get(mockEnv).get();
254+
255+
assertTrue(result.getCanManageEntity());
256+
// Verify common privileges are also set
257+
assertTrue(result.getCanEditLineage());
258+
assertTrue(result.getCanEditProperties());
259+
assertTrue(result.getCanEditTags());
260+
assertTrue(result.getCanEditGlossaryTerms());
261+
assertTrue(result.getCanEditOwners());
262+
assertTrue(result.getCanEditDescription());
263+
assertTrue(result.getCanEditLinks());
264+
}
265+
266+
@Test
267+
public void testGetBusinessAttributeSuccessWithoutPermissions() throws Exception {
268+
final BusinessAttribute businessAttribute = new BusinessAttribute();
269+
businessAttribute.setUrn(businessAttributeUrn);
270+
271+
EntityClient mockClient = Mockito.mock(EntityClient.class);
272+
DataFetchingEnvironment mockEnv = setUpTestWithoutPermissions(businessAttribute);
273+
274+
EntityPrivilegesResolver resolver = new EntityPrivilegesResolver(mockClient);
275+
EntityPrivileges result = resolver.get(mockEnv).get();
276+
277+
assertFalse(result.getCanManageEntity());
278+
// Verify common privileges are also denied
279+
assertFalse(result.getCanEditLineage());
280+
assertFalse(result.getCanEditProperties());
281+
assertFalse(result.getCanEditTags());
282+
assertFalse(result.getCanEditGlossaryTerms());
283+
assertFalse(result.getCanEditOwners());
284+
assertFalse(result.getCanEditDescription());
285+
assertFalse(result.getCanEditLinks());
286+
}
287+
288+
@Test
289+
public void testGetBusinessAttributeWithCommonPrivileges() throws Exception {
290+
final BusinessAttribute businessAttribute = new BusinessAttribute();
291+
businessAttribute.setUrn(businessAttributeUrn);
292+
293+
EntityClient mockClient = Mockito.mock(EntityClient.class);
294+
DataFetchingEnvironment mockEnv = setUpTestWithPermissions(businessAttribute);
295+
296+
EntityPrivilegesResolver resolver = new EntityPrivilegesResolver(mockClient);
297+
EntityPrivileges result = resolver.get(mockEnv).get();
298+
299+
// Test that business attribute gets all common privileges
300+
assertNotNull(result);
301+
assertTrue(result.getCanManageEntity());
302+
assertTrue(result.getCanEditLineage());
303+
assertTrue(result.getCanEditProperties());
304+
assertTrue(result.getCanEditAssertions());
305+
assertTrue(result.getCanEditIncidents());
306+
assertTrue(result.getCanEditDomains());
307+
assertTrue(result.getCanEditDataProducts());
308+
assertTrue(result.getCanEditDeprecation());
309+
assertTrue(result.getCanEditGlossaryTerms());
310+
assertTrue(result.getCanEditTags());
311+
assertTrue(result.getCanEditOwners());
312+
assertTrue(result.getCanEditDescription());
313+
assertTrue(result.getCanEditLinks());
314+
assertTrue(result.getCanManageAssetSummary());
315+
}
241316
}

datahub-web-react/src/app/entityV2/Entity.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,10 @@ export enum EntityCapabilityType {
105105
* Related context documents for this entity
106106
*/
107107
RELATED_DOCUMENTS,
108+
/**
109+
* Adding a business attribute to the entity
110+
*/
111+
BUSINESS_ATTRIBUTES,
108112
}
109113

110114
export interface EntityMenuActions {

datahub-web-react/src/app/entityV2/businessAttribute/BusinessAttributeEntity.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { EntityMenuItems } from '@app/entityV2/shared/EntityDropdown/EntityMenuA
99
import { EntityProfile } from '@app/entityV2/shared/containers/profile/EntityProfile';
1010
import { SidebarAboutSection } from '@app/entityV2/shared/containers/profile/sidebar/AboutSection/SidebarAboutSection';
1111
import { SidebarOwnerSection } from '@app/entityV2/shared/containers/profile/sidebar/Ownership/sidebar/SidebarOwnerSection';
12+
import { SidebarGlossaryTermsSection } from '@app/entityV2/shared/containers/profile/sidebar/SidebarGlossaryTermsSection';
1213
import { SidebarTagsSection } from '@app/entityV2/shared/containers/profile/sidebar/SidebarTagsSection';
1314
import { getDataForEntityType } from '@app/entityV2/shared/containers/profile/utils';
1415
import { DocumentationTab } from '@app/entityV2/shared/tabs/Documentation/DocumentationTab';
@@ -128,9 +129,12 @@ export class BusinessAttributeEntity implements Entity<BusinessAttribute> {
128129
{
129130
component: SidebarTagsSection,
130131
properties: {
131-
hasTags: true,
132-
hasTerms: true,
133132
customTagPath: 'properties.tags',
133+
},
134+
},
135+
{
136+
component: SidebarGlossaryTermsSection,
137+
properties: {
134138
customTermPath: 'properties.glossaryTerms',
135139
},
136140
},
@@ -149,7 +153,7 @@ export class BusinessAttributeEntity implements Entity<BusinessAttribute> {
149153
EntityCapabilityType.OWNERS,
150154
EntityCapabilityType.TAGS,
151155
EntityCapabilityType.GLOSSARY_TERMS,
152-
// EntityCapabilityType.BUSINESS_ATTRIBUTES,
156+
EntityCapabilityType.BUSINESS_ATTRIBUTES,
153157
]);
154158
};
155159
}

datahub-web-react/src/app/entityV2/shared/containers/profile/sidebar/SidebarGlossaryTermsSection.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { EMPTY_MESSAGES } from '@app/entityV2/shared/constants';
77
import EmptySectionText from '@app/entityV2/shared/containers/profile/sidebar/EmptySectionText';
88
import SectionActionButton from '@app/entityV2/shared/containers/profile/sidebar/SectionActionButton';
99
import { SidebarSection } from '@app/entityV2/shared/containers/profile/sidebar/SidebarSection';
10+
import { useEntityDataExtractor } from '@app/entityV2/shared/containers/profile/sidebar/hooks/useEntityDataExtractor';
1011
import { ENTITY_PROFILE_GLOSSARY_TERMS_ID } from '@app/onboarding/config/EntityProfileOnboardingConfig';
1112
import AddTagTerm from '@app/sharedV2/tags/AddTagTerm';
1213
import TagTermGroup from '@app/sharedV2/tags/TagTermGroup';
@@ -22,17 +23,23 @@ const Content = styled.div`
2223

2324
interface Props {
2425
readOnly?: boolean;
26+
properties?: any;
2527
}
2628

27-
export const SidebarGlossaryTermsSection = ({ readOnly }: Props) => {
29+
export const SidebarGlossaryTermsSection = ({ readOnly, properties }: Props) => {
2830
const { entityType, entityData } = useEntityData();
2931
const refetch = useRefetch();
3032
const mutationUrn = useMutationUrn();
3133

3234
const [showAddModal, setShowAddModal] = useState(false);
3335
const [addModalType, setAddModalType] = useState<EntityType | undefined>(undefined);
3436

35-
const areTermsEmpty = !entityData?.glossaryTerms?.terms?.length;
37+
// Extract glossary terms using custom hook
38+
const { data: glossaryTerms, isEmpty: areTermsEmpty } = useEntityDataExtractor({
39+
customPath: properties?.customTermPath,
40+
defaultPath: 'glossaryTerms',
41+
arrayProperty: 'terms',
42+
});
3643

3744
const canEditTerms = !!entityData?.privileges?.canEditGlossaryTerms;
3845

@@ -44,7 +51,7 @@ export const SidebarGlossaryTermsSection = ({ readOnly }: Props) => {
4451
<Content>
4552
{!areTermsEmpty ? (
4653
<TagTermGroup
47-
editableGlossaryTerms={entityData?.glossaryTerms}
54+
editableGlossaryTerms={glossaryTerms}
4855
canAddTerm
4956
canRemove
5057
entityUrn={mutationUrn}

datahub-web-react/src/app/entityV2/shared/containers/profile/sidebar/SidebarTagsSection.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { EMPTY_MESSAGES } from '@app/entityV2/shared/constants';
77
import EmptySectionText from '@app/entityV2/shared/containers/profile/sidebar/EmptySectionText';
88
import SectionActionButton from '@app/entityV2/shared/containers/profile/sidebar/SectionActionButton';
99
import { SidebarSection } from '@app/entityV2/shared/containers/profile/sidebar/SidebarSection';
10+
import { useEntityDataExtractor } from '@app/entityV2/shared/containers/profile/sidebar/hooks/useEntityDataExtractor';
1011
import { ENTITY_PROFILE_TAGS_ID } from '@app/onboarding/config/EntityProfileOnboardingConfig';
1112
import AddTagTerm from '@app/sharedV2/tags/AddTagTerm';
1213
import TagTermGroup from '@app/sharedV2/tags/TagTermGroup';
@@ -22,17 +23,23 @@ const Content = styled.div`
2223

2324
interface Props {
2425
readOnly?: boolean;
26+
properties?: any;
2527
}
2628

27-
export const SidebarTagsSection = ({ readOnly }: Props) => {
29+
export const SidebarTagsSection = ({ readOnly, properties }: Props) => {
2830
const { entityType, entityData } = useEntityData();
2931
const refetch = useRefetch();
3032
const mutationUrn = useMutationUrn();
3133

3234
const [showAddModal, setShowAddModal] = useState(false);
3335
const [addModalType, setAddModalType] = useState<EntityType | undefined>(undefined);
3436

35-
const areTagsEmpty = !entityData?.globalTags?.tags?.length;
37+
// Extract tags using custom hook
38+
const { data: tags, isEmpty: areTagsEmpty } = useEntityDataExtractor({
39+
customPath: properties?.customTagPath,
40+
defaultPath: 'globalTags',
41+
arrayProperty: 'tags',
42+
});
3643

3744
const canEditTags = !!entityData?.privileges?.canEditTags;
3845

@@ -44,7 +51,7 @@ export const SidebarTagsSection = ({ readOnly }: Props) => {
4451
<Content>
4552
{!areTagsEmpty ? (
4653
<TagTermGroup
47-
editableTags={entityData?.globalTags}
54+
editableTags={tags}
4855
canAddTag
4956
canRemove
5057
showEmptyMessage

0 commit comments

Comments
 (0)