diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index 371b86ad70..d4174ab24e 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -7,6 +7,8 @@ All notable changes to the **Prowler UI** are documented in this file. ### 🚀 Added - `Cloud Provider` type filter to providers page [(#8473)](https://github.com/prowler-cloud/prowler/pull/8473) +- New menu item under Configuration section for quick access to the Mutelist [(#8444)](https://github.com/prowler-cloud/prowler/pull/8444) + ### 🔄 Changed diff --git a/ui/app/(prowler)/layout.tsx b/ui/app/(prowler)/layout.tsx index ff931d8f19..9bcaa79240 100644 --- a/ui/app/(prowler)/layout.tsx +++ b/ui/app/(prowler)/layout.tsx @@ -3,11 +3,13 @@ import "@/styles/globals.css"; import { Metadata, Viewport } from "next"; import React from "react"; +import { getProviders } from "@/actions/providers"; import MainLayout from "@/components/ui/main-layout/main-layout"; import { Toaster } from "@/components/ui/toast"; import { fontSans } from "@/config/fonts"; import { siteConfig } from "@/config/site"; import { cn } from "@/lib/utils"; +import { StoreInitializer } from "@/store/ui/store-initializer"; import { Providers } from "../providers"; @@ -29,11 +31,14 @@ export const viewport: Viewport = { ], }; -export default function RootLayout({ +export default async function RootLayout({ children, }: { children: React.ReactNode; }) { + const providersData = await getProviders({ page: 1, pageSize: 1 }); + const hasProviders = !!(providersData?.data && providersData.data.length > 0); + return ( @@ -45,6 +50,7 @@ export default function RootLayout({ )} > + {children} diff --git a/ui/app/(prowler)/providers/page.tsx b/ui/app/(prowler)/providers/page.tsx index 435b8d73b3..51da6777e9 100644 --- a/ui/app/(prowler)/providers/page.tsx +++ b/ui/app/(prowler)/providers/page.tsx @@ -33,7 +33,7 @@ export default async function Providers({ <>
- +
@@ -76,8 +76,6 @@ const ProvidersContent = async ({ pageSize, }); - const hasProviders = providersData?.data && providersData.data.length > 0; - const providerGroupDict = providersData?.included ?.filter((item: any) => item.type === "provider-groups") @@ -100,7 +98,7 @@ const ProvidersContent = async ({ <>
- +
diff --git a/ui/app/(prowler)/scans/page.tsx b/ui/app/(prowler)/scans/page.tsx index dda91b1747..6a56ba5a71 100644 --- a/ui/app/(prowler)/scans/page.tsx +++ b/ui/app/(prowler)/scans/page.tsx @@ -94,7 +94,7 @@ export default async function Scans({ />
- +
}> diff --git a/ui/components/providers/forms/muted-findings-config-form.tsx b/ui/components/providers/forms/muted-findings-config-form.tsx index b1950d8848..d6fdff5e9b 100644 --- a/ui/components/providers/forms/muted-findings-config-form.tsx +++ b/ui/components/providers/forms/muted-findings-config-form.tsx @@ -28,10 +28,12 @@ import { interface MutedFindingsConfigFormProps { setIsOpen: Dispatch>; + onCancel?: () => void; } export const MutedFindingsConfigForm = ({ setIsOpen, + onCancel, }: MutedFindingsConfigFormProps) => { const [config, setConfig] = useState(null); const [configText, setConfigText] = useState(""); @@ -237,6 +239,7 @@ export const MutedFindingsConfigForm = ({
diff --git a/ui/components/providers/index.ts b/ui/components/providers/index.ts index ef9ec70003..f37085f087 100644 --- a/ui/components/providers/index.ts +++ b/ui/components/providers/index.ts @@ -1,3 +1,4 @@ +export * from "../../store/ui/store-initializer"; export * from "./add-provider-button"; export * from "./credentials-update-info"; export * from "./forms/delete-form"; diff --git a/ui/components/providers/muted-findings-config-button.tsx b/ui/components/providers/muted-findings-config-button.tsx index e2d895fb76..645b6e6584 100644 --- a/ui/components/providers/muted-findings-config-button.tsx +++ b/ui/components/providers/muted-findings-config-button.tsx @@ -1,36 +1,38 @@ "use client"; import { SettingsIcon } from "lucide-react"; -import { useState } from "react"; import { CustomAlertModal, CustomButton } from "@/components/ui/custom"; +import { useUIStore } from "@/store/ui/store"; import { MutedFindingsConfigForm } from "./forms"; -interface MutedFindingsConfigButtonProps { - isDisabled?: boolean; -} - -export const MutedFindingsConfigButton = ({ - isDisabled = false, -}: MutedFindingsConfigButtonProps) => { - const [isOpen, setIsOpen] = useState(false); +export const MutedFindingsConfigButton = () => { + const { + isMutelistModalOpen, + openMutelistModal, + closeMutelistModal, + hasProviders, + } = useUIStore(); const handleOpenModal = () => { - if (!isDisabled) { - setIsOpen(true); + if (hasProviders) { + openMutelistModal(); } }; return ( <> - + } onPress={handleOpenModal} - isDisabled={isDisabled} + isDisabled={!hasProviders} > Configure Mutelist diff --git a/ui/components/ui/sidebar/collapse-menu-button.tsx b/ui/components/ui/sidebar/collapse-menu-button.tsx index cf9450a930..e74169f9a7 100644 --- a/ui/components/ui/sidebar/collapse-menu-button.tsx +++ b/ui/components/ui/sidebar/collapse-menu-button.tsx @@ -1,5 +1,6 @@ "use client"; +import { Tooltip } from "@nextui-org/react"; import { DropdownMenuArrow } from "@radix-ui/react-dropdown-menu"; import { ChevronDown } from "lucide-react"; import Link from "next/link"; @@ -19,12 +20,6 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu/dropdown-menu"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip/tooltip"; import { cn } from "@/lib/utils"; import { CollapseMenuButtonProps } from "@/types"; @@ -94,90 +89,188 @@ export const CollapseMenuButton = ({ {submenus.map( - ({ href, label, active, icon: SubIcon, target }, index) => ( - - ), +
+ +
+ + ); + } + + return ( + + ); + }, )}
) : ( - - - - - - - - - {label} - - - + + + + + {label} - {submenus.map(({ href, label, active, icon: SubIcon }, index) => ( - - - -

{label}

- -
- ))} + {submenus.map( + ( + { href, label, active, icon: SubIcon, disabled, onClick }, + index, + ) => { + const isActive = + (active === undefined && pathname === href) || active; + + if (disabled && label === "Mutelist") { + return ( + +
+ +
+ +

{label}

+
+
+
+
+ ); + } + + return ( + + {disabled ? ( +
+ +

{label}

+
+ ) : ( + + +

{label}

+ + )} +
+ ); + }, + )}
diff --git a/ui/components/ui/sidebar/menu.tsx b/ui/components/ui/sidebar/menu.tsx index 9ce1b2feb3..251bb3dd1f 100644 --- a/ui/components/ui/sidebar/menu.tsx +++ b/ui/components/ui/sidebar/menu.tsx @@ -17,6 +17,9 @@ import { import { useAuth } from "@/hooks"; import { getMenuList } from "@/lib/menu-list"; import { cn } from "@/lib/utils"; +import { useUIStore } from "@/store/ui/store"; +import { GroupProps } from "@/types"; +import { RolePermissionAttributes } from "@/types/users"; import { Button } from "../button/button"; import { CustomButton } from "../custom/custom-button"; @@ -24,7 +27,7 @@ import { ScrollArea } from "../scroll-area/scroll-area"; interface MenuHideRule { label: string; - condition: (permissions: any) => boolean; + condition: (permissions: RolePermissionAttributes) => boolean; } // Configuration for hiding menu items based on permissions @@ -48,16 +51,16 @@ const MENU_HIDE_RULES: MenuHideRule[] = [ // }, ]; -const hideMenuItems = (menuGroups: any[], labelsToHide: string[]) => { +const hideMenuItems = (menuGroups: GroupProps[], labelsToHide: string[]) => { return menuGroups.map((group) => ({ ...group, menus: group.menus - .filter((menu: any) => !labelsToHide.includes(menu.label)) - .map((menu: any) => ({ + .filter((menu) => !labelsToHide.includes(menu.label)) + .map((menu) => ({ ...menu, submenus: menu.submenus?.filter( - (submenu: any) => !labelsToHide.includes(submenu.label), + (submenu) => !labelsToHide.includes(submenu.label), ) || [], })), })); @@ -66,7 +69,12 @@ const hideMenuItems = (menuGroups: any[], labelsToHide: string[]) => { export const Menu = ({ isOpen }: { isOpen: boolean }) => { const pathname = usePathname(); const { permissions } = useAuth(); - const menuList = getMenuList(pathname); + const { hasProviders, openMutelistModal } = useUIStore(); + const menuList = getMenuList({ + pathname, + hasProviders, + openMutelistModal, + }); const labelsToHide = MENU_HIDE_RULES.filter((rule) => rule.condition(permissions), @@ -122,7 +130,7 @@ export const Menu = ({ isOpen }: { isOpen: boolean }) => { ) : (

)} - {menus.map((menu: any, index: number) => { + {menus.map((menu, index) => { const { href, label, diff --git a/ui/lib/menu-list.ts b/ui/lib/menu-list.ts index 41be0020be..e727a9d528 100644 --- a/ui/lib/menu-list.ts +++ b/ui/lib/menu-list.ts @@ -17,6 +17,7 @@ import { User, UserCog, Users, + VolumeX, Warehouse, } from "lucide-react"; @@ -34,7 +35,17 @@ import { } from "@/components/icons/Icons"; import { GroupProps } from "@/types"; -export const getMenuList = (pathname: string): GroupProps[] => { +interface MenuListOptions { + pathname: string; + hasProviders?: boolean; + openMutelistModal?: () => void; +} + +export const getMenuList = ({ + pathname, + hasProviders, + openMutelistModal, +}: MenuListOptions): GroupProps[] => { return [ { groupLabel: "", @@ -146,6 +157,14 @@ export const getMenuList = (pathname: string): GroupProps[] => { icon: Settings, submenus: [ { href: "/providers", label: "Cloud Providers", icon: CloudCog }, + { + // Use trailing slash to prevent both menu items from being active at /providers + href: "/providers/", + label: "Mutelist", + icon: VolumeX, + disabled: hasProviders === false, + onClick: openMutelistModal, + }, { href: "/manage-groups", label: "Provider Groups", icon: Group }, { href: "/scans", label: "Scan Jobs", icon: Timer }, { href: "/integrations", label: "Integrations", icon: Puzzle }, diff --git a/ui/store/index.ts b/ui/store/index.ts index 36c2042a88..c059e1cb31 100644 --- a/ui/store/index.ts +++ b/ui/store/index.ts @@ -1 +1 @@ -export * from "./ui/ui-store"; +export * from "./ui/store"; diff --git a/ui/store/ui/store-initializer.tsx b/ui/store/ui/store-initializer.tsx new file mode 100644 index 0000000000..d2f1d32965 --- /dev/null +++ b/ui/store/ui/store-initializer.tsx @@ -0,0 +1,27 @@ +"use client"; + +import { useEffect } from "react"; + +import { useUIStore } from "@/store/ui/store"; + +interface StoreInitializerProps { + values: { + hasProviders?: boolean; + // Add more properties here as needed + // otherProperty?: string; + }; +} + +export function StoreInitializer({ values }: StoreInitializerProps) { + const setHasProviders = useUIStore((state) => state.setHasProviders); + + useEffect(() => { + // Initialize store values from server + if (values.hasProviders !== undefined) { + setHasProviders(values.hasProviders); + } + // Add more setters here as needed in the future + }, [values.hasProviders, setHasProviders]); + + return null; +} diff --git a/ui/store/ui/store.ts b/ui/store/ui/store.ts new file mode 100644 index 0000000000..edc329654f --- /dev/null +++ b/ui/store/ui/store.ts @@ -0,0 +1,32 @@ +import { create } from "zustand"; +import { persist } from "zustand/middleware"; + +interface UIStoreState { + isSideMenuOpen: boolean; + isMutelistModalOpen: boolean; + hasProviders: boolean; + + openSideMenu: () => void; + closeSideMenu: () => void; + openMutelistModal: () => void; + closeMutelistModal: () => void; + setHasProviders: (value: boolean) => void; +} + +export const useUIStore = create()( + persist( + (set) => ({ + isSideMenuOpen: false, + isMutelistModalOpen: false, + hasProviders: false, + openSideMenu: () => set({ isSideMenuOpen: true }), + closeSideMenu: () => set({ isSideMenuOpen: false }), + openMutelistModal: () => set({ isMutelistModalOpen: true }), + closeMutelistModal: () => set({ isMutelistModalOpen: false }), + setHasProviders: (value: boolean) => set({ hasProviders: value }), + }), + { + name: "ui-store", + }, + ), +); diff --git a/ui/store/ui/ui-store.ts b/ui/store/ui/ui-store.ts deleted file mode 100644 index db9e8e6674..0000000000 --- a/ui/store/ui/ui-store.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { create } from "zustand"; -import { persist } from "zustand/middleware"; - -interface SidebarStoreState { - isSideMenuOpen: boolean; - - openSideMenu: () => void; - closeSideMenu: () => void; -} - -export const useUIStore = create()( - persist( - (set) => ({ - isSideMenuOpen: false, - openSideMenu: () => set({ isSideMenuOpen: true }), - closeSideMenu: () => set({ isSideMenuOpen: false }), - }), - { - name: "sidebar-store", - }, - ), -); diff --git a/ui/types/components.ts b/ui/types/components.ts index 9fc48b9fc0..0fbb838787 100644 --- a/ui/types/components.ts +++ b/ui/types/components.ts @@ -20,6 +20,8 @@ export type SubmenuProps = { label: string; active?: boolean; icon: IconComponent; + disabled?: boolean; + onClick?: () => void; }; export type MenuProps = {