diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/UpdateNameResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/UpdateNameResolver.java index 93e2c9d9e6b75..6529a3a66bfa8 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/UpdateNameResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/mutate/UpdateNameResolver.java @@ -7,6 +7,7 @@ import com.linkedin.datahub.graphql.authorization.AuthorizationUtils; import com.linkedin.datahub.graphql.exception.AuthorizationException; import com.linkedin.datahub.graphql.generated.UpdateNameInput; +import com.linkedin.domain.DomainProperties; import com.linkedin.glossary.GlossaryTermInfo; import com.linkedin.glossary.GlossaryNodeInfo; import com.linkedin.metadata.Constants; @@ -44,6 +45,8 @@ public CompletableFuture get(DataFetchingEnvironment environment) throw return updateGlossaryTermName(targetUrn, input, environment.getContext()); case Constants.GLOSSARY_NODE_ENTITY_NAME: return updateGlossaryNodeName(targetUrn, input, environment.getContext()); + case Constants.DOMAIN_ENTITY_NAME: + return updateDomainName(targetUrn, input, environment.getContext()); default: throw new RuntimeException( String.format("Failed to update name. Unsupported resource type %s provided.", targetUrn)); @@ -98,4 +101,28 @@ private Boolean updateGlossaryNodeName( } throw new AuthorizationException("Unauthorized to perform this action. Please contact your DataHub administrator."); } + + private Boolean updateDomainName( + Urn targetUrn, + UpdateNameInput input, + QueryContext context + ) { + if (AuthorizationUtils.canManageDomains(context)) { + try { + DomainProperties domainProperties = (DomainProperties) getAspectFromEntity( + targetUrn.toString(), Constants.DOMAIN_PROPERTIES_ASPECT_NAME, _entityService, null); + if (domainProperties == null) { + throw new IllegalArgumentException("Domain does not exist"); + } + domainProperties.setName(input.getName()); + Urn actor = CorpuserUrn.createFromString(context.getActorUrn()); + persistAspect(targetUrn, Constants.DOMAIN_PROPERTIES_ASPECT_NAME, domainProperties, actor, _entityService); + + return true; + } catch (Exception e) { + throw new RuntimeException(String.format("Failed to perform update against input %s", input), e); + } + } + throw new AuthorizationException("Unauthorized to perform this action. Please contact your DataHub administrator."); + } } diff --git a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/glossary/UpdateNameResolverTest.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/glossary/UpdateNameResolverTest.java index b704264b1c308..e3edfe0efe134 100644 --- a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/glossary/UpdateNameResolverTest.java +++ b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/glossary/UpdateNameResolverTest.java @@ -7,6 +7,7 @@ import com.linkedin.datahub.graphql.QueryContext; import com.linkedin.datahub.graphql.generated.UpdateNameInput; import com.linkedin.datahub.graphql.resolvers.mutate.UpdateNameResolver; +import com.linkedin.domain.DomainProperties; import com.linkedin.events.metadata.ChangeType; import com.linkedin.glossary.GlossaryNodeInfo; import com.linkedin.glossary.GlossaryTermInfo; @@ -29,8 +30,10 @@ public class UpdateNameResolverTest { private static final String NEW_NAME = "New Name"; private static final String TERM_URN = "urn:li:glossaryTerm:11115397daf94708a8822b8106cfd451"; private static final String NODE_URN = "urn:li:glossaryNode:22225397daf94708a8822b8106cfd451"; + private static final String DOMAIN_URN = "urn:li:domain:22225397daf94708a8822b8106cfd451"; private static final UpdateNameInput INPUT = new UpdateNameInput(NEW_NAME, TERM_URN); private static final UpdateNameInput INPUT_FOR_NODE = new UpdateNameInput(NEW_NAME, NODE_URN); + private static final UpdateNameInput INPUT_FOR_DOMAIN = new UpdateNameInput(NEW_NAME, DOMAIN_URN); private static final CorpuserUrn TEST_ACTOR_URN = new CorpuserUrn("test"); private MetadataChangeProposal setupTests(DataFetchingEnvironment mockEnv, EntityService mockService) throws Exception { @@ -111,6 +114,43 @@ public void testGetSuccessForNode() throws Exception { ); } + @Test + public void testGetSuccessForDomain() throws Exception { + EntityService mockService = Mockito.mock(EntityService.class); + Mockito.when(mockService.exists(Urn.createFromString(DOMAIN_URN))).thenReturn(true); + DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class); + Mockito.when(mockEnv.getArgument("input")).thenReturn(INPUT_FOR_DOMAIN); + + QueryContext mockContext = getMockAllowContext(); + Mockito.when(mockContext.getAuthentication()).thenReturn(Mockito.mock(Authentication.class)); + Mockito.when(mockContext.getActorUrn()).thenReturn(TEST_ACTOR_URN.toString()); + Mockito.when(mockEnv.getContext()).thenReturn(mockContext); + + final String name = "test name"; + Mockito.when(mockService.getAspect( + Urn.createFromString(DOMAIN_URN), + Constants.DOMAIN_PROPERTIES_ASPECT_NAME, + 0)) + .thenReturn(new DomainProperties().setName(name)); + + final MetadataChangeProposal proposal = new MetadataChangeProposal(); + proposal.setEntityUrn(Urn.createFromString(DOMAIN_URN)); + proposal.setEntityType(Constants.DOMAIN_ENTITY_NAME); + DomainProperties properties = new DomainProperties(); + properties.setName(NEW_NAME); + proposal.setAspectName(Constants.DOMAIN_PROPERTIES_ASPECT_NAME); + proposal.setAspect(GenericRecordUtils.serializeAspect(properties)); + proposal.setChangeType(ChangeType.UPSERT); + + UpdateNameResolver resolver = new UpdateNameResolver(mockService); + + assertTrue(resolver.get(mockEnv).get()); + Mockito.verify(mockService, Mockito.times(1)).ingestProposal( + Mockito.eq(proposal), + Mockito.any() + ); + } + @Test public void testGetFailureEntityDoesNotExist() throws Exception { EntityService mockService = Mockito.mock(EntityService.class); diff --git a/datahub-web-react/src/Mocks.tsx b/datahub-web-react/src/Mocks.tsx index 84dbb303b63fd..cab81edff9b19 100644 --- a/datahub-web-react/src/Mocks.tsx +++ b/datahub-web-react/src/Mocks.tsx @@ -26,6 +26,7 @@ import { RecommendationRenderType, RelationshipDirection, Container, + PlatformPrivileges, } from './types.generated'; import { GetTagDocument } from './graphql/tag.generated'; import { GetMlModelDocument } from './graphql/mlModel.generated'; @@ -3289,3 +3290,17 @@ export const mocks = [ }, }, ]; + +export const platformPrivileges: PlatformPrivileges = { + viewAnalytics: true, + managePolicies: true, + manageIdentities: true, + generatePersonalAccessTokens: true, + manageDomains: true, + manageIngestion: true, + manageSecrets: true, + manageTokens: true, + manageTests: true, + manageGlossaries: true, + manageUserCredentials: true, +}; diff --git a/datahub-web-react/src/app/entity/domain/DomainEntity.tsx b/datahub-web-react/src/app/entity/domain/DomainEntity.tsx index 1890fb0dad1bd..fad78c63139bf 100644 --- a/datahub-web-react/src/app/entity/domain/DomainEntity.tsx +++ b/datahub-web-react/src/app/entity/domain/DomainEntity.tsx @@ -65,6 +65,7 @@ export class DomainEntity implements Entity { useUpdateQuery={undefined} getOverrideProperties={this.getOverridePropertiesFromEntity} headerDropdownItems={new Set([EntityMenuItems.COPY_URL])} + isNameEditable tabs={[ { name: 'Entities', diff --git a/datahub-web-react/src/app/entity/shared/containers/profile/__tests__/EntityHeader.test.tsx b/datahub-web-react/src/app/entity/shared/containers/profile/__tests__/EntityHeader.test.tsx new file mode 100644 index 0000000000000..9ebf2923880f4 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/containers/profile/__tests__/EntityHeader.test.tsx @@ -0,0 +1,50 @@ +import { platformPrivileges } from '../../../../../../Mocks'; +import { EntityType } from '../../../../../../types.generated'; +import { getCanEditName } from '../header/EntityHeader'; + +describe('getCanEditName', () => { + it('should return true for Terms if manageGlossaries privilege is true', () => { + const canEditName = getCanEditName(EntityType.GlossaryTerm, platformPrivileges); + + expect(canEditName).toBe(true); + }); + + it('should return false for Terms if manageGlossaries privilege is false', () => { + const privilegesWithoutGlossaries = { ...platformPrivileges, manageGlossaries: false }; + const canEditName = getCanEditName(EntityType.GlossaryTerm, privilegesWithoutGlossaries); + + expect(canEditName).toBe(false); + }); + + it('should return true for Nodes if manageGlossaries privilege is true', () => { + const canEditName = getCanEditName(EntityType.GlossaryNode, platformPrivileges); + + expect(canEditName).toBe(true); + }); + + it('should return false for Nodes if manageGlossaries privilege is false', () => { + const privilegesWithoutGlossaries = { ...platformPrivileges, manageGlossaries: false }; + const canEditName = getCanEditName(EntityType.GlossaryNode, privilegesWithoutGlossaries); + + expect(canEditName).toBe(false); + }); + + it('should return true for Domains if manageDomains privilege is true', () => { + const canEditName = getCanEditName(EntityType.Domain, platformPrivileges); + + expect(canEditName).toBe(true); + }); + + it('should return false for Domains if manageDomains privilege is false', () => { + const privilegesWithoutDomains = { ...platformPrivileges, manageDomains: false }; + const canEditName = getCanEditName(EntityType.Domain, privilegesWithoutDomains); + + expect(canEditName).toBe(false); + }); + + it('should return false for an unsupported entity', () => { + const canEditName = getCanEditName(EntityType.Chart, platformPrivileges); + + expect(canEditName).toBe(false); + }); +}); diff --git a/datahub-web-react/src/app/entity/shared/containers/profile/header/EntityHeader.tsx b/datahub-web-react/src/app/entity/shared/containers/profile/header/EntityHeader.tsx index b5a34ee9a1edd..fb7d22a13b350 100644 --- a/datahub-web-react/src/app/entity/shared/containers/profile/header/EntityHeader.tsx +++ b/datahub-web-react/src/app/entity/shared/containers/profile/header/EntityHeader.tsx @@ -100,11 +100,13 @@ const TopButtonsWrapper = styled.div` margin-bottom: 8px; `; -function getCanEditName(entityType: EntityType, privileges?: PlatformPrivileges) { +export function getCanEditName(entityType: EntityType, privileges?: PlatformPrivileges) { switch (entityType) { case EntityType.GlossaryTerm: case EntityType.GlossaryNode: return privileges?.manageGlossaries; + case EntityType.Domain: + return privileges?.manageDomains; default: return false; }