Skip to content

Commit 42887af

Browse files
ATLAS-5260: ATLAS UI: [UI] Detail page – terms from entity header, labels/UDP save fixes (dashboard + dashboardv2) (#585)
( cherry picked from 459281e)
1 parent ce32b8c commit 42887af

13 files changed

Lines changed: 696 additions & 121 deletions

File tree

dashboard/src/api/apiMethods/detailpageApiMethod.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {
2525
detailPageRelationshipApiUrl,
2626
detailPageRelationshipAttributesApiUrl
2727
} from "../apiUrlLinks/detailpageUrl";
28-
import { _get } from "./apiMethod";
28+
import { _get, _post } from "./apiMethod";
2929

3030
const getDetailPageData = (guid: string, params: object, header?: string) => {
3131
const config = {
@@ -60,13 +60,21 @@ const getAuditData = (params: object) => {
6060
return _get(auditApiurl(), config);
6161
};
6262

63-
const getLabels = (guid: string, formData: object) => {
63+
const getEntityHeader = (guid: string) => {
64+
const config = {
65+
method: "GET",
66+
params: {}
67+
};
68+
return _get(detailpageApiUrl(guid, "header"), config);
69+
};
70+
71+
const getLabels = (guid: string, formData: string[]) => {
6472
const config = {
6573
method: "POST",
6674
params: {},
6775
data: formData
6876
};
69-
return _get(detailPageLabelApiUrl(guid), config);
77+
return _post(detailPageLabelApiUrl(guid), config);
7078
};
7179

7280
const getEntityBusinessMetadata = (guid: string, formData: object) => {
@@ -199,6 +207,7 @@ export {
199207
getDetailPageAuditData,
200208
getDetailPageRauditData,
201209
getAuditData,
210+
getEntityHeader,
202211
getLabels,
203212
getEntityBusinessMetadata,
204213
getDetailPageRelationship,

dashboard/src/redux/slice/detailPageSlice.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,15 @@
1616
*/
1717

1818
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
19-
import { getDetailPageData } from "../../api/apiMethods/detailpageApiMethod";
19+
import {
20+
getDetailPageData,
21+
getEntityHeader
22+
} from "../../api/apiMethods/detailpageApiMethod";
2023
import { cloneDeep } from "@utils/Helper";
24+
import {
25+
mapHeaderMeaningsToRelationshipMeanings,
26+
shouldMergeMeaningsFromEntityHeader
27+
} from "@utils/entityDetailMeaningsUtils";
2128

2229
export const fetchDetailPageData = createAsyncThunk(
2330
"detailPage/fetchDetailPageData",
@@ -26,6 +33,21 @@ export const fetchDetailPageData = createAsyncThunk(
2633
const response = await getDetailPageData(guid, { minExtInfo: true });
2734
const { data = {} } = response || {};
2835
const responseData = cloneDeep(data);
36+
const entity = responseData.entity;
37+
if (entity && shouldMergeMeaningsFromEntityHeader(entity)) {
38+
try {
39+
const headerResp = await getEntityHeader(guid);
40+
const header = headerResp?.data;
41+
const headerMeanings = header?.meanings;
42+
if (Array.isArray(headerMeanings) && headerMeanings.length > 0) {
43+
entity.relationshipAttributes = entity.relationshipAttributes || {};
44+
entity.relationshipAttributes.meanings =
45+
mapHeaderMeaningsToRelationshipMeanings(headerMeanings);
46+
}
47+
} catch {
48+
// Terms are optional; detail page still loads without header.
49+
}
50+
}
2951
return responseData;
3052
} catch (error) {
3153
return rejectWithValue(error);
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
/**
19+
* When entity GET uses ignoreRelationships=true, relationshipAttributes.meanings
20+
* is omitted. GET /v2/entity/guid/{guid}/header still returns meanings
21+
* (AtlasTermAssignmentHeader[]). Map them to the shape used by entity detail
22+
* (guid, relationshipGuid for remove / ShowMoreView).
23+
*/
24+
export const mapHeaderMeaningsToRelationshipMeanings = (
25+
meanings: unknown
26+
): any[] => {
27+
if (!Array.isArray(meanings) || meanings.length === 0) {
28+
return [];
29+
}
30+
return meanings.map((m: any) => {
31+
const guid = m.guid ?? m.termGuid;
32+
const relationshipGuid = m.relationshipGuid ?? m.relationGuid;
33+
const relationshipStatus =
34+
m.relationshipStatus ??
35+
(m.status != null ? String(m.status) : "ACTIVE");
36+
return {
37+
...m,
38+
guid,
39+
relationshipGuid,
40+
relationshipStatus,
41+
termGuid: m.termGuid ?? guid
42+
};
43+
});
44+
};
45+
46+
export const shouldMergeMeaningsFromEntityHeader = (entity: any): boolean => {
47+
if (!entity || !entity.typeName) {
48+
return false;
49+
}
50+
if (String(entity.typeName).startsWith("AtlasGlossary")) {
51+
return false;
52+
}
53+
const existing = entity.relationshipAttributes?.meanings;
54+
return !Array.isArray(existing) || existing.length === 0;
55+
};

dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/Labels.tsx

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -111,19 +111,38 @@ const Labels = ({ loading, labels }: any) => {
111111
}
112112
};
113113

114+
const normalizeLabelsPayload = (raw: unknown): string[] => {
115+
if (!Array.isArray(raw)) {
116+
return [];
117+
}
118+
const out: string[] = [];
119+
for (const item of raw) {
120+
if (typeof item === "string") {
121+
const t = item.trim();
122+
if (t) {
123+
out.push(t);
124+
}
125+
continue;
126+
}
127+
if (item && typeof item === "object") {
128+
const o = item as { inputValue?: string; value?: string };
129+
const v = o.inputValue ?? o.value;
130+
if (typeof v === "string" && v.trim()) {
131+
out.push(v.trim());
132+
}
133+
}
134+
}
135+
return out;
136+
};
137+
114138
const onSubmit = async (values: any) => {
115-
let formData = { ...values };
116-
if(isEmpty(formData.labels) && isEmpty(labels)){
139+
const formData = { ...values };
140+
const payload = normalizeLabelsPayload(formData.labels);
141+
if (payload.length === 0 && (!labels || labels.length === 0)) {
117142
return;
118143
}
119-
let data = formData.labels?.map((obj: { inputValue: any }) => {
120-
if (obj.inputValue) {
121-
return obj.inputValue;
122-
}
123-
return obj;
124-
});
125144
try {
126-
await getLabels(guid, data);
145+
await getLabels(guid, payload);
127146
toast.dismiss(toastId.current);
128147
toastId.current = toast.success(
129148
"One or more labels were updated successfully"

dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/UserDefinedProperties.tsx

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,19 @@ const UserDefinedProperties = ({ loading, customAttributes, entity }: any) => {
101101
};
102102

103103
const structureAttributes = (list: any) => {
104-
let obj: any = {};
105-
list.map((o: any) => {
106-
obj[o.key] = o.value;
104+
const obj: Record<string, string> = {};
105+
if (!Array.isArray(list)) {
106+
return obj;
107+
}
108+
list.forEach((o: { key: string; value: string }) => {
109+
const key =
110+
typeof o?.key === "string"
111+
? o.key.trim()
112+
: String(o?.key ?? "").trim();
113+
if (key === "") {
114+
return;
115+
}
116+
obj[key] = o?.value ?? "";
107117
});
108118
return obj;
109119
};
@@ -218,11 +228,15 @@ const UserDefinedProperties = ({ loading, customAttributes, entity }: any) => {
218228
variant="outlined"
219229
color="success"
220230
size="small"
221-
onClick={(e: { stopPropagation: () => void }) => {
222-
e.stopPropagation();
223-
setAddLabel(true);
224-
reset({ customAttributes: [defaultField] });
225-
}}
231+
onClick={(e: { stopPropagation: () => void }) => {
232+
e.stopPropagation();
233+
setAddLabel(true);
234+
reset({
235+
customAttributes: !isEmpty(defaultFieldValues)
236+
? defaultFieldValues
237+
: [defaultField]
238+
});
239+
}}
226240
>
227241
Cancel
228242
</CustomButton>

dashboard/src/views/DetailPage/EntityDetailTabs/RelationshipCard.tsx

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ type RelationshipCardProps = {
3636
onToggleSort?: (attributeName: string) => void
3737
onToggleShowDeleted?: (attributeName: string) => void
3838
onPageLimitChange?: (attributeName: string, value: string) => void
39-
onPageLimitSubmit?: (attributeName: string) => void
39+
onPageLimitSubmit?: (attributeName: string, rawValue: string) => void
4040
}
4141

4242
function RelationshipCard({
@@ -63,6 +63,8 @@ function RelationshipCard({
6363
return data.length
6464
}, [totalCount, data.length])
6565

66+
const minPageLimit = resolvedTotal <= 1 ? 1 : 2
67+
6668
const canLoadMore = resolvedTotal > data.length
6769
const lastRequestSizeRef = useRef<number | null>(null)
6870
const prevScrollRef = useRef<{ height: number; top: number } | null>(null)
@@ -148,7 +150,7 @@ function RelationshipCard({
148150
event: React.KeyboardEvent<HTMLInputElement>
149151
) => {
150152
if (event.key === 'Enter') {
151-
onPageLimitSubmit?.(attributeName)
153+
onPageLimitSubmit?.(attributeName, event.currentTarget.value)
152154
}
153155
}
154156

@@ -240,17 +242,32 @@ function RelationshipCard({
240242
const isEmptyCard = resolvedTotal === 0 && isEmpty(data)
241243
const isZeroOrOneRecord = data.length <= 1
242244
const bodyStyle = useMemo(() => {
243-
if (isZeroOrOneRecord) {
244-
return { minHeight: 72 }
245-
}
246245
const maxVisibleRows = 9
247246
const rowHeight = 22
248247
const bodyPadding = 16
248+
const maxHeight = maxVisibleRows * rowHeight + bodyPadding
249+
250+
if (isZeroOrOneRecord && !canLoadMore) {
251+
return { minHeight: 72 }
252+
}
253+
254+
if (canLoadMore) {
255+
const visibleRows = Math.min(Math.max(data.length, 1), maxVisibleRows)
256+
let baseHeight = visibleRows * rowHeight + bodyPadding
257+
baseHeight = Math.max(baseHeight, 80)
258+
const h = Math.min(baseHeight, maxHeight)
259+
return {
260+
height: h,
261+
maxHeight: h,
262+
minHeight: 0,
263+
overflowY: 'auto' as const,
264+
}
265+
}
266+
249267
const visibleRows = Math.min(data.length, maxVisibleRows)
250268
const baseHeight = visibleRows * rowHeight + bodyPadding
251-
const maxHeight = maxVisibleRows * rowHeight + bodyPadding
252269
return { height: Math.min(baseHeight, maxHeight), minHeight: 0 }
253-
}, [data.length, isZeroOrOneRecord])
270+
}, [data.length, isZeroOrOneRecord, canLoadMore])
254271

255272
const filteredData = useMemo(() => {
256273
const query = searchQuery.trim().toLowerCase()
@@ -398,12 +415,11 @@ function RelationshipCard({
398415
<input
399416
id={`page-limit-${attributeName}`}
400417
type='number'
401-
min={1}
402-
max={resolvedTotal}
418+
min={minPageLimit}
403419
value={pageLimit && pageLimit > 0 ? pageLimit : ''}
404420
onChange={handlePageLimitInputChange}
405421
onKeyDown={handlePageLimitKeyDown}
406-
aria-label='Page limit'
422+
aria-label={`Page limit (minimum ${minPageLimit} for this card)`}
407423
/>
408424
</div>
409425
</div>

0 commit comments

Comments
 (0)