From ac93672752d9cc08108f868444b5b0610dd31017 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Wed, 4 Sep 2024 17:10:26 +0200 Subject: [PATCH 01/13] chore: WIP --- actions/providers.ts | 15 ++- app/(prowler)/providers/page.tsx | 25 ++--- .../providers/table/DataTableProvider.tsx | 15 +++ .../providers/table/FilterColumnTable.tsx | 97 +++++++++++++++++++ hooks/index.ts | 1 + hooks/useDebounce.ts | 15 +++ types/components.ts | 12 ++- 7 files changed, 155 insertions(+), 25 deletions(-) create mode 100644 components/providers/table/FilterColumnTable.tsx create mode 100644 hooks/useDebounce.ts diff --git a/actions/providers.ts b/actions/providers.ts index 8c50440f46..b69ce2e98a 100644 --- a/actions/providers.ts +++ b/actions/providers.ts @@ -10,7 +10,7 @@ export const getProvider = async ({ page = 1, query = "", sort = "", - filter = "", + filters = {}, }) => { const session = await auth(); const tenantId = session?.user.tenantId; @@ -19,11 +19,16 @@ export const getProvider = async ({ const keyServer = process.env.LOCAL_SERVER_URL; const url = new URL(`${keyServer}/providers`); - if (page) url.searchParams.append("page[number]", page.toString()); - if (query) url.searchParams.append("filter[query]", query); - if (sort) url.searchParams.append("sort", sort); - if (filter) url.searchParams.append("filter[provider]", filter); + if (page) url.searchParams.append("page[number]", page.toString()); + if (query) url.searchParams.append("filter[search]", query); + if (sort) url.searchParams.append("sort", sort); + + // Handle multiple filters + Object.entries(filters).forEach(([key, value]) => { + url.searchParams.append(key, String(value)); + }); + console.log({ query }); try { const providers = await fetch(`${url.toString()}`, { headers: { diff --git a/app/(prowler)/providers/page.tsx b/app/(prowler)/providers/page.tsx index c6d380ccac..a28d4ba08d 100644 --- a/app/(prowler)/providers/page.tsx +++ b/app/(prowler)/providers/page.tsx @@ -10,16 +10,12 @@ import { SkeletonTableProvider, } from "@/components/providers"; import { Header } from "@/components/ui"; +import { SearchParamsProps } from "@/types"; export default async function Providers({ searchParams, }: { - searchParams?: { - query?: string; - page?: string; - sort?: string; - filter?: string; - }; + searchParams: SearchParamsProps; }) { const searchParamsKey = JSON.stringify(searchParams || {}); @@ -43,19 +39,18 @@ export default async function Providers({ const SSRDataTable = async ({ searchParams, }: { - searchParams?: { - query?: string; - page?: string; - sort?: string; - filter?: string; - }; + searchParams: SearchParamsProps; }) => { const query = searchParams?.query || ""; - const page = searchParams?.page ? parseInt(searchParams.page) : 1; + const page = searchParams?.page || "1"; const sort = searchParams?.sort || ""; - const filter = searchParams?.filter || ""; - const providersData = await getProvider({ query, page, sort, filter }); + // Extract all filter parameters + const filters = Object.fromEntries( + Object.entries(searchParams).filter(([key]) => key.startsWith("filter[")), + ); + + const providersData = await getProvider({ query, page, sort, filters }); const [providers] = await Promise.all([providersData]); if (providers?.errors) redirect("/providers"); diff --git a/components/providers/table/DataTableProvider.tsx b/components/providers/table/DataTableProvider.tsx index 2a037faf4f..151a269f96 100644 --- a/components/providers/table/DataTableProvider.tsx +++ b/components/providers/table/DataTableProvider.tsx @@ -2,8 +2,10 @@ import { ColumnDef, + ColumnFiltersState, flexRender, getCoreRowModel, + getFilteredRowModel, getPaginationRowModel, getSortedRowModel, SortingState, @@ -22,6 +24,7 @@ import { import { MetaDataProps } from "@/types"; import { DataTablePagination } from "./DataTablePagination"; +import { FilterColumnTable } from "./FilterColumnTable"; interface DataTableProviderProps { columns: ColumnDef[]; @@ -35,6 +38,7 @@ export function DataTableProvider({ metadata, }: DataTableProviderProps) { const [sorting, setSorting] = useState([]); + const [columnFilters, setColumnFilters] = useState([]); const table = useReactTable({ data, columns, @@ -43,12 +47,23 @@ export function DataTableProvider({ getPaginationRowModel: getPaginationRowModel(), onSortingChange: setSorting, getSortedRowModel: getSortedRowModel(), + onColumnFiltersChange: setColumnFilters, + getFilteredRowModel: getFilteredRowModel(), state: { sorting, + columnFilters, }, }); + + const filters = [ + { key: "provider", values: ["aws", "gcp", "azure"] }, + { key: "connected", values: ["true", "null"] }, + // Add more filter categories as needed + ]; + return ( <> +
diff --git a/components/providers/table/FilterColumnTable.tsx b/components/providers/table/FilterColumnTable.tsx new file mode 100644 index 0000000000..739d630d0f --- /dev/null +++ b/components/providers/table/FilterColumnTable.tsx @@ -0,0 +1,97 @@ +import { Button, Input } from "@nextui-org/react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useCallback, useEffect, useMemo, useState } from "react"; + +import { useDebounce } from "@/hooks/useDebounce"; + +interface FilterColumnTableProps { + filters: { key: string; values: string[] }[]; +} + +export function FilterColumnTable({ filters }: FilterColumnTableProps) { + const router = useRouter(); + const searchParams = useSearchParams(); + const [searchQuery, setSearchQuery] = useState(""); + const debouncedSearchQuery = useDebounce(searchQuery, 300); + + const activeFilters = useMemo(() => { + const currentFilters: Record = {}; + Array.from(searchParams.entries()).forEach(([key, value]) => { + if (key.startsWith("filter[") && key.endsWith("]")) { + const filterKey = key.slice(7, -1); + if (filters.some((filter) => filter.key === filterKey)) { + // eslint-disable-next-line security/detect-object-injection + currentFilters[filterKey] = value; + } + } + }); + return currentFilters; + }, [searchParams, filters]); + + const applyFilter = useCallback( + (key: string, value: string) => { + const params = new URLSearchParams(searchParams); + const filterKey = `filter[${key}]`; + + if (params.get(filterKey) === value) { + // If the filter is already set, remove it + params.delete(filterKey); + } else { + // Otherwise, set or update the filter + params.set(filterKey, value); + } + + router.push(`?${params.toString()}`); + }, + [router, searchParams], + ); + + const applySearch = useCallback( + (query: string) => { + const params = new URLSearchParams(searchParams.toString()); + if (query) { + params.set("filter[search]", query); + } else { + params.delete("filter[search]"); + } + console.log(params.toString(), "en el apply search"); + router.push(`?${params.toString()}`, { scroll: false }); + }, + [router, searchParams], + ); + + useEffect(() => { + applySearch(debouncedSearchQuery); + }, [debouncedSearchQuery, applySearch]); + + return ( +
+
+ setSearchQuery(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + applySearch(searchQuery); + } + }} + /> +
+
+ {filters.flatMap(({ key, values }) => + values.map((value) => ( + + )), + )} +
+
+ ); +} diff --git a/hooks/index.ts b/hooks/index.ts index 709aadd864..b32251b922 100644 --- a/hooks/index.ts +++ b/hooks/index.ts @@ -1 +1,2 @@ +export * from "./useDebounce"; export * from "./useLocalStorage"; diff --git a/hooks/useDebounce.ts b/hooks/useDebounce.ts new file mode 100644 index 0000000000..3351cba2fc --- /dev/null +++ b/hooks/useDebounce.ts @@ -0,0 +1,15 @@ +import { useEffect, useState } from "react"; + +export function useDebounce(value: T, delay?: number): T { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const timer = setTimeout(() => setDebouncedValue(value), delay ?? 800); + + return () => { + clearTimeout(timer); + }; + }, [value, delay]); + + return debouncedValue; +} diff --git a/types/components.ts b/types/components.ts index 0fc88a2190..8aa6744b11 100644 --- a/types/components.ts +++ b/types/components.ts @@ -9,12 +9,14 @@ export type IconProps = { style?: React.CSSProperties; }; -export interface searchParamsProps { - searchParams: { - page?: string; - }; +// export interface searchParamsProps { +// searchParams: { +// page?: string; +// }; +// } +export interface SearchParamsProps { + [key: string]: string | string[] | undefined; } - export interface ProviderProps { id: string; type: "providers"; From bb32af93b2ab27a9a0a462a465897c2231674db7 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Thu, 5 Sep 2024 13:12:20 +0200 Subject: [PATCH 02/13] feat: filters, search and sorting is working as expected --- actions/providers.ts | 8 ++-- app/(prowler)/providers/page.tsx | 16 ++++---- .../providers/table/DataTableColumnHeader.tsx | 15 +++++-- .../providers/table/FilterColumnTable.tsx | 40 ++++++++++++++++--- hooks/useDebounce.ts | 2 +- 5 files changed, 60 insertions(+), 21 deletions(-) diff --git a/actions/providers.ts b/actions/providers.ts index b69ce2e98a..2ee10763e8 100644 --- a/actions/providers.ts +++ b/actions/providers.ts @@ -26,11 +26,13 @@ export const getProvider = async ({ // Handle multiple filters Object.entries(filters).forEach(([key, value]) => { - url.searchParams.append(key, String(value)); + if (key !== "filter[search]") { + url.searchParams.append(key, String(value)); + } }); - console.log({ query }); + try { - const providers = await fetch(`${url.toString()}`, { + const providers = await fetch(url.toString(), { headers: { "X-Tenant-ID": `${tenantId}`, }, diff --git a/app/(prowler)/providers/page.tsx b/app/(prowler)/providers/page.tsx index a28d4ba08d..a234ffaa8c 100644 --- a/app/(prowler)/providers/page.tsx +++ b/app/(prowler)/providers/page.tsx @@ -1,5 +1,4 @@ import { Spacer } from "@nextui-org/react"; -import { redirect } from "next/navigation"; import { Suspense } from "react"; import { getProvider } from "@/actions"; @@ -41,25 +40,24 @@ const SSRDataTable = async ({ }: { searchParams: SearchParamsProps; }) => { - const query = searchParams?.query || ""; - const page = searchParams?.page || "1"; - const sort = searchParams?.sort || ""; + const page = parseInt(searchParams.page?.toString() || "1", 10); + const sort = searchParams.sort?.toString() || ""; // Extract all filter parameters const filters = Object.fromEntries( Object.entries(searchParams).filter(([key]) => key.startsWith("filter[")), ); - const providersData = await getProvider({ query, page, sort, filters }); - const [providers] = await Promise.all([providersData]); + // Extract query from filters + const query = (filters["filter[search]"] as string) || ""; - if (providers?.errors) redirect("/providers"); + const providersData = await getProvider({ query, page, sort, filters }); return ( ); }; diff --git a/components/providers/table/DataTableColumnHeader.tsx b/components/providers/table/DataTableColumnHeader.tsx index 6051a3ef87..3b460ae937 100644 --- a/components/providers/table/DataTableColumnHeader.tsx +++ b/components/providers/table/DataTableColumnHeader.tsx @@ -27,7 +27,8 @@ export const DataTableColumnHeader = ({ const searchParams = useSearchParams(); const getToggleSortingHandler = () => { - const currentSortParam = searchParams.get("sort"); + const currentParams = new URLSearchParams(searchParams.toString()); + const currentSortParam = currentParams.get("sort"); let newSortParam = ""; if (currentSortParam === `${param}`) { @@ -41,13 +42,21 @@ export const DataTableColumnHeader = ({ newSortParam = `${param}`; } - // Construct the new URL with the sorting parameter - const newUrl = newSortParam ? `${pathname}?sort=${newSortParam}` : pathname; + // Update or remove the sort parameter + if (newSortParam) { + currentParams.set("sort", newSortParam); + } else { + currentParams.delete("sort"); + } + + // Construct the new URL with all parameters + const newUrl = `${pathname}?${currentParams.toString()}`; router.push(newUrl, { scroll: false, }); }; + const renderSortIcon = () => { const currentSortParam = searchParams.get("sort"); if ( diff --git a/components/providers/table/FilterColumnTable.tsx b/components/providers/table/FilterColumnTable.tsx index 739d630d0f..02e7a54802 100644 --- a/components/providers/table/FilterColumnTable.tsx +++ b/components/providers/table/FilterColumnTable.tsx @@ -1,4 +1,5 @@ import { Button, Input } from "@nextui-org/react"; +import { XCircle } from "lucide-react"; // Import the clear icon import { useRouter, useSearchParams } from "next/navigation"; import { useCallback, useEffect, useMemo, useState } from "react"; @@ -11,7 +12,10 @@ interface FilterColumnTableProps { export function FilterColumnTable({ filters }: FilterColumnTableProps) { const router = useRouter(); const searchParams = useSearchParams(); - const [searchQuery, setSearchQuery] = useState(""); + const [searchQuery, setSearchQuery] = useState(() => { + // Initialize searchQuery with the current search filter value from URL + return searchParams.get("filter[search]") || ""; + }); const debouncedSearchQuery = useDebounce(searchQuery, 300); const activeFilters = useMemo(() => { @@ -54,19 +58,29 @@ export function FilterColumnTable({ filters }: FilterColumnTableProps) { } else { params.delete("filter[search]"); } - console.log(params.toString(), "en el apply search"); - router.push(`?${params.toString()}`, { scroll: false }); + router.replace(`?${params.toString()}`, { scroll: false }); }, [router, searchParams], ); + const clearAllFilters = useCallback(() => { + const params = new URLSearchParams(); + router.push(`?${params.toString()}`, { scroll: false }); + setSearchQuery(""); // Clear the search input + }, [router]); + + const clearIconSearch = () => { + setSearchQuery(""); + applySearch(""); + }; + useEffect(() => { applySearch(debouncedSearchQuery); }, [debouncedSearchQuery, applySearch]); return (
-
+
+ + + ) + } />
-
+
{filters.flatMap(({ key, values }) => values.map((value) => ( )), )} +
); diff --git a/hooks/useDebounce.ts b/hooks/useDebounce.ts index 3351cba2fc..f0d8d7c3bf 100644 --- a/hooks/useDebounce.ts +++ b/hooks/useDebounce.ts @@ -4,7 +4,7 @@ export function useDebounce(value: T, delay?: number): T { const [debouncedValue, setDebouncedValue] = useState(value); useEffect(() => { - const timer = setTimeout(() => setDebouncedValue(value), delay ?? 800); + const timer = setTimeout(() => setDebouncedValue(value), delay ?? 900); return () => { clearTimeout(timer); From 2e09667babaa72a3aabbbf7e5220abaf0d1e163a Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Thu, 5 Sep 2024 13:13:02 +0200 Subject: [PATCH 03/13] fix: fix type for SearchParamsProps in all pages --- app/(prowler)/compliance/page.tsx | 20 +++++++++++++++----- app/(prowler)/services/page.tsx | 19 ++++++++++++++----- app/(prowler)/users/page.tsx | 20 +++++++++++++++----- types/components.ts | 5 ----- 4 files changed, 44 insertions(+), 20 deletions(-) diff --git a/app/(prowler)/compliance/page.tsx b/app/(prowler)/compliance/page.tsx index e610187e06..6cc1c5c11e 100644 --- a/app/(prowler)/compliance/page.tsx +++ b/app/(prowler)/compliance/page.tsx @@ -9,24 +9,34 @@ import { } from "@/components/compliance"; import { FilterControls } from "@/components/filters"; import { Header } from "@/components/ui"; -import { searchParamsProps } from "@/types"; +import { SearchParamsProps } from "@/types"; + +export default async function Compliance({ + searchParams, +}: { + searchParams: SearchParamsProps; +}) { + const searchParamsKey = JSON.stringify(searchParams || {}); -export default async function Compliance({ searchParams }: searchParamsProps) { return ( <>
- }> + }> ); } -const SSRComplianceGrid = async ({ searchParams }: searchParamsProps) => { - const page = searchParams.page ? parseInt(searchParams.page) : 1; +const SSRComplianceGrid = async ({ + searchParams, +}: { + searchParams: SearchParamsProps; +}) => { + const page = parseInt(searchParams.page?.toString() || "1", 10); const compliancesData = await getCompliance({ page }); const [compliances] = await Promise.all([compliancesData]); diff --git a/app/(prowler)/services/page.tsx b/app/(prowler)/services/page.tsx index 872d7f3042..24fef60b25 100644 --- a/app/(prowler)/services/page.tsx +++ b/app/(prowler)/services/page.tsx @@ -6,9 +6,14 @@ import { getService } from "@/actions/services"; import { FilterControls } from "@/components/filters"; import { ServiceCard, ServiceSkeletonGrid } from "@/components/services"; import { Header } from "@/components/ui"; -import { searchParamsProps } from "@/types"; +import { SearchParamsProps } from "@/types"; -export default async function Services({ searchParams }: searchParamsProps) { +export default async function Services({ + searchParams, +}: { + searchParams: SearchParamsProps; +}) { + const searchParamsKey = JSON.stringify(searchParams || {}); return ( <>
- }> + }> ); } -const SSRServiceGrid = async ({ searchParams }: searchParamsProps) => { - const page = searchParams.page ? parseInt(searchParams.page) : 1; +const SSRServiceGrid = async ({ + searchParams, +}: { + searchParams: SearchParamsProps; +}) => { + const page = parseInt(searchParams.page?.toString() || "1", 10); const servicesData = await getService({ page }); const [services] = await Promise.all([servicesData]); diff --git a/app/(prowler)/users/page.tsx b/app/(prowler)/users/page.tsx index cff19b91e0..a7f643d73b 100644 --- a/app/(prowler)/users/page.tsx +++ b/app/(prowler)/users/page.tsx @@ -11,14 +11,20 @@ import { DataTableUser, SkeletonTableUser, } from "@/components/users"; -import { searchParamsProps } from "@/types"; +import { SearchParamsProps } from "@/types"; -export default async function Users({ searchParams }: searchParamsProps) { +export default async function Users({ + searchParams, +}: { + searchParams: SearchParamsProps; +}) { const session = await auth(); if (session?.user?.role !== "admin") { redirect("/"); } + const searchParamsKey = JSON.stringify(searchParams || {}); + return ( <>
@@ -28,7 +34,7 @@ export default async function Users({ searchParams }: searchParamsProps) {
- }> + }>
@@ -36,8 +42,12 @@ export default async function Users({ searchParams }: searchParamsProps) { ); } -const SSRDataTable = async ({ searchParams }: searchParamsProps) => { - const page = searchParams.page ? parseInt(searchParams.page) : 1; +const SSRDataTable = async ({ + searchParams, +}: { + searchParams: SearchParamsProps; +}) => { + const page = parseInt(searchParams.page?.toString() || "1", 10); const usersData = await getUsers({ page }); const [users] = await Promise.all([usersData]); diff --git a/types/components.ts b/types/components.ts index 8aa6744b11..bd2502a44b 100644 --- a/types/components.ts +++ b/types/components.ts @@ -9,11 +9,6 @@ export type IconProps = { style?: React.CSSProperties; }; -// export interface searchParamsProps { -// searchParams: { -// page?: string; -// }; -// } export interface SearchParamsProps { [key: string]: string | string[] | undefined; } From 97616213dba9fbbac9f5762d32b137fdffe0874f Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Thu, 5 Sep 2024 19:05:36 +0200 Subject: [PATCH 04/13] chore: main filters are working and tweaks styles --- app/(prowler)/providers/page.tsx | 3 ++ components/filters/CustomDatePicker.tsx | 42 +++++++++++++++------ components/filters/CustomProviderInputs.tsx | 16 ++++++-- components/filters/CustomSelectProvider.tsx | 33 +++++++++++++++- components/filters/FilterControls.tsx | 34 ++++++++++++++++- 5 files changed, 111 insertions(+), 17 deletions(-) diff --git a/app/(prowler)/providers/page.tsx b/app/(prowler)/providers/page.tsx index a234ffaa8c..0d86b9ff2f 100644 --- a/app/(prowler)/providers/page.tsx +++ b/app/(prowler)/providers/page.tsx @@ -2,6 +2,7 @@ import { Spacer } from "@nextui-org/react"; import { Suspense } from "react"; import { getProvider } from "@/actions"; +import { FilterControls } from "@/components/filters"; import { AddProviderModal, ColumnsProvider, @@ -22,6 +23,8 @@ export default async function Providers({ <>
+ +
diff --git a/components/filters/CustomDatePicker.tsx b/components/filters/CustomDatePicker.tsx index 667078bc8f..5bc7a7d6d5 100644 --- a/components/filters/CustomDatePicker.tsx +++ b/components/filters/CustomDatePicker.tsx @@ -7,21 +7,41 @@ import { today, } from "@internationalized/date"; import { Button, ButtonGroup, DatePicker } from "@nextui-org/react"; -import { useDateFormatter, useLocale } from "@react-aria/i18n"; -import React from "react"; +import { useLocale } from "@react-aria/i18n"; +import { useRouter, useSearchParams } from "next/navigation"; +import React, { useCallback } from "react"; export const CustomDatePicker = () => { + const router = useRouter(); + const searchParams = useSearchParams(); const defaultDate = today(getLocalTimeZone()); const [value, setValue] = React.useState(defaultDate); const { locale } = useLocale(); - const formatter = useDateFormatter({ dateStyle: "full" }); const now = today(getLocalTimeZone()); const nextWeek = startOfWeek(now.add({ weeks: 1 }), locale); const nextMonth = startOfMonth(now.add({ months: 1 })); + const applyDateFilter = useCallback( + (date: any) => { + const params = new URLSearchParams(searchParams.toString()); + if (date) { + params.set("filter[updated_at__lte]", date.toString()); + } else { + params.delete("filter[updated_at__lte]"); + } + router.push(`?${params.toString()}`, { scroll: false }); + }, + [router, searchParams], + ); + + const handleDateChange = (newValue: any) => { + setValue(newValue); + applyDateFilter(newValue); + }; + return (
{ size="sm" variant="bordered" > - - - + + + } calendarProps={{ @@ -50,15 +74,11 @@ export const CustomDatePicker = () => { }, }} value={value} - onChange={setValue} + onChange={handleDateChange} label="Scan date" size="sm" variant="flat" /> -

- Selected date:{" "} - {value ? formatter.format(value.toDate(getLocalTimeZone())) : "--"} -

); }; diff --git a/components/filters/CustomProviderInputs.tsx b/components/filters/CustomProviderInputs.tsx index d97b03b842..373b6255b1 100644 --- a/components/filters/CustomProviderInputs.tsx +++ b/components/filters/CustomProviderInputs.tsx @@ -4,12 +4,13 @@ import { AWSProviderBadge, AzureProviderBadge, GCPProviderBadge, + KS8ProviderBadge, } from "../icons/providers-badge"; export const CustomProviderInputAWS = () => { return (
- +

Amazon Web Services

); @@ -18,7 +19,7 @@ export const CustomProviderInputAWS = () => { export const CustomProviderInputAzure = () => { return (
- +

Azure

); @@ -27,8 +28,17 @@ export const CustomProviderInputAzure = () => { export const CustomProviderInputGCP = () => { return (
- +

Google Cloud Platform

); }; + +export const CustomProviderInputKubernetes = () => { + return ( +
+ +

Kubernetes

+
+ ); +}; diff --git a/components/filters/CustomSelectProvider.tsx b/components/filters/CustomSelectProvider.tsx index 222793fdf8..dd4254fdd3 100644 --- a/components/filters/CustomSelectProvider.tsx +++ b/components/filters/CustomSelectProvider.tsx @@ -1,11 +1,14 @@ "use client"; import { Select, SelectItem } from "@nextui-org/react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useCallback, useEffect, useState } from "react"; import { CustomProviderInputAWS, CustomProviderInputAzure, CustomProviderInputGCP, + CustomProviderInputKubernetes, } from "./CustomProviderInputs"; const dataInputsProvider = [ @@ -27,11 +30,33 @@ const dataInputsProvider = [ { key: "kubernetes", label: "Kubernetes", - value: , + value: , }, ]; export const CustomSelectProvider = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + const [selectedProvider, setSelectedProvider] = useState(""); + + const applyProviderFilter = useCallback( + (value: string) => { + const params = new URLSearchParams(searchParams.toString()); + if (value) { + params.set("filter[provider]", value); + } else { + params.delete("filter[provider]"); + } + router.push(`?${params.toString()}`, { scroll: false }); + }, + [router, searchParams], + ); + + useEffect(() => { + const providerFromUrl = searchParams.get("filter[provider]") || ""; + setSelectedProvider(providerFromUrl); + }, [searchParams]); + return ( setSearchQuery(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + applySearch(searchQuery); + } + }} + endContent={ + searchQuery && ( + + ) + } + size="sm" + /> + ); +}; diff --git a/components/filters/FilterControls.tsx b/components/filters/FilterControls.tsx index ea4bf5dace..a1ddc3d011 100644 --- a/components/filters/FilterControls.tsx +++ b/components/filters/FilterControls.tsx @@ -2,30 +2,26 @@ import { Button } from "@nextui-org/react"; import { useRouter, useSearchParams } from "next/navigation"; -import React from "react"; -import { useCallback } from "react"; +import React, { useCallback } from "react"; -// import { SearchParamsProps } from "../../types/components"; import { CustomAccountSelection } from "./CustomAccountSelection"; import { CustomCheckboxMutedFindings } from "./CustomCheckboxMutedFindings"; import { CustomDatePicker } from "./CustomDatePicker"; +import { CustomSearchInput } from "./CustomSearchInput"; import { CustomSelectProvider } from "./CustomSelectProvider"; interface FilterControlsProps { mutedFindings?: boolean; - // searchParams: SearchParamsProps; } export const FilterControls: React.FC = ({ mutedFindings = true, - // searchParams, }) => { const router = useRouter(); const searchParams = useSearchParams(); const clearAllFilters = useCallback(() => { const params = new URLSearchParams(searchParams.toString()); - // Remove all filter parameters Array.from(params.keys()).forEach((key) => { if (key.startsWith("filter[")) { params.delete(key); @@ -33,14 +29,16 @@ export const FilterControls: React.FC = ({ }); router.push(`?${params.toString()}`, { scroll: false }); }, [router, searchParams]); + return (
+ - ) - } - /> -
-
- {filters.flatMap(({ key, values }) => - values.map((value) => ( - - )), - )} - -
+
+ {filters.flatMap(({ key, values }) => + values.map((value) => ( + + )), + )}
); } diff --git a/store/ui/ui-store.ts b/store/ui/ui-store.ts index b47d474c94..db9e8e6674 100644 --- a/store/ui/ui-store.ts +++ b/store/ui/ui-store.ts @@ -11,7 +11,7 @@ interface SidebarStoreState { export const useUIStore = create()( persist( (set) => ({ - isSideMenuOpen: true, + isSideMenuOpen: false, openSideMenu: () => set({ isSideMenuOpen: true }), closeSideMenu: () => set({ isSideMenuOpen: false }), }), From a9ff875a3acc8e2fc3323ce4b95b6801b2e3b070 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Fri, 6 Sep 2024 09:10:41 +0200 Subject: [PATCH 07/13] style: re-style action dropdown --- .../ButtonCheckConnectionProvider.tsx | 6 +- components/providers/ButtonDeleteProvider.tsx | 6 +- .../providers/table/ColumnsProvider.tsx | 64 ++++++++++++++++--- 3 files changed, 59 insertions(+), 17 deletions(-) diff --git a/components/providers/ButtonCheckConnectionProvider.tsx b/components/providers/ButtonCheckConnectionProvider.tsx index 9c3076de78..5b921a7dfb 100644 --- a/components/providers/ButtonCheckConnectionProvider.tsx +++ b/components/providers/ButtonCheckConnectionProvider.tsx @@ -4,18 +4,16 @@ import { Button } from "@nextui-org/react"; import React from "react"; import { useFormStatus } from "react-dom"; -import { CheckIcon } from "../icons"; - export const ButtonCheckConnectionProvider = () => { const { pending } = useFormStatus(); return ( ); }; diff --git a/components/providers/ButtonDeleteProvider.tsx b/components/providers/ButtonDeleteProvider.tsx index ef13a5c9e5..ceabce0c6d 100644 --- a/components/providers/ButtonDeleteProvider.tsx +++ b/components/providers/ButtonDeleteProvider.tsx @@ -4,18 +4,16 @@ import { Button } from "@nextui-org/react"; import React from "react"; import { useFormStatus } from "react-dom"; -import { DeleteIcon } from "../icons"; - export const ButtonDeleteProvider = () => { const { pending } = useFormStatus(); return ( ); }; diff --git a/components/providers/table/ColumnsProvider.tsx b/components/providers/table/ColumnsProvider.tsx index 7a6b6d6270..e08050e620 100644 --- a/components/providers/table/ColumnsProvider.tsx +++ b/components/providers/table/ColumnsProvider.tsx @@ -5,9 +5,11 @@ import { Dropdown, DropdownItem, DropdownMenu, + DropdownSection, DropdownTrigger, } from "@nextui-org/react"; import { ColumnDef } from "@tanstack/react-table"; +import clsx from "clsx"; import { add } from "date-fns"; import { VerticalDotsIcon } from "@/components/icons"; @@ -19,11 +21,19 @@ import { DateWithTime } from "../DateWithTime"; import { DeleteProvider } from "../DeleteProvider"; import { ProviderInfo } from "../ProviderInfo"; import { DataTableColumnHeader } from "./DataTableColumnHeader"; - const getProviderData = (row: { original: ProviderProps }) => { return row.original; }; +import { + AddNoteBulkIcon, + DeleteDocumentBulkIcon, + EditDocumentBulkIcon, +} from "@nextui-org/shared-icons"; + +const iconClasses = + "text-2xl text-default-500 pointer-events-none flex-shrink-0"; + export const ColumnsProvider: ColumnDef[] = [ { header: "ID", @@ -117,19 +127,55 @@ export const ColumnsProvider: ColumnDef[] = [ const { id } = getProviderData(row); return (
- + - - - - - - - + + + } + > + + + + } + > + Edit provider + + + + + } + > + + +
From bc7c3bd74b22f8fe4cfa33a487b0a4e678266034 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Fri, 6 Sep 2024 10:20:59 +0200 Subject: [PATCH 08/13] refactor: remove two components and create a new one, reducing code and improving efficiency --- .../ButtonCheckConnectionProvider.tsx | 19 ------------ components/providers/ButtonDeleteProvider.tsx | 19 ------------ .../providers/CheckConnectionProvider.tsx | 4 +-- components/providers/DeleteProvider.tsx | 4 +-- components/providers/index.ts | 2 -- .../ui/custom/CustomButtonClientAction.tsx | 30 +++++++++++++++++++ components/ui/custom/index.ts | 1 + 7 files changed, 35 insertions(+), 44 deletions(-) delete mode 100644 components/providers/ButtonCheckConnectionProvider.tsx delete mode 100644 components/providers/ButtonDeleteProvider.tsx create mode 100644 components/ui/custom/CustomButtonClientAction.tsx diff --git a/components/providers/ButtonCheckConnectionProvider.tsx b/components/providers/ButtonCheckConnectionProvider.tsx deleted file mode 100644 index 5b921a7dfb..0000000000 --- a/components/providers/ButtonCheckConnectionProvider.tsx +++ /dev/null @@ -1,19 +0,0 @@ -"use client"; - -import { Button } from "@nextui-org/react"; -import React from "react"; -import { useFormStatus } from "react-dom"; - -export const ButtonCheckConnectionProvider = () => { - const { pending } = useFormStatus(); - return ( - - ); -}; diff --git a/components/providers/ButtonDeleteProvider.tsx b/components/providers/ButtonDeleteProvider.tsx deleted file mode 100644 index ceabce0c6d..0000000000 --- a/components/providers/ButtonDeleteProvider.tsx +++ /dev/null @@ -1,19 +0,0 @@ -"use client"; - -import { Button } from "@nextui-org/react"; -import React from "react"; -import { useFormStatus } from "react-dom"; - -export const ButtonDeleteProvider = () => { - const { pending } = useFormStatus(); - return ( - - ); -}; diff --git a/components/providers/CheckConnectionProvider.tsx b/components/providers/CheckConnectionProvider.tsx index ed3f5db69a..a7d4ec8360 100644 --- a/components/providers/CheckConnectionProvider.tsx +++ b/components/providers/CheckConnectionProvider.tsx @@ -4,8 +4,8 @@ import { useRef } from "react"; import { checkConnectionProvider } from "@/actions"; +import { CustomButtonClientAction } from "../ui/custom"; import { useToast } from "../ui/toast"; -import { ButtonCheckConnectionProvider } from "./ButtonCheckConnectionProvider"; export const CheckConnectionProvider = ({ id }: { id: string }) => { const ref = useRef(null); @@ -35,7 +35,7 @@ export const CheckConnectionProvider = ({ id }: { id: string }) => { return (
- + ); }; diff --git a/components/providers/DeleteProvider.tsx b/components/providers/DeleteProvider.tsx index dba2cf22e8..c5090ef274 100644 --- a/components/providers/DeleteProvider.tsx +++ b/components/providers/DeleteProvider.tsx @@ -4,8 +4,8 @@ import { useRef } from "react"; import { deleteProvider } from "@/actions"; +import { CustomButtonClientAction } from "../ui/custom"; import { useToast } from "../ui/toast"; -import { ButtonDeleteProvider } from "./ButtonDeleteProvider"; export const DeleteProvider = ({ id }: { id: string }) => { const ref = useRef(null); @@ -35,7 +35,7 @@ export const DeleteProvider = ({ id }: { id: string }) => { return (
- + ); }; diff --git a/components/providers/index.ts b/components/providers/index.ts index 0b4074236c..58c8dae90f 100644 --- a/components/providers/index.ts +++ b/components/providers/index.ts @@ -1,8 +1,6 @@ export * from "./AddProvider"; export * from "./AddProviderModal"; export * from "./ButtonAddProvider"; -export * from "./ButtonCheckConnectionProvider"; -export * from "./ButtonDeleteProvider"; export * from "./CheckConnectionProvider"; export * from "./CustomRadioProvider"; export * from "./DateWithTime"; diff --git a/components/ui/custom/CustomButtonClientAction.tsx b/components/ui/custom/CustomButtonClientAction.tsx new file mode 100644 index 0000000000..ef31fbb5ec --- /dev/null +++ b/components/ui/custom/CustomButtonClientAction.tsx @@ -0,0 +1,30 @@ +"use client"; + +import { Button, CircularProgress } from "@nextui-org/react"; +import clsx from "clsx"; +import { useFormStatus } from "react-dom"; + +interface CustomButtonClientActionProps { + buttonLabel: string; + danger?: boolean; +} + +export const CustomButtonClientAction = ({ + buttonLabel, + danger = false, +}: CustomButtonClientActionProps) => { + const { pending } = useFormStatus(); + + return ( + + ); +}; diff --git a/components/ui/custom/index.ts b/components/ui/custom/index.ts index 126110ade6..dbc111a3cc 100644 --- a/components/ui/custom/index.ts +++ b/components/ui/custom/index.ts @@ -1,2 +1,3 @@ export * from "./CustomBox"; +export * from "./CustomButtonClientAction"; export * from "./CustomInput"; From ff0ba89a3f9e6604ade1108490c8ff67a1e76f71 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Fri, 6 Sep 2024 10:25:57 +0200 Subject: [PATCH 09/13] feat: clean all filters button is removing now the sort param --- components/filters/FilterControls.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/filters/FilterControls.tsx b/components/filters/FilterControls.tsx index a1ddc3d011..cd70ca9c44 100644 --- a/components/filters/FilterControls.tsx +++ b/components/filters/FilterControls.tsx @@ -23,7 +23,7 @@ export const FilterControls: React.FC = ({ const clearAllFilters = useCallback(() => { const params = new URLSearchParams(searchParams.toString()); Array.from(params.keys()).forEach((key) => { - if (key.startsWith("filter[")) { + if (key.startsWith("filter[") || key === "sort") { params.delete(key); } }); From 5326ffbcc9e067f324f1e2f88ae3e7dbf152df52 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Fri, 6 Sep 2024 11:10:14 +0200 Subject: [PATCH 10/13] feat: add CustomRegionSelection for the filters --- app/(prowler)/page.tsx | 2 +- app/(prowler)/providers/page.tsx | 2 +- .../filters/CustomCheckboxMutedFindings.tsx | 18 ++----- components/filters/CustomRegionSelection.tsx | 50 +++++++++++++++++++ components/filters/FilterControls.tsx | 24 ++++++--- components/filters/index.ts | 1 + 6 files changed, 75 insertions(+), 22 deletions(-) create mode 100644 components/filters/CustomRegionSelection.tsx diff --git a/app/(prowler)/page.tsx b/app/(prowler)/page.tsx index 9577693ca8..b352170bd1 100644 --- a/app/(prowler)/page.tsx +++ b/app/(prowler)/page.tsx @@ -11,7 +11,7 @@ export default function Home() { <>
- +
- +
diff --git a/components/filters/CustomCheckboxMutedFindings.tsx b/components/filters/CustomCheckboxMutedFindings.tsx index 62715403fe..0ebfcb7755 100644 --- a/components/filters/CustomCheckboxMutedFindings.tsx +++ b/components/filters/CustomCheckboxMutedFindings.tsx @@ -1,20 +1,10 @@ import { Checkbox } from "@nextui-org/react"; import React from "react"; -interface CustomCheckboxMutedFindingsProps { - mutedFindings?: boolean; -} - -export const CustomCheckboxMutedFindings: React.FC< - CustomCheckboxMutedFindingsProps -> = ({ mutedFindings }) => { +export const CustomCheckboxMutedFindings = () => { return ( - <> - {mutedFindings && ( - - Include Muted Findings - - )} - + + Include Muted Findings + ); }; diff --git a/components/filters/CustomRegionSelection.tsx b/components/filters/CustomRegionSelection.tsx new file mode 100644 index 0000000000..9204da3d33 --- /dev/null +++ b/components/filters/CustomRegionSelection.tsx @@ -0,0 +1,50 @@ +"use client"; +import { Select, SelectItem } from "@nextui-org/react"; + +const regions = [ + { key: "af-south-1", label: "AF South 1" }, + { key: "ap-east-1", label: "AP East 1" }, + { key: "ap-northeast-1", label: "AP Northeast 1" }, + { key: "ap-northeast-2", label: "AP Northeast 2" }, + { key: "ap-northeast-3", label: "AP Northeast 3" }, + { key: "ap-south-1", label: "AP South 1" }, + { key: "ap-south-2", label: "AP South 2" }, + { key: "ap-southeast-1", label: "AP Southeast 1" }, + { key: "ap-southeast-2", label: "AP Southeast 2" }, + { key: "ap-southeast-3", label: "AP Southeast 3" }, + { key: "ap-southeast-4", label: "AP Southeast 4" }, + { key: "ca-central-1", label: "CA Central 1" }, + { key: "ca-west-1", label: "CA West 1" }, + { key: "eu-central-1", label: "EU Central 1" }, + { key: "eu-central-2", label: "EU Central 2" }, + { key: "eu-north-1", label: "EU North 1" }, + { key: "eu-south-1", label: "EU South 1" }, + { key: "eu-south-2", label: "EU South 2" }, + { key: "eu-west-1", label: "EU West 1" }, + { key: "eu-west-2", label: "EU West 2" }, + { key: "eu-west-3", label: "EU West 3" }, + { key: "il-central-1", label: "IL Central 1" }, + { key: "me-central-1", label: "ME Central 1" }, + { key: "me-south-1", label: "ME South 1" }, + { key: "sa-east-1", label: "SA East 1" }, + { key: "us-east-1", label: "US East 1" }, + { key: "us-east-2", label: "US East 2" }, + { key: "us-west-1", label: "US West 1" }, + { key: "us-west-2", label: "US West 2" }, +]; + +export const CustomRegionSelection = () => { + return ( + + ); +}; diff --git a/components/filters/FilterControls.tsx b/components/filters/FilterControls.tsx index cd70ca9c44..af76da5679 100644 --- a/components/filters/FilterControls.tsx +++ b/components/filters/FilterControls.tsx @@ -7,15 +7,26 @@ import React, { useCallback } from "react"; import { CustomAccountSelection } from "./CustomAccountSelection"; import { CustomCheckboxMutedFindings } from "./CustomCheckboxMutedFindings"; import { CustomDatePicker } from "./CustomDatePicker"; +import { CustomRegionSelection } from "./CustomRegionSelection"; import { CustomSearchInput } from "./CustomSearchInput"; import { CustomSelectProvider } from "./CustomSelectProvider"; interface FilterControlsProps { + search?: boolean; + providers?: boolean; + date?: boolean; + regions?: boolean; + accounts?: boolean; mutedFindings?: boolean; } export const FilterControls: React.FC = ({ - mutedFindings = true, + search = false, + providers = false, + date = false, + regions = false, + accounts = false, + mutedFindings = false, }) => { const router = useRouter(); const searchParams = useSearchParams(); @@ -32,11 +43,12 @@ export const FilterControls: React.FC = ({ return (
- - - - - + {search && } + {providers && } + {date && } + {regions && } + {accounts && } + {mutedFindings && } + + {showClearButton && ( + + )}
); }; From 3d120b350557da9eaada4e500be1661eb3ab40a5 Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Fri, 6 Sep 2024 16:42:12 +0200 Subject: [PATCH 12/13] chore: WIP --- components/providers/table/DataTableProvider.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/components/providers/table/DataTableProvider.tsx b/components/providers/table/DataTableProvider.tsx index 151a269f96..b89fa2a6c6 100644 --- a/components/providers/table/DataTableProvider.tsx +++ b/components/providers/table/DataTableProvider.tsx @@ -55,9 +55,13 @@ export function DataTableProvider({ }, }); + // This will be used to have "custom" filters in the table and will be + // passed to the FilterColumnTable component. This needs to be dynamic + // based on the columns that are available in the table. + const filters = [ { key: "provider", values: ["aws", "gcp", "azure"] }, - { key: "connected", values: ["true", "null"] }, + { key: "connected", values: ["false", "true"] }, // Add more filter categories as needed ]; From 3cc9910f61e265889bdbc73da10aa7ed44f94abe Mon Sep 17 00:00:00 2001 From: Pablo Lara Date: Fri, 6 Sep 2024 16:45:01 +0200 Subject: [PATCH 13/13] fix: prevent crash when there is no connection with the API --- app/(prowler)/providers/page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/(prowler)/providers/page.tsx b/app/(prowler)/providers/page.tsx index 0a7fdd0949..7b2d11118e 100644 --- a/app/(prowler)/providers/page.tsx +++ b/app/(prowler)/providers/page.tsx @@ -59,8 +59,8 @@ const SSRDataTable = async ({ return ( ); };