feat(groups & owners) Add ability to edit group name + assign creator as owner of metadata (#6047)

Adds ability to edit user group names from the UI. Also sets the creator or Glossary Terms, Glossary Nodes, and Domains to be the owner of said item.
This commit is contained in:
Ankit keshari 2022-10-07 01:06:39 +05:30 committed by GitHub
parent 3106e42e89
commit 11092c73cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 230 additions and 50 deletions

View File

@ -767,7 +767,7 @@ public class GmsGraphQLEngine {
.dataFetcher("removeUser", new RemoveUserResolver(this.entityClient))
.dataFetcher("removeGroup", new RemoveGroupResolver(this.entityClient))
.dataFetcher("updateUserStatus", new UpdateUserStatusResolver(this.entityClient))
.dataFetcher("createDomain", new CreateDomainResolver(this.entityClient))
.dataFetcher("createDomain", new CreateDomainResolver(this.entityClient, this.entityService))
.dataFetcher("deleteDomain", new DeleteDomainResolver(entityClient))
.dataFetcher("setDomain", new SetDomainResolver(this.entityClient, this.entityService))
.dataFetcher("batchSetDomain", new BatchSetDomainResolver(this.entityService))
@ -789,8 +789,8 @@ public class GmsGraphQLEngine {
.dataFetcher("updateTest", new UpdateTestResolver(this.entityClient))
.dataFetcher("deleteTest", new DeleteTestResolver(this.entityClient))
.dataFetcher("reportOperation", new ReportOperationResolver(this.entityClient))
.dataFetcher("createGlossaryTerm", new CreateGlossaryTermResolver(this.entityClient))
.dataFetcher("createGlossaryNode", new CreateGlossaryNodeResolver(this.entityClient))
.dataFetcher("createGlossaryTerm", new CreateGlossaryTermResolver(this.entityClient, this.entityService))
.dataFetcher("createGlossaryNode", new CreateGlossaryNodeResolver(this.entityClient, this.entityService))
.dataFetcher("updateParentNode", new UpdateParentNodeResolver(entityService))
.dataFetcher("deleteGlossaryEntity",
new DeleteGlossaryEntityResolver(this.entityClient, this.entityService))

View File

@ -5,9 +5,13 @@ import com.linkedin.datahub.graphql.QueryContext;
import com.linkedin.datahub.graphql.authorization.AuthorizationUtils;
import com.linkedin.datahub.graphql.exception.AuthorizationException;
import com.linkedin.datahub.graphql.generated.CreateDomainInput;
import com.linkedin.datahub.graphql.generated.OwnerEntityType;
import com.linkedin.datahub.graphql.generated.OwnershipType;
import com.linkedin.datahub.graphql.resolvers.mutate.util.OwnerUtils;
import com.linkedin.domain.DomainProperties;
import com.linkedin.entity.client.EntityClient;
import com.linkedin.events.metadata.ChangeType;
import com.linkedin.metadata.entity.EntityService;
import com.linkedin.metadata.Constants;
import com.linkedin.metadata.key.DomainKey;
import com.linkedin.metadata.utils.EntityKeyUtils;
@ -30,6 +34,7 @@ import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.*;
public class CreateDomainResolver implements DataFetcher<CompletableFuture<String>> {
private final EntityClient _entityClient;
private final EntityService _entityService;
@Override
public CompletableFuture<String> get(DataFetchingEnvironment environment) throws Exception {
@ -63,7 +68,9 @@ public class CreateDomainResolver implements DataFetcher<CompletableFuture<Strin
proposal.setAspect(GenericRecordUtils.serializeAspect(mapDomainProperties(input)));
proposal.setChangeType(ChangeType.UPSERT);
return _entityClient.ingestProposal(proposal, context.getAuthentication());
String domainUrn = _entityClient.ingestProposal(proposal, context.getAuthentication());
OwnerUtils.addCreatorAsOwner(context, domainUrn, OwnerEntityType.CORP_USER, OwnershipType.TECHNICAL_OWNER, _entityService);
return domainUrn;
} catch (Exception e) {
log.error("Failed to create Domain with id: {}, name: {}: {}", input.getId(), input.getName(), e.getMessage());
throw new RuntimeException(String.format("Failed to create Domain with id: %s, name: %s", input.getId(), input.getName()), e);

View File

@ -6,9 +6,13 @@ import com.linkedin.datahub.graphql.QueryContext;
import com.linkedin.datahub.graphql.authorization.AuthorizationUtils;
import com.linkedin.datahub.graphql.exception.AuthorizationException;
import com.linkedin.datahub.graphql.generated.CreateGlossaryEntityInput;
import com.linkedin.datahub.graphql.generated.OwnerEntityType;
import com.linkedin.datahub.graphql.generated.OwnershipType;
import com.linkedin.datahub.graphql.resolvers.mutate.util.OwnerUtils;
import com.linkedin.entity.client.EntityClient;
import com.linkedin.events.metadata.ChangeType;
import com.linkedin.glossary.GlossaryNodeInfo;
import com.linkedin.metadata.entity.EntityService;
import com.linkedin.metadata.Constants;
import com.linkedin.metadata.key.GlossaryNodeKey;
import com.linkedin.metadata.utils.EntityKeyUtils;
@ -30,6 +34,7 @@ import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument;
public class CreateGlossaryNodeResolver implements DataFetcher<CompletableFuture<String>> {
private final EntityClient _entityClient;
private final EntityService _entityService;
@Override
public CompletableFuture<String> get(DataFetchingEnvironment environment) throws Exception {
@ -56,7 +61,9 @@ public class CreateGlossaryNodeResolver implements DataFetcher<CompletableFuture
proposal.setAspect(GenericRecordUtils.serializeAspect(mapGlossaryNodeInfo(input)));
proposal.setChangeType(ChangeType.UPSERT);
return _entityClient.ingestProposal(proposal, context.getAuthentication());
String glossaryNodeUrn = _entityClient.ingestProposal(proposal, context.getAuthentication());
OwnerUtils.addCreatorAsOwner(context, glossaryNodeUrn, OwnerEntityType.CORP_USER, OwnershipType.TECHNICAL_OWNER, _entityService);
return glossaryNodeUrn;
} catch (Exception e) {
log.error("Failed to create GlossaryNode with id: {}, name: {}: {}", input.getId(), input.getName(), e.getMessage());
throw new RuntimeException(String.format("Failed to create GlossaryNode with id: %s, name: %s", input.getId(), input.getName()), e);

View File

@ -6,9 +6,13 @@ import com.linkedin.datahub.graphql.QueryContext;
import com.linkedin.datahub.graphql.authorization.AuthorizationUtils;
import com.linkedin.datahub.graphql.exception.AuthorizationException;
import com.linkedin.datahub.graphql.generated.CreateGlossaryEntityInput;
import com.linkedin.datahub.graphql.generated.OwnerEntityType;
import com.linkedin.datahub.graphql.generated.OwnershipType;
import com.linkedin.datahub.graphql.resolvers.mutate.util.OwnerUtils;
import com.linkedin.entity.client.EntityClient;
import com.linkedin.events.metadata.ChangeType;
import com.linkedin.glossary.GlossaryTermInfo;
import com.linkedin.metadata.entity.EntityService;
import com.linkedin.metadata.Constants;
import com.linkedin.metadata.key.GlossaryTermKey;
import com.linkedin.metadata.utils.EntityKeyUtils;
@ -30,6 +34,7 @@ import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument;
public class CreateGlossaryTermResolver implements DataFetcher<CompletableFuture<String>> {
private final EntityClient _entityClient;
private final EntityService _entityService;
@Override
public CompletableFuture<String> get(DataFetchingEnvironment environment) throws Exception {
@ -56,7 +61,9 @@ public class CreateGlossaryTermResolver implements DataFetcher<CompletableFuture
proposal.setAspect(GenericRecordUtils.serializeAspect(mapGlossaryTermInfo(input)));
proposal.setChangeType(ChangeType.UPSERT);
return _entityClient.ingestProposal(proposal, context.getAuthentication());
String glossaryTermUrn = _entityClient.ingestProposal(proposal, context.getAuthentication());
OwnerUtils.addCreatorAsOwner(context, glossaryTermUrn, OwnerEntityType.CORP_USER, OwnershipType.TECHNICAL_OWNER, _entityService);
return glossaryTermUrn;
} catch (Exception e) {
log.error("Failed to create GlossaryTerm with id: {}, name: {}: {}", input.getId(), input.getName(), e.getMessage());
throw new RuntimeException(String.format("Failed to create GlossaryTerm with id: %s, name: %s", input.getId(), input.getName()), e);

View File

@ -10,6 +10,7 @@ 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.identity.CorpGroupInfo;
import com.linkedin.metadata.Constants;
import com.linkedin.metadata.entity.EntityService;
import graphql.schema.DataFetcher;
@ -47,6 +48,8 @@ public class UpdateNameResolver implements DataFetcher<CompletableFuture<Boolean
return updateGlossaryNodeName(targetUrn, input, environment.getContext());
case Constants.DOMAIN_ENTITY_NAME:
return updateDomainName(targetUrn, input, environment.getContext());
case Constants.CORP_GROUP_ENTITY_NAME:
return updateGroupName(targetUrn, input, environment.getContext());
default:
throw new RuntimeException(
String.format("Failed to update name. Unsupported resource type %s provided.", targetUrn));
@ -125,4 +128,28 @@ public class UpdateNameResolver implements DataFetcher<CompletableFuture<Boolean
}
throw new AuthorizationException("Unauthorized to perform this action. Please contact your DataHub administrator.");
}
private Boolean updateGroupName(
Urn targetUrn,
UpdateNameInput input,
QueryContext context
) {
if (AuthorizationUtils.canManageUsersAndGroups(context)) {
try {
CorpGroupInfo corpGroupInfo = (CorpGroupInfo) getAspectFromEntity(
targetUrn.toString(), Constants.CORP_GROUP_INFO_ASPECT_NAME, _entityService, null);
if (corpGroupInfo == null) {
throw new IllegalArgumentException("Group does not exist");
}
corpGroupInfo.setDisplayName(input.getName());
Urn actor = CorpuserUrn.createFromString(context.getActorUrn());
persistAspect(targetUrn, Constants.CORP_GROUP_INFO_ASPECT_NAME, corpGroupInfo, 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.");
}
}

View File

@ -1,6 +1,7 @@
package com.linkedin.datahub.graphql.resolvers.mutate.util;
import com.google.common.collect.ImmutableList;
import com.linkedin.common.urn.CorpuserUrn;
import com.linkedin.common.Owner;
import com.linkedin.common.OwnerArray;
@ -34,6 +35,7 @@ import static com.linkedin.datahub.graphql.resolvers.mutate.MutationUtils.*;
// TODO: Move to consuming from OwnerService
@Slf4j
public class OwnerUtils {
private static final ConjunctivePrivilegeGroup ALL_PRIVILEGES_GROUP = new ConjunctivePrivilegeGroup(ImmutableList.of(
PoliciesConfig.EDIT_ENTITY_PRIVILEGE.getType()
));
@ -218,4 +220,23 @@ public class OwnerUtils {
entityService.ingestProposal(change, getAuditStamp(actor), false);
}
}
public static void addCreatorAsOwner(
QueryContext context,
String urn,
OwnerEntityType ownerEntityType,
com.linkedin.datahub.graphql.generated.OwnershipType ownershipType,
EntityService entityService) {
try {
Urn actorUrn = CorpuserUrn.createFromString(context.getActorUrn());
addOwnersToResources(
ImmutableList.of(new OwnerInput(actorUrn.toString(), ownerEntityType, ownershipType)),
ImmutableList.of(new ResourceRefInput(urn, null, null)),
actorUrn,
entityService
);
} catch (Exception e) {
log.error(String.format("Failed to add creator as owner of tag %s", urn), e);
}
}
}

View File

@ -1,17 +1,12 @@
package com.linkedin.datahub.graphql.resolvers.tag;
import com.google.common.collect.ImmutableList;
import com.linkedin.common.urn.CorpuserUrn;
import com.linkedin.common.urn.Urn;
import com.linkedin.data.template.SetMode;
import com.linkedin.datahub.graphql.QueryContext;
import com.linkedin.datahub.graphql.authorization.AuthorizationUtils;
import com.linkedin.datahub.graphql.exception.AuthorizationException;
import com.linkedin.datahub.graphql.generated.CreateTagInput;
import com.linkedin.datahub.graphql.generated.OwnerEntityType;
import com.linkedin.datahub.graphql.generated.OwnerInput;
import com.linkedin.datahub.graphql.generated.OwnershipType;
import com.linkedin.datahub.graphql.generated.ResourceRefInput;
import com.linkedin.datahub.graphql.resolvers.mutate.util.OwnerUtils;
import com.linkedin.entity.client.EntityClient;
import com.linkedin.events.metadata.ChangeType;
@ -74,7 +69,7 @@ public class CreateTagResolver implements DataFetcher<CompletableFuture<String>>
proposal.setChangeType(ChangeType.UPSERT);
String tagUrn = _entityClient.ingestProposal(proposal, context.getAuthentication());
addCreatorAsOwner(context, tagUrn);
OwnerUtils.addCreatorAsOwner(context, tagUrn, OwnerEntityType.CORP_USER, OwnershipType.TECHNICAL_OWNER, _entityService);
return tagUrn;
} catch (Exception e) {
log.error("Failed to create Tag with id: {}, name: {}: {}", input.getId(), input.getName(), e.getMessage());
@ -89,18 +84,4 @@ public class CreateTagResolver implements DataFetcher<CompletableFuture<String>>
result.setDescription(input.getDescription(), SetMode.IGNORE_NULL);
return result;
}
private void addCreatorAsOwner(QueryContext context, String tagUrn) {
try {
Urn actorUrn = CorpuserUrn.createFromString(context.getActorUrn());
OwnerUtils.addOwnersToResources(
ImmutableList.of(new OwnerInput(actorUrn.toString(), OwnerEntityType.CORP_USER, OwnershipType.TECHNICAL_OWNER)),
ImmutableList.of(new ResourceRefInput(tagUrn, null, null)),
actorUrn,
_entityService
);
} catch (Exception e) {
log.error(String.format("Failed to add creator as owner of tag %s", tagUrn), e);
}
}
}

View File

@ -9,6 +9,7 @@ import com.linkedin.events.metadata.ChangeType;
import com.linkedin.metadata.Constants;
import com.linkedin.metadata.key.DomainKey;
import com.linkedin.metadata.utils.GenericRecordUtils;
import com.linkedin.metadata.entity.EntityService;
import com.linkedin.mxe.MetadataChangeProposal;
import com.linkedin.r2.RemoteInvocationException;
import graphql.schema.DataFetchingEnvironment;
@ -27,12 +28,16 @@ public class CreateDomainResolverTest {
"test-name",
"test-description"
);
private static final String TEST_ENTITY_URN = "urn:li:dataset:(urn:li:dataPlatform:mysql,my-test,PROD)";
private static final String TEST_TAG_1_URN = "urn:li:tag:test-id-1";
private static final String TEST_TAG_2_URN = "urn:li:tag:test-id-2";
@Test
public void testGetSuccess() throws Exception {
// Create resolver
EntityClient mockClient = Mockito.mock(EntityClient.class);
CreateDomainResolver resolver = new CreateDomainResolver(mockClient);
EntityService mockService = Mockito.mock(EntityService.class);
CreateDomainResolver resolver = new CreateDomainResolver(mockClient, mockService);
// Execute resolver
QueryContext mockContext = getMockAllowContext();
@ -65,7 +70,8 @@ public class CreateDomainResolverTest {
public void testGetUnauthorized() throws Exception {
// Create resolver
EntityClient mockClient = Mockito.mock(EntityClient.class);
CreateDomainResolver resolver = new CreateDomainResolver(mockClient);
EntityService mockService = Mockito.mock(EntityService.class);
CreateDomainResolver resolver = new CreateDomainResolver(mockClient, mockService);
// Execute resolver
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
@ -83,10 +89,11 @@ public class CreateDomainResolverTest {
public void testGetEntityClientException() throws Exception {
// Create resolver
EntityClient mockClient = Mockito.mock(EntityClient.class);
EntityService mockService = Mockito.mock(EntityService.class);
Mockito.doThrow(RemoteInvocationException.class).when(mockClient).ingestProposal(
Mockito.any(),
Mockito.any(Authentication.class));
CreateDomainResolver resolver = new CreateDomainResolver(mockClient);
CreateDomainResolver resolver = new CreateDomainResolver(mockClient, mockService);
// Execute resolver
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);

View File

@ -10,6 +10,7 @@ import com.linkedin.glossary.GlossaryNodeInfo;
import com.linkedin.metadata.Constants;
import com.linkedin.metadata.key.GlossaryNodeKey;
import com.linkedin.metadata.utils.GenericRecordUtils;
import com.linkedin.metadata.entity.EntityService;
import com.linkedin.mxe.MetadataChangeProposal;
import graphql.schema.DataFetchingEnvironment;
import org.mockito.Mockito;
@ -74,10 +75,11 @@ public class CreateGlossaryNodeResolverTest {
@Test
public void testGetSuccess() throws Exception {
EntityClient mockClient = Mockito.mock(EntityClient.class);
EntityService mockService = Mockito.mock(EntityService.class);
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
final MetadataChangeProposal proposal = setupTest(mockEnv, TEST_INPUT, "test-description", parentNodeUrn);
CreateGlossaryNodeResolver resolver = new CreateGlossaryNodeResolver(mockClient);
CreateGlossaryNodeResolver resolver = new CreateGlossaryNodeResolver(mockClient, mockService);
resolver.get(mockEnv).get();
Mockito.verify(mockClient, Mockito.times(1)).ingestProposal(
@ -89,10 +91,11 @@ public class CreateGlossaryNodeResolverTest {
@Test
public void testGetSuccessNoDescription() throws Exception {
EntityClient mockClient = Mockito.mock(EntityClient.class);
EntityService mockService = Mockito.mock(EntityService.class);
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
final MetadataChangeProposal proposal = setupTest(mockEnv, TEST_INPUT_NO_DESCRIPTION, "", parentNodeUrn);
CreateGlossaryNodeResolver resolver = new CreateGlossaryNodeResolver(mockClient);
CreateGlossaryNodeResolver resolver = new CreateGlossaryNodeResolver(mockClient, mockService);
resolver.get(mockEnv).get();
Mockito.verify(mockClient, Mockito.times(1)).ingestProposal(
@ -104,10 +107,11 @@ public class CreateGlossaryNodeResolverTest {
@Test
public void testGetSuccessNoParentNode() throws Exception {
EntityClient mockClient = Mockito.mock(EntityClient.class);
EntityService mockService = Mockito.mock(EntityService.class);
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
final MetadataChangeProposal proposal = setupTest(mockEnv, TEST_INPUT_NO_PARENT_NODE, "test-description", null);
CreateGlossaryNodeResolver resolver = new CreateGlossaryNodeResolver(mockClient);
CreateGlossaryNodeResolver resolver = new CreateGlossaryNodeResolver(mockClient, mockService);
resolver.get(mockEnv).get();
Mockito.verify(mockClient, Mockito.times(1)).ingestProposal(

View File

@ -10,6 +10,7 @@ import com.linkedin.glossary.GlossaryTermInfo;
import com.linkedin.metadata.Constants;
import com.linkedin.metadata.key.GlossaryTermKey;
import com.linkedin.metadata.utils.GenericRecordUtils;
import com.linkedin.metadata.entity.EntityService;
import com.linkedin.mxe.MetadataChangeProposal;
import graphql.schema.DataFetchingEnvironment;
import org.mockito.Mockito;
@ -75,10 +76,11 @@ public class CreateGlossaryTermResolverTest {
@Test
public void testGetSuccess() throws Exception {
EntityClient mockClient = Mockito.mock(EntityClient.class);
EntityService mockService = Mockito.mock(EntityService.class);
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
final MetadataChangeProposal proposal = setupTest(mockEnv, TEST_INPUT, "test-description", parentNodeUrn);
CreateGlossaryTermResolver resolver = new CreateGlossaryTermResolver(mockClient);
CreateGlossaryTermResolver resolver = new CreateGlossaryTermResolver(mockClient, mockService);
resolver.get(mockEnv).get();
Mockito.verify(mockClient, Mockito.times(1)).ingestProposal(
@ -90,10 +92,11 @@ public class CreateGlossaryTermResolverTest {
@Test
public void testGetSuccessNoDescription() throws Exception {
EntityClient mockClient = Mockito.mock(EntityClient.class);
EntityService mockService = Mockito.mock(EntityService.class);
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
final MetadataChangeProposal proposal = setupTest(mockEnv, TEST_INPUT_NO_DESCRIPTION, "", parentNodeUrn);
CreateGlossaryTermResolver resolver = new CreateGlossaryTermResolver(mockClient);
CreateGlossaryTermResolver resolver = new CreateGlossaryTermResolver(mockClient, mockService);
resolver.get(mockEnv).get();
Mockito.verify(mockClient, Mockito.times(1)).ingestProposal(
@ -105,10 +108,11 @@ public class CreateGlossaryTermResolverTest {
@Test
public void testGetSuccessNoParentNode() throws Exception {
EntityClient mockClient = Mockito.mock(EntityClient.class);
EntityService mockService = Mockito.mock(EntityService.class);
DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class);
final MetadataChangeProposal proposal = setupTest(mockEnv, TEST_INPUT_NO_PARENT_NODE, "test-description", null);
CreateGlossaryTermResolver resolver = new CreateGlossaryTermResolver(mockClient);
CreateGlossaryTermResolver resolver = new CreateGlossaryTermResolver(mockClient, mockService);
resolver.get(mockEnv).get();
Mockito.verify(mockClient, Mockito.times(1)).ingestProposal(

View File

@ -1,10 +1,11 @@
import { Divider, message, Space, Button, Typography, Row, Col, Tooltip } from 'antd';
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import { EditOutlined, LockOutlined, MailOutlined, SlackOutlined } from '@ant-design/icons';
import { useHistory, useRouteMatch } from 'react-router-dom';
import { useUpdateCorpGroupPropertiesMutation } from '../../../graphql/group.generated';
import { EntityRelationshipsResult, Ownership } from '../../../types.generated';
import { useUpdateNameMutation } from '../../../graphql/mutations.generated';
import GroupEditModal from './GroupEditModal';
import CustomAvatar from '../../shared/avatar/CustomAvatar';
@ -20,6 +21,7 @@ import {
GroupsSection,
} from '../shared/SidebarStyledComponents';
import GroupMembersSideBarSection from './GroupMembersSideBarSection';
import { useGetAuthenticatedUser } from '../../useGetAuthenticatedUser';
const { Paragraph } = Typography;
@ -34,7 +36,7 @@ type SideBarData = {
groupOwnerShip: Ownership;
isExternalGroup: boolean;
externalGroupType: string | undefined;
urn: string | undefined;
urn: string;
};
type Props = {
@ -61,10 +63,21 @@ const GroupNameHeader = styled(Row)`
min-height: 100px;
`;
const GroupName = styled.div`
const GroupTitle = styled(Typography.Title)`
max-width: 260px;
word-wrap: break-word;
width: 140px;
&&& {
margin-bottom: 0;
word-break: break-all;
margin-left: 10px;
}
.ant-typography-edit {
font-size: 16px;
margin-left: 10px;
}
`;
/**
@ -90,7 +103,31 @@ export default function GroupInfoSidebar({ sideBarData, refetch }: Props) {
/* eslint-disable @typescript-eslint/no-unused-vars */
const [editGroupModal, showEditGroupModal] = useState(false);
const canEditGroup = true; // TODO; Replace this will fine-grained understanding of user permissions.
const me = useGetAuthenticatedUser();
const canEditGroup = me?.platformPrivileges.manageIdentities;
const [groupTitle, setGroupTitle] = useState(name);
const [updateName] = useUpdateNameMutation();
useEffect(() => {
setGroupTitle(groupTitle);
}, [groupTitle]);
// Update Group Title
// eslint-disable-next-line @typescript-eslint/no-shadow
const handleTitleUpdate = async (name: string) => {
setGroupTitle(name);
await updateName({ variables: { input: { name, urn } } })
.then(() => {
message.success({ content: 'Name Updated', duration: 2 });
refetch();
})
.catch((e: unknown) => {
message.destroy();
if (e instanceof Error) {
message.error({ content: `Failed to update name: \n ${e.message || ''}`, duration: 3 });
}
});
};
const getEditModalData = {
urn,
@ -135,7 +172,9 @@ export default function GroupInfoSidebar({ sideBarData, refetch }: Props) {
/>
</Col>
<Col>
<GroupName>{name}</GroupName>
<GroupTitle level={3} editable={canEditGroup ? { onChange: handleTitleUpdate } : false}>
{groupTitle}
</GroupTitle>
</Col>
<Col>
{isExternalGroup && (

View File

@ -1,11 +1,13 @@
import { InfoCircleOutlined } from '@ant-design/icons';
import { Divider, Popover, Tooltip, Typography } from 'antd';
import React from 'react';
import { InfoCircleOutlined } from '@ant-design/icons';
import { Divider, message, Modal, Popover, Tooltip, Typography } from 'antd';
import { blue } from '@ant-design/colors';
import styled from 'styled-components';
import moment from 'moment';
import { Deprecation } from '../../../../../types.generated';
import { getLocaleTimezone } from '../../../../shared/time/timeUtils';
import { ANTD_GRAY } from '../../constants';
import { useBatchUpdateDeprecationMutation } from '../../../../../graphql/mutations.generated';
const DeprecatedContainer = styled.div`
width: 104px;
@ -55,12 +57,30 @@ const StyledInfoCircleOutlined = styled(InfoCircleOutlined)`
color: #ef5b5b;
`;
const UndeprecatedIcon = styled(InfoCircleOutlined)`
font-size: 14px;
padding-right: 6px;
`;
const IconGroup = styled.div`
font-size: 12px;
color: 'black';
&:hover {
color: ${blue[4]};
cursor: pointer;
}
`;
type Props = {
urn: string;
deprecation: Deprecation;
preview?: boolean | null;
refetch?: () => void;
showUndeprecate: boolean | null;
};
export const DeprecationPill = ({ deprecation, preview }: Props) => {
export const DeprecationPill = ({ deprecation, preview, urn, refetch, showUndeprecate }: Props) => {
const [batchUpdateDeprecationMutation] = useBatchUpdateDeprecationMutation();
/**
* Deprecation Decommission Timestamp
*/
@ -78,6 +98,30 @@ export const DeprecationPill = ({ deprecation, preview }: Props) => {
const hasDetails = deprecation.note !== '' || deprecation.decommissionTime !== null;
const isDividerNeeded = deprecation.note !== '' && deprecation.decommissionTime !== null;
const batchUndeprecate = () => {
batchUpdateDeprecationMutation({
variables: {
input: {
resources: [{ resourceUrn: urn }],
deprecated: false,
},
},
})
.then(({ errors }) => {
if (!errors) {
message.success({ content: 'Marked assets as un-deprecated!', duration: 2 });
refetch?.();
}
})
.catch((e) => {
message.destroy();
message.error({
content: `Failed to mark assets as un-deprecated: \n ${e.message || ''}`,
duration: 3,
});
});
};
return (
<Popover
overlayStyle={{ maxWidth: 240 }}
@ -95,6 +139,27 @@ export const DeprecationPill = ({ deprecation, preview }: Props) => {
</Tooltip>
</Typography.Text>
)}
{isDividerNeeded && <ThinDivider />}
{showUndeprecate && (
<IconGroup
onClick={() =>
Modal.confirm({
title: `Confirm Mark as un-deprecated`,
content: `Are you sure you want to mark this asset as un-deprecated?`,
onOk() {
batchUndeprecate();
},
onCancel() {},
okText: 'Yes',
maskClosable: true,
closable: true,
})
}
>
<UndeprecatedIcon />
Mark as un-deprecated
</IconGroup>
)}
</>
) : (
'No additional details'

View File

@ -26,13 +26,16 @@ export default function DeprecationDropdown({ urns, disabled = false, refetch }:
})
.then(({ errors }) => {
if (!errors) {
message.success({ content: 'Marked assets as undeprecated!', duration: 2 });
message.success({ content: 'Marked assets as un-deprecated!', duration: 2 });
refetch?.();
}
})
.catch((e) => {
message.destroy();
message.error({ content: `Failed to mark assets as undeprecated: \n ${e.message || ''}`, duration: 3 });
message.error({
content: `Failed to mark assets as un-deprecated: \n ${e.message || ''}`,
duration: 3,
});
});
};
@ -48,11 +51,11 @@ export default function DeprecationDropdown({ urns, disabled = false, refetch }:
},
},
{
title: 'Mark as undeprecated',
title: 'Mark as un-deprecated',
onClick: () => {
Modal.confirm({
title: `Confirm Mark as undeprecated`,
content: `Are you sure you want to mark these assets as undeprecated?`,
title: `Confirm Mark as un-deprecated`,
content: `Are you sure you want to mark these assets as un-deprecated?`,
onOk() {
batchUndeprecate();
},

View File

@ -125,7 +125,13 @@ export const EntityHeader = ({
<TitleWrapper>
<EntityName isNameEditable={canEditName} />
{entityData?.deprecation?.deprecated && (
<DeprecationPill deprecation={entityData?.deprecation} preview={isCompact} />
<DeprecationPill
urn={urn}
deprecation={entityData?.deprecation}
showUndeprecate
preview={isCompact}
refetch={refetch}
/>
)}
{entityData?.health?.map((health) => (
<EntityHealthStatus

View File

@ -288,7 +288,9 @@ export default function DefaultPreviewCard({
</EntityTitle>
)}
</Link>
{deprecation?.deprecated && <DeprecationPill deprecation={deprecation} preview />}
{deprecation?.deprecated && (
<DeprecationPill deprecation={deprecation} urn="" showUndeprecate={false} preview />
)}
{externalUrl && (
<ExternalUrlContainer>
<ExternalUrlButton type="link" href={externalUrl} target="_blank">