This is an automated email from the ASF dual-hosted git repository.
pawarprasad123 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/atlas.git
The following commit(s) were added to refs/heads/master by this push:
new 6ec5cc366 ATLAS-5285: ATLAS UI: Dashboard - Common changes that are
related to refresh the page. (#614)
6ec5cc366 is described below
commit 6ec5cc3669e420e799d33a018ce912c2097010fb
Author: Prasad Pawar <[email protected]>
AuthorDate: Fri May 8 13:18:24 2026 +0530
ATLAS-5285: ATLAS UI: Dashboard - Common changes that are related to
refresh the page. (#614)
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",
}}
>