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 42d2ff924a49d7c4dae60fcff82e4bf284034af3 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> }
