You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@atlas.apache.org by pp...@apache.org on 2026/05/08 09:30:04 UTC

(atlas) 03/08: ATLAS-5267: ATLAS UI: Create Glossary: Add Import glossary terms tab (template + upload) (#617)

This is an automated email from the ASF dual-hosted git repository.

pawarprasad123 pushed a commit to branch feature/pr-9-License-added-forNewfile
in repository https://gitbox.apache.org/repos/asf/atlas.git

commit 9ce5cb57ca5f4cdab8a7428b101857d2cbc47de5
Author: Prasad Pawar <49...@users.noreply.github.com>
AuthorDate: Fri May 8 12:54:08 2026 +0530

    ATLAS-5267: ATLAS UI: Create Glossary: Add Import glossary terms tab (template + upload) (#617)
---
 dashboard/src/components/ImportDialog.tsx          |  44 +-
 dashboard/src/utils/apiErrorToastMessage.ts        |  43 ++
 dashboard/src/utils/glossaryImportFlow.ts          |  57 +++
 .../DetailPage/GlossaryDetails/TermRelation.tsx    |   1 +
 .../src/views/Glossary/AddUpdateCategoryForm.tsx   |   1 +
 .../src/views/Glossary/AddUpdateGlossaryForm.tsx   | 450 ++++++++++++++++-----
 dashboard/src/views/Glossary/AddUpdateTermForm.tsx |   1 +
 dashboard/src/views/Glossary/AssignCategory.tsx    |   1 +
 dashboard/src/views/Glossary/AssignTerm.tsx        |   1 +
 dashboard/src/views/Glossary/DeleteGlossary.tsx    |  52 +--
 10 files changed, 517 insertions(+), 134 deletions(-)

diff --git a/dashboard/src/components/ImportDialog.tsx b/dashboard/src/components/ImportDialog.tsx
index a3adfbb54..10d82aa1d 100644
--- a/dashboard/src/components/ImportDialog.tsx
+++ b/dashboard/src/components/ImportDialog.tsx
@@ -37,7 +37,8 @@ import List from "@mui/material/List";
 import ListItem from "@mui/material/ListItem";
 import ListItemText from "@mui/material/ListItemText";
 import ArrowBackIosNewIcon from "@mui/icons-material/ArrowBackIosNew";
-import { getGlossaryImport } from "../api/apiMethods/glossaryApiMethod";
+import { postGlossaryImportFormData } from "@utils/glossaryImportFlow";
+import { getApiErrorToastMessage } from "@utils/apiErrorToastMessage";
 
 const BootstrapDialog = styled(Dialog)(({ theme }) => ({
   "& .MuiDialogContent-root": {
@@ -77,22 +78,27 @@ export const ImportDialog: React.FC<CustomModalProps> = ({
   const onUpload = async () => {
     if (fileData) {
       try {
-        let apiMethod =
+        const onProgress = (progressValue: number) => {
+          setProgress(progressValue);
+        };
+        const importResp =
           title == "Import Business Metadata"
-            ? getBusinessMetadataImport
-            : getGlossaryImport;
-        let formData = new FormData();
-        formData.append("file", fileData);
-        const importResp = await apiMethod(formData, {
-          onUploadProgress: (progressEvent: {
-            loaded: number;
-            total: number;
-          }) => {
-            let progressValue =
-              (progressEvent.loaded / progressEvent.total) * 100;
-            setProgress(progressValue);
-          }
-        });
+            ? await (async () => {
+                const formData = new FormData();
+                formData.append("file", fileData);
+                return getBusinessMetadataImport(formData, {
+                  onUploadProgress: (progressEvent: {
+                    loaded: number;
+                    total: number;
+                  }) => {
+                    if (!progressEvent.total) return;
+                    onProgress(
+                      (progressEvent.loaded / progressEvent.total) * 100
+                    );
+                  }
+                });
+              })()
+            : await postGlossaryImportFormData(fileData, onProgress);
 
         if (importResp.data.failedImportInfoList == undefined) {
           toast.dismiss(toastId.current);
@@ -114,8 +120,12 @@ export const ImportDialog: React.FC<CustomModalProps> = ({
         }
         setImportData(importResp.data);
       } catch (error) {
+        const message = getApiErrorToastMessage(error);
+        if (message === null) {
+          return;
+        }
         toast.dismiss(toastId.current);
-        toastId.current = toast.error(`Invalid JSON response from server`);
+        toastId.current = toast.error(message);
       }
     }
   };
diff --git a/dashboard/src/utils/apiErrorToastMessage.ts b/dashboard/src/utils/apiErrorToastMessage.ts
new file mode 100644
index 000000000..7afa98d0b
--- /dev/null
+++ b/dashboard/src/utils/apiErrorToastMessage.ts
@@ -0,0 +1,43 @@
+/*
+ * 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.
+ */
+
+/**
+ * Resolves user-visible toast text from an axios/fetchApi error.
+ * @returns null when the caller must not toast (403 is handled in fetchApi).
+ */
+export const getApiErrorToastMessage = (error: unknown): string | null => {
+	const er = error as {
+		response?: { status?: number; data?: unknown };
+	};
+	if (er?.response?.status === 403) {
+		return null;
+	}
+	const data = er?.response?.data;
+	if (data !== null && data !== undefined && typeof data === "object") {
+		const o = data as Record<string, unknown>;
+		if (o.errorMessage != null && String(o.errorMessage).trim() !== "") {
+			return String(o.errorMessage);
+		}
+		if (o.msgDesc != null && String(o.msgDesc).trim() !== "") {
+			return String(o.msgDesc);
+		}
+	}
+	if (typeof data === "string" && data.trim() !== "") {
+		return data;
+	}
+	return "Invalid JSON response from server";
+};
diff --git a/dashboard/src/utils/glossaryImportFlow.ts b/dashboard/src/utils/glossaryImportFlow.ts
new file mode 100644
index 000000000..a3f564050
--- /dev/null
+++ b/dashboard/src/utils/glossaryImportFlow.ts
@@ -0,0 +1,57 @@
+/*
+ * 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 {
+	getGlossaryImport,
+	getGlossaryImportTmpl
+} from '@api/apiMethods/glossaryApiMethod';
+
+/**
+ * Same behavior as glossary branch of SideBarTree.downloadFile.
+ */
+export const downloadGlossaryImportTemplate = async (): Promise<void> => {
+	const apiResp = await getGlossaryImportTmpl({});
+	const text =
+		apiResp && typeof apiResp.data !== 'undefined'
+			? String(apiResp.data)
+			: '';
+	const blob = new Blob([text], { type: 'text/plain' });
+	const url = window.URL.createObjectURL(blob);
+	const link = document.createElement('a');
+	link.href = url;
+	link.setAttribute('download', 'template');
+	document.body.appendChild(link);
+	link.click();
+	document.body.removeChild(link);
+	window.URL.revokeObjectURL(url);
+};
+
+export const postGlossaryImportFormData = (
+	file: File,
+	onUploadProgress?: (progressPercent: number) => void
+) => {
+	const formData = new FormData();
+	formData.append('file', file);
+	return getGlossaryImport(formData, {
+		onUploadProgress: (progressEvent: { loaded: number; total: number }) => {
+			if (!onUploadProgress || !progressEvent.total) return;
+			const progressValue =
+				(progressEvent.loaded / progressEvent.total) * 100;
+			onUploadProgress(progressValue);
+		}
+	});
+};
diff --git a/dashboard/src/views/DetailPage/GlossaryDetails/TermRelation.tsx b/dashboard/src/views/DetailPage/GlossaryDetails/TermRelation.tsx
index ba1381528..68d069ea7 100644
--- a/dashboard/src/views/DetailPage/GlossaryDetails/TermRelation.tsx
+++ b/dashboard/src/views/DetailPage/GlossaryDetails/TermRelation.tsx
@@ -219,6 +219,7 @@ const TermRelation = ({ glossaryTypeData }: any) => {
           button2Label={editModal ? "Update" : "Close"}
           button2Handler={editModal ? handleSubmit(onSubmit) : handleCloseModal}
           disableButton2={isSubmitting}
+          button2Loading={isSubmitting}
           maxWidth="md"
         >
           <form onSubmit={handleSubmit(onSubmit)}>
diff --git a/dashboard/src/views/Glossary/AddUpdateCategoryForm.tsx b/dashboard/src/views/Glossary/AddUpdateCategoryForm.tsx
index 12a80b6de..bd9084861 100644
--- a/dashboard/src/views/Glossary/AddUpdateCategoryForm.tsx
+++ b/dashboard/src/views/Glossary/AddUpdateCategoryForm.tsx
@@ -146,6 +146,7 @@ const AddUpdateCategoryForm = (props: {
         button1Handler={onClose}
         button2Label={isAdd ? "Create" : "Update"}
         disableButton2={isSubmitting}
+        button2Loading={isSubmitting}
         maxWidth="sm"
         button2Handler={handleSubmit(onSubmit)}
       >
diff --git a/dashboard/src/views/Glossary/AddUpdateGlossaryForm.tsx b/dashboard/src/views/Glossary/AddUpdateGlossaryForm.tsx
index 4aa2248d1..5f4a22854 100644
--- a/dashboard/src/views/Glossary/AddUpdateGlossaryForm.tsx
+++ b/dashboard/src/views/Glossary/AddUpdateGlossaryForm.tsx
@@ -19,108 +19,370 @@ import CustomModal from "@components/Modal";
 import GlossaryForm from "./GlossaryForm";
 import { useForm } from "react-hook-form";
 import {
-  createGlossary,
-  editGlossary
+	createGlossary,
+	editGlossary
 } from "@api/apiMethods/glossaryApiMethod";
 import { isEmpty, serverError } from "@utils/Utils";
+import {
+	downloadGlossaryImportTemplate,
+	postGlossaryImportFormData
+} from "@utils/glossaryImportFlow";
+import { getApiErrorToastMessage } from "@utils/apiErrorToastMessage";
 import { toast } from "react-toastify";
-import { useRef } from "react";
+import { useCallback, useEffect, useRef, useState } from "react";
+import type { MouseEvent } from "react";
 import { useAppDispatch, useAppSelector } from "@hooks/reducerHook";
 import { fetchGlossaryData } from "@redux/slice/glossarySlice";
+import {
+	Button,
+	IconButton,
+	List,
+	ListItem,
+	ListItemText,
+	Stack,
+	ToggleButton,
+	ToggleButtonGroup
+} from "@mui/material";
+import FileDownloadIcon from "@mui/icons-material/FileDownload";
+import ArrowBackIosNewIcon from "@mui/icons-material/ArrowBackIosNew";
+import ImportLayout from "@views/SideBar/Import/ImportLayout";
+import { LightTooltip } from "@components/muiComponents";
+
+type GlossaryCreateTab = "create" | "import";
+
+const hasSelectedImportFile = (fileData: unknown): boolean => {
+	if (fileData == null) return false;
+	if (fileData instanceof File) return true;
+	if (typeof fileData === "object" && Object.keys(fileData as object).length > 0)
+		return true;
+	return false;
+};
 
 const AddUpdateGlossaryForm = (props: {
-  open: any;
-  onClose: any;
-  isAdd: any;
-  node: Record<string, any> | undefined;
+	open: any;
+	onClose: any;
+	isAdd: any;
+	node: Record<string, any> | undefined;
 }) => {
-  const { open, onClose, isAdd, node } = props;
-  const dispatch = useAppDispatch();
-  const toastId: any = useRef(null);
-  const { glossaryData }: any = useAppSelector((state: any) => state.glossary);
-  const { id } = node || {};
-  let defaultValue: Record<string, string> = {};
-  let glossaryObj: Record<string, string> = {};
-  if (!isAdd) {
-    glossaryObj = glossaryData.find((obj: { name: string }) => {
-      return obj.name == id;
-    });
-    const { name, shortDescription, longDescription } = glossaryObj || {};
-
-    defaultValue["name"] = name;
-    defaultValue["shortDescription"] = shortDescription;
-    defaultValue["longDescription"] = longDescription;
-  }
-  const {
-    control,
-    handleSubmit,
-    setValue,
-    formState: { isSubmitting }
-  } = useForm({
-    defaultValues: isAdd ? {} : defaultValue,
-    mode: "onChange",
-    shouldUnregister: true
-  });
-
-  const onSubmit = async (formValues: any) => {
-    let formData = { ...formValues };
-    const { guid, qualifiedName } = glossaryObj;
-    const {
-      name,
-      shortDescription,
-      longDescription
-    }: { name: string; shortDescription: string; longDescription: string } =
-      formData;
-    let data: Record<string, string> = {};
-    if (!isAdd) {
-      data["guid"] = guid;
-      data["qualifiedName"] = qualifiedName;
-    }
-    data["name"] = name;
-    data["shortDescription"] = !isEmpty(shortDescription)
-      ? shortDescription
-      : "";
-    data["longDescription"] = !isEmpty(longDescription) ? longDescription : "";
-
-    try {
-      if (isAdd) {
-        await createGlossary(data);
-      } else {
-        await editGlossary(guid, data);
-      }
-
-      await dispatch(fetchGlossaryData());
-      toast.dismiss(toastId.current);
-      toastId.current = toast.success(
-        `Glossary ${name} was ${isAdd ? "created" : "updated"} successfully`
-      );
-      onClose();
-    } catch (error) {
-      serverError(error, toastId);
-    }
-  };
-
-  return (
-    <>
-      <CustomModal
-        open={open}
-        onClose={onClose}
-        title={isAdd ? "Create Glossary" : "Edit Glossary"}
-        button1Label="Cancel"
-        button1Handler={onClose}
-        button2Label={isAdd ? "Create" : "Update"}
-        maxWidth="sm"
-        button2Handler={handleSubmit(onSubmit)}
-        disableButton2={isSubmitting}
-      >
-        <GlossaryForm
-          control={control}
-          handleSubmit={handleSubmit(onSubmit)}
-          setValue={setValue}
-        />
-      </CustomModal>
-    </>
-  );
+	const { open, onClose, isAdd, node } = props;
+	const dispatch = useAppDispatch();
+	const toastId: any = useRef(null);
+	const { glossaryData }: any = useAppSelector((state: any) => state.glossary);
+	const { id } = node || {};
+	let defaultValue: Record<string, string> = {};
+	let glossaryObj: Record<string, string> = {};
+	if (!isAdd) {
+		glossaryObj = glossaryData.find((obj: { name: string }) => {
+			return obj.name == id;
+		});
+		const { name, shortDescription, longDescription } = glossaryObj || {};
+
+		defaultValue["name"] = name;
+		defaultValue["shortDescription"] = shortDescription;
+		defaultValue["longDescription"] = longDescription;
+	}
+	const {
+		control,
+		handleSubmit,
+		setValue,
+		formState: { isSubmitting }
+	} = useForm({
+		defaultValues: isAdd ? {} : defaultValue,
+		mode: "onChange",
+		shouldUnregister: true
+	});
+
+	const [glossaryCreateTab, setGlossaryCreateTab] =
+		useState<GlossaryCreateTab>("create");
+	const [importFileData, setImportFileData] = useState<any>([]);
+	const [importProgress, setImportProgress] = useState(0);
+	const [importErrorDetails, setImportErrorDetails] = useState(false);
+	const [importData, setImportData] = useState<any>(null);
+	const [importUploading, setImportUploading] = useState(false);
+
+	const resetImportUi = useCallback(() => {
+		setGlossaryCreateTab("create");
+		setImportFileData([]);
+		setImportProgress(0);
+		setImportErrorDetails(false);
+		setImportData(null);
+		setImportUploading(false);
+	}, []);
+
+	const handleModalClose = useCallback(() => {
+		resetImportUi();
+		onClose();
+	}, [onClose, resetImportUi]);
+
+	useEffect(() => {
+		if (!open) {
+			resetImportUi();
+		}
+	}, [open, resetImportUi]);
+
+	const handleGlossaryCreateTabChange = (
+		_event: MouseEvent<HTMLElement>,
+		next: GlossaryCreateTab | null
+	) => {
+		if (next == null) return;
+		setGlossaryCreateTab(next);
+		if (next === "create") {
+			setImportFileData([]);
+			setImportProgress(0);
+			setImportErrorDetails(false);
+			setImportData(null);
+		}
+	};
+
+	const handleDownloadTemplate = async () => {
+		try {
+			await downloadGlossaryImportTemplate();
+		} catch {
+			toast.dismiss(toastId.current);
+			toastId.current = toast.error("Could not download template");
+		}
+	};
+
+	const handleImportUpload = async () => {
+		if (!hasSelectedImportFile(importFileData)) return;
+		setImportUploading(true);
+		try {
+			const importResp = await postGlossaryImportFormData(
+				importFileData,
+				(pct) => setImportProgress(pct)
+			);
+			if (importResp.data.failedImportInfoList == undefined) {
+				toast.dismiss(toastId.current);
+				toastId.current = toast.success(
+					`File: ${importFileData.name} imported successfully`
+				);
+				await dispatch(fetchGlossaryData());
+				handleModalClose();
+				return;
+			}
+			if (importResp.data.failedImportInfoList != undefined) {
+				toast.dismiss(toastId.current);
+				toastId.current = toast.error(
+					importResp.data.failedImportInfoList[0].remarks
+				);
+				setImportErrorDetails(true);
+			}
+			setImportData(importResp.data);
+		} catch (error) {
+			const message = getApiErrorToastMessage(error);
+			if (message === null) {
+				return;
+			}
+			toast.dismiss(toastId.current);
+			toastId.current = toast.error(message);
+		} finally {
+			setImportUploading(false);
+		}
+	};
+
+	const onSubmit = async (formValues: any) => {
+		let formData = { ...formValues };
+		const { guid, qualifiedName } = glossaryObj;
+		const {
+			name,
+			shortDescription,
+			longDescription
+		}: { name: string; shortDescription: string; longDescription: string } =
+			formData;
+		let data: Record<string, string> = {};
+		if (!isAdd) {
+			data["guid"] = guid;
+			data["qualifiedName"] = qualifiedName;
+		}
+		data["name"] = name;
+		data["shortDescription"] = !isEmpty(shortDescription)
+			? shortDescription
+			: "";
+		data["longDescription"] = !isEmpty(longDescription) ? longDescription : "";
+
+		try {
+			if (isAdd) {
+				await createGlossary(data);
+			} else {
+				await editGlossary(guid, data);
+			}
+
+			await dispatch(fetchGlossaryData());
+			toast.dismiss(toastId.current);
+			toastId.current = toast.success(
+				`Glossary ${name} was ${isAdd ? "created" : "updated"} successfully`
+			);
+			handleModalClose();
+		} catch (error) {
+			serverError(error, toastId);
+		}
+	};
+
+	const showImportError = isAdd && glossaryCreateTab === "import" && importErrorDetails;
+
+	const modalTitle = showImportError
+		? "Error Details"
+		: isAdd
+			? "Create Glossary"
+			: "Edit Glossary";
+
+	const titleIconNode =
+		showImportError ? (
+			<IconButton
+				aria-label="Back to import file"
+				onClick={(e) => {
+					e.stopPropagation();
+					setImportErrorDetails(false);
+				}}
+				size="small"
+				sx={{ color: (theme) => theme.palette.grey[500] }}
+			>
+				<LightTooltip title="Back to import file">
+					<ArrowBackIosNewIcon sx={{ fontSize: "1.25rem" }} />
+				</LightTooltip>
+			</IconButton>
+		) : undefined;
+
+	const isImportMode = isAdd && glossaryCreateTab === "import" && !importErrorDetails;
+
+	const button2Label = (() => {
+		if (!isAdd) return "Update";
+		if (glossaryCreateTab === "create") return "Create";
+		if (importErrorDetails) return "";
+		return "Upload";
+	})();
+
+	const button2Handler = (() => {
+		if (!isAdd || glossaryCreateTab === "create") return handleSubmit(onSubmit);
+		if (importErrorDetails) return () => {};
+		return handleImportUpload;
+	})();
+
+	const disableButton2 = (() => {
+		if (!isAdd) return isSubmitting;
+		if (glossaryCreateTab === "create") return isSubmitting;
+		if (importErrorDetails) return true;
+		return !hasSelectedImportFile(importFileData);
+	})();
+
+	const button2Loading =
+		isAdd && glossaryCreateTab === "import" && !importErrorDetails
+			? importUploading
+			: isSubmitting;
+
+	const hideButton2 = Boolean(
+		isAdd && glossaryCreateTab === "import" && importErrorDetails
+	);
+
+	return (
+		<>
+			<CustomModal
+				open={open}
+				onClose={handleModalClose}
+				title={modalTitle}
+				titleIcon={titleIconNode}
+				button1Label="Cancel"
+				button1Handler={handleModalClose}
+				button2Label={button2Label}
+				maxWidth="sm"
+				button2Handler={button2Handler}
+				disableButton2={disableButton2}
+				button2Loading={button2Loading}
+				hideButton2={hideButton2}
+			>
+				<Stack>
+					{isAdd && (
+						<Stack marginBottom="1rem">
+							<ToggleButtonGroup
+								exclusive
+								value={glossaryCreateTab}
+								onChange={handleGlossaryCreateTabChange}
+								size="small"
+								color="primary"
+								aria-label="Choose create glossary or import glossary terms"
+							>
+								<ToggleButton
+									value="create"
+									className="entity-form-toggle-btn"
+									data-cy="create-glossary-tab"
+								>
+									Create glossary
+								</ToggleButton>
+								<ToggleButton
+									value="import"
+									className="entity-form-toggle-btn"
+									data-cy="import-glossary-terms-tab"
+								>
+									Import glossary terms
+								</ToggleButton>
+							</ToggleButtonGroup>
+						</Stack>
+					)}
+					{(!isAdd || glossaryCreateTab === "create") && (
+						<GlossaryForm
+							control={control}
+							handleSubmit={handleSubmit(onSubmit)}
+							setValue={setValue}
+						/>
+					)}
+					{isImportMode && (
+						<Stack gap={2}>
+							<Button
+								variant="outlined"
+								color="primary"
+								startIcon={<FileDownloadIcon />}
+								onClick={handleDownloadTemplate}
+								aria-label="Download import template"
+							>
+								Download import template
+							</Button>
+							<ImportLayout
+								setFileData={setImportFileData}
+								progressVal={importProgress}
+								setProgress={setImportProgress}
+								selectedFile={
+									importFileData !== undefined &&
+									hasSelectedImportFile(importFileData)
+										? [importFileData]
+										: []
+								}
+								errorDetails={importErrorDetails}
+							/>
+						</Stack>
+					)}
+					{showImportError && importData?.failedImportInfoList && (
+						<Stack
+							sx={{
+								width: "100%",
+								minHeight: 200,
+								maxHeight: 400,
+								bgcolor: "background.paper"
+							}}
+						>
+							<List>
+								{importData.failedImportInfoList.map(
+									(
+										value: {
+											index: number;
+											remarks: string;
+										},
+										index: number
+									) => (
+										<ListItem key={value.index} disableGutters disablePadding>
+											<ListItemText
+												className="dropzone-listitem"
+												primary={`${index + 1}. ${value.remarks}`}
+											/>
+										</ListItem>
+									)
+								)}
+							</List>
+						</Stack>
+					)}
+				</Stack>
+			</CustomModal>
+		</>
+	);
 };
 
 export default AddUpdateGlossaryForm;
diff --git a/dashboard/src/views/Glossary/AddUpdateTermForm.tsx b/dashboard/src/views/Glossary/AddUpdateTermForm.tsx
index feea62032..f5ece0289 100644
--- a/dashboard/src/views/Glossary/AddUpdateTermForm.tsx
+++ b/dashboard/src/views/Glossary/AddUpdateTermForm.tsx
@@ -142,6 +142,7 @@ const AddUpdateTermForm = (props: {
         button1Handler={onClose}
         button2Label={isAdd ? "Create" : "Update"}
         disableButton2={isSubmitting}
+        button2Loading={isSubmitting}
         maxWidth="sm"
         button2Handler={handleSubmit(onSubmit)}
       >
diff --git a/dashboard/src/views/Glossary/AssignCategory.tsx b/dashboard/src/views/Glossary/AssignCategory.tsx
index 9e588ae3c..5a6f8f8b9 100644
--- a/dashboard/src/views/Glossary/AssignCategory.tsx
+++ b/dashboard/src/views/Glossary/AssignCategory.tsx
@@ -288,6 +288,7 @@ const AssignCategory = ({
         maxWidth="sm"
         button2Handler={assignCatgeory}
         disableButton2={loading}
+        button2Loading={loading}
       >
         <Stack>
           <TextField
diff --git a/dashboard/src/views/Glossary/AssignTerm.tsx b/dashboard/src/views/Glossary/AssignTerm.tsx
index 14eceb720..e4c2fc0ed 100644
--- a/dashboard/src/views/Glossary/AssignTerm.tsx
+++ b/dashboard/src/views/Glossary/AssignTerm.tsx
@@ -452,6 +452,7 @@ const AssignTerm = ({
         maxWidth="sm"
         button2Handler={relatedTerm ? handleSubmit(onSubmit) : assignTerm}
         disableButton2={isSubmitting}
+        button2Loading={isSubmitting}
         isDirty={!isEmpty(selectedNode)}
       >
         {relatedTerm ? (
diff --git a/dashboard/src/views/Glossary/DeleteGlossary.tsx b/dashboard/src/views/Glossary/DeleteGlossary.tsx
index 744447494..c4b5dcf60 100644
--- a/dashboard/src/views/Glossary/DeleteGlossary.tsx
+++ b/dashboard/src/views/Glossary/DeleteGlossary.tsx
@@ -26,6 +26,7 @@ import { Typography } from "@mui/material";
 import { fetchGlossaryData } from "@redux/slice/glossarySlice";
 import { isEmpty, serverError } from "@utils/Utils";
 import { useRef } from "react";
+import { useAsyncPending } from "@hooks/useAsyncPending";
 import { useLocation, useNavigate, useParams } from "react-router-dom";
 import { toast } from "react-toastify";
 
@@ -48,36 +49,39 @@ const DeleteGlossary = (props: {
   const navigate = useNavigate();
   const dispatchApi = useAppDispatch();
   const toastId: any = useRef(null);
+  const { pending: deleteInProgress, run: runDelete } = useAsyncPending();
 
   const fetchCurrentData = async () => {
     await dispatchApi(fetchGlossaryData());
   };
 
-  const handleRemove = async () => {
-    try {
-      gtype == "term"
-        ? await deleteGlossaryorType(cGuid)
-        : await deleteGlossaryorTerm(guid);
-      updatedData();
-      fetchCurrentData();
-      toast.success(
-        `${
-          gtype == "term" ? "Term" : "Glossary"
-        } ${id} was deleted successfully`
-      );
-      if (!isEmpty(glossaryGuid) || !isEmpty(glossaryType)) {
-        navigate(
-          {
-            pathname: "/"
-          },
-          { replace: true }
+  const handleRemove = () => {
+    void runDelete(async () => {
+      try {
+        gtype == "term"
+          ? await deleteGlossaryorType(cGuid)
+          : await deleteGlossaryorTerm(guid);
+        updatedData();
+        await fetchCurrentData();
+        toast.success(
+          `${
+            gtype == "term" ? "Term" : "Glossary"
+          } ${id} was deleted successfully`
         );
+        if (!isEmpty(glossaryGuid) || !isEmpty(glossaryType)) {
+          navigate(
+            {
+              pathname: "/"
+            },
+            { replace: true }
+          );
+        }
+        onClose();
+        setExpandNode(null);
+      } catch (error) {
+        serverError(error, toastId);
       }
-      onClose();
-      setExpandNode(null);
-    } catch (error) {
-      serverError(error, toastId);
-    }
+    });
   };
 
   return (
@@ -92,6 +96,8 @@ const DeleteGlossary = (props: {
         button2Label="Ok"
         maxWidth="sm"
         button2Handler={handleRemove}
+        disableButton2={deleteInProgress}
+        button2Loading={deleteInProgress}
       >
         <Typography fontSize={15}>
           Are you sure you want to delete{" "}