Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1494,7 +1494,7 @@ private void configureMutationResolvers(final RuntimeWiring.Builder builder) {
"updateOwnershipType", new UpdateOwnershipTypeResolver(this.ownershipTypeService))
.dataFetcher(
"updateDisplayProperties",
new UpdateDisplayPropertiesResolver(this.entityService))
new UpdateDisplayPropertiesResolver(this.entityService, this.entityClient))
.dataFetcher(
"deleteOwnershipType", new DeleteOwnershipTypeResolver(this.ownershipTypeService))
.dataFetcher("submitFormPrompt", new SubmitFormPromptResolver(this.formService))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import com.linkedin.datahub.graphql.concurrency.GraphQLConcurrencyUtils;
import com.linkedin.datahub.graphql.exception.AuthorizationException;
import com.linkedin.datahub.graphql.generated.DisplayPropertiesUpdateInput;
import com.linkedin.datahub.graphql.resolvers.mutate.util.GlossaryUtils;
import com.linkedin.entity.client.EntityClient;
import com.linkedin.metadata.Constants;
import com.linkedin.metadata.entity.EntityService;
import com.linkedin.metadata.entity.EntityUtils;
Expand All @@ -26,6 +28,7 @@
@RequiredArgsConstructor
public class UpdateDisplayPropertiesResolver implements DataFetcher<CompletableFuture<Boolean>> {
private final EntityService _entityService;
private final EntityClient _entityClient;

@Override
public CompletableFuture<Boolean> get(DataFetchingEnvironment environment) throws Exception {
Expand All @@ -46,7 +49,20 @@ public CompletableFuture<Boolean> get(DataFetchingEnvironment environment) throw

return GraphQLConcurrencyUtils.supplyAsync(
() -> {
if (!AuthorizationUtils.canManageDomains(context)) {
// displayProperties is shared across domains and glossary entities. Authorize
// based on the target entity type so a glossary editor can change a term/node
// color without also having MANAGE_DOMAINS, and vice versa.
final String entityType = targetUrn.getEntityType();
final boolean authorized;
if (Constants.GLOSSARY_TERM_ENTITY_NAME.equals(entityType)
|| Constants.GLOSSARY_NODE_ENTITY_NAME.equals(entityType)) {
authorized =
GlossaryUtils.canManageGlossaries(context)
|| GlossaryUtils.canUpdateGlossaryEntity(targetUrn, context, _entityClient);
} else {
authorized = AuthorizationUtils.canManageDomains(context);
}
if (!authorized) {
throw new AuthorizationException(
"Unauthorized to perform this action. Please contact your DataHub administrator.");
}
Expand Down Expand Up @@ -97,10 +113,9 @@ public CompletableFuture<Boolean> get(DataFetchingEnvironment environment) throw
e.getMessage());
throw new RuntimeException(
String.format(
"Failed to update DisplayProperties for urn: {}, properties: {}. {}",
targetUrn.toString(),
input.toString(),
e.getMessage()));
"Failed to update DisplayProperties for urn: %s, properties: %s. %s",
targetUrn.toString(), input.toString(), e.getMessage()),
e);
}
},
this.getClass().getSimpleName(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import static com.linkedin.metadata.Constants.APPLICATION_MEMBERSHIP_ASPECT_NAME;
import static com.linkedin.metadata.Constants.ASSET_SETTINGS_ASPECT_NAME;
import static com.linkedin.metadata.Constants.DISPLAY_PROPERTIES_ASPECT_NAME;
import static com.linkedin.metadata.Constants.FORMS_ASPECT_NAME;
import static com.linkedin.metadata.Constants.GLOSSARY_NODE_ENTITY_NAME;
import static com.linkedin.metadata.Constants.GLOSSARY_NODE_INFO_ASPECT_NAME;
Expand Down Expand Up @@ -54,6 +55,7 @@ public class GlossaryNodeType
FORMS_ASPECT_NAME,
APPLICATION_MEMBERSHIP_ASPECT_NAME,
ASSET_SETTINGS_ASPECT_NAME,
DISPLAY_PROPERTIES_ASPECT_NAME,
INSTITUTIONAL_MEMORY_ASPECT_NAME);

private final EntityClient _entityClient;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ public class GlossaryTermType
STRUCTURED_PROPERTIES_ASPECT_NAME,
FORMS_ASPECT_NAME,
APPLICATION_MEMBERSHIP_ASPECT_NAME,
DISPLAY_PROPERTIES_ASPECT_NAME,
ASSET_SETTINGS_ASPECT_NAME);

