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
The following commit(s) were added to refs/heads/ATLAS-5246-v1 by this push:
new a8464fd95 ATLAS-5288: ATLAS UI: Dashboard - Classification and Admin
Audits (#621)
a8464fd95 is described below
commit a8464fd95fac7ff119774119165bcb976101ed7e
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{" "}