mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-23 20:42:02 +00:00
Merge pull request #52 from prowler-cloud/PRWLR-4626-Implement-newTable-Providers
Implement filters, search with the API
This commit is contained in:
+11
-4
@@ -10,7 +10,7 @@ export const getProvider = async ({
|
||||
page = 1,
|
||||
query = "",
|
||||
sort = "",
|
||||
filter = "",
|
||||
filters = {},
|
||||
}) => {
|
||||
const session = await auth();
|
||||
const tenantId = session?.user.tenantId;
|
||||
@@ -19,13 +19,20 @@ 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 (query) url.searchParams.append("filter[search]", query);
|
||||
if (sort) url.searchParams.append("sort", sort);
|
||||
if (filter) url.searchParams.append("filter[provider]", filter);
|
||||
|
||||
// Handle multiple filters
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
if (key !== "filter[search]") {
|
||||
url.searchParams.append(key, String(value));
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const providers = await fetch(`${url.toString()}`, {
|
||||
const providers = await fetch(url.toString(), {
|
||||
headers: {
|
||||
"X-Tenant-ID": `${tenantId}`,
|
||||
},
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<Header title="Compliance" icon="fluent-mdl2:compliance-audit" />
|
||||
<Spacer y={4} />
|
||||
<FilterControls mutedFindings={false} />
|
||||
<Spacer y={4} />
|
||||
<Suspense key={searchParams.page} fallback={<ComplianceSkeletonGrid />}>
|
||||
<Suspense key={searchParamsKey} fallback={<ComplianceSkeletonGrid />}>
|
||||
<SSRComplianceGrid searchParams={searchParams} />
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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]);
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ export default function Home() {
|
||||
<>
|
||||
<Header title="Scan Overview" icon="solar:pie-chart-2-outline" />
|
||||
<Spacer y={4} />
|
||||
<FilterControls />
|
||||
<FilterControls providers date accounts regions mutedFindings />
|
||||
<Spacer y={10} />
|
||||
<div className="grid grid-cols-12 gap-4">
|
||||
<CustomBox
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Spacer } from "@nextui-org/react";
|
||||
import { redirect } from "next/navigation";
|
||||
import { Suspense } from "react";
|
||||
|
||||
import { getProvider } from "@/actions";
|
||||
import { FilterControls } from "@/components/filters";
|
||||
import {
|
||||
AddProviderModal,
|
||||
ColumnsProvider,
|
||||
@@ -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 || {});
|
||||
|
||||
@@ -27,6 +23,8 @@ export default async function Providers({
|
||||
<>
|
||||
<Header title="Providers" icon="fluent:cloud-sync-24-regular" />
|
||||
<Spacer y={4} />
|
||||
<FilterControls search providers />
|
||||
<Spacer y={4} />
|
||||
<div className="flex flex-col items-end w-full">
|
||||
<div className="flex space-x-6">
|
||||
<AddProviderModal />
|
||||
@@ -43,28 +41,26 @@ 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 sort = searchParams?.sort || "";
|
||||
const filter = searchParams?.filter || "";
|
||||
const page = parseInt(searchParams.page?.toString() || "1", 10);
|
||||
const sort = searchParams.sort?.toString() || "";
|
||||
|
||||
const providersData = await getProvider({ query, page, sort, filter });
|
||||
const [providers] = await Promise.all([providersData]);
|
||||
// Extract all filter parameters
|
||||
const filters = Object.fromEntries(
|
||||
Object.entries(searchParams).filter(([key]) => key.startsWith("filter[")),
|
||||
);
|
||||
|
||||
if (providers?.errors) redirect("/providers");
|
||||
// Extract query from filters
|
||||
const query = (filters["filter[search]"] as string) || "";
|
||||
|
||||
const providersData = await getProvider({ query, page, sort, filters });
|
||||
|
||||
return (
|
||||
<DataTableProvider
|
||||
columns={ColumnsProvider}
|
||||
data={providers?.data ?? []}
|
||||
metadata={providers?.meta}
|
||||
data={providersData?.data || []}
|
||||
metadata={providersData?.meta}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<Header
|
||||
@@ -18,15 +23,19 @@ export default async function Services({ searchParams }: searchParamsProps) {
|
||||
<Spacer y={4} />
|
||||
<FilterControls />
|
||||
<Spacer y={4} />
|
||||
<Suspense key={searchParams.page} fallback={<ServiceSkeletonGrid />}>
|
||||
<Suspense key={searchParamsKey} fallback={<ServiceSkeletonGrid />}>
|
||||
<SSRServiceGrid searchParams={searchParams} />
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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]);
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<Header title="User Management" icon="ci:users" />
|
||||
@@ -28,7 +34,7 @@ export default async function Users({ searchParams }: searchParamsProps) {
|
||||
<AddUserModal />
|
||||
</div>
|
||||
<Spacer y={6} />
|
||||
<Suspense key={searchParams.page} fallback={<SkeletonTableUser />}>
|
||||
<Suspense key={searchParamsKey} fallback={<SkeletonTableUser />}>
|
||||
<SSRDataTable searchParams={searchParams} />
|
||||
</Suspense>
|
||||
</div>
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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 && (
|
||||
<Checkbox className="xl:-mt-8" size="md" color="danger">
|
||||
Include Muted Findings
|
||||
</Checkbox>
|
||||
)}
|
||||
</>
|
||||
<Checkbox className="xl:-mt-8" size="md" color="danger">
|
||||
Include Muted Findings
|
||||
</Checkbox>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,21 +7,55 @@ 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, useEffect, useRef } 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 initialRender = useRef(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialRender.current) {
|
||||
initialRender.current = false;
|
||||
return;
|
||||
}
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
if (params.size === 0) {
|
||||
// If all params are cleared, reset to default date
|
||||
setValue(defaultDate);
|
||||
}
|
||||
}, [searchParams]);
|
||||
|
||||
const handleDateChange = (newValue: any) => {
|
||||
setValue(newValue);
|
||||
applyDateFilter(newValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col md:gap-2 w-full">
|
||||
<DatePicker
|
||||
@@ -34,9 +68,13 @@ export const CustomDatePicker = () => {
|
||||
size="sm"
|
||||
variant="bordered"
|
||||
>
|
||||
<Button onPress={() => setValue(now)}>Today</Button>
|
||||
<Button onPress={() => setValue(nextWeek)}>Next week</Button>
|
||||
<Button onPress={() => setValue(nextMonth)}>Next month</Button>
|
||||
<Button onPress={() => handleDateChange(now)}>Today</Button>
|
||||
<Button onPress={() => handleDateChange(nextWeek)}>
|
||||
Next week
|
||||
</Button>
|
||||
<Button onPress={() => handleDateChange(nextMonth)}>
|
||||
Next month
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
}
|
||||
calendarProps={{
|
||||
@@ -50,15 +88,11 @@ export const CustomDatePicker = () => {
|
||||
},
|
||||
}}
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
onChange={handleDateChange}
|
||||
label="Scan date"
|
||||
size="sm"
|
||||
variant="flat"
|
||||
/>
|
||||
<p className="hidden md:flex text-default-500 text-sm">
|
||||
Selected date:{" "}
|
||||
{value ? formatter.format(value.toDate(getLocalTimeZone())) : "--"}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,12 +4,13 @@ import {
|
||||
AWSProviderBadge,
|
||||
AzureProviderBadge,
|
||||
GCPProviderBadge,
|
||||
KS8ProviderBadge,
|
||||
} from "../icons/providers-badge";
|
||||
|
||||
export const CustomProviderInputAWS = () => {
|
||||
return (
|
||||
<div className="flex gap-x-2 items-center">
|
||||
<AWSProviderBadge width={30} height={30} />
|
||||
<AWSProviderBadge width={25} height={25} />
|
||||
<p className="text-sm">Amazon Web Services</p>
|
||||
</div>
|
||||
);
|
||||
@@ -18,7 +19,7 @@ export const CustomProviderInputAWS = () => {
|
||||
export const CustomProviderInputAzure = () => {
|
||||
return (
|
||||
<div className="flex gap-x-2 items-center">
|
||||
<AzureProviderBadge width={30} height={30} />
|
||||
<AzureProviderBadge width={25} height={25} />
|
||||
<p className="text-sm">Azure</p>
|
||||
</div>
|
||||
);
|
||||
@@ -27,8 +28,17 @@ export const CustomProviderInputAzure = () => {
|
||||
export const CustomProviderInputGCP = () => {
|
||||
return (
|
||||
<div className="flex gap-x-2 items-center">
|
||||
<GCPProviderBadge width={30} height={30} />
|
||||
<GCPProviderBadge width={25} height={25} />
|
||||
<p className="text-sm">Google Cloud Platform</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const CustomProviderInputKubernetes = () => {
|
||||
return (
|
||||
<div className="flex gap-x-2 items-center">
|
||||
<KS8ProviderBadge width={25} height={25} />
|
||||
<p className="text-sm">Kubernetes</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
<Select
|
||||
label="Region"
|
||||
placeholder="Select a region"
|
||||
selectionMode="multiple"
|
||||
className="w-full"
|
||||
size="sm"
|
||||
>
|
||||
{regions.map((acc) => (
|
||||
<SelectItem key={acc.key}>{acc.label}</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
import { Input } from "@nextui-org/react";
|
||||
import { XCircle } from "lucide-react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import { useDebounce } from "../../hooks/useDebounce";
|
||||
|
||||
export const CustomSearchInput: React.FC = () => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const [searchQuery, setSearchQuery] = useState(() => {
|
||||
return searchParams.get("filter[search]") || "";
|
||||
});
|
||||
const debouncedSearchQuery = useDebounce(searchQuery, 300);
|
||||
|
||||
const applySearch = useCallback(
|
||||
(query: string) => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
if (query) {
|
||||
params.set("filter[search]", query);
|
||||
} else {
|
||||
params.delete("filter[search]");
|
||||
}
|
||||
router.replace(`?${params.toString()}`, { scroll: false });
|
||||
},
|
||||
[router, searchParams],
|
||||
);
|
||||
|
||||
const clearIconSearch = () => {
|
||||
setSearchQuery("");
|
||||
applySearch("");
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
applySearch(debouncedSearchQuery);
|
||||
}, [debouncedSearchQuery, applySearch]);
|
||||
|
||||
return (
|
||||
<Input
|
||||
variant="bordered"
|
||||
placeholder="Search..."
|
||||
label="Search"
|
||||
labelPlacement="inside"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
applySearch(searchQuery);
|
||||
}
|
||||
}}
|
||||
endContent={
|
||||
searchQuery && (
|
||||
<button onClick={clearIconSearch} className="focus:outline-none">
|
||||
<XCircle className="h-4 w-4 text-default-400" />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
size="sm"
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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: <CustomProviderInputGCP />,
|
||||
value: <CustomProviderInputKubernetes />,
|
||||
},
|
||||
];
|
||||
|
||||
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 (
|
||||
<Select
|
||||
items={dataInputsProvider}
|
||||
@@ -43,6 +68,12 @@ export const CustomSelectProvider = () => {
|
||||
base: "w-full",
|
||||
trigger: "h-12",
|
||||
}}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
setSelectedProvider(value);
|
||||
applyProviderFilter(value);
|
||||
}}
|
||||
selectedKeys={selectedProvider ? [selectedProvider] : []}
|
||||
renderValue={(items) => {
|
||||
return items.map((item) => {
|
||||
return (
|
||||
|
||||
@@ -1,23 +1,74 @@
|
||||
import React from "react";
|
||||
"use client";
|
||||
|
||||
import { Button } from "@nextui-org/react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import React, { useCallback, useEffect, useState } 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<FilterControlsProps> = ({
|
||||
mutedFindings = true,
|
||||
search = false,
|
||||
providers = false,
|
||||
date = false,
|
||||
regions = false,
|
||||
accounts = false,
|
||||
mutedFindings = false,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const [showClearButton, setShowClearButton] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const hasFilters = Array.from(searchParams.keys()).some(
|
||||
(key) => key.startsWith("filter[") || key === "sort",
|
||||
);
|
||||
setShowClearButton(hasFilters);
|
||||
}, [searchParams]);
|
||||
|
||||
const clearAllFilters = useCallback(() => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
Array.from(params.keys()).forEach((key) => {
|
||||
if (key.startsWith("filter[") || key === "sort") {
|
||||
params.delete(key);
|
||||
}
|
||||
});
|
||||
router.push(`?${params.toString()}`, { scroll: false });
|
||||
}, [router, searchParams]);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-x-4 gap-y-4">
|
||||
<CustomSelectProvider />
|
||||
<CustomDatePicker />
|
||||
<CustomAccountSelection />
|
||||
<CustomCheckboxMutedFindings mutedFindings={mutedFindings} />
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-x-4 gap-y-4 items-center">
|
||||
{search && <CustomSearchInput />}
|
||||
{providers && <CustomSelectProvider />}
|
||||
{date && <CustomDatePicker />}
|
||||
{regions && <CustomRegionSelection />}
|
||||
{accounts && <CustomAccountSelection />}
|
||||
{mutedFindings && <CustomCheckboxMutedFindings />}
|
||||
|
||||
{showClearButton && (
|
||||
<Button
|
||||
className="w-full md:w-fit transition-all duration-300 ease-in-out"
|
||||
onClick={clearAllFilters}
|
||||
variant="flat"
|
||||
color="default"
|
||||
size="sm"
|
||||
>
|
||||
Clear all
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,5 +2,6 @@ export * from "../filters/CustomAccountSelection";
|
||||
export * from "../filters/CustomCheckboxMutedFindings";
|
||||
export * from "../filters/CustomDatePicker";
|
||||
export * from "../filters/CustomProviderInputs";
|
||||
export * from "../filters/CustomRegionSelection";
|
||||
export * from "../filters/CustomSelectProvider";
|
||||
export * from "../filters/FilterControls";
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
"use client";
|
||||
|
||||
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 (
|
||||
<Button
|
||||
variant="light"
|
||||
spinner={pending ? "Checking..." : " Check"}
|
||||
type="submit"
|
||||
area-disabled={pending}
|
||||
>
|
||||
<CheckIcon size={20} /> Check connection
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
"use client";
|
||||
|
||||
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 (
|
||||
<Button
|
||||
variant="light"
|
||||
spinner={pending ? "Removing..." : "Remove"}
|
||||
type="submit"
|
||||
area-disabled={pending}
|
||||
>
|
||||
<DeleteIcon size={20} /> Delete
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -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<HTMLFormElement>(null);
|
||||
@@ -35,7 +35,7 @@ export const CheckConnectionProvider = ({ id }: { id: string }) => {
|
||||
return (
|
||||
<form ref={ref} action={clientAction} className="flex gap-x-2">
|
||||
<input type="hidden" name="id" value={id} />
|
||||
<ButtonCheckConnectionProvider />
|
||||
<CustomButtonClientAction buttonLabel="Check Connection" />
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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<HTMLFormElement>(null);
|
||||
@@ -35,7 +35,7 @@ export const DeleteProvider = ({ id }: { id: string }) => {
|
||||
return (
|
||||
<form ref={ref} action={clientAction} className="flex gap-x-2">
|
||||
<input type="hidden" name="id" value={id} />
|
||||
<ButtonDeleteProvider />
|
||||
<CustomButtonClientAction buttonLabel="Delete" danger />
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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<ProviderProps>[] = [
|
||||
{
|
||||
header: "ID",
|
||||
@@ -117,19 +127,55 @@ export const ColumnsProvider: ColumnDef<ProviderProps>[] = [
|
||||
const { id } = getProviderData(row);
|
||||
return (
|
||||
<div className="relative flex justify-end items-center gap-2">
|
||||
<Dropdown className="bg-background border-1 border-default-200">
|
||||
<Dropdown className="shadow-xl" placement="bottom">
|
||||
<DropdownTrigger>
|
||||
<Button isIconOnly radius="full" size="sm" variant="light">
|
||||
<VerticalDotsIcon className="text-default-400" />
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu>
|
||||
<DropdownItem textValue="Check Connection">
|
||||
<CheckConnectionProvider id={id} />
|
||||
</DropdownItem>
|
||||
<DropdownItem textValue="Delete Provider">
|
||||
<DeleteProvider id={id} />
|
||||
</DropdownItem>
|
||||
<DropdownMenu
|
||||
closeOnSelect
|
||||
aria-label="Actions"
|
||||
color="default"
|
||||
variant="flat"
|
||||
>
|
||||
<DropdownSection title="Actions">
|
||||
<DropdownItem
|
||||
key="new"
|
||||
description="Check the connection to the provider"
|
||||
shortcut="⌘N"
|
||||
startContent={<AddNoteBulkIcon className={iconClasses} />}
|
||||
>
|
||||
<CheckConnectionProvider id={id} />
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key="edit"
|
||||
description="Allows you to edit the provider"
|
||||
shortcut="⌘⇧E"
|
||||
startContent={
|
||||
<EditDocumentBulkIcon className={iconClasses} />
|
||||
}
|
||||
>
|
||||
Edit provider
|
||||
</DropdownItem>
|
||||
</DropdownSection>
|
||||
<DropdownSection title="Danger zone">
|
||||
<DropdownItem
|
||||
key="delete"
|
||||
className="text-danger"
|
||||
color="danger"
|
||||
description="Delete the provider permanently"
|
||||
textValue="Delete Provider"
|
||||
shortcut="⌘⇧D"
|
||||
startContent={
|
||||
<DeleteDocumentBulkIcon
|
||||
className={clsx(iconClasses, "!text-danger")}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<DeleteProvider id={id} />
|
||||
</DropdownItem>
|
||||
</DropdownSection>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
@@ -27,7 +27,8 @@ export const DataTableColumnHeader = <TData, TValue>({
|
||||
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 = <TData, TValue>({
|
||||
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 (
|
||||
|
||||
@@ -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<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
@@ -35,6 +38,7 @@ export function DataTableProvider<TData, TValue>({
|
||||
metadata,
|
||||
}: DataTableProviderProps<TData, TValue>) {
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
@@ -43,12 +47,27 @@ export function DataTableProvider<TData, TValue>({
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
onSortingChange: setSorting,
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
state: {
|
||||
sorting,
|
||||
columnFilters,
|
||||
},
|
||||
});
|
||||
|
||||
// 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: ["false", "true"] },
|
||||
// Add more filter categories as needed
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<FilterColumnTable filters={filters} />
|
||||
<div className="rounded-md border w-full">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Button } from "@nextui-org/react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useCallback, useMemo } from "react";
|
||||
|
||||
interface FilterColumnTableProps {
|
||||
filters: { key: string; values: string[] }[];
|
||||
}
|
||||
|
||||
export function FilterColumnTable({ filters }: FilterColumnTableProps) {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const activeFilters = useMemo(() => {
|
||||
const currentFilters: Record<string, string> = {};
|
||||
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)) {
|
||||
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) {
|
||||
params.delete(filterKey);
|
||||
} else {
|
||||
params.set(filterKey, value);
|
||||
}
|
||||
|
||||
router.push(`?${params.toString()}`);
|
||||
},
|
||||
[router, searchParams],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center space-x-2">
|
||||
{filters.flatMap(({ key, values }) =>
|
||||
values.map((value) => (
|
||||
<Button
|
||||
key={`${key}-${value}`}
|
||||
onClick={() => applyFilter(key, value)}
|
||||
variant={activeFilters[key] === value ? "faded" : "light"}
|
||||
size="sm"
|
||||
>
|
||||
{value || "All"}
|
||||
</Button>
|
||||
)),
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<Button
|
||||
className={clsx("bg-transparent border-none p-0 m-0 h-fit min-w-min", {
|
||||
"text-danger": danger,
|
||||
})}
|
||||
spinner={<CircularProgress aria-label="Loading..." size="sm" />}
|
||||
type="submit"
|
||||
area-disabled={pending}
|
||||
>
|
||||
{buttonLabel}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from "./CustomBox";
|
||||
export * from "./CustomButtonClientAction";
|
||||
export * from "./CustomInput";
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from "./useDebounce";
|
||||
export * from "./useLocalStorage";
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function useDebounce<T>(value: T, delay?: number): T {
|
||||
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setDebouncedValue(value), delay ?? 900);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [value, delay]);
|
||||
|
||||
return debouncedValue;
|
||||
}
|
||||
@@ -11,7 +11,7 @@ interface SidebarStoreState {
|
||||
export const useUIStore = create<SidebarStoreState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
isSideMenuOpen: true,
|
||||
isSideMenuOpen: false,
|
||||
openSideMenu: () => set({ isSideMenuOpen: true }),
|
||||
closeSideMenu: () => set({ isSideMenuOpen: false }),
|
||||
}),
|
||||
|
||||
+2
-5
@@ -9,12 +9,9 @@ export type IconProps = {
|
||||
style?: React.CSSProperties;
|
||||
};
|
||||
|
||||
export interface searchParamsProps {
|
||||
searchParams: {
|
||||
page?: string;
|
||||
};
|
||||
export interface SearchParamsProps {
|
||||
[key: string]: string | string[] | undefined;
|
||||
}
|
||||
|
||||
export interface ProviderProps {
|
||||
id: string;
|
||||
type: "providers";
|
||||
|
||||
Reference in New Issue
Block a user