pawarprasad123 commented on code in PR #688:
URL: https://github.com/apache/atlas/pull/688#discussion_r3781808034
##########
dashboard/src/views/SideBar/SideBarBody.tsx:
##########
@@ -101,61 +101,101 @@ const DrawerHeader = styled("div")(({ theme }) => ({
marginBottom: "1rem",
}));
+
const SideBarBody = (props: {
- loading: boolean;
- handleOpenModal: any;
- handleOpenAboutModal: any;
+ handleOpenModal: () => void;
+ handleOpenAboutModal: () => void;
}) => {
const location = useLocation();
const routes = useRoutes(AppRoutes as RouteObject[]);
const history = useHistory();
const dispatch = useAppDispatch();
- const { loading: loader, handleOpenModal, handleOpenAboutModal } = props;
+ const { handleOpenModal, handleOpenAboutModal } = props;
const navigate = useNavigate();
- const { loading } = useSelector((state: TypeHeaderState) =>
state.typeHeader);
const { relationshipSearch = {} } = globalSessionData || {};
const [open, setOpen] = useState(true);
const [searchTerm, setSearchTerm] = useState<string>("");
+ const { data: versionData } = useAppSelector((state) =>
state.session?.versionData || {});
+ const searchParams = new URLSearchParams(location.search);
+
+ const isCustomFilterActive = searchParams.get("isCF") === "true";
+ const isGlossaryActive = !isCustomFilterActive &&
(location.pathname.includes("/glossary") || !!searchParams.get("gtype") ||
!!searchParams.get("term") || !!searchParams.get("category"));
+ const isBusinessMetadataActive = !isCustomFilterActive &&
location.pathname.includes("/administrator/businessMetadata");
+ const isClassificationActive = !isCustomFilterActive &&
(!!searchParams.get("tag") || location.pathname.includes("/tag/tagAttribute"));
+ const isRelationshipActive = !isCustomFilterActive &&
(!!searchParams.get("relationshipName") ||
location.pathname.includes("/relationshipDetailPage"));
+
+ const isEntitiesActive = !isCustomFilterActive &&
(!!searchParams.get("type") || location.pathname.includes("/detailPage"));
+
+ const modules = [
+ { id: "entities", title: "Entities", isActive: isEntitiesActive, iconUrl:
"/img/sidebar-icons/icon-entities.svg", Component: EntitiesTree, isVisible:
true },
+ { id: "classification", title: "Classifications", isActive:
isClassificationActive, iconUrl: "/img/sidebar-icons/icon-classifications.svg",
Component: ClassificationTree, isVisible: true },
+ { id: "glossary", title: "Glossary", isActive: isGlossaryActive, iconUrl:
"/img/sidebar-icons/icon-glossary.svg", Component: GlossaryTree, isVisible:
true },
+ { id: "businessMetadata", title: "Business Metadata", isActive:
isBusinessMetadataActive, iconUrl:
"/img/sidebar-icons/icon-business-metadata.svg", Component:
BusinessMetadataTree, isVisible: true },
+ { id: "relationships", title: "Relationships", isActive:
isRelationshipActive, iconUrl: "/img/sidebar-icons/icon-relationships.svg",
Component: RelationshipsTree, isVisible: !!relationshipSearch },
+ { id: "customFilters", title: "Custom Filters", isActive:
isCustomFilterActive, iconUrl: "/img/sidebar-icons/icon-custom-filters.svg",
Component: CustomFiltersTree, isVisible: true }
+ ];
const handleDrawerOpen = () => {
setOpen(!open);
};
- const [position, setPosition] = useState<string |
number>(defaultDrawerWidth);
- const draggerRef = useRef<HTMLDivElement>(null);
- const headerRef = useRef<HTMLDivElement>(null);
- const windowWidth = window.innerWidth;
- const minPosition = 300;
- const maxPosition = windowWidth * 0.6;
-
- const handleMouseMove = (e: MouseEvent) => {
- let newPosition = e.clientX;
+ const [popoverAnchor, setPopoverAnchor] = useState<HTMLButtonElement |
null>(null);
+ const [activePopover, setActivePopover] = useState<string | null>(null);
+ const [popoverMaxHeight, setPopoverMaxHeight] = useState<string>('calc(100vh
- 100px)');
+ const [isBottomHalf, setIsBottomHalf] = useState<boolean>(false);
+
+ const handlePopoverOpen = (event: React.MouseEvent<HTMLButtonElement>, id:
string) => {
+ const target = event.currentTarget;
+
+ const openNewPopover = () => {
+ setPopoverAnchor(target);
+ setActivePopover(id);
+
+ // Calculate remaining screen height from the anchor to the bottom
+ const rect = target.getBoundingClientRect();
+ const spaceBelow = window.innerHeight - rect.top - 24;
+ const isBottom = spaceBelow < 350;
+ setIsBottomHalf(isBottom);
+
+ if (isBottom) {
+ const spaceAbove = rect.bottom - 24;
+ setPopoverMaxHeight(`${Math.max(250, spaceAbove)}px`);
+ } else {
+ setPopoverMaxHeight(`${Math.max(250, spaceBelow)}px`);
+ }
+ };
- if (newPosition < minPosition) {
- newPosition = minPosition;
- } else if (newPosition > maxPosition) {
- newPosition = maxPosition;
+ // If a different popover is already open, close it first to ensure clean
unmount
+ if (activePopover && activePopover !== id) {
+ handlePopoverClose();
+ setTimeout(openNewPopover, 0);
Review Comment:
line -168-171:
setTimeout(openNewPopover, 0) when switching popovers has no cleanup. Rapid
clicks can leave wrong popover open. Prefer synchronous state update or
store/clear timeout in a ref with cleanup on unmount.
##########
dashboard/src/views/SideBar/SideBarBody.tsx:
##########
@@ -101,61 +101,101 @@ const DrawerHeader = styled("div")(({ theme }) => ({
marginBottom: "1rem",
}));
+
const SideBarBody = (props: {
- loading: boolean;
- handleOpenModal: any;
- handleOpenAboutModal: any;
+ handleOpenModal: () => void;
+ handleOpenAboutModal: () => void;
}) => {
const location = useLocation();
const routes = useRoutes(AppRoutes as RouteObject[]);
const history = useHistory();
const dispatch = useAppDispatch();
- const { loading: loader, handleOpenModal, handleOpenAboutModal } = props;
+ const { handleOpenModal, handleOpenAboutModal } = props;
const navigate = useNavigate();
- const { loading } = useSelector((state: TypeHeaderState) =>
state.typeHeader);
const { relationshipSearch = {} } = globalSessionData || {};
const [open, setOpen] = useState(true);
const [searchTerm, setSearchTerm] = useState<string>("");
+ const { data: versionData } = useAppSelector((state) =>
state.session?.versionData || {});
+ const searchParams = new URLSearchParams(location.search);
+
+ const isCustomFilterActive = searchParams.get("isCF") === "true";
Review Comment:
Multiple modules can appear active simultaneously
line 121-127:
Active checks are independent. A URL with both type and tag can highlight
Entities and Classifications. Consider mutual exclusion or priority (e.g. isCF
already takes precedence).
##########
dashboard/src/components/SidebarSearchInput.tsx:
##########
@@ -0,0 +1,78 @@
+/*
+ * 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 React, { ChangeEvent } from "react";
+import { Paper, InputBase, Stack } from "@mui/material";
+import ClearIcon from "@mui/icons-material/Clear";
+import { IconButton } from "@components/muiComponents";
+
+interface SidebarSearchInputProps {
+ searchTerm: string;
+ onChange: (value: string) => void;
+ dataCy?: string;
+}
+
+export const SidebarSearchInput: React.FC<SidebarSearchInputProps> = ({
+ searchTerm,
+ onChange,
+ dataCy
+}) => (
+ <Paper
+ sx={{
+ width: "100%",
+ paddingLeft: "8px",
+ display: "flex",
+ alignItems: "center"
+ }}
+ className="sidebar-searchbar"
+ >
+ <InputBase
+ fullWidth
+ sx={{ color: "rgba(0, 0, 0, 0.7)" }}
+ placeholder="Search"
+ inputProps={{ "aria-label": "search" }}
+ value={searchTerm}
+ onChange={(e: ChangeEvent<HTMLInputElement>) => onChange(e.target.value)}
+ data-cy={dataCy}
+ endAdornment={
+ <Stack direction="row" alignItems="center" gap="4px">
+ {searchTerm.length > 0 && (
+ <IconButton
+ size="small"
+ onClick={() => onChange("")}
+ edge="end"
+ sx={{ padding: "4px" }}
+ >
+ <ClearIcon fontSize="small" sx={{ color: "rgba(0, 0, 0, 0.4)" }}
/>
+ </IconButton>
+ )}
+ <img
+ src="/img/sidebar-icons/icon-search.svg"
+ style={{
Review Comment:
Inline style on search icon — consider moving to SCSS under
.sidebar-searchbar.
##########
dashboard/src/views/SideBar/SideBarBody.tsx:
##########
@@ -246,38 +323,117 @@ const SideBarBody = (props: {
backgroundColor: "#034858",
}}
>
- {/* Collapsed sidebar logo */}
+ {/* Collapsed sidebar logo and module icons */}
{!open && (
- <div
- style={{
- width: "100%",
- textAlign: "center",
- paddingLeft: "12px",
- display: "flex",
- alignItems: "center",
- justifyContent: "center",
- minHeight: "64px",
- cursor: "pointer",
- boxSizing: "border-box",
- }}
- role="button"
- tabIndex={0}
- aria-label="Atlas home — refresh dashboard"
- onClick={handleAtlasLogoClick}
- onKeyDown={handleAtlasLogoKeyDown}
- data-cy="apache-atlas-logo-collapsed"
+ <Stack
+ alignItems="center"
+ sx={{ width: "100%", flex: 1, minHeight: 0, overflowY: "auto",
overflowX: "hidden", boxSizing: "border-box", pb: "60px" }}
>
- <img
- src={apacheAtlasLogo}
- alt="Apache Atlas logo"
- style={{
- width: "29px",
- height: "auto",
- maxWidth: "100%",
- display: "block",
+ <div
+ className="collapsed-logo-container"
+ role="button"
+ tabIndex={0}
+ aria-label="Atlas home — refresh dashboard"
+ onClick={handleAtlasLogoClick}
+ onKeyDown={handleAtlasLogoKeyDown}
+ data-cy="apache-atlas-logo-collapsed"
+ >
+ <img
+ src={apacheAtlasLogo}
+ alt="Apache Atlas logo"
+ className="collapsed-logo-img"
+ />
+ </div>
+
+ {/* Module Icons for Mini Drawer */}
+ <Stack alignItems="stretch" gap="1rem" sx={{ width: "100%" }}>
+ {/* Search */}
+ <Box sx={{ display: "flex", justifyContent: "center",
borderLeft: "4px solid transparent", borderRight: "4px solid transparent",
background: "transparent" }}>
+ <Tooltip title="Search" placement="right">
+ <IconButton aria-expanded={open} onClick={() =>
setOpen(true)} sx={{ '&:hover': { background: 'rgba(255, 255, 255, 0.1)' } }}>
+ <img src="/img/sidebar-icons/icon-search.svg"
className="sidebar-module-icon" alt="search" />
+ </IconButton>
+ </Tooltip>
+ </Box>
+
+ {modules.filter(m => m.isVisible).map(m => (
+ <Box
+ key={m.id}
+ className={m.isActive ? "sidebar-icon-active" : ""}
+ sx={{
+ display: "flex",
+ justifyContent: "center",
+ borderLeft: "4px solid transparent",
+ borderRight: "4px solid transparent",
+ background: "transparent"
+ }}
+ >
+ <Tooltip title={m.title} placement="right">
+ <IconButton onClick={(e) => handlePopoverOpen(e, m.id)}
sx={{ color: m.isActive ? "white" : "rgba(255, 255, 255, 0.6)", '&:hover': {
color: 'white', background: 'rgba(255, 255, 255, 0.1)' } }}>
Review Comment:
Accessibility — module icon buttons
line 372–374
Module IconButtons rely on Tooltip only. Add aria-label={m.title} and
aria-expanded={activePopover === m.id} for screen readers and keyboard users.
line 353
Search expand button uses aria-expanded={open} — open is drawer state, not
popover. Misleading for assistive tech; use aria-label="Expand sidebar search".
line54-60
Clear button has no aria-label. Add aria-label="Clear search".
##########
dashboard/src/components/EntityDisplayImage.tsx:
##########
@@ -15,86 +15,58 @@
* limitations under the License.
*/
-import { useEffect, useState } from "react";
-import { Avatar, Skeleton } from "@mui/material";
+import { Avatar } from "@mui/material";
import { getEntityIconPath } from "../utils/Utils";
+interface DisplayImageProps {
+ entity: Record<string, unknown>;
+ width?: string | number;
+ height?: string | number;
+ avatarDisplay?: boolean;
+ isProcess?: boolean;
+}
+
const DisplayImage = ({
entity,
width,
height,
avatarDisplay,
isProcess
-}: any) => {
- const [imageUrl, setImageUrl] = useState<any>(null);
- const [checkEntityImage, setCheckEntityImage] = useState<any>({
- [entity.guid]: false
- });
-
- useEffect(() => {
- const fetchImagePath = async () => {
- let entityData = { ...entity, ...{ isProcess: isProcess } };
- let imagePath: any = getEntityIconPath({ entityData: entityData });
- try {
- const response = await fetch(imagePath);
- const contentType: any = response.headers.get("Content-Type");
-
- if (contentType.startsWith("image/")) {
- let cache = { [entityData.guid]: imagePath };
- setCheckEntityImage(cache);
- setImageUrl(getEntityIconPath({ entityData: entityData }));
- } else {
- setImageUrl(
- getEntityIconPath({ entityData: entityData, errorUrl: imagePath })
- );
- }
- } catch (error) {
- setImageUrl(
- getEntityIconPath({ entityData: entityData, errorUrl: imagePath })
- );
- }
- };
+}: DisplayImageProps) => {
+ const entityData = { ...entity, isProcess: isProcess };
+
+ const primaryUrl = getEntityIconPath({ entityData }) || "";
+ const fallbackUrl = getEntityIconPath({ entityData, errorUrl: primaryUrl })
|| "";
- fetchImagePath();
- }, []);
+ const handleError = (e: React.SyntheticEvent<HTMLImageElement, Event>) => {
Review Comment:
handleError correctly sets target.onerror = null before fallback. Add test
when fallback also fails (no infinite loop).
##########
dashboard/src/views/SideBar/SideBarBody.tsx:
##########
@@ -308,189 +464,121 @@ const SideBarBody = (props: {
data-cy="atlas-logo"
/>
</span>
- <Paper
- sx={{
- width: "100%",
- }}
- className="sidebar-searchbar"
- >
- <InputBase
- fullWidth
- sx={{ color: "rgba(0, 0, 0, 0.7)" }}
- placeholder="Entities, Classifications, Glossaries"
- inputProps={{ "aria-label": "search" }}
- value={searchTerm}
- onChange={(e: ChangeEvent<HTMLInputElement>) => {
- setSearchTerm(e.target.value);
- }}
- data-cy="searchNode"
- />
-
- <IconButton type="submit" size="small" aria-label="search">
- <SearchIcon fontSize="inherit" />
- </IconButton>
- </Paper>
+ <SidebarSearchInput
+ searchTerm={searchTerm}
+ onChange={setSearchTerm}
+ dataCy="searchNode"
+ />
</Stack>
</DrawerHeader>
)}
<Paper
className="sidebar-wrapper"
sx={{
flex: 1,
- overflow: "hidden auto",
- paddingBottom: "0px", // Account for bottom toggle button
- ...(open == false && {
+ overflowX: "hidden",
+ overflowY: "auto",
+ paddingBottom: "48px", // Added space so it doesn't touch the
bottom toggle button
+ ...(!open && {
overflow: "hidden",
+ display: "none",
}),
}}
>
- <div
- className="sidebar-treeview-container"
- data-cy="r_entityTreeRender"
- >
- <Suspense
- fallback={
- <SkeletonLoader
- animation="pulse"
- variant="text"
- width={330}
- count={5}
- />
- // <Stack className="tree-item-loader-box">
- // </Stack>
- }
- >
- <EntitiesTree
- sideBarOpen={open}
- loading={loading}
- searchTerm={searchTerm}
- />
- </Suspense>
- </div>
+ {open && (
+ <>
+ <div
+ className="sidebar-treeview-container"
+ data-cy="r_entityTreeRender"
+ >
+ <Suspense
+ fallback={<TreeSkeletonLoader count={2} />}
+ >
+ <EntitiesTree
+ sideBarOpen={open}
+ searchTerm={searchTerm}
+ />
+ </Suspense>
+ </div>
- <div
- className="sidebar-treeview-container"
- data-cy="r_classificationTreeRender"
- >
- <Suspense
- fallback={
- <SkeletonLoader
- animation="pulse"
- variant="text"
- width={330}
- count={5}
- />
- // <Stack className="tree-item-loader-box">
- // </Stack>
- }
- >
- <ClassificationTree
- sideBarOpen={open}
- loading={loader}
- searchTerm={searchTerm}
- />
- </Suspense>
- </div>
+ <div
+ className="sidebar-treeview-container"
+ data-cy="r_classificationTreeRender"
+ >
+ <Suspense
+ fallback={<TreeSkeletonLoader count={2} />}
+ >
+ <ClassificationTree
+ sideBarOpen={open}
+ searchTerm={searchTerm}
+ />
+ </Suspense>
+ </div>
- <div
- className="sidebar-treeview-container"
- data-cy="r_businessMetadataTreeRender"
- >
- <Suspense
- fallback={
- <SkeletonLoader
- animation="pulse"
- variant="text"
- width={330}
- count={5}
- />
- // <Stack className="tree-item-loader-box">
- // </Stack>
- }
- >
- <BusinessMetadataTree
- sideBarOpen={open}
- searchTerm={searchTerm}
- />
- </Suspense>
- </div>
+ <div
+ className="sidebar-treeview-container"
+ data-cy="r_glossaryTreeRender"
+ >
+ <Suspense
+ fallback={<TreeSkeletonLoader count={2} />}
+ >
+ <GlossaryTree sideBarOpen={open} searchTerm={searchTerm} />
+ </Suspense>
+ </div>
- <div
- className="sidebar-treeview-container"
- data-cy="r_glossaryTreeRender"
- >
- <Suspense
- fallback={
- <SkeletonLoader
- animation="pulse"
- variant="text"
- width={330}
- count={5}
- />
- // <Stack className="tree-item-loader-box">
- // </Stack>
- }
- >
- <GlossaryTree sideBarOpen={open} searchTerm={searchTerm} />
- </Suspense>
- </div>
- {relationshipSearch && (
- <div
- className="sidebar-treeview-container"
- data-cy="r_relationshipTreeRender"
- >
- <Suspense
- fallback={
- <SkeletonLoader
- animation="pulse"
- variant="text"
- width={330}
- count={5}
+ <div
+ className="sidebar-treeview-container"
+ data-cy="r_businessMetadataTreeRender"
+ >
+ <Suspense
+ fallback={<TreeSkeletonLoader count={2} />}
+ >
+ <BusinessMetadataTree
+ sideBarOpen={open}
+ searchTerm={searchTerm}
/>
- // <Stack className="tree-item-loader-box">
- // </Stack>
- }
+ </Suspense>
+ </div>
+ {relationshipSearch && (
+ <div
+ className="sidebar-treeview-container"
+ data-cy="r_relationshipTreeRender"
+ >
+ <Suspense
+ fallback={<TreeSkeletonLoader count={2} />}
+ >
+ <RelationshipsTree
+ sideBarOpen={open}
+ searchTerm={searchTerm}
+ />
+ </Suspense>
+ </div>
+ )}
+
+ <div
+ className="sidebar-treeview-container"
+ data-cy="r_customFilterTreeRender"
>
- <RelationshipsTree
- sideBarOpen={open}
- searchTerm={searchTerm}
- />
- </Suspense>
- </div>
+ <Suspense
+ fallback={<TreeSkeletonLoader count={2} />}
+ >
+ <CustomFiltersTree sideBarOpen={open}
searchTerm={searchTerm} />
+ </Suspense>
+ </div>
+ </>
)}
-
- <div
- className="sidebar-treeview-container"
- data-cy="r_customFilterTreeRender"
- >
- <Suspense
- fallback={
- <SkeletonLoader
- animation="pulse"
- variant="text"
- width={330}
- count={5}
- />
- // <Stack className="tree-item-loader-box">
- // </Stack>
- }
- >
- <CustomFiltersTree sideBarOpen={open} searchTerm={searchTerm}
/>
- </Suspense>
- </div>
</Paper>
<div
- style={{
- width: "100%",
- textAlign: "right",
- padding: "8px",
- position: "sticky",
- bottom: "0px",
- zIndex: "9",
- left: "0",
- background: "#034858",
- }}
+ className={`sidebar-toggle-container ${open ?
'sidebar-toggle-open' : 'sidebar-toggle-closed'}`}
>
+ {open && (
+ <Box display="flex" flexDirection="column" gap="4px"
alignItems="flex-start" pl="4px">
+ <Typography variant="body2" sx={{ color: "rgba(255, 255, 255,
0.6)", pl: '4px' }}>
Review Comment:
low:
Version shows V ${versionData.Version} with no loading/error fallback if
fetchVersionData fails.
##########
dashboard/src/views/DashboardOverview/DashboardOverview.tsx:
##########
@@ -97,11 +97,12 @@ const DashboardOverview = () => {
maxWidth: "100%",
boxSizing: "border-box",
backgroundColor: "#f5f7f9",
- padding: 3,
- borderRadius: 2
+ borderRadius: 2,
+ pb: 3,
+ pr: 3
}}
>
- <Grid container spacing={3} sx={{ width: "100%",
alignItems: "stretch" }}>
+ <Grid container spacing={3} sx={{ m: 0, width: "100%",
alignItems: "stretch" }}>
Review Comment:
Padding/grid margin changes — confirm intentional for this ticket.
verify this.
##########
dashboard/src/views/SideBar/SideBarBody.tsx:
##########
@@ -101,61 +101,101 @@ const DrawerHeader = styled("div")(({ theme }) => ({
marginBottom: "1rem",
}));
+
const SideBarBody = (props: {
- loading: boolean;
- handleOpenModal: any;
- handleOpenAboutModal: any;
+ handleOpenModal: () => void;
+ handleOpenAboutModal: () => void;
}) => {
const location = useLocation();
const routes = useRoutes(AppRoutes as RouteObject[]);
const history = useHistory();
const dispatch = useAppDispatch();
- const { loading: loader, handleOpenModal, handleOpenAboutModal } = props;
+ const { handleOpenModal, handleOpenAboutModal } = props;
const navigate = useNavigate();
- const { loading } = useSelector((state: TypeHeaderState) =>
state.typeHeader);
const { relationshipSearch = {} } = globalSessionData || {};
const [open, setOpen] = useState(true);
const [searchTerm, setSearchTerm] = useState<string>("");
+ const { data: versionData } = useAppSelector((state) =>
state.session?.versionData || {});
+ const searchParams = new URLSearchParams(location.search);
+
+ const isCustomFilterActive = searchParams.get("isCF") === "true";
+ const isGlossaryActive = !isCustomFilterActive &&
(location.pathname.includes("/glossary") || !!searchParams.get("gtype") ||
!!searchParams.get("term") || !!searchParams.get("category"));
+ const isBusinessMetadataActive = !isCustomFilterActive &&
location.pathname.includes("/administrator/businessMetadata");
+ const isClassificationActive = !isCustomFilterActive &&
(!!searchParams.get("tag") || location.pathname.includes("/tag/tagAttribute"));
+ const isRelationshipActive = !isCustomFilterActive &&
(!!searchParams.get("relationshipName") ||
location.pathname.includes("/relationshipDetailPage"));
+
+ const isEntitiesActive = !isCustomFilterActive &&
(!!searchParams.get("type") || location.pathname.includes("/detailPage"));
+
+ const modules = [
+ { id: "entities", title: "Entities", isActive: isEntitiesActive, iconUrl:
"/img/sidebar-icons/icon-entities.svg", Component: EntitiesTree, isVisible:
true },
+ { id: "classification", title: "Classifications", isActive:
isClassificationActive, iconUrl: "/img/sidebar-icons/icon-classifications.svg",
Component: ClassificationTree, isVisible: true },
+ { id: "glossary", title: "Glossary", isActive: isGlossaryActive, iconUrl:
"/img/sidebar-icons/icon-glossary.svg", Component: GlossaryTree, isVisible:
true },
+ { id: "businessMetadata", title: "Business Metadata", isActive:
isBusinessMetadataActive, iconUrl:
"/img/sidebar-icons/icon-business-metadata.svg", Component:
BusinessMetadataTree, isVisible: true },
+ { id: "relationships", title: "Relationships", isActive:
isRelationshipActive, iconUrl: "/img/sidebar-icons/icon-relationships.svg",
Component: RelationshipsTree, isVisible: !!relationshipSearch },
+ { id: "customFilters", title: "Custom Filters", isActive:
isCustomFilterActive, iconUrl: "/img/sidebar-icons/icon-custom-filters.svg",
Component: CustomFiltersTree, isVisible: true }
+ ];
const handleDrawerOpen = () => {
Review Comment:
Popover state not cleared when drawer expands
line 138–140
handleDrawerOpen() toggles open but does not reset activePopover /
popoverAnchor. If a popover was open, collapsing again can reopen it with a
stale anchor. Call handlePopoverClose() when setOpen(true).
line 353:
Search icon onClick={() => setOpen(true)} has the same issue — expand drawer
should also close any active popover.
##########
dashboard/src/components/EntityDisplayImage.tsx:
##########
@@ -15,86 +15,58 @@
* limitations under the License.
*/
-import { useEffect, useState } from "react";
-import { Avatar, Skeleton } from "@mui/material";
+import { Avatar } from "@mui/material";
import { getEntityIconPath } from "../utils/Utils";
+interface DisplayImageProps {
+ entity: Record<string, unknown>;
+ width?: string | number;
+ height?: string | number;
+ avatarDisplay?: boolean;
+ isProcess?: boolean;
+}
+
const DisplayImage = ({
entity,
width,
height,
avatarDisplay,
isProcess
-}: any) => {
- const [imageUrl, setImageUrl] = useState<any>(null);
- const [checkEntityImage, setCheckEntityImage] = useState<any>({
- [entity.guid]: false
- });
-
- useEffect(() => {
- const fetchImagePath = async () => {
- let entityData = { ...entity, ...{ isProcess: isProcess } };
- let imagePath: any = getEntityIconPath({ entityData: entityData });
- try {
- const response = await fetch(imagePath);
- const contentType: any = response.headers.get("Content-Type");
-
- if (contentType.startsWith("image/")) {
- let cache = { [entityData.guid]: imagePath };
- setCheckEntityImage(cache);
- setImageUrl(getEntityIconPath({ entityData: entityData }));
- } else {
- setImageUrl(
- getEntityIconPath({ entityData: entityData, errorUrl: imagePath })
- );
- }
- } catch (error) {
- setImageUrl(
- getEntityIconPath({ entityData: entityData, errorUrl: imagePath })
- );
- }
- };
+}: DisplayImageProps) => {
+ const entityData = { ...entity, isProcess: isProcess };
+
+ const primaryUrl = getEntityIconPath({ entityData }) || "";
+ const fallbackUrl = getEntityIconPath({ entityData, errorUrl: primaryUrl })
|| "";
- fetchImagePath();
- }, []);
+ const handleError = (e: React.SyntheticEvent<HTMLImageElement, Event>) => {
+ const target = e.currentTarget;
+ if (target.src !== fallbackUrl) {
+ target.onerror = null;
+ target.src = fallbackUrl;
+ }
+ };
- return imageUrl != undefined ? (
+ return (
<div className="search-result-table-name-col" data-cy="entityIcon">
- {checkEntityImage[entity.guid] !== false ? (
- avatarDisplay == undefined ? (
- <img
- className="search-result-table-img"
- id={entity.guid}
- data-cy={entity.guid}
- src={checkEntityImage[entity.guid]}
- alt="Entity Icon"
- />
- ) : (
- <Avatar
- alt="entityImg"
- src={checkEntityImage[entity.guid]}
- sx={{ width: width, height: height }}
- variant="square"
- ></Avatar>
- )
- ) : avatarDisplay == undefined ? (
+ {avatarDisplay === undefined ? (
Review Comment:
mising negative test
When avatarDisplay === undefined, width/height props are ignored on <img>.
Confirm intentional (may affect table layout).
##########
dashboard/src/views/SideBar/SideBarBody.tsx:
##########
@@ -246,38 +323,117 @@ const SideBarBody = (props: {
backgroundColor: "#034858",
}}
>
- {/* Collapsed sidebar logo */}
+ {/* Collapsed sidebar logo and module icons */}
{!open && (
- <div
- style={{
- width: "100%",
- textAlign: "center",
- paddingLeft: "12px",
- display: "flex",
- alignItems: "center",
- justifyContent: "center",
- minHeight: "64px",
- cursor: "pointer",
- boxSizing: "border-box",
- }}
- role="button"
- tabIndex={0}
- aria-label="Atlas home — refresh dashboard"
- onClick={handleAtlasLogoClick}
- onKeyDown={handleAtlasLogoKeyDown}
- data-cy="apache-atlas-logo-collapsed"
+ <Stack
+ alignItems="center"
+ sx={{ width: "100%", flex: 1, minHeight: 0, overflowY: "auto",
overflowX: "hidden", boxSizing: "border-box", pb: "60px" }}
>
- <img
- src={apacheAtlasLogo}
- alt="Apache Atlas logo"
- style={{
- width: "29px",
- height: "auto",
- maxWidth: "100%",
- display: "block",
+ <div
+ className="collapsed-logo-container"
+ role="button"
+ tabIndex={0}
+ aria-label="Atlas home — refresh dashboard"
+ onClick={handleAtlasLogoClick}
+ onKeyDown={handleAtlasLogoKeyDown}
+ data-cy="apache-atlas-logo-collapsed"
+ >
+ <img
+ src={apacheAtlasLogo}
+ alt="Apache Atlas logo"
+ className="collapsed-logo-img"
+ />
+ </div>
+
+ {/* Module Icons for Mini Drawer */}
+ <Stack alignItems="stretch" gap="1rem" sx={{ width: "100%" }}>
+ {/* Search */}
+ <Box sx={{ display: "flex", justifyContent: "center",
borderLeft: "4px solid transparent", borderRight: "4px solid transparent",
background: "transparent" }}>
+ <Tooltip title="Search" placement="right">
+ <IconButton aria-expanded={open} onClick={() =>
setOpen(true)} sx={{ '&:hover': { background: 'rgba(255, 255, 255, 0.1)' } }}>
+ <img src="/img/sidebar-icons/icon-search.svg"
className="sidebar-module-icon" alt="search" />
+ </IconButton>
+ </Tooltip>
+ </Box>
+
+ {modules.filter(m => m.isVisible).map(m => (
+ <Box
+ key={m.id}
+ className={m.isActive ? "sidebar-icon-active" : ""}
+ sx={{
+ display: "flex",
+ justifyContent: "center",
+ borderLeft: "4px solid transparent",
+ borderRight: "4px solid transparent",
+ background: "transparent"
+ }}
+ >
+ <Tooltip title={m.title} placement="right">
+ <IconButton onClick={(e) => handlePopoverOpen(e, m.id)}
sx={{ color: m.isActive ? "white" : "rgba(255, 255, 255, 0.6)", '&:hover': {
color: 'white', background: 'rgba(255, 255, 255, 0.1)' } }}>
+ <img src={m.iconUrl} className="sidebar-module-icon"
alt={m.title.toLowerCase()} />
+ </IconButton>
+ </Tooltip>
+ </Box>
+ ))}
+ </Stack>
Review Comment:
Inline { color: "#D3D3D3", fontWeight: "600" } duplicates
.sidebar-tree-highlight in sidebar.scss:329-332. Use the CSS class for
consistency.
##########
dashboard/src/views/SideBar/SideBarBody.tsx:
##########
@@ -101,61 +101,101 @@ const DrawerHeader = styled("div")(({ theme }) => ({
marginBottom: "1rem",
}));
+
const SideBarBody = (props: {
- loading: boolean;
- handleOpenModal: any;
- handleOpenAboutModal: any;
+ handleOpenModal: () => void;
+ handleOpenAboutModal: () => void;
}) => {
const location = useLocation();
const routes = useRoutes(AppRoutes as RouteObject[]);
const history = useHistory();
const dispatch = useAppDispatch();
- const { loading: loader, handleOpenModal, handleOpenAboutModal } = props;
+ const { handleOpenModal, handleOpenAboutModal } = props;
const navigate = useNavigate();
- const { loading } = useSelector((state: TypeHeaderState) =>
state.typeHeader);
const { relationshipSearch = {} } = globalSessionData || {};
const [open, setOpen] = useState(true);
const [searchTerm, setSearchTerm] = useState<string>("");
+ const { data: versionData } = useAppSelector((state) =>
state.session?.versionData || {});
+ const searchParams = new URLSearchParams(location.search);
+
+ const isCustomFilterActive = searchParams.get("isCF") === "true";
+ const isGlossaryActive = !isCustomFilterActive &&
(location.pathname.includes("/glossary") || !!searchParams.get("gtype") ||
!!searchParams.get("term") || !!searchParams.get("category"));
+ const isBusinessMetadataActive = !isCustomFilterActive &&
location.pathname.includes("/administrator/businessMetadata");
+ const isClassificationActive = !isCustomFilterActive &&
(!!searchParams.get("tag") || location.pathname.includes("/tag/tagAttribute"));
+ const isRelationshipActive = !isCustomFilterActive &&
(!!searchParams.get("relationshipName") ||
location.pathname.includes("/relationshipDetailPage"));
+
+ const isEntitiesActive = !isCustomFilterActive &&
(!!searchParams.get("type") || location.pathname.includes("/detailPage"));
+
+ const modules = [
Review Comment:
low:
modules array recreated every render. Optional useMemo on [location,
relationshipSearch].
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]