diff --git a/package.json b/package.json index bb5bb8e629..efd1770db0 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "homepage": "/dashboard", "dependencies": { - "@devtron-labs/devtron-fe-common-lib": "4.0.4-pre-0", + "@devtron-labs/devtron-fe-common-lib": "4.0.4-beta-7", "@esbuild-plugins/node-globals-polyfill": "0.2.3", "@sentry/browser": "7.119.1", "@sentry/integrations": "7.50.0", diff --git a/src/Pages/Shared/CommandBar/utils.tsx b/src/Pages/Shared/CommandBar/utils.tsx index 704b32919e..23defe29dc 100644 --- a/src/Pages/Shared/CommandBar/utils.tsx +++ b/src/Pages/Shared/CommandBar/utils.tsx @@ -11,6 +11,7 @@ import { import { QueryParams as ChartStoreQueryParams } from '@Components/charts/constants' import { getNavigationList } from '@Components/Navigation' +import { hasNavigationGroupItems } from '@Components/Navigation/utils' import { getClusterChangeRedirectionUrl } from '@Components/ResourceBrowser/Utils' import { URLS } from '@Config/routes' @@ -103,36 +104,46 @@ const getNavItemBreakdownItems = ( export const getNavigationGroups = (serverMode: SERVER_MODE, isSuperAdmin: boolean): CommandBarGroupType[] => getNavigationList(serverMode).map((group) => { - const parsedItems = group.items.flatMap( - ({ hasSubMenu, subItems, title, href, id, icon, keywords }) => { - if (hasSubMenu && subItems?.length) { - return subItems.map((subItem) => ({ - title: `${title} / ${subItem.title}`, - id: subItem.id, - // Since icon is not present for some subItems, using from group - icon: NAV_SUB_ITEMS_ICON_MAPPING[id] || group.icon, - // TODO: No href present for some subItems - href: subItem.href ?? null, - keywords: subItem.keywords || [], - })) - } - - const breakdownItems = getNavItemBreakdownItems(id, serverMode, isSuperAdmin) - - if (breakdownItems.length) { - return breakdownItems - } - - return { - title, - id, - icon: icon || 'ic-arrow-right', - // TODO: No href present for some items - href: href ?? null, - keywords: keywords || [], - } - }, - ) + const parsedItems = hasNavigationGroupItems(group) + ? group.items.flatMap( + ({ hasSubMenu, subItems, title, href, id, icon, keywords }) => { + if (hasSubMenu && subItems?.length) { + return subItems.map((subItem) => ({ + title: `${title} / ${subItem.title}`, + id: subItem.id, + // Since icon is not present for some subItems, using from group + icon: NAV_SUB_ITEMS_ICON_MAPPING[id] || group.icon, + // TODO: No href present for some subItems + href: subItem.href ?? null, + keywords: subItem.keywords || [], + })) + } + + const breakdownItems = getNavItemBreakdownItems(id, serverMode, isSuperAdmin) + + if (breakdownItems.length) { + return breakdownItems + } + + return { + title, + id, + icon: icon || 'ic-arrow-right', + // TODO: No href present for some items + href: href ?? null, + keywords: keywords || [], + } + }, + ) + : [ + { + title: group.title, + id: group.id, + icon: group.icon, + href: group.href, + keywords: [], + } as CommandBarGroupType['items'][number], + ] return { title: group.title, diff --git a/src/components/Navigation/Navigation.tsx b/src/components/Navigation/Navigation.tsx index 1b5a7c9f8e..f6ae1c4732 100644 --- a/src/components/Navigation/Navigation.tsx +++ b/src/components/Navigation/Navigation.tsx @@ -42,7 +42,13 @@ import { NavGroup } from './NavGroup' import { NavigationLogo, NavigationLogoExpanded } from './NavigationLogo' import { NavItem } from './NavItem' import { NavGroupProps, NavigationProps } from './types' -import { doesNavigationItemMatchPath, filterNavigationItems, findActiveNavigationItemOfNavGroup } from './utils' +import { + doesNavigationGroupMatchPath, + doesNavigationItemMatchPath, + filterNavigationItems, + findActiveNavigationItemOfNavGroup, + hasNavigationGroupItems, +} from './utils' import './styles.scss' @@ -143,17 +149,23 @@ export const Navigation = ({ const NAVIGATION_LIST = useMemo(() => getNavigationList(serverMode), [serverMode]) const selectedNavGroup = useMemo( - () => NAVIGATION_LIST.find(({ items }) => items.some((item) => doesNavigationItemMatchPath(item, pathname))), - [pathname], + () => NAVIGATION_LIST.find((group) => doesNavigationGroupMatchPath(group, pathname)), + [NAVIGATION_LIST, pathname], + ) + + const selectedExpandableNavGroup = useMemo( + () => (hasNavigationGroupItems(selectedNavGroup) ? selectedNavGroup : null), + [selectedNavGroup], ) // The current navigation group is the one that is hovered or the one that is active, \ // this is used to determine which nav group items are to be shown in expanded state. - const currentNavGroup = hoveredNavGroup || selectedNavGroup + const currentNavGroup = hoveredNavGroup || selectedExpandableNavGroup const isExpanded = !!hoveredNavGroup const navItems = useMemo( - () => (currentNavGroup ? filterNavigationItems(currentNavGroup.items, searchText) : []), + () => + hasNavigationGroupItems(currentNavGroup) ? filterNavigationItems(currentNavGroup.items, searchText) : [], [currentNavGroup, searchText], ) @@ -164,6 +176,7 @@ export const Navigation = ({ // Prevent navigation, if the item is already active if ( selectedNavGroup?.id === navItem.id && + hasNavigationGroupItems(selectedNavGroup) && doesNavigationItemMatchPath(findActiveNavigationItemOfNavGroup(selectedNavGroup.items), pathname) ) { e.preventDefault() @@ -172,6 +185,13 @@ export const Navigation = ({ category: 'Navigation', action: `nav-${navItem.id}`, }) + + if (!hasNavigationGroupItems(navItem)) { + setHoveredNavGroup(null) + setSearchText('') + return + } + setHoveredNavGroup(navItem) setSearchText('') } @@ -186,25 +206,27 @@ export const Navigation = ({ setSearchText('') } - const handleNavGroupHover = (navGroup: typeof hoveredNavGroup) => (isHovered: boolean) => { - clearTimeout(timeoutRef.current) + const handleNavGroupHover = + (navGroup: NavigationGroupType & { items: NonNullable }) => + (isHovered: boolean) => { + clearTimeout(timeoutRef.current) - if (isHovered) { - if (!hoveredNavGroup) { - setHoveredNavGroup(navGroup) - return - } + if (isHovered) { + if (!hoveredNavGroup) { + setHoveredNavGroup(navGroup) + return + } - timeoutRef.current = setTimeout(() => { - setHoveredNavGroup(navGroup) - setSearchText('') - }, 50) + timeoutRef.current = setTimeout(() => { + setHoveredNavGroup(navGroup) + setSearchText('') + }, 50) + } } - } const handleOpenExpandedNavigation = (e: MouseEvent) => { if (!hoveredNavGroup && e.target === e.currentTarget) { - setHoveredNavGroup(selectedNavGroup) + setHoveredNavGroup(selectedExpandableNavGroup) } } @@ -253,8 +275,16 @@ export const Navigation = ({ isExpanded={isExpanded} isSelected={hoveredNavGroup?.id === item.id || selectedNavGroup?.id === item.id} onClick={handleNavGroupClick(item)} - to={findActiveNavigationItemOfNavGroup(item.items)?.href} - onHover={handleNavGroupHover(item)} + to={ + hasNavigationGroupItems(item) + ? findActiveNavigationItemOfNavGroup(item.items)?.href + : item.href + } + onHover={ + hasNavigationGroupItems(item) + ? handleNavGroupHover(item) + : handleCloseExpandedNavigation(true) + } showTooltip={item.disabled} /> ))} diff --git a/src/components/Navigation/constants.ts b/src/components/Navigation/constants.ts index f4e890c9be..75ff8f58d4 100644 --- a/src/components/Navigation/constants.ts +++ b/src/components/Navigation/constants.ts @@ -2,7 +2,7 @@ import { NavigationGroupType, NavigationItemType, ROUTER_URLS, SERVER_MODE } fro import { importComponentFromFELibrary } from '@Components/common' -import { filterNavGroupAndItem } from './utils' +import { filterNavGroupAndItem, hasNavigationGroupItems } from './utils' const APPLICATION_MANAGEMENT_POLICIES_NAV_ITEM: NavigationItemType = importComponentFromFELibrary( 'APPLICATION_MANAGEMENT_POLICIES_NAV_ITEM', @@ -322,6 +322,12 @@ const NAVIGATION_LIST: NavigationGroupType[] = [ ], isAvailableInEA: true, }, + { + id: 'audit-logs', + title: 'Audit logs', + icon: 'ic-file-log-search', + href: ROUTER_URLS.AUDIT_LOGS, + }, ] export const getNavigationList = (serverMode: SERVER_MODE): NavigationGroupType[] => { @@ -332,37 +338,41 @@ export const getNavigationList = (serverMode: SERVER_MODE): NavigationGroupType[ ), ) - const filteredNavItems = filteredNavGroup.map((group) => { - const filteredItems = group.items.filter((item) => - filterNavGroupAndItem( - { - forceHideEnvKey: item.forceHideEnvKey, - hideNav: item.hideNav, - isAvailableInEA: item.isAvailableInEA, - }, - serverMode, - ), - ) - return { ...group, items: filteredItems } - }) + return filteredNavGroup.map((group) => { + if (!hasNavigationGroupItems(group)) { + return group + } + + const filteredItems = group.items + .filter((item) => + filterNavGroupAndItem( + { + forceHideEnvKey: item.forceHideEnvKey, + hideNav: item.hideNav, + isAvailableInEA: item.isAvailableInEA, + }, + serverMode, + ), + ) + .map((item) => { + if (item.hasSubMenu && item.subItems) { + const filteredSubItems = item.subItems.filter((subItem) => + filterNavGroupAndItem( + { + forceHideEnvKey: subItem.forceHideEnvKey, + hideNav: subItem.hideNav, + isAvailableInEA: subItem.isAvailableInEA, + }, + serverMode, + ), + ) - return filteredNavItems.map((group) => ({ - ...group, - items: group.items.map((item) => { - if (item.hasSubMenu && item.subItems) { - const filteredSubItems = item.subItems.filter((subItem) => - filterNavGroupAndItem( - { - forceHideEnvKey: subItem.forceHideEnvKey, - hideNav: subItem.hideNav, - isAvailableInEA: subItem.isAvailableInEA, - }, - serverMode, - ), - ) - return { ...item, subItems: filteredSubItems } - } - return item - }), - })) + return { ...item, subItems: filteredSubItems } + } + + return item + }) + + return { ...group, items: filteredItems } as NavigationGroupType + }) } diff --git a/src/components/Navigation/utils.ts b/src/components/Navigation/utils.ts index 27a89bff0b..5b2706f9c3 100644 --- a/src/components/Navigation/utils.ts +++ b/src/components/Navigation/utils.ts @@ -1,5 +1,6 @@ import { CommonNavigationItemType, + NavigationGroupType, NavigationItemType, SERVER_MODE, TreeNode, @@ -108,6 +109,10 @@ export const doesNavigationItemMatchPath = ( return !navItem.disabled && item.href && isSubPath(item.href, pathname) } +export const hasNavigationGroupItems = ( + group: NavigationGroupType | null | undefined, +): group is NavigationGroupType & { items: NavigationItemType[] } => Array.isArray(group?.items) + /** * Finds the first enabled navigation item within a group. * @param items The navigation item group to search. @@ -116,6 +121,14 @@ export const doesNavigationItemMatchPath = ( export const findActiveNavigationItemOfNavGroup = (items: NavigationItemType[]) => items.find(({ disabled }) => !disabled) +export const doesNavigationGroupMatchPath = (group: NavigationGroupType, pathname: string): boolean => { + if (hasNavigationGroupItems(group)) { + return group.items.some((item) => doesNavigationItemMatchPath(item, pathname)) + } + + return !group.disabled && isSubPath(group.href, pathname) +} + export const filterNavGroupAndItem = ( item: Pick, serverMode: SERVER_MODE, diff --git a/src/components/common/navigation/NavigationRoutes.tsx b/src/components/common/navigation/NavigationRoutes.tsx index 5985505849..6a36e87964 100644 --- a/src/components/common/navigation/NavigationRoutes.tsx +++ b/src/components/common/navigation/NavigationRoutes.tsx @@ -132,6 +132,8 @@ const CostVisibilityRouter = importComponentFromFELibrary('CostVisibilityRouter' const AIRecommendations = importComponentFromFELibrary('AIRecommendations', null, 'function') const AIChatProvider = importComponentFromFELibrary('AIChatProvider', null, 'function') +const AuditLogsRouter = importComponentFromFELibrary('AuditLogs', null, 'function') + const NavigationRoutes = ({ reloadVersionConfig }: Readonly) => { const navigate = useNavigate() const location = useLocation() @@ -564,6 +566,13 @@ const NavigationRoutes = ({ reloadVersionConfig }: Readonly} /> + {serverMode === SERVER_MODE.FULL && AuditLogsRouter && ( + } + /> + )} {!window._env_.K8S_CLIENT ? [ ...(serverMode === SERVER_MODE.FULL diff --git a/src/config/constants.ts b/src/config/constants.ts index 4ad3402cfe..9befc86417 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -89,6 +89,7 @@ export const Routes = { EPHEMERAL_CONTAINERS: 'k8s/resources/ephemeralContainers', APP_EDIT: 'app/edit', APPLICATION_EXTERNAL_HELM_RELEASE: 'application/external-helm-release', + AUDIT_LOG: 'audit-log', JOB_CI_DETAIL: 'job/ci-pipeline/list', diff --git a/yarn.lock b/yarn.lock index 29d7531bd3..6310855c8d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1738,9 +1738,9 @@ __metadata: languageName: node linkType: hard -"@devtron-labs/devtron-fe-common-lib@npm:4.0.4-pre-0": - version: 4.0.4-pre-0 - resolution: "@devtron-labs/devtron-fe-common-lib@npm:4.0.4-pre-0" +"@devtron-labs/devtron-fe-common-lib@npm:4.0.4-beta-7": + version: 4.0.4-beta-7 + resolution: "@devtron-labs/devtron-fe-common-lib@npm:4.0.4-beta-7" dependencies: "@codemirror/autocomplete": "npm:6.18.6" "@codemirror/lang-json": "npm:6.0.1" @@ -1794,7 +1794,7 @@ __metadata: react-select: 5.8.0 rxjs: ^7.8.1 yaml: ^2.8.3 - checksum: 10c0/ae699b1673b72454475d699a6f593658bcb9f4fa5e8443a4c794429795ac060ef91fb1fdb371fd12b496fca9e146674585cd31cfd1d26b48f4fa7aae5d363ffc + checksum: 10c0/762f4e6482f0c504b6ee3e7b167304088b296d2b7ad0ee9f962a4dc5c9fc0891e73b388b87671363f5c94cffb35179a8a5db90d09ef1862e7954b78fce81b009 languageName: node linkType: hard @@ -5529,7 +5529,7 @@ __metadata: version: 0.0.0-use.local resolution: "dashboard@workspace:." dependencies: - "@devtron-labs/devtron-fe-common-lib": "npm:4.0.4-pre-0" + "@devtron-labs/devtron-fe-common-lib": "npm:4.0.4-beta-7" "@esbuild-plugins/node-globals-polyfill": "npm:0.2.3" "@playwright/test": "npm:^1.32.1" "@sentry/browser": "npm:7.119.1"