This is an automated email from the ASF dual-hosted git repository. ppawar pushed a commit to branch ATLAS-5243 in repository https://gitbox.apache.org/repos/asf/atlas.git
commit e88c8635a175a2239253c6b471e9002eff4757a8 Author: Prasad Pawar <[email protected]> AuthorDate: Tue Mar 17 18:44:50 2026 +0530 ATLAS-5243: Atlas UI: Fix relationship tab UX: navigation, graph popup, and skeleton handling in classic and React UIs --- 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;
