This is an automated email from the ASF dual-hosted git repository. pawarprasad123 pushed a commit to branch ATLAS-5246-v1 in repository https://gitbox.apache.org/repos/asf/atlas.git
commit fb7cdcd08c0d23d9ff3fcbd8d6694e3f9a1912fe Author: Prasad Pawar <[email protected]> AuthorDate: Fri May 8 13:00:16 2026 +0530 ATLAS-5288: ATLAS UI: Dashboard - Classification and Admin Audits (#621) --- .../src/components/TypeDefAuditDetailModal.tsx | 108 +++++++++++++++++++++ dashboard/src/utils/auditTypeDefUtils.ts | 90 +++++++++++++++++ .../views/Administrator/AdministratorLayout.tsx | 22 ++++- .../views/Administrator/Audits/AuditResults.tsx | 78 ++------------- dashboard/src/views/Classification/AddTag.tsx | 1 + .../src/views/Classification/AddTagAttributes.tsx | 1 + .../views/Classification/ClassificationForm.tsx | 5 +- dashboard/src/views/Classification/DeleteTag.tsx | 1 + .../EntityDetailTabs/ClassificationsTab.tsx | 6 ++ 9 files changed, 240 insertions(+), 72 deletions(-) diff --git a/dashboard/src/components/TypeDefAuditDetailModal.tsx b/dashboard/src/components/TypeDefAuditDetailModal.tsx new file mode 100644 index 000000000..1e780d6fb --- /dev/null +++ b/dashboard/src/components/TypeDefAuditDetailModal.tsx @@ -0,0 +1,108 @@ +/* + * 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 { Divider, Stack } from "@mui/material"; +import CustomModal from "@components/Modal"; +import { getValues } from "@components/commonComponents"; +import { StyledPaper } from "@utils/Muiutils"; +import { category } from "@utils/Enum"; +import { isArray, isEmpty } from "@utils/Utils"; + +export interface TypeDefAuditDetail { + category?: string; + name?: string; + [key: string]: unknown; +} + +interface TypeDefAuditDetailModalProps { + open: boolean; + onClose: () => void; + /** Full type-definition payload from audit `result` (same shape as audit tab click). */ + detailObject: TypeDefAuditDetail | Record<string, unknown> | null; + maxWidth?: "xs" | "sm" | "md" | "lg" | "xl"; +} + +const buildTitle = (detailObject: TypeDefAuditDetailModalProps["detailObject"]): string => { + if (isEmpty(detailObject)) return "Type Details"; + const cat = detailObject?.category; + const name = detailObject?.name; + if (cat != null && name != null && category[String(cat)]) { + return `${category[String(cat)]} Type Details: ${String(name)}`; + } + if (name != null) return `Type Details: ${String(name)}`; + return "Type Details"; +}; + +export const TypeDefAuditDetailModal = ({ + open, + onClose, + detailObject, + maxWidth = "md", +}: TypeDefAuditDetailModalProps) => { + return ( + <CustomModal + open={open} + onClose={onClose} + title={buildTitle(detailObject)} + footer={false} + button1Handler={undefined} + button2Handler={undefined} + maxWidth={maxWidth} + > + <StyledPaper variant="outlined"> + {!isEmpty(detailObject) + ? Object.entries(detailObject as Record<string, unknown>) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([keys, value]) => ( + <div key={keys}> + <Stack direction="row" spacing={4} marginBottom={1} marginTop={1}> + <div + style={{ + flex: 1, + wordBreak: "break-all", + textAlign: "left", + fontWeight: "600", + }} + > + {`${keys} ${isArray(value) ? `(${(value as unknown[]).length})` : ""}`} + </div> + <div + style={{ + flex: 1, + wordBreak: "break-all", + textAlign: "left", + }} + > + {getValues( + value, + undefined, + undefined, + undefined, + "properties" + )} + </div> + </Stack> + <Divider /> + </div> + )) + : "No Record Found"} + </StyledPaper> + </CustomModal> + ); +}; + +export default TypeDefAuditDetailModal; diff --git a/dashboard/src/utils/auditTypeDefUtils.ts b/dashboard/src/utils/auditTypeDefUtils.ts new file mode 100644 index 000000000..c23097578 --- /dev/null +++ b/dashboard/src/utils/auditTypeDefUtils.ts @@ -0,0 +1,90 @@ +/* + * 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 { jsonParse } from "@utils/Utils"; + +/** TYPE_DEF_* audit params that use the same detail modal as the Administrator Audits tab */ +export const TYPE_DEF_DETAIL_MODAL_PARAMS = new Set([ + "ENUM", + "ENTITY", + "STRUCT", + "RELATIONSHIP", +]); + +export interface AuditRecordLike { + operation?: string; + params?: string; + result?: string; +} + +/** + * Pulls the full typedef object from an audit `result` JSON for ENUM / ENTITY / + * STRUCT / RELATIONSHIP rows (matches expanded audit row click payload). + */ +export const extractTypeDefDetailObject = ( + record: AuditRecordLike, + resolvedName: string, + resolvedGuid: string +): Record<string, unknown> | null => { + const op = record.operation; + if ( + op !== "TYPE_DEF_CREATE" && + op !== "TYPE_DEF_UPDATE" && + op !== "TYPE_DEF_DELETE" + ) { + return null; + } + const paramKeys = (record.params ?? "") + .split(",") + .map((k) => k.trim()) + .filter(Boolean); + + try { + const resultObj = + typeof record.result === "string" ? jsonParse(record.result) : record.result; + if (!resultObj || typeof resultObj !== "object") return null; + + const keysToScan = + paramKeys.length > 0 ? paramKeys : Object.keys(resultObj as object); + + for (const key of keysToScan) { + if (!TYPE_DEF_DETAIL_MODAL_PARAMS.has(key)) continue; + const list = (resultObj as Record<string, unknown>)[key]; + if (!Array.isArray(list) || list.length === 0) continue; + const match = list.find((item: { name?: string; guid?: string }) => { + if (!item || typeof item !== "object") return false; + if (resolvedName && item.name === resolvedName) return true; + if (resolvedGuid && item.guid === resolvedGuid) return true; + return false; + }); + if (match && typeof match === "object") { + return match as Record<string, unknown>; + } + } + + const firstKey = paramKeys.find((k) => TYPE_DEF_DETAIL_MODAL_PARAMS.has(k)); + if (firstKey) { + const list = (resultObj as Record<string, unknown>)[firstKey]; + if (Array.isArray(list) && list[0] && typeof list[0] === "object") { + return list[0] as Record<string, unknown>; + } + } + } catch { + /* ignore */ + } + return null; +}; diff --git a/dashboard/src/views/Administrator/AdministratorLayout.tsx b/dashboard/src/views/Administrator/AdministratorLayout.tsx index 302da4313..7e1e2abc8 100644 --- a/dashboard/src/views/Administrator/AdministratorLayout.tsx +++ b/dashboard/src/views/Administrator/AdministratorLayout.tsx @@ -19,7 +19,7 @@ import { LinkTab } from "@components/muiComponents"; import { Stack, Tabs } from "@mui/material"; import { Item, samePageLinkNavigation } from "@utils/Muiutils"; import { isEmpty } from "@utils/Utils"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import { useLocation, useNavigate } from "react-router-dom"; import BusinessMetadataTab from "./BusinessMetadataTab"; import Enumerations from "./Enumerations"; @@ -44,6 +44,26 @@ const AdministratorLayout = () => { !isEmpty(activeTab) ? allTabs.findIndex((val) => val === activeTab) : 0 ); + useEffect(() => { + const tabIndex = !isEmpty(activeTab) + ? allTabs.findIndex((val) => val === activeTab) + : 0; + const resolvedIndex = tabIndex >= 0 ? tabIndex : 0; + setValue(resolvedIndex); + + if (activeTab && activeTab !== "businessMetadata") { + setForm(false); + } + const createParam = searchParams.get("create"); + if (createParam === "true" && activeTab === "businessMetadata") { + setForm(true); + setBMAttribute({}); + const newParams = new URLSearchParams(location.search); + newParams.delete("create"); + navigate({ pathname: "/administrator", search: newParams.toString() }, { replace: true }); + } + }, [activeTab, location.search, navigate]); + const handleChange = (event: React.SyntheticEvent, newValue: number) => { if ( event.type !== "click" || diff --git a/dashboard/src/views/Administrator/Audits/AuditResults.tsx b/dashboard/src/views/Administrator/Audits/AuditResults.tsx index 9deef0525..a287456f7 100644 --- a/dashboard/src/views/Administrator/Audits/AuditResults.tsx +++ b/dashboard/src/views/Administrator/Audits/AuditResults.tsx @@ -15,22 +15,13 @@ * limitations under the License. */ -import { - Divider, - Grid, - Link, - List, - ListItem, - ListItemText, - Stack, - Typography, -} from "@mui/material"; +import { Grid, Link, List, ListItem, ListItemText, Typography } from "@mui/material"; import { auditAction, category } from "@utils/Enum"; -import { isArray, isEmpty, jsonParse } from "@utils/Utils"; +import { isEmpty, jsonParse } from "@utils/Utils"; import CustomModal from "@components/Modal"; +import TypeDefAuditDetailModal from "@components/TypeDefAuditDetailModal"; import { useState } from "react"; -import { getValues } from "@components/commonComponents"; -import { Item, StyledPaper } from "@utils/Muiutils"; +import { Item } from "@utils/Muiutils"; import AuditsTab from "@views/DetailPage/EntityDetailTabs/AuditsTab"; import ImportExportAudits from "./ImportExportAudits"; @@ -196,65 +187,12 @@ const AuditResults = ({ componentProps, row }: any) => { <ImportExportAudits auditObj={auditObj} /> )} - <CustomModal + <TypeDefAuditDetailModal open={openModal} onClose={handleCloseModal} - title={`${category[currentResultObj.category]} Type Details: ${ - currentResultObj.name - }`} - footer={false} - button1Handler={undefined} - button2Handler={undefined} - > - <StyledPaper variant="outlined"> - {" "} - {!isEmpty(currentResultObj) - ? Object.entries(currentResultObj) - .sort() - .map(([keys, value]: [string, any]) => { - return ( - <> - <Stack - direction="row" - spacing={4} - marginBottom={1} - marginTop={1} - > - <div - style={{ - flex: 1, - wordBreak: "break-all", - textAlign: "left", - fontWeight: "600", - }} - > - {`${keys} ${ - isArray(value) ? `(${value.length})` : "" - }`} - </div> - <div - style={{ - flex: 1, - wordBreak: "break-all", - textAlign: "left", - }} - > - {getValues( - value, - undefined, - undefined, - undefined, - "properties" - )} - </div> - </Stack> - <Divider /> - </> - ); - }) - : "No Record Found"} - </StyledPaper> - </CustomModal> + detailObject={currentResultObj} + maxWidth="sm" + /> {(operation == "PURGE" || operation == "AUTO_PURGE") && ( <CustomModal diff --git a/dashboard/src/views/Classification/AddTag.tsx b/dashboard/src/views/Classification/AddTag.tsx index f06fbc6bb..1ec08ac9f 100644 --- a/dashboard/src/views/Classification/AddTag.tsx +++ b/dashboard/src/views/Classification/AddTag.tsx @@ -381,6 +381,7 @@ const AddTag = (props: { disableButton2={ isEmpty(classificationData) ? true : isSubmitting } + button2Loading={isSubmitting} isDirty={ isEmpty(classificationData) ? false diff --git a/dashboard/src/views/Classification/AddTagAttributes.tsx b/dashboard/src/views/Classification/AddTagAttributes.tsx index 107045a17..34d4392e8 100644 --- a/dashboard/src/views/Classification/AddTagAttributes.tsx +++ b/dashboard/src/views/Classification/AddTagAttributes.tsx @@ -145,6 +145,7 @@ const AddTagAttributes = ({ open, onClose }: any) => { button2Label="Add" button2Handler={handleSubmit(onSubmit)} disableButton2={isSubmitting} + button2Loading={isSubmitting} > <form onSubmit={handleSubmit(onSubmit)}> {/* <TagAtrributes control={control} /> */} diff --git a/dashboard/src/views/Classification/ClassificationForm.tsx b/dashboard/src/views/Classification/ClassificationForm.tsx index 170f9f16c..943cabdb6 100644 --- a/dashboard/src/views/Classification/ClassificationForm.tsx +++ b/dashboard/src/views/Classification/ClassificationForm.tsx @@ -48,6 +48,7 @@ import { paramsType } from "@models/detailPageType"; import TagAtrributes from "./TagAttributes"; import { fetchClassificationData } from "@redux/slice/typeDefSlices/typedefClassificationSlice"; import { AntSwitch } from "@utils/Muiutils"; +import { refreshDashboardHomeData } from "@utils/refreshDashboardHome"; const ClassificationForm = ({ open, @@ -202,7 +203,8 @@ const ClassificationForm = ({ isAdd ? "created" : "updated" } successfully` ); - fetchInitialData(); + await fetchInitialData(); + refreshDashboardHomeData(dispatchApi); } catch (error) { console.error( `Error while ${isAdd ? "creating" : "updating"} classification`, @@ -223,6 +225,7 @@ const ClassificationForm = ({ button1Handler={onClose} button2Label={isAdd ? "Create" : "Save"} disableButton2={isSubmitting} + button2Loading={isSubmitting} isDirty={isDirty} maxWidth="sm" button2Handler={handleSubmit(onSubmit)} diff --git a/dashboard/src/views/Classification/DeleteTag.tsx b/dashboard/src/views/Classification/DeleteTag.tsx index 71b4448f6..4bc2d7a19 100644 --- a/dashboard/src/views/Classification/DeleteTag.tsx +++ b/dashboard/src/views/Classification/DeleteTag.tsx @@ -77,6 +77,7 @@ const DeleteTag = (props: { maxWidth="sm" button2Handler={handleRemove} disableButton2={loader} + button2Loading={loader} > <Typography fontSize={15}> Are you sure you want to delete classification diff --git a/dashboard/src/views/DetailPage/EntityDetailTabs/ClassificationsTab.tsx b/dashboard/src/views/DetailPage/EntityDetailTabs/ClassificationsTab.tsx index c898692c5..0fdd395f6 100644 --- a/dashboard/src/views/DetailPage/EntityDetailTabs/ClassificationsTab.tsx +++ b/dashboard/src/views/DetailPage/EntityDetailTabs/ClassificationsTab.tsx @@ -78,6 +78,7 @@ const ClassificationsTab: React.FC<EntityDetailTabProps> = ({ ); const [openModal, setOpenModal] = useState<boolean>(false); + const [removeAssignmentLoading, setRemoveAssignmentLoading] = useState(false); const [tagModal, setTagModal] = useState<boolean>(false); const [updateTable, setUpdateTable] = useState(moment.now()); const [rowdata, setRowData] = useState(); @@ -129,6 +130,7 @@ const ClassificationsTab: React.FC<EntityDetailTabProps> = ({ const handleRemove = async () => { try { + setRemoveAssignmentLoading(true); await removeClassification(guid, currentValue.selectedValue); if (!isEmpty(guid)) { dispatchApi(fetchDetailPageData(guid as string)); @@ -145,6 +147,8 @@ const ClassificationsTab: React.FC<EntityDetailTabProps> = ({ setOpenModal(false); } catch (error) { serverError(error, toastId); + } finally { + setRemoveAssignmentLoading(false); } }; @@ -381,6 +385,8 @@ const ClassificationsTab: React.FC<EntityDetailTabProps> = ({ button1Handler={handleCloseModal} button2Label="Remove" button2Handler={handleRemove} + disableButton2={removeAssignmentLoading} + button2Loading={removeAssignmentLoading} > <Typography fontSize={15}> Remove: <b>{currentValue.selectedValue}</b> assignment from{" "}
