Merge pull request #52 from prowler-cloud/PRWLR-4626-Implement-newTable-Providers

Implement filters, search with the API
This commit is contained in:
Pablo Lara
2024-09-06 16:47:00 +02:00
committed by GitHub
29 changed files with 536 additions and 144 deletions
+11 -4
View File
@@ -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}`,
},
+15 -5
View File
@@ -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]);
+1 -1
View File
@@ -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
+18 -22
View File
@@ -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}
/>
);
};
+14 -5
View File
@@ -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]);
+15 -5
View File
@@ -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>
);
};
+45 -11
View File
@@ -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>
);
};
+13 -3
View File
@@ -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>
);
};
+61
View File
@@ -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"
/>
);
};
+32 -1
View File
@@ -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 (
+58 -7
View File
@@ -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>
);
};
+1
View File
@@ -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>
);
};
+2 -2
View File
@@ -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>
);
};
-2
View File
@@ -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";
+55 -9
View File
@@ -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
View File
@@ -1,2 +1,3 @@
export * from "./CustomBox";
export * from "./CustomButtonClientAction";
export * from "./CustomInput";
+1
View File
@@ -1 +1,2 @@
export * from "./useDebounce";
export * from "./useLocalStorage";
+15
View File
@@ -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;
}
+1 -1
View File
@@ -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
View File
@@ -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";