From 0d8ff1a674367c3ac189c44c1b135e5116782b21 Mon Sep 17 00:00:00 2001 From: Brijesh Bhalala Date: Fri, 10 Jul 2026 18:45:31 +0530 Subject: [PATCH 1/3] ATLAS-5342: Atlas React UI: Entity modification functionalities (Add Classifications, Terms, Labels, Business Metadata) remain active for DELETED entities --- .../ShowMore/DrawerBodyChipView.tsx | 5 +- .../src/components/ShowMore/ShowMoreView.tsx | 15 ++- .../ShowMore/__tests__/ShowMoreView.test.tsx | 23 ++++ dashboard/src/utils/EntityStatus.ts | 21 ++++ .../ClassificationCoverage.tsx | 2 +- .../views/DetailPage/DetailPageAttributes.tsx | 85 ++++++++------- .../src/views/DetailPage/EntityDetailPage.tsx | 95 ++++++++-------- .../EntityDetailTabs/AttributeProperties.tsx | 3 +- .../EntityDetailTabs/ClassificationsTab.tsx | 7 +- .../PropertiesTab/BMAttributes.tsx | 101 ++++++++++-------- .../EntityDetailTabs/PropertiesTab/Labels.tsx | 61 ++++++----- .../PropertiesTab/PropertiesTab.tsx | 2 +- .../PropertiesTab/UserDefinedProperties.tsx | 67 +++++++----- 13 files changed, 290 insertions(+), 197 deletions(-) create mode 100644 dashboard/src/utils/EntityStatus.ts diff --git a/dashboard/src/components/ShowMore/DrawerBodyChipView.tsx b/dashboard/src/components/ShowMore/DrawerBodyChipView.tsx index 8d6c06c6f14..c655d46e23c 100644 --- a/dashboard/src/components/ShowMore/DrawerBodyChipView.tsx +++ b/dashboard/src/components/ShowMore/DrawerBodyChipView.tsx @@ -37,6 +37,7 @@ import SearchIcon from "@mui/icons-material/Search"; import ErrorRoundedIcon from "@mui/icons-material/ErrorRounded"; import { Link as MuiLink } from "@mui/material"; import { cloneDeep } from "@utils/Helper"; +import { EntityStatus } from "@utils/EntityStatus"; const CHIP_MAX_WIDTH = "200px"; @@ -328,11 +329,11 @@ const DrawerBodyChipView = ({ } onDelete={ - !isEmpty(removeApiMethod) && !isDeleteIcon + currentEntity?.status !== EntityStatus.DELETED && !isEmpty(removeApiMethod) && !isDeleteIcon ? () => { handleDelete(obj[displayKey] || obj); } - : isDeleteIcon && obj.count > 1 + : currentEntity?.status !== EntityStatus.DELETED && isDeleteIcon && obj.count > 1 ? () => { const searchParams = new URLSearchParams(); searchParams.set("tabActive", "classification"); diff --git a/dashboard/src/components/ShowMore/ShowMoreView.tsx b/dashboard/src/components/ShowMore/ShowMoreView.tsx index d6543410ead..ca40eb250c2 100644 --- a/dashboard/src/components/ShowMore/ShowMoreView.tsx +++ b/dashboard/src/components/ShowMore/ShowMoreView.tsx @@ -19,7 +19,7 @@ import Typography from "@mui/material/Typography"; import Chip from "@mui/material/Chip"; import MuiLink from "@mui/material/Link"; import { LightTooltip } from "../muiComponents"; -import { useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { EllipsisText } from "../commonComponents"; import { extractKeyValueFromEntity, isEmpty, serverError } from "@utils/Utils"; import { useAppDispatch, useAppSelector } from "@hooks/reducerHook"; @@ -31,8 +31,9 @@ import ErrorRoundedIcon from "@mui/icons-material/ErrorRounded"; import { fetchGlossaryData } from "@redux/slice/glossarySlice"; import { fetchGlossaryDetails } from "@redux/slice/glossaryDetailsSlice"; import ShowMoreDrawer from "./ShowMoreDrawer"; -import { openDrawer } from "@redux/slice/drawerSlice"; +import { openDrawer, closeDrawer } from "@redux/slice/drawerSlice"; import { cloneDeep } from "@utils/Helper"; +import { EntityStatus } from "@utils/EntityStatus"; const CHIP_MAX_WIDTH = "200px"; @@ -73,6 +74,12 @@ const ShowMoreView = ({ const gType = searchParams.get("gtype"); const dispatchApi = useAppDispatch(); + useEffect(() => { + return () => { + dispatchApi(closeDrawer()); + }; + }, [dispatchApi]); + const { classificationData = {} }: any = useAppSelector( (state: any) => state.classification ); @@ -310,13 +317,13 @@ const ShowMoreView = ({ } component="a" onDelete={ - !isEmpty(removeApiMethod) && !isDeleteIcon + !isEmpty(removeApiMethod) && !isDeleteIcon && currentEntity?.status !== EntityStatus.DELETED ? () => { // Handle undefined displayKey by extracting a string value const deleteValue = obj[displayKey] || obj.displayText || obj.text || obj.name || ''; handleDelete(deleteValue); } - : isDeleteIcon && obj.count > 1 + : isDeleteIcon && obj.count > 1 && currentEntity?.status !== EntityStatus.DELETED ? () => { const searchParams = new URLSearchParams(); searchParams.set("tabActive", "classification"); diff --git a/dashboard/src/components/ShowMore/__tests__/ShowMoreView.test.tsx b/dashboard/src/components/ShowMore/__tests__/ShowMoreView.test.tsx index 5e618258048..1ee9ca290da 100644 --- a/dashboard/src/components/ShowMore/__tests__/ShowMoreView.test.tsx +++ b/dashboard/src/components/ShowMore/__tests__/ShowMoreView.test.tsx @@ -151,6 +151,9 @@ jest.mock('@redux/slice/drawerSlice', () => ({ openDrawer: jest.fn((id: string) => ({ type: 'drawer/openDrawer', payload: id + })), + closeDrawer: jest.fn(() => ({ + type: 'drawer/closeDrawer' })) })); @@ -739,6 +742,26 @@ describe('ShowMoreView', () => { }); describe('Delete Icon Functionality', () => { + it('should not show delete button when currentEntity status is DELETED', () => { + const dataWithCount = [ + { typeName: 'Tag1' } + ]; + + render( + + + + ); + + expect(screen.queryByTestId('chip-ondelete-button')).not.toBeInTheDocument(); + }); + + it('should show count when isDeleteIcon is true and count > 1', () => { const dataWithCount = [ { typeName: 'Tag1', count: 2 }, diff --git a/dashboard/src/utils/EntityStatus.ts b/dashboard/src/utils/EntityStatus.ts new file mode 100644 index 00000000000..10e5d616f2b --- /dev/null +++ b/dashboard/src/utils/EntityStatus.ts @@ -0,0 +1,21 @@ +/* + * 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 enum EntityStatus { + ACTIVE = "ACTIVE", + DELETED = "DELETED" +} diff --git a/dashboard/src/views/DashboardOverview/ClassificationCoverage.tsx b/dashboard/src/views/DashboardOverview/ClassificationCoverage.tsx index 03c27b94dca..faa54da6ae6 100644 --- a/dashboard/src/views/DashboardOverview/ClassificationCoverage.tsx +++ b/dashboard/src/views/DashboardOverview/ClassificationCoverage.tsx @@ -289,7 +289,7 @@ const ClassificationCoverage = memo( aria-label="Open classification search" > {numberFormatWithComma(typesInUse)} of{" "} - {numberFormatWithComma(classificationTypeDefinitions)} + {numberFormatWithComma(classificationTypeDefinitions)}{" "} classification types are in use (have at least one entity). diff --git a/dashboard/src/views/DetailPage/DetailPageAttributes.tsx b/dashboard/src/views/DetailPage/DetailPageAttributes.tsx index 3bbe6fe402f..12cc51154a0 100644 --- a/dashboard/src/views/DetailPage/DetailPageAttributes.tsx +++ b/dashboard/src/views/DetailPage/DetailPageAttributes.tsx @@ -41,6 +41,7 @@ const getDescriptionForDisplay = (desc: unknown): string => { }; import { useState } from "react"; import { useAppSelector } from "@hooks/reducerHook"; +import { EntityStatus } from "@utils/EntityStatus"; import { toast } from "react-toastify"; import EditOutlinedIcon from "@mui/icons-material/EditOutlined"; import { removeClassification } from "@api/apiMethods/classificationApiMethod"; @@ -147,7 +148,7 @@ const DetailPageAttribute = ({ {name}{" "} - {isEmpty(bmguid) && ( + {isEmpty(bmguid) && !loading && data?.status !== EntityStatus.DELETED && ( Classifications - - { - setOpenAddTagModal(true); - }} - > - {" "} - - + {!loading && data?.status !== EntityStatus.DELETED && ( + + { + setOpenAddTagModal(true); + }} + > + {" "} + + + )} Terms - - { - if (!hasAnyGlossaryTerms) { - toast.dismiss(); - toast.info("There are no available terms"); - return; - } - setOpenAddTermModal(true); - }} - > - {" "} - - + {!loading && data?.status !== EntityStatus.DELETED && ( + + { + if (!hasAnyGlossaryTerms) { + toast.dismiss(); + toast.info("There are no available terms"); + return; + } + setOpenAddTermModal(true); + }} + > + {" "} + + + )} { let entityObj = !isEmpty(entityDefObj) && !isEmpty(entity) ? entityDefObj.find((obj: { name: string }) => { - return obj.name == entity.typeName; - }) + return obj.name == entity.typeName; + }) : {}; let superTypes = !isEmpty(entityDefObj) ? getNestedSuperTypes({ - data: entityObj, - collection: entityDefObj - }) + data: entityObj, + collection: entityDefObj + }) : []; let isLineageRender: boolean | null = superTypes.find((type) => { if (type === "DataSet" || type === "Process") { @@ -327,7 +328,7 @@ const EntityDetailPage: React.FC = () => { className="detail-page-paper" variant="outlined" > - {loading ? ( + {loading || detailPageData === null ? ( { }} > - {loading ? ( + {loading || detailPageData === null ? ( { > Classifications - - { - setOpenAddTagModal(true); - }} - > - {" "} - - + {entity?.status !== EntityStatus.DELETED && ( + + { + setOpenAddTagModal(true); + }} + > + {" "} + + + )} { {!entity?.typeName?.includes("AtlasGlossary") && ( - {loading ? ( + {loading || detailPageData === null ? ( { > Terms - - { - if (!hasAnyGlossaryTerms) { - toast.dismiss(); - toast.info("There are no available terms"); - return; - } - setOpenAddTermModal(true); - }} - > - - - + {entity?.status !== EntityStatus.DELETED && ( + + { + if (!hasAnyGlossaryTerms) { + toast.dismiss(); + toast.info("There are no available terms"); + return; + } + setOpenAddTermModal(true); + }} + > + + + + )} { - {entityUpdate && ( + {entityUpdate && !loading && entity?.status !== EntityStatus.DELETED && ( = ({ let values = info.row.original; return ( - {(guid == values?.entityGuid || + {!loading && entity?.status !== EntityStatus.DELETED && (guid == values?.entityGuid || (guid != values?.entityGuid && values.entityStatus == "DELETED")) && ( @@ -279,7 +280,7 @@ const ClassificationsTab: React.FC = ({ )} - {guid == values?.entityGuid && ( + {!loading && entity?.status !== EntityStatus.DELETED && guid == values?.entityGuid && ( = ({ enableSorting: false } ], - [updateTable] + [updateTable, entity] ); return ( diff --git a/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/BMAttributes.tsx b/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/BMAttributes.tsx index 2fb291ebdd4..30c673952e5 100644 --- a/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/BMAttributes.tsx +++ b/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/BMAttributes.tsx @@ -57,6 +57,7 @@ import { toast } from "react-toastify"; import { cloneDeep } from "@utils/Helper"; import moment from "moment-timezone"; import { fetchDetailPageData } from "@redux/slice/detailPageSlice"; +import { EntityStatus } from "@utils/EntityStatus"; const defaultField = { key: null, @@ -376,22 +377,24 @@ const BMAttributes = ({ loading, bmAttributes, entity }: any) => { {addLabel ? ( - void }) => { - e.stopPropagation(); - setExpanded("bmDataPanel"); - setAddLabel(false); - if (!isEmpty(defaultFieldValues)) { - reset({ businessMetadata: defaultFieldValues }); - } - }} - > - {!isEmpty(bmAttributes) ? "Edit" : "Add"} - + !loading && entity?.status !== EntityStatus.DELETED && ( + void }) => { + e.stopPropagation(); + setExpanded("bmDataPanel"); + setAddLabel(false); + if (!isEmpty(defaultFieldValues)) { + reset({ businessMetadata: defaultFieldValues }); + } + }} + > + {!isEmpty(bmAttributes) ? "Edit" : "Add"} + + ) ) : ( <> { justifyContent="center" > - No properties have been created yet. To add a - property, click{" "} - void }) => { - e.stopPropagation(); - setAddLabel(false); - }} - style={{ textDecoration: "underline" }} - > - here - + {entity?.status === EntityStatus.DELETED ? ( + "No properties have been created yet." + ) : ( + <> + No properties have been created yet. To add a + property, click{" "} + void }) => { + e.stopPropagation(); + setAddLabel(false); + }} + style={{ textDecoration: "underline" }} + > + here + + + )} )} ) : ( <> - - { - e.stopPropagation(); - append(defaultField); - }} - startIcon={} - > - Add New Attributes - - + {!loading && entity?.status !== EntityStatus.DELETED && ( + + { + e.stopPropagation(); + append(defaultField); + }} + startIcon={} + > + Add New Attributes + + + )} {fields.map((field, index) => { const keySelected = !isEmpty( bmAttributesValues?.[index]?.key diff --git a/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/Labels.tsx b/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/Labels.tsx index 77b4f21b7cd..d6ed16741b5 100644 --- a/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/Labels.tsx +++ b/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/Labels.tsx @@ -43,10 +43,11 @@ import { useParams } from "react-router-dom"; import { getLabels } from "@api/apiMethods/detailpageApiMethod"; import { useAppDispatch } from "@hooks/reducerHook"; import { fetchDetailPageData } from "@redux/slice/detailPageSlice"; +import { EntityStatus } from "@utils/EntityStatus"; const filter = createFilterOptions(); -const Labels = ({ loading, labels }: any) => { +const Labels = ({ loading, labels, entity }: any) => { const { guid }: any = useParams(); const toastId: any = useRef(null); const dispatchApi = useAppDispatch(); @@ -189,19 +190,21 @@ const Labels = ({ loading, labels }: any) => { {addLabel ? ( - void }) => { - e.stopPropagation(); - setExpanded("labelsPanel"); - setAddLabel(false); - }} - > - {!isEmpty(labels) ? "Edit" : "Add"} - + !loading && entity?.status !== EntityStatus.DELETED && ( + void }) => { + e.stopPropagation(); + setExpanded("labelsPanel"); + setAddLabel(false); + }} + > + {!isEmpty(labels) ? "Edit" : "Add"} + + ) ) : ( <> { }) ) : ( - No labels have been created yet. To add a labels, click{" "} - void }) => { - e.stopPropagation(); - setAddLabel(false); - }} - style={{ textDecoration: "underline" }} - > - here - + {entity?.status === EntityStatus.DELETED ? ( + "No labels have been created yet." + ) : ( + <> + No labels have been created yet. To add a labels, click{" "} + void }) => { + e.stopPropagation(); + setAddLabel(false); + }} + style={{ textDecoration: "underline" }} + > + here + + + )} )} diff --git a/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/PropertiesTab.tsx b/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/PropertiesTab.tsx index 0a473032082..8b8705a9ee5 100644 --- a/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/PropertiesTab.tsx +++ b/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/PropertiesTab.tsx @@ -56,7 +56,7 @@ const PropertiesTab = (props: { customAttributes={customAttributes} entity={entity} /> - + { {addLabel ? ( - void }) => { - e.stopPropagation(); - setExpanded("userDefinedPanel"); - setAddLabel(false); - if (!isEmpty(defaultFieldValues)) { - reset({ customAttributes: defaultFieldValues }); - } - }} - > - {!isEmpty(customAttributes) ? "Edit" : "Add"} - + !loading && entity?.status !== EntityStatus.DELETED && ( + void }) => { + e.stopPropagation(); + setExpanded("userDefinedPanel"); + setAddLabel(false); + if (!isEmpty(defaultFieldValues)) { + reset({ customAttributes: defaultFieldValues }); + } + }} + > + {!isEmpty(customAttributes) ? "Edit" : "Add"} + + ) ) : ( <> { }) ) : ( - No properties have been created yet. To add a - property,click{" "} - void }) => { - e.stopPropagation(); - setAddLabel(false); - }} - style={{ textDecoration: "underline" }} - > - here - + {entity?.status === EntityStatus.DELETED ? ( + "No properties have been created yet." + ) : ( + <> + No properties have been created yet. To add a + property,click{" "} + void }) => { + e.stopPropagation(); + setAddLabel(false); + }} + style={{ textDecoration: "underline" }} + > + here + + + )} )} From 6780c111f273dda2f8425308a54b350c327219ef Mon Sep 17 00:00:00 2001 From: Brijesh Bhalala Date: Mon, 27 Jul 2026 18:27:11 +0530 Subject: [PATCH 2/3] ATLAS-5342: Atlas React UI: Entity modification functionalities (Add Classifications, Terms, Labels, Business Metadata) remain active for DELETED entities --- dashboard/src/styles/propertiesTab.scss | 4 + .../PropertiesTab/BMAttributes.tsx | 28 ++- .../EntityDetailTabs/PropertiesTab/Labels.tsx | 21 +- .../PropertiesTab/UserDefinedProperties.tsx | 28 ++- .../__tests__/BMAttributes.test.tsx | 203 ++++++++++++++++++ .../PropertiesTab/__tests__/Labels.test.tsx | 174 +++++++++++++++ .../__tests__/UserDefinedProperties.test.tsx | 194 +++++++++++++++++ 7 files changed, 623 insertions(+), 29 deletions(-) create mode 100644 dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/__tests__/BMAttributes.test.tsx create mode 100644 dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/__tests__/Labels.test.tsx create mode 100644 dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/__tests__/UserDefinedProperties.test.tsx diff --git a/dashboard/src/styles/propertiesTab.scss b/dashboard/src/styles/propertiesTab.scss index 3c9076a1751..748c37198fb 100644 --- a/dashboard/src/styles/propertiesTab.scss +++ b/dashboard/src/styles/propertiesTab.scss @@ -78,3 +78,7 @@ .audit-attributes-item:nth-child(3) { flex: 0 0 100%; } + +.text-underline { + text-decoration: underline; +} diff --git a/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/BMAttributes.tsx b/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/BMAttributes.tsx index 30c673952e5..489a871ea59 100644 --- a/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/BMAttributes.tsx +++ b/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/BMAttributes.tsx @@ -64,21 +64,27 @@ const defaultField = { value: "" }; -const BMAttributes = ({ loading, bmAttributes, entity }: any) => { +type BMAttributesProps = { + loading: boolean | undefined; + bmAttributes: Record; + entity: any; +}; + +const BMAttributes = ({ loading, bmAttributes, entity }: BMAttributesProps) => { const dispatchApi = useAppDispatch(); - const { guid }: any = useParams(); - const toastId: any = useRef(null); - const { entityData }: any = useAppSelector((state: any) => state.entity); + const { guid } = useParams(); + const toastId = useRef(null); + const { entityData } = useAppSelector((state: any) => state.entity); const { entityDefs } = entityData || {}; let filterEntityData = cloneDeep(entityDefs); - const { businessMetaData }: any = useAppSelector( + const { businessMetaData } = useAppSelector( (state: any) => state.businessMetaData ); const { businessMetadataDefs } = businessMetaData || {}; let businessAttributes = cloneDeep(bmAttributes); let bmAttributesData = Object.entries(businessAttributes).map( - ([key, value]: any) => { + ([key, value]: [string, any]) => { let foundBusinessMetadata = businessMetadataDefs.find( (obj: { name: any }) => obj.name == key ); @@ -212,7 +218,7 @@ const BMAttributes = ({ loading, bmAttributes, entity }: any) => { control }); - const onSubmit = async (values: { businessMetadata: any }) => { + const onSubmit = async (values: { businessMetadata: any[] }) => { let formData = { ...values }; const { businessMetadata } = formData; let data: Record = {}; @@ -301,7 +307,7 @@ const BMAttributes = ({ loading, bmAttributes, entity }: any) => { handleSubmit(onSubmit)(); }; - const renderValues = (values: any) => { + const renderValues = (values: { value: any; typeName: string }) => { const { value, typeName } = values; if ( @@ -480,7 +486,7 @@ const BMAttributes = ({ loading, bmAttributes, entity }: any) => { - {Object.entries(obj).map(([key, value]: any) => { + {Object.entries(obj).map(([key, value]: [string, any]) => { return ( <> {key != @@ -552,7 +558,7 @@ const BMAttributes = ({ loading, bmAttributes, entity }: any) => { e.stopPropagation(); setAddLabel(false); }} - style={{ textDecoration: "underline" }} + className="text-color-green cursor-pointer text-underline" > here @@ -572,7 +578,7 @@ const BMAttributes = ({ loading, bmAttributes, entity }: any) => { }} variant="outlined" size="small" - onClick={(e: any) => { + onClick={(e: React.MouseEvent) => { e.stopPropagation(); append(defaultField); }} diff --git a/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/Labels.tsx b/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/Labels.tsx index d6ed16741b5..f8644272c4c 100644 --- a/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/Labels.tsx +++ b/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/Labels.tsx @@ -45,11 +45,18 @@ import { useAppDispatch } from "@hooks/reducerHook"; import { fetchDetailPageData } from "@redux/slice/detailPageSlice"; import { EntityStatus } from "@utils/EntityStatus"; -const filter = createFilterOptions(); +type LabelOption = string | { inputValue?: string; value?: string }; +const filter = createFilterOptions(); -const Labels = ({ loading, labels, entity }: any) => { - const { guid }: any = useParams(); - const toastId: any = useRef(null); +type LabelsProps = { + loading: boolean | undefined; + labels: string[]; + entity: any; +}; + +const Labels = ({ loading, labels, entity }: LabelsProps) => { + const { guid } = useParams(); + const toastId = useRef(null); const dispatchApi = useAppDispatch(); const [addLabel, setAddLabel] = useState(true); const [expanded, setExpanded] = useState(false); @@ -101,7 +108,7 @@ const Labels = ({ loading, labels, entity }: any) => { setLoader(false); setOpen(false); }; - const onInputChange = (_event: any, value: string) => { + const onInputChange = (_event: React.SyntheticEvent, value: string) => { if (value) { setOpen(true); setLoader(true); @@ -136,7 +143,7 @@ const Labels = ({ loading, labels, entity }: any) => { return out; }; - const onSubmit = async (values: any) => { + const onSubmit = async (values: Record) => { const formData = { ...values }; const payload = normalizeLabelsPayload(formData.labels); if (payload.length === 0 && (!labels || labels.length === 0)) { @@ -290,7 +297,7 @@ const Labels = ({ loading, labels, entity }: any) => { e.stopPropagation(); setAddLabel(false); }} - style={{ textDecoration: "underline" }} + className="text-color-green cursor-pointer text-underline" > here diff --git a/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/UserDefinedProperties.tsx b/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/UserDefinedProperties.tsx index c137d43ee80..f88880fc508 100644 --- a/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/UserDefinedProperties.tsx +++ b/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/UserDefinedProperties.tsx @@ -57,10 +57,16 @@ const defaultField = { value: "" }; -const UserDefinedProperties = ({ loading, customAttributes, entity }: any) => { +type UserDefinedPropertiesProps = { + loading: boolean | undefined; + customAttributes: Record; + entity: any; +}; + +const UserDefinedProperties = ({ loading, customAttributes, entity }: UserDefinedPropertiesProps) => { const dispatchApi = useAppDispatch(); - const { guid }: any = useParams(); - const toastId: any = useRef(null); + const { guid } = useParams(); + const toastId = useRef(null); const [addLabel, setAddLabel] = useState(true); const [expanded, setExpanded] = useState(false); let attributes = cloneDeep(customAttributes); @@ -102,7 +108,7 @@ const UserDefinedProperties = ({ loading, customAttributes, entity }: any) => { } }; - const structureAttributes = (list: any) => { + const structureAttributes = (list: { key: string; value: string }[]) => { const obj: Record = {}; if (!Array.isArray(list)) { return obj; @@ -120,7 +126,7 @@ const UserDefinedProperties = ({ loading, customAttributes, entity }: any) => { return obj; }; - const onSubmit = async (values: any) => { + const onSubmit = async (values: Record) => { let formData = { ...values }; let entityObj = cloneDeep(entity); let properties = structureAttributes(formData.customAttributes); @@ -153,10 +159,10 @@ const UserDefinedProperties = ({ loading, customAttributes, entity }: any) => { handleSubmit(onSubmit)(); }; - const validateKeyUnique = (value: any, index: number): any => { + const validateKeyUnique = (value: string, index: number): string | boolean => { const items = getValues("customAttributes"); const duplicate = items.some( - (item: { key: any }, i: any) => item.key === value && i !== index + (item: { key: string }, i: number) => item.key === value && i !== index ); return duplicate ? "Key must be unique" : true; }; @@ -270,7 +276,7 @@ const UserDefinedProperties = ({ loading, customAttributes, entity }: any) => { {!isEmpty(customAttributes) ? ( Object.entries(customAttributes) .sort() - .map(([keys, value]: [string, any]) => { + .map(([keys, value]: [string, string]) => { return ( <> { e.stopPropagation(); setAddLabel(false); }} - style={{ textDecoration: "underline" }} + className="text-color-green cursor-pointer text-underline" > here @@ -332,7 +338,7 @@ const UserDefinedProperties = ({ loading, customAttributes, entity }: any) => { ) : ( <> - {fields.map((item: any, index) => { + {fields.map((item: Record, index) => { return ( { size="small" aria-label="add" className="cursor-pointer" - onClick={(e: any) => { + onClick={(e: React.MouseEvent) => { e.stopPropagation(); append(defaultField); }} diff --git a/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/__tests__/BMAttributes.test.tsx b/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/__tests__/BMAttributes.test.tsx new file mode 100644 index 00000000000..10d5ce30772 --- /dev/null +++ b/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/__tests__/BMAttributes.test.tsx @@ -0,0 +1,203 @@ +/* + * 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 React from 'react'; +import { render, screen, fireEvent, waitFor } from '@utils/test-utils'; +import userEvent from '@testing-library/user-event'; +import '@testing-library/jest-dom'; +import BMAttributes from '../BMAttributes'; +import { ThemeProvider, createTheme } from '@mui/material/styles'; + +const theme = createTheme(); + +// Mock dependencies +const mockDispatch = jest.fn(); +jest.mock('@hooks/reducerHook', () => ({ + useAppDispatch: () => mockDispatch, + useAppSelector: jest.fn((selector) => { + const state = { + entity: { + entityData: { + entityDefs: [ + { + name: 'DataSet', + businessAttributeDefs: { + 'Group1': [ + { name: 'attr1', typeName: 'string' }, + { name: 'attr2', typeName: 'int' } + ] + } + } + ] + } + }, + businessMetaData: { + businessMetaData: { + businessMetadataDefs: [ + { + name: 'Group1', + attributeDefs: [ + { name: 'attr1', typeName: 'string' }, + { name: 'attr2', typeName: 'int' } + ] + } + ] + } + } + }; + return selector(state); + }) +})); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: () => ({ guid: 'test-guid-123' }) +})); + +const mockGetEntityBusinessMetadata = jest.fn(); +jest.mock('@api/apiMethods/detailpageApiMethod', () => ({ + getEntityBusinessMetadata: (...args: any[]) => mockGetEntityBusinessMetadata(...args) +})); + +jest.mock('react-toastify', () => ({ + toast: { + dismiss: jest.fn(), + success: jest.fn(() => 'toast-id'), + error: jest.fn(() => 'toast-id') + } +})); + +jest.mock('@utils/Utils', () => ({ + ...jest.requireActual('@utils/Utils'), + serverError: jest.fn() +})); + +jest.mock('@redux/slice/detailPageSlice', () => ({ + fetchDetailPageData: jest.fn((guid: string) => ({ type: 'fetchDetailPageData', payload: guid })) +})); + +// Mock BMAttributesFields to avoid complex form input mocks unless needed +jest.mock('../BMAttributesFields', () => { + return function MockBMAttributesFields(props: any) { + return
{props.obj?.name}
; + }; +}); + +const TestWrapper: React.FC> = ({ children }) => ( + {children} +); + +describe('BMAttributes Component', () => { + const defaultProps = { + loading: false, + bmAttributes: { + 'Group1': { + 'attr1': 'value1', + 'attr2': 100 + } + }, + entity: { guid: 'test-guid-123', status: 'ACTIVE', typeName: 'DataSet' } + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders existing business metadata correctly', () => { + render(); + + expect(screen.getByText('Business Metadata')).toBeInTheDocument(); + expect(screen.getByText('Group1')).toBeInTheDocument(); + expect(screen.getByText('attr1 (string)')).toBeInTheDocument(); + expect(screen.getByText('attr2 (int)')).toBeInTheDocument(); + // BMAttributes renders HTML for string values + expect(screen.getByText('value1')).toBeInTheDocument(); + expect(screen.getByText('100')).toBeInTheDocument(); + }); + + it('shows empty state message when empty', () => { + render(); + + expect(screen.getByText(/No properties have been created yet/i)).toBeInTheDocument(); + }); + + it('does not allow editing if entity is deleted', () => { + render(); + + expect(screen.getByText(/No properties have been created yet/i)).toBeInTheDocument(); + expect(screen.queryByText(/To add a property, click/i)).not.toBeInTheDocument(); + expect(screen.queryByText('Add')).not.toBeInTheDocument(); + }); + + it('switches to edit mode on Edit button click', () => { + render(); + + const editBtn = screen.getAllByRole('button', { name: /edit/i })[0]; + fireEvent.click(editBtn); + + expect(screen.getAllByTestId('bm-fields-mock')).toHaveLength(2); + expect(screen.getByText('attr1')).toBeInTheDocument(); + expect(screen.getByText('attr2')).toBeInTheDocument(); + }); + + it('adds new attribute dynamically', async () => { + render(); + + fireEvent.click(screen.getAllByRole('button', { name: /edit/i })[0]); + + const addAttrBtn = screen.getByRole('button', { name: /Add New Attributes/i }); + fireEvent.click(addAttrBtn); + + // There should be 3 items now + const removeBtns = screen.getAllByTestId('RemoveOutlinedIcon'); + expect(removeBtns).toHaveLength(3); + }); + + it('submits form successfully', async () => { + mockGetEntityBusinessMetadata.mockResolvedValueOnce({ data: {} }); + + render(); + + fireEvent.click(screen.getAllByRole('button', { name: /edit/i })[0]); + + const saveBtn = screen.getAllByRole('button', { name: /save/i })[0]; + fireEvent.click(saveBtn); + + await waitFor(() => { + expect(mockGetEntityBusinessMetadata).toHaveBeenCalledWith('test-guid-123', expect.any(Object)); + expect(mockDispatch).toHaveBeenCalled(); + }); + }); + + it('handles API failure on save gracefully', async () => { + mockGetEntityBusinessMetadata.mockRejectedValueOnce(new Error('Network error')); + + render(); + + fireEvent.click(screen.getAllByRole('button', { name: /edit/i })[0]); + + const saveBtn = screen.getAllByRole('button', { name: /save/i })[0]; + fireEvent.click(saveBtn); + + await waitFor(() => { + expect(mockGetEntityBusinessMetadata).toHaveBeenCalled(); + }); + + const { serverError } = require('@utils/Utils'); + expect(serverError).toHaveBeenCalled(); + }); +}); diff --git a/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/__tests__/Labels.test.tsx b/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/__tests__/Labels.test.tsx new file mode 100644 index 00000000000..4ffc2e9e660 --- /dev/null +++ b/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/__tests__/Labels.test.tsx @@ -0,0 +1,174 @@ +/* + * 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 React from 'react'; +import { render, screen, fireEvent, waitFor } from '@utils/test-utils'; +import userEvent from '@testing-library/user-event'; +import '@testing-library/jest-dom'; +import Labels from '../Labels'; +import { ThemeProvider, createTheme } from '@mui/material/styles'; + +const theme = createTheme(); + +// Mock dependencies +const mockDispatch = jest.fn(); +jest.mock('@hooks/reducerHook', () => ({ + useAppDispatch: () => mockDispatch +})); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: () => ({ guid: 'test-guid-123' }) +})); + +const mockGetLabels = jest.fn(); +const mockGetGlobalSearchResult = jest.fn(); +jest.mock('@api/apiMethods/detailpageApiMethod', () => ({ + getLabels: (...args: any[]) => mockGetLabels(...args) +})); +jest.mock('@api/apiMethods/searchApiMethod', () => ({ + getGlobalSearchResult: (...args: any[]) => mockGetGlobalSearchResult(...args) +})); + +jest.mock('react-toastify', () => ({ + toast: { + dismiss: jest.fn(), + success: jest.fn(() => 'toast-id'), + error: jest.fn(() => 'toast-id') + } +})); + +jest.mock('@utils/Utils', () => ({ + ...jest.requireActual('@utils/Utils'), + serverError: jest.fn() +})); + +jest.mock('@redux/slice/detailPageSlice', () => ({ + fetchDetailPageData: jest.fn((guid: string) => ({ type: 'fetchDetailPageData', payload: guid })) +})); + +const TestWrapper: React.FC> = ({ children }) => ( + {children} +); + +describe('Labels Component', () => { + const defaultProps = { + loading: false, + labels: ['Label1', 'Label2'], + entity: { status: 'ACTIVE' } + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders existing labels correctly', () => { + render(); + + // Labels are shown in an accordion that is expanded by default since labels exist + expect(screen.getByText('Labels')).toBeInTheDocument(); + expect(screen.getByText('Label1')).toBeInTheDocument(); + expect(screen.getByText('Label2')).toBeInTheDocument(); + }); + + it('shows no labels message when empty', () => { + render(); + + expect(screen.getByText(/No labels have been created yet/i)).toBeInTheDocument(); + }); + + it('does not allow editing if entity is deleted', () => { + render(); + + expect(screen.getByText(/No labels have been created yet/i)).toBeInTheDocument(); + expect(screen.queryByText(/To add a labels, click/i)).not.toBeInTheDocument(); + expect(screen.queryByText('Add')).not.toBeInTheDocument(); + }); + + it('allows clicking edit to show autocomplete form', async () => { + render(); + + const editBtn = screen.getByTestId('edit-label'); + fireEvent.click(editBtn); + + expect(await screen.findByPlaceholderText('Select Label')).toBeInTheDocument(); + }); + + it('allows clicking "here" to add label when empty', async () => { + render(); + + const hereText = screen.getByText('here'); + fireEvent.click(hereText); + + expect(await screen.findByPlaceholderText('Select Label')).toBeInTheDocument(); + }); + + it('calls API and dispatches action on save', async () => { + mockGetLabels.mockResolvedValueOnce({ data: {} }); + + render(); + + // Click Edit + fireEvent.click(screen.getAllByRole('button', { name: /edit/i })[0]); + + // Since autocomplete already has default value, let's just save + const saveBtn = screen.getAllByRole('button', { name: /save/i })[0]; + fireEvent.click(saveBtn); + + await waitFor(() => { + expect(mockGetLabels).toHaveBeenCalledWith('test-guid-123', ['Label1', 'Label2']); + expect(mockDispatch).toHaveBeenCalled(); + }); + }); + + it('handles API failure on save gracefully', async () => { + mockGetLabels.mockRejectedValueOnce(new Error('Network error')); + + render(); + + // Click Edit + fireEvent.click(screen.getAllByRole('button', { name: /edit/i })[0]); + + // Save + const saveBtn = screen.getAllByRole('button', { name: /save/i })[0]; + fireEvent.click(saveBtn); + + await waitFor(() => { + expect(mockGetLabels).toHaveBeenCalled(); + }); + + // Error is handled via serverError util mock + const { serverError } = require('@utils/Utils'); + expect(serverError).toHaveBeenCalled(); + }); + + it('fetches label suggestions on open', async () => { + mockGetGlobalSearchResult.mockResolvedValueOnce({ data: { suggestions: ['Label3', 'Label4'] } }); + + render(); + + // Click Edit + fireEvent.click(screen.getAllByRole('button', { name: /edit/i })[0]); + + const input = await screen.findByPlaceholderText('Select Label'); + fireEvent.mouseDown(input); + + await waitFor(() => { + expect(mockGetGlobalSearchResult).toHaveBeenCalledWith('suggestions', expect.objectContaining({ params: { fieldName: '__labels' } })); + }); + }); +}); diff --git a/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/__tests__/UserDefinedProperties.test.tsx b/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/__tests__/UserDefinedProperties.test.tsx new file mode 100644 index 00000000000..05b32b6ab87 --- /dev/null +++ b/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/__tests__/UserDefinedProperties.test.tsx @@ -0,0 +1,194 @@ +/* + * 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 React from 'react'; +import { render, screen, fireEvent, waitFor } from '@utils/test-utils'; +import userEvent from '@testing-library/user-event'; +import '@testing-library/jest-dom'; +import UserDefinedProperties from '../UserDefinedProperties'; +import { ThemeProvider, createTheme } from '@mui/material/styles'; + +const theme = createTheme(); + +// Mock dependencies +const mockDispatch = jest.fn(); +jest.mock('@hooks/reducerHook', () => ({ + useAppDispatch: () => mockDispatch +})); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: () => ({ guid: 'test-guid-123' }) +})); + +const mockCreateEntity = jest.fn(); +jest.mock('@api/apiMethods/entityFormApiMethod', () => ({ + createEntity: (...args: any[]) => mockCreateEntity(...args) +})); + +jest.mock('@utils/entityPayloadEnrichmentUtils', () => ({ + enrichEntityPayloadForRelationshipSave: jest.fn(async (entity) => entity) +})); + +jest.mock('react-toastify', () => ({ + toast: { + dismiss: jest.fn(), + success: jest.fn(() => 'toast-id'), + error: jest.fn(() => 'toast-id') + } +})); + +jest.mock('@utils/Utils', () => ({ + ...jest.requireActual('@utils/Utils'), + serverError: jest.fn() +})); + +jest.mock('@redux/slice/detailPageSlice', () => ({ + fetchDetailPageData: jest.fn((guid: string) => ({ type: 'fetchDetailPageData', payload: guid })) +})); + +const TestWrapper: React.FC> = ({ children }) => ( + {children} +); + +describe('UserDefinedProperties Component', () => { + const defaultProps = { + loading: false, + customAttributes: { key1: 'value1', key2: 'value2' }, + entity: { guid: 'test-guid-123', status: 'ACTIVE', customAttributes: {} } + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders existing properties correctly', () => { + render(); + + expect(screen.getByText('User-defined properties')).toBeInTheDocument(); + expect(screen.getByText('key1')).toBeInTheDocument(); + expect(screen.getByText('value1')).toBeInTheDocument(); + expect(screen.getByText('key2')).toBeInTheDocument(); + expect(screen.getByText('value2')).toBeInTheDocument(); + }); + + it('shows empty state message when empty', () => { + render(); + + expect(screen.getByText(/No properties have been created yet/i)).toBeInTheDocument(); + }); + + it('does not allow editing if entity is deleted', () => { + render(); + + expect(screen.getByText(/No properties have been created yet/i)).toBeInTheDocument(); + expect(screen.queryByText(/To add a property,click/i)).not.toBeInTheDocument(); + expect(screen.queryByText('Add')).not.toBeInTheDocument(); + }); + + it('switches to edit mode on Edit button click', () => { + render(); + + const editBtn = screen.getByRole('button', { name: /edit/i }); + fireEvent.click(editBtn); + + expect(screen.getByDisplayValue('key1')).toBeInTheDocument(); + expect(screen.getByDisplayValue('value1')).toBeInTheDocument(); + }); + + it('adds and removes fields dynamically', async () => { + render(); + + // Edit mode + fireEvent.click(screen.getAllByRole('button', { name: /edit/i })[0]); + + // Should have 2 inputs initially for keys + let keyInputs = screen.getAllByPlaceholderText('key'); + expect(keyInputs).toHaveLength(2); + + // Add new field + const addBtns = screen.getAllByTestId('AddOutlinedIcon'); + fireEvent.click(addBtns[0]); + + keyInputs = screen.getAllByPlaceholderText('key'); + expect(keyInputs).toHaveLength(3); + + // Remove field + const removeBtns = screen.getAllByTestId('RemoveOutlinedIcon'); + fireEvent.click(removeBtns[0]); // Removing first + + keyInputs = screen.getAllByPlaceholderText('key'); + expect(keyInputs).toHaveLength(2); + }); + + it('validates unique keys', async () => { + render(); + + fireEvent.click(screen.getAllByRole('button', { name: /edit/i })[0]); + + const keyInputs = screen.getAllByPlaceholderText('key'); + // Change second key to 'key1' to cause duplicate + fireEvent.change(keyInputs[1], { target: { value: 'key1' } }); + + // Form submission + const saveBtn = screen.getAllByRole('button', { name: /save/i })[0]; + fireEvent.click(saveBtn); + + await waitFor(() => { + expect(screen.getByText('Key must be unique')).toBeInTheDocument(); + expect(mockCreateEntity).not.toHaveBeenCalled(); + }); + }); + + it('submits form successfully', async () => { + mockCreateEntity.mockResolvedValueOnce({ data: {} }); + + render(); + + fireEvent.click(screen.getAllByRole('button', { name: /edit/i })[0]); + + const saveBtn = screen.getAllByRole('button', { name: /save/i })[0]; + fireEvent.click(saveBtn); + + await waitFor(() => { + expect(mockCreateEntity).toHaveBeenCalledWith({ + entity: expect.objectContaining({ + customAttributes: { key1: 'value1', key2: 'value2' } + }) + }); + expect(mockDispatch).toHaveBeenCalled(); + }); + }); + + it('handles API failure on save gracefully', async () => { + mockCreateEntity.mockRejectedValueOnce(new Error('Network error')); + + render(); + + fireEvent.click(screen.getAllByRole('button', { name: /edit/i })[0]); + + const saveBtn = screen.getAllByRole('button', { name: /save/i })[0]; + fireEvent.click(saveBtn); + + await waitFor(() => { + expect(mockCreateEntity).toHaveBeenCalled(); + }); + + const { serverError } = require('@utils/Utils'); + expect(serverError).toHaveBeenCalled(); + }); +}); From ea34fa095c5a400f1e704f91e9757a53ead3af23 Mon Sep 17 00:00:00 2001 From: Brijesh Bhalala Date: Fri, 31 Jul 2026 12:52:14 +0530 Subject: [PATCH 3/3] ATLAS-5342: Atlas React UI: Entity modification functionalities (Add Classifications, Terms, Labels, Business Metadata) remain active for DELETED entities --- .../src/components/EntityDisplayImage.tsx | 9 +++++--- .../__tests__/EntityDisplayImage.test.tsx | 22 +++++++++---------- .../PropertiesTab/BMAttributes.tsx | 11 +++++----- .../EntityDetailTabs/PropertiesTab/Labels.tsx | 7 +++--- .../PropertiesTab/UserDefinedProperties.tsx | 3 +-- .../__tests__/BMAttributes.test.tsx | 16 +++++++------- .../PropertiesTab/__tests__/Labels.test.tsx | 14 ++++++------ .../__tests__/UserDefinedProperties.test.tsx | 20 ++++++++--------- 8 files changed, 50 insertions(+), 52 deletions(-) diff --git a/dashboard/src/components/EntityDisplayImage.tsx b/dashboard/src/components/EntityDisplayImage.tsx index d9223d1d25b..64d5ef06469 100644 --- a/dashboard/src/components/EntityDisplayImage.tsx +++ b/dashboard/src/components/EntityDisplayImage.tsx @@ -18,6 +18,7 @@ import { useEffect, useState } from "react"; import { Avatar, Skeleton } from "@mui/material"; import { getEntityIconPath } from "../utils/Utils"; +import axios from "axios"; const DisplayImage = ({ entity, @@ -36,10 +37,12 @@ const DisplayImage = ({ let entityData = { ...entity, ...{ isProcess: isProcess } }; let imagePath: any = getEntityIconPath({ entityData: entityData }); try { - const response = await fetch(imagePath); - const contentType: any = response.headers.get("Content-Type"); + const response = await axios.get(imagePath, { + responseType: "blob" + }); + const contentType: any = response.headers["content-type"]; - if (contentType.startsWith("image/")) { + if (contentType && contentType.startsWith("image/")) { let cache = { [entityData.guid]: imagePath }; setCheckEntityImage(cache); setImageUrl(getEntityIconPath({ entityData: entityData })); diff --git a/dashboard/src/components/__tests__/EntityDisplayImage.test.tsx b/dashboard/src/components/__tests__/EntityDisplayImage.test.tsx index 41f759ec1da..04dc1c5c6de 100644 --- a/dashboard/src/components/__tests__/EntityDisplayImage.test.tsx +++ b/dashboard/src/components/__tests__/EntityDisplayImage.test.tsx @@ -29,6 +29,7 @@ import React from 'react' import { render, waitFor, act } from '@testing-library/react' import DisplayImage from '../EntityDisplayImage' +import axios from 'axios' // Import Utils to spy on it import * as Utils from '../../utils/Utils' @@ -41,18 +42,15 @@ jest.mock('../../utils/Utils', () => ({ })) const mockFetch = (contentType: string | null, shouldReject?: boolean) => { - if (shouldReject) { - (global as any).fetch = jest.fn().mockRejectedValue(new Error('fetch failed')) - return - } - (global as any).fetch = jest.fn().mockResolvedValue({ - ok: true, - headers: { - get: jest.fn((header: string) => { - return header === 'Content-Type' ? (contentType || '') : null - }) - } - }) + if (shouldReject) { + jest.spyOn(axios, 'get').mockRejectedValue(new Error('fetch failed')) + return + } + jest.spyOn(axios, 'get').mockResolvedValue({ + headers: { + "content-type": contentType || '' + } + }) } describe('EntityDisplayImage', () => { diff --git a/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/BMAttributes.tsx b/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/BMAttributes.tsx index 489a871ea59..e8966fda881 100644 --- a/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/BMAttributes.tsx +++ b/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/BMAttributes.tsx @@ -552,13 +552,12 @@ const BMAttributes = ({ loading, bmAttributes, entity }: BMAttributesProps) => { No properties have been created yet. To add a property, click{" "} void }) => { - e.stopPropagation(); - setAddLabel(false); - }} className="text-color-green cursor-pointer text-underline" + component="span" + onClick={(e: { stopPropagation: () => void }) => { + e.stopPropagation(); + setAddLabel(false); + }} > here diff --git a/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/Labels.tsx b/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/Labels.tsx index f8644272c4c..d8481831ff1 100644 --- a/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/Labels.tsx +++ b/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/Labels.tsx @@ -150,8 +150,8 @@ const Labels = ({ loading, labels, entity }: LabelsProps) => { return; } try { - await getLabels(guid, payload); - toast.dismiss(toastId.current); + await getLabels(guid as string, payload); + if (toastId.current) { toast.dismiss(toastId.current); } toastId.current = toast.success( "One or more labels were updated successfully" ); @@ -162,7 +162,7 @@ const Labels = ({ loading, labels, entity }: LabelsProps) => { setAddLabel(true); } catch (error) { - toast.dismiss(toastId.current); + if (toastId.current) { toast.dismiss(toastId.current); } serverError(error, toastId); } }; @@ -291,7 +291,6 @@ const Labels = ({ loading, labels, entity }: LabelsProps) => { <> No labels have been created yet. To add a labels, click{" "} void }) => { e.stopPropagation(); diff --git a/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/UserDefinedProperties.tsx b/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/UserDefinedProperties.tsx index f88880fc508..a264adfb164 100644 --- a/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/UserDefinedProperties.tsx +++ b/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/UserDefinedProperties.tsx @@ -319,13 +319,12 @@ const UserDefinedProperties = ({ loading, customAttributes, entity }: UserDefine No properties have been created yet. To add a property,click{" "} void }) => { e.stopPropagation(); setAddLabel(false); }} - className="text-color-green cursor-pointer text-underline" > here diff --git a/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/__tests__/BMAttributes.test.tsx b/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/__tests__/BMAttributes.test.tsx index 10d5ce30772..208439f0d14 100644 --- a/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/__tests__/BMAttributes.test.tsx +++ b/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/__tests__/BMAttributes.test.tsx @@ -16,7 +16,7 @@ */ import React from 'react'; -import { render, screen, fireEvent, waitFor } from '@utils/test-utils'; +import { render, screen, fireEvent, waitFor, act } from '@utils/test-utils'; import userEvent from '@testing-library/user-event'; import '@testing-library/jest-dom'; import BMAttributes from '../BMAttributes'; @@ -146,7 +146,7 @@ describe('BMAttributes Component', () => { it('switches to edit mode on Edit button click', () => { render(); - const editBtn = screen.getAllByRole('button', { name: /edit/i })[0]; + const editBtn = screen.getByText('Edit').closest('button')!; fireEvent.click(editBtn); expect(screen.getAllByTestId('bm-fields-mock')).toHaveLength(2); @@ -157,9 +157,9 @@ describe('BMAttributes Component', () => { it('adds new attribute dynamically', async () => { render(); - fireEvent.click(screen.getAllByRole('button', { name: /edit/i })[0]); + fireEvent.click(screen.getByText('Edit').closest('button')!); - const addAttrBtn = screen.getByRole('button', { name: /Add New Attributes/i }); + const addAttrBtn = screen.getByRole('button', { name: /Add New Attribute/i }); fireEvent.click(addAttrBtn); // There should be 3 items now @@ -172,10 +172,10 @@ describe('BMAttributes Component', () => { render(); - fireEvent.click(screen.getAllByRole('button', { name: /edit/i })[0]); + fireEvent.click(screen.getByText('Edit').closest('button')!); const saveBtn = screen.getAllByRole('button', { name: /save/i })[0]; - fireEvent.click(saveBtn); + await act(async () => { fireEvent.submit(saveBtn.closest('form')!); }); await waitFor(() => { expect(mockGetEntityBusinessMetadata).toHaveBeenCalledWith('test-guid-123', expect.any(Object)); @@ -188,10 +188,10 @@ describe('BMAttributes Component', () => { render(); - fireEvent.click(screen.getAllByRole('button', { name: /edit/i })[0]); + fireEvent.click(screen.getByText('Edit').closest('button')!); const saveBtn = screen.getAllByRole('button', { name: /save/i })[0]; - fireEvent.click(saveBtn); + await act(async () => { fireEvent.submit(saveBtn.closest('form')!); }); await waitFor(() => { expect(mockGetEntityBusinessMetadata).toHaveBeenCalled(); diff --git a/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/__tests__/Labels.test.tsx b/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/__tests__/Labels.test.tsx index 4ffc2e9e660..09f8b2cc6c6 100644 --- a/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/__tests__/Labels.test.tsx +++ b/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/__tests__/Labels.test.tsx @@ -16,7 +16,7 @@ */ import React from 'react'; -import { render, screen, fireEvent, waitFor } from '@utils/test-utils'; +import { render, screen, fireEvent, waitFor, act } from '@utils/test-utils'; import userEvent from '@testing-library/user-event'; import '@testing-library/jest-dom'; import Labels from '../Labels'; @@ -102,7 +102,7 @@ describe('Labels Component', () => { it('allows clicking edit to show autocomplete form', async () => { render(); - const editBtn = screen.getByTestId('edit-label'); + const editBtn = screen.getByText('Edit').closest('button')!; fireEvent.click(editBtn); expect(await screen.findByPlaceholderText('Select Label')).toBeInTheDocument(); @@ -123,11 +123,11 @@ describe('Labels Component', () => { render(); // Click Edit - fireEvent.click(screen.getAllByRole('button', { name: /edit/i })[0]); + fireEvent.click(screen.getByText('Edit').closest('button')!); // Since autocomplete already has default value, let's just save const saveBtn = screen.getAllByRole('button', { name: /save/i })[0]; - fireEvent.click(saveBtn); + await act(async () => { fireEvent.submit(saveBtn.closest('form')!); }); await waitFor(() => { expect(mockGetLabels).toHaveBeenCalledWith('test-guid-123', ['Label1', 'Label2']); @@ -141,11 +141,11 @@ describe('Labels Component', () => { render(); // Click Edit - fireEvent.click(screen.getAllByRole('button', { name: /edit/i })[0]); + fireEvent.click(screen.getByText('Edit').closest('button')!); // Save const saveBtn = screen.getAllByRole('button', { name: /save/i })[0]; - fireEvent.click(saveBtn); + await act(async () => { fireEvent.submit(saveBtn.closest('form')!); }); await waitFor(() => { expect(mockGetLabels).toHaveBeenCalled(); @@ -162,7 +162,7 @@ describe('Labels Component', () => { render(); // Click Edit - fireEvent.click(screen.getAllByRole('button', { name: /edit/i })[0]); + fireEvent.click(screen.getByText('Edit').closest('button')!); const input = await screen.findByPlaceholderText('Select Label'); fireEvent.mouseDown(input); diff --git a/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/__tests__/UserDefinedProperties.test.tsx b/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/__tests__/UserDefinedProperties.test.tsx index 05b32b6ab87..151f7e19b9f 100644 --- a/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/__tests__/UserDefinedProperties.test.tsx +++ b/dashboard/src/views/DetailPage/EntityDetailTabs/PropertiesTab/__tests__/UserDefinedProperties.test.tsx @@ -16,7 +16,7 @@ */ import React from 'react'; -import { render, screen, fireEvent, waitFor } from '@utils/test-utils'; +import { render, screen, fireEvent, waitFor, act } from '@utils/test-utils'; import userEvent from '@testing-library/user-event'; import '@testing-library/jest-dom'; import UserDefinedProperties from '../UserDefinedProperties'; @@ -103,7 +103,7 @@ describe('UserDefinedProperties Component', () => { it('switches to edit mode on Edit button click', () => { render(); - const editBtn = screen.getByRole('button', { name: /edit/i }); + const editBtn = screen.getByText('Edit').closest('button')!; fireEvent.click(editBtn); expect(screen.getByDisplayValue('key1')).toBeInTheDocument(); @@ -114,7 +114,7 @@ describe('UserDefinedProperties Component', () => { render(); // Edit mode - fireEvent.click(screen.getAllByRole('button', { name: /edit/i })[0]); + fireEvent.click(screen.getByText('Edit').closest('button')!); // Should have 2 inputs initially for keys let keyInputs = screen.getAllByPlaceholderText('key'); @@ -138,7 +138,7 @@ describe('UserDefinedProperties Component', () => { it('validates unique keys', async () => { render(); - fireEvent.click(screen.getAllByRole('button', { name: /edit/i })[0]); + fireEvent.click(screen.getByText('Edit').closest('button')!); const keyInputs = screen.getAllByPlaceholderText('key'); // Change second key to 'key1' to cause duplicate @@ -146,10 +146,10 @@ describe('UserDefinedProperties Component', () => { // Form submission const saveBtn = screen.getAllByRole('button', { name: /save/i })[0]; - fireEvent.click(saveBtn); + await act(async () => { fireEvent.submit(saveBtn.closest('form')!); }); await waitFor(() => { - expect(screen.getByText('Key must be unique')).toBeInTheDocument(); + expect(screen.getAllByText('Key must be unique')[0]).toBeInTheDocument(); expect(mockCreateEntity).not.toHaveBeenCalled(); }); }); @@ -159,10 +159,10 @@ describe('UserDefinedProperties Component', () => { render(); - fireEvent.click(screen.getAllByRole('button', { name: /edit/i })[0]); + fireEvent.click(screen.getByText('Edit').closest('button')!); const saveBtn = screen.getAllByRole('button', { name: /save/i })[0]; - fireEvent.click(saveBtn); + await act(async () => { fireEvent.submit(saveBtn.closest('form')!); }); await waitFor(() => { expect(mockCreateEntity).toHaveBeenCalledWith({ @@ -179,10 +179,10 @@ describe('UserDefinedProperties Component', () => { render(); - fireEvent.click(screen.getAllByRole('button', { name: /edit/i })[0]); + fireEvent.click(screen.getByText('Edit').closest('button')!); const saveBtn = screen.getAllByRole('button', { name: /save/i })[0]; - fireEvent.click(saveBtn); + await act(async () => { fireEvent.submit(saveBtn.closest('form')!); }); await waitFor(() => { expect(mockCreateEntity).toHaveBeenCalled();