You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@atlas.apache.org by pp...@apache.org on 2026/03/31 15:32:15 UTC

(atlas) branch atlas-2.5 updated: ATLAS-5260: ATLAS UI: [UI] Detail page – terms from entity header, labels/UDP save fixes (dashboard + dashboardv2) (#585) ( cherry picked from 459281e94ee056dd2cafe00b2b63b79a29187e03)

This is an automated email from the ASF dual-hosted git repository.

ppawar pushed a commit to branch atlas-2.5
in repository https://gitbox.apache.org/repos/asf/atlas.git


The following commit(s) were added to refs/heads/atlas-2.5 by this push:
     new 42887af20 ATLAS-5260: ATLAS UI: [UI] Detail page – terms from entity header, labels/UDP save fixes (dashboard + dashboardv2) (#585) ( cherry picked from 459281e94ee056dd2cafe00b2b63b79a29187e03)
42887af20 is described below

commit 42887af20d027129ed8376159e014ff0e28d4dc4
Author: Prasad Pawar <49...@users.noreply.github.com>
AuthorDate: Tue Mar 31 12:18:57 2026 +0530

    ATLAS-5260: ATLAS UI: [UI] Detail page – terms from entity header, labels/UDP save fixes (dashboard + dashboardv2) (#585)
    ( cherry picked from 459281e94ee056dd2cafe00b2b63b79a29187e03)
---
 .../src/api/apiMethods/detailpageApiMethod.ts      |  15 +-
 dashboard/src/redux/slice/detailPageSlice.ts       |  24 ++-
 dashboard/src/utils/entityDetailMeaningsUtils.ts   |  55 ++++++
 .../EntityDetailTabs/PropertiesTab/Labels.tsx      |  37 +++-
 .../PropertiesTab/UserDefinedProperties.tsx        |  30 ++-
 .../EntityDetailTabs/RelationshipCard.tsx          |  36 +++-
 .../EntityDetailTabs/RelationshipsTab.tsx          | 162 ++++++++++++++--
 dashboardv2/public/css/scss/table.scss             |   4 +
 .../js/views/detail_page/DetailPageLayoutView.js   | 127 +++++++++++--
 .../detail_page/RelationshipCardsLayoutView.js     | 110 ++++++++---
 .../public/js/views/entity/EntityUserDefineView.js | 208 +++++++++++++++++----
 .../js/views/graph/RelationshipLayoutView.js       |   3 +
 .../js/views/profile/ProfileTableLayoutView.js     |   6 +-
 13 files changed, 696 insertions(+), 121 deletions(-)

diff --git a/dashboard/src/api/apiMethods/detailpageApiMethod.ts b/dashboard/src/api/apiMethods/detailpageApiMethod.ts
index 323d92aa9..fea1b7b88 100644
--- a/dashboard/src/api/apiMethods/detailpageApiMethod.ts
+++ b/dashboard/src/api/apiMethods/detailpageApiMethod.ts
@@ -25,7 +25,7 @@ import {
   detailPageRelationshipApiUrl,
   detailPageRelationshipAttributesApiUrl
 } from "../apiUrlLinks/detailpageUrl";
-import { _get } from "./apiMethod";
+import { _get, _post } from "./apiMethod";
 
 const getDetailPageData = (guid: string, params: object, header?: string) => {
   const config = {
@@ -60,13 +60,21 @@ const getAuditData = (params: object) => {
   return _get(auditApiurl(), config);
 };
 
-const getLabels = (guid: string, formData: object) => {
+const getEntityHeader = (guid: string) => {
+  const config = {
+    method: "GET",
+    params: {}
+  };
+  return _get(detailpageApiUrl(guid, "header"), config);
+};
+
+const getLabels = (guid: string, formData: string[]) => {
   const config = {
     method: "POST",
     params: {},
     data: formData
   };
-  return _get(detailPageLabelApiUrl(guid), config);
+  return _post(detailPageLabelApiUrl(guid), config);
 };
 
 const getEntityBusinessMetadata = (guid: string, formData: object) => {
@@ -199,6 +207,7 @@ export {
   getDetailPageAuditData,
   getDetailPageRauditData,
   getAuditData,
+  getEntityHeader,
   getLabels,
   getEntityBusinessMetadata,
   getDetailPageRelationship,
diff --git a/dashboard/src/redux/slice/detailPageSlice.ts b/dashboard/src/redux/slice/detailPageSlice.ts
index 32c2400e3..a48ac02c0 100644
--- a/dashboard/src/redux/slice/detailPageSlice.ts
+++ b/dashboard/src/redux/slice/detailPageSlice.ts
@@ -16,8 +16,15 @@
  */
 
 import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
-import { getDetailPageData } from "../../api/apiMethods/detailpageApiMethod";
+import {
+  getDetailPageData,
+  getEntityHeader
+} from "../../api/apiMethods/detailpageApiMethod";
 import { cloneDeep } from "@utils/Helper";
+import {
+  mapHeaderMeaningsToRelationshipMeanings,
+  shouldMergeMeaningsFromEntityHeader
+} from "@utils/entityDetailMeaningsUtils";
 
 export const fetchDetailPageData = createAsyncThunk(
   "detailPage/fetchDetailPageData",
@@ -26,6 +33,21 @@ export const fetchDetailPageData = createAsyncThunk(
       const response = await getDetailPageData(guid, { minExtInfo: true });
       const { data = {} } = response || {};
       const responseData = cloneDeep(data);
+      const entity = responseData.entity;
+      if (entity && shouldMergeMeaningsFromEntityHeader(entity)) {
+        try {
+          const headerResp = await getEntityHeader(guid);
+          const header = headerResp?.data;
+          const headerMeanings = header?.meanings;
+          if (Array.isArray(headerMeanings) && headerMeanings.length > 0) {
+            entity.relationshipAttributes = entity.relationshipAttributes || {};
+            entity.relationshipAttributes.meanings =
+              mapHeaderMeaningsToRelationshipMeanings(headerMeanings);
+          }
+        } catch {
+          // Terms are optional; detail page still loads without header.
+        }
+      }
       return responseData;
     } catch (error) {
       return rejectWithValue(error);
diff --git a/dashboard/src/utils/entityDetailMeaningsUtils.ts b/dashboard/src/utils/entityDetailMeaningsUtils.ts
new file mode 100644
index 000000000..611db78a1
--- /dev/null
+++ b/dashboard/src/utils/entityDetailMeaningsUtils.ts
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * When entity GET uses ignoreRelationships=true, relationshipAttributes.meanings
+ * is omitted. GET /v2/entity/guid/{guid}/header still returns meanings
+ * (AtlasTermAssignmentHeader[]). Map them to the shape used by entity detail
+ * (guid, relationshipGuid for remove / ShowMoreView).
+ */
+export const mapHeaderMeaningsToRelationshipMeanings = (
+  meanings: unknown
+): any[] => {
+  if (!Array.isArray(meanings) || meanings.length === 0) {
+    return [];
+  }
+  return meanings.map((m: any) => {
+    const guid = m.guid ?? m.termGuid;
+    const relationshipGuid = m.relationshipGuid ?? m.relationGuid;
+    const relationshipStatus =
+      m.relationshipStatus ??
+      (m.status != null ? String(m.status) : "ACTIVE");
+    return {
+      ...m,
+      guid,
+      relationshipGuid,
+      relationshipStatus,
+      termGuid: m.termGuid ?? guid
+    };
+  });
+};
+
+export const shouldMergeMeaningsFromEntityHeader = (entity: any): boolean => {
+  if (!entity || !entity.typeName) {
+    return false;
+  }
+  if (String(entity.typeName).startsWith("AtlasGlossary")) {
+    return false;
+  }
+  const existing = entity.relationshipAttributes?.meanings;
+  return !Array.isArray(existing) || existing.length === 0;
+};
diff --git a/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/Labels.tsx b/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/Labels.tsx
index e27a4df75..77b4f21b7 100644
--- a/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/Labels.tsx
+++ b/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/Labels.tsx
@@ -111,19 +111,38 @@ const Labels = ({ loading, labels }: any) => {
     }
   };
 
+  const normalizeLabelsPayload = (raw: unknown): string[] => {
+    if (!Array.isArray(raw)) {
+      return [];
+    }
+    const out: string[] = [];
+    for (const item of raw) {
+      if (typeof item === "string") {
+        const t = item.trim();
+        if (t) {
+          out.push(t);
+        }
+        continue;
+      }
+      if (item && typeof item === "object") {
+        const o = item as { inputValue?: string; value?: string };
+        const v = o.inputValue ?? o.value;
+        if (typeof v === "string" && v.trim()) {
+          out.push(v.trim());
+        }
+      }
+    }
+    return out;
+  };
+
   const onSubmit = async (values: any) => {
-    let formData = { ...values };
-    if(isEmpty(formData.labels) && isEmpty(labels)){
+    const formData = { ...values };
+    const payload = normalizeLabelsPayload(formData.labels);
+    if (payload.length === 0 && (!labels || labels.length === 0)) {
       return;
     }
-    let data = formData.labels?.map((obj: { inputValue: any }) => {
-      if (obj.inputValue) {
-        return obj.inputValue;
-      }
-      return obj;
-    });
     try {
-      await getLabels(guid, data);
+      await getLabels(guid, payload);
       toast.dismiss(toastId.current);
       toastId.current = toast.success(
         "One or more labels were updated successfully"
diff --git a/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/UserDefinedProperties.tsx b/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/UserDefinedProperties.tsx
index b04a72d6c..4f8080dd5 100644
--- a/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/UserDefinedProperties.tsx
+++ b/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/UserDefinedProperties.tsx
@@ -101,9 +101,19 @@ const UserDefinedProperties = ({ loading, customAttributes, entity }: any) => {
     };
 
   const structureAttributes = (list: any) => {
-    let obj: any = {};
-    list.map((o: any) => {
-      obj[o.key] = o.value;
+    const obj: Record<string, string> = {};
+    if (!Array.isArray(list)) {
+      return obj;
+    }
+    list.forEach((o: { key: string; value: string }) => {
+      const key =
+        typeof o?.key === "string"
+          ? o.key.trim()
+          : String(o?.key ?? "").trim();
+      if (key === "") {
+        return;
+      }
+      obj[key] = o?.value ?? "";
     });
     return obj;
   };
@@ -218,11 +228,15 @@ const UserDefinedProperties = ({ loading, customAttributes, entity }: any) => {
                             variant="outlined"
                             color="success"
                             size="small"
-                            onClick={(e: { stopPropagation: () => void }) => {
-                              e.stopPropagation();
-                              setAddLabel(true);
-                              reset({ customAttributes: [defaultField] });
-                            }}
+                          onClick={(e: { stopPropagation: () => void }) => {
+                            e.stopPropagation();
+                            setAddLabel(true);
+                            reset({
+                              customAttributes: !isEmpty(defaultFieldValues)
+                                ? defaultFieldValues
+                                : [defaultField]
+                            });
+                          }}
                           >
                             Cancel
                           </CustomButton>
diff --git a/dashboard/src/views/DetailPage/EntityDetailTabs/RelationshipCard.tsx b/dashboard/src/views/DetailPage/EntityDetailTabs/RelationshipCard.tsx
index c9bcebb0e..8d03fc0c8 100644
--- a/dashboard/src/views/DetailPage/EntityDetailTabs/RelationshipCard.tsx
+++ b/dashboard/src/views/DetailPage/EntityDetailTabs/RelationshipCard.tsx
@@ -36,7 +36,7 @@ type RelationshipCardProps = {
 	onToggleSort?: (attributeName: string) => void
 	onToggleShowDeleted?: (attributeName: string) => void
 	onPageLimitChange?: (attributeName: string, value: string) => void
-	onPageLimitSubmit?: (attributeName: string) => void
+	onPageLimitSubmit?: (attributeName: string, rawValue: string) => void
 }
 
 function RelationshipCard({
@@ -63,6 +63,8 @@ function RelationshipCard({
 		return data.length
 	}, [totalCount, data.length])
 
+	const minPageLimit = resolvedTotal <= 1 ? 1 : 2
+
 	const canLoadMore = resolvedTotal > data.length
 	const lastRequestSizeRef = useRef<number | null>(null)
 	const prevScrollRef = useRef<{ height: number; top: number } | null>(null)
@@ -148,7 +150,7 @@ function RelationshipCard({
 		event: React.KeyboardEvent<HTMLInputElement>
 	) => {
 		if (event.key === 'Enter') {
-			onPageLimitSubmit?.(attributeName)
+			onPageLimitSubmit?.(attributeName, event.currentTarget.value)
 		}
 	}
 
@@ -240,17 +242,32 @@ function RelationshipCard({
 	const isEmptyCard = resolvedTotal === 0 && isEmpty(data)
 	const isZeroOrOneRecord = data.length <= 1
 	const bodyStyle = useMemo(() => {
-		if (isZeroOrOneRecord) {
-			return { minHeight: 72 }
-		}
 		const maxVisibleRows = 9
 		const rowHeight = 22
 		const bodyPadding = 16
+		const maxHeight = maxVisibleRows * rowHeight + bodyPadding
+
+		if (isZeroOrOneRecord && !canLoadMore) {
+			return { minHeight: 72 }
+		}
+
+		if (canLoadMore) {
+			const visibleRows = Math.min(Math.max(data.length, 1), maxVisibleRows)
+			let baseHeight = visibleRows * rowHeight + bodyPadding
+			baseHeight = Math.max(baseHeight, 80)
+			const h = Math.min(baseHeight, maxHeight)
+			return {
+				height: h,
+				maxHeight: h,
+				minHeight: 0,
+				overflowY: 'auto' as const,
+			}
+		}
+
 		const visibleRows = Math.min(data.length, maxVisibleRows)
 		const baseHeight = visibleRows * rowHeight + bodyPadding
-		const maxHeight = maxVisibleRows * rowHeight + bodyPadding
 		return { height: Math.min(baseHeight, maxHeight), minHeight: 0 }
-	}, [data.length, isZeroOrOneRecord])
+	}, [data.length, isZeroOrOneRecord, canLoadMore])
 
 	const filteredData = useMemo(() => {
 		const query = searchQuery.trim().toLowerCase()
@@ -398,12 +415,11 @@ function RelationshipCard({
 							<input
 								id={`page-limit-${attributeName}`}
 								type='number'
-								min={1}
-								max={resolvedTotal}
+								min={minPageLimit}
 								value={pageLimit && pageLimit > 0 ? pageLimit : ''}
 								onChange={handlePageLimitInputChange}
 								onKeyDown={handlePageLimitKeyDown}
-								aria-label='Page limit'
+								aria-label={`Page limit (minimum ${minPageLimit} for this card)`}
 							/>
 						</div>
 					</div>
diff --git a/dashboard/src/views/DetailPage/EntityDetailTabs/RelationshipsTab.tsx b/dashboard/src/views/DetailPage/EntityDetailTabs/RelationshipsTab.tsx
index 9f26786f8..41dfeab24 100644
--- a/dashboard/src/views/DetailPage/EntityDetailTabs/RelationshipsTab.tsx
+++ b/dashboard/src/views/DetailPage/EntityDetailTabs/RelationshipsTab.tsx
@@ -43,6 +43,12 @@ const RelationshipsTab: React.FC<EntityDetailTabProps> = ({
   const { guid } = useParams<{ guid: string }>();
   const toastId = useRef<any>(null);
   const fetchStartedRef = useRef<boolean>(false);
+  const prevMeaningsSigRef = useRef<string | null>(null);
+  const meaningsCardControlsRef = useRef<{
+    isSorted: boolean;
+    showDeleted: boolean;
+  }>({ isSorted: false, showDeleted: true });
+  const prevCardTotalCountsRef = useRef<Record<string, number>>({});
   const { entityData } = useSelector((state: EntityState) => state.entity);
   const entityTypeName = entity?.typeName;
 
@@ -83,6 +89,7 @@ const RelationshipsTab: React.FC<EntityDetailTabProps> = ({
     setShowInitialSkeletons(true);
     setHasRelationshipApiError(false);
     fetchStartedRef.current = false;
+    prevMeaningsSigRef.current = null;
 
     if (initialSkeletonTimerRef.current) {
       clearTimeout(initialSkeletonTimerRef.current);
@@ -114,6 +121,26 @@ const RelationshipsTab: React.FC<EntityDetailTabProps> = ({
     return [...new Set(names)];
   }, [entityData, entityTypeName]);
 
+  /** Stable fingerprint of assigned glossary terms for meanings card refresh. */
+  const meaningsSignature = useMemo((): string => {
+    const raw = entity?.relationshipAttributes?.meanings;
+    if (!isArray(raw) || isEmpty(raw)) {
+      return '__none__';
+    }
+    const ids = raw
+      .map((m: any) => {
+        if (m && typeof m === 'object') {
+          return String(
+            m.guid || m.termGuid || m.qualifiedName || ''
+          ).trim();
+        }
+        return '';
+      })
+      .filter(Boolean)
+      .sort();
+    return ids.join('|');
+  }, [entity?.relationshipAttributes?.meanings]);
+
   const handleChange = (
     _event: React.MouseEvent<HTMLElement>,
     newAlignment: string
@@ -129,17 +156,33 @@ const RelationshipsTab: React.FC<EntityDetailTabProps> = ({
   };
 
   // Fetch initial relationship attributes data
+  /** Page limit may be 1 only when the card total is 0 or 1; otherwise minimum is 2. */
+  const getMinPageLimitForTotal = (totalCount?: number): number => {
+    if (typeof totalCount !== "number" || !Number.isFinite(totalCount)) {
+      return 1;
+    }
+    if (totalCount <= 0) {
+      return 1;
+    }
+    return totalCount <= 1 ? 1 : 2;
+  };
+
+  const DEFAULT_RELATIONSHIP_PAGE_LIMIT = 100;
+
   const getPageLimit = (relationName: string, totalCount?: number) => {
-    const defaultLimit =
-      typeof totalCount === "number" && totalCount < 100 ? totalCount : 100;
+    const minL = getMinPageLimitForTotal(totalCount);
     const currentLimit = pageLimitByAttr[relationName];
     if (typeof currentLimit !== "number") {
-      return defaultLimit;
+      return Math.max(minL, DEFAULT_RELATIONSHIP_PAGE_LIMIT);
     }
-    if (typeof totalCount === "number" && currentLimit > totalCount) {
+    if (
+      typeof totalCount === "number" &&
+      Number.isFinite(totalCount) &&
+      currentLimit > totalCount
+    ) {
       return totalCount;
     }
-    return currentLimit;
+    return Math.max(minL, currentLimit);
   };
 
   const getRelationshipParams = (
@@ -150,6 +193,8 @@ const RelationshipsTab: React.FC<EntityDetailTabProps> = ({
       isSorted?: boolean;
       showDeleted?: boolean;
       limit?: number;
+      /** Use API default page size instead of stored limit (e.g. after term count changes). */
+      ignoreStoredLimit?: boolean;
     }
   ) => {
     const isSorted =
@@ -163,7 +208,12 @@ const RelationshipsTab: React.FC<EntityDetailTabProps> = ({
     const limit =
       typeof overrides?.limit === "number"
         ? overrides.limit
-        : getPageLimit(relationName, totalCount);
+        : overrides?.ignoreStoredLimit
+          ? Math.max(
+              getMinPageLimitForTotal(totalCount),
+              DEFAULT_RELATIONSHIP_PAGE_LIMIT
+            )
+          : getPageLimit(relationName, totalCount);
     return {
       limit,
       offset,
@@ -188,6 +238,7 @@ const RelationshipsTab: React.FC<EntityDetailTabProps> = ({
       isSorted?: boolean;
       showDeleted?: boolean;
       limit?: number;
+      ignoreStoredLimit?: boolean;
     }
   ) => {
     const params = getRelationshipParams(
@@ -328,22 +379,43 @@ const RelationshipsTab: React.FC<EntityDetailTabProps> = ({
     if (isEmpty(cardTotalCounts)) {
       return;
     }
+    const snapshot = { ...prevCardTotalCountsRef.current };
     setPageLimitByAttr((prev) => {
       const next = { ...prev };
       Object.keys(cardTotalCounts).forEach((relationName) => {
         const totalCount = cardTotalCounts[relationName];
-        const defaultLimit = totalCount < 100 ? totalCount : 100;
+        const minL = getMinPageLimitForTotal(totalCount);
         const current = next[relationName];
+        const prevTotal = snapshot[relationName];
+
         if (typeof current !== "number") {
-          next[relationName] = defaultLimit;
           return;
         }
-        if (current > totalCount) {
+        if (totalCount > 0 && current > totalCount) {
           next[relationName] = totalCount;
+          return;
+        }
+        if (current < minL) {
+          next[relationName] = minL;
+          return;
+        }
+        if (
+          typeof prevTotal === "number" &&
+          totalCount > prevTotal &&
+          current === prevTotal &&
+          prevTotal > 0
+        ) {
+          next[relationName] = Math.max(minL, DEFAULT_RELATIONSHIP_PAGE_LIMIT);
         }
       });
       return next;
     });
+    Object.keys(cardTotalCounts).forEach((key) => {
+      const v = cardTotalCounts[key];
+      if (typeof v === "number") {
+        prevCardTotalCountsRef.current[key] = v;
+      }
+    });
   }, [cardTotalCounts]);
 
   const resetRelationshipData = async (
@@ -352,6 +424,11 @@ const RelationshipsTab: React.FC<EntityDetailTabProps> = ({
       isSorted?: boolean;
       showDeleted?: boolean;
       limit?: number;
+      ignoreStoredLimit?: boolean;
+    },
+    options?: {
+      /** Use when term assignments changed so stale totalCount (e.g. 0) does not cap the page. */
+      refreshTotalFromApi?: boolean;
     }
   ) => {
     if (!guid || cardLoadingByName[relationName]) {
@@ -359,12 +436,18 @@ const RelationshipsTab: React.FC<EntityDetailTabProps> = ({
     }
     setCardResettingByName((prev) => ({ ...prev, [relationName]: true }));
     try {
-      const totalCount = cardTotalCounts[relationName];
+      const totalCount = options?.refreshTotalFromApi
+        ? undefined
+        : cardTotalCounts[relationName];
       const response = await fetchRelationshipData(
         relationName,
         0,
         totalCount,
-        overrides
+        {
+          ...(overrides || {}),
+          ignoreStoredLimit:
+            !!options?.refreshTotalFromApi || !!overrides?.ignoreStoredLimit,
+        }
       );
       const entities = response.entities;
       const updated = isArray(entities)
@@ -390,6 +473,41 @@ const RelationshipsTab: React.FC<EntityDetailTabProps> = ({
     }
   };
 
+  const resetRelationshipDataRef = useRef(resetRelationshipData);
+  resetRelationshipDataRef.current = resetRelationshipData;
+
+  meaningsCardControlsRef.current = {
+    isSorted: !!sortByNameByAttr['meanings'],
+    showDeleted: !!showDeletedByAttr['meanings']
+  };
+
+  useEffect(() => {
+    if (
+      !guid ||
+      !initialLoadDone ||
+      !relationNames.includes('meanings')
+    ) {
+      return;
+    }
+    if (prevMeaningsSigRef.current === null) {
+      prevMeaningsSigRef.current = meaningsSignature;
+      return;
+    }
+    if (prevMeaningsSigRef.current === meaningsSignature) {
+      return;
+    }
+    prevMeaningsSigRef.current = meaningsSignature;
+    const c = meaningsCardControlsRef.current;
+    void resetRelationshipDataRef.current(
+      'meanings',
+      {
+        isSorted: c.isSorted,
+        showDeleted: c.showDeleted
+      },
+      { refreshTotalFromApi: true }
+    );
+  }, [meaningsSignature, initialLoadDone, guid, relationNames]);
+
   const handleToggleSort = (relationName: string) => {
     const nextSorted = !sortByNameByAttr[relationName];
     setSortByNameByAttr((prev) => ({
@@ -417,16 +535,20 @@ const RelationshipsTab: React.FC<EntityDetailTabProps> = ({
     setPageLimitByAttr((prev) => ({ ...prev, [relationName]: parsed }));
   };
 
-  const handlePageLimitSubmit = (relationName: string) => {
+  const handlePageLimitSubmit = (relationName: string, rawValue: string) => {
     const totalCount = cardTotalCounts[relationName] ?? 0;
-    const defaultLimit = totalCount < 100 ? totalCount : 100;
-    const currentLimit = pageLimitByAttr[relationName];
-    let nextLimit = currentLimit;
-    if (!Number.isFinite(currentLimit) || currentLimit <= 0) {
-      nextLimit = defaultLimit;
-    }
-    if (totalCount > 0 && nextLimit > totalCount) {
-      nextLimit = totalCount;
+    const minL = getMinPageLimitForTotal(totalCount);
+    const parsed = parseInt(String(rawValue).trim(), 10);
+    let nextLimit = Number.isFinite(parsed) && parsed > 0
+      ? parsed
+      : DEFAULT_RELATIONSHIP_PAGE_LIMIT;
+    if (totalCount > 0) {
+      nextLimit = Math.max(minL, nextLimit);
+      if (nextLimit > totalCount) {
+        nextLimit = totalCount;
+      }
+    } else {
+      nextLimit = Math.max(1, nextLimit);
     }
     setPageLimitByAttr((prev) => ({ ...prev, [relationName]: nextLimit }));
     resetRelationshipData(relationName, { limit: nextLimit });
diff --git a/dashboardv2/public/css/scss/table.scss b/dashboardv2/public/css/scss/table.scss
index 905b5eddf..11de40d7a 100644
--- a/dashboardv2/public/css/scss/table.scss
+++ b/dashboardv2/public/css/scss/table.scss
@@ -68,6 +68,10 @@ tr.empty {
     }
 }
 
+.detail-page #tab-details .entity-detail-table {
+    min-height: 0;
+}
+
 .type-node-details {
     .header {
         .pretty.p-switch .state:before {
diff --git a/dashboardv2/public/js/views/detail_page/DetailPageLayoutView.js b/dashboardv2/public/js/views/detail_page/DetailPageLayoutView.js
index 59006946f..b69732aab 100644
--- a/dashboardv2/public/js/views/detail_page/DetailPageLayoutView.js
+++ b/dashboardv2/public/js/views/detail_page/DetailPageLayoutView.js
@@ -151,8 +151,13 @@ define(['require',
             bindEvents: function() {
                 var that = this;
                 this.listenTo(this.collection, 'reset', function() {
+                    var tags = {};
+                    try {
                     this.entityObject = this.collection.first().toJSON();
                     var collectionJSON = this.entityObject.entity;
+                    collectionJSON.attributes = collectionJSON.attributes || {};
+                    collectionJSON.relationshipAttributes =
+                        collectionJSON.relationshipAttributes || {};
                     this.activeEntityDef = this.entityDefCollection.fullCollection.findWhere({ name: collectionJSON.typeName });
                     if (!this.activeEntityDef) {
                         Utils.backButtonClick();
@@ -253,7 +258,7 @@ define(['require',
                                 this.ui.description.hide();
                             }
                         }
-                        var tags = {
+                        tags = {
                             'self': [],
                             'propagated': [],
                             'propagatedMap': {},
@@ -284,8 +289,37 @@ define(['require',
                         } else {
                             this.generateTag([]);
                         }
-                        if (collectionJSON.relationshipAttributes && collectionJSON.relationshipAttributes.meanings) {
-                            this.generateTerm(collectionJSON.relationshipAttributes.meanings);
+                        if (collectionJSON && !_.startsWith(collectionJSON.typeName, "AtlasGlossary")) {
+                            if (collectionJSON.relationshipAttributes && collectionJSON.relationshipAttributes.meanings && collectionJSON.relationshipAttributes.meanings.length) {
+                                this.generateTerm(collectionJSON.relationshipAttributes.meanings);
+                                this.ensureRelationshipView();
+                            } else {
+                                $.ajax({
+                                    url: UrlLinks.entityHeaderApiUrl(that.id),
+                                    type: 'GET',
+                                    contentType: 'application/json',
+                                    dataType: 'json',
+                                    success: function(header) {
+                                        var raw = header && header.meanings ?
+                                            header.meanings : [];
+                                        var normalized = raw.length ?
+                                            that.normalizeHeaderMeanings(raw) : [];
+                                        var model = that.collection.first();
+                                        if (model) {
+                                            var ent = model.get('entity');
+                                            ent.relationshipAttributes =
+                                                ent.relationshipAttributes || {};
+                                            ent.relationshipAttributes.meanings = normalized;
+                                            model.set('entity', ent);
+                                            if (that.detailPageObj) {
+                                                that.detailPageObj.entity = ent;
+                                            }
+                                        }
+                                        that.generateTerm(normalized);
+                                        that.ensureRelationshipView();
+                                    }
+                                });
+                            }
                         }
                         if (Globals.entityTypeConfList && _.isEmptyArray(Globals.entityTypeConfList)) {
                             this.editEntity = true;
@@ -301,7 +335,6 @@ define(['require',
                             collectionJSON.attributes.columns = valueSorted;
                         }
                     }
-                    this.hideLoader();
                     var obj = {
                         entity: collectionJSON,
                         guid: this.id,
@@ -330,9 +363,22 @@ define(['require',
                         this.renderEntityBusinessMetadataView(obj);
                     }
                     this.detailPageObj = obj;
-                    if ((this.value && this.value.tabActive === 'relationship') ||
-                        this.$('.tab-content .tab-pane[role="relationship"]').hasClass('active')) {
+                    var relationshipTabActive = (this.value && this.value.tabActive === 'relationship') ||
+                        this.$('.tab-content .tab-pane[role="relationship"]').hasClass('active');
+                    if (relationshipTabActive) {
                         this.renderRelationshipLayoutView(obj);
+                    } else {
+                        var relViewStale = this.RRelationshipLayoutView.currentView;
+                        if (relViewStale && collectionJSON) {
+                            relViewStale.entity = collectionJSON;
+                            relViewStale.guid = this.id;
+                            if (this.entityObject && this.entityObject.referredEntities) {
+                                relViewStale.referredEntities = _.extend({}, this.entityObject.referredEntities);
+                            }
+                            if (_.isFunction(relViewStale.createData)) {
+                                relViewStale.graphData = relViewStale.createData(relViewStale.entity);
+                            }
+                        }
                     }
                     this.renderAuditTableLayoutView(obj);
                     this.renderTagTableLayoutView(obj);
@@ -369,8 +415,11 @@ define(['require',
                         var schemaElementsAttribute = schemaOptions && schemaOptions.schemaElementsAttribute;
                         if (!_.isEmpty(schemaElementsAttribute)) {
                             this.$('.schemaTable').show();
+                            var relA = collectionJSON.relationshipAttributes || {};
+                            var attrA = collectionJSON.attributes || {};
                             this.renderSchemaLayoutView(_.extend({}, obj, {
-                                attribute: collectionJSON.relationshipAttributes[schemaElementsAttribute] || collectionJSON.attributes[schemaElementsAttribute]
+                                attribute: relA[schemaElementsAttribute] ||
+                                    attrA[schemaElementsAttribute]
                             }));
                         } else {
                             this.$('.schemaTable').hide();
@@ -388,9 +437,22 @@ define(['require',
                             this.redirectToDefaultTab("lineage");
                         }
                     }
+                    } catch (err) {
+                        if (window.console && console.error) {
+                            console.error(err);
+                        }
+                        Utils.notifyError({
+                            content: (err && err.message) ? err.message :
+                                'Failed to refresh entity detail.'
+                        });
+                    } finally {
+                        this.hideLoader();
+                        this.$('.fontLoader-relative').removeClass('show');
+                    }
 
                 }, this);
                 this.listenTo(this.collection, 'error', function(model, response) {
+                    this.hideLoader();
                     this.$('.fontLoader-relative').removeClass('show');
                     if (response.responseJSON) {
                         Utils.notifyError({
@@ -474,7 +536,15 @@ define(['require',
                 }
                 var relationshipView = this.RRelationshipLayoutView.currentView;
                 if (relationshipView && relationshipView.ensureCardsView) {
-                    relationshipView.ensureCardsView(false);
+                    relationshipView.entity = this.detailPageObj.entity;
+                    relationshipView.guid = this.detailPageObj.guid;
+                    if (this.entityObject && this.entityObject.referredEntities) {
+                        relationshipView.referredEntities = _.extend({}, this.entityObject.referredEntities);
+                    }
+                    if (_.isFunction(relationshipView.createData)) {
+                        relationshipView.graphData = relationshipView.createData(relationshipView.entity);
+                    }
+                    relationshipView.ensureCardsView(true);
                 } else {
                     this.renderRelationshipLayoutView(this.detailPageObj);
                 }
@@ -538,15 +608,31 @@ define(['require',
             },
             onClickTermCross: function(e) {
                 var $el = $(e.currentTarget),
-                    termGuid = $el.data('guid'),
-                    termName = $el.text(),
+                    termGuid = $el.data('guid') || $el.data('termGuid'),
+                    relFromChip = $el.data('relationshipGuid'),
+                    termName = ($el.prev('span').length ? $el.prev('span').text() : '').trim(),
                     that = this,
-                    termObj = _.find(this.collection.first().get('entity').relationshipAttributes.meanings, { guid: termGuid });
+                    entity = this.collection.first() && this.collection.first().get('entity'),
+                    meanings = entity && entity.relationshipAttributes && entity.relationshipAttributes.meanings,
+                    termObj = meanings ? _.find(meanings, function(m) {
+                        var tg = m.termGuid || m.guid;
+                        return tg === termGuid || String(tg) === String(termGuid);
+                    }) : null,
+                    relationshipGuid = relFromChip || (termObj && (termObj.relationshipGuid || termObj.relationGuid));
+                if (!termName) {
+                    termName = ($el.closest('[data-id="termClick"]').attr('title') || '').trim();
+                }
+                if (!termGuid || !relationshipGuid) {
+                    Utils.notifyError({
+                        content: "Cannot remove term: missing term or relationship reference. Refresh the page and try again."
+                    });
+                    return;
+                }
                 CommonViewFunction.removeCategoryTermAssociation({
                     termGuid: termGuid,
                     model: {
                         guid: that.id,
-                        relationshipGuid: termObj.relationshipGuid
+                        relationshipGuid: relationshipGuid
                     },
                     collection: that.glossaryCollection,
                     msg: "<div class='ellipsis-with-margin'>Remove: " + "<b>" + _.escape(termName) + "</b> assignment from <b>" + this.name + "?</b></div>",
@@ -560,6 +646,19 @@ define(['require',
                     }
                 });
             },
+            normalizeHeaderMeanings: function(meanings) {
+                return _.map(meanings, function(m) {
+                    var guid = m.guid || m.termGuid;
+                    var relationshipGuid = m.relationshipGuid || m.relationGuid;
+                    var relationshipStatus = m.relationshipStatus || (m.status ? String(m.status) : "ACTIVE");
+                    return _.extend({}, m, {
+                        guid: guid,
+                        relationshipGuid: relationshipGuid,
+                        relationshipStatus: relationshipStatus,
+                        termGuid: m.termGuid || guid
+                    });
+                });
+            },
             generateTag: function(tagObject) {
                 var that = this,
                     tagData = "",
@@ -583,7 +682,9 @@ define(['require',
                     termData = "";
                 _.each(data, function(val) {
                     var glossaryName = val.qualifiedName ? val.qualifiedName : val.displayText;
-                    termData += '<span class="btn btn-action btn-sm btn-icon btn-blue" data-id="termClick" title= "' + _.escape(glossaryName) + '"><span>' + _.escape(glossaryName) + '</span><i class="' + (val.relationshipStatus == "ACTIVE" ? 'fa fa-close' : "") + '" data-id="deleteTerm" data-guid="' + val.guid + '" data-type="term" title="Remove Term"></i></span>';
+                    var termGuidAttr = val.termGuid || val.guid;
+                    var relGuidAttr = val.relationshipGuid || val.relationGuid || "";
+                    termData += '<span class="btn btn-action btn-sm btn-icon btn-blue" data-id="termClick" title="' + _.escape(glossaryName) + '"><span>' + _.escape(glossaryName) + '</span><i class="' + (val.relationshipStatus == "ACTIVE" ? 'fa fa-close' : "") + '" data-id="deleteTerm" data-guid="' + _.escape(termGuidAttr) + '" data-relationship-guid="' + _.escape(relGuidAttr) + '" data-type="term" title="Remove Term"></i></span>';
                 });
                 this.ui.termList.find("span.btn").remove();
                 this.ui.termList.prepend(termData);
diff --git a/dashboardv2/public/js/views/detail_page/RelationshipCardsLayoutView.js b/dashboardv2/public/js/views/detail_page/RelationshipCardsLayoutView.js
index 6c60e36bc..81bf2aa2b 100644
--- a/dashboardv2/public/js/views/detail_page/RelationshipCardsLayoutView.js
+++ b/dashboardv2/public/js/views/detail_page/RelationshipCardsLayoutView.js
@@ -45,9 +45,31 @@ define([
             this.sortByName = {};
             this.showDeletedByName = {};
             this.pageLimitByName = {};
+            /** Previous approximate counts per card — used to grow page limit when totals increase (e.g. new term). */
+            this._lastCardCountSnapshot = {};
             this.isInitialLoading = false;
             this.hasApiError = false;
         },
+        adjustPageLimitWhenTotalGrew: function(name, previousTotal, newTotal) {
+            if (!_.isNumber(previousTotal) || !_.isNumber(newTotal) || newTotal <= previousTotal) {
+                return false;
+            }
+            var cur = this.pageLimitByName[name];
+            var minL = newTotal <= 1 ? 1 : 2;
+            var defLimit = Math.max(minL, 100);
+            if (_.isNumber(cur) && cur === previousTotal && previousTotal > 0) {
+                this.pageLimitByName[name] = defLimit;
+                return true;
+            }
+            return false;
+        },
+        getMinPageLimitForTotal: function(totalCount) {
+            var c = _.isNumber(totalCount) ? totalCount : parseInt(totalCount, 10);
+            if (!_.isFinite(c) || c <= 0) {
+                return 1;
+            }
+            return c <= 1 ? 1 : 2;
+        },
         onRender: function() {
             this.fetchInitialCards();
         },
@@ -93,15 +115,16 @@ define([
         },
         getPageLimit: function(relationName, totalCount) {
             var count = _.isNumber(totalCount) ? totalCount : parseInt(totalCount, 10);
-            var defaultLimit = _.isFinite(count) && count < 100 ? count : 100;
+            var defaultLimit = 100;
+            var minL = this.getMinPageLimitForTotal(totalCount);
             var currentLimit = this.pageLimitByName[relationName];
             if (!_.isNumber(currentLimit)) {
-                return defaultLimit;
+                return Math.max(minL, defaultLimit);
             }
             if (_.isFinite(count) && currentLimit > count) {
                 return count;
             }
-            return currentLimit;
+            return Math.max(minL, currentLimit);
         },
         getRelationshipParams: function(relationName, offset, totalCount, overrides) {
             var isSorted = _.isBoolean(overrides && overrides.isSorted)
@@ -110,9 +133,21 @@ define([
             var showDeleted = _.isBoolean(overrides && overrides.showDeleted)
                 ? overrides.showDeleted
                 : !!this.showDeletedByName[relationName];
-            var limit = _.isNumber(overrides && overrides.limit)
-                ? overrides.limit
-                : this.getPageLimit(relationName, totalCount);
+            var count = _.isNumber(totalCount) ? totalCount : parseInt(totalCount, 10);
+            var minForTotal = this.getMinPageLimitForTotal(totalCount);
+            var ignoreStored = overrides && overrides.ignoreStoredLimit;
+            var limit;
+            if (_.isNumber(overrides && overrides.limit)) {
+                limit = overrides.limit;
+                if (_.isFinite(count) && count > 0) {
+                    var maxL = count < 100 ? count : 100;
+                    limit = Math.min(maxL, Math.max(minForTotal, limit));
+                }
+            } else if (ignoreStored) {
+                limit = Math.max(minForTotal, 100);
+            } else {
+                limit = this.getPageLimit(relationName, totalCount);
+            }
             return {
                 limit: limit,
                 offset: offset,
@@ -192,7 +227,7 @@ define([
                     that.cardCounts[name] = 0;
                     if (_.isUndefined(that.sortByName[name])) { that.sortByName[name] = false; }
                     if (_.isUndefined(that.showDeletedByName[name])) { that.showDeletedByName[name] = true; }
-                    that.pageLimitByName[name] = that.getPageLimit(name, 0);
+                    that._lastCardCountSnapshot[name] = 0;
                     return;
                 }
                 var entities = response.entities ? response.entities : [];
@@ -213,8 +248,24 @@ define([
                 if (_.isUndefined(that.showDeletedByName[name])) {
                     that.showDeletedByName[name] = true;
                 }
-                if (_.isUndefined(that.pageLimitByName[name])) {
-                    that.pageLimitByName[name] = that.getPageLimit(name, that.cardCounts[name]);
+                var prevSnap = that._lastCardCountSnapshot[name];
+                var didBump = that.adjustPageLimitWhenTotalGrew(name, prevSnap, that.cardCounts[name]);
+                that._lastCardCountSnapshot[name] = that.cardCounts[name];
+                var listLen = (that.cardData[name] || []).length;
+                var totalN = that.cardCounts[name];
+                if (didBump && _.isNumber(totalN) && listLen < totalN) {
+                    that.exhaustedByName[name] = false;
+                    _.defer(function() {
+                        if (that.resettingByName[name] || that.loadingByName[name]) {
+                            return;
+                        }
+                        that.resetRelationshipData(name);
+                    });
+                    return;
+                }
+                var minClamp = that.getMinPageLimitForTotal(that.cardCounts[name]);
+                if (_.isNumber(that.pageLimitByName[name]) && that.pageLimitByName[name] < minClamp) {
+                    that.pageLimitByName[name] = minClamp;
                 }
                 var updatedCount = that.getTotalCountForName(name, that.cardData[name].length);
                 if (updatedCount <= that.cardData[name].length) {
@@ -354,6 +405,13 @@ define([
                 } else if (!_.isUndefined(response.totalCount)) {
                     that.cardCounts[relationName] = response.totalCount;
                 }
+                var prevSnapR = that._lastCardCountSnapshot[relationName];
+                that.adjustPageLimitWhenTotalGrew(relationName, prevSnapR, that.cardCounts[relationName]);
+                that._lastCardCountSnapshot[relationName] = that.cardCounts[relationName];
+                var minAfterReset = that.getMinPageLimitForTotal(that.cardCounts[relationName]);
+                if (_.isNumber(that.pageLimitByName[relationName]) && that.pageLimitByName[relationName] < minAfterReset) {
+                    that.pageLimitByName[relationName] = minAfterReset;
+                }
                 that.exhaustedByName[relationName] = false;
             }).fail(function(error) {
                 Utils.notifyError({
@@ -416,9 +474,15 @@ define([
                 ? (visibleRows * rowHeight + bodyPadding)
                 : 0;
             var isZeroOrOneRecord = list.length <= 1;
-            var scrollStyle = isZeroOrOneRecord
-                ? "style='min-height:72px;overflow-y:auto;'"
-                : "style='max-height:" + scrollHeight + "px; overflow-y: auto;'";
+            var scrollStyle;
+            if (isZeroOrOneRecord && !canLoadMore) {
+                scrollStyle = "style='min-height:72px;overflow-y:auto;'";
+            } else if (canLoadMore && isZeroOrOneRecord) {
+                var capPx = Math.max(72, Math.min(2 * rowHeight + bodyPadding, maxVisibleRows * rowHeight + bodyPadding));
+                scrollStyle = "style='max-height:" + capPx + "px;min-height:72px;overflow-y:auto;'";
+            } else {
+                scrollStyle = "style='max-height:" + scrollHeight + "px; overflow-y: auto;'";
+            }
             var emptyClass = list.length ? "" : " relationship-card--empty";
             var noMatchHtml = "<div class='relationship-card-no-match' style='display:none'></div>";
             var loadMoreHtml = canLoadMore
@@ -446,12 +510,13 @@ define([
                 "<span>Show deleted</span></label>" +
                 "</div>";
             var pageLimit = this.getPageLimit(name, count);
+            var minPageLimit = this.getMinPageLimitForTotal(count);
             var footerHtml = "<div class='relationship-card-footer'>" +
                 "<div class='relationship-card-footer-row'>" +
                 "<span class='relationship-card-count'>Showing " + list.length + " of " + count + "</span>" +
                 "<div class='relationship-card-page-limit'>" +
                 "<label for='page-limit-" + _.escape(name) + "'>Limit</label>" +
-                "<input id='page-limit-" + _.escape(name) + "' class='relationship-card-page-limit-input' type='number' min='1' max='" + count + "' data-name='" + _.escape(name) + "' value='" + pageLimit + "' />" +
+                "<input id='page-limit-" + _.escape(name) + "' class='relationship-card-page-limit-input' type='number' min='" + minPageLimit + "' data-name='" + _.escape(name) + "' value='" + pageLimit + "' />" +
                 "</div>" +
                 "</div>" +
                 "</div>";
@@ -655,14 +720,17 @@ define([
                     return;
                 }
                 var count = that.getTotalCountForName(name, 0);
-                var defaultLimit = count < 100 ? count : 100;
-                var currentLimit = that.pageLimitByName[name];
-                var nextLimit = currentLimit;
-                if (!_.isFinite(nextLimit) || nextLimit <= 0) {
-                    nextLimit = defaultLimit;
-                }
-                if (count > 0 && nextLimit > count) {
-                    nextLimit = count;
+                var minL = that.getMinPageLimitForTotal(count);
+                var rawValue = $(e.currentTarget).val();
+                var parsed = parseInt(String(rawValue).trim(), 10);
+                var nextLimit = _.isFinite(parsed) && parsed > 0 ? parsed : 100;
+                if (count > 0) {
+                    nextLimit = Math.max(minL, nextLimit);
+                    if (nextLimit > count) {
+                        nextLimit = count;
+                    }
+                } else {
+                    nextLimit = Math.max(1, nextLimit);
                 }
                 that.pageLimitByName[name] = nextLimit;
                 that.resetRelationshipData(name, { limit: nextLimit });
diff --git a/dashboardv2/public/js/views/entity/EntityUserDefineView.js b/dashboardv2/public/js/views/entity/EntityUserDefineView.js
index 311fbe505..4c898267a 100644
--- a/dashboardv2/public/js/views/entity/EntityUserDefineView.js
+++ b/dashboardv2/public/js/views/entity/EntityUserDefineView.js
@@ -24,7 +24,8 @@ define(['require',
     'utils/Enums',
     'utils/Messages',
     'utils/CommonViewFunction',
-], function(require, Backbone, EntityUserDefineView_tmpl, VEntity, Utils, Enums, Messages, CommonViewFunction) {
+    'utils/UrlLinks',
+], function(require, Backbone, EntityUserDefineView_tmpl, VEntity, Utils, Enums, Messages, CommonViewFunction, UrlLinks) {
     'use strict';
 
     return Backbone.Marionette.LayoutView.extend({
@@ -57,7 +58,7 @@ define(['require',
             return events;
         },
         initialize: function(options) {
-            _.extend(this, _.pick(options, 'entity', 'customFilter', 'renderAuditTableLayoutView'));
+            _.extend(this, _.pick(options, 'entity', 'customFilter', 'renderAuditTableLayoutView', 'entityDefCollection'));
             this.userDefineAttr = this.entity && this.entity.customAttributes || [];
             this.initialCall = false;
             this.swapItem = false, this.saveAttrItems = false;
@@ -144,44 +145,183 @@ define(['require',
             });
             return obj;
         },
+        getMergedRelationshipAttributeDefs: function() {
+            var typeName = this.entity && this.entity.typeName;
+            if (!this.entityDefCollection || !typeName) {
+                return [];
+            }
+            var entityDef = this.entityDefCollection.fullCollection.findWhere({ name: typeName });
+            if (!entityDef) {
+                return [];
+            }
+            var merged = Utils.getNestedSuperTypeObj({
+                data: entityDef.toJSON(),
+                collection: this.entityDefCollection,
+                attrMerge: true,
+                seperateRelatioshipAttr: true
+            });
+            return (merged && merged.relationshipAttributeDefs) || [];
+        },
+        isRelationshipAttrValueMissing: function(val) {
+            if (val === undefined || val === null) {
+                return true;
+            }
+            if (Array.isArray(val)) {
+                return val.length === 0;
+            }
+            if (typeof val === 'object') {
+                return !val.guid;
+            }
+            return false;
+        },
+        /**
+         * Detail page GET keeps ignoreRelationships=true. For UDP save, one
+         * extra GET with ignoreRelationships=false fills relationshipAttributes
+         * so mandatory refs match the real edges (avoids multi-type attrs
+         * e.g. table vs iceberg_table_storagedesc where search/entities[0]
+         * picks the wrong neighbor).
+         */
+        enrichEntityPayloadForRelationshipSave: function(entityJson, done) {
+            var that = this;
+            entityJson.relationshipAttributes = entityJson.relationshipAttributes || {};
+            var srcMeanings = that.entity && that.entity.relationshipAttributes &&
+                that.entity.relationshipAttributes.meanings;
+            var hadMeanings = !!(srcMeanings && srcMeanings.length);
+            if (hadMeanings) {
+                entityJson.relationshipAttributes.meanings = srcMeanings;
+            }
+            var fullUrl = UrlLinks.baseUrlV2 + '/entity/guid/' +
+                encodeURIComponent(entityJson.guid) +
+                '?ignoreRelationships=false';
+            $.ajax({
+                url: fullUrl,
+                type: 'GET',
+                dataType: 'json'
+            }).done(function(resp) {
+                var fullEntity = resp && resp.entity;
+                if (fullEntity && fullEntity.relationshipAttributes) {
+                    _.each(fullEntity.relationshipAttributes, function(val, key) {
+                        if (key === 'meanings' && hadMeanings) {
+                            return;
+                        }
+                        if (!that.isRelationshipAttrValueMissing(
+                                entityJson.relationshipAttributes[key])) {
+                            return;
+                        }
+                        entityJson.relationshipAttributes[key] = val;
+                    });
+                }
+                done();
+            }).fail(function() {
+                that.enrichMandatoryRelsViaRelationshipSearch(entityJson, done);
+            });
+        },
+        /**
+         * Fallback when full entity GET fails: fill mandatory missing rel attrs
+         * via relationship search (ambiguous if several types share attr name).
+         */
+        enrichMandatoryRelsViaRelationshipSearch: function(entityJson, done) {
+            var that = this;
+            var relDefs = this.getMergedRelationshipAttributeDefs();
+            var toFetch = [];
+            _.each(relDefs, function(rd) {
+                if (rd.isOptional !== false) {
+                    return;
+                }
+                var name = rd.name;
+                if (!that.isRelationshipAttrValueMissing(
+                        entityJson.relationshipAttributes[name])) {
+                    return;
+                }
+                toFetch.push(rd);
+            });
+            if (!toFetch.length) {
+                done();
+                return;
+            }
+            var ajaxCalls = _.map(toFetch, function(rd) {
+                var relationName = rd.name;
+                return $.ajax({
+                    url: UrlLinks.relationshipSearchV2ApiUrl(),
+                    type: 'GET',
+                    dataType: 'json',
+                    data: {
+                        limit: 100,
+                        offset: 0,
+                        guid: entityJson.guid,
+                        disableDefaultSorting: true,
+                        excludeDeletedEntities: false,
+                        includeSubClassifications: true,
+                        includeSubTypes: true,
+                        includeClassificationAttributes: true,
+                        relation: relationName,
+                        getApproximateCount: true
+                    }
+                }).then(function(response) {
+                    var entities = (response && response.entities) ? response.entities : [];
+                    if (!entities.length) {
+                        return;
+                    }
+                    var isSet = rd.cardinality === 'SET' ||
+                        (rd.typeName && rd.typeName.indexOf('array') === 0);
+                    if (isSet) {
+                        entityJson.relationshipAttributes[relationName] = _.map(entities, function(ent) {
+                            return { guid: ent.guid, typeName: ent.typeName };
+                        });
+                    } else {
+                        var t = entities[0];
+                        entityJson.relationshipAttributes[relationName] = {
+                            guid: t.guid,
+                            typeName: t.typeName
+                        };
+                    }
+                });
+            });
+            $.when.apply($, ajaxCalls).always(function() {
+                done();
+            });
+        },
         saveAttributes: function(list) {
             var that = this;
             var entityJson = that.entityModel.toJSON();
             var properties = that.structureAttributes(list);
             entityJson.customAttributes = properties;
-            var payload = { entity: entityJson };
-            that.entityModel.createOreditEntity({
-                data: JSON.stringify(payload),
-                type: 'POST',
-                success: function() {
-                    var msg = that.initialCall ? 'addSuccessMessage' : 'editSuccessMessage',
-                        caption = "One or more user-defined propertie"; // 's' will be added in abbreviation function
-                    that.customAttibutes = list;
-                    if (list.length === 0) {
-                        msg = 'removeSuccessMessage';
-                        caption = "One or more existing user-defined propertie";
-                    }
-                    Utils.notifySuccess({
-                        content: caption + Messages.getAbbreviationMsg(true, msg)
-                    });
-                    that.swapItem = false;
-                    that.saveAttrItems = false;
-                    that.render();
-                    if (that.renderAuditTableLayoutView) {
-                        that.renderAuditTableLayoutView();
+            that.enrichEntityPayloadForRelationshipSave(entityJson, function() {
+                var payload = { entity: entityJson };
+                that.entityModel.createOreditEntity({
+                    data: JSON.stringify(payload),
+                    type: 'POST',
+                    success: function() {
+                        var msg = that.initialCall ? 'addSuccessMessage' : 'editSuccessMessage',
+                            caption = "One or more user-defined propertie"; // 's' added in abbreviation fn
+                        that.customAttibutes = list;
+                        if (list.length === 0) {
+                            msg = 'removeSuccessMessage';
+                            caption = "One or more existing user-defined propertie";
+                        }
+                        Utils.notifySuccess({
+                            content: caption + Messages.getAbbreviationMsg(true, msg)
+                        });
+                        that.swapItem = false;
+                        that.saveAttrItems = false;
+                        that.render();
+                        if (that.renderAuditTableLayoutView) {
+                            that.renderAuditTableLayoutView();
+                        }
+                    },
+                    error: function(e) {
+                        that.initialCall = false;
+                        Utils.notifyError({
+                            content: (e && e.message) ? e.message :
+                                'Failed to save user-defined properties'
+                        });
+                        that.ui.saveAttrItems.attr("disabled", false);
+                    },
+                    complete: function() {
+                        that.ui.saveAttrItems.attr("disabled", false);
+                        that.initialCall = false;
                     }
-                },
-                error: function(e) {
-                    that.initialCall = false;
-                    Utils.notifySuccess({
-                        content: e.message
-                    });
-                    that.ui.saveAttrItems.attr("disabled", false);
-                },
-                complete: function() {
-                    that.ui.saveAttrItems.attr("disabled", false);
-                    that.initialCall = false;
-                }
+                });
             });
         },
         setAttributeModal: function(itemView) {
diff --git a/dashboardv2/public/js/views/graph/RelationshipLayoutView.js b/dashboardv2/public/js/views/graph/RelationshipLayoutView.js
index e47cb9bd2..9c11c0017 100644
--- a/dashboardv2/public/js/views/graph/RelationshipLayoutView.js
+++ b/dashboardv2/public/js/views/graph/RelationshipLayoutView.js
@@ -180,6 +180,9 @@ define([
                     if (forceRefresh) {
                         this.relationshipCardsViewInstance.cardData = {};
                         this.relationshipCardsViewInstance.cardCounts = {};
+                        this.relationshipCardsViewInstance.pageLimitByName = {};
+                        this.relationshipCardsViewInstance.exhaustedByName = {};
+                        this.relationshipCardsViewInstance._lastCardCountSnapshot = {};
                         this.relationshipCardsViewInstance.fetchInitialCards();
                     } else {
                         var hasData = _.keys(this.relationshipCardsViewInstance.cardData || {}).length > 0;
diff --git a/dashboardv2/public/js/views/profile/ProfileTableLayoutView.js b/dashboardv2/public/js/views/profile/ProfileTableLayoutView.js
index 54dabe247..8fb55b468 100644
--- a/dashboardv2/public/js/views/profile/ProfileTableLayoutView.js
+++ b/dashboardv2/public/js/views/profile/ProfileTableLayoutView.js
@@ -91,8 +91,10 @@ define(['require',
                             referredEntities: that.entityObject.referredEntities
                         });
 
-                        var columns = collectionJSON.relationshipAttributes.columns || collectionJSON.attributes.columns,
-                            db = collectionJSON.relationshipAttributes.db || collectionJSON.attributes.db
+                        var relAttrs = collectionJSON.relationshipAttributes || {},
+                            attrs = collectionJSON.attributes || {},
+                            columns = relAttrs.columns || attrs.columns,
+                            db = relAttrs.db || attrs.db;
 
                         that.renderTableLayoutView();
                         if (that.entityDetail) {