diff --git a/datahub-web-react/src/app/entityV2/shared/EntityDropdown/EntityDropdown.tsx b/datahub-web-react/src/app/entityV2/shared/EntityDropdown/EntityDropdown.tsx
index ea49b3902b1e..95ac986b17c2 100644
--- a/datahub-web-react/src/app/entityV2/shared/EntityDropdown/EntityDropdown.tsx
+++ b/datahub-web-react/src/app/entityV2/shared/EntityDropdown/EntityDropdown.tsx
@@ -1,7 +1,7 @@
-import { Menu } from '@components';
-import MoreVertOutlinedIcon from '@mui/icons-material/MoreVertOutlined';
+import { Menu, toast } from '@components';
import { ClockCounterClockwise } from '@phosphor-icons/react/dist/csr/ClockCounterClockwise';
import { Copy } from '@phosphor-icons/react/dist/csr/Copy';
+import { DotsThreeVertical } from '@phosphor-icons/react/dist/csr/DotsThreeVertical';
import { Envelope } from '@phosphor-icons/react/dist/csr/Envelope';
import { FolderOpen } from '@phosphor-icons/react/dist/csr/FolderOpen';
import { FolderPlus } from '@phosphor-icons/react/dist/csr/FolderPlus';
@@ -15,12 +15,11 @@ import { Plus } from '@phosphor-icons/react/dist/csr/Plus';
import { Share } from '@phosphor-icons/react/dist/csr/Share';
import { Trash } from '@phosphor-icons/react/dist/csr/Trash';
import { Warning } from '@phosphor-icons/react/dist/csr/Warning';
-import { message } from 'antd';
import qs from 'query-string';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Redirect, useHistory } from 'react-router';
-import styled, { useTheme } from 'styled-components';
+import { useTheme } from 'styled-components';
import { EventType } from '@app/analytics';
import analytics from '@app/analytics/analytics';
@@ -61,18 +60,6 @@ import DeprecatedIcon from '@images/deprecated-status.svg?react';
// Tab path segment passed to getEntityPath — a route identifier, not user-visible copy.
const INCIDENTS_TAB_NAME = 'Incidents';
-const StyledMoreIcon = styled(MoreVertOutlinedIcon)`
- &&& {
- display: flex;
- font-size: 20px;
- padding: 2px;
-
- :hover {
- color: ${(p) => p.theme.colors.textBrand};
- }
- }
-`;
-
interface Options {
hideDeleteMessage?: boolean;
skipDeleteWait?: boolean;
@@ -144,7 +131,7 @@ const EntityDropdown = (props: Props) => {
const [isChangeHistoryOpen, setIsChangeHistoryOpen] = useState(false);
const handleUpdateDeprecation = async (deprecatedStatus: boolean) => {
- message.loading({ content: tcf('updating') });
+ toast.loading(tcf('updating'));
try {
await updateDeprecation({
variables: {
@@ -156,20 +143,17 @@ const EntityDropdown = (props: Props) => {
},
},
});
- message.destroy();
- message.success({ content: t('deprecation.updated'), duration: 2 });
+ toast.destroy();
+ toast.success(t('deprecation.updated'), { duration: 2 });
analytics.event({
type: EventType.SetDeprecation,
entityUrns: [urn],
deprecated: deprecatedStatus,
});
} catch (e: unknown) {
- message.destroy();
+ toast.destroy();
if (e instanceof Error) {
- message.error({
- content: t('deprecation.updateError', { errorMessage: e.message || '' }),
- duration: 2,
- });
+ toast.error(t('deprecation.updateError', { errorMessage: e.message || '' }), { duration: 2 });
}
}
refetchForEntity?.();
@@ -197,7 +181,7 @@ const EntityDropdown = (props: Props) => {
icon: Link,
onClick: () => {
navigator.clipboard.writeText(pageUrl);
- message.info(t('menuItem.copiedUrl'), 1.2);
+ toast.info(t('menuItem.copiedUrl'), { duration: 1.2 });
},
});
}
@@ -516,7 +500,7 @@ const EntityDropdown = (props: Props) => {
return (
<>
{isCreateTermModalVisible && (
{
- message.loading({ content: tcf('updating') });
+ toast.loading(tcf('updating'));
try {
await updateDeprecation({
variables: {
@@ -34,20 +42,17 @@ export default function UpdateDeprecationMenuAction() {
},
},
});
- message.destroy();
- message.success({ content: t('deprecation.updated'), duration: 2 });
+ toast.destroy();
+ toast.success(t('deprecation.updated'), { duration: 2 });
analytics.event({
type: EventType.SetDeprecation,
entityUrns: [urn],
deprecated: deprecatedStatus,
});
} catch (e: unknown) {
- message.destroy();
+ toast.destroy();
if (e instanceof Error) {
- message.error({
- content: t('deprecation.updateError', { errorMessage: e.message || '' }),
- duration: 2,
- });
+ toast.error(t('deprecation.updateError', { errorMessage: e.message || '' }), { duration: 2 });
}
}
refetchForEntity?.();
@@ -62,8 +67,12 @@ export default function UpdateDeprecationMenuAction() {
: t('deprecation.markUnTooltip', { entityName: entityRegistry.getEntityName(entityType) })
}
>
-
!entityData?.deprecation?.deprecated
? setIsDeprecationModalVisible(true)
@@ -71,8 +80,8 @@ export default function UpdateDeprecationMenuAction() {
}
data-testid="entity-menu-deprecate-button"
>
-
-
+
+
{isDeprecationModalVisible && (
void;
- refetch?: () => void;
+ refetch?: (result?: DeprecationModalResult) => void;
zIndexOverride?: number;
};
const SCHEMA_FIELD_PREFIX = 'urn:li:schemaField:';
+const FieldGroup = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+`;
+
+const ReplacementControls = styled.div`
+ align-self: flex-start;
+`;
+
export const UpdateDeprecationModal = ({ urns, resourceRefs, onClose, refetch, zIndexOverride }: Props) => {
const { t } = useTranslation('entity.shared.entityDropdown');
const { t: tc } = useTranslation('common.actions');
const { t: tcf } = useTranslation('common.feedback');
const { entityWithSchema } = useGetEntityWithSchema();
const schemaMetadata: any = entityWithSchema?.schemaMetadata || undefined;
+ const entityRegistry = useEntityRegistry();
const [batchUpdateDeprecation] = useBatchUpdateDeprecationMutation();
- const [isReplacementModalVisible, setIsReplacementModalVisible] = React.useState(false);
- const [replacementUrn, setReplacementUrn] = React.useState(null);
- const entityRegistry = useEntityRegistry();
+ const [isReplacementModalVisible, setIsReplacementModalVisible] = useState(false);
+ const [replacementUrn, setReplacementUrn] = useState(null);
+ const [note, setNote] = useState('');
+ const [decommissionTime, setDecommissionTime] = useState(undefined);
const isDeprecatingFields =
!!resourceRefs && resourceRefs.length > 0 && resourceRefs[0].subResourceType === SubResourceType.DatasetField;
@@ -55,23 +71,16 @@ export const UpdateDeprecationModal = ({ urns, resourceRefs, onClose, refetch, z
skip: !replacementUrn || replacementUrn?.startsWith(SCHEMA_FIELD_PREFIX),
});
- const [form] = Form.useForm();
-
- const handleClose = () => {
- form.resetFields();
- onClose();
- };
-
- const handleOk = async (formData: any) => {
- message.loading({ content: tcf('updating') });
+ const handleSubmit = async () => {
+ toast.loading(tcf('updating'));
try {
await batchUpdateDeprecation({
variables: {
input: {
resources: resourceRefs || urns.map((resourceUrn) => ({ resourceUrn })),
deprecated: true,
- note: formData.note,
- decommissionTime: formData.decommissionTime && formData.decommissionTime.unix() * 1000,
+ note,
+ decommissionTime: decommissionTime ? decommissionTime.unix() * 1000 : null,
replacement: replacementUrn,
},
},
@@ -82,136 +91,132 @@ export const UpdateDeprecationModal = ({ urns, resourceRefs, onClose, refetch, z
deprecated: true,
resources: isDeprecatingFields ? resourceRefs : undefined,
});
- message.destroy();
- message.success({ content: t('deprecation.updated'), duration: 2 });
+ toast.destroy();
+ toast.success(t('deprecation.updated'), { duration: 2 });
} catch (e: unknown) {
- message.destroy();
+ toast.destroy();
if (e instanceof Error) {
- message.error(
- handleBatchError(urns, e, {
- content: t('deprecation.updateError', { errorMessage: e.message || '' }),
- duration: 2,
- }),
- );
+ const fallback = {
+ content: t('deprecation.updateError', { errorMessage: e.message || '' }),
+ duration: 2,
+ };
+ const { content, duration } = handleBatchError(urns, e, fallback);
+ toast.error(content, { duration });
}
}
- refetch?.();
- handleClose();
+ refetch?.({
+ note: note || null,
+ decommissionTime: decommissionTime ? decommissionTime.unix() * 1000 : null,
+ replacement: replacementData?.entities?.[0] ?? null,
+ });
+ onClose();
};
return (
-
+
-
-
-
-
- {isReplacementModalVisible && !isDeprecatingFields && (
- {
- if (entityUrns.length > 0) {
- setReplacementUrn(entityUrns[0]);
+ placeholder={t('deprecation.reasonPlaceholder')}
+ value={note}
+ onChange={(e) => setNote(e.target.value)}
+ rows={4}
+ autoFocus
+ />
+ setDecommissionTime(v)}
+ />
+
+ {isReplacementModalVisible && !isDeprecatingFields && (
+ {
+ if (entityUrns.length > 0) {
+ setReplacementUrn(entityUrns[0]);
+ }
+ setIsReplacementModalVisible(false);
+ }}
+ onCancel={() => setIsReplacementModalVisible(false)}
+ fixedEntityTypes={Array.from(
+ entityRegistry.getTypesWithSupportedCapabilities(EntityCapabilityType.DEPRECATION),
+ )}
+ />
+ )}
+ {isReplacementModalVisible && isDeprecatingFields && (
+ setIsReplacementModalVisible(false)}
+ buttons={[
+ {
+ text: tc('cancel'),
+ variant: 'text',
+ onClick: () => setIsReplacementModalVisible(false),
+ },
+ {
+ text: tc('save'),
+ onClick: () => setIsReplacementModalVisible(false),
+ },
+ ]}
+ >
+ ({
+ value: field.fieldPath,
+ label: downgradeV2FieldPath(field.fieldPath),
+ })) || []
+ }
+ onUpdate={(vals) => {
+ if (vals.length > 0) {
+ setReplacementUrn(
+ generateSchemaFieldUrn(vals[0], resourceFromWhichReplacementIsSelected || ''),
+ );
}
- setIsReplacementModalVisible(false);
}}
- onCancel={() => setIsReplacementModalVisible(false)}
- fixedEntityTypes={Array.from(
- entityRegistry.getTypesWithSupportedCapabilities(EntityCapabilityType.DEPRECATION),
- )}
/>
- )}
- {isReplacementModalVisible && isDeprecatingFields && (
- setIsReplacementModalVisible(false)}
- onOk={() => setIsReplacementModalVisible(false)}
- buttons={[]}
- >
-
-
- )}
- {replacementUrn && replacementLoading && }
+
+ )}
+
+
+ {replacementUrn && replacementLoading && }
{replacementUrn && !replacementLoading && !!replacementData?.entities?.[0] && (
{
- setIsReplacementModalVisible(true);
- }}
+ onClick={() => setIsReplacementModalVisible(true)}
entity={replacementData?.entities?.[0] as any}
/>
)}
{replacementUrn && isDeprecatingFields && (
-
-
+
+
);
};
diff --git a/datahub-web-react/src/app/glossaryV2/GlossaryBrowser/TermItem.tsx b/datahub-web-react/src/app/glossaryV2/GlossaryBrowser/TermItem.tsx
index ca814fd58193..441b2c359afb 100644
--- a/datahub-web-react/src/app/glossaryV2/GlossaryBrowser/TermItem.tsx
+++ b/datahub-web-react/src/app/glossaryV2/GlossaryBrowser/TermItem.tsx
@@ -1,7 +1,9 @@
import React from 'react';
import { useHistory } from 'react-router-dom';
+import styled from 'styled-components/macro';
import { useGlossaryEntityData } from '@app/entityV2/shared/GlossaryEntityContext';
+import { DeprecationIcon } from '@app/entityV2/shared/components/styled/DeprecationIcon';
import { EDITING_DOCUMENTATION_URL_PARAM } from '@app/entityV2/shared/constants';
import { useGlossaryActiveTabPath } from '@app/entityV2/shared/containers/profile/utils';
import { SelectedMark } from '@app/glossaryV2/GlossaryBrowser/SelectedMark';
@@ -22,6 +24,27 @@ import { EntityType } from '@types';
// Row chrome (RowContainer/LeftContent/IconSlot/Title) lives in `treeRow.styles.ts`
// — shared with `NodeItem` so the two leaf-row types stay visually identical.
+const TitleContent = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ min-width: 0;
+ flex: 1;
+ overflow: hidden;
+`;
+
+const DeprecationSlot = styled.span`
+ display: inline-flex;
+ align-items: center;
+ flex-shrink: 0;
+ line-height: 0;
+
+ & svg {
+ width: 12px;
+ height: 12px;
+ }
+`;
+
interface Props {
term: ChildGlossaryTermFragment;
isSelecting?: boolean;
@@ -75,6 +98,17 @@ function TermItem(props: Props) {
const displayName = entityRegistry.getDisplayName(term.type, isOnEntityPage ? entityData : term);
+ // Prefer the profile page's live (post-mutation) deprecation state over the sidebar's own
+ // fetch when this row is the currently-open entity, mirroring the same isOnEntityPage
+ // pattern already used above for the display name.
+ const deprecation = isOnEntityPage ? entityData?.deprecation : term.deprecation;
+
+ const deprecationBadge = deprecation?.deprecated && (
+
+
+
+ );
+
return (
- {displayName}
+
+ {displayName}
+ {deprecationBadge}
+
{isMultiSelected && }
diff --git a/datahub-web-react/src/app/glossaryV2/GlossaryBrowser/__tests__/TermItem.test.tsx b/datahub-web-react/src/app/glossaryV2/GlossaryBrowser/__tests__/TermItem.test.tsx
new file mode 100644
index 000000000000..e6a6f7e8a7c8
--- /dev/null
+++ b/datahub-web-react/src/app/glossaryV2/GlossaryBrowser/__tests__/TermItem.test.tsx
@@ -0,0 +1,109 @@
+import { render, screen } from '@testing-library/react';
+import React from 'react';
+import { BrowserRouter } from 'react-router-dom';
+import { ThemeProvider } from 'styled-components';
+
+import { useGlossaryEntityData } from '@app/entityV2/shared/GlossaryEntityContext';
+import TermItem from '@app/glossaryV2/GlossaryBrowser/TermItem';
+import themeV2 from '@conf/theme/themeV2';
+
+import { EntityType } from '@types';
+
+vi.mock('@app/useEntityRegistry', () => ({
+ useEntityRegistry: () => ({
+ getDisplayName: vi.fn(() => 'Adoptions'),
+ getEntityUrl: vi.fn(() => '/glossaryTerm/test'),
+ }),
+}));
+
+vi.mock('@app/entityV2/shared/components/styled/DeprecationIcon', () => ({
+ DeprecationIcon: () => ,
+}));
+
+vi.mock('@app/entityV2/shared/GlossaryEntityContext', () => ({
+ useGlossaryEntityData: vi.fn(() => ({ entityData: null })),
+}));
+
+const baseTerm = {
+ urn: 'urn:li:glossaryTerm:test',
+ type: EntityType.GlossaryTerm,
+ name: 'Adoptions',
+ hierarchicalName: 'Adoptions.Adoptions',
+ properties: { name: 'Adoptions', description: null },
+ domain: null,
+} as any;
+
+const renderTermItem = (term: any) =>
+ render(
+
+
+
+
+ ,
+ );
+
+describe('TermItem — deprecation badge', () => {
+ it('renders deprecation icon when term is deprecated', () => {
+ renderTermItem({
+ ...baseTerm,
+ deprecation: { deprecated: true, note: 'Replaced', actor: null, decommissionTime: null },
+ });
+ expect(screen.getByTestId('deprecation-icon')).toBeInTheDocument();
+ });
+
+ it('does not render deprecation icon when deprecated is false', () => {
+ renderTermItem({
+ ...baseTerm,
+ deprecation: { deprecated: false, note: null, actor: null, decommissionTime: null },
+ });
+ expect(screen.queryByTestId('deprecation-icon')).not.toBeInTheDocument();
+ });
+
+ it('does not render deprecation icon when deprecation is null', () => {
+ renderTermItem({ ...baseTerm, deprecation: null });
+ expect(screen.queryByTestId('deprecation-icon')).not.toBeInTheDocument();
+ });
+
+ it('does not render deprecation icon when deprecation is absent', () => {
+ renderTermItem({ ...baseTerm });
+ expect(screen.queryByTestId('deprecation-icon')).not.toBeInTheDocument();
+ });
+});
+
+describe('TermItem — live deprecation state when this row is the open entity', () => {
+ it('shows the badge from entityData even when the sidebar-fetched term is not deprecated', () => {
+ vi.mocked(useGlossaryEntityData).mockReturnValueOnce({
+ entityData: { urn: baseTerm.urn, deprecation: { deprecated: true, note: '', decommissionTime: null } },
+ } as any);
+ renderTermItem({
+ ...baseTerm,
+ deprecation: { deprecated: false, note: null, actor: null, decommissionTime: null },
+ });
+ expect(screen.getByTestId('deprecation-icon')).toBeInTheDocument();
+ });
+
+ it('clears a stale badge when entityData shows un-deprecated but the sidebar fetch is still deprecated', () => {
+ vi.mocked(useGlossaryEntityData).mockReturnValueOnce({
+ entityData: { urn: baseTerm.urn, deprecation: { deprecated: false, note: null, decommissionTime: null } },
+ } as any);
+ renderTermItem({
+ ...baseTerm,
+ deprecation: { deprecated: true, note: 'stale', actor: null, decommissionTime: null },
+ });
+ expect(screen.queryByTestId('deprecation-icon')).not.toBeInTheDocument();
+ });
+
+ it('falls back to the term prop when entityData belongs to a different entity', () => {
+ vi.mocked(useGlossaryEntityData).mockReturnValueOnce({
+ entityData: {
+ urn: 'urn:li:glossaryTerm:someOtherTerm',
+ deprecation: { deprecated: false, note: null, decommissionTime: null },
+ },
+ } as any);
+ renderTermItem({
+ ...baseTerm,
+ deprecation: { deprecated: true, note: null, actor: null, decommissionTime: null },
+ });
+ expect(screen.getByTestId('deprecation-icon')).toBeInTheDocument();
+ });
+});
diff --git a/datahub-web-react/src/graphql/glossaryNode.graphql b/datahub-web-react/src/graphql/glossaryNode.graphql
index d53eadf66cf3..76cba9af99e5 100644
--- a/datahub-web-react/src/graphql/glossaryNode.graphql
+++ b/datahub-web-react/src/graphql/glossaryNode.graphql
@@ -13,6 +13,9 @@ fragment childGlossaryTerm on GlossaryTerm {
domain {
...entityDomain
}
+ deprecation {
+ ...deprecationFields
+ }
}
fragment glossaryNodeFields on GlossaryNode {