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) {