private final EntityClient _entityClient;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import com.linkedin.application.Applications;
import com.linkedin.common.Deprecation;
import com.linkedin.common.DisplayProperties;
import com.linkedin.common.Forms;
import com.linkedin.common.InstitutionalMemory;
import com.linkedin.common.Ownership;
Expand All @@ -19,6 +20,7 @@
import com.linkedin.datahub.graphql.types.application.ApplicationAssociationMapper;
import com.linkedin.datahub.graphql.types.common.mappers.AssetSettingsMapper;
import com.linkedin.datahub.graphql.types.common.mappers.DeprecationMapper;
import com.linkedin.datahub.graphql.types.common.mappers.DisplayPropertiesMapper;
import com.linkedin.datahub.graphql.types.common.mappers.InstitutionalMemoryMapper;
import com.linkedin.datahub.graphql.types.common.mappers.OwnershipMapper;
import com.linkedin.datahub.graphql.types.common.mappers.util.MappingHelper;
Expand Down Expand Up @@ -115,6 +117,11 @@ public GlossaryTerm apply(
ASSET_SETTINGS_ASPECT_NAME,
((entity, dataMap) ->
entity.setSettings(AssetSettingsMapper.map(new AssetSettings(dataMap)))));
mappingHelper.mapToResult(
DISPLAY_PROPERTIES_ASPECT_NAME,
((glossaryTerm, dataMap) ->
glossaryTerm.setDisplayProperties(
DisplayPropertiesMapper.map(context, new DisplayProperties(dataMap)))));

