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 bfde94988 ATLAS-5243: Atlas UI: Fix relationship tab UX: navigation,
graph popup, and skeleton handling in classic and React UIs (#572) ( cherry
picked from 1c548f9a697e35b07d6f6b1a0b28f75c28f33ca8)
bfde94988 is described below
commit bfde94988f24a13688cda923f8465ff8fe6b733c
Author: Prasad Pawar <[email protected]>
AuthorDate: Tue Mar 17 20:19:35 2026 +0530
ATLAS-5243: Atlas UI: Fix relationship tab UX: navigation, graph popup, and
skeleton handling in classic and React UIs (#572)
( cherry picked from 1c548f9a697e35b07d6f6b1a0b28f75c28f33ca8)
---
dashboard/src/styles/detailPage.scss | 9 ++
.../EntityDetailTabs/RelationshipCard.tsx | 14 ++-
.../EntityDetailTabs/RelationshipLineage.tsx | 107 +++++++++------------
.../EntityDetailTabs/RelationshipsTab.tsx | 13 ++-
dashboardv2/public/css/scss/relationship.scss | 8 ++
dashboardv2/public/js/router/Router.js | 18 ++--
.../js/views/detail_page/DetailPageLayoutView.js | 5 +-
.../detail_page/RelationshipCardsLayoutView.js | 15 ++-
.../js/views/graph/RelationshipLayoutView.js | 17 ++--
9 files changed, 117 insertions(+), 89 deletions(-)
diff --git a/dashboard/src/styles/detailPage.scss
b/dashboard/src/styles/detailPage.scss
index 0322027e3..360927694 100644
--- a/dashboard/src/styles/detailPage.scss
+++ b/dashboard/src/styles/detailPage.scss
@@ -345,6 +345,15 @@ pre.code-block .json-string {
text-decoration: underline;
}
+.relationship-card__link--deleted,
+.relationship-card__text--deleted {
+ color: #bb5838;
+}
+
+.relationship-card__link--deleted:hover {
+ color: #bb5838;
+}
+
.relationship-card__text {
color: #334155;
}
diff --git
a/dashboard/src/views/DetailPage/EntityDetailTabs/RelationshipCard.tsx
b/dashboard/src/views/DetailPage/EntityDetailTabs/RelationshipCard.tsx
index dc7ea748e..c9bcebb0e 100644
--- a/dashboard/src/views/DetailPage/EntityDetailTabs/RelationshipCard.tsx
+++ b/dashboard/src/views/DetailPage/EntityDetailTabs/RelationshipCard.tsx
@@ -336,6 +336,16 @@ function RelationshipCard({
const itemGuid
= item?.guid
const href =
itemGuid ? `/detailPage/${itemGuid}` : ''
const
displayText = getDisplayText(item)
+ const ref =
itemGuid ? referredEntities?.[itemGuid] : null
+ const status =
+
ref?.status || item?.status || item?.attributes?.status || ''
+ const isDeleted
= status === 'DELETED'
+ const linkClass
= isDeleted
+ ?
'relationship-card__link relationship-card__link--deleted'
+ :
'relationship-card__link'
+ const textClass
= isDeleted
+ ?
'relationship-card__text relationship-card__text--deleted'
+ :
'relationship-card__text'
return (
<li
key={`${attributeName}-${index}`}
@@ -350,13 +360,13 @@ function RelationshipCard({
onKeyDown={(event) =>
handleKeyDown(event, href)
}
-
className='relationship-card__link'
+
className={linkClass}
>
{displayText}
</Link>
</LightTooltip>
) : (
-
<span className='relationship-card__text'>
+
<span className={textClass}>
{displayText}
</span>
)}
diff --git
a/dashboard/src/views/DetailPage/EntityDetailTabs/RelationshipLineage.tsx
b/dashboard/src/views/DetailPage/EntityDetailTabs/RelationshipLineage.tsx
index 61a86a8f3..c5fb1f653 100644
--- a/dashboard/src/views/DetailPage/EntityDetailTabs/RelationshipLineage.tsx
+++ b/dashboard/src/views/DetailPage/EntityDetailTabs/RelationshipLineage.tsx
@@ -47,7 +47,6 @@ import ArrowRightAltIcon from
"@mui/icons-material/ArrowRightAlt";
import { CloseIcon, LightTooltip } from "@components/muiComponents";
import { useAppSelector } from "@hooks/reducerHook";
import { Link as MUILink } from "@mui/material";
-import DeleteOutlineOutlinedIcon from
"@mui/icons-material/DeleteOutlineOutlined";
const CustomLink = ({
href,
@@ -59,7 +58,7 @@ const CustomLink = ({
params
}: any): any => {
return (
- <li className={status} style={{ listStyle: "numeric" }}>
+ <li className={status}>
<MUILink
component={RouterLink}
to={{
@@ -133,9 +132,10 @@ const RelationshipLineage = ({
const createGraph = (data) => {
// Use getBoundingClientRect to get the actual width and height
const svgElement = svgRef?.current;
- const { width, height } = svgElement
- ? svgElement.getBoundingClientRect()
- : { width: 0, height: 0 };
+ const rect = svgElement ? svgElement.getBoundingClientRect() : null;
+ const width = rect ? rect.width : 0;
+ const height = rect ? rect.height : 0;
+ const padding = 60;
let nodes = d3.values(data.nodes);
let links = data.links;
@@ -147,8 +147,8 @@ const RelationshipLineage = ({
var svg = d3
.select(svgElement)
- .attr("viewBox", `0 0 ${width} ${height}`)
- .attr("enable-background", `new 0 0 ${width} ${height}`),
+ .attr("viewBox", `${-padding} ${-padding} ${width + padding * 2}
${height + padding * 2}`)
+ .attr("enable-background", `new ${-padding} ${-padding} ${width +
padding * 2} ${height + padding * 2}`),
node,
path;
@@ -295,24 +295,30 @@ const RelationshipLineage = ({
return "#fff";
});
- var countBox = circleContainer.append("g");
+ var countBox = circleContainer.append("g").attr("class",
"relationship-node-count");
countBox
.append("circle")
- .attr("cx", 18)
- .attr("cy", -20)
+ .attr("cx", 22)
+ .attr("cy", 32)
.attr("r", function (d) {
if (isArray(d.value) && d.value.length > 1) {
- return 10;
+ return 12;
}
- });
+ })
+ .attr("fill", "#333")
+ .attr("stroke", "#fff")
+ .attr("stroke-width", "1.5px");
countBox
.append("text")
- .attr("dx", 18)
- .attr("dy", -16)
+ .attr("x", 22)
+ .attr("y", 32)
.attr("text-anchor", "middle")
- .attr("fill", defaultEntityColor)
+ .attr("dominant-baseline", "middle")
+ .attr("fill", "#fff")
+ .style("font-size", "11px")
+ .style("font-weight", "600")
.text(function (d) {
if (isArray(d.value) && d.value.length > 1) {
return d.value.length;
@@ -452,11 +458,13 @@ const RelationshipLineage = ({
let searchString = options.searchString;
let listString = [];
const getEntityTypelist = (options) => {
- let activeEntityColor = "#4a90e2";
+ let activeEntityColor = "#1976d2";
let deletedEntityColor = "#BB5838";
+ const entityStatus = options.entityStatus || options.status;
+ const relationshipStatus = options.relationshipStatus || options.status;
const getdefault = (obj) => {
let options = obj.options;
- let status = entityStateReadOnly[options.entityStatus ||
options.status]
+ let status = entityStateReadOnly[entityStatus]
? " deleted-relation"
: "";
let nodeGuid = options.guid;
@@ -504,61 +512,26 @@ const RelationshipLineage = ({
};
const getWithButton = (obj) => {
let options = obj.options;
- let status = entityStateReadOnly[options.entityStatus ||
options.status]
+ let status = entityStateReadOnly[entityStatus]
? " deleted-relation"
: "";
let entityColor = obj.color;
let name = obj.name;
- let typeName = options.typeName;
- let relationship = obj.relationship || false;
- let entity = obj.entity || false;
- let icon = '<i class="fa fa-trash"></i>';
- let title = "Deleted";
- if (relationship) {
- icon = '<i class="fa fa-long-arrow-right"></i>';
- status = entityStateReadOnly[
- options.relationshipStatus || options.status
- ]
+ if (obj.relationship) {
+ status = entityStateReadOnly[relationshipStatus]
? "deleted-relation"
: "";
- title = "Relationship Deleted";
}
return (
- <li className={status} style={{ listStyle: "numeric" }}>
+ <li className={status}>
<MUILink
component={RouterLink}
- to={`#!/detailPage/${options.guid}?tabActive=relationship`}
+ to={`/detailPage/${options.guid}?tabActive=relationship`}
style={{ color: entityColor }}
>
{name} ({options.typeName})
</MUILink>
-
- <Button
- type="button"
- title={title}
- className="btn btn-sm deleteBtn deletedTableBtn btn-action"
- startIcon={
- relationship ? (
- <ArrowRightAltIcon sx={{ fontSize: "1.25rem" }} />
- ) : (
- <LightTooltip title="Deleted">
- <IconButton
- aria-label="back"
- sx={{
- display: "inline-flex",
- position: "relative",
- padding: "4px",
- marginLeft: "4px",
- color: (theme) => theme.palette.grey[500]
- }}
- >
- <DeleteOutlineOutlinedIcon sx={{ fontSize: "1.25rem" }}
/>
- </IconButton>
- </LightTooltip>
- )
- }
- ></Button>
</li>
);
};
@@ -566,22 +539,22 @@ const RelationshipLineage = ({
const name = options.entityName
? options.entityName
: extractKeyValueFromEntity(options, "displayText").name;
- if (options.entityStatus == "ACTIVE") {
- if (options.relationshipStatus == "ACTIVE") {
+ if (entityStatus === "ACTIVE") {
+ if (relationshipStatus === "ACTIVE") {
return getdefault({
color: activeEntityColor,
options: options,
name: name
});
- } else if (options.relationshipStatus == "DELETED") {
+ } else if (relationshipStatus === "DELETED") {
return getWithButton({
- color: activeEntityColor,
+ color: deletedEntityColor,
options: options,
name: name,
relationship: true
});
}
- } else if (options.entityStatus == "DELETED") {
+ } else if (entityStatus === "DELETED") {
return getWithButton({
color: deletedEntityColor,
options: options,
@@ -655,10 +628,16 @@ const RelationshipLineage = ({
{/* )} */}
<Stack
+ component="ol"
gap={"0.875rem"}
- paddingLeft="0.5rem"
maxHeight="220px"
- sx={{ overflowY: "auto" }}
+ sx={{
+ overflowY: "auto",
+ listStyleType: "decimal",
+ listStylePosition: "outside",
+ paddingLeft: "1.75em",
+ margin: 0
+ }}
>
{listString}
</Stack>
diff --git
a/dashboard/src/views/DetailPage/EntityDetailTabs/RelationshipsTab.tsx
b/dashboard/src/views/DetailPage/EntityDetailTabs/RelationshipsTab.tsx
index c6e07e8e2..9f26786f8 100644
--- a/dashboard/src/views/DetailPage/EntityDetailTabs/RelationshipsTab.tsx
+++ b/dashboard/src/views/DetailPage/EntityDetailTabs/RelationshipsTab.tsx
@@ -70,6 +70,8 @@ const RelationshipsTab: React.FC<EntityDetailTabProps> = ({
Record<string, boolean>
>({});
const [showInitialSkeletons, setShowInitialSkeletons] =
useState<boolean>(true);
+ const [hasRelationshipApiError, setHasRelationshipApiError] =
+ useState<boolean>(false);
const initialSkeletonTimerRef = useRef<ReturnType<typeof setTimeout> | null>(
null
);
@@ -79,6 +81,7 @@ const RelationshipsTab: React.FC<EntityDetailTabProps> = ({
setCardData({});
setCardTotalCounts({});
setShowInitialSkeletons(true);
+ setHasRelationshipApiError(false);
fetchStartedRef.current = false;
if (initialSkeletonTimerRef.current) {
@@ -239,6 +242,7 @@ const RelationshipsTab: React.FC<EntityDetailTabProps> = ({
})
.catch((err) => {
console.error(`Error fetching relationship ${relationName}:`, err);
+ setHasRelationshipApiError(true);
setCardData((prev) => ({ ...prev, [relationName]: [] }));
setSortByNameByAttr((prev) => ({
...prev,
@@ -254,6 +258,11 @@ const RelationshipsTab: React.FC<EntityDetailTabProps> = ({
if (completedCount >= totalCount) {
setInitialLoadDone(true);
setCardLoading(false);
+ setShowInitialSkeletons(false);
+ if (initialSkeletonTimerRef.current) {
+ clearTimeout(initialSkeletonTimerRef.current);
+ initialSkeletonTimerRef.current = null;
+ }
}
});
});
@@ -555,7 +564,9 @@ const RelationshipsTab: React.FC<EntityDetailTabProps> = ({
Object.values(cardData).every((arr) => isEmpty(arr)) &&
!checked) ? (
<div className="relationship-cards-empty">
- No relationship data available
+ {hasRelationshipApiError
+ ? "Failed to load relationship data"
+ : "No relationship data available"}
</div>
) : (
<div className="relationship-cards-grid
relationship-cards-grid--custom">
diff --git a/dashboardv2/public/css/scss/relationship.scss
b/dashboardv2/public/css/scss/relationship.scss
index 98e22a8fc..133cabcae 100644
--- a/dashboardv2/public/css/scss/relationship.scss
+++ b/dashboardv2/public/css/scss/relationship.scss
@@ -295,6 +295,14 @@
&:hover {
text-decoration: underline;
}
+
+ &.relationship-card-link--deleted {
+ color: $delete_link;
+
+ &:hover {
+ color: $delete_link;
+ }
+ }
}
.value-text {
diff --git a/dashboardv2/public/js/router/Router.js
b/dashboardv2/public/js/router/Router.js
index 7af2ca1c4..3f89f2d2a 100644
--- a/dashboardv2/public/js/router/Router.js
+++ b/dashboardv2/public/js/router/Router.js
@@ -181,16 +181,16 @@ define([
});
var dOptions = _.extend({ id: id, value: paramObj },
options);
- that.renderViewIfNotExists({
- view: App.rNContent,
- viewName: "DetailPageLayoutView",
- manualRender: function() {
- this.view.currentView.manualRender(dOptions);
- },
- render: function() {
- return new DetailPageLayoutView(dOptions);
+ var currentView = App.rNContent.currentView;
+ var isSameEntity = currentView && currentView._viewName
=== "DetailPageLayoutView" && currentView.id === id;
+ if (isSameEntity) {
+ currentView.manualRender(dOptions);
+ } else {
+ if (currentView && currentView.destroy) {
+ currentView.destroy();
}
- });
+ App.rNContent.show(new DetailPageLayoutView(dOptions));
+ }
});
}
},
diff --git a/dashboardv2/public/js/views/detail_page/DetailPageLayoutView.js
b/dashboardv2/public/js/views/detail_page/DetailPageLayoutView.js
index 0c4328f54..59006946f 100644
--- a/dashboardv2/public/js/views/detail_page/DetailPageLayoutView.js
+++ b/dashboardv2/public/js/views/detail_page/DetailPageLayoutView.js
@@ -294,9 +294,9 @@ define(['require',
this.editEntity = true;
}
}
- if (collectionJSON.attributes &&
collectionJSON.attributes.columns) {
+ if (collectionJSON.attributes &&
_.isArray(collectionJSON.attributes.columns)) {
var valueSorted =
_.sortBy(collectionJSON.attributes.columns, function(val) {
- return val.attributes &&
val.attributes.position
+ return val && val.attributes &&
val.attributes.position;
});
collectionJSON.attributes.columns = valueSorted;
}
@@ -449,6 +449,7 @@ define(['require',
var oldId = this.id;
_.extend(this, _.pick(options, 'value', 'id'));
if (this.id !== oldId) {
+ this.detailPageObj = null;
this.collection.url = UrlLinks.entitiesApiUrl({ guid:
this.id, minExtInfo: true });
this.fetchCollection();
}
diff --git
a/dashboardv2/public/js/views/detail_page/RelationshipCardsLayoutView.js
b/dashboardv2/public/js/views/detail_page/RelationshipCardsLayoutView.js
index b0444cc40..6c60e36bc 100644
--- a/dashboardv2/public/js/views/detail_page/RelationshipCardsLayoutView.js
+++ b/dashboardv2/public/js/views/detail_page/RelationshipCardsLayoutView.js
@@ -46,6 +46,7 @@ define([
this.showDeletedByName = {};
this.pageLimitByName = {};
this.isInitialLoading = false;
+ this.hasApiError = false;
},
onRender: function() {
this.fetchInitialCards();
@@ -179,6 +180,7 @@ define([
return;
}
+ this.hasApiError = false;
if (_.isFunction(this.onDataLoading)) {
this.onDataLoading(true);
}
@@ -233,6 +235,7 @@ define([
},
function(error) {
console.error("[RelationshipCardsLayoutView] API
error for", name, ":", error);
+ that.hasApiError = true;
processResponse(name, null);
that.renderCards();
return null;
@@ -390,13 +393,16 @@ define([
var displayLabel = showTypeNameInDisplay && typeName
? displayText + " (" + typeName + ")"
: displayText;
- var href = item && item.guid ? "#/detailPage/" + item.guid :
"";
+ var status = (ref && ref.status) || item.status ||
(item.attributes && item.attributes.status) || "";
+ var isDeleted = status === "DELETED";
+ var deletedClass = isDeleted ? "
relationship-card-link--deleted" : "";
+ var href = item && item.guid ? "#!/detailPage/" + item.guid +
"?tabActive=relationship" : "";
var qualifiedName = item && item.qualifiedName ?
item.qualifiedName : "";
var nameValue = item && item.attributes &&
item.attributes.name ? item.attributes.name : "";
var searchText = _.escape((displayText + " " + qualifiedName +
" " + nameValue).toLowerCase());
return "<li class='relationship-card-item' data-search='" +
searchText + "'>" +
- (href ? "<a class='relationship-card-link' href='" + href
+ "'>" + _.escape(displayLabel) + "</a>" :
- "<span class='relationship-card-link'>" +
_.escape(displayLabel) + "</span>") +
+ (href ? "<a class='relationship-card-link" + deletedClass
+ "' href='" + href + "'>" + _.escape(displayLabel) + "</a>" :
+ "<span class='relationship-card-link" + deletedClass +
"'>" + _.escape(displayLabel) + "</span>") +
"</li>";
}).join("");
@@ -585,7 +591,8 @@ define([
return _.has(that.cardData, n) && _.isEmpty(that.cardData[n]);
});
if (cardCount === 0 && allLoadedAndEmpty && !this.showEmptyValues)
{
- html = "<div class='relationship-cards-empty'>No relationship
data available</div>";
+ var emptyMsg = this.hasApiError ? "Failed to load relationship
data" : "No relationship data available";
+ html = "<div class='relationship-cards-empty'>" + emptyMsg +
"</div>";
}
if (this.$el && this.$el.length) {
diff --git a/dashboardv2/public/js/views/graph/RelationshipLayoutView.js
b/dashboardv2/public/js/views/graph/RelationshipLayoutView.js
index 35d04261a..e47cb9bd2 100644
--- a/dashboardv2/public/js/views/graph/RelationshipLayoutView.js
+++ b/dashboardv2/public/js/views/graph/RelationshipLayoutView.js
@@ -253,7 +253,7 @@ define([
listString = "",
data = this.selectedNodeData,
typeName = this.selectedNodeType,
- activeEntityColor = "#00b98b",
+ activeEntityColor = "#1976d2",
deletedEntityColor = "#BB5838",
defaultEntityColor = "#e0e0e0",
normalizeEntity = function(entity) {
@@ -277,11 +277,11 @@ define([
entityTypeButton = "";
if (guid) {
if (options.entity) {
- entityTypeButton = "<a href='#/detailPage/" +
guid + "' class='entity-type-name' style='color:" + options.color + "'>" + name
+ "</a>";
+ entityTypeButton = "<a href='#!/detailPage/" +
guid + "' class='entity-type-name' style='color:" + options.color + "'>" + name
+ "</a>";
} else if (options.relationship) {
entityTypeButton = "<a
href='#/relationshipDetailPage/" + guid + "' class='entity-type-name'
style='color:" + options.color + "'>" + name + "</a>";
} else {
- entityTypeButton = "<a href='#/detailPage/" +
guid + "' class='entity-type-name' style='color:" + options.color + "'>" + name
+ "</a>";
+ entityTypeButton = "<a href='#!/detailPage/" +
guid + "' class='entity-type-name' style='color:" + options.color + "'>" + name
+ "</a>";
}
} else {
entityTypeButton = "<pre class='entity-type-name'
style='color:" + options.color + "'>" + name + "</pre>";
@@ -346,12 +346,14 @@ define([
}.bind(this);
var buildListItem = function(item) {
var name = item.entityName || Utils.getName(item,
"displayText");
- var href = item.guid ? "#/detailPage/" + item.guid : "";
+ var typeName = item.typeName || "";
+ var displayLabel = typeName ? name + " (" + typeName + ")"
: name;
+ var href = item.guid ? "#!/detailPage/" + item.guid +
"?tabActive=relationship" : "";
var isDeleted = (item.entityStatus || item.status) ==
"DELETED";
var color = isDeleted ? deletedEntityColor :
activeEntityColor;
var content = href
- ? "<a href='" + href + "' class='entity-type-name'
style='color:" + color + "'>" + _.escape(name) + "</a>"
- : "<span class='entity-type-name' style='color:" +
color + "'>" + _.escape(name) + "</span>";
+ ? "<a href='" + href + "' class='entity-type-name'
style='color:" + color + "'>" + _.escape(displayLabel) + "</a>"
+ : "<span class='entity-type-name' style='color:" +
color + "'>" + _.escape(displayLabel) + "</span>";
return "<li class='entity-list-item'>" + content + "</li>";
};
if (_.isArray(data)) {
@@ -363,8 +365,9 @@ define([
}
_.each(_.sortBy(data, "entityName"), function(val) {
var name = val.entityName || Utils.getName(val,
"displayText");
+ var searchTarget = (name + " " + (val.typeName ||
"")).toLowerCase();
if (searchString) {
- if
(name.toLowerCase().includes(searchString.toLowerCase())) {
+ if
(searchTarget.includes(searchString.toLowerCase())) {
listString += buildListItem(val);
} else {
return;