({
// Mock MUI components
jest.mock('@mui/material', () => ({
- Stack: ({ children, spacing, direction, ...props }: any) => (
+ Stack: ({ children, spacing, direction, ...props }: Record
) => (
({
{children}
),
- Typography: ({ children, variant, color, ...props }: any) => (
+ Typography: ({ children, variant, color, ...props }: Record) => (
({
{children}
),
- List: ({ children, dense, ...props }: any) => (
+ List: ({ children, dense, ...props }: Record) => (
{children}
),
- ListItem: ({ children, button, component, href, target, ...props }: any) => (
+ ListItem: ({ children, button, component, href, target, ...props }: Record) => (
({
{children}
),
- ListItemText: ({ primary, ...props }: any) => (
+ ListItemText: ({ primary, ...props }: Record) => (
{primary}
)
}))
-// Mock Utils
-const mockServerError = jest.fn()
-jest.mock('@utils/Utils', () => ({
- serverError: (...args: any[]) => mockServerError(...args)
-}))
-
-// Mock console.error to avoid noise in test output
-const originalConsoleError = console.error
-beforeAll(() => {
- console.error = jest.fn()
-})
-
-afterAll(() => {
- console.error = originalConsoleError
-})
-
describe('About', () => {
+ let useAppSelectorSpy: jest.SpyInstance
+
beforeEach(() => {
jest.clearAllMocks()
- mockGetVersion.mockClear()
- mockServerError.mockClear()
+ useAppSelectorSpy = jest.spyOn(reducerHook, 'useAppSelector')
})
- it('should render skeleton loader when loading', async () => {
- mockGetVersion.mockImplementation(() => new Promise(() => {})) // Never resolves
+ it('should render skeleton loader when loading', () => {
+ useAppSelectorSpy.mockReturnValue({ data: {}, loading: true })
render()
@@ -132,24 +113,17 @@ describe('About', () => {
expect(skeletonLoader).toHaveAttribute('data-width', '100%')
})
- it('should render version data when API call succeeds', async () => {
+ it('should render version data when loading is false and data is available', () => {
const mockVersionData = {
Version: '3.0.0-SNAPSHOT',
Description: 'Metadata Management Platform',
Revision: 'abc123'
}
- mockGetVersion.mockResolvedValue({
- data: mockVersionData
- })
+ useAppSelectorSpy.mockReturnValue({ data: mockVersionData, loading: false })
render()
- // Wait for loading to finish
- await waitFor(() => {
- expect(screen.queryByTestId('skeleton-loader')).not.toBeInTheDocument()
- })
-
// Check version is displayed
expect(screen.getByText(/Version:/i)).toBeInTheDocument()
expect(screen.getByText('3.0.0-SNAPSHOT')).toBeInTheDocument()
@@ -169,17 +143,11 @@ describe('About', () => {
expect(listItem).toHaveAttribute('data-target', '_blank')
})
- it('should render empty version when API returns empty data object', async () => {
- mockGetVersion.mockResolvedValue({
- data: {}
- })
+ it('should render empty version when data object is empty', () => {
+ useAppSelectorSpy.mockReturnValue({ data: {}, loading: false })
render()
- await waitFor(() => {
- expect(screen.queryByTestId('skeleton-loader')).not.toBeInTheDocument()
- })
-
// Version should be displayed but empty
expect(screen.getByText(/Version:/i)).toBeInTheDocument()
const versionTypography = screen.getByTestId('typography-body1')
@@ -187,235 +155,46 @@ describe('About', () => {
expect(versionTypography.textContent).toContain('Version:')
})
- it('should render empty version when API returns undefined data', async () => {
- mockGetVersion.mockResolvedValue({
- data: undefined
- })
+ it('should render empty version when data is undefined', () => {
+ useAppSelectorSpy.mockReturnValue({ data: undefined, loading: false })
render()
- await waitFor(() => {
- expect(screen.queryByTestId('skeleton-loader')).not.toBeInTheDocument()
- })
-
expect(screen.getByText(/Version:/i)).toBeInTheDocument()
})
- it('should render empty version when API returns null data', async () => {
- mockGetVersion.mockResolvedValue({
- data: null
- })
+ it('should render empty version when data is null', () => {
+ useAppSelectorSpy.mockReturnValue({ data: null, loading: false })
render()
- await waitFor(() => {
- expect(screen.queryByTestId('skeleton-loader')).not.toBeInTheDocument()
- })
-
expect(screen.getByText(/Version:/i)).toBeInTheDocument()
})
- it('should handle API call error and call serverError', async () => {
- const mockError = new Error('Network error')
- mockGetVersion.mockRejectedValue(mockError)
+ it('should handle versionData with undefined Version property', () => {
+ useAppSelectorSpy.mockReturnValue({ data: { Description: 'Some description' }, loading: false })
render()
- await waitFor(() => {
- expect(mockServerError).toHaveBeenCalledWith(mockError, expect.any(Object))
- })
-
- // Should still show content (not skeleton) after error
- await waitFor(() => {
- expect(screen.queryByTestId('skeleton-loader')).not.toBeInTheDocument()
- })
-
- expect(screen.getByText(/Version:/i)).toBeInTheDocument()
- })
-
- it('should handle API call error with response data', async () => {
- const mockError = {
- response: {
- data: {
- errorMessage: 'Server error occurred'
- }
- }
- }
- mockGetVersion.mockRejectedValue(mockError)
-
- render()
-
- await waitFor(() => {
- expect(mockServerError).toHaveBeenCalledWith(mockError, expect.any(Object))
- })
-
- await waitFor(() => {
- expect(screen.queryByTestId('skeleton-loader')).not.toBeInTheDocument()
- })
- })
-
- it('should call getVersion on component mount', async () => {
- mockGetVersion.mockResolvedValue({
- data: { Version: '1.0.0' }
- })
-
- render()
-
- expect(mockGetVersion).toHaveBeenCalledTimes(1)
- expect(mockGetVersion).toHaveBeenCalledWith()
-
- await waitFor(() => {
- expect(screen.queryByTestId('skeleton-loader')).not.toBeInTheDocument()
- })
- })
-
- it('should render correct Typography variants and colors', async () => {
- mockGetVersion.mockResolvedValue({
- data: { Version: '2.0.0' }
- })
-
- render()
-
- await waitFor(() => {
- expect(screen.queryByTestId('skeleton-loader')).not.toBeInTheDocument()
- })
-
- // Check Typography variants
- const body1Typography = screen.getByTestId('typography-body1')
- expect(body1Typography).toBeInTheDocument()
-
- const body2Typographies = screen.getAllByTestId('typography-body2')
- expect(body2Typographies.length).toBeGreaterThan(0)
-
- // Check color prop for "Get involved!" text
- const getInvolvedTypography = body2Typographies.find(
- (el) => el.textContent === 'Get involved!'
- )
- expect(getInvolvedTypography).toHaveAttribute('data-color', 'info.main')
- })
-
- it('should render Stack components with correct props', async () => {
- mockGetVersion.mockResolvedValue({
- data: { Version: '1.0.0' }
- })
-
- render()
-
- await waitFor(() => {
- expect(screen.queryByTestId('skeleton-loader')).not.toBeInTheDocument()
- })
-
- // Check main Stack
- const mainStack = screen.getByTestId('stack')
- expect(mainStack).toHaveAttribute('data-spacing', '2')
-
- // Check column Stack
- const columnStack = screen.getByTestId('stack-column')
- expect(columnStack).toHaveAttribute('data-spacing', '1')
- expect(columnStack).toHaveAttribute('data-direction', 'column')
- })
-
- it('should render List with dense prop', async () => {
- mockGetVersion.mockResolvedValue({
- data: { Version: '1.0.0' }
- })
-
- render()
-
- await waitFor(() => {
- expect(screen.queryByTestId('skeleton-loader')).not.toBeInTheDocument()
- })
-
- const list = screen.getByTestId('list')
- expect(list).toHaveAttribute('data-dense', 'true')
- })
-
- it('should render ListItemText with correct primary text', async () => {
- mockGetVersion.mockResolvedValue({
- data: { Version: '1.0.0' }
- })
-
- render()
-
- await waitFor(() => {
- expect(screen.queryByTestId('skeleton-loader')).not.toBeInTheDocument()
- })
-
- const listItemText = screen.getByTestId('list-item-text')
- expect(listItemText).toHaveAttribute(
- 'data-primary',
- 'Licensed under the Apache License Version 2.0'
- )
- })
-
- it('should handle versionData with undefined Version property', async () => {
- mockGetVersion.mockResolvedValue({
- data: { Description: 'Some description' }
- })
-
- render()
-
- await waitFor(() => {
- expect(screen.queryByTestId('skeleton-loader')).not.toBeInTheDocument()
- })
-
expect(screen.getByText(/Version:/i)).toBeInTheDocument()
const versionTypography = screen.getByTestId('typography-body1')
expect(versionTypography.textContent).toContain('Version:')
expect(versionTypography.textContent).not.toContain('undefined')
})
- it('should set loader to false after successful API call', async () => {
- mockGetVersion.mockResolvedValue({
- data: { Version: '1.0.0' }
- })
+ it('should render gracefully on Redux error state', () => {
+ useAppSelectorSpy.mockReturnValue({ data: null, loading: false, error: 'Network error' })
render()
- // Initially should show loader
- expect(screen.getByTestId('skeleton-loader')).toBeInTheDocument()
-
- // After API resolves, loader should be hidden
- await waitFor(() => {
- expect(screen.queryByTestId('skeleton-loader')).not.toBeInTheDocument()
- })
- })
-
- it('should set loader to false after failed API call', async () => {
- mockGetVersion.mockRejectedValue(new Error('API Error'))
-
- render()
-
- // Initially should show loader
- expect(screen.getByTestId('skeleton-loader')).toBeInTheDocument()
-
- // After API rejects, loader should be hidden
- await waitFor(() => {
- expect(screen.queryByTestId('skeleton-loader')).not.toBeInTheDocument()
- })
- })
-
- it('should handle API response with null response object', async () => {
- mockGetVersion.mockResolvedValue(null)
-
- render()
-
- await waitFor(() => {
- expect(screen.queryByTestId('skeleton-loader')).not.toBeInTheDocument()
- })
-
- expect(screen.getByText(/Version:/i)).toBeInTheDocument()
- })
-
- it('should handle API response with undefined response object', async () => {
- mockGetVersion.mockResolvedValue(undefined)
-
- render()
-
- await waitFor(() => {
- expect(screen.queryByTestId('skeleton-loader')).not.toBeInTheDocument()
- })
-
+ // Verify no skeleton is stuck
+ expect(screen.queryByTestId('skeleton-loader')).not.toBeInTheDocument()
+
+ // Verify exact error message
+ expect(screen.getByText('Unknown (failed to fetch version)')).toBeInTheDocument()
+
+ // Verify no crash, UI still renders
expect(screen.getByText(/Version:/i)).toBeInTheDocument()
+ expect(screen.getByText('Get involved!')).toBeInTheDocument()
})
})
diff --git a/dashboard/src/views/Layout/__tests__/DebugMetrics.test.tsx b/dashboard/src/views/Layout/__tests__/DebugMetrics.test.tsx
index ffc296dd8ad..ac9335e9053 100644
--- a/dashboard/src/views/Layout/__tests__/DebugMetrics.test.tsx
+++ b/dashboard/src/views/Layout/__tests__/DebugMetrics.test.tsx
@@ -24,6 +24,7 @@
import React from 'react'
import { render, screen, fireEvent, waitFor } from '@utils/test-utils'
+import '@testing-library/jest-dom'
import { ThemeProvider, createTheme } from '@mui/material/styles'
import DebugMetrics from '../DebugMetrics'
@@ -35,21 +36,21 @@ jest.mock('@api/apiMethods/metricsApiMethods', () => ({
// Mock MUI components
jest.mock('@components/muiComponents', () => ({
- AutorenewIcon: ({ className }: any) => AutorenewIcon
,
- CustomButton: ({ children, onClick, variant, size, 'data-cy': dataCy }: any) => (
+ AutorenewIcon: ({ className }: Record) => AutorenewIcon
,
+ CustomButton: ({ children, onClick, variant, size, 'data-cy': dataCy }: Record) => (
),
- LightTooltip: ({ children, title }: any) => (
+ LightTooltip: ({ children, title }: Record) => (
{children}
),
- LinkTab: ({ label }: any) => {label}
+ LinkTab: ({ label }: Record) => {label}
}))
// Mock TableLayout component
jest.mock('@components/Table/TableLayout', () => ({
- TableLayout: ({ data, columns, emptyText, isFetching, columnVisibility, clientSideSorting, columnSort, showPagination, showRowSelection, tableFilters }: any) => (
+ TableLayout: ({ data, columns, emptyText, isFetching, columnVisibility, clientSideSorting, columnSort, showPagination, showRowSelection, tableFilters }: Record) => (
{isFetching &&
Loading...
}
{!isFetching && (!data || data.length === 0) &&
{emptyText}
}
@@ -82,21 +83,21 @@ jest.mock('@components/Table/TableLayout', () => ({
// Mock MUI components
jest.mock('@mui/material', () => ({
Divider: () =>
Divider
,
- Grid: ({ children, container }: any) =>
{children}
,
- List: ({ children, className }: any) =>
{children}
,
- ListItem: ({ children, className }: any) =>
{children}
,
- ListItemText: ({ primary, secondary }: any) => (
+ Grid: ({ children, container }: Record
) => {children}
,
+ List: ({ children, className }: Record) => {children}
,
+ ListItem: ({ children, className }: Record) => {children}
,
+ ListItemText: ({ primary, secondary }: Record) => (
),
- Stack: ({ children, ...props }: any) => {children}
,
- styled: (component: any) => (styles: any) => component,
- Tabs: ({ children, value, className, 'data-cy': dataCy }: any) => (
+ Stack: ({ children, ...props }: Record) => {children}
,
+ styled: (component: Record) => (styles: Record) => component,
+ Tabs: ({ children, value, className, 'data-cy': dataCy }: Record) => (
{children}
),
- Tooltip: ({ children, title, arrow, placement, classes }: any) => {
+ Tooltip: ({ children, title, arrow, placement, classes }: Record) => {
// Call the styled component's theme function to cover line 53
const theme = createTheme()
const styledStyles = {
@@ -118,7 +119,7 @@ jest.mock('@mui/material', () => ({
tooltipClasses: {
tooltip: 'tooltip-class'
},
- Typography: ({ children, color, className }: any) => (
+ Typography: ({ children, color, className }: Record) => (
{children}
)
}))
@@ -136,9 +137,9 @@ const mockCustomSortBy = jest.fn()
const mockServerError = jest.fn()
jest.mock('@utils/Utils', () => ({
- isEmpty: (val: any) => mockIsEmpty(val),
+ isEmpty: (val: Record) => mockIsEmpty(val),
customSortBy: (arr: any, keys: string[]) => mockCustomSortBy(arr, keys),
- serverError: (error: any, toastId: any) => mockServerError(error, toastId)
+ serverError: (error: any, toastId: Record) => mockServerError(error, toastId)
}))
// Mock moment
@@ -153,7 +154,7 @@ jest.mock('moment', () => {
// Mock Item component
jest.mock('@utils/Muiutils', () => ({
- Item: ({ children, variant, className }: any) => (
+ Item: ({ children, variant, className }: Record) => (
{children}
)
}))
@@ -193,14 +194,14 @@ describe('DebugMetrics', () => {
beforeEach(() => {
jest.clearAllMocks()
- mockIsEmpty.mockImplementation((val: any) => {
+ mockIsEmpty.mockImplementation((val: Record) => {
if (val == null) return true
if (Array.isArray(val)) return val.length === 0
if (typeof val === 'object') return Object.keys(val).length === 0
if (val === '') return true
return false
})
- mockCustomSortBy.mockImplementation((arr: any) => arr || [])
+ mockCustomSortBy.mockImplementation((arr: Record) => arr || [])
mockMomentNow.mockReturnValue(1234567890)
mockGetDebugMetrics.mockResolvedValue({
data: mockDebugMetricsData
@@ -354,7 +355,7 @@ describe('DebugMetrics', () => {
}
}
mockGetDebugMetrics.mockResolvedValue({ data: dataWithEmptyName })
- mockIsEmpty.mockImplementation((val: any) => val === '')
+ mockIsEmpty.mockImplementation((val: Record) => val === '')
mockCustomSortBy.mockReturnValue([dataWithEmptyName['api1']])
render()
@@ -375,7 +376,7 @@ describe('DebugMetrics', () => {
}
}
mockGetDebugMetrics.mockResolvedValue({ data: dataWithNullName })
- mockIsEmpty.mockImplementation((val: any) => val == null)
+ mockIsEmpty.mockImplementation((val: Record) => val == null)
mockCustomSortBy.mockReturnValue([dataWithNullName['api1']])
render()
@@ -406,7 +407,7 @@ describe('DebugMetrics', () => {
}
}
mockGetDebugMetrics.mockResolvedValue({ data: dataWithEmptyNumops })
- mockIsEmpty.mockImplementation((val: any) => val == null)
+ mockIsEmpty.mockImplementation((val: Record) => val == null)
mockCustomSortBy.mockReturnValue([dataWithEmptyNumops['api1']])
render()
@@ -438,7 +439,7 @@ describe('DebugMetrics', () => {
}
}
mockGetDebugMetrics.mockResolvedValue({ data: dataWithEmptyMinTime })
- mockIsEmpty.mockImplementation((val: any) => val == null)
+ mockIsEmpty.mockImplementation((val: Record) => val == null)
mockCustomSortBy.mockReturnValue([dataWithEmptyMinTime['api1']])
render()
@@ -491,7 +492,7 @@ describe('DebugMetrics', () => {
}
}
mockGetDebugMetrics.mockResolvedValue({ data: dataWithEmptyMaxTime })
- mockIsEmpty.mockImplementation((val: any) => val == null)
+ mockIsEmpty.mockImplementation((val: Record) => val == null)
mockCustomSortBy.mockReturnValue([dataWithEmptyMaxTime['api1']])
render()
@@ -544,7 +545,7 @@ describe('DebugMetrics', () => {
}
}
mockGetDebugMetrics.mockResolvedValue({ data: dataWithEmptyAvgTime })
- mockIsEmpty.mockImplementation((val: any) => val == null)
+ mockIsEmpty.mockImplementation((val: Record) => val == null)
mockCustomSortBy.mockReturnValue([dataWithEmptyAvgTime['api1']])
render()
@@ -673,7 +674,7 @@ describe('DebugMetrics', () => {
it('should handle empty debugMetricsData object', async () => {
mockGetDebugMetrics.mockResolvedValue({ data: {} })
- mockIsEmpty.mockImplementation((val: any) => {
+ mockIsEmpty.mockImplementation((val: Record) => {
if (val == null) return true
if (typeof val === 'object' && Object.keys(val).length === 0) return true
return false
@@ -1052,7 +1053,7 @@ describe('DebugMetrics', () => {
}
}
mockGetDebugMetrics.mockResolvedValue({ data: dataWithEmptyStringName })
- mockIsEmpty.mockImplementation((val: any) => val === '')
+ mockIsEmpty.mockImplementation((val: Record) => val === '')
mockCustomSortBy.mockReturnValue([dataWithEmptyStringName['api1']])
render()
@@ -1073,7 +1074,7 @@ describe('DebugMetrics', () => {
}
}
mockGetDebugMetrics.mockResolvedValue({ data: dataWithUndefinedName })
- mockIsEmpty.mockImplementation((val: any) => val == null)
+ mockIsEmpty.mockImplementation((val: Record) => val == null)
mockCustomSortBy.mockReturnValue([dataWithUndefinedName['api1']])
render()
@@ -1094,7 +1095,7 @@ describe('DebugMetrics', () => {
}
}
mockGetDebugMetrics.mockResolvedValue({ data: dataWithAllNullTimes })
- mockIsEmpty.mockImplementation((val: any) => val == null)
+ mockIsEmpty.mockImplementation((val: Record) => val == null)
mockCustomSortBy.mockReturnValue([dataWithAllNullTimes['api1']])
render()
@@ -1116,7 +1117,7 @@ describe('DebugMetrics', () => {
}
}
mockGetDebugMetrics.mockResolvedValue({ data: dataWithAllEmpty })
- mockIsEmpty.mockImplementation((val: any) => val == null)
+ mockIsEmpty.mockImplementation((val: Record) => val == null)
mockCustomSortBy.mockReturnValue([dataWithAllEmpty['api1']])
render()
@@ -1199,7 +1200,7 @@ describe('DebugMetrics', () => {
it('should handle API response with empty data object', async () => {
mockGetDebugMetrics.mockResolvedValue({ data: {} })
- mockIsEmpty.mockImplementation((val: any) => {
+ mockIsEmpty.mockImplementation((val: Record) => {
if (val == null) return true
if (typeof val === 'object' && Object.keys(val).length === 0) return true
return false
diff --git a/dashboard/src/views/SideBar/SideBarBody.tsx b/dashboard/src/views/SideBar/SideBarBody.tsx
index 91b5bda096f..2e64f2cbd5a 100644
--- a/dashboard/src/views/SideBar/SideBarBody.tsx
+++ b/dashboard/src/views/SideBar/SideBarBody.tsx
@@ -15,17 +15,20 @@
* limitations under the License.
*/
+import { createPortal } from "react-dom";
import { styled } from "@mui/material/styles";
import {
Suspense,
useCallback,
useEffect,
useState,
- ChangeEvent,
KeyboardEvent,
lazy,
useRef,
+ useMemo,
} from "react";
+import TreeSkeletonLoader from "@components/TreeSkeletonLoader";
+import { SidebarSearchInput } from "@components/SidebarSearchInput";
import atlasLogo from "/img/atlas_logo.svg";
import apacheAtlasLogo from "/img/apache-atlas-logo.svg";
import {
@@ -39,25 +42,23 @@ import {
import Drawer from "@mui/material/Drawer";
import CssBaseline from "@mui/material/CssBaseline";
import { IconButton } from "@components/muiComponents";
-import { useSelector } from "react-redux";
-import SearchIcon from "@mui/icons-material/Search";
-import { InputBase, Paper, Stack } from "@mui/material";
-import { TypeHeaderState } from "@models/treeStructureType.js";
+
+import { Paper, Stack, Box, Popover, Typography, Tooltip, CircularProgress } from "@mui/material";
import { globalSessionData, PathAssociateWithModule } from "@utils/Enum";
import KeyboardDoubleArrowLeftIcon from "@mui/icons-material/KeyboardDoubleArrowLeft";
import KeyboardDoubleArrowRightIcon from "@mui/icons-material/KeyboardDoubleArrowRight";
-import { useAppDispatch } from "@hooks/reducerHook";
+import { useAppDispatch, useAppSelector } from "@hooks/reducerHook";
import { fetchEnumData } from "@redux/slice/enumSlice";
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 { fetchVersionData } from "@redux/slice/sessionSlice";
import { refreshDashboardHomeData } from "@utils/refreshDashboardHome";
import ErrorPage from "@views/ErrorPage";
import AppRoutes from "@views/AppRoutes";
import ErrorBoundaryWithNavigate from "../../ErrorBoundary";
import useHistory from "@utils/history.js";
-import SkeletonLoader from "@components/SkeletonLoader";
const Header = lazy(() => import("@views/Layout/Header"));
@@ -74,23 +75,14 @@ const CustomFiltersTree = lazy(() => import("./SideBarTree/CustomFiltersTree"));
export const defaultDrawerWidth = "20%";
-const Main = styled("main", { shouldForwardProp: (prop) => prop !== "open" })<{
- open?: boolean;
-}>(({ theme, open }) => ({
+const Main = styled("main")(({ theme }) => ({
flexGrow: 1,
- padding: theme.spacing(3),
- transition: theme.transitions.create("margin", {
+ minWidth: 0,
+ padding: 0,
+ transition: theme.transitions.create(["margin", "width"], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
- marginLeft: `-${defaultDrawerWidth}`,
- ...(open && {
- transition: theme.transitions.create("margin", {
- easing: theme.transitions.easing.easeOut,
- duration: theme.transitions.duration.enteringScreen,
- }),
- marginLeft: 0,
- }),
}));
const DrawerHeader = styled("div")(({ theme }) => ({
@@ -101,61 +93,122 @@ 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 relationshipSearch = Boolean(globalSessionData?.relationshipSearch);
const [open, setOpen] = useState(true);
const [searchTerm, setSearchTerm] = useState("");
+ const { data: versionData, loading: isVersionLoading, error: versionError } = useAppSelector((state) => state.session?.versionData || {});
+ const activeModule = useMemo(() => {
+ const searchParams = new URLSearchParams(location.search);
+ if (searchParams.get("isCF") === "true") return "customFilters";
+ if (location.pathname.includes("/glossary") || !!searchParams.get("gtype") || !!searchParams.get("term") || !!searchParams.get("category")) return "glossary";
+ if (location.pathname.includes("/administrator/businessMetadata")) return "businessMetadata";
+ if (!!searchParams.get("tag") || location.pathname.includes("/tag/tagAttribute")) return "classification";
+ if (!!searchParams.get("relationshipName") || location.pathname.includes("/relationshipDetailPage")) return "relationships";
+ if (!!searchParams.get("type") || location.pathname.includes("/detailPage")) return "entities";
+ return null;
+ }, [location.pathname, location.search]);
+
+ const isCustomFilterActive = activeModule === "customFilters";
+ const isGlossaryActive = activeModule === "glossary";
+ const isBusinessMetadataActive = activeModule === "businessMetadata";
+ const isClassificationActive = activeModule === "classification";
+ const isRelationshipActive = activeModule === "relationships";
+ const isEntitiesActive = activeModule === "entities";
+
+ const modules = useMemo(() => [
+ { 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 }
+ ], [
+ isEntitiesActive,
+ isClassificationActive,
+ isGlossaryActive,
+ isBusinessMetadataActive,
+ isRelationshipActive,
+ isCustomFilterActive,
+ relationshipSearch
+ ]);
+
+ const [popoverAnchor, setPopoverAnchor] = useState(null);
+ const [activePopover, setActivePopover] = useState(null);
+ const [popoverMaxHeight, setPopoverMaxHeight] = useState("calc(100vh - 100px)");
+ const [isBottomHalf, setIsBottomHalf] = useState(false);
+ const [popoverContainer, setPopoverContainer] = useState(null);
+ const [sidebarRefs, setSidebarRefs] = useState>({});
+ const refCallbacks = useRef void>>({});
+ const setSidebarRef = useCallback((id: string) => {
+ if (!refCallbacks.current[id]) {
+ refCallbacks.current[id] = (el: HTMLDivElement | null) => {
+ setSidebarRefs(prev => prev[id] === el ? prev : { ...prev, [id]: el });
+ };
+ }
+ return refCallbacks.current[id];
+ }, []);
- const handleDrawerOpen = () => {
- setOpen(!open);
- };
- const [position, setPosition] = useState(defaultDrawerWidth);
- const draggerRef = useRef(null);
- const headerRef = useRef(null);
- const windowWidth = window.innerWidth;
- const minPosition = 300;
- const maxPosition = windowWidth * 0.6;
+ const handlePopoverOpen = (event: React.MouseEvent, id: string) => {
+ const target = event.currentTarget;
- const handleMouseMove = (e: MouseEvent) => {
- let newPosition = e.clientX;
+ setPopoverAnchor(target);
+ setActivePopover(id);
- if (newPosition < minPosition) {
- newPosition = minPosition;
- } else if (newPosition > maxPosition) {
- newPosition = maxPosition;
- }
+ // 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);
- setPosition(newPosition);
+ if (isBottom) {
+ const spaceAbove = rect.bottom - 24;
+ setPopoverMaxHeight(`${Math.max(250, spaceAbove)}px`);
+ } else {
+ setPopoverMaxHeight(`${Math.max(250, spaceBelow)}px`);
+ }
};
- const handleMouseUp = () => {
- window.removeEventListener("mousemove", handleMouseMove);
- window.removeEventListener("mouseup", handleMouseUp);
+ const handlePopoverClose = () => {
+ setPopoverAnchor(null);
+ setActivePopover(null);
};
- const handleMouseDown = () => {
- window.addEventListener("mousemove", handleMouseMove);
- window.addEventListener("mouseup", handleMouseUp);
+ const handleDrawerOpen = () => {
+ setOpen(!open);
+ if (!open) {
+ handlePopoverClose();
+ }
};
+
+
+ const renderPopoverSearch = () => (
+
+
+
+ );
+
+ const headerRef = useRef(null);
+
useEffect(() => {
dispatch(fetchTypeHeaderData());
dispatch(fetchRootEntity());
dispatch(fetchRootClassification());
dispatch(fetchEnumData());
dispatch(fetchMetricEntity());
+ dispatch(fetchVersionData());
}, [dispatch]);
const handleAtlasLogoClick = useCallback(() => {
@@ -179,15 +232,7 @@ const SideBarBody = (props: {
[handleAtlasLogoClick]
);
- useEffect(() => {
- const draggerElement = draggerRef.current;
- draggerElement?.addEventListener("mousedown", handleMouseDown);
-
- return () => {
- draggerElement?.removeEventListener("mousedown", handleMouseDown);
- };
- }, []);
const routeConfig = Object.keys(PathAssociateWithModule).map((key) => {
return {
@@ -199,97 +244,143 @@ const SideBarBody = (props: {
});
const matched = matchRoutes(routeConfig, location.pathname);
+ const isMatched = !!matched;
+
+ const rightSideContent = useMemo(() => (
+
+
+
+
+
+
+
+ {isMatched || location.pathname.includes("!") ? (
+
+
+
+ }
+ >
+
+ {" "}
+
+
+ ) : (
+
+ )}
+
+
+ ), [isMatched, location.pathname, history, handleOpenModal, handleOpenAboutModal]);
return (
-
- {/* Collapsed sidebar logo */}
+
+ {/* Collapsed sidebar logo and module icons */}
{!open && (
-
-

+

+
+
+ {/* Module Icons for Mini Drawer */}
+
+ {/* Search */}
+
+
+ { setOpen(true); handlePopoverClose(); }} className="sidebar-module-btn">
+
+
+
+
+
+ {modules.filter(m => m.isVisible).map(m => (
+
+
+ handlePopoverOpen(e, m.id)} className={`sidebar-module-btn ${m.isActive ? "active" : ""}`}>
+
+
+
+
+ ))}
+
+
+
-
+ >
+ {renderPopoverSearch()}
+
-
-
- //
- //
- }
- >
-
-
-
-
-
-
- //
- //
- }
- >
-
-
-
-
-
-
- //
- //
- }
- >
-
-
-
+
+ }
+ >
+ {sidebarRefs["entities"] &&
+ createPortal(
+ ,
+ (!open && activePopover === "entities" && popoverContainer)
+ ? popoverContainer
+ : sidebarRefs["entities"]
+ )}
+
+
+
+
+ }
+ >
+ {sidebarRefs["classification"] &&
+ createPortal(
+ ,
+ (!open && activePopover === "classification" && popoverContainer)
+ ? popoverContainer
+ : sidebarRefs["classification"]
+ )}
+
+
+
+
+ }
+ >
+ {sidebarRefs["glossary"] &&
+ createPortal(
+ ,
+ (!open && activePopover === "glossary" && popoverContainer)
+ ? popoverContainer
+ : sidebarRefs["glossary"]
+ )}
+
+
+
+
+ }
+ >
+ {sidebarRefs["businessMetadata"] &&
+ createPortal(
+ ,
+ (!open && activePopover === "businessMetadata" && popoverContainer)
+ ? popoverContainer
+ : sidebarRefs["businessMetadata"]
+ )}
+
+
+ {relationshipSearch && (
+
+ }
+ >
+ {sidebarRefs["relationships"] &&
+ createPortal(
+ ,
+ (!open && activePopover === "relationships" && popoverContainer)
+ ? popoverContainer
+ : sidebarRefs["relationships"]
+ )}
+
+
+ )}
-
-
- //
- //
- }
- >
-
-
-
- {relationshipSearch && (
-
-
- //
- //
- }
+
-
-
+ }
+ >
+ {sidebarRefs["customFilters"] &&
+ createPortal(
+ ,
+ (!open && activePopover === "customFilters" && popoverContainer)
+ ? popoverContainer
+ : sidebarRefs["customFilters"]
+ )}
+
+
+
+
+ {open && (
+
+
+ {isVersionLoading ? (
+
+ ) : versionData?.Version ? (
+ `V ${versionData.Version}`
+ ) : versionError ? (
+ 'Version unavailable'
+ ) : (
+ ''
+ )}
+
)}
-
-
- //
- //
- }
- >
-
-
-
-
-
-
handleDrawerOpen()}>
+ handleDrawerOpen()}>
{open ? (
) : (
)}
@@ -509,93 +580,9 @@ const SideBarBody = (props: {
-
-
-
-
-
-
-
- {matched || location.pathname.includes("!") ? (
-
-
- {/* */}
-
- }
- >
-
- {" "}
-
-
- ) : (
-
- )}
-
-
+ {rightSideContent}
);
diff --git a/dashboard/src/views/SideBar/SideBarTree/BusinessMetadataTree.tsx b/dashboard/src/views/SideBar/SideBarTree/BusinessMetadataTree.tsx
index c7fdb84bfe0..f737289b8e8 100644
--- a/dashboard/src/views/SideBar/SideBarTree/BusinessMetadataTree.tsx
+++ b/dashboard/src/views/SideBar/SideBarTree/BusinessMetadataTree.tsx
@@ -66,6 +66,7 @@ const BusinessMetadataTree = (props: Props) => {
sideBarOpen={sideBarOpen}
loader={loading}
searchTerm={searchTerm}
+ isPopover={props.isPopover}
/>
);
};
diff --git a/dashboard/src/views/SideBar/SideBarTree/ClassificationTree.tsx b/dashboard/src/views/SideBar/SideBarTree/ClassificationTree.tsx
index db8e66b087b..c01798ef8cf 100644
--- a/dashboard/src/views/SideBar/SideBarTree/ClassificationTree.tsx
+++ b/dashboard/src/views/SideBar/SideBarTree/ClassificationTree.tsx
@@ -254,6 +254,7 @@ const ClassificationTree = (props: Props) => {
sideBarOpen={sideBarOpen}
loader={loadingClassification}
searchTerm={searchTerm}
+ isPopover={props.isPopover}
/>
);
};
diff --git a/dashboard/src/views/SideBar/SideBarTree/CustomFiltersTree.tsx b/dashboard/src/views/SideBar/SideBarTree/CustomFiltersTree.tsx
index 72c6d3a0eff..0b11f316a79 100644
--- a/dashboard/src/views/SideBar/SideBarTree/CustomFiltersTree.tsx
+++ b/dashboard/src/views/SideBar/SideBarTree/CustomFiltersTree.tsx
@@ -38,12 +38,12 @@ import {
import { fetchSavedSearchData } from "@redux/slice/savedSearchSlice.ts";
import { globalSessionData } from "@utils/Enum.ts";
-const CustomFiltersTree = ({ sideBarOpen, searchTerm }: Props) => {
+const CustomFiltersTree = ({ sideBarOpen, searchTerm, isPopover }: Props) => {
const dispatch = useAppDispatch();
const { savedSearchData }: any = useAppSelector(
(state: any) => state.savedSearch
);
- const { relationshipSearch = {} } = globalSessionData || {};
+ const { relationshipSearch = false } = globalSessionData || {};
const [savedSearchTypeData, setSavedSearchTypeData] = useState<
SavedSearchArrType
@@ -174,6 +174,7 @@ const CustomFiltersTree = ({ sideBarOpen, searchTerm }: Props) => {
sideBarOpen={sideBarOpen}
loader={customFilterLoader}
searchTerm={searchTerm}
+ isPopover={isPopover}
/>
);
};
diff --git a/dashboard/src/views/SideBar/SideBarTree/EntitiesTree.tsx b/dashboard/src/views/SideBar/SideBarTree/EntitiesTree.tsx
index 74ffd544804..a18f09817b3 100644
--- a/dashboard/src/views/SideBar/SideBarTree/EntitiesTree.tsx
+++ b/dashboard/src/views/SideBar/SideBarTree/EntitiesTree.tsx
@@ -34,7 +34,7 @@ import { fetchEntityData } from "@redux/slice/typeDefSlices/typedefEntitySlice.t
import { fetchTypeHeaderData } from "@redux/slice/typeDefSlices/typeDefHeaderSlice.ts";
import { fetchMetricEntity } from "@redux/slice/metricsSlice.ts";
-const EntitiesTree = ({ sideBarOpen, searchTerm }: Props) => {
+const EntitiesTree = ({ sideBarOpen, searchTerm, isPopover }: Props) => {
const dispatch = useAppDispatch();
const { typeHeaderData, loading }: TypedefHeaderDataType = useAppSelector(
(state: any) => state.typeHeader
@@ -267,6 +267,7 @@ const EntitiesTree = ({ sideBarOpen, searchTerm }: Props) => {
sideBarOpen={sideBarOpen}
loader={loading}
searchTerm={searchTerm}
+ isPopover={isPopover}
/>
);
};
diff --git a/dashboard/src/views/SideBar/SideBarTree/GlossaryTree.tsx b/dashboard/src/views/SideBar/SideBarTree/GlossaryTree.tsx
index bfa7fac66cd..543d39384ae 100644
--- a/dashboard/src/views/SideBar/SideBarTree/GlossaryTree.tsx
+++ b/dashboard/src/views/SideBar/SideBarTree/GlossaryTree.tsx
@@ -37,7 +37,7 @@ import {
} from "@models/glossaryTreeType.ts";
import { fetchGlossaryData } from "@redux/slice/glossarySlice.ts";
-const GlossaryTree = ({ sideBarOpen, searchTerm }: Props) => {
+const GlossaryTree = ({ sideBarOpen, searchTerm, isPopover }: Props) => {
const dispatch = useAppDispatch();
const { glossaryData, loading }: any = useAppSelector(
(state: any) => state.glossary
@@ -209,6 +209,7 @@ const GlossaryTree = ({ sideBarOpen, searchTerm }: Props) => {
sideBarOpen={sideBarOpen}
loader={loading}
searchTerm={searchTerm}
+ isPopover={isPopover}
/>
);
};
diff --git a/dashboard/src/views/SideBar/SideBarTree/RelationShipsTree.tsx b/dashboard/src/views/SideBar/SideBarTree/RelationShipsTree.tsx
index b1036d46a7b..de63259ec04 100644
--- a/dashboard/src/views/SideBar/SideBarTree/RelationShipsTree.tsx
+++ b/dashboard/src/views/SideBar/SideBarTree/RelationShipsTree.tsx
@@ -63,6 +63,7 @@ const RelationshipsTree = (props: Props) => {
sideBarOpen={sideBarOpen}
loader={loading}
searchTerm={searchTerm}
+ isPopover={props.isPopover}
/>
);
};
diff --git a/dashboard/src/views/SideBar/SideBarTree/SideBarTree.tsx b/dashboard/src/views/SideBar/SideBarTree/SideBarTree.tsx
index c031e1e587b..5a81e49a8b9 100644
--- a/dashboard/src/views/SideBar/SideBarTree/SideBarTree.tsx
+++ b/dashboard/src/views/SideBar/SideBarTree/SideBarTree.tsx
@@ -27,6 +27,7 @@ import {
useRef,
useState,
useMemo,
+ useCallback,
SyntheticEvent,
memo,
} from "react";
@@ -77,24 +78,61 @@ import AddUpdateGlossaryForm from "@views/Glossary/AddUpdateGlossaryForm";
import RefreshIcon from "@mui/icons-material/Refresh";
import { AntSwitch } from "@utils/Muiutils";
import { IconButton } from "@components/muiComponents";
-import SkeletonLoader from "@components/SkeletonLoader";
+
+import TreeSkeletonLoader from "@components/TreeSkeletonLoader";
+
+type SelectedNode = {
+ type: string | null;
+ tag: string | null;
+ relationship: string | null;
+ businessMetadata: string | null;
+ term: string | null;
+ customFilter: string | null;
+};
type CustomContentRootProps = HTMLAttributes & {
- selectedNodeType?: any;
- selectedNodeTag?: any;
- selectedNodeRelationship?: any;
- selectedNodeBM?: any;
- node?: any;
- selectedNode?: any;
+ selectedNodeType?: string | null;
+ selectedNodeTag?: string | null;
+ selectedNodeRelationship?: string | null;
+ selectedNodeBM?: string | null;
+ selectedNodeTerm?: string | null;
+ selectedNodeCustomFilter?: string | null;
+ node?: string | null;
+ selectedNode?: SelectedNode;
+};
+
+interface SavedSearchItem {
+ name?: string;
+ searchType?: string;
+ searchParameters?: Record;
+ [key: string]: unknown;
+}
+
+const HoverableTreeItemContainer = ({
+ children,
+ ...props
+}: {
+ children: React.ReactNode | ((isHovered: boolean) => React.ReactNode);
+} & HTMLAttributes & CustomContentRootProps) => {
+ const [isHovered, setIsHovered] = useState(false);
+ return (
+ setIsHovered(true)}
+ onMouseLeave={() => setIsHovered(false)}
+ {...props}
+ >
+ {typeof children === "function" ? children(isHovered) : children}
+
+ );
};
const CustomContentRoot = styled("div")(
({ theme, ...props }) => ({
WebkitTapHighlightColor: "#0E8173",
"&&:hover, &&.Mui-disabled, &&.Mui-focused, &&.Mui-selected, &&.Mui-selected.Mui-focused, &&.Mui-selected:hover":
- {
- backgroundColor: "transparent",
- },
+ {
+ backgroundColor: "transparent",
+ },
".MuiTreeItem-contentBar": {
position: "absolute",
width: "100%",
@@ -120,20 +158,25 @@ const CustomContentRoot = styled("div")(
...((props.selectedNodeType === props.node ||
props.selectedNodeTag === props.node ||
props.selectedNodeRelationship === props.node ||
- props.selectedNodeBM === props.node) && {
+ props.selectedNodeBM === props.node ||
+ props.selectedNodeTerm === props.node ||
+ props.selectedNodeCustomFilter === props.node) && {
"&.Mui-selected .MuiTreeItem-contentBar": {
backgroundColor: "rgba(255,255,255,0.08)",
- borderLeft: "4px solid #2ccebb",
+ borderLeft: "4px solid var(--sidebar-active)",
// color: "white"
// borderRadius: "4px"
},
}),
- ...(props?.selectedNode == props?.node && {
+ ...((props.selectedNodeType === props.node ||
+ props.selectedNodeTag === props.node ||
+ props.selectedNodeRelationship === props.node ||
+ props.selectedNodeBM === props.node ||
+ props.selectedNodeTerm === props.node ||
+ props.selectedNodeCustomFilter === props.node) && {
"&.Mui-selected .MuiTreeItem-label": {
color: "white",
},
- }),
- ...(props?.selectedNode == props?.node && {
"&.Mui-selected & .MuiTreeItem-content svg": {
color: "white",
},
@@ -197,30 +240,33 @@ const CustomContent = forwardRef(function CustomContent(
};
const labelProps = isValidElement(props.label)
- ? (props.label.props as CustomContentRootProps)
- : undefined;
+ ? (props.label.props as CustomContentRootProps)
+ : undefined;
return (
{
handleClick(e);
@@ -237,26 +283,40 @@ const CustomContent = forwardRef(function CustomContent(
);
});
+const StyledCustomTreeItem = styled(TreeItem)(() => ({
+ "& .MuiTreeItem-label": {
+ // fontWeight: "400 !important",
+ fontSize: "14px !important",
+ lineHeight: "24px !important",
+ color: "rgba(255,255,255,0.8)",
+ },
+
+ "& .MuiTreeItem-content svg": {
+ color: "rgba(255,255,255,0.8)",
+ fontSize: "14px !important",
+ },
+}));
+
+export const StyledParentTreeItem = styled(TreeItem)(() => ({
+ "& .MuiTreeItem-label": {
+ fontWeight: "600 !important",
+ fontSize: "14px !important",
+ lineHeight: "26px !important",
+ color: "white",
+ },
+ "& .MuiTreeItem-content svg": {
+ color: "white",
+ fontSize: "20px !important",
+ },
+}));
+
const CustomTreeItem = memo(
forwardRef(function CustomTreeItem(
props: TreeItemProps,
ref: Ref
) {
return (
- = ({
treeData,
treeName,
@@ -287,1027 +348,1049 @@ const BarTreeView: FC<{
sideBarOpen,
searchTerm,
loader,
+ isPopover,
}) => {
- const dispatch = useAppDispatch();
- const { savedSearchData }: any = useAppSelector(
- (state: any) => state.savedSearch
- );
- const { bmguid } = useParams();
- const location = useLocation();
- const navigate = useNavigate();
- const searchParams = new URLSearchParams(location.search);
- const [expand, setExpand] = useState(null);
- const [selectedNode, setSelectedNode] = useState<{
- type: string | null;
- tag: string | null;
- relationship: string | null;
- businessMetadata: string | null;
- }>({
- type: null,
- tag: null,
- relationship: null,
- businessMetadata: null,
- });
-
- const [openModal, setOpenModal] = useState(false);
- const toastId: any = useRef(null);
- const open = Boolean(expand);
- const [expandedItems, setExpandedItems] = useState([]);
- const [tagModal, setTagModal] = useState(false);
- const [glossaryModal, setGlossaryModal] = useState(false);
- const { businessMetaData }: any = useAppSelector(
- (state: any) => state.businessMetaData
- );
-
- const filteredData = useMemo(() => {
- return treeData.filter((node) => {
- return (
- node.label?.toLowerCase().includes(searchTerm.toLowerCase()) ||
- (node.children &&
- node.children.some((child) =>
- child.label?.toLowerCase().includes(searchTerm.toLowerCase())
- ))
- );
+ const savedSearchData = useAppSelector(
+ (state) => state.savedSearch?.savedSearchData as SavedSearchItem[] | null
+ );
+ const { bmguid } = useParams();
+ const dispatch = useAppDispatch();
+ const location = useLocation();
+ const navigate = useNavigate();
+ const searchParams = new URLSearchParams(location.search);
+ const [expand, setExpand] = useState(null);
+ const [selectedNode, setSelectedNode] = useState({
+ type: null,
+ tag: null,
+ relationship: null,
+ businessMetadata: null,
+ term: null,
+ customFilter: null,
});
- }, [treeData, searchTerm]);
- const displayTreeName = useMemo(() => {
- return treeName === "CustomFilters" ? "Custom Filters" : treeName
- }, [treeName]);
+ const [openModal, setOpenModal] = useState(false);
+ const toastId = useRef(null);
+ const open = Boolean(expand);
+ const [expandedItems, setExpandedItems] = useState([]);
+ const [tagModal, setTagModal] = useState(false);
+ const [glossaryModal, setGlossaryModal] = useState(false);
+ const businessMetaData = useAppSelector(
+ (state) => state.businessMetaData?.businessMetaData as Record | null
+ );
- const highlightText = useMemo(() => {
- return (text: string) => {
- if (!searchTerm) return text;
+ const filteredData = useMemo(() => {
+ if (!searchTerm) return treeData;
+ const lowerSearch = searchTerm.toLowerCase();
+ return treeData.reduce((acc: TreeNode[], node: TreeNode) => {
+ const nodeMatches = node.label?.toLowerCase().includes(lowerSearch);
+ let filteredChildren = node.children;
+ if (!nodeMatches && node.children) {
+ filteredChildren = node.children.filter((child: TreeNode) =>
+ child.label?.toLowerCase().includes(lowerSearch)
+ );
+ }
+ if (nodeMatches || (filteredChildren && filteredChildren.length > 0)) {
+ acc.push({ ...node, children: filteredChildren });
+ }
+ return acc;
+ }, []);
+ }, [treeData, searchTerm]);
- const parts = text.split(new RegExp(`(${searchTerm})`, "gi"));
- return parts.map((part, index) =>
- part.toLowerCase() === searchTerm.toLowerCase() ? (
-
- {part}
-
- ) : (
- part
- )
- );
- };
- }, [searchTerm]);
-
- const expandedItemsMemo = useMemo(() => {
- const allNodeIds = filteredData.flatMap((node) => {
- return [
- node.id,
- ...(node.children ? node.children.map((child) => child.id) : []),
- ];
- });
- return [...allNodeIds, ...[treeName]];
- }, [filteredData, treeName]);
+ const displayTreeName = useMemo(() => {
+ return treeName === "CustomFilters" ? "Custom Filters" : treeName
+ }, [treeName]);
- useEffect(() => {
- setExpandedItems(expandedItemsMemo);
- }, [expandedItemsMemo]);
+ const highlightText = useMemo(() => {
+ return (text: string) => {
+ if (!searchTerm) return text;
- useEffect(() => {
- const searchParams = new URLSearchParams(location.search);
- const nodeIdFromParamsType = searchParams.get("type");
- const nodeIdFromParamsTag = searchParams.get("tag");
- const nodeIdFromParamsRelationshipName =
- searchParams.get("relationshipName");
- const nodeIdFromBMName = location.pathname.includes(
- "/administrator/businessMetadata"
- );
+ const escapeRegExp = (string: string) => {
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
+ };
+
+ const escapedSearchTerm = escapeRegExp(searchTerm);
+ const parts = text.split(new RegExp(`(${escapedSearchTerm})`, "gi"));
+ return parts.map((part, index) =>
+ part.toLowerCase() === searchTerm.toLowerCase() ? (
+
+ {part}
+
+ ) : (
+ part
+ )
+ );
+ };
+ }, [searchTerm]);
+
+ const getNodeId = useCallback((node: TreeNode) => {
+ if (treeName === "Classifications" && node.types === "parent") {
+ return node.label;
+ } else if (treeName === "Classifications" && node.types === "child") {
+ return `${node.id}@${node.label}`;
+ }
+ return !isEmpty(node?.parent) ? `${node.id}@${node?.parent}` : node.id;
+ }, [treeName]);
- const bmObj = !isEmpty(businessMetaData?.businessMetadataDefs)
- ? businessMetaData?.businessMetadataDefs?.find((obj: EnumTypeDefData) => {
- if (bmguid == obj.guid) {
+ const expandedItemsMemo = useMemo(() => {
+ const parentNodeIds = filteredData.map((node) => getNodeId(node));
+ return [...parentNodeIds, treeName];
+ }, [filteredData, treeName, getNodeId]);
+
+ useEffect(() => {
+ setExpandedItems(expandedItemsMemo);
+ }, [expandedItemsMemo]);
+
+ useEffect(() => {
+ const searchParams = new URLSearchParams(location.search);
+ const nodeIdFromParamsType = searchParams.get("type");
+ const nodeIdFromParamsTag = searchParams.get("tag");
+ const nodeIdFromParamsRelationshipName =
+ searchParams.get("relationshipName");
+ const nodeIdFromBMName = location.pathname.includes(
+ "/administrator/businessMetadata"
+ );
+ const nodeIdFromParamsTerm = searchParams.get("term") || searchParams.get("category") || searchParams.get("gtype") || location.pathname.split("/glossary/")[1];
+ const nodeIdFromCustomFilter = searchParams.get("customFilter");
+
+ const bmObj = !isEmpty(businessMetaData?.businessMetadataDefs)
+ ? businessMetaData?.businessMetadataDefs?.find((obj: EnumTypeDefData) => {
+ if (bmguid === obj.guid) {
return obj;
}
})
- : {};
- const { name = "" } = bmObj || {};
-
- setSelectedNode({
- type: nodeIdFromParamsType,
- tag: nodeIdFromParamsTag,
- relationship: nodeIdFromParamsRelationshipName,
- businessMetadata: nodeIdFromBMName ? name : null,
- });
+ : {};
+ const { name = "" } = bmObj || {};
- if (
- !nodeIdFromParamsType &&
- !nodeIdFromParamsTag &&
- !nodeIdFromParamsRelationshipName &&
- !nodeIdFromBMName
- ) {
setSelectedNode({
- type: null,
- tag: null,
- relationship: null,
- businessMetadata: null,
+ type: nodeIdFromParamsType,
+ tag: nodeIdFromParamsTag,
+ relationship: nodeIdFromParamsRelationshipName,
+ businessMetadata: nodeIdFromBMName ? name : null,
+ term: nodeIdFromParamsTerm || null,
+ customFilter: nodeIdFromCustomFilter || null,
});
- }
- }, [location.search]);
- const getEmptyTypesTitle = () => {
- switch (treeName) {
- case "Entities":
- return `${isEmptyServicetype ? "Hide" : "Show"} empty service types`;
+ if (nodeIdFromParamsTerm && typeof nodeIdFromParamsTerm === "string" && nodeIdFromParamsTerm.includes("@") && treeName === "Glossary") {
+ const glossaryName = nodeIdFromParamsTerm.split("@")[1];
+ if (glossaryName) {
+ const parentNode = treeData.find((n) => n.label === glossaryName);
+ if (parentNode) {
+ const nodeIdToExpand = getNodeId(parentNode);
+ setExpandedItems((prev) => {
+ if (!prev.includes(nodeIdToExpand)) {
+ return [...prev, nodeIdToExpand];
+ }
+ return prev;
+ });
+ }
+ }
+ }
- case "Classifications":
- return `${isEmptyServicetype ? "Show" : "Hide"} unused classifications`;
+ if (
+ !nodeIdFromParamsType &&
+ !nodeIdFromParamsTag &&
+ !nodeIdFromParamsRelationshipName &&
+ !nodeIdFromBMName &&
+ !nodeIdFromParamsTerm &&
+ !nodeIdFromCustomFilter
+ ) {
+ setSelectedNode({
+ type: null,
+ tag: null,
+ relationship: null,
+ businessMetadata: null,
+ term: null,
+ customFilter: null,
+ });
+ }
+ }, [location.pathname, location.search, treeData, treeName, businessMetaData, bmguid, getNodeId]);
- case "Glossary":
- return `Show ${isEmptyServicetype ? "Category" : "Term"}`;
+ const getEmptyTypesTitle = () => {
+ switch (treeName) {
+ case "Entities":
+ return `${isEmptyServicetype ? "Hide" : "Show"} empty service types`;
- case "CustomFilters":
- return `Show ${isEmptyServicetype ? "all" : "Type"}`;
+ case "Classifications":
+ return `${isEmptyServicetype ? "Show" : "Hide"} unused classifications`;
- default:
- return "";
- }
- };
+ case "Glossary":
+ return `Show ${isEmptyServicetype ? "Category" : "Term"}`;
- const handleExpandedItemsChange = (
- _event: SyntheticEvent,
- newExpandedItems: string[]
- ) => {
- setExpandedItems(newExpandedItems);
- };
+ case "CustomFilters":
+ return `Show ${isEmptyServicetype ? "all" : "Type"}`;
- const handleOpenModal = () => {
- setOpenModal(true);
- };
- const handleCloseModal = () => {
- setOpenModal(false);
- };
+ default:
+ return "";
+ }
+ };
- const handleClickMenu = (event: MouseEvent) => {
- event.stopPropagation();
- setExpand(event.currentTarget);
- };
+ const handleExpandedItemsChange = (
+ _event: SyntheticEvent,
+ newExpandedItems: string[]
+ ) => {
+ setExpandedItems(newExpandedItems);
+ };
- const handleClose = () => {
- setExpand(null);
- };
+ const handleOpenModal = () => {
+ setOpenModal(true);
+ };
+ const handleCloseModal = () => {
+ setOpenModal(false);
+ };
- const handleCloseTagModal = () => {
- setTagModal(false);
- };
- const handleCloseGlossaryModal = () => {
- setGlossaryModal(false);
- };
+ const handleClickMenu = (event: MouseEvent) => {
+ event.stopPropagation();
+ setExpand(event.currentTarget);
+ };
- const handleClickNode = (nodeId: string) => {
- const searchParams = new URLSearchParams(location.search);
- const isTypeMatch = searchParams.get("type") === nodeId;
- const isTagMatch = searchParams.get("tag") === nodeId;
- const isRelationshipMatch = searchParams.get("relationshipName") === nodeId;
- const isBusinessMetadataMatch = location.pathname.includes(
- "/administrator/businessMetadata"
- );
+ const handleClose = () => {
+ setExpand(null);
+ };
- if (isTypeMatch) {
- setSelectedNode({
- type: nodeId,
- tag: null,
- relationship: null,
- businessMetadata: null,
- });
- }
- if (isTagMatch) {
- setSelectedNode({
- type: null,
- tag: nodeId,
- relationship: null,
- businessMetadata: null,
- });
- }
- if (isRelationshipMatch) {
- setSelectedNode({
- type: null,
- tag: null,
- relationship: nodeId,
- businessMetadata: null,
- });
- }
- if (isBusinessMetadataMatch) {
- setSelectedNode({
- type: null,
- tag: null,
- relationship: null,
- businessMetadata: nodeId,
- });
- }
- };
+ const handleCloseTagModal = () => {
+ setTagModal(false);
+ };
+ const handleCloseGlossaryModal = () => {
+ setGlossaryModal(false);
+ };
- const getNodeId = (node: TreeNode) => {
- if (treeName == "Classifications" && node.types == "parent") {
- return node.label;
- } else if (treeName == "Classifications" && node.types == "child") {
- return `${node.id}@${node.label}`;
- }
- return !isEmpty(node?.parent) ? `${node.id}@${node?.parent}` : node.id;
- };
+ const handleClickNode = (nodeId: string) => {
+ const searchParams = new URLSearchParams(location.search);
+ const isTypeMatch = searchParams.get("type") === nodeId;
+ const isTagMatch = searchParams.get("tag") === nodeId;
+ const isRelationshipMatch = searchParams.get("relationshipName") === nodeId;
+ const isBusinessMetadataMatch = location.pathname.includes(
+ "/administrator/businessMetadata"
+ );
- const handleNodeClick = (
- node: TreeNode,
- treeName: string,
- searchParams: URLSearchParams,
- navigate: NavigateFunction,
- isEmptyServicetype: boolean | undefined,
- savedSearchData: any,
- toastId: any
- ) => {
- globalSearchFilterInitialQuery.setQuery({});
- searchParams.delete("tabActive");
-
- if (treeName === "Classifications") {
- handleClickNode(node.id);
- } else {
- handleClickNode(node.id);
- }
-
- if (node.id === "No Records Found") {
- if (typeof event !== "undefined" && event.stopPropagation) {
- event.stopPropagation();
+ if (isTypeMatch) {
+ setSelectedNode({
+ type: nodeId,
+ tag: null,
+ relationship: null,
+ businessMetadata: null,
+ term: null,
+ customFilter: null,
+ });
+ }
+ if (isTagMatch) {
+ setSelectedNode({
+ type: null,
+ tag: nodeId,
+ relationship: null,
+ businessMetadata: null,
+ term: null,
+ customFilter: null,
+ });
+ }
+ if (isRelationshipMatch) {
+ setSelectedNode({
+ type: null,
+ tag: null,
+ relationship: nodeId,
+ businessMetadata: null,
+ term: null,
+ customFilter: null,
+ });
+ }
+ if (isBusinessMetadataMatch) {
+ setSelectedNode({
+ type: null,
+ tag: null,
+ relationship: null,
+ businessMetadata: nodeId,
+ term: null,
+ customFilter: null,
+ });
}
- return;
- }
-
- if (shouldSetSearchParams(node, treeName)) {
- setSearchParams(
- node,
- treeName,
- searchParams,
- isEmptyServicetype,
- savedSearchData
+ };
+
+ const handleNodeClick = (
+ node: TreeNode,
+ treeName: string,
+ searchParams: URLSearchParams,
+ navigate: NavigateFunction,
+ isEmptyServicetype: boolean | undefined,
+ savedSearchData: SavedSearchItem[] | null | undefined,
+ toastId: React.MutableRefObject
+ ) => {
+ globalSearchFilterInitialQuery.setQuery({});
+ searchParams.delete("tabActive");
+
+ if (treeName === "Classifications") {
+ handleClickNode(node.id);
+ } else {
+ handleClickNode(node.id);
+ }
+
+ if (node.id === "No Records Found") {
+ if (typeof event !== "undefined" && event.stopPropagation) {
+ event.stopPropagation();
+ }
+ return;
+ }
+
+ if (shouldSetSearchParams(node, treeName)) {
+ setSearchParams(
+ node,
+ treeName,
+ searchParams,
+ isEmptyServicetype,
+ savedSearchData
+ );
+ navigateToPath(
+ node,
+ treeName,
+ searchParams,
+ navigate,
+ isEmptyServicetype,
+ toastId
+ );
+ }
+ };
+
+ const shouldSetSearchParams = (node: TreeNode, treeName: string) => {
+ if (treeName === "CustomFilters" && node.types === "parent") {
+ return false;
+ }
+ return (
+ node.children === undefined ||
+ isEmpty(node.children) ||
+ treeName === "Classifications" ||
+ (treeName === "Glossary" && node.types === "child")
);
- navigateToPath(
- node,
- treeName,
- searchParams,
- navigate,
- isEmptyServicetype,
- toastId
+ };
+
+ const setSearchParams = (
+ node: TreeNode,
+ treeName: string,
+ searchParams: URLSearchParams,
+ isEmptyServicetype: boolean | undefined,
+ savedSearchData: SavedSearchItem[] | null | undefined
+ ) => {
+ searchParams.set(
+ "searchType",
+ node.parent === "ADVANCED" ? "dsl" : "basic"
);
- }
- };
- const shouldSetSearchParams = (node: TreeNode, treeName: string) => {
- if (treeName === "CustomFilters" && node.types === "parent") {
- return false;
- }
- return (
- node.children === undefined ||
- isEmpty(node.children) ||
- treeName === "Classifications" ||
- (treeName === "Glossary" && node.types === "child")
- );
- };
+ switch (treeName) {
+ case "Entities":
+ searchParams.delete("relationshipName");
+ searchParams.set("type", node.id);
+ break;
+ case "Classifications": {
+ searchParams.delete("relationshipName");
+ const id = node.label.split(" (")[0];
+ searchParams.set("tag", id);
+ break;
+ }
+ case "Glossary":
+ setGlossarySearchParams(node, searchParams, isEmptyServicetype);
+ break;
+ case "Relationships":
+ case "CustomFilters":
+ setCustomFiltersSearchParams(node, searchParams, savedSearchData);
+ break;
+ default:
+ break;
+ }
- const setSearchParams = (
- node: TreeNode,
- treeName: string,
- searchParams: URLSearchParams,
- isEmptyServicetype: boolean | undefined,
- savedSearchData: any
- ) => {
- searchParams.set(
- "searchType",
- node.parent === "ADVANCED" ? "dsl" : "basic"
- );
+ if (treeName !== "CustomFilters") {
+ searchParams.delete("attributes");
+ searchParams.delete("entityFilters");
+ searchParams.delete("tagFilters");
+ searchParams.delete("relationshipFilters");
+ searchParams.set("pageLimit", "25");
+ searchParams.set("pageOffset", "0");
+ }
+ };
- switch (treeName) {
- case "Entities":
- searchParams.delete("relationshipName");
- searchParams.set("type", node.id);
- break;
- case "Classifications": {
- searchParams.delete("relationshipName");
- const id = node.label.split(" (")[0];
- searchParams.set("tag", id);
- break;
+ const setGlossarySearchParams = (
+ node: TreeNode,
+ searchParams: URLSearchParams,
+ isEmptyServicetype: boolean | undefined
+ ) => {
+ const guid =
+ !isEmptyServicetype && node.cGuid !== undefined
+ ? node.cGuid
+ : node.guid || "";
+ searchParams.delete("relationshipName");
+
+ if (isEmptyServicetype) {
+ searchParams.set("term", `${node.id}@${node.parent}`);
+ } else {
+ searchParams.delete("type");
+ searchParams.delete("tag");
+ searchParams.set("gid", node.guid as string);
}
- case "Glossary":
- setGlossarySearchParams(node, searchParams, isEmptyServicetype);
- break;
- case "Relationships":
- case "CustomFilters":
- setCustomFiltersSearchParams(node, searchParams, savedSearchData);
- break;
- default:
- break;
- }
-
- if (treeName !== "CustomFilters") {
- searchParams.delete("attributes");
- searchParams.delete("entityFilters");
- searchParams.delete("tagFilters");
- searchParams.delete("relationshipFilters");
- searchParams.set("pageLimit", "25");
- searchParams.set("pageOffset", "0");
- }
- };
- const setGlossarySearchParams = (
- node: TreeNode,
- searchParams: URLSearchParams,
- isEmptyServicetype: boolean | undefined
- ) => {
- const guid =
- !isEmptyServicetype && node.cGuid !== undefined
- ? node.cGuid
- : node.guid || "";
- searchParams.delete("relationshipName");
-
- if (isEmptyServicetype) {
- searchParams.set("term", `${node.id}@${node.parent}`);
- } else {
- searchParams.delete("type");
- searchParams.delete("tag");
- searchParams.set("gid", node.guid as string);
- }
-
- searchParams.set("gtype", `${isEmptyServicetype ? "term" : "category"}`);
- searchParams.set("viewType", `${isEmptyServicetype ? "term" : "category"}`);
- searchParams.set("guid", guid);
- };
+ searchParams.set("gtype", `${isEmptyServicetype ? "term" : "category"}`);
+ searchParams.set("viewType", `${isEmptyServicetype ? "term" : "category"}`);
+ searchParams.set("guid", guid);
+ };
- const setCustomFiltersSearchParams = (
- node: TreeNode,
- searchParams: URLSearchParams,
- savedSearchData: any[]
- ) => {
- // Clear all existing params except searchType
- const keys = Array.from(searchParams.keys());
- for (let i = 0; i < keys.length; i++) {
- if (keys[i] !== "searchType") {
- searchParams.delete(keys[i]);
- }
- }
-
- // Clear globalSearchFilterInitialQuery when applying new saved search
- globalSearchFilterInitialQuery.setQuery({});
-
- if (treeName === "CustomFilters") {
- const params = savedSearchData.find((obj) => obj.name === node.id);
- if (params) {
- const searchParamsObj = params?.searchParameters || {};
-
- // Step 1: Set searchType based on saved search type
- if (params.searchType) {
- const searchTypeValue = params.searchType === "ADVANCED" ? "dsl" : "basic";
- searchParams.set("searchType", searchTypeValue);
+ const setCustomFiltersSearchParams = (
+ node: TreeNode,
+ searchParams: URLSearchParams,
+ savedSearchData: SavedSearchItem[] | null | undefined
+ ) => {
+ // Clear all existing params except searchType
+ const keys = Array.from(searchParams.keys());
+ for (let i = 0; i < keys.length; i++) {
+ if (keys[i] !== "searchType") {
+ searchParams.delete(keys[i]);
}
-
- // Step 2: Apply basic search parameters (excluding filters which are handled separately)
- for (const key in searchParamsObj) {
- if (shouldSetCustomFilterParam(node, key) &&
+ }
+
+ // Clear globalSearchFilterInitialQuery when applying new saved search
+ globalSearchFilterInitialQuery.setQuery({});
+
+ if (treeName === "CustomFilters" && savedSearchData) {
+ const params = savedSearchData.find((obj) => obj.name === node.id);
+ if (params) {
+ const searchParamsObj = (params?.searchParameters || {}) as Record;
+
+ // Step 1: Set searchType based on saved search type
+ if (params.searchType) {
+ const searchTypeValue = params.searchType === "ADVANCED" ? "dsl" : "basic";
+ searchParams.set("searchType", searchTypeValue);
+ }
+
+ // Step 2: Apply basic search parameters (excluding filters which are handled separately)
+ for (const key in searchParamsObj) {
+ if (shouldSetCustomFilterParam(node, key) &&
!["entityFilters", "tagFilters", "relationshipFilters"].includes(key)) {
- setCustomFilterParam(searchParams, key, searchParamsObj[key]);
+ setCustomFilterParam(searchParams, key, searchParamsObj[key]);
+ }
}
- }
-
- // Step 3: Convert and apply entityFilters from API format to URL string format
- if (searchParamsObj.entityFilters && !isEmpty(searchParamsObj.entityFilters)) {
- const clonedFilter = cloneDeep(searchParamsObj.entityFilters);
- const ruleUrl = attributeFilter.generateUrl({
- value: clonedFilter,
- formatedDateToLong: true
- });
-
- if (ruleUrl && !isEmpty(ruleUrl) && typeof ruleUrl === "string") {
- searchParams.set("entityFilters", ruleUrl);
-
- // Convert API format to query builder format for Filters component UI
- const qbFilter = convertApiToQueryBuilder(searchParamsObj.entityFilters);
- if (qbFilter && (qbFilter.rules || qbFilter.combinator)) {
- globalSearchFilterInitialQuery.setQuery({
- entityFilters: qbFilter
- });
+
+ // Step 3: Convert and apply entityFilters from API format to URL string format
+ if (searchParamsObj.entityFilters && !isEmpty(searchParamsObj.entityFilters)) {
+ const clonedFilter = cloneDeep(searchParamsObj.entityFilters);
+ const ruleUrl = attributeFilter.generateUrl({
+ value: clonedFilter,
+ formatedDateToLong: true
+ });
+
+ if (ruleUrl && !isEmpty(ruleUrl) && typeof ruleUrl === "string") {
+ searchParams.set("entityFilters", ruleUrl);
+
+ // Convert API format to query builder format for Filters component UI
+ const qbFilter = convertApiToQueryBuilder(searchParamsObj.entityFilters as Record);
+ if (qbFilter && (qbFilter.rules || qbFilter.combinator)) {
+ globalSearchFilterInitialQuery.setQuery({
+ entityFilters: qbFilter
+ });
+ }
}
}
- }
-
- // Step 4: Convert and apply tagFilters from API format to URL string format
- if (searchParamsObj.tagFilters && !isEmpty(searchParamsObj.tagFilters)) {
- const clonedFilter = cloneDeep(searchParamsObj.tagFilters);
- const ruleUrl = attributeFilter.generateUrl({
- value: clonedFilter,
- formatedDateToLong: true
- });
-
- if (ruleUrl && !isEmpty(ruleUrl) && typeof ruleUrl === "string") {
- searchParams.set("tagFilters", ruleUrl);
-
- // Convert API format to query builder format for Filters component UI
- const qbFilter = convertApiToQueryBuilder(searchParamsObj.tagFilters);
- if (qbFilter && (qbFilter.rules || qbFilter.combinator)) {
- globalSearchFilterInitialQuery.setQuery({
- tagFilters: qbFilter
- });
+
+ // Step 4: Convert and apply tagFilters from API format to URL string format
+ if (searchParamsObj.tagFilters && !isEmpty(searchParamsObj.tagFilters)) {
+ const clonedFilter = cloneDeep(searchParamsObj.tagFilters);
+ const ruleUrl = attributeFilter.generateUrl({
+ value: clonedFilter,
+ formatedDateToLong: true
+ });
+
+ if (ruleUrl && !isEmpty(ruleUrl) && typeof ruleUrl === "string") {
+ searchParams.set("tagFilters", ruleUrl);
+
+ // Convert API format to query builder format for Filters component UI
+ const qbFilter = convertApiToQueryBuilder(searchParamsObj.tagFilters as Record);
+ if (qbFilter && (qbFilter.rules || qbFilter.combinator)) {
+ globalSearchFilterInitialQuery.setQuery({
+ tagFilters: qbFilter
+ });
+ }
}
}
- }
-
- // Step 5: Convert and apply relationshipFilters from API format to URL string format
- if (searchParamsObj.relationshipFilters && !isEmpty(searchParamsObj.relationshipFilters)) {
- const clonedFilter = cloneDeep(searchParamsObj.relationshipFilters);
- const ruleUrl = attributeFilter.generateUrl({
- value: clonedFilter,
- formatedDateToLong: true
- });
-
- if (ruleUrl && !isEmpty(ruleUrl) && typeof ruleUrl === "string") {
- searchParams.set("relationshipFilters", ruleUrl);
-
- // Convert API format to query builder format for Filters component UI
- const qbFilter = convertApiToQueryBuilder(searchParamsObj.relationshipFilters);
- if (qbFilter && (qbFilter.rules || qbFilter.combinator)) {
- globalSearchFilterInitialQuery.setQuery({
- relationshipFilters: qbFilter
- });
+
+ // Step 5: Convert and apply relationshipFilters from API format to URL string format
+ if (searchParamsObj.relationshipFilters && !isEmpty(searchParamsObj.relationshipFilters)) {
+ const clonedFilter = cloneDeep(searchParamsObj.relationshipFilters);
+ const ruleUrl = attributeFilter.generateUrl({
+ value: clonedFilter,
+ formatedDateToLong: true
+ });
+
+ if (ruleUrl && !isEmpty(ruleUrl) && typeof ruleUrl === "string") {
+ searchParams.set("relationshipFilters", ruleUrl);
+
+ // Convert API format to query builder format for Filters component UI
+ const qbFilter = convertApiToQueryBuilder(searchParamsObj.relationshipFilters as Record);
+ if (qbFilter && (qbFilter.rules || qbFilter.combinator)) {
+ globalSearchFilterInitialQuery.setQuery({
+ relationshipFilters: qbFilter
+ });
+ }
}
}
+
+ searchParams.set("isCF", "true");
+ searchParams.set("customFilter", node.id);
}
-
- searchParams.set("isCF", "true");
+ } else {
+ searchParams.set("relationshipName", node.id);
}
- } else {
- searchParams.set("relationshipName", node.id);
- }
- };
-
- // Helper function to convert API format filter (criterion/condition) to query builder format (rules/combinator)
- const convertApiToQueryBuilder = (apiFilter: any): any => {
- if (!apiFilter || typeof apiFilter !== "object") {
- return null;
- }
-
- const result: any = {};
-
- // Convert condition to combinator
- if (apiFilter.condition) {
- result.combinator = apiFilter.condition.toLowerCase();
- } else {
- result.combinator = "and"; // default
- }
-
- // Convert criterion to rules
- if (apiFilter.criterion && Array.isArray(apiFilter.criterion)) {
- result.rules = apiFilter.criterion.map((rule: any) => {
- // If nested condition, recurse
- if (rule.condition || rule.criterion) {
- return convertApiToQueryBuilder(rule);
- }
- // Convert API rule format to query builder format
- return {
- field: rule.attributeName || rule.id,
- operator: rule.operator,
- value: rule.attributeValue || rule.value,
- type: rule.type || rule.attributeType
- };
- });
- } else if (apiFilter.rules && Array.isArray(apiFilter.rules)) {
- // Already in query builder format
- result.rules = apiFilter.rules.map((rule: any) =>
- rule.condition || rule.criterion ? convertApiToQueryBuilder(rule) : rule
- );
- }
-
- return Object.keys(result).length > 0 ? result : null;
- };
+ };
- const shouldSetCustomFilterParam = (node: TreeNode, key: string) => {
- return (
- node.parent === "BASIC" ||
- node.parent === "ADVANCED" ||
- (node.parent === "BASIC_RELATIONSHIP" &&
- (key === "relationshipName" || key === "limit" || key === "offset"))
- );
- };
+ // Helper function to convert API format filter (criterion/condition) to query builder format (rules/combinator)
+ const convertApiToQueryBuilder = (
+ apiFilter: Record | null | undefined
+ ): Record | null => {
+ if (!apiFilter || typeof apiFilter !== "object") {
+ return null;
+ }
- const setCustomFilterParam = (
- searchParams: URLSearchParams,
- key: string,
- value: any
- ) => {
- if (key === "limit") {
- searchParams.set("pageLimit", value || 25);
- } else if (key === "offset") {
- searchParams.set("pageOffset", value);
- } else if (key === "typeName") {
- searchParams.set("type", value);
- } else if (key === "classification") {
- // Map classification to tag parameter for URL (matching classic UI)
- searchParams.set("tag", value);
- } else if (key === "termName") {
- // Map termName (API format) to term parameter for URL (matching classic UI)
- searchParams.set("term", value);
- } else if (value !== null && value !== undefined && value !== "") {
- // Only set parameter if value is not null, undefined, or empty string
- searchParams.set(key, value);
- }
- };
+ const result: Record = {};
+
+ // Convert condition to combinator
+ if (typeof apiFilter.condition === "string") {
+ result.combinator = apiFilter.condition.toLowerCase();
+ } else {
+ result.combinator = "and"; // default
+ }
- const navigateToPath = (
- node: TreeNode,
- treeName: string,
- searchParams: URLSearchParams,
- navigate: NavigateFunction,
- isEmptyServicetype: boolean | undefined,
- toastId: any
- ) => {
- switch (treeName) {
- case "Business MetaData":
- searchParams.delete("relationshipName");
- navigate(
- { pathname: `administrator/businessMetadata/${node.guid}` },
- { replace: true }
+ // Convert criterion to rules
+ if (Array.isArray(apiFilter.criterion)) {
+ result.rules = apiFilter.criterion.map((rule: Record) => {
+ // If nested condition, recurse
+ if (rule.condition || rule.criterion) {
+ return convertApiToQueryBuilder(rule);
+ }
+ // Convert API rule format to query builder format
+ return {
+ field: String(rule.attributeName || rule.id || ""),
+ operator: rule.operator as string,
+ value: rule.attributeValue || rule.value,
+ type: String(rule.type || rule.attributeType || "")
+ };
+ });
+ } else if (Array.isArray(apiFilter.rules)) {
+ // Already in query builder format
+ result.rules = apiFilter.rules.map((rule: Record) =>
+ rule.condition || rule.criterion ? convertApiToQueryBuilder(rule) : rule
);
- break;
- case "Glossary":
- if (!isEmptyServicetype) {
- searchParams.delete("relationshipName");
- navigate(
- {
- pathname: `glossary/${
- node.cGuid !== undefined ? node.cGuid : node.guid
- }`,
- search: searchParams.toString(),
- },
- { replace: true }
- );
- } else if (node.types === "parent") {
- toast.dismiss(toastId.current);
- toastId.current = toast.warning("Create a Term or Category");
- } else {
+ }
+
+ return Object.keys(result).length > 0 ? result : null;
+ };
+
+ const shouldSetCustomFilterParam = (node: TreeNode, key: string) => {
+ return (
+ node.parent === "BASIC" ||
+ node.parent === "ADVANCED" ||
+ (node.parent === "BASIC_RELATIONSHIP" &&
+ (key === "relationshipName" || key === "limit" || key === "offset"))
+ );
+ };
+
+ const setCustomFilterParam = (
+ searchParams: URLSearchParams,
+ key: string,
+ value: unknown
+ ) => {
+ if (key === "limit") {
+ searchParams.set("pageLimit", String(value || 25));
+ } else if (key === "offset") {
+ searchParams.set("pageOffset", String(value));
+ } else if (key === "typeName") {
+ searchParams.set("type", String(value));
+ } else if (key === "classification") {
+ // Map classification to tag parameter for URL (matching classic UI)
+ searchParams.set("tag", String(value));
+ } else if (key === "termName") {
+ // Map termName (API format) to term parameter for URL (matching classic UI)
+ searchParams.set("term", String(value));
+ } else if (value !== null && value !== undefined && value !== "") {
+ // Only set parameter if value is not null, undefined, or empty string
+ searchParams.set(key, String(value));
+ }
+ };
+
+ const navigateToPath = (
+ node: TreeNode,
+ treeName: string,
+ searchParams: URLSearchParams,
+ navigate: NavigateFunction,
+ isEmptyServicetype: boolean | undefined,
+ toastId: React.MutableRefObject
+ ) => {
+ switch (treeName) {
+ case "Business MetaData":
searchParams.delete("relationshipName");
navigate(
- {
- pathname: "/search/searchResult",
- search: searchParams.toString(),
- },
- { replace: true }
- );
- }
- break;
- case "Relationships":
- case "CustomFilters":
- if (
- treeName == "Relationships" ||
- (treeName == "CustomFilters" && node.parent == "BASIC_RELATIONSHIP")
- ) {
- navigate(
- {
- pathname: `relationship/relationshipSearchresult`,
- search: searchParams.toString(),
- },
+ { pathname: `administrator/businessMetadata/${node.guid}` },
{ replace: true }
);
- } else {
+ break;
+ case "Glossary":
+ if (!isEmptyServicetype) {
+ searchParams.delete("relationshipName");
+ navigate(
+ {
+ pathname: `glossary/${node.cGuid !== undefined ? node.cGuid : node.guid
+ }`,
+ search: searchParams.toString(),
+ },
+ { replace: true }
+ );
+ } else if (node.types === "parent") {
+ if (toastId.current !== null) {
+ toast.dismiss(toastId.current);
+ }
+ toastId.current = toast.warning("Create a Term or Category");
+ } else {
+ searchParams.delete("relationshipName");
+ navigate(
+ {
+ pathname: "/search/searchResult",
+ search: searchParams.toString(),
+ },
+ { replace: true }
+ );
+ }
+ break;
+ case "Relationships":
+ case "CustomFilters":
+ if (
+ treeName === "Relationships" ||
+ (treeName === "CustomFilters" && node.parent === "BASIC_RELATIONSHIP")
+ ) {
+ navigate(
+ {
+ pathname: `relationship/relationshipSearchresult`,
+ search: searchParams.toString(),
+ },
+ { replace: true }
+ );
+ } else {
+ searchParams.delete("relationshipName");
+ navigate(
+ {
+ pathname: "/search/searchResult",
+ search: searchParams.toString(),
+ },
+ { replace: true }
+ );
+ }
+ break;
+ default:
searchParams.delete("relationshipName");
navigate(
- {
- pathname: "/search/searchResult",
- search: searchParams.toString(),
- },
+ { pathname: "/search/searchResult", search: searchParams.toString() },
{ replace: true }
);
- }
- break;
- default:
- searchParams.delete("relationshipName");
- navigate(
- { pathname: "/search/searchResult", search: searchParams.toString() },
- { replace: true }
- );
- break;
- }
- };
-
- const TreeLabelWithTooltip: React.FC<{ label: string }> = ({ label }) => {
- const labelRef = useRef(null);
- const [isOverflown, setIsOverflown] = useState(false);
-
- useEffect(() => {
- const el = labelRef.current;
- if (el) {
- setIsOverflown(el.scrollWidth > el.clientWidth);
+ break;
}
- }, [label, searchTerm]);
+ };
- return (
-
-
- {highlightText(label)}
-
-
- );
- };
+ const TreeLabelWithTooltip: React.FC<{ label: string }> = ({ label }) => {
+ const labelRef = useRef(null);
+ const [isOverflown, setIsOverflown] = useState(false);
- const renderTreeItem = (node: TreeNode) =>
- node?.id && (
- ) => {
- handleNodeClick(
- node,
- treeName,
- searchParams,
- navigate,
- isEmptyServicetype,
- savedSearchData,
- toastId
- );
- },
- className: "custom-treeitem-label",
- } as any)}
+ useEffect(() => {
+ const el = labelRef.current;
+ if (el) {
+ setIsOverflown(el.scrollWidth > el.clientWidth);
+ }
+ }, [label]);
+
+ return (
+
+
- {node.id != "No Records Found" && (
-
- )}
-
- {(treeName == "Entities" ||
- treeName == "Classifications" ||
- treeName == "CustomFilters" ||
- treeName == "Glossary") &&
- node.id != "No Records Found" && (
-
+ {highlightText(label)}
+
+
+ );
+ };
+
+ const renderTreeItem = (node: TreeNode) =>
+ node?.id && (
+ ) => {
+ handleNodeClick(
+ node,
+ treeName,
+ searchParams,
+ navigate,
+ isEmptyServicetype,
+ savedSearchData,
+ toastId
+ );
+ },
+ className: "custom-treeitem-label",
+ } as any)}
+ >
+ {(isHovered: boolean) => (
+ <>
+ {node.id !== "No Records Found" && (
+
+ )}
+
+ {(treeName === "Entities" ||
+ treeName === "Classifications" ||
+ treeName === "CustomFilters" ||
+ treeName === "Glossary") &&
+ node.id !== "No Records Found" && (
+
+ )}
+ >
)}
-
- }
- >
- {node.children && node.children.map((child) => renderTreeItem(child))}
-
- );
+
+ }
+ >
+ {node.children && node.children.map((child) => renderTreeItem(child))}
+
+ );
- const downloadFile = async () => {
- try {
- if (treeName == "Glossary") {
- await downloadGlossaryImportTemplate();
- return;
- }
- const apiResp: any = await getBusinessMetadataImportTmpl({});
- const text: string = apiResp ? apiResp.data : "";
- const blob = new Blob([text], { type: "text/plain" });
+ const downloadFile = async () => {
+ try {
+ if (treeName === "Glossary") {
+ await downloadGlossaryImportTemplate();
+ return;
+ }
+ const apiResp = await getBusinessMetadataImportTmpl({});
+ const text: string = apiResp && typeof apiResp === "object" && "data" in apiResp ? String(apiResp.data) : "";
+ const blob = new Blob([text], { type: "text/plain" });
- const url = window.URL.createObjectURL(blob);
+ const url = window.URL.createObjectURL(blob);
- const link = document.createElement("a");
- link.href = url;
- link.setAttribute("download", "template_business_metadata");
+ const link = document.createElement("a");
+ link.href = url;
+ link.setAttribute("download", "template_business_metadata");
- document.body.appendChild(link);
+ document.body.appendChild(link);
- link.click();
+ link.click();
- document.body.removeChild(link);
- window.URL.revokeObjectURL(url);
- } catch {
- /* ignore download error */
- }
- };
+ document.body.removeChild(link);
+ window.URL.revokeObjectURL(url);
+ } catch {
+ /* ignore download error */
+ }
+ };
- const label = { inputProps: { "aria-label": "Switch demo" } };
- return (
- <>
-
-
+
-
-
- {displayTreeName}
-
-
-
- {
- e.stopPropagation();
- refreshData();
- }}
- disabled={loader}
- >
-
-
-
-
-
- {(treeName == "Entities" ||
- treeName == "Classifications" ||
- treeName == "Glossary") && (
- <>
- {
-
- void }) => {
- e.stopPropagation();
- if (setisEmptyServicetype) {
- setisEmptyServicetype(!isEmptyServicetype);
- }
- }}
- data-cy="showEmptyServiceType"
- inputProps={{ "aria-label": "ant design" }}
- />
-
- }
- >
- )}
+
+
+
+ {treeName === "Entities" &&
}
+ {treeName === "Classifications" &&
}
+ {treeName === "Business MetaData" &&
}
+ {treeName === "Glossary" &&
}
+ {treeName === "CustomFilters" &&
}
+ {displayTreeName}
+
+
+
+ {
+ e.stopPropagation();
+ refreshData();
+ }}
+ disabled={loader}
+ >
+
+
+
- {(treeName == "Entities" ||
- treeName == "Classifications" ||
- treeName == "Glossary") && (
- {
- e.stopPropagation();
- handleClickMenu(e);
- }}
- data-cy="dropdownMenuButton"
- fontSize="small"
- />
- )}
- {treeName == "Business MetaData" && (
-
-
+ {
+
+ void }) => {
+ e.stopPropagation();
+ if (setisEmptyServicetype) {
+ setisEmptyServicetype(!isEmptyServicetype);
+ }
+ }}
+ data-cy="showEmptyServiceType"
+ inputProps={{ "aria-label": "ant design" }}
+ />
+
+ }
+ >
+ )}
+
+ {(treeName === "Entities" ||
+ treeName === "Classifications" ||
+ treeName === "Glossary") && (
+ ) => {
+ e.stopPropagation();
+ handleClickMenu(e as unknown as MouseEvent);
+ }}
+ data-cy="dropdownMenuButton"
+ fontSize="small"
+ />
+ )}
+
+ {treeName === "Business MetaData" && (
+
+ {
+ e.stopPropagation();
+ const newSearchParams = new URLSearchParams();
+
+ newSearchParams.set("tabActive", "businessMetadata");
+ navigate(
+ {
+ pathname: `administrator`,
+ search: newSearchParams.toString(),
+ },
+ { replace: true }
+ );
+ }}
+ data-cy="createBusinessMetadata"
+ />
+
+ )}
+
+
-
-
- }
- sx={{
- "& .MuiTreeItem-label": {
- fontWeight: "600 !important",
- fontSize: "14px !important",
- lineHeight: "26px !important",
- color: "white",
- },
- "& .MuiTreeItem-content svg": {
- color: "white",
- fontSize: "20px !important",
- },
- }}
- >
- {loader ? (
-
- ) : (
- filteredData.map((node: TreeNode) => renderTreeItem(node))
- )}
-
-
- {
+
+
+
+
+ Export Glossary
+
+
+ )}
+
+
+ }
+ >
+ {loader ? (
+
+ ) : (
+ filteredData.map((node: TreeNode) => renderTreeItem(node))
+ )}
+
+
+
{
void dispatch(fetchGlossaryData());
}
- : undefined
- }
+ : undefined
+ }
+ />
+
+
+
+ {tagModal && (
+
-
-
-
- {tagModal && (
-
- )}
- {glossaryModal && (
-
- )}
- >
- );
-};
+ )}
+ {glossaryModal && (
+
+ )}
+ >
+ );
+ };
export default BarTreeView;
diff --git a/dashboard/src/views/SideBar/SideBarTree/__tests__/SideBarTree.test.tsx b/dashboard/src/views/SideBar/SideBarTree/__tests__/SideBarTree.test.tsx
index 88b046432ac..79dea44b449 100644
--- a/dashboard/src/views/SideBar/SideBarTree/__tests__/SideBarTree.test.tsx
+++ b/dashboard/src/views/SideBar/SideBarTree/__tests__/SideBarTree.test.tsx
@@ -27,6 +27,7 @@
*/
import React from 'react'
+import '@testing-library/jest-dom'
import { render, screen, waitFor, fireEvent, act, cleanup } from '@testing-library/react'
import { Provider } from 'react-redux'
import { configureStore } from '@reduxjs/toolkit'
@@ -74,7 +75,7 @@ jest.mock('@redux/slice/glossarySlice', () => ({
}))
jest.mock('@components/ImportDialog', () => {
- return function MockImportDialog(props: any) {
+ return function MockImportDialog(props: Record) {
return props.open ? (