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

pawarprasad123 pushed a commit to branch 
feature/pr-1-redux-refresh-modals-header-sidebar
in repository https://gitbox.apache.org/repos/asf/atlas.git

commit c4c4af64e00dc95f890c768b6dd0c88e0f013c68
Author: Prasad Pawar <[email protected]>
AuthorDate: Wed May 6 14:09:04 2026 +0530

    ATLAS-5285: ATLAS UI: Dashboard - Common changes that are related to 
refresh the page.
    Atlas UI dashboard changes - Base for refresh UX
---
 dashboard/src/components/DialogShowMoreLess.tsx    |   1 +
 dashboard/src/components/Modal.tsx                 | 110 +++++++++++++++------
 dashboard/src/hooks/useAsyncPending.ts             |  43 ++++++++
 dashboard/src/redux/reducers/reducers.ts           |   2 +
 dashboard/src/redux/slice/dashboardRefreshSlice.ts |  39 ++++++++
 dashboard/src/utils/refreshDashboardHome.ts        |  30 ++++++
 dashboard/src/views/Layout/Header.tsx              |  23 +++--
 dashboard/src/views/SideBar/SideBarBody.tsx        |  71 ++++++++-----
 8 files changed, 257 insertions(+), 62 deletions(-)

diff --git a/dashboard/src/components/DialogShowMoreLess.tsx 
b/dashboard/src/components/DialogShowMoreLess.tsx
index d782222e8..03492d077 100644
--- a/dashboard/src/components/DialogShowMoreLess.tsx
+++ b/dashboard/src/components/DialogShowMoreLess.tsx
@@ -576,6 +576,7 @@ const DialogShowMoreLess = ({
           button2Label="Remove"
           button2Handler={handleRemove}
           disableButton2={removeLoader}
+          button2Loading={removeLoader}
           isDirty={true}
         >
           {relatedTerm ? (
diff --git a/dashboard/src/components/Modal.tsx 
b/dashboard/src/components/Modal.tsx
index 75682d277..373e5fee2 100644
--- a/dashboard/src/components/Modal.tsx
+++ b/dashboard/src/components/Modal.tsx
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-import { styled } from "@mui/material/styles";
+import { styled, type Theme } from "@mui/material/styles";
 import CircularProgress from "@mui/material/CircularProgress";
 import Dialog from "@mui/material/Dialog";
 import DialogActions from "@mui/material/DialogActions";
@@ -54,9 +54,13 @@ interface CustomModalProps {
   button2Label?: string | undefined;
   button2Handler: any;
   disableButton2?: string | object | boolean;
+  /** Shows spinner on primary action and blocks both footer buttons while an 
async action runs */
+  button2Loading?: boolean;
   maxWidth?: any;
   footer?: boolean;
   isDirty?: boolean;
+  /** When true, only the first footer button is rendered (e.g. import error 
state). */
+  hideButton2?: boolean;
 }
 
 export const CustomModal: React.FC<CustomModalProps> = ({
@@ -71,10 +75,18 @@ export const CustomModal: React.FC<CustomModalProps> = ({
   button2Label,
   button2Handler,
   disableButton2,
+  button2Loading,
   maxWidth,
   isDirty,
-  footer
+  footer,
+  hideButton2
 }) => {
+  const isLoading = Boolean(button2Loading);
+  const primaryDisabled =
+    isLoading ||
+    Boolean(disableButton2) ||
+    (isDirty !== undefined && !isDirty);
+
   const handleClick = (event: { stopPropagation: () => void }) => {
     event.stopPropagation();
   };
@@ -84,6 +96,7 @@ export const CustomModal: React.FC<CustomModalProps> = ({
         <BootstrapDialog
           maxWidth={maxWidth || "sm"}
           aria-labelledby="customized-dialog-title"
+          aria-busy={isLoading}
           open={open}
           sx={{
             "& .MuiDialog-paper": {
@@ -124,8 +137,12 @@ export const CustomModal: React.FC<CustomModalProps> = ({
                 }}
                 onClick={(e) => {
                   e.stopPropagation();
+                  if (isLoading) {
+                    return;
+                  }
                   onClose();
                 }}
+                disabled={isLoading}
               >
                 <CloseIcon sx={{ width: "0.75em", height: "0.75em" }} />
               </IconButton>
@@ -145,8 +162,12 @@ export const CustomModal: React.FC<CustomModalProps> = ({
                   <CustomButton
                     variant="outlined"
                     color="primary"
+                    disabled={isLoading}
                     onClick={(e: Event) => {
                       e.stopPropagation();
+                      if (isLoading) {
+                        return;
+                      }
                       button1Handler();
                     }}
                   >
@@ -154,37 +175,62 @@ export const CustomModal: React.FC<CustomModalProps> = ({
                   </CustomButton>
                 )}
 
-                <CustomButton
-                  variant="contained"
-                  color="primary"
-                  aria-label="close"
-                  primary={true}
-                  disabled={
-                    disableButton2 || (isDirty != undefined ? !isDirty : false)
-                  }
-                  sx={{
-                    ...(disableButton2 && {
-                      "&.Mui-disabled": {
-                        pointerEvents: "unset",
-                        cursor: "not-allowed"
+                {!hideButton2 && (
+                  <CustomButton
+                    variant="contained"
+                    color="primary"
+                    aria-label={
+                      isLoading
+                        ? "Action in progress, please wait"
+                        : "Confirm dialog action"
+                    }
+                    primary={true}
+                    disabled={primaryDisabled}
+                    sx={{
+                      minWidth: isLoading ? 88 : undefined,
+                      ...(isLoading && {
+                        "&.Mui-disabled": {
+                          opacity: 1,
+                          pointerEvents: "none",
+                          cursor: "progress",
+                          backgroundColor: (theme: Theme) =>
+                            theme.palette.primary.main,
+                          color: (theme: Theme) =>
+                            theme.palette.primary.contrastText
+                        }
+                      }),
+                      ...(primaryDisabled &&
+                        !isLoading && {
+                          "&.Mui-disabled": {
+                            pointerEvents: "unset",
+                            cursor: "not-allowed"
+                          }
+                        })
+                    }}
+                    startIcon={
+                      isLoading ? (
+                        <CircularProgress
+                          aria-hidden
+                          size={18}
+                          thickness={4}
+                          sx={{
+                            color: (theme: Theme) =>
+                              theme.palette.primary.contrastText
+                          }}
+                        />
+                      ) : undefined
+                    }
+                    onClick={(e: Event) => {
+                      e.stopPropagation();
+                      if (isLoading) {
+                        return;
                       }
-                    })
-                  }}
-                  startIcon={
-                    disableButton2 && isDirty ? (
-                      <CircularProgress
-                        sx={{ color: "white", fontWeight: "600" }}
-                        size="14px"
-                      />
-                    ) : undefined
-                  }
-                  onClick={(e: Event) => {
-                    e.stopPropagation();
-                    button2Handler();
-                  }}
-                >
-                  {button2Label}
-                </CustomButton>
+                      button2Handler();
+                    }}
+                  >
+                    {button2Label}
+                  </CustomButton>
+                )}
               </DialogActions>
             </Stack>
           )}
diff --git a/dashboard/src/hooks/useAsyncPending.ts 
b/dashboard/src/hooks/useAsyncPending.ts
new file mode 100644
index 000000000..b058f4392
--- /dev/null
+++ b/dashboard/src/hooks/useAsyncPending.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.
+ */
+
+import { useCallback, useRef, useState } from "react";
+
+/**
+ * Single-flight async guard + pending flag for primary actions (modals, 
forms).
+ * Reuse wherever a button triggers an API call and should show loading state.
+ */
+export const useAsyncPending = () => {
+       const [pending, setPending] = useState(false);
+       const busyRef = useRef(false);
+
+       const run = useCallback(async <T,>(fn: () => Promise<T>): Promise<T | 
undefined> => {
+               if (busyRef.current) {
+                       return undefined;
+               }
+               busyRef.current = true;
+               setPending(true);
+               try {
+                       return await fn();
+               } finally {
+                       busyRef.current = false;
+                       setPending(false);
+               }
+       }, []);
+
+       return { pending, run };
+};
diff --git a/dashboard/src/redux/reducers/reducers.ts 
b/dashboard/src/redux/reducers/reducers.ts
index 205006341..425fd9f80 100644
--- a/dashboard/src/redux/reducers/reducers.ts
+++ b/dashboard/src/redux/reducers/reducers.ts
@@ -32,6 +32,7 @@ import { relationshipsReducer } from 
"@redux/slice/typeDefSlices/typedefRelation
 import { metricsReducer } from "@redux/slice/metricsSlice";
 import { savedSearchReducer } from "@redux/slice/savedSearchSlice";
 import { drawerSliceReducer } from "@redux/slice/drawerSlice";
+import { dashboardRefreshReducer } from "@redux/slice/dashboardRefreshSlice";
 
 const rootReducer = combineReducers({
   session: sessionReducer,
@@ -39,6 +40,7 @@ const rootReducer = combineReducers({
   typeHeader: typeHeaderReducer,
   allEntityTypes: allEntityTypesReducer,
   metrics: metricsReducer,
+  dashboardRefresh: dashboardRefreshReducer,
   classification: classificationReducer,
   businessMetaData: businessMetadataReducer,
   glossary: glossaryReducer,
diff --git a/dashboard/src/redux/slice/dashboardRefreshSlice.ts 
b/dashboard/src/redux/slice/dashboardRefreshSlice.ts
new file mode 100644
index 000000000..cb21a5c9d
--- /dev/null
+++ b/dashboard/src/redux/slice/dashboardRefreshSlice.ts
@@ -0,0 +1,39 @@
+/*
+ * 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 { createSlice } from "@reduxjs/toolkit";
+
+interface DashboardRefreshState {
+       version: number;
+}
+
+const initialState: DashboardRefreshState = {
+       version: 0,
+};
+
+const dashboardRefreshSlice = createSlice({
+       name: "dashboardRefresh",
+       initialState,
+       reducers: {
+               requestDashboardRefresh: (state) => {
+                       state.version += 1;
+               },
+       },
+});
+
+export const { requestDashboardRefresh } = dashboardRefreshSlice.actions;
+export const dashboardRefreshReducer = dashboardRefreshSlice.reducer;
diff --git a/dashboard/src/utils/refreshDashboardHome.ts 
b/dashboard/src/utils/refreshDashboardHome.ts
new file mode 100644
index 000000000..1a2a6571c
--- /dev/null
+++ b/dashboard/src/utils/refreshDashboardHome.ts
@@ -0,0 +1,30 @@
+/*
+ * 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 type { AppDispatch } from "@redux/store/store";
+import { requestDashboardRefresh } from "@redux/slice/dashboardRefreshSlice";
+import { fetchMetricEntity } from "@redux/slice/metricsSlice";
+
+/**
+ * Soft-refresh dashboard home: bumps subscribers (latest entities, activity,
+ * QuickSearch) and refetches Redux metrics — same net calls as a full reload
+ * for those areas, without remounting the whole app.
+ */
+export const refreshDashboardHomeData = (dispatch: AppDispatch): void => {
+       dispatch(requestDashboardRefresh());
+       dispatch(fetchMetricEntity());
+};
diff --git a/dashboard/src/views/Layout/Header.tsx 
b/dashboard/src/views/Layout/Header.tsx
index 9e00e7eea..80fc2dc06 100644
--- a/dashboard/src/views/Layout/Header.tsx
+++ b/dashboard/src/views/Layout/Header.tsx
@@ -29,7 +29,13 @@ import BarChartOutlinedIcon from 
"@mui/icons-material/BarChartOutlined";
 import AccountCircleRoundedIcon from 
"@mui/icons-material/AccountCircleRounded";
 import { useLocation, useNavigate } from "react-router-dom";
 import QuickSearch from "@components/GlobalSearch/QuickSearch";
-import { Logout } from "@mui/icons-material";
+import CreateDropdown from "@components/CreateDropdown";
+import {
+       AdminPanelSettingsOutlined,
+       HelpOutlineOutlined,
+       Logout,
+       SwapHorizOutlined
+} from "@mui/icons-material";
 import { useAppSelector } from "@hooks/reducerHook";
 import {
   Button,
@@ -174,15 +180,17 @@ const Header: React.FC<Header> = ({
           </Button>
         </LightTooltip>
       )}
-      {location.pathname != "/" &&
-        location.pathname != "/search" &&
-        location.pathname != "/!" &&
-        (!location.pathname.includes("!") ) && (
+      {location.pathname !== "/" &&
+        location.pathname !== "/search" && (
           <div style={{ display: "flex", justifyContent: "center", flex: "1" 
}}>
             <QuickSearch />
           </div>
         )}
-      <div className="header-menu">
+      {(location.pathname === "/" || location.pathname === "/search") && (
+        <div style={{ flex: "1" }} />
+      )}
+      <div className="header-menu" style={{ display: "flex", alignItems: 
"center", gap: 8 }}>
+        <CreateDropdown />
         <LightTooltip title="Downloads">
           <IconButton
             size="small"
@@ -260,9 +268,11 @@ const Header: React.FC<Header> = ({
             }}
             data-cy="administrator"
           >
+            <AdminPanelSettingsOutlined sx={{ marginRight: "4px" }} 
fontSize="inherit" />
             Administration
           </MenuItem>
           <MenuItem dense onClick={handleNestedMenuClick} data-cy="help">
+            <HelpOutlineOutlined sx={{ marginRight: "4px" }} 
fontSize="inherit" />
             Help
           </MenuItem>
           <Divider />
@@ -275,6 +285,7 @@ const Header: React.FC<Header> = ({
               handleClose();
             }}
           >
+            <SwapHorizOutlined sx={{ marginRight: "4px" }} fontSize="inherit" 
/>
             Switch to Classic
           </MenuItem>
           <MenuItem
diff --git a/dashboard/src/views/SideBar/SideBarBody.tsx 
b/dashboard/src/views/SideBar/SideBarBody.tsx
index 65d8861e5..91b5bda09 100644
--- a/dashboard/src/views/SideBar/SideBarBody.tsx
+++ b/dashboard/src/views/SideBar/SideBarBody.tsx
@@ -18,9 +18,11 @@
 import { styled } from "@mui/material/styles";
 import {
   Suspense,
+  useCallback,
   useEffect,
   useState,
   ChangeEvent,
+  KeyboardEvent,
   lazy,
   useRef,
 } from "react";
@@ -50,6 +52,7 @@ import { fetchRootClassification } from 
"@redux/slice/rootClassificationSlice";
 import { fetchTypeHeaderData } from 
"@redux/slice/typeDefSlices/typeDefHeaderSlice";
 import { fetchRootEntity } from "@redux/slice/allEntityTypesSlice";
 import { fetchMetricEntity } from "@redux/slice/metricsSlice";
+import { refreshDashboardHomeData } from "@utils/refreshDashboardHome";
 import ErrorPage from "@views/ErrorPage";
 import AppRoutes from "@views/AppRoutes";
 import ErrorBoundaryWithNavigate from "../../ErrorBoundary";
@@ -153,7 +156,28 @@ const SideBarBody = (props: {
     dispatch(fetchRootClassification());
     dispatch(fetchEnumData());
     dispatch(fetchMetricEntity());
-  }, []);
+  }, [dispatch]);
+
+  const handleAtlasLogoClick = useCallback(() => {
+    refreshDashboardHomeData(dispatch);
+    navigate(
+      {
+        pathname: "/search",
+      },
+      { replace: true }
+    );
+  }, [dispatch, navigate]);
+
+  const handleAtlasLogoKeyDown = useCallback(
+    (e: KeyboardEvent<HTMLElement>) => {
+      if (e.key !== "Enter" && e.key !== " ") {
+        return;
+      }
+      e.preventDefault();
+      handleAtlasLogoClick();
+    },
+    [handleAtlasLogoClick]
+  );
 
   useEffect(() => {
     const draggerElement = draggerRef.current;
@@ -236,14 +260,11 @@ const SideBarBody = (props: {
                 cursor: "pointer",
                 boxSizing: "border-box",
               }}
-              onClick={() => {
-                navigate(
-                  {
-                    pathname: "/search",
-                  },
-                  { replace: true }
-                );
-              }}
+              role="button"
+              tabIndex={0}
+              aria-label="Atlas home — refresh dashboard"
+              onClick={handleAtlasLogoClick}
+              onKeyDown={handleAtlasLogoKeyDown}
               data-cy="apache-atlas-logo-collapsed"
             >
               <img
@@ -271,20 +292,22 @@ const SideBarBody = (props: {
               }}
             >
               <Stack gap="1.5rem" width="100%" marginTop="1rem">
-                <img
-                  src={atlasLogo}
-                  alt="Atlas logo"
-                  onClick={() => {
-                    navigate(
-                      {
-                        pathname: "/search",
-                      },
-                      { replace: true }
-                    );
-                  }}
-                  className="header-logo cursor-pointer"
-                  data-cy="atlas-logo"
-                />
+                <span
+                  role="button"
+                  tabIndex={0}
+                  aria-label="Atlas home — refresh dashboard"
+                  onClick={handleAtlasLogoClick}
+                  onKeyDown={handleAtlasLogoKeyDown}
+                  className="inline-block cursor-pointer"
+                >
+                  <img
+                    src={atlasLogo}
+                    alt=""
+                    aria-hidden
+                    className="header-logo"
+                    data-cy="atlas-logo"
+                  />
+                </span>
                 <Paper
                   sx={{
                     width: "100%",
@@ -493,7 +516,7 @@ const SideBarBody = (props: {
           }),
           margin: "0",
           overflowX: "auto",
-          background: "#f5f5f5",
+          background: "#f5f7f9",
           padding: "0",
         }}
       >

Reply via email to