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 402a09c2a ATLAS-5256: ATLAS UI: Header - Create Button and Dropdown
Menu. (#618)
402a09c2a is described below
commit 402a09c2a8ce2ef75b98db1068831ae62a6b9273
Author: Prasad Pawar <[email protected]>
AuthorDate: Fri May 8 12:56:21 2026 +0530
ATLAS-5256: ATLAS UI: Header - Create Button and Dropdown Menu. (#618)
* ATLAS-5256: ATLAS UI: Header - Create Button and Dropdown Menu
* ATLAS-5256: ATLAS UI: Header - Create Button and Dropdown Menu
---
.../components/CreateDropdown/CreateDropdown.tsx | 176 +++++++++++++++++++++
dashboard/src/components/CreateDropdown/index.ts | 1 +
dashboard/src/components/QueryBuilder/Filters.tsx | 123 +++++++++++---
.../src/components/ShowMore/DrawerBodyChipView.tsx | 1 +
dashboard/src/components/ShowMore/ShowMoreView.tsx | 3 +
dashboard/src/components/Table/TablePagination.tsx | 120 ++++++++------
dashboard/src/components/TreeNodeIcons.tsx | 42 +++--
dashboard/src/utils/Enum.ts | 9 ++
.../BusinessMetadataAtrributeForm.tsx | 1 +
.../BusinessMetadata/BusinessMetadataForm.tsx | 2 +
.../BusinessMetadataDetailsLayout.tsx | 2 +
.../EntityDetailTabs/PropagationPropertyModal.tsx | 1 +
dashboard/src/views/Entity/EntityForm.tsx | 1 +
dashboard/src/views/SaveFilters/SaveFilters.tsx | 1 +
dashboard/src/views/Statistics/ServerStats.tsx | 7 +-
dashboard/src/views/Statistics/Statistics.tsx | 45 ++++--
16 files changed, 432 insertions(+), 103 deletions(-)
diff --git a/dashboard/src/components/CreateDropdown/CreateDropdown.tsx
b/dashboard/src/components/CreateDropdown/CreateDropdown.tsx
new file mode 100644
index 000000000..cb32d4e2a
--- /dev/null
+++ b/dashboard/src/components/CreateDropdown/CreateDropdown.tsx
@@ -0,0 +1,176 @@
+/*
+ * 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 { useState, useCallback } from "react";
+import { Button, Menu, MenuItem } from "@mui/material";
+import AddIcon from "@mui/icons-material/Add";
+import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
+import CategoryIcon from "@mui/icons-material/Category";
+import LocalOfferIcon from "@mui/icons-material/LocalOffer";
+import MenuBookIcon from "@mui/icons-material/MenuBook";
+import BusinessCenterIcon from "@mui/icons-material/BusinessCenter";
+import ListIcon from "@mui/icons-material/List";
+import { useNavigate } from "react-router-dom";
+import EntityForm from "@views/Entity/EntityForm";
+import ClassificationForm from "@views/Classification/ClassificationForm";
+import AddUpdateGlossaryForm from "@views/Glossary/AddUpdateGlossaryForm";
+const CreateDropdown = () => {
+ const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
+ const [entityModal, setEntityModal] = useState(false);
+ const [classificationModal, setClassificationModal] = useState(false);
+ const [glossaryModal, setGlossaryModal] = useState(false);
+ const navigate = useNavigate();
+
+ const open = Boolean(anchorEl);
+
+ const handleClick = useCallback((event: React.MouseEvent<HTMLElement>)
=> {
+ setAnchorEl(event.currentTarget);
+ }, []);
+
+ const handleClose = useCallback(() => {
+ setAnchorEl(null);
+ }, []);
+
+ const handleEntityClick = useCallback(() => {
+ handleClose();
+ setEntityModal(true);
+ }, [handleClose]);
+
+ const handleClassificationClick = useCallback(() => {
+ handleClose();
+ setClassificationModal(true);
+ }, [handleClose]);
+
+ const handleGlossaryClick = useCallback(() => {
+ handleClose();
+ setGlossaryModal(true);
+ }, [handleClose]);
+
+ const handleBusinessMetadataClick = useCallback(() => {
+ handleClose();
+ navigate({
+ pathname: "/administrator",
+ search: "tabActive=businessMetadata&create=true"
+ });
+ }, [handleClose, navigate]);
+
+ const handleEnumClick = useCallback(() => {
+ handleClose();
+ navigate({
+ pathname: "/administrator",
+ search: "tabActive=enum"
+ });
+ }, [handleClose, navigate]);
+
+ return (
+ <>
+ <Button
+ variant="contained"
+ color="primary"
+ size="small"
+ onClick={handleClick}
+ endIcon={<ExpandMoreIcon />}
+ startIcon={<AddIcon />}
+ aria-controls={open ? "create-menu" : undefined}
+ aria-haspopup="true"
+ aria-expanded={open ? "true" : undefined}
+ data-cy="create-dropdown"
+ >
+ Create
+ </Button>
+ <Menu
+ id="create-menu"
+ anchorEl={anchorEl}
+ open={open}
+ onClose={handleClose}
+ anchorOrigin={{ horizontal: "left", vertical:
"bottom" }}
+ transformOrigin={{ horizontal: "left",
vertical: "top" }}
+ slotProps={{
+ paper: {
+ elevation: 2,
+ sx: { mt: 1.5, minWidth: 200 }
+ }
+ }}
+ >
+ <MenuItem
+ onClick={handleEntityClick}
+ data-cy="create-entity"
+ sx={{ gap: 1.5 }}
+ >
+ <CategoryIcon fontSize="small" />
+ Entity
+ </MenuItem>
+ <MenuItem
+ onClick={handleClassificationClick}
+ data-cy="create-classification"
+ sx={{ gap: 1.5 }}
+ >
+ <LocalOfferIcon fontSize="small" />
+ Classification
+ </MenuItem>
+ <MenuItem
+ onClick={handleGlossaryClick}
+ data-cy="create-glossary"
+ sx={{ gap: 1.5 }}
+ >
+ <MenuBookIcon fontSize="small" />
+ Glossary
+ </MenuItem>
+ <MenuItem
+ onClick={handleBusinessMetadataClick}
+ data-cy="create-business-metadata"
+ sx={{ gap: 1.5 }}
+ >
+ <BusinessCenterIcon fontSize="small" />
+ Business Metadata
+ </MenuItem>
+ <MenuItem
+ onClick={handleEnumClick}
+ data-cy="create-enum"
+ sx={{ gap: 1.5 }}
+ >
+ <ListIcon fontSize="small" />
+ Enum
+ </MenuItem>
+ </Menu>
+
+ {entityModal && (
+ <EntityForm
+ open={entityModal}
+ onClose={() => setEntityModal(false)}
+ />
+ )}
+ {classificationModal && (
+ <ClassificationForm
+ open={classificationModal}
+ isAdd={true}
+ onClose={() =>
setClassificationModal(false)}
+ />
+ )}
+ {glossaryModal && (
+ <AddUpdateGlossaryForm
+ open={glossaryModal}
+ isAdd={true}
+ onClose={() => setGlossaryModal(false)}
+ node={undefined}
+ />
+ )}
+ </>
+ );
+};
+
+export default CreateDropdown;
diff --git a/dashboard/src/components/CreateDropdown/index.ts
b/dashboard/src/components/CreateDropdown/index.ts
new file mode 100644
index 000000000..48cc7382b
--- /dev/null
+++ b/dashboard/src/components/CreateDropdown/index.ts
@@ -0,0 +1 @@
+export { default } from "./CreateDropdown";
diff --git a/dashboard/src/components/QueryBuilder/Filters.tsx
b/dashboard/src/components/QueryBuilder/Filters.tsx
index 2b305510e..d162f28e6 100644
--- a/dashboard/src/components/QueryBuilder/Filters.tsx
+++ b/dashboard/src/components/QueryBuilder/Filters.tsx
@@ -25,7 +25,7 @@ import {
FormControlLabel
} from "@mui/material";
-import { useState } from "react";
+import { useState, useEffect, useMemo } from "react";
import {
Accordion,
AccordionDetails,
@@ -47,6 +47,7 @@ import { useAppSelector } from "@hooks/reducerHook";
import { cloneDeep } from "@utils/Helper";
import { getObjDef } from
"@views/Administrator/Audits/AuditsFilter/AuditFiltersFields";
import { attributeFilter } from "@utils/CommonViewFunction";
+import { getDisplayOperator } from "@utils/Enum";
import moment from "moment";
import RelationshipFilters from "./RelationshipFilters";
import TypeFilters from "./TypeFilters/TypeFilters";
@@ -70,28 +71,66 @@ const Filters = ({
const tagParams = searchParams.get("tag");
const relationshipParams = searchParams.get("relationshipName");
const entityFilterParams = searchParams.get("entityFilters");
- const [checkedEntities, setCheckedEntities] = useState<any>(
- !isEmpty(searchParams.get("includeDE"))
- ? searchParams.get("includeDE")
- : false
+ const [checkedEntities, setCheckedEntities] = useState<boolean>(
+ searchParams.get("includeDE") === "true" || searchParams.get("includeDE")
=== true
);
- const [checkedSubClassifications, setCheckedSubClassifications] =
- useState<any>(
- !isEmpty(searchParams.get("excludeSC"))
- ? searchParams.get("excludeSC")
- : false
- );
- const [checkedSubTypes, setCheckedSubTypes] = useState<any>(
- !isEmpty(searchParams.get("excludeST"))
- ? searchParams.get("excludeST")
- : false
+ const [checkedSubClassifications, setCheckedSubClassifications] =
useState<boolean>(
+ searchParams.get("excludeSC") === "true" || searchParams.get("excludeSC")
=== true
+ );
+ const [checkedSubTypes, setCheckedSubTypes] = useState<boolean>(
+ searchParams.get("excludeST") === "true" || searchParams.get("excludeST")
=== true
);
+ const parseFiltersFromUrl = (params: string | null) => {
+ if (isEmpty(params)) return null;
+ const parsed = attributeFilter.extractUrl({
+ value: params,
+ formatDate: true
+ });
+ if (!parsed?.rules) return null;
+ const rulesArr = Array.isArray(parsed.rules)
+ ? parsed.rules
+ : Object.keys(parsed.rules || {}).map((k) => parsed.rules[k]);
+ const mappedRules = rulesArr
+ .filter((r) => r && !r.condition)
+ .map((r, i) => ({
+ field: r.id,
+ operator: getDisplayOperator(r.operator) || r.operator,
+ value: r.value,
+ id: `url-rule-${i}`
+ }));
+ if (mappedRules.length === 0) return null;
+ return {
+ combinator: (parsed.condition || "AND").toLowerCase(),
+ rules: mappedRules
+ };
+ };
+
+ const entityFiltersFromUrl = parseFiltersFromUrl(entityFilterParams);
+ const tagFilterParams = searchParams.get("tagFilters");
+ const relationshipFilterParams = searchParams.get("relationshipFilters");
+ const tagFiltersFromUrl = parseFiltersFromUrl(tagFilterParams);
+ const relationshipFiltersFromUrl =
parseFiltersFromUrl(relationshipFilterParams);
+
const [typeQuery, setTypeQuery] = useState(
!isEmpty(globalSearchFilterInitialQuery.getQuery()?.entityFilters) &&
!isEmpty(entityFilterParams)
? globalSearchFilterInitialQuery.getQuery()?.entityFilters
- : initialQuery
+ : !isEmpty(entityFiltersFromUrl)
+ ? entityFiltersFromUrl
+ : initialQuery
);
+
+ useEffect(() => {
+ if (
+ !isEmpty(entityFilterParams) &&
+ isEmpty(globalSearchFilterInitialQuery.getQuery()?.entityFilters) &&
+ !isEmpty(entityFiltersFromUrl)
+ ) {
+ globalSearchFilterInitialQuery.setQuery({
+ entityFilters: entityFiltersFromUrl
+ });
+ }
+ }, [entityFilterParams]);
const [classificationQuery, setClassificationQuery] = useState(
!isEmpty(globalSearchFilterInitialQuery.getQuery()?.tagFilters)
? globalSearchFilterInitialQuery.getQuery()?.tagFilters
@@ -125,28 +164,63 @@ const Filters = ({
businessMetadata: businessMetadataDefs
};
+ const appliedIncludeDE = searchParams.get("includeDE") === "true" ||
searchParams.get("includeDE") === true;
+ const appliedExcludeSC = searchParams.get("excludeSC") === "true" ||
searchParams.get("excludeSC") === true;
+ const appliedExcludeST = searchParams.get("excludeST") === "true" ||
searchParams.get("excludeST") === true;
+
+ const hasChanges = useMemo(() => {
+ const switchChanged =
+ !!checkedEntities !== !!appliedIncludeDE ||
+ !!checkedSubClassifications !== !!appliedExcludeSC ||
+ !!checkedSubTypes !== !!appliedExcludeST;
+
+ const normalizeQuery = (q: RuleGroupType | null) => {
+ if (!q || !q.rules || q.rules.length === 0) return JSON.stringify({
combinator: "and", rules: [] });
+ return JSON.stringify({ combinator: q.combinator || "and", rules:
q.rules });
+ };
+
+ const entityQueryChanged =
+ normalizeQuery(typeQuery) !== normalizeQuery(entityFiltersFromUrl ||
initialQuery);
+ const tagQueryChanged =
+ normalizeQuery(classificationQuery) !== normalizeQuery(tagFiltersFromUrl
|| initialQuery);
+ const relationshipQueryChanged =
+ normalizeQuery(relationshipQuery) !==
normalizeQuery(relationshipFiltersFromUrl || initialQuery);
+
+ return switchChanged || entityQueryChanged || tagQueryChanged ||
relationshipQueryChanged;
+ }, [
+ checkedEntities,
+ checkedSubClassifications,
+ checkedSubTypes,
+ appliedIncludeDE,
+ appliedExcludeSC,
+ appliedExcludeST,
+ typeQuery,
+ classificationQuery,
+ relationshipQuery,
+ entityFiltersFromUrl,
+ tagFiltersFromUrl,
+ relationshipFiltersFromUrl
+ ]);
+
const handleSwitchChangeEntities = (
event: React.ChangeEvent<HTMLInputElement>
) => {
event.stopPropagation();
- searchParams.set("includeDE", event.target.checked);
- setCheckedEntities(event.target.checked);
+ setCheckedEntities(Boolean(event.target.checked));
};
const handleSwitchChangeSubClassification = (
event: React.ChangeEvent<HTMLInputElement>
) => {
event.stopPropagation();
- searchParams.set("excludeSC", event.target.checked);
- setCheckedSubClassifications(event.target.checked);
+ setCheckedSubClassifications(Boolean(event.target.checked));
};
const handleSwitchChangeSubTypes = (
event: React.ChangeEvent<HTMLInputElement>
) => {
event.stopPropagation();
- searchParams.set("excludeST", event.target.checked);
- setCheckedSubTypes(event.target.checked);
+ setCheckedSubTypes(Boolean(event.target.checked));
};
const paramsObject: Record<string, any> = {};
@@ -187,9 +261,9 @@ const Filters = ({
let rules_widgets = null;
let systemAttrArr;
- if (!isEmpty(paramsObject)) {
+ if (!isEmpty(paramsObject?.entityFilters)) {
rules_widgets = attributeFilter.extractUrl({
- value: undefined,
+ value: paramsObject.entityFilters,
formatDate: true
});
}
@@ -699,6 +773,7 @@ const Filters = ({
<CustomButton
variant="contained"
size="small"
+ disabled={!hasChanges}
onClick={() => {
applyFilter();
}}
diff --git a/dashboard/src/components/ShowMore/DrawerBodyChipView.tsx
b/dashboard/src/components/ShowMore/DrawerBodyChipView.tsx
index 5a082ef7e..38f702af6 100644
--- a/dashboard/src/components/ShowMore/DrawerBodyChipView.tsx
+++ b/dashboard/src/components/ShowMore/DrawerBodyChipView.tsx
@@ -388,6 +388,7 @@ const DrawerBodyChipView = ({
button2Label="Remove"
button2Handler={handleRemove}
disableButton2={removeLoader}
+ button2Loading={removeLoader}
>
<Typography fontSize={14}>
Remove:{" "}
diff --git a/dashboard/src/components/ShowMore/ShowMoreView.tsx
b/dashboard/src/components/ShowMore/ShowMoreView.tsx
index 5a0f1cfc0..3761d0a4d 100644
--- a/dashboard/src/components/ShowMore/ShowMoreView.tsx
+++ b/dashboard/src/components/ShowMore/ShowMoreView.tsx
@@ -182,6 +182,8 @@ const ShowMoreView = ({
}
} catch (error) {
serverError(error, toastId);
+ } finally {
+ setRemoveLoader(false);
}
};
@@ -379,6 +381,7 @@ const ShowMoreView = ({
button2Label="Remove"
button2Handler={handleRemove}
disableButton2={removeLoader}
+ button2Loading={removeLoader}
>
<Typography fontSize={14}>
Remove:{" "}
diff --git a/dashboard/src/components/Table/TablePagination.tsx
b/dashboard/src/components/Table/TablePagination.tsx
index 7577f0007..792d1a711 100644
--- a/dashboard/src/components/Table/TablePagination.tsx
+++ b/dashboard/src/components/Table/TablePagination.tsx
@@ -369,6 +369,18 @@ const TablePagination: React.FC<PaginationProps> = ({
? pageTo
: Math.min((pageIndex + 1) * pageSize, totalDatasetRows);
+ /** Last page may return fewer than `limit` rows; cap "to" at known total. */
+ const displayToCapped =
+ isServerSide &&
+ typeof totalCount === "number" &&
+ totalCount >= 0
+ ? Math.min(displayTo, totalCount)
+ : displayTo;
+
+ const footerRangeStart =
+ totalDatasetRows === 0 ? 0 : Math.min(displayFrom, displayToCapped);
+ const footerRangeEnd = totalDatasetRows === 0 ? 0 : displayToCapped;
+
return (
<Stack
spacing={{ xs: 1, sm: 2 }}
@@ -381,8 +393,16 @@ const TablePagination: React.FC<PaginationProps> = ({
>
<div>
<span className="text-grey">
- Showing <u>{totalDatasetRows.toLocaleString()} records</u> From{" "}
- {displayFrom} - {displayTo}
+ {totalDatasetRows === 0 ? (
+ "No records to display"
+ ) : (
+ <>
+ Showing {footerRangeStart.toLocaleString()}-
+ {footerRangeEnd.toLocaleString()} of{" "}
+ {totalDatasetRows.toLocaleString()}{" "}
+ {totalDatasetRows === 1 ? "record" : "records"}
+ </>
+ )}
</span>
</div>
@@ -491,25 +511,27 @@ const TablePagination: React.FC<PaginationProps> = ({
value={pendingGoToPageVal}
/>
<LightTooltip title="Goto Page">
- <IconButton
- type="button"
- size="small"
- className={`${
- !isEmpty(pendingGoToPageVal)
- ? "cursor-pointer"
- : "cursor-not-allowed"
- } table-pagination-gotopage-button`}
- aria-label="search"
- onClick={() => {
- if (!isEmpty(pendingGoToPageVal)) {
- setGoToPageTrigger(pendingGoToPageVal);
- handleGoToPage();
- }
- }}
- disabled={isEmpty(pendingGoToPageVal)}
- >
- Go
- </IconButton>
+ <span style={{ display: "inline-flex" }}>
+ <IconButton
+ type="button"
+ size="small"
+ className={`${
+ !isEmpty(pendingGoToPageVal)
+ ? "cursor-pointer"
+ : "cursor-not-allowed"
+ } table-pagination-gotopage-button`}
+ aria-label="search"
+ onClick={() => {
+ if (!isEmpty(pendingGoToPageVal)) {
+ setGoToPageTrigger(pendingGoToPageVal);
+ handleGoToPage();
+ }
+ }}
+ disabled={isEmpty(pendingGoToPageVal)}
+ >
+ Go
+ </IconButton>
+ </span>
</LightTooltip>
</Paper>
</Stack>
@@ -517,19 +539,21 @@ const TablePagination: React.FC<PaginationProps> = ({
<Stack flexDirection="row" alignItems="center">
<LightTooltip title="Previous">
- <IconButton
- size="small"
- className="pagination-page-change-btn"
- onClick={handlePreviousPage}
- disabled={isPreviousDisabled}
- aria-label="previous page"
- >
- {theme.direction === "rtl" ? (
- <KeyboardArrowRight />
- ) : (
- <KeyboardArrowLeft />
- )}
- </IconButton>
+ <span style={{ display: "inline-flex" }}>
+ <IconButton
+ size="small"
+ className="pagination-page-change-btn"
+ onClick={handlePreviousPage}
+ disabled={isPreviousDisabled}
+ aria-label="previous page"
+ >
+ {theme.direction === "rtl" ? (
+ <KeyboardArrowRight />
+ ) : (
+ <KeyboardArrowLeft />
+ )}
+ </IconButton>
+ </span>
</LightTooltip>
<LightTooltip title={`Page ${activePage}`}>
@@ -539,19 +563,21 @@ const TablePagination: React.FC<PaginationProps> = ({
</LightTooltip>
<LightTooltip title="Next">
- <IconButton
- size="small"
- className="pagination-page-change-btn"
- onClick={handleNextPage}
- disabled={isNextDisabled}
- aria-label="next page"
- >
- {theme.direction === "rtl" ? (
- <KeyboardArrowLeft />
- ) : (
- <KeyboardArrowRight />
- )}
- </IconButton>
+ <span style={{ display: "inline-flex" }}>
+ <IconButton
+ size="small"
+ className="pagination-page-change-btn"
+ onClick={handleNextPage}
+ disabled={isNextDisabled}
+ aria-label="next page"
+ >
+ {theme.direction === "rtl" ? (
+ <KeyboardArrowLeft />
+ ) : (
+ <KeyboardArrowRight />
+ )}
+ </IconButton>
+ </span>
</LightTooltip>
</Stack>
</>
diff --git a/dashboard/src/components/TreeNodeIcons.tsx
b/dashboard/src/components/TreeNodeIcons.tsx
index 3e56f70e8..5403a2a9d 100644
--- a/dashboard/src/components/TreeNodeIcons.tsx
+++ b/dashboard/src/components/TreeNodeIcons.tsx
@@ -16,6 +16,7 @@
*/
import { useRef, useState } from "react";
+import { useAsyncPending } from "../hooks/useAsyncPending";
import IconButton from "@mui/material/IconButton";
import Menu from "@mui/material/Menu";
import Stack from "@mui/material/Stack";
@@ -72,6 +73,9 @@ const TreeNodeIcons = (props: {
const [glossaryModal, setGlossaryModal] = useState<boolean>(false);
const [termModal, setTermModal] = useState(false);
const [categoryModal, setCategoryModal] = useState(false);
+ const [savedSearchDeleteLoading, setSavedSearchDeleteLoading] =
+ useState(false);
+ const { pending: renameSubmitting, run: runRenameAction } =
useAsyncPending();
const openNode = Boolean(expandNode);
@@ -128,6 +132,7 @@ const TreeNodeIcons = (props: {
const handleRemove = async () => {
try {
+ setSavedSearchDeleteLoading(true);
await removeSavedSearch(selectedSearchData.guid);
setDeleteModal(false);
setExpandNode(null);
@@ -142,23 +147,27 @@ const TreeNodeIcons = (props: {
toastId.current = toast.success(`${node.id} was deleted successfully`);
} catch (error) {
serverError(error, toastId);
+ } finally {
+ setSavedSearchDeleteLoading(false);
}
};
- const handleEdit = async () => {
- try {
- let filterData = { ...selectedSearchData, name: value };
- await editSavedSearch(filterData as CustomFiltersNodeType, "PUT");
- updatedData();
- setRenameModal(false);
- setExpandNode(null);
- toast.dismiss(toastId.current);
- toastId.current = toast.success(
- `${filterData.name} was updated successfully`
- );
- } catch (error) {
- serverError(error, toastId);
- }
+ const handleEdit = () => {
+ void runRenameAction(async () => {
+ try {
+ let filterData = { ...selectedSearchData, name: value };
+ await editSavedSearch(filterData as CustomFiltersNodeType, "PUT");
+ updatedData();
+ setRenameModal(false);
+ setExpandNode(null);
+ toast.dismiss(toastId.current);
+ toastId.current = toast.success(
+ `${filterData.name} was updated successfully`
+ );
+ } catch (error) {
+ serverError(error, toastId);
+ }
+ });
};
return (
<>
@@ -483,7 +492,8 @@ const TreeNodeIcons = (props: {
button1Handler={handleCloseRenameModal}
button2Label="Update"
button2Handler={handleEdit}
- // disableButton2={value}
+ disableButton2={renameSubmitting}
+ button2Loading={renameSubmitting}
>
<Stack
sx={{
@@ -518,6 +528,8 @@ const TreeNodeIcons = (props: {
button1Handler={handleCloseDeleteModal}
button2Label="Ok"
button2Handler={handleRemove}
+ disableButton2={savedSearchDeleteLoading}
+ button2Loading={savedSearchDeleteLoading}
>
<Typography fontSize={15}>
Are you sure you want to delete <strong>{node.id}</strong> ?{""}
diff --git a/dashboard/src/utils/Enum.ts b/dashboard/src/utils/Enum.ts
index 76ee0712d..50644400b 100644
--- a/dashboard/src/utils/Enum.ts
+++ b/dashboard/src/utils/Enum.ts
@@ -319,6 +319,15 @@ export const queryBuilderUIOperatorToAPI = {
export const queryBuilderApiOperatorToUI = invert(queryBuilderUIOperatorToAPI);
+/**
+ * Converts API operator (eq, lte, gte, etc.) to display symbol (=, <=, >=,
etc.)
+ * Used when rendering filter query chips above search results table
+ */
+export const getDisplayOperator = (operator: string): string => {
+ const mapped = (queryBuilderApiOperatorToUI as Map<string,
string>).get(operator);
+ return mapped ?? operator;
+};
+
export const queryBuilderDateRangeUIValueToAPI: Record<string, string> = {
Today: "TODAY",
Yesterday: "YESTERDAY",
diff --git
a/dashboard/src/views/BusinessMetadata/BusinessMetadataAtrributeForm.tsx
b/dashboard/src/views/BusinessMetadata/BusinessMetadataAtrributeForm.tsx
index 6e161f68f..d32eb7017 100644
--- a/dashboard/src/views/BusinessMetadata/BusinessMetadataAtrributeForm.tsx
+++ b/dashboard/src/views/BusinessMetadata/BusinessMetadataAtrributeForm.tsx
@@ -898,6 +898,7 @@ const BusinessMetadataAttributeForm = ({
maxWidth="sm"
button2Handler={handleSubmit(onSubmit)}
disableButton2={isSubmitting}
+ button2Loading={isSubmitting}
>
<Stack gap={2} paddingTop="2rem" paddingBottom="2rem">
<EnumCreateUpdate
diff --git a/dashboard/src/views/BusinessMetadata/BusinessMetadataForm.tsx
b/dashboard/src/views/BusinessMetadata/BusinessMetadataForm.tsx
index 950a6b3ff..9d31691e5 100644
--- a/dashboard/src/views/BusinessMetadata/BusinessMetadataForm.tsx
+++ b/dashboard/src/views/BusinessMetadata/BusinessMetadataForm.tsx
@@ -42,6 +42,7 @@ import BusinessMetadataAttributeForm from
"./BusinessMetadataAtrributeForm";
import { setEditBMAttribute } from "@redux/slice/createBMSlice";
import { cloneDeep } from "@utils/Helper";
import { fetchBusinessMetaData } from
"@redux/slice/typeDefSlices/typedefBusinessMetadataSlice";
+import { fetchEntityData } from
"@redux/slice/typeDefSlices/typedefEntitySlice";
import { defaultType } from "@utils/Enum";
import { getTypeName } from "@utils/CommonViewFunction";
@@ -340,6 +341,7 @@ const BusinessMetaDataForm = ({
let bmName = response?.data?.businessMetadataDefs?.[0]?.name;
toastMssg(bmName);
dispatchState(fetchBusinessMetaData());
+ dispatchState(fetchEntityData());
setBMAttribute({});
setForm(false);
} catch (error) {
diff --git
a/dashboard/src/views/DetailPage/BusinessMetadataDetails/BusinessMetadataDetailsLayout.tsx
b/dashboard/src/views/DetailPage/BusinessMetadataDetails/BusinessMetadataDetailsLayout.tsx
index bb6f8f138..214c9faad 100644
---
a/dashboard/src/views/DetailPage/BusinessMetadataDetails/BusinessMetadataDetailsLayout.tsx
+++
b/dashboard/src/views/DetailPage/BusinessMetadataDetails/BusinessMetadataDetailsLayout.tsx
@@ -45,6 +45,7 @@ import { createEditBusinessMetadata } from
"@api/apiMethods/typeDefApiMethods";
import { cloneDeep } from "@utils/Helper";
import { defaultAttrObj, defaultType } from "@utils/Enum";
import { fetchBusinessMetaData } from
"@redux/slice/typeDefSlices/typedefBusinessMetadataSlice";
+import { fetchEntityData } from
"@redux/slice/typeDefSlices/typedefEntitySlice";
import { getTypeName } from "@utils/CommonViewFunction";
const BusinessMetadataDetailsLayout = () => {
@@ -241,6 +242,7 @@ const BusinessMetadataDetailsLayout = () => {
try {
await createEditBusinessMetadata("business_metadata", "PUT", data);
dispatchState(fetchBusinessMetaData());
+ dispatchState(fetchEntityData());
toast.success(
"One or more Business Metadata attributes were updated successfully"
);
diff --git
a/dashboard/src/views/DetailPage/EntityDetailTabs/PropagationPropertyModal.tsx
b/dashboard/src/views/DetailPage/EntityDetailTabs/PropagationPropertyModal.tsx
index 5ab64aef6..1ea05c701 100644
---
a/dashboard/src/views/DetailPage/EntityDetailTabs/PropagationPropertyModal.tsx
+++
b/dashboard/src/views/DetailPage/EntityDetailTabs/PropagationPropertyModal.tsx
@@ -421,6 +421,7 @@ const PropagationPropertyModal = ({
button2Label="Update"
button2Handler={() => onSubmit()}
disableButton2={loading}
+ button2Loading={loading}
>
<Stack gap={2}>
<Stack direction="row" alignItems="center" gap="0.5rem">
diff --git a/dashboard/src/views/Entity/EntityForm.tsx
b/dashboard/src/views/Entity/EntityForm.tsx
index f075b943f..e6a66fa99 100644
--- a/dashboard/src/views/Entity/EntityForm.tsx
+++ b/dashboard/src/views/Entity/EntityForm.tsx
@@ -521,6 +521,7 @@ const EntityForm = ({
button2Label={guid ? "Update" : "Create"}
button2Handler={handleSubmit(onSubmit)}
disableButton2={isSubmitting}
+ button2Loading={isSubmitting}
isDirty={isDirty}
maxWidth="md"
>
diff --git a/dashboard/src/views/SaveFilters/SaveFilters.tsx
b/dashboard/src/views/SaveFilters/SaveFilters.tsx
index fed2fe27f..ac65237ec 100644
--- a/dashboard/src/views/SaveFilters/SaveFilters.tsx
+++ b/dashboard/src/views/SaveFilters/SaveFilters.tsx
@@ -206,6 +206,7 @@ const SaveFilters = ({
maxWidth="sm"
button2Handler={handleSubmit(onSubmit)}
disableButton2={isSubmitting}
+ button2Loading={isSubmitting}
>
{" "}
<form onSubmit={handleSubmit(onSubmit)}>
diff --git a/dashboard/src/views/Statistics/ServerStats.tsx
b/dashboard/src/views/Statistics/ServerStats.tsx
index 9f6974614..5f131c41c 100644
--- a/dashboard/src/views/Statistics/ServerStats.tsx
+++ b/dashboard/src/views/Statistics/ServerStats.tsx
@@ -118,12 +118,11 @@ const ServerStats = ({ selectedValue, currentMetricsData
}: any) => {
let notificationTableHeader = [
"Count",
- "Avg",
- "Time (ms)",
+ "Avg time (ms)",
"Creates",
"Updates",
- "Deletes"
- // "Failed"
+ "Deletes",
+ "Failed"
];
let topciOffsetTableHeader = [
diff --git a/dashboard/src/views/Statistics/Statistics.tsx
b/dashboard/src/views/Statistics/Statistics.tsx
index 6204a6d08..51bb42077 100644
--- a/dashboard/src/views/Statistics/Statistics.tsx
+++ b/dashboard/src/views/Statistics/Statistics.tsx
@@ -23,6 +23,7 @@ import ClassificationStats from "./ClassificationStats";
import {
Autocomplete,
Badge,
+ CircularProgress,
IconButton,
Stack,
TextField
@@ -72,6 +73,7 @@ const Statistics = ({
value: "Current"
});
const [loading, setLoading] = useState(true);
+ const [refreshBusy, setRefreshBusy] = useState(false);
useEffect(() => {
fetchMetricsStatsDetails();
@@ -117,12 +119,20 @@ const Statistics = ({
};
const handleRefresh = async () => {
- await fetchMetricsStatsDetails();
- await dispatch(fetchMetricEntity());
- if (toastId.current) {
- toast.dismiss(toastId.current);
+ if (refreshBusy) {
+ return;
+ }
+ setRefreshBusy(true);
+ try {
+ await fetchMetricsStatsDetails();
+ await dispatch(fetchMetricEntity());
+ if (toastId.current) {
+ toast.dismiss(toastId.current);
+ }
+ toastId.current = toast.success("Metric data is refreshed");
+ } finally {
+ setRefreshBusy(false);
}
- toastId.current = toast.success("Metric data is refreshed");
};
return (
@@ -137,14 +147,23 @@ const Statistics = ({
postTitleIcon={
<Stack justifyContent="center" alignItems="center">
<LightTooltip title="Refresh Data">
- <IconButton
- size="small"
- onClick={() => {
- handleRefresh();
- }}
- >
- <Refresh />
- </IconButton>
+ <span>
+ <IconButton
+ size="small"
+ onClick={() => {
+ void handleRefresh();
+ }}
+ disabled={refreshBusy || loading}
+ aria-busy={refreshBusy}
+ aria-label="Refresh metrics data"
+ >
+ {refreshBusy ? (
+ <CircularProgress size={20} thickness={4} color="inherit"
/>
+ ) : (
+ <Refresh />
+ )}
+ </IconButton>
+ </span>
</LightTooltip>
</Stack>
}