// If there's no name property, resort to the legacy name computation.
if (result.getGlossaryTermInfo() != null && result.getGlossaryTermInfo().getName() == null) {
Expand Down
5 changes: 5 additions & 0 deletions datahub-graphql-core/src/main/resources/entity.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -2609,6 +2609,11 @@ type GlossaryTerm implements Entity {
The forms associated with the Dataset
"""
forms: Forms

"""
Display properties for the glossary term
"""
displayProperties: DisplayProperties
}

"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ export function ToastRenderer() {

return ReactDOM.createPortal(
<ThemeProvider theme={theme}>
<ToastContainer>
<ToastContainer data-testid="toast-notification-container">
{currentToasts.map((entry) => (
<ToastItem key={entry.id} entry={entry} />
))}
Expand Down
54 changes: 43 additions & 11 deletions datahub-web-react/src/app/domainV2/CreateDomainModal.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { Collapse, Form, message } from 'antd';
// antd `Form` and `Collapse` are retained because alchemy does not currently provide
// equivalents for `Form` (with field-level rules / Form.useForm) or collapsible panels.
import { toast } from '@components';
import { Collapse, Form } from 'antd';
import React, { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import styled from 'styled-components';
import styled, { useTheme } from 'styled-components';

import { Label } from '@components/components/TextArea/components';

Expand All @@ -17,9 +20,10 @@ import { useReloadableContext } from '@app/sharedV2/reloadableContext/hooks/useR
import { ReloadableKeyTypeNamespace } from '@app/sharedV2/reloadableContext/types';
import { getReloadableKeyType } from '@app/sharedV2/reloadableContext/utils';
import { useIsNestedDomainsEnabled } from '@app/useAppConfig';
import { Input, Modal, TextArea } from '@src/alchemy-components';
import { ColorPicker, Input, Modal, TextArea } from '@src/alchemy-components';

import { useCreateDomainMutation } from '@graphql/domain.generated';
import { useUpdateDisplayPropertiesMutation } from '@graphql/mutations.generated';
import { DataHubPageModuleType, EntityType } from '@types';

const FormItem = styled(Form.Item)`
Expand Down Expand Up @@ -57,11 +61,18 @@ export default function CreateDomainModal({ onClose, onCreate }: Props) {
const { t: tl } = useTranslation('common.labels');
const isNestedDomainsEnabled = useIsNestedDomainsEnabled();
const [createDomainMutation] = useCreateDomainMutation();
const [updateDisplayPropertiesMutation] = useUpdateDisplayPropertiesMutation();
const { entityData, setNewDomain } = useDomainsContextV2();
const theme = useTheme();
const [selectedParentUrn, setSelectedParentUrn] = useState<string>(
(isNestedDomainsEnabled && entityData?.urn) || '',
);
const [createButtonEnabled, setCreateButtonEnabled] = useState(false);
const [selectedColor, setSelectedColor] = useState<string>(theme.colors.colorPickerDefault);
// Whether the user has explicitly picked a color. If false, we let the backend fall back to
// the deterministic palette color generated from the URN instead of persisting the default
// gray placeholder and overriding it.
const [colorWasPicked, setColorWasPicked] = useState(false);
const [form] = Form.useForm();
const { loaded: userLoaded, user } = useUserContext();
const [selectedOwnerUrns, setSelectedOwnerUrns] = useState<string[]>([]);
Expand Down Expand Up @@ -102,19 +113,31 @@ export default function CreateDomainModal({ onClose, onCreate }: Props) {
type: EventType.CreateDomainEvent,
parentDomainUrn: selectedParentUrn || undefined,
});
message.success({
content: t('create.success'),
duration: 3,
});
toast.success(t('create.success'), { duration: 3 });
const newDomainUrn = data?.createDomain || '';
// Only persist the color if the user actually picked one. Otherwise we'd
// save the gray placeholder default and override the deterministic palette
// color the UI would have generated from the URN. Best-effort follow-up so
// a color failure doesn't block creation.
if (newDomainUrn && colorWasPicked) {
updateDisplayPropertiesMutation({
variables: {
urn: newDomainUrn,
input: { colorHex: selectedColor },
},
}).catch((e) => {
console.error('Failed to set domain color after creation', e);
});
}
onCreate?.(
data?.createDomain || '',
newDomainUrn,
form.getFieldValue(ID_FIELD_NAME),
form.getFieldValue(NAME_FIELD_NAME),
form.getFieldValue(DESCRIPTION_FIELD_NAME),
selectedParentUrn || undefined,
);
const newDomain: UpdatedDomain = {
urn: data?.createDomain || '',
urn: newDomainUrn,
type: EntityType.Domain,
id: form.getFieldValue(ID_FIELD_NAME),
properties: {
Expand All @@ -134,8 +157,7 @@ export default function CreateDomainModal({ onClose, onCreate }: Props) {
}
})
.catch((e) => {
message.destroy();
message.error({ content: t('create.error', { errorMessage: e.message || '' }), duration: 3 });
toast.error(t('create.error', { errorMessage: e.message || '' }), { duration: 3 });
})
.finally(() => {
onClose();
Expand Down Expand Up @@ -204,6 +226,16 @@ export default function CreateDomainModal({ onClose, onCreate }: Props) {
data-testid="create-domain-description"
/>
</FormItemWithMargin>
<FormItemWithMargin>
<Label>{tl('color')}</Label>
<ColorPicker
initialColor={selectedColor}
onChange={(c) => {
setSelectedColor(c);
setColorWasPicked(true);
}}
/>
</FormItemWithMargin>
{isNestedDomainsEnabled && (
<FormItemWithMargin>
<Label>{t('create.parentLabel')}</Label>
Expand Down
10 changes: 8 additions & 2 deletions datahub-web-react/src/app/entity/EntityPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,18 @@ export const EntityPage = ({ entityType }: Props) => {
entityType === EntityType.DataProcessInstance ||
entityType === EntityType.GlossaryNode;

// Build the profile JSX up-front (and unconditionally) so any hooks that entity definitions
// inline through `getProfileTabs()` run on every render. Otherwise the hook count flips when
// `canViewEntityPage` transitions from undefined → defined after the privileges query
// resolves, producing a Rules-of-Hooks violation.
const profile = entityRegistry.renderProfile(entityType, urn);

return (
<>
{error && <ErrorSection />}
{data && !canViewEntityPage && <UnauthorizedPage />}
{canViewEntityPage &&
((showNewPage && <>{entityRegistry.renderProfile(entityType, urn)}</>) || (
((showNewPage && <>{profile}</>) || (
<BrowsableEntityPage
isBrowsable={isBrowsable}
urn={urn}
Expand All @@ -86,7 +92,7 @@ export const EntityPage = ({ entityType }: Props) => {
{isLineageMode && isLineageSupported ? (
<LineageExplorer type={entityType} urn={urn} />
) : (
entityRegistry.renderProfile(entityType, urn)
profile
)}
</BrowsableEntityPage>
))}
Expand Down
Loading
Loading