Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 13 additions & 10 deletions dashboard/src/components/ImportDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ import ListItemText from "@mui/material/ListItemText";
import ArrowBackIosNewIcon from "@mui/icons-material/ArrowBackIosNew";
import { postGlossaryImportFormData } from "@utils/glossaryImportFlow";
import { getApiErrorToastMessage } from "@utils/apiErrorToastMessage";
import {
buildGlossaryImportFailureSummary,
formatGlossaryImportFailure,
GlossaryImportFailure
} from "@utils/glossaryImportUtils";

const BootstrapDialog = styled(Dialog)(({ theme }) => ({
"& .MuiDialogContent-root": {
Expand Down Expand Up @@ -113,7 +118,7 @@ export const ImportDialog: React.FC<CustomModalProps> = ({
if (importResp.data.failedImportInfoList != undefined) {
toast.dismiss(toastId.current);
toastId.current = toast.error(
importResp.data.failedImportInfoList[0].remarks
buildGlossaryImportFailureSummary(importResp.data)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical issue Blovker: — Business Metadata regression:
ImportDialog serves both glossary and business metadata (title == "Import Business Metadata"). The PR applies glossary-specific formatting to the shared failure block:

toastId.current = toast.error(
  buildGlossaryImportFailureSummary(importResp.data)  // ← "Glossary import completed..."
);

-And error details use formatGlossaryImportFailure(), which assumes glossary semantics (child@parent). For business metadata, backend stores fields differently (parentObjectName = guid, childObjectName = attributes), producing misleading labels like attributes@guid: error.


solution:

ImportDialog is shared between glossary and business metadata imports. Please guard glossary-specific formatting:

const isGlossaryImport = title !== "Import Business Metadata";
toast.error(
  isGlossaryImport
    ? buildGlossaryImportFailureSummary(importResp.data)
    : importResp.data.failedImportInfoList[0]?.remarks ?? "Import failed"
);

Same guard needed in the error details list (line 207–218).

);

setErrorDetails(true);
Expand Down Expand Up @@ -200,17 +205,15 @@ export const ImportDialog: React.FC<CustomModalProps> = ({
>
<List>
{importData.failedImportInfoList.map(
(
value: {
index: number;
remarks: string;
},
index: number
) => (
<ListItem key={value.index} disableGutters disablePadding>
(value: GlossaryImportFailure, index: number) => (
<ListItem
key={`${value.childObjectName || "term"}-${index}`}
disableGutters
disablePadding
>
<ListItemText
className="dropzone-listitem"
primary={`${index + 1}. ${value.remarks}`}
primary={`${index + 1}. ${formatGlossaryImportFailure(value)}`}
/>
</ListItem>
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocker:

Test Suites: 1 failed, 2 passed, 3 total
Tests: 1 failed, 48 passed, 49 total

Failure: ImportDialog.test.tsx — expectations not updated for new behavior:

Suggestion: (ImportDialog.test.tsx line ~103–122):
Tests must be updated to match new toast summary and formatted error lines. Also add a Business Metadata failure test to ensure glossary formatting is not applied there.

Expand Down
50 changes: 50 additions & 0 deletions dashboard/src/utils/__tests__/glossaryImportUtils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* 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.
*/

import {
buildGlossaryImportFailureSummary,
formatGlossaryImportFailure
} from "../glossaryImportUtils";

describe("glossaryImportUtils", () => {
it("formats failure with glossary term label", () => {
expect(
formatGlossaryImportFailure({
childObjectName: "Patient",
parentObjectName: "Healthcare Glossary",
remarks: "Reference not found"
})
).toBe("Patient@Healthcare Glossary: Reference not found");
});

it("builds import summary with success and failure counts", () => {
expect(
buildGlossaryImportFailureSummary({
successImportInfoList: [{ childObjectName: "A" }],
failedImportInfoList: [
{
childObjectName: "Patient",
parentObjectName: "Healthcare Glossary",
remarks: "Invalid relation"
}
]
})
).toBe(
"Glossary import completed with 1 failure(s) out of 2 term(s). See error details."
);
});
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing negative / edge cases:

Only remarks (no names)
Only childObjectName
Empty both lists → "0 failure(s) out of 0 term(s)"
Only failures, no successes
Empty remarks → fallback "Import failed"

suggestion:
line 23:
Please add negative/edge tests: remarks-only, child-only, empty lists, failures-only (no successes).

49 changes: 49 additions & 0 deletions dashboard/src/utils/glossaryImportUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* 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.
*/

export interface GlossaryImportFailure {
childObjectName?: string;
parentObjectName?: string;
remarks?: string;
rowNumber?: number;
}

export interface GlossaryImportResponse {
failedImportInfoList?: GlossaryImportFailure[];
successImportInfoList?: GlossaryImportFailure[];
}

export const formatGlossaryImportFailure = (
failure: GlossaryImportFailure
): string => {
const termLabel =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requires both childObjectName and parentObjectName for @ format

-add test for childObjectName-only case (same-glossary shorthand failures from backend)

failure.childObjectName && failure.parentObjectName
? `${failure.childObjectName}@${failure.parentObjectName}`
: failure.childObjectName || "Unknown term";

return `${termLabel}: ${failure.remarks || "Import failed"}`;
};

export const buildGlossaryImportFailureSummary = (
response: GlossaryImportResponse
): string => {
const failedCount = response.failedImportInfoList?.length || 0;
const successCount = response.successImportInfoList?.length || 0;
const totalCount = failedCount + successCount;

return `Glossary import completed with ${failedCount} failure(s) out of ${totalCount} term(s). See error details.`;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary string is hardcoded glossary-specific ("Glossary import completed...")

  • Consider renaming to make scope explicit, e.g. buildGlossaryImportFailureSummary, and do not reuse for Business Metadata

};
23 changes: 13 additions & 10 deletions dashboard/src/views/Glossary/AddUpdateGlossaryForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ import {
postGlossaryImportFormData
} from "@utils/glossaryImportFlow";
import { getApiErrorToastMessage } from "@utils/apiErrorToastMessage";
import {
buildGlossaryImportFailureSummary,
formatGlossaryImportFailure,
GlossaryImportFailure
} from "@utils/glossaryImportUtils";
import { toast } from "react-toastify";
import { useCallback, useEffect, useRef, useState } from "react";
import type { MouseEvent } from "react";
Expand Down Expand Up @@ -163,7 +168,7 @@ const AddUpdateGlossaryForm = (props: {
if (importResp.data.failedImportInfoList != undefined) {
toast.dismiss(toastId.current);
toastId.current = toast.error(
importResp.data.failedImportInfoList[0].remarks
buildGlossaryImportFailureSummary(importResp.data)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add tests for glossary import failure: summary toast, formatted error list, and back-to-upload navigation — similar to ImportDialog.test.tsx.

);
setImportErrorDetails(true);
}
Expand Down Expand Up @@ -361,17 +366,15 @@ const AddUpdateGlossaryForm = (props: {
>
<List>
{importData.failedImportInfoList.map(
(
value: {
index: number;
remarks: string;
},
index: number
) => (
<ListItem key={value.index} disableGutters disablePadding>
(value: GlossaryImportFailure, index: number) => (
<ListItem
key={`${value.childObjectName || "term"}-${index}`}
disableGutters
disablePadding
>
<ListItemText
className="dropzone-listitem"
primary={`${index + 1}. ${value.remarks}`}
primary={`${index + 1}. ${formatGlossaryImportFailure(value)}`}
/>
</ListItem>
)
Expand Down
27 changes: 17 additions & 10 deletions dashboardv2/public/js/views/import/ImportLayoutView.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,18 +117,25 @@ define([
var success = true;
if (response.failedImportInfoList && response.failedImportInfoList.length) {
var errorStr = '',
notificationMsg = '';
failedCount = response.failedImportInfoList.length,
successCount = (response.successImportInfoList && response.successImportInfoList.length) || 0,
totalCount = failedCount + successCount,
notificationMsg = 'Glossary import completed with ' + failedCount + ' failure(s) out of ' + totalCount + ' term(s). See error details.';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hardcoded "Glossary import completed..."

Use that.isGlossary for notification text; BM import should not say "Glossary import completed...".

Suggestion:
ImportLayoutView is used for BM too (isGlossary flag exists). Use that.isGlossary to pick message

success = false;
that.ui.errorDetails.empty();
Utils.defaultErrorHandler(null, file.xhr, { defaultErrorMessage: response.failedImportInfoList[0].remarks });
if (response.failedImportInfoList.length > 1) {
var modalTitle = '<div class="back-button importBackBtn" title="Back to import file"><i class="fa fa-angle-left "></i> </div> <div class="modal-name">Error Details</div>';
_.each(response.failedImportInfoList, function(err_obj) {
errorStr += '<li>' + _.escape(err_obj.remarks) + '</li>';
});
that.ui.errorDetails.append(errorStr);
that.toggleErrorAndDropZoneView({ title: modalTitle, isErrorView: true });
}
Utils.notifyError({
content: notificationMsg
});
var modalTitle = '<div class="back-button importBackBtn" title="Back to import file"><i class="fa fa-angle-left "></i> </div> <div class="modal-name">Error Details</div>';
_.each(response.failedImportInfoList, function(err_obj, index) {
var termLabel = err_obj.childObjectName || 'Unknown term';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line 131–134
issue:
termLabel bug: if parentObjectName exists but childObjectName is missing, label becomes "undefined@Glossary"

Suggestion:
Match React logic: only use @ when both exist

var termLabel = 'Unknown term';
if (err_obj.childObjectName && err_obj.parentObjectName) {
    termLabel = err_obj.childObjectName + '@' + err_obj.parentObjectName;
} else if (err_obj.childObjectName) {
    termLabel = err_obj.childObjectName;
}

if (err_obj.parentObjectName) {
termLabel = err_obj.childObjectName + '@' + err_obj.parentObjectName;
}
errorStr += '<li>' + (index + 1) + '. ' + _.escape(termLabel) + ': ' + _.escape(err_obj.remarks || '') + '</li>';
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line 131-136

Duplicated formatting logic vs React util
Acceptable for legacy JS, but consider extracting shared helper if both dashboards must stay in sync

that.ui.errorDetails.append(errorStr);
that.toggleErrorAndDropZoneView({ title: modalTitle, isErrorView: true });
}
if (success) {
that.modal.trigger("cancel");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.apache.atlas.model.glossary.AtlasGlossary;
import org.apache.atlas.model.glossary.AtlasGlossaryCategory;
import org.apache.atlas.model.glossary.AtlasGlossaryTerm;
import org.apache.atlas.model.glossary.AtlasGlossaryTermHeader;
import org.apache.atlas.model.glossary.relations.AtlasRelatedCategoryHeader;
import org.apache.atlas.model.glossary.relations.AtlasRelatedTermHeader;
import org.apache.atlas.model.glossary.relations.AtlasTermCategorizationHeader;
Expand All @@ -53,9 +54,12 @@

import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
Expand Down Expand Up @@ -945,6 +949,8 @@ public BulkImportResponse importGlossaryData(InputStream inputStream, String fil
List<AtlasGlossaryTerm> glossaryTermsWithRelations = glossaryTermUtils.getGlossaryTermDataWithRelations(fileData, ret);

updateGlossaryTermsRelation(glossaryTermsWithRelations, ret);

reconcileBulkImportResponse(ret);
} finally {
glossaryTermUtils.clearImportCache();
}
Expand Down Expand Up @@ -1181,8 +1187,21 @@ private void createGlossaryTerms(List<AtlasGlossaryTerm> glossaryTerms, BulkImpo
String glossaryName = getGlossaryName(glossaryTerm);

try {
if (termExists2(glossaryTerm)) {
String existingTermGuid = getExistingTermGuid(glossaryTerm);

glossaryTermUtils.cacheImportedTermGuid(glossaryTerm.getQualifiedName(), existingTermGuid);

bulkImportResponse.addToSuccessImportInfoList(new ImportInfo(glossaryName, glossaryTermName, SUCCESS,
AtlasJson.toJson(getGlossaryTermHeader(existingTermGuid, glossaryTerm.getQualifiedName()))));

continue;
}

AtlasGlossaryTerm createdTerm = createTerm(glossaryTerm);

glossaryTermUtils.cacheImportedTermGuid(createdTerm.getQualifiedName(), createdTerm.getGuid());

bulkImportResponse.addToSuccessImportInfoList(new ImportInfo(glossaryName, glossaryTermName, SUCCESS, AtlasJson.toJson(createdTerm.getGlossaryTermHeader())));
} catch (AtlasBaseException e) {
LOG.error(AtlasErrorCode.FAILED_TO_CREATE_GLOSSARY_TERM.toString(), glossaryTermName, e);
Expand All @@ -1194,6 +1213,20 @@ private void createGlossaryTerms(List<AtlasGlossaryTerm> glossaryTerms, BulkImpo
checkForSuccessImports(bulkImportResponse);
}

private String getExistingTermGuid(AtlasGlossaryTerm glossaryTerm) {
Map<String, Object> uniqAttr = new HashMap<>();

uniqAttr.put(QUALIFIED_NAME_ATTR, glossaryTerm.getQualifiedName());

AtlasVertex vertex = AtlasGraphUtilsV2.findByUniqueAttributes(atlasTypeRegistry.getEntityTypeByName(GlossaryUtils.ATLAS_GLOSSARY_TERM_TYPENAME), uniqAttr);

return vertex != null ? AtlasGraphUtilsV2.getIdFromVertex(vertex) : null;
}

private AtlasGlossaryTermHeader getGlossaryTermHeader(String termGuid, String qualifiedName) {
return new AtlasGlossaryTermHeader(termGuid, qualifiedName);
}

private void updateGlossaryTermsRelation(List<AtlasGlossaryTerm> glossaryTerms, BulkImportResponse bulkImportResponse) {
for (AtlasGlossaryTerm glossaryTerm : glossaryTerms) {
glossaryTermUtils.updateGlossaryTermRelations(glossaryTerm);
Expand Down Expand Up @@ -1232,6 +1265,61 @@ private void checkForSuccessImports(BulkImportResponse bulkImportResponse) throw
}
}

private void reconcileBulkImportResponse(BulkImportResponse bulkImportResponse) {
if (CollectionUtils.isEmpty(bulkImportResponse.getFailedImportInfoList())) {
return;
}

Map<String, ImportInfo> mergedFailures = new LinkedHashMap<>();

for (ImportInfo failedInfo : bulkImportResponse.getFailedImportInfoList()) {
String termKey = getImportTermKey(failedInfo);

if (StringUtils.isBlank(termKey)) {
mergedFailures.put("row-" + failedInfo.getRemarks(), failedInfo);
continue;
}

ImportInfo existingFailure = mergedFailures.get(termKey);

if (existingFailure == null) {
mergedFailures.put(termKey, failedInfo);
} else {
existingFailure.setRemarks(mergeImportRemarks(existingFailure.getRemarks(), failedInfo.getRemarks()));
}
}

bulkImportResponse.setFailedImportInfoList(new ArrayList<>(mergedFailures.values()));

Set<String> failedTermKeys = mergedFailures.keySet();

bulkImportResponse.getSuccessImportInfoList().removeIf(successInfo -> failedTermKeys.contains(getImportTermKey(successInfo)));
}

private String getImportTermKey(ImportInfo importInfo) {
if (importInfo == null || StringUtils.isBlank(importInfo.getChildObjectName())) {
return StringUtils.EMPTY;
}

return importInfo.getParentObjectName() + "|" + importInfo.getChildObjectName();
}

private String mergeImportRemarks(String existingRemarks, String newRemarks) {
if (StringUtils.isBlank(existingRemarks)) {
return newRemarks;
}

if (StringUtils.isBlank(newRemarks) || StringUtils.equals(existingRemarks, newRemarks)) {
return existingRemarks;
}

Set<String> uniqueRemarks = new HashSet<>(Arrays.asList(existingRemarks.split(System.lineSeparator())));

uniqueRemarks.add(newRemarks);

return String.join(System.lineSeparator(), uniqueRemarks);
}

static class PaginationHelper<T> {
private final int pageStart;
private final int pageEnd;
Expand Down
Loading
Loading