This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch atlas-2.6
in repository https://gitbox.apache.org/repos/asf/atlas.git
The following commit(s) were added to refs/heads/atlas-2.6 by this push:
new 9729bb9cc ATLAS-5347: Atlas React UI: Skeleton loader stuck
indefinitely in Administration Audit tab when expanding Purge/Auto-Purge entity
details (#707)
9729bb9cc is described below
commit 9729bb9cc24fa1f97d9fa54e87819aa61a6a1258
Author: Nixon Rodrigues <[email protected]>
AuthorDate: Wed Jul 29 11:59:51 2026 +0530
ATLAS-5347: Atlas React UI: Skeleton loader stuck indefinitely in
Administration Audit tab when expanding Purge/Auto-Purge entity details (#707)
* ATLAS-5347: Atlas React UI: Skeleton loader stuck indefinitely in
Administration Audit tab when expanding Purge/Auto-Purge entity details
Co-authored-by: Brijesh Bhalala <[email protected]>
---
.../views/Administrator/Audits/AuditResults.tsx | 2 +-
.../Audits/__tests__/AuditResults.test.tsx | 5 +-
.../EntityDetailTabs/AttributeProperties.tsx | 2 +-
.../__tests__/AttributeProperties.test.tsx | 38 +++++++++++-
.../store/graph/v2/AtlasRelationshipStoreV2.java | 13 ++++
.../org/apache/atlas/web/rest/DiscoveryREST.java | 9 ++-
.../web/rest/SearchDownloadFileValidator.java | 71 ++++++++++++++++++++++
7 files changed, 129 insertions(+), 11 deletions(-)
diff --git a/dashboard/src/views/Administrator/Audits/AuditResults.tsx
b/dashboard/src/views/Administrator/Audits/AuditResults.tsx
index 984221b15..acde90ff3 100644
--- a/dashboard/src/views/Administrator/Audits/AuditResults.tsx
+++ b/dashboard/src/views/Administrator/Audits/AuditResults.tsx
@@ -232,7 +232,7 @@ const AuditResults = ({ componentProps, row }: any) => {
button2Handler={undefined}
maxWidth="lg"
>
- <AuditsTab auditResultGuid={currentPurgeResultObj} />
+ <AuditsTab auditResultGuid={currentPurgeResultObj} loading={false} />
</CustomModal>
)}
</>
diff --git
a/dashboard/src/views/Administrator/Audits/__tests__/AuditResults.test.tsx
b/dashboard/src/views/Administrator/Audits/__tests__/AuditResults.test.tsx
index 0fc580d4d..1896838f7 100644
--- a/dashboard/src/views/Administrator/Audits/__tests__/AuditResults.test.tsx
+++ b/dashboard/src/views/Administrator/Audits/__tests__/AuditResults.test.tsx
@@ -112,8 +112,8 @@ jest.mock('@utils/Muiutils', () => ({
jest.mock('@views/DetailPage/EntityDetailTabs/AuditsTab', () => ({
__esModule: true,
- default: ({ auditResultGuid }: any) => (
- <div data-testid="audits-tab">AuditsTab - {auditResultGuid}</div>
+ default: ({ auditResultGuid, loading }: any) => (
+ <div data-testid="audits-tab" data-loading={loading}>AuditsTab -
{auditResultGuid}</div>
)
}));
@@ -425,6 +425,7 @@ describe('AuditResults Component', () => {
expect(screen.getByTestId('modal-title')).toHaveTextContent('Purged
Entity Details: guid-1');
expect(screen.getByTestId('audits-tab')).toBeInTheDocument();
+ expect(screen.getByTestId('audits-tab')).toHaveAttribute('data-loading',
'false');
});
it('should open auto purge modal with correct title', async () => {
diff --git
a/dashboard/src/views/DetailPage/EntityDetailTabs/AttributeProperties.tsx
b/dashboard/src/views/DetailPage/EntityDetailTabs/AttributeProperties.tsx
index 0aaeb774e..bdf10246d 100644
--- a/dashboard/src/views/DetailPage/EntityDetailTabs/AttributeProperties.tsx
+++ b/dashboard/src/views/DetailPage/EntityDetailTabs/AttributeProperties.tsx
@@ -222,7 +222,7 @@ const AttributeProperties = ({
</Stack>
</AccordionSummary>
<AccordionDetails>
- {loading == undefined || loading || isEmpty(entityData) ? (
+ {loading === true || (!auditDetails && isEmpty(entityData)) ? (
<>
<SkeletonLoader count={3} animation="wave" />
</>
diff --git
a/dashboard/src/views/DetailPage/EntityDetailTabs/__tests__/AttributeProperties.test.tsx
b/dashboard/src/views/DetailPage/EntityDetailTabs/__tests__/AttributeProperties.test.tsx
index 2eb488a38..158b9b66f 100644
---
a/dashboard/src/views/DetailPage/EntityDetailTabs/__tests__/AttributeProperties.test.tsx
+++
b/dashboard/src/views/DetailPage/EntityDetailTabs/__tests__/AttributeProperties.test.tsx
@@ -325,7 +325,7 @@ describe('AttributeProperties', () => {
expect(screen.getByTestId('skeleton-loader')).toBeInTheDocument();
});
- it('should render loading skeleton when loading is undefined',
() => {
+ it('should render properties when loading is undefined and
entityData is available', () => {
render(
<TestWrapper>
<AttributeProperties
@@ -337,7 +337,7 @@ describe('AttributeProperties', () => {
</TestWrapper>
);
-
expect(screen.getByTestId('skeleton-loader')).toBeInTheDocument();
+
expect(screen.queryByTestId('skeleton-loader')).not.toBeInTheDocument();
});
it('should render loading skeleton when entityData is empty',
() => {
@@ -364,6 +364,40 @@ describe('AttributeProperties', () => {
expect(screen.getByTestId('skeleton-loader')).toBeInTheDocument();
});
+ it('should not render skeleton in auditDetails mode when
loading is undefined', () => {
+ mockUseSelector.mockImplementation((selector: any) =>
+ selector({ entity: { entityData: {} } })
+ );
+ render(
+ <TestWrapper>
+ <AttributeProperties
+ entity={defaultMockEntity}
+
referredEntities={defaultMockReferredEntities}
+ loading={undefined}
+ auditDetails={true}
+ propertiesName="Technical"
+ />
+ </TestWrapper>
+ );
+
expect(screen.queryByTestId('skeleton-loader')).not.toBeInTheDocument();
+ expect(screen.getByText('Technical
Properties')).toBeInTheDocument();
+ });
+
+ it('should render skeleton in auditDetails mode when loading is
true', () => {
+ render(
+ <TestWrapper>
+ <AttributeProperties
+ entity={defaultMockEntity}
+
referredEntities={defaultMockReferredEntities}
+ loading={true}
+ auditDetails={true}
+ propertiesName="Technical"
+ />
+ </TestWrapper>
+ );
+
expect(screen.getByTestId('skeleton-loader')).toBeInTheDocument();
+ });
+
it('should render "No Record Found" when properties are empty',
() => {
const emptyEntity = {
typeName: 'DataSet',
diff --git
a/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/AtlasRelationshipStoreV2.java
b/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/AtlasRelationshipStoreV2.java
index 459fc1da8..de8b3b2a6 100644
---
a/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/AtlasRelationshipStoreV2.java
+++
b/repository/src/main/java/org/apache/atlas/repository/store/graph/v2/AtlasRelationshipStoreV2.java
@@ -22,6 +22,7 @@ import org.apache.atlas.AtlasErrorCode;
import org.apache.atlas.RequestContext;
import org.apache.atlas.annotation.GraphTransaction;
import org.apache.atlas.authorize.AtlasAuthorizationUtils;
+import org.apache.atlas.authorize.AtlasEntityAccessRequest;
import org.apache.atlas.authorize.AtlasPrivilege;
import org.apache.atlas.authorize.AtlasRelationshipAccessRequest;
import org.apache.atlas.exception.AtlasBaseException;
@@ -214,6 +215,7 @@ public class AtlasRelationshipStoreV2 implements
AtlasRelationshipStore {
LOG.debug("==> getById({})", guid);
AtlasEdge edge = graphHelper.getEdgeForGUID(guid);
+ verifyRelationshipReadAccess(edge);
AtlasRelationship ret =
entityRetriever.mapEdgeToAtlasRelationship(edge);
LOG.debug("<== getById({}): {}", guid, ret);
@@ -227,6 +229,7 @@ public class AtlasRelationshipStoreV2 implements
AtlasRelationshipStore {
LOG.debug("==> getExtInfoById({})", guid);
AtlasEdge edge = graphHelper.getEdgeForGUID(guid);
+ verifyRelationshipReadAccess(edge);
AtlasRelationshipWithExtInfo ret =
entityRetriever.mapEdgeToAtlasRelationshipWithExtInfo(edge);
LOG.debug("<== getExtInfoById({}): {}", guid, ret);
@@ -771,4 +774,14 @@ public class AtlasRelationshipStoreV2 implements
AtlasRelationshipStore {
private void createAndQueueTask(String taskType, AtlasEdge
relationshipEdge, AtlasRelationship relationship) {
deleteDelegate.getHandler().createAndQueueTask(taskType,
relationshipEdge, relationship);
}
+
+ private void verifyRelationshipReadAccess(AtlasEdge edge) throws
AtlasBaseException {
+ AtlasEntityHeader end1Entity =
entityRetriever.toAtlasEntityHeaderWithClassifications(edge.getOutVertex());
+ AtlasEntityHeader end2Entity =
entityRetriever.toAtlasEntityHeaderWithClassifications(edge.getInVertex());
+
+ AtlasAuthorizationUtils.verifyAccess(new
AtlasEntityAccessRequest(typeRegistry, AtlasPrivilege.ENTITY_READ, end1Entity),
+ "read relationship: end1 guid=", end1Entity.getGuid());
+ AtlasAuthorizationUtils.verifyAccess(new
AtlasEntityAccessRequest(typeRegistry, AtlasPrivilege.ENTITY_READ, end2Entity),
+ "read relationship: end2 guid=", end2Entity.getGuid());
+ }
}
diff --git a/webapp/src/main/java/org/apache/atlas/web/rest/DiscoveryREST.java
b/webapp/src/main/java/org/apache/atlas/web/rest/DiscoveryREST.java
index e318a53b5..95253671e 100644
--- a/webapp/src/main/java/org/apache/atlas/web/rest/DiscoveryREST.java
+++ b/webapp/src/main/java/org/apache/atlas/web/rest/DiscoveryREST.java
@@ -412,17 +412,16 @@ public class DiscoveryREST {
@Timed
@Path("download/{filename}")
@Produces(MediaType.APPLICATION_OCTET_STREAM)
- public Response downloadSearchResultFile(@PathParam("filename") String
fileName) {
+ public Response downloadSearchResultFile(@PathParam("filename") String
fileName) throws AtlasBaseException {
File dir = new File(SearchResultDownloadTask.DOWNLOAD_DIR_PATH,
RequestContext.getCurrentUser());
- File csvFile = new File(dir, fileName);
+ File csvFile =
SearchDownloadFileValidator.resolveDownloadFile(fileName, dir);
- if (!csvFile.exists()) {
+ if (!csvFile.exists() || !csvFile.isFile()) {
return Response.noContent().build();
}
Response.ResponseBuilder response = Response.ok(csvFile);
-
- response.header("Content-Disposition", "attachment; filename=\"" +
fileName + "\"");
+ response.header("Content-Disposition", "attachment; filename=\"" +
csvFile.getName() + "\"");
return response.build();
}
diff --git
a/webapp/src/main/java/org/apache/atlas/web/rest/SearchDownloadFileValidator.java
b/webapp/src/main/java/org/apache/atlas/web/rest/SearchDownloadFileValidator.java
new file mode 100644
index 000000000..2f3f53ebc
--- /dev/null
+++
b/webapp/src/main/java/org/apache/atlas/web/rest/SearchDownloadFileValidator.java
@@ -0,0 +1,71 @@
+/**
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.
+ */
+package org.apache.atlas.web.rest;
+
+import org.apache.atlas.AtlasErrorCode;
+import org.apache.atlas.exception.AtlasBaseException;
+import org.apache.commons.lang3.StringUtils;
+
+import java.io.File;
+import java.io.IOException;
+
+import static
org.apache.atlas.repository.store.graph.v2.tasks.searchdownload.SearchResultDownloadTask.CSV_FILE_EXTENSION;
+
+final class SearchDownloadFileValidator {
+ static final String INVALID_DOWNLOAD_FILE_NAME_MSG = "Invalid download
file name";
+
+ private SearchDownloadFileValidator() {
+ }
+
+ static File resolveDownloadFile(String fileName, File userDownloadDir)
throws AtlasBaseException {
+ validateFileName(fileName);
+
+ File csvFile = new File(userDownloadDir, fileName);
+
+ try {
+ String userDirPath = userDownloadDir.getCanonicalPath();
+ String filePath = csvFile.getCanonicalPath();
+
+ if (!filePath.startsWith(userDirPath + File.separator)) {
+ throw new AtlasBaseException(AtlasErrorCode.BAD_REQUEST,
INVALID_DOWNLOAD_FILE_NAME_MSG);
+ }
+ } catch (IOException e) {
+ throw new AtlasBaseException(AtlasErrorCode.BAD_REQUEST,
INVALID_DOWNLOAD_FILE_NAME_MSG);
+ }
+
+ return csvFile;
+ }
+
+ private static void validateFileName(String fileName) throws
AtlasBaseException {
+ if (StringUtils.isBlank(fileName)) {
+ throw new AtlasBaseException(AtlasErrorCode.BAD_REQUEST,
INVALID_DOWNLOAD_FILE_NAME_MSG);
+ }
+
+ if (fileName.indexOf('/') >= 0 || fileName.indexOf('\\') >= 0 ||
fileName.contains("..")) {
+ throw new AtlasBaseException(AtlasErrorCode.BAD_REQUEST,
INVALID_DOWNLOAD_FILE_NAME_MSG);
+ }
+
+ if (!fileName.endsWith(CSV_FILE_EXTENSION)) {
+ throw new AtlasBaseException(AtlasErrorCode.BAD_REQUEST,
INVALID_DOWNLOAD_FILE_NAME_MSG);
+ }
+
+ if (fileName.indexOf("_BASIC_") <= 0 && fileName.indexOf("_DSL_") <=
0) {
+ throw new AtlasBaseException(AtlasErrorCode.BAD_REQUEST,
INVALID_DOWNLOAD_FILE_NAME_MSG);
+ }
+ }
